Skip to main content

sharepoint_cli/
cli.rs

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