greentic_dev/
cli.rs

1use std::path::PathBuf;
2
3use clap::{Args, Parser, Subcommand, ValueEnum};
4
5#[derive(Parser, Debug)]
6#[command(name = "greentic-dev")]
7#[command(version)]
8#[command(about = "Greentic developer tooling CLI")]
9pub struct Cli {
10    #[command(subcommand)]
11    pub command: Command,
12}
13
14#[derive(Subcommand, Debug)]
15pub enum Command {
16    /// Flow tooling (validate, lint, bundle inspection)
17    #[command(subcommand)]
18    Flow(FlowCommand),
19    /// Pack tooling (build deterministic packs, run locally)
20    #[command(subcommand)]
21    Pack(PackCommand),
22    /// Component tooling (delegates to `greentic-component` or uses built-ins)
23    #[command(subcommand)]
24    Component(ComponentCommand),
25    /// Manage greentic-dev configuration
26    #[command(subcommand)]
27    Config(ConfigCommand),
28    /// MCP tooling (feature = "mcp")
29    #[cfg(feature = "mcp")]
30    #[command(subcommand)]
31    Mcp(McpCommand),
32}
33
34#[derive(Subcommand, Debug)]
35pub enum FlowCommand {
36    /// Validate a flow YAML file and emit the canonical bundle JSON
37    Validate(FlowValidateArgs),
38    /// Add a configured component step to a flow via config-flow
39    AddStep(FlowAddStepArgs),
40}
41
42#[derive(Args, Debug)]
43pub struct FlowValidateArgs {
44    /// Path to the flow definition (YAML)
45    #[arg(short = 'f', long = "file")]
46    pub file: PathBuf,
47    /// Emit compact JSON instead of pretty-printing
48    #[arg(long = "json")]
49    pub json: bool,
50}
51
52#[derive(Args, Debug)]
53pub struct FlowAddStepArgs {
54    /// Flow identifier (maps to flows/<id>.ygtc)
55    pub flow_id: String,
56    /// Component coordinate (store://... or repo://...). If omitted, greentic-dev will prompt.
57    #[arg(long = "coordinate")]
58    pub coordinate: Option<String>,
59    /// Distributor profile to use (overrides GREENTIC_DISTRIBUTOR_PROFILE/env config)
60    #[arg(long = "profile")]
61    pub profile: Option<String>,
62    /// Config flow selection
63    #[arg(long = "mode", value_enum)]
64    pub mode: Option<ConfigFlowModeArg>,
65    /// Automatically append routing from an existing node (if provided)
66    #[arg(long = "after")]
67    pub after: Option<String>,
68}
69
70#[derive(Subcommand, Debug)]
71pub enum PackCommand {
72    /// Build a deterministic .gtpack from a validated flow bundle
73    Build(PackBuildArgs),
74    /// Execute a pack locally with mocks/telemetry support
75    Run(PackRunArgs),
76    /// Verify a built pack archive (.gtpack)
77    Verify(PackVerifyArgs),
78    /// Initialize a pack workspace from a remote coordinate
79    Init(PackInitArgs),
80    /// Scaffold a pack workspace via the `packc` CLI
81    New(PackNewArgs),
82}
83
84#[derive(Args, Debug)]
85pub struct PackBuildArgs {
86    /// Path to the flow definition (YAML)
87    #[arg(short = 'f', long = "file")]
88    pub file: PathBuf,
89    /// Output path for the generated pack
90    #[arg(short = 'o', long = "out")]
91    pub out: PathBuf,
92    /// Signing mode for the generated pack
93    #[arg(long = "sign", default_value = "dev", value_enum)]
94    pub sign: PackSignArg,
95    /// Optional path to pack metadata (pack.toml)
96    #[arg(long = "meta")]
97    pub meta: Option<PathBuf>,
98    /// Directory containing local component builds
99    #[arg(long = "component-dir", value_name = "DIR")]
100    pub component_dir: Option<PathBuf>,
101}
102
103#[derive(Args, Debug)]
104pub struct PackRunArgs {
105    /// Path to the pack (.gtpack) to execute
106    #[arg(short = 'p', long = "pack")]
107    pub pack: PathBuf,
108    /// Flow entry identifier override
109    #[arg(long = "entry")]
110    pub entry: Option<String>,
111    /// JSON payload to use as run input
112    #[arg(long = "input")]
113    pub input: Option<String>,
114    /// Enforcement policy for pack signatures
115    #[arg(long = "policy", default_value = "devok", value_enum)]
116    pub policy: RunPolicyArg,
117    /// OTLP collector endpoint (optional)
118    #[arg(long = "otlp")]
119    pub otlp: Option<String>,
120    /// Comma-separated list of allowed outbound hosts
121    #[arg(long = "allow")]
122    pub allow: Option<String>,
123    /// Mocks toggle
124    #[arg(long = "mocks", default_value = "on", value_enum)]
125    pub mocks: MockSettingArg,
126    /// Directory to persist run artifacts (transcripts, logs)
127    #[arg(long = "artifacts")]
128    pub artifacts: Option<PathBuf>,
129}
130
131#[derive(Args, Debug)]
132pub struct PackVerifyArgs {
133    /// Path to the pack (.gtpack) to verify
134    #[arg(short = 'p', long = "pack")]
135    pub pack: PathBuf,
136    /// Verification policy for signatures
137    #[arg(long = "policy", default_value = "devok", value_enum)]
138    pub policy: VerifyPolicyArg,
139    /// Emit the manifest JSON on success
140    #[arg(long = "json")]
141    pub json: bool,
142}
143
144#[derive(Args, Debug)]
145pub struct PackInitArgs {
146    /// Remote pack coordinate (e.g. pack://org/name@1.0.0)
147    pub from: String,
148    /// Distributor profile to use (overrides GREENTIC_DISTRIBUTOR_PROFILE/env config)
149    #[arg(long = "profile")]
150    pub profile: Option<String>,
151}
152
153#[derive(Args, Debug, Clone, Default)]
154#[command(disable_help_flag = true)]
155pub struct PackNewArgs {
156    /// Arguments passed directly to the `packc new` command
157    #[arg(
158        value_name = "ARGS",
159        trailing_var_arg = true,
160        allow_hyphen_values = true
161    )]
162    pub passthrough: Vec<String>,
163}
164
165#[derive(Subcommand, Debug, Clone)]
166pub enum ComponentCommand {
167    /// Add a remote component to the current workspace via the distributor
168    Add(ComponentAddArgs),
169    /// Delegate to the `greentic-component` CLI (default passthrough)
170    #[command(external_subcommand)]
171    Passthrough(Vec<String>),
172}
173
174#[derive(Args, Debug, Clone)]
175pub struct ComponentAddArgs {
176    /// Remote component coordinate (e.g. component://org/name@^1.0)
177    pub coordinate: String,
178    /// Distributor profile to use (overrides GREENTIC_DISTRIBUTOR_PROFILE/env config)
179    #[arg(long = "profile")]
180    pub profile: Option<String>,
181    /// Resolution intent (dev or runtime)
182    #[arg(long = "intent", default_value = "dev", value_enum)]
183    pub intent: DevIntentArg,
184}
185
186#[cfg(feature = "mcp")]
187#[derive(Subcommand, Debug)]
188pub enum McpCommand {
189    /// Inspect MCP provider metadata
190    Doctor(McpDoctorArgs),
191}
192
193#[cfg(feature = "mcp")]
194#[derive(Args, Debug)]
195pub struct McpDoctorArgs {
196    /// MCP provider identifier or config path
197    pub provider: String,
198    /// Emit compact JSON instead of pretty output
199    #[arg(long = "json")]
200    pub json: bool,
201}
202
203#[derive(Subcommand, Debug)]
204pub enum ConfigCommand {
205    /// Set a key in ~/.greentic/config.toml (e.g. defaults.component.org)
206    Set(ConfigSetArgs),
207}
208
209#[derive(Args, Debug)]
210pub struct ConfigSetArgs {
211    /// Config key path (e.g. defaults.component.org)
212    pub key: String,
213    /// Value to assign to the key (stored as a string)
214    pub value: String,
215    /// Override config file path (default: ~/.greentic/config.toml)
216    #[arg(long = "file")]
217    pub file: Option<PathBuf>,
218}
219
220#[derive(Copy, Clone, Debug, ValueEnum)]
221pub enum PackSignArg {
222    Dev,
223    None,
224}
225
226#[derive(Copy, Clone, Debug, ValueEnum)]
227pub enum RunPolicyArg {
228    Strict,
229    Devok,
230}
231
232#[derive(Copy, Clone, Debug, ValueEnum)]
233pub enum VerifyPolicyArg {
234    Strict,
235    Devok,
236}
237
238#[derive(Copy, Clone, Debug, ValueEnum)]
239pub enum MockSettingArg {
240    On,
241    Off,
242}
243
244#[derive(Copy, Clone, Debug, ValueEnum)]
245pub enum ConfigFlowModeArg {
246    Default,
247    Custom,
248}
249#[derive(Copy, Clone, Debug, ValueEnum)]
250pub enum DevIntentArg {
251    Dev,
252    Runtime,
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use clap::Parser;
259
260    #[test]
261    fn parses_component_passthrough_args() {
262        let cli = Cli::parse_from([
263            "greentic-dev",
264            "component",
265            "new",
266            "--name",
267            "demo",
268            "--json",
269        ]);
270        let Command::Component(ComponentCommand::Passthrough(args)) = cli.command else {
271            panic!("expected component passthrough variant");
272        };
273        assert_eq!(
274            args,
275            vec![
276                "new".to_string(),
277                "--name".into(),
278                "demo".into(),
279                "--json".into()
280            ]
281        );
282    }
283
284    #[test]
285    fn parses_pack_new_args() {
286        let cli = Cli::parse_from(["greentic-dev", "pack", "new", "--name", "demo-pack"]);
287        let Command::Pack(PackCommand::New(args)) = cli.command else {
288            panic!("expected pack new variant");
289        };
290        assert_eq!(
291            args.passthrough,
292            vec!["--name".to_string(), "demo-pack".to_string()]
293        );
294    }
295
296    #[test]
297    fn parses_config_set_command() {
298        let cli = Cli::parse_from([
299            "greentic-dev",
300            "config",
301            "set",
302            "defaults.component.org",
303            "ai.greentic",
304            "--file",
305            "/tmp/config.toml",
306        ]);
307        let Command::Config(ConfigCommand::Set(args)) = cli.command else {
308            panic!("expected config set variant");
309        };
310        assert_eq!(args.key, "defaults.component.org");
311        assert_eq!(args.value, "ai.greentic");
312        assert_eq!(
313            args.file.as_ref().map(|p| p.display().to_string()),
314            Some("/tmp/config.toml".into())
315        );
316    }
317}