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 #[arg(long = "in", value_name = "DIR")]
22 pub input: PathBuf,
23
24 #[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}