Skip to main content

packc/cli/
mod.rs

1#![forbid(unsafe_code)]
2
3use std::{convert::TryFrom, path::PathBuf};
4
5use anyhow::Result;
6use clap::{Parser, Subcommand};
7use greentic_types::{EnvId, TenantCtx, TenantId};
8use tokio::runtime::Runtime;
9
10pub mod add_extension;
11pub mod components;
12pub mod config;
13pub mod gui;
14pub mod input;
15pub mod inspect;
16pub mod lint;
17pub mod plan;
18pub mod providers;
19pub mod resolve;
20pub mod sign;
21pub mod update;
22pub mod verify;
23
24use crate::telemetry::set_current_tenant_ctx;
25use crate::{build, new, runtime};
26
27#[derive(Debug, Parser)]
28#[command(name = "greentic-pack", about = "Greentic pack CLI", version)]
29pub struct Cli {
30    /// Logging filter (overrides PACKC_LOG)
31    #[arg(long = "log", default_value = "info", global = true)]
32    pub verbosity: String,
33
34    /// Force offline mode (disables any network activity)
35    #[arg(long, global = true)]
36    pub offline: bool,
37
38    /// Override cache directory (defaults to pack_dir/.packc or GREENTIC_PACK_CACHE_DIR)
39    #[arg(long = "cache-dir", global = true)]
40    pub cache_dir: Option<PathBuf>,
41
42    /// Optional config overrides in TOML/JSON (greentic-config layer)
43    #[arg(long = "config-override", value_name = "FILE", global = true)]
44    pub config_override: Option<PathBuf>,
45
46    /// Emit machine-readable JSON output where applicable
47    #[arg(long, global = true)]
48    pub json: bool,
49
50    #[command(subcommand)]
51    pub command: Command,
52}
53
54#[allow(clippy::large_enum_variant)]
55#[derive(Debug, Subcommand)]
56pub enum Command {
57    /// Build a pack component and supporting artifacts
58    Build(BuildArgs),
59    /// Lint a pack manifest, flows, and templates
60    Lint(self::lint::LintArgs),
61    /// Sync pack.yaml components with files under components/
62    Components(self::components::ComponentsArgs),
63    /// Sync pack.yaml components and flows with files under the pack root
64    Update(self::update::UpdateArgs),
65    /// Scaffold a new pack directory
66    New(new::NewArgs),
67    /// Sign a pack manifest using an Ed25519 private key
68    Sign(self::sign::SignArgs),
69    /// Verify a pack's manifest signature
70    Verify(self::verify::VerifyArgs),
71    /// GUI-related tooling
72    #[command(subcommand)]
73    Gui(self::gui::GuiCommand),
74    /// Diagnose a pack archive (.gtpack) or source directory (runs validation)
75    Doctor(self::inspect::InspectArgs),
76    /// Deprecated alias for `doctor`
77    Inspect(self::inspect::InspectArgs),
78    /// Inspect resolved configuration (provenance and warnings)
79    Config(self::config::ConfigArgs),
80    /// Generate a DeploymentPlan from a pack archive or source directory.
81    Plan(self::plan::PlanArgs),
82    /// Provider extension helpers.
83    #[command(subcommand)]
84    Providers(self::providers::ProvidersCommand),
85    /// Add data to pack extensions.
86    #[command(subcommand)]
87    AddExtension(self::add_extension::AddExtensionCommand),
88    /// Resolve component references and write pack.lock.json
89    Resolve(self::resolve::ResolveArgs),
90}
91
92#[derive(Debug, Clone, Parser)]
93pub struct BuildArgs {
94    /// Root directory of the pack (must contain pack.yaml)
95    #[arg(long = "in", value_name = "DIR")]
96    pub input: PathBuf,
97
98    /// Skip running `packc update` before building (default: update first)
99    #[arg(long = "no-update", default_value_t = false)]
100    pub no_update: bool,
101
102    /// Output path for the built Wasm component (legacy; writes a stub)
103    #[arg(long = "out", value_name = "FILE")]
104    pub component_out: Option<PathBuf>,
105
106    /// Output path for the generated manifest (CBOR); defaults to dist/manifest.cbor
107    #[arg(long, value_name = "FILE")]
108    pub manifest: Option<PathBuf>,
109
110    /// Output path for the generated SBOM (legacy; writes a stub JSON)
111    #[arg(long, value_name = "FILE")]
112    pub sbom: Option<PathBuf>,
113
114    /// Output path for the generated & canonical .gtpack archive (default: dist/<pack_dir>.gtpack)
115    #[arg(long = "gtpack-out", value_name = "FILE")]
116    pub gtpack_out: Option<PathBuf>,
117
118    /// Optional path to pack.lock.json (default: <pack_dir>/pack.lock.json)
119    #[arg(long = "lock", value_name = "FILE")]
120    pub lock: Option<PathBuf>,
121
122    /// Bundle strategy for component artifacts (cache=embed wasm, none=refs only)
123    #[arg(long = "bundle", value_enum, default_value = "cache")]
124    pub bundle: crate::build::BundleMode,
125
126    /// When set, the command validates input without writing artifacts
127    #[arg(long)]
128    pub dry_run: bool,
129
130    /// Optional JSON file with additional secret requirements (migration bridge)
131    #[arg(long = "secrets-req", value_name = "FILE")]
132    pub secrets_req: Option<PathBuf>,
133
134    /// Default secret scope to apply when missing (dev-only), format: env/tenant[/team]
135    #[arg(long = "default-secret-scope", value_name = "ENV/TENANT[/TEAM]")]
136    pub default_secret_scope: Option<String>,
137
138    /// Allow OCI component refs in extensions to be tag-based (default requires sha256 digest)
139    #[arg(long = "allow-oci-tags", default_value_t = false)]
140    pub allow_oci_tags: bool,
141
142    /// Require manifest metadata for flow-referenced components
143    #[arg(long, default_value_t = false)]
144    pub require_component_manifests: bool,
145
146    /// Skip auto-including extra directories (e.g. schemas/, templates/)
147    #[arg(long = "no-extra-dirs", default_value_t = false)]
148    pub no_extra_dirs: bool,
149
150    /// Include source files (pack.yaml, flows) inside the generated .gtpack for debugging
151    #[arg(long = "dev", default_value_t = false)]
152    pub dev: bool,
153}
154
155pub fn run() -> Result<()> {
156    Runtime::new()?.block_on(run_with_cli(Cli::parse(), false))
157}
158
159/// Resolve the logging filter to use for telemetry initialisation.
160pub fn resolve_env_filter(cli: &Cli) -> String {
161    std::env::var("PACKC_LOG").unwrap_or_else(|_| cli.verbosity.clone())
162}
163
164/// Execute the CLI using a pre-parsed argument set.
165pub async fn run_with_cli(cli: Cli, warn_inspect_alias: bool) -> Result<()> {
166    let runtime = runtime::resolve_runtime(
167        Some(std::env::current_dir()?.as_path()),
168        cli.cache_dir.as_deref(),
169        cli.offline,
170        cli.config_override.as_deref(),
171    )?;
172
173    // Install telemetry according to resolved config.
174    crate::telemetry::install_with_config("packc", &runtime.resolved.config.telemetry)?;
175
176    set_current_tenant_ctx(&TenantCtx::new(
177        EnvId::try_from("local").expect("static env id"),
178        TenantId::try_from("packc").expect("static tenant id"),
179    ));
180
181    match cli.command {
182        Command::Build(args) => {
183            build::run(&build::BuildOptions::from_args(args, &runtime)?).await?
184        }
185        Command::Lint(args) => self::lint::handle(args, cli.json)?,
186        Command::Components(args) => self::components::handle(args, cli.json)?,
187        Command::Update(args) => self::update::handle(args, cli.json)?,
188        Command::New(args) => new::handle(args, cli.json, &runtime).await?,
189        Command::Sign(args) => self::sign::handle(args, cli.json)?,
190        Command::Verify(args) => self::verify::handle(args, cli.json)?,
191        Command::Gui(cmd) => self::gui::handle(cmd, cli.json, &runtime).await?,
192        Command::Inspect(args) | Command::Doctor(args) => {
193            if warn_inspect_alias {
194                eprintln!("WARNING: `inspect` is deprecated; use `doctor`.");
195            }
196            self::inspect::handle(args, cli.json, &runtime).await?
197        }
198        Command::Config(args) => self::config::handle(args, cli.json, &runtime)?,
199        Command::Plan(args) => self::plan::handle(&args)?,
200        Command::Providers(cmd) => self::providers::run(cmd)?,
201        Command::AddExtension(cmd) => self::add_extension::handle(cmd)?,
202        Command::Resolve(args) => self::resolve::handle(args, &runtime, true).await?,
203    }
204
205    Ok(())
206}