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`)
23    Component(ComponentPassthroughArgs),
24    /// Manage greentic-dev configuration
25    #[command(subcommand)]
26    Config(ConfigCommand),
27    /// MCP tooling (feature = "mcp")
28    #[cfg(feature = "mcp")]
29    #[command(subcommand)]
30    Mcp(McpCommand),
31}
32
33#[derive(Subcommand, Debug)]
34pub enum FlowCommand {
35    /// Validate a flow YAML file and emit the canonical bundle JSON
36    Validate(FlowValidateArgs),
37}
38
39#[derive(Args, Debug)]
40pub struct FlowValidateArgs {
41    /// Path to the flow definition (YAML)
42    #[arg(short = 'f', long = "file")]
43    pub file: PathBuf,
44    /// Emit compact JSON instead of pretty-printing
45    #[arg(long = "json")]
46    pub json: bool,
47}
48
49#[derive(Subcommand, Debug)]
50pub enum PackCommand {
51    /// Build a deterministic .gtpack from a validated flow bundle
52    Build(PackBuildArgs),
53    /// Execute a pack locally with mocks/telemetry support
54    Run(PackRunArgs),
55    /// Scaffold a pack workspace via the `packc` CLI
56    New(PackNewArgs),
57}
58
59#[derive(Args, Debug)]
60pub struct PackBuildArgs {
61    /// Path to the flow definition (YAML)
62    #[arg(short = 'f', long = "file")]
63    pub file: PathBuf,
64    /// Output path for the generated pack
65    #[arg(short = 'o', long = "out")]
66    pub out: PathBuf,
67    /// Signing mode for the generated pack
68    #[arg(long = "sign", default_value = "dev", value_enum)]
69    pub sign: PackSignArg,
70    /// Optional path to pack metadata (pack.toml)
71    #[arg(long = "meta")]
72    pub meta: Option<PathBuf>,
73    /// Directory containing local component builds
74    #[arg(long = "component-dir", value_name = "DIR")]
75    pub component_dir: Option<PathBuf>,
76}
77
78#[derive(Args, Debug)]
79pub struct PackRunArgs {
80    /// Path to the pack (.gtpack) to execute
81    #[arg(short = 'p', long = "pack")]
82    pub pack: PathBuf,
83    /// Flow entry identifier override
84    #[arg(long = "entry")]
85    pub entry: Option<String>,
86    /// JSON payload to use as run input
87    #[arg(long = "input")]
88    pub input: Option<String>,
89    /// Enforcement policy for pack signatures
90    #[arg(long = "policy", default_value = "devok", value_enum)]
91    pub policy: RunPolicyArg,
92    /// OTLP collector endpoint (optional)
93    #[arg(long = "otlp")]
94    pub otlp: Option<String>,
95    /// Comma-separated list of allowed outbound hosts
96    #[arg(long = "allow")]
97    pub allow: Option<String>,
98    /// Mocks toggle
99    #[arg(long = "mocks", default_value = "on", value_enum)]
100    pub mocks: MockSettingArg,
101    /// Directory to persist run artifacts (transcripts, logs)
102    #[arg(long = "artifacts")]
103    pub artifacts: Option<PathBuf>,
104}
105
106#[derive(Args, Debug, Clone, Default)]
107pub struct PackNewArgs {
108    /// Arguments passed directly to the `packc new` command
109    #[arg(
110        value_name = "ARGS",
111        trailing_var_arg = true,
112        allow_hyphen_values = true
113    )]
114    pub passthrough: Vec<String>,
115}
116
117#[derive(Args, Debug, Clone, Default)]
118pub struct ComponentPassthroughArgs {
119    /// Arguments passed directly to the `greentic-component` CLI
120    #[arg(
121        value_name = "ARGS",
122        trailing_var_arg = true,
123        allow_hyphen_values = true
124    )]
125    pub passthrough: Vec<String>,
126}
127
128#[cfg(feature = "mcp")]
129#[derive(Subcommand, Debug)]
130pub enum McpCommand {
131    /// Inspect MCP provider metadata
132    Doctor(McpDoctorArgs),
133}
134
135#[cfg(feature = "mcp")]
136#[derive(Args, Debug)]
137pub struct McpDoctorArgs {
138    /// MCP provider identifier or config path
139    pub provider: String,
140    /// Emit compact JSON instead of pretty output
141    #[arg(long = "json")]
142    pub json: bool,
143}
144
145#[derive(Subcommand, Debug)]
146pub enum ConfigCommand {
147    /// Set a key in ~/.greentic/config.toml (e.g. defaults.component.org)
148    Set(ConfigSetArgs),
149}
150
151#[derive(Args, Debug)]
152pub struct ConfigSetArgs {
153    /// Config key path (e.g. defaults.component.org)
154    pub key: String,
155    /// Value to assign to the key (stored as a string)
156    pub value: String,
157    /// Override config file path (default: ~/.greentic/config.toml)
158    #[arg(long = "file")]
159    pub file: Option<PathBuf>,
160}
161
162#[derive(Copy, Clone, Debug, ValueEnum)]
163pub enum PackSignArg {
164    Dev,
165    None,
166}
167
168#[derive(Copy, Clone, Debug, ValueEnum)]
169pub enum RunPolicyArg {
170    Strict,
171    Devok,
172}
173
174#[derive(Copy, Clone, Debug, ValueEnum)]
175pub enum MockSettingArg {
176    On,
177    Off,
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use clap::Parser;
184
185    #[test]
186    fn parses_component_passthrough_args() {
187        let cli = Cli::parse_from([
188            "greentic-dev",
189            "component",
190            "new",
191            "--name",
192            "demo",
193            "--json",
194        ]);
195        let Command::Component(args) = cli.command else {
196            panic!("expected component passthrough variant");
197        };
198        assert_eq!(
199            args.passthrough,
200            vec![
201                "new".to_string(),
202                "--name".into(),
203                "demo".into(),
204                "--json".into()
205            ]
206        );
207    }
208
209    #[test]
210    fn parses_pack_new_args() {
211        let cli = Cli::parse_from(["greentic-dev", "pack", "new", "--name", "demo-pack"]);
212        let Command::Pack(PackCommand::New(args)) = cli.command else {
213            panic!("expected pack new variant");
214        };
215        assert_eq!(
216            args.passthrough,
217            vec!["--name".to_string(), "demo-pack".to_string()]
218        );
219    }
220
221    #[test]
222    fn parses_config_set_command() {
223        let cli = Cli::parse_from([
224            "greentic-dev",
225            "config",
226            "set",
227            "defaults.component.org",
228            "ai.greentic",
229            "--file",
230            "/tmp/config.toml",
231        ]);
232        let Command::Config(ConfigCommand::Set(args)) = cli.command else {
233            panic!("expected config set variant");
234        };
235        assert_eq!(args.key, "defaults.component.org");
236        assert_eq!(args.value, "ai.greentic");
237        assert_eq!(
238            args.file.as_ref().map(|p| p.display().to_string()),
239            Some("/tmp/config.toml".into())
240        );
241    }
242}