Skip to main content

packc/cli/
lint.rs

1#![forbid(unsafe_code)]
2
3use std::collections::BTreeSet;
4use std::path::PathBuf;
5
6use anyhow::{Result, bail};
7use clap::Parser;
8use greentic_flow::compile_ygtc_str;
9use greentic_pack::validate::oauth_capability_requirement_diagnostics_for_flow;
10use tracing::info;
11
12use crate::config::load_pack_config;
13use crate::extension_refs::{
14    default_extensions_file_path, default_extensions_lock_file_path, read_extensions_file,
15    read_extensions_lock_file, validate_extensions_lock_alignment,
16};
17
18#[derive(Debug, Parser)]
19pub struct LintArgs {
20    /// Root directory of the pack (must contain pack.yaml)
21    #[arg(long = "in", value_name = "DIR")]
22    pub input: PathBuf,
23
24    /// Allow OCI component refs in extensions to be tag-based (default requires sha256 digest)
25    #[arg(long = "allow-oci-tags", default_value_t = false)]
26    pub allow_oci_tags: bool,
27}
28
29pub fn handle(args: LintArgs, json: bool) -> Result<()> {
30    let pack_dir = normalize(args.input);
31    info!(path = %pack_dir.display(), "linting pack");
32
33    let extensions_file = default_extensions_file_path(&pack_dir);
34    let source_extensions = if extensions_file.exists() {
35        Some(read_extensions_file(&extensions_file)?)
36    } else {
37        None
38    };
39    let extensions_lock = default_extensions_lock_file_path(&pack_dir);
40    if extensions_lock.exists() {
41        let lock = read_extensions_lock_file(&extensions_lock)?;
42        if let Some(source) = source_extensions.as_ref() {
43            validate_extensions_lock_alignment(source, &lock)?;
44        }
45    }
46
47    let cfg = load_pack_config(&pack_dir)?;
48    crate::extensions::validate_components_extension(&cfg.extensions, args.allow_oci_tags)?;
49    crate::extensions::validate_deployer_extension(&cfg.extensions, &pack_dir)?;
50    crate::extensions::validate_static_routes_extension(&cfg.extensions, &pack_dir)?;
51
52    let required_capabilities: BTreeSet<String> = cfg
53        .dependencies
54        .iter()
55        .flat_map(|dep| dep.required_capabilities.iter())
56        .map(|cap| cap.trim().to_string())
57        .filter(|cap| !cap.is_empty())
58        .collect();
59
60    let mut compiled = 0usize;
61    let mut oauth_diagnostics = Vec::new();
62    for flow in &cfg.flows {
63        let src = std::fs::read_to_string(&flow.file)?;
64        let compiled_flow = compile_ygtc_str(&src)?;
65        oauth_diagnostics.extend(oauth_capability_requirement_diagnostics_for_flow(
66            flow.id.as_str(),
67            &compiled_flow,
68            &required_capabilities,
69        ));
70        compiled += 1;
71    }
72
73    if !oauth_diagnostics.is_empty() {
74        let mut message = String::from("OAuth capability requirement checks failed during lint:\n");
75        for diag in oauth_diagnostics {
76            if let Some(path) = diag.path.as_deref() {
77                message.push_str(&format!("- [{}] {}: {}\n", diag.code, path, diag.message));
78            } else {
79                message.push_str(&format!("- [{}] {}\n", diag.code, diag.message));
80            }
81            if let Some(hint) = diag.hint.as_deref() {
82                message.push_str(&format!("  hint: {hint}\n"));
83            }
84        }
85        bail!(message.trim_end().to_string());
86    }
87
88    if json {
89        println!(
90            "{}",
91            serde_json::to_string_pretty(&serde_json::json!({
92                "status": crate::cli_i18n::t("cli.status.ok"),
93                "pack_id": cfg.pack_id,
94                "version": cfg.version,
95                "flows": compiled,
96                "components": cfg.components.len(),
97                "dependencies": cfg.dependencies.len(),
98            }))?
99        );
100    } else {
101        println!("{}", crate::cli_i18n::t("cli.lint.ok_header"));
102        println!(
103            "{}",
104            crate::cli_i18n::tf("cli.lint.pack", &[&cfg.pack_id, &cfg.version.to_string()])
105        );
106        println!(
107            "{}",
108            crate::cli_i18n::tf("cli.lint.flows", &[&compiled.to_string()])
109        );
110        println!(
111            "{}",
112            crate::cli_i18n::tf("cli.lint.components", &[&cfg.components.len().to_string()])
113        );
114        println!(
115            "{}",
116            crate::cli_i18n::tf(
117                "cli.lint.dependencies",
118                &[&cfg.dependencies.len().to_string()]
119            )
120        );
121    }
122
123    Ok(())
124}
125
126fn normalize(path: PathBuf) -> PathBuf {
127    if path.is_absolute() {
128        path
129    } else {
130        std::env::current_dir()
131            .unwrap_or_else(|_| PathBuf::from("."))
132            .join(path)
133    }
134}