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