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