1use 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 #[arg(
24 long,
25 short = 'o',
26 global = true,
27 default_value = "auto",
28 value_name = "FORMAT"
29 )]
30 pub output: OutputFormat,
31
32 #[arg(long, global = true, hide = true)]
34 pub json: bool,
35
36 #[arg(long, global = true)]
38 pub quiet: bool,
39
40 #[arg(long, global = true)]
42 pub no_color: bool,
43
44 #[arg(long, global = true, env = ENV_PROFILE)]
46 pub profile: Option<String>,
47
48 #[arg(long, global = true, env = ENV_TENANT)]
50 pub tenant: Option<String>,
51
52 #[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 Init(InitArgs),
64 #[command(subcommand)]
66 Auth(AuthCmd),
67 #[command(subcommand)]
69 Config(ConfigCmd),
70 #[command(subcommand)]
72 Sites(SitesCmd),
73 #[command(subcommand)]
75 Drives(DrivesCmd),
76 #[command(subcommand)]
78 Files(FilesCmd),
79 Doctor {
81 #[arg(long)]
83 offline: bool,
84 },
85 Completions { shell: Shell },
87 Schema {
89 #[arg(long)]
91 command: Option<String>,
92 },
93}
94
95#[derive(Debug, Subcommand)]
96pub enum AuthCmd {
97 Login,
99 Logout,
101 Status {
103 #[arg(long, default_value_t = 50)]
105 limit: usize,
106 #[arg(long)]
108 page: Option<String>,
109 #[arg(long, value_delimiter = ',')]
111 fields: Vec<String>,
112 },
113}
114
115#[derive(Debug, Args)]
116pub struct InitArgs {
117 #[arg(long, env = config::ENV_DEFAULT_SITE)]
119 pub default_site: Option<String>,
120
121 #[arg(long)]
123 pub no_login: bool,
124
125 #[arg(long, env = config::ENV_READ_ONLY)]
127 pub read_only: bool,
128}
129
130#[derive(Debug, Subcommand)]
131pub enum ConfigCmd {
132 Show,
134 Path,
136}
137
138#[derive(Debug, Subcommand)]
139pub enum SitesCmd {
140 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 #[arg(long, value_delimiter = ',')]
152 fields: Vec<String>,
153 },
154 Use {
156 site: String,
158 },
159}
160
161#[derive(Debug, Subcommand)]
162pub enum DrivesCmd {
163 List {
165 site: String,
166 #[arg(long, default_value_t = 50)]
167 limit: usize,
168 #[arg(long)]
169 all: bool,
170 #[arg(long, value_delimiter = ',')]
172 fields: Vec<String>,
173 },
174}
175
176#[derive(Debug, Subcommand)]
177pub enum FilesCmd {
178 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 #[arg(long, value_delimiter = ',')]
192 fields: Vec<String>,
193 },
194 Stat {
196 #[arg(value_name = "REF")]
197 reference: String,
198 },
199 Download {
201 #[arg(value_name = "REF")]
202 reference: String,
203 #[arg(long, short = 'p')]
205 path: Option<String>,
206 #[arg(long)]
207 overwrite: bool,
208 },
209 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 #[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 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}