Skip to main content

sharepoint_cli/
cli.rs

1//! CLI entry point: clap derive structs and the `run` dispatcher.
2
3use clap::{Parser, Subcommand};
4
5use crate::config::{self, ConfigFile, ENV_CLIENT_ID, ENV_PROFILE, ENV_TENANT, ResolvedConfig};
6use crate::error::Result;
7use crate::output::{OutputConfig, OutputFormat};
8
9#[derive(Debug, Parser)]
10#[command(
11    name = "sharepoint",
12    about = "Agent-friendly SharePoint Online CLI",
13    after_help = "Run `sharepoint schema` for a machine-readable description of all commands.",
14    version,
15    propagate_version = true,
16    disable_help_subcommand = true
17)]
18pub struct Cli {
19    /// Output format: auto (JSON when piped), text, or json.
20    #[arg(
21        long,
22        short = 'o',
23        global = true,
24        default_value = "auto",
25        value_name = "FORMAT"
26    )]
27    pub output: OutputFormat,
28
29    /// Suppress informational messages on stderr.
30    #[arg(long, global = true)]
31    pub quiet: bool,
32
33    /// Active config profile (default: "default"). Env: SHAREPOINT_PROFILE.
34    #[arg(long, global = true, env = ENV_PROFILE)]
35    pub profile: Option<String>,
36
37    /// Tenant override. Env: SHAREPOINT_TENANT_ID.
38    #[arg(long, global = true, env = ENV_TENANT)]
39    pub tenant: Option<String>,
40
41    /// Client ID override. Env: SHAREPOINT_CLIENT_ID.
42    #[arg(long, global = true, env = ENV_CLIENT_ID)]
43    pub client_id: Option<String>,
44
45    #[command(subcommand)]
46    pub command: Command,
47}
48
49#[derive(Debug, Subcommand)]
50pub enum Command {
51    /// Interactive setup + first device-code login.
52    Init,
53    /// Sub-commands: login, logout, status.
54    #[command(subcommand)]
55    Auth(AuthCmd),
56    /// Sub-commands: show, path.
57    #[command(subcommand)]
58    Config(ConfigCmd),
59    /// Sub-commands: list, use.
60    #[command(subcommand)]
61    Sites(SitesCmd),
62    /// Sub-commands: list.
63    #[command(subcommand)]
64    Drives(DrivesCmd),
65    /// Sub-commands: ls, stat, download, find.
66    #[command(subcommand)]
67    Files(FilesCmd),
68    /// Emit a machine-readable description of all commands and their output shapes.
69    Schema,
70}
71
72#[derive(Debug, Subcommand)]
73pub enum AuthCmd {
74    /// Run the device-code flow and cache the resulting tokens.
75    Login,
76    /// Delete cached tokens for the active profile's tenant/client.
77    Logout,
78    /// Show cached account info, expiry, scopes.
79    Status {
80        /// Maximum number of accounts to show.
81        #[arg(long, default_value_t = 50)]
82        limit: usize,
83        /// Opaque pagination cursor from a previous response's `next` field.
84        #[arg(long)]
85        page: Option<String>,
86        /// Comma-separated fields to include (e.g. username,expires_at).
87        #[arg(long, value_delimiter = ',')]
88        fields: Vec<String>,
89    },
90}
91
92#[derive(Debug, Subcommand)]
93pub enum ConfigCmd {
94    /// Print the resolved config (token & secrets masked).
95    Show,
96    /// Print the absolute path to the config file.
97    Path,
98}
99
100#[derive(Debug, Subcommand)]
101pub enum SitesCmd {
102    /// List sites. Without --query: followed sites; with --query: search.
103    List {
104        #[arg(long)]
105        query: Option<String>,
106        #[arg(long, default_value_t = 50)]
107        limit: usize,
108        #[arg(long)]
109        all: bool,
110        #[arg(long)]
111        page: Option<String>,
112        /// Comma-separated output fields to include (e.g. id,name,url).
113        #[arg(long, value_delimiter = ',')]
114        fields: Vec<String>,
115    },
116    /// Set `default_site` in the active profile.
117    Use {
118        /// Site name or URL.
119        site: String,
120    },
121}
122
123#[derive(Debug, Subcommand)]
124pub enum DrivesCmd {
125    /// List drives (libraries) for a site reference.
126    List {
127        site: String,
128        #[arg(long, default_value_t = 50)]
129        limit: usize,
130        #[arg(long)]
131        all: bool,
132        /// Comma-separated output fields to include (e.g. id,name,drive_type).
133        #[arg(long, value_delimiter = ',')]
134        fields: Vec<String>,
135    },
136}
137
138#[derive(Debug, Subcommand)]
139pub enum FilesCmd {
140    /// List items at a reference (folder).
141    Ls {
142        #[arg(value_name = "REF")]
143        reference: String,
144        #[arg(short = 'r', long)]
145        recursive: bool,
146        #[arg(long)]
147        limit: Option<usize>,
148        #[arg(long)]
149        all: bool,
150        #[arg(long)]
151        page: Option<String>,
152        /// Comma-separated output fields to include (e.g. name,size,kind).
153        #[arg(long, value_delimiter = ',')]
154        fields: Vec<String>,
155    },
156    /// Show metadata for a single item.
157    Stat {
158        #[arg(value_name = "REF")]
159        reference: String,
160    },
161    /// Download a file. PATH or `-` for stdout.
162    Download {
163        #[arg(value_name = "REF")]
164        reference: String,
165        /// Destination path (or `-` for stdout). Use `--output`/`-o` as aliases (rewritten in argv before clap).
166        #[arg(long, short = 'p')]
167        path: Option<String>,
168        #[arg(long)]
169        overwrite: bool,
170    },
171    /// Search inside a drive (by query and/or shell glob).
172    Find {
173        #[arg(value_name = "REF")]
174        reference: String,
175        #[arg(long)]
176        query: Option<String>,
177        #[arg(long)]
178        name: Option<String>,
179        #[arg(long, default_value_t = 200)]
180        limit: usize,
181        #[arg(long)]
182        all: bool,
183        #[arg(long)]
184        page: Option<String>,
185        /// Comma-separated output fields to include (e.g. name,size,kind).
186        #[arg(long, value_delimiter = ',')]
187        fields: Vec<String>,
188    },
189}
190
191pub struct Runtime {
192    pub out: OutputConfig,
193    pub cfg: ResolvedConfig,
194    pub config_file: ConfigFile,
195    pub config_path: std::path::PathBuf,
196    pub cache_path: std::path::PathBuf,
197}
198
199impl Runtime {
200    pub fn build(cli: &Cli) -> Result<Self> {
201        let config_path = config::config_path()?;
202        let config_file = config::load_file(&config_path)?;
203        let env_lookup =
204            |k: &str| -> Option<String> { std::env::var(k).ok().filter(|s| !s.is_empty()) };
205        let mut cfg = config::resolve(&config_file, cli.profile.as_deref(), &env_lookup)?;
206        if let Some(t) = &cli.tenant {
207            cfg.tenant_id = Some(t.clone());
208        }
209        if let Some(c) = &cli.client_id {
210            cfg.client_id = Some(c.clone());
211        }
212        let cache_path = config::token_cache_path()?;
213        Ok(Self {
214            out: OutputConfig::new(cli.output, cli.quiet),
215            cfg,
216            config_file,
217            config_path,
218            cache_path,
219        })
220    }
221}
222
223pub async fn run(cli: Cli) -> Result<()> {
224    // Schema runs before any config/auth is needed.
225    if matches!(cli.command, Command::Schema) {
226        return crate::commands::schema::run();
227    }
228    let rt = Runtime::build(&cli)?;
229    match cli.command {
230        Command::Schema => unreachable!(),
231        Command::Init => crate::commands::init::run(&rt).await,
232        Command::Auth(sub) => crate::commands::auth::run(&rt, sub).await,
233        Command::Config(sub) => crate::commands::config::run(&rt, sub).await,
234        Command::Sites(sub) => crate::commands::sites::run(&rt, sub).await,
235        Command::Drives(sub) => crate::commands::drives::run(&rt, sub).await,
236        Command::Files(sub) => crate::commands::files::run(&rt, sub).await,
237    }
238}