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, short = 'y', global = true)]
42 pub yes: bool,
43
44 #[arg(long, global = true)]
46 pub no_color: bool,
47
48 #[arg(long, global = true, env = ENV_PROFILE)]
50 pub profile: Option<String>,
51
52 #[arg(long, global = true, env = ENV_TENANT)]
54 pub tenant: Option<String>,
55
56 #[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 Init(InitArgs),
68 #[command(subcommand)]
70 Auth(AuthCmd),
71 #[command(subcommand)]
73 Profile(ProfileCmd),
74 #[command(subcommand)]
76 Config(ConfigCmd),
77 #[command(subcommand)]
79 Sites(SitesCmd),
80 #[command(subcommand)]
82 Drives(DrivesCmd),
83 #[command(subcommand)]
85 Files(FilesCmd),
86 Doctor {
88 #[arg(long)]
90 offline: bool,
91 },
92 Completions { shell: Shell },
94 Schema {
96 #[arg(long)]
98 command: Option<String>,
99 },
100}
101
102#[derive(Debug, Subcommand)]
103pub enum AuthCmd {
104 Login,
106 Logout,
108 Status {
110 #[arg(long)]
112 offline: bool,
113 #[arg(long, default_value_t = 50)]
115 limit: usize,
116 #[arg(long)]
118 page: Option<String>,
119 #[arg(long, value_delimiter = ',')]
121 fields: Vec<String>,
122 },
123}
124
125#[derive(Debug, Subcommand)]
126pub enum ProfileCmd {
127 List,
129 Use { name: String },
131 Remove { name: String },
133}
134
135#[derive(Debug, Args)]
136pub struct InitArgs {
137 #[arg(long, env = config::ENV_DEFAULT_SITE)]
139 pub default_site: Option<String>,
140
141 #[arg(long)]
143 pub no_login: bool,
144
145 #[arg(long, env = config::ENV_READ_ONLY)]
147 pub read_only: bool,
148}
149
150#[derive(Debug, Subcommand)]
151pub enum ConfigCmd {
152 Show,
154 Path,
156}
157
158#[derive(Debug, Subcommand)]
159pub enum SitesCmd {
160 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 #[arg(long, value_delimiter = ',')]
172 fields: Vec<String>,
173 },
174 Use {
176 site: String,
178 },
179}
180
181#[derive(Debug, Subcommand)]
182pub enum DrivesCmd {
183 List {
185 site: String,
186 #[arg(long, default_value_t = 50)]
187 limit: usize,
188 #[arg(long)]
189 all: bool,
190 #[arg(long, value_delimiter = ',')]
192 fields: Vec<String>,
193 },
194}
195
196#[derive(Debug, Subcommand)]
197pub enum FilesCmd {
198 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 #[arg(long, value_delimiter = ',')]
212 fields: Vec<String>,
213 },
214 Stat {
216 #[arg(value_name = "REF")]
217 reference: String,
218 },
219 Download {
221 #[arg(value_name = "REF")]
222 reference: String,
223 #[arg(long, short = 'p')]
225 path: Option<String>,
226 #[arg(long)]
227 overwrite: bool,
228 },
229 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 #[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 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}