Skip to main content

systemprompt_cli/runner/args/
mod.rs

1//! Top-level clap argument definitions and the command tree.
2//!
3//! Defines the global option groups, the [`Cli`] parser, and the [`Commands`]
4//! subcommand tree, along with the mapping from each command to its bootstrap
5//! [`CommandDescriptor`] and the argument-reconstruction used when forwarding
6//! to a remote tenant.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use clap::{Parser, Subcommand};
12
13use crate::commands::{admin, analytics, build, cloud, core, infrastructure, plugins, web};
14use crate::descriptor::{CommandDescriptor, DescribeCommand};
15
16#[derive(Debug, Clone, Copy, clap::Args)]
17pub struct VerbosityOpts {
18    #[arg(
19        long,
20        short = 'v',
21        global = true,
22        hide = true,
23        help = "Increase verbosity"
24    )]
25    pub verbose: bool,
26
27    #[arg(
28        long,
29        short = 'q',
30        global = true,
31        hide = true,
32        conflicts_with = "verbose",
33        help = "Suppress output"
34    )]
35    pub quiet: bool,
36
37    #[arg(long, global = true, hide = true, help = "Debug logging")]
38    pub debug: bool,
39}
40
41#[derive(Debug, Clone, Copy, clap::Args)]
42pub struct OutputOpts {
43    #[arg(long, global = true, hide = true, help = "JSON output")]
44    pub json: bool,
45
46    #[arg(
47        long,
48        global = true,
49        hide = true,
50        conflicts_with = "json",
51        help = "YAML output"
52    )]
53    pub yaml: bool,
54}
55
56#[derive(Debug, Clone, Copy, clap::Args)]
57pub struct DisplayOpts {
58    #[arg(long, global = true, hide = true, help = "Disable colors")]
59    pub no_color: bool,
60
61    #[arg(long, global = true, hide = true, help = "Non-interactive mode")]
62    pub non_interactive: bool,
63}
64
65#[derive(Debug, clap::Args)]
66pub struct DatabaseOpts {
67    #[arg(long, global = true, help = "Direct database URL (bypasses profile)")]
68    pub database_url: Option<String>,
69}
70
71#[derive(Debug, clap::Args)]
72pub struct ProfileOpts {
73    #[arg(
74        long,
75        global = true,
76        help = "Profile name to use (overrides active session)"
77    )]
78    pub profile: Option<String>,
79}
80
81#[derive(Debug, Parser)]
82#[command(name = "systemprompt")]
83#[command(about = "Agent orchestration and AI operations.")]
84#[command(version = env!("CARGO_PKG_VERSION"))]
85#[command(
86    before_help = "\x1b[38;5;208m</\x1b[1;37mSYSTEMPROMPT\x1b[38;5;208m.\x1b[0;37mio\x1b[38;5;\
87                   208m>\x1b[0m"
88)]
89#[command(after_help = "\
90GETTING STARTED:
91  systemprompt core skills list                 List all skills
92
93GLOBAL OPTIONS (apply to all commands):
94  -v, --verbose         Increase verbosity
95  -q, --quiet           Suppress output
96      --debug           Debug logging
97      --json            JSON output
98      --yaml            YAML output
99      --no-color        Disable colors
100      --non-interactive Non-interactive mode
101      --database-url    Direct database URL (bypasses profile)
102      --profile         Profile name to use (overrides active session)")]
103pub struct Cli {
104    #[command(flatten)]
105    pub verbosity: VerbosityOpts,
106
107    #[command(flatten)]
108    pub output: OutputOpts,
109
110    #[command(flatten)]
111    pub display: DisplayOpts,
112
113    #[command(flatten)]
114    pub database: DatabaseOpts,
115
116    #[command(flatten)]
117    pub profile_opts: ProfileOpts,
118
119    #[command(subcommand)]
120    pub command: Option<Commands>,
121}
122
123#[derive(Debug, Subcommand)]
124pub enum Commands {
125    #[command(
126        subcommand,
127        about = "Core operations: skills, content, files, contexts"
128    )]
129    Core(core::CoreCommands),
130
131    #[command(
132        subcommand,
133        about = "Infrastructure management (services, db, jobs, logs, system)"
134    )]
135    Infra(infrastructure::InfraCommands),
136
137    #[command(
138        subcommand,
139        about = "Administration (users, agents, config, setup, session)"
140    )]
141    Admin(admin::AdminCommands),
142
143    #[command(subcommand, about = "Cloud deployment, backup, and setup")]
144    Cloud(cloud::CloudCommands),
145
146    #[command(subcommand, about = "Analytics and metrics reporting")]
147    Analytics(analytics::AnalyticsCommands),
148
149    #[command(subcommand, about = "Web service configuration management")]
150    Web(web::WebCommands),
151
152    #[command(subcommand, about = "Plugins, extensions, and MCP server management")]
153    Plugins(plugins::PluginsCommands),
154
155    #[command(subcommand, about = "Build MCP extensions")]
156    Build(build::BuildCommands),
157}
158
159impl DescribeCommand for Commands {
160    fn descriptor(&self) -> CommandDescriptor {
161        self.bootstrap_descriptor()
162            .with_data_impact(self.data_impact())
163    }
164}
165
166impl Commands {
167    #[must_use]
168    pub const fn serves_api(&self) -> bool {
169        match self {
170            Self::Infra(infrastructure::InfraCommands::Services(cmd)) => cmd.serves_api(),
171            _ => false,
172        }
173    }
174
175    fn bootstrap_descriptor(&self) -> CommandDescriptor {
176        match self {
177            Self::Cloud(cmd) => cmd.descriptor(),
178            Self::Plugins(cmd) => cmd.descriptor(),
179            Self::Admin(admin::AdminCommands::Session(cmd)) => cmd.descriptor(),
180            Self::Admin(admin::AdminCommands::Config(admin::config::ConfigCommands::Secret(_)))
181            | Self::Build(_) => CommandDescriptor::PROFILE_ONLY,
182            Self::Admin(admin::AdminCommands::Setup(_))
183            | Self::Core(
184                core::CoreCommands::Marketplace(core::marketplace::MarketplaceCommands::Import(_))
185                | core::CoreCommands::Services(
186                    core::services::ServicesCommands::Validate(_)
187                    | core::services::ServicesCommands::Bundle(_)
188                    | core::services::ServicesCommands::Keygen(_),
189                ),
190            ) => CommandDescriptor::NONE,
191            Self::Core(core::CoreCommands::Services(
192                core::services::ServicesCommands::Publish(args),
193            )) => {
194                if args.auth_secret.is_some() {
195                    CommandDescriptor::PROFILE_AND_SECRETS
196                } else {
197                    CommandDescriptor::NONE
198                }
199            },
200            Self::Core(core::CoreCommands::Services(
201                core::services::ServicesCommands::Inspect(args),
202            )) => {
203                if args.active {
204                    CommandDescriptor::PROFILE_SECRETS_AND_PATHS
205                } else {
206                    CommandDescriptor::NONE
207                }
208            },
209            Self::Admin(admin::AdminCommands::Config(_))
210            | Self::Web(_)
211            | Self::Core(
212                core::CoreCommands::Hooks(_)
213                | core::CoreCommands::Plugins(_)
214                | core::CoreCommands::Skills(
215                    core::skills::SkillsCommands::List(_) | core::skills::SkillsCommands::Show(_),
216                ),
217            ) => CommandDescriptor::PROFILE_SECRETS_AND_PATHS,
218            Self::Infra(cmd) => infra_descriptor(cmd),
219            Self::Analytics(_) => CommandDescriptor::FULL
220                .with_skip_validation()
221                .with_read_only(),
222            _ => CommandDescriptor::FULL,
223        }
224    }
225}
226
227// Why: the registry lives in a OnceLock, so whoever installs it first decides
228// whether discovered models are served. The server must be that installer,
229// with discovery; a plain `try_init` here made `try_init_with_discovery` a
230// silent no-op on every boot.
231const fn infra_descriptor(cmd: &infrastructure::InfraCommands) -> CommandDescriptor {
232    use infrastructure::InfraCommands;
233    use infrastructure::jobs::JobsCommands;
234    use infrastructure::logs::LogsCommands;
235    use infrastructure::services::ServicesCommands;
236
237    match cmd {
238        InfraCommands::Services(
239            ServicesCommands::Serve { .. } | ServicesCommands::Start { .. },
240        ) => CommandDescriptor::PROFILE_SECRETS_AND_PATHS.with_model_discovery(),
241        InfraCommands::Services(_) => CommandDescriptor::PROFILE_SECRETS_AND_PATHS,
242        InfraCommands::Jobs(JobsCommands::Run(_) | JobsCommands::List) => {
243            CommandDescriptor::FULL.with_skip_validation()
244        },
245        InfraCommands::Logs(LogsCommands::Delete(_) | LogsCommands::Cleanup(_)) => {
246            CommandDescriptor::FULL
247        },
248        InfraCommands::Logs(_) => CommandDescriptor::FULL.with_read_only(),
249        _ => CommandDescriptor::FULL,
250    }
251}
252
253mod assemble;
254mod impact;
255
256pub use assemble::{
257    build_cli_config, has_local_export_flag, has_local_export_flag_in, reconstruct_args,
258    reconstruct_args_from,
259};