Skip to main content

meta_ast/interface/
args.rs

1use std::str::FromStr;
2
3use clap::Parser;
4
5/// Polyglot static analyzer that builds symbol surfaces and cross-file dependency graphs.
6#[derive(Parser)]
7#[command(name = "meta-ast", version, about = "Polyglot static analyzer")]
8pub enum Cli {
9    /// Inspect a project directory/file and extract all symbol definitions
10    ///
11    /// Examples:
12    ///   meta-ast inspect ./my-project
13    ///   meta-ast inspect ./my-project/main.py -f yaml -o symbols.yaml
14    ///   meta-ast inspect ./my-project --language python
15    Inspect(InspectArgs),
16
17    /// Build a cross-file dependency graph and analyze Strongly Connected Components (SCCs)
18    ///
19    /// Examples:
20    ///   meta-ast graph ./my-project
21    ///   meta-ast graph ./my-project --html -o project_graph.html
22    ///   meta-ast graph ./my-project -f yaml -o graph.yaml
23    Graph(GraphArgs),
24
25    /// Scan cross-language call sites and generate MetaCall deployment manifests
26    ///
27    /// Examples:
28    ///   meta-ast deploy ./my-project --out ./deploy-dir
29    ///   meta-ast deploy ./my-project --check
30    #[cfg(feature = "metacall-deploy")]
31    Deploy(DeployArgs),
32}
33
34fn parse_format(s: &str) -> Result<crate::output::OutputFormat, String> {
35    match s.to_lowercase().as_str() {
36        "json" => Ok(crate::output::OutputFormat::Json),
37        "yaml" | "yml" => Ok(crate::output::OutputFormat::Yaml),
38        _ => Err(format!("invalid format '{s}': expected 'json' or 'yaml'")),
39    }
40}
41
42fn parse_language(s: &str) -> Result<crate::language::LangId, String> {
43    let normalized = s.to_lowercase();
44    crate::language::LangId::from_str(&normalized).map_err(|_| {
45        let all = crate::language::LangId::all();
46        let names: Vec<_> = all.iter().map(|l| l.as_ref()).collect();
47        format!(
48            "invalid language '{s}': expected one of {}",
49            names.join(", ")
50        )
51    })
52}
53
54#[derive(Parser)]
55pub struct InspectArgs {
56    /// Root directory or source file to inspect
57    pub path: std::path::PathBuf,
58
59    /// Output file path (prints to stdout if omitted)
60    #[arg(short, long)]
61    pub output: Option<std::path::PathBuf>,
62
63    /// Only analyze files detected as this language
64    #[arg(short, long, value_parser = parse_language)]
65    pub language: Option<crate::language::LangId>,
66
67    /// Output format for the extracted symbols
68    #[arg(short = 'f', long, default_value = "json", value_parser = parse_format)]
69    pub format: crate::output::OutputFormat,
70
71    /// Diagnostic severity that makes the run exit with status 1
72    #[arg(long, value_enum, default_value_t = crate::interface::report::FailOn::Error)]
73    pub fail_on: crate::interface::report::FailOn,
74}
75
76#[derive(Parser)]
77pub struct GraphArgs {
78    /// Root directory to analyze
79    pub path: std::path::PathBuf,
80
81    /// Output file path (defaults to stdout, or `<path>.html` with --html)
82    #[arg(short, long)]
83    pub output: Option<std::path::PathBuf>,
84
85    /// Only analyze files detected as this language
86    #[arg(short, long, value_parser = parse_language)]
87    pub language: Option<crate::language::LangId>,
88
89    /// Output serialization format for the graph structure
90    #[arg(short = 'f', long, default_value = "json", value_parser = parse_format)]
91    pub format: crate::output::OutputFormat,
92
93    /// Diagnostic severity that makes the run exit with status 1
94    #[arg(long, value_enum, default_value_t = crate::interface::report::FailOn::Error)]
95    pub fail_on: crate::interface::report::FailOn,
96
97    /// Generate an interactive HTML dashboard with graph visualization
98    #[arg(long)]
99    pub html: bool,
100
101    /// Open the written dashboard in the default browser (needs --html)
102    #[arg(long, requires = "html")]
103    pub open: bool,
104
105    /// Also emit a portable datagraph export (requires --features dataflow)
106    #[cfg(feature = "dataflow")]
107    #[arg(long)]
108    pub datagraph: bool,
109
110    /// Output file for the datagraph export (defaults to the graph output name plus `.datagraph.json`)
111    #[cfg(feature = "dataflow")]
112    #[arg(long)]
113    pub datagraph_output: Option<std::path::PathBuf>,
114
115    /// Enter watch mode: monitor the project and re-analyze on file changes
116    #[cfg(feature = "watch")]
117    #[arg(long)]
118    pub watch: bool,
119
120    /// Debounce duration in milliseconds for watch mode (default: 200)
121    #[cfg(feature = "watch")]
122    #[arg(long, default_value = "200")]
123    pub watch_debounce: u64,
124}
125
126#[cfg(feature = "metacall-deploy")]
127#[derive(Parser)]
128pub struct DeployArgs {
129    /// Root directory of the project to analyze
130    pub path: std::path::PathBuf,
131
132    /// Output format for generated manifests
133    #[arg(short = 'f', long, default_value = "json", value_parser = parse_format)]
134    pub format: crate::output::OutputFormat,
135
136    /// Check mode: diff generated manifests against existing metacall.json
137    #[arg(long)]
138    pub check: bool,
139
140    /// Output directory for generated manifests and mesh annotation
141    #[arg(short, long, default_value = ".")]
142    pub out: std::path::PathBuf,
143
144    /// Maximum number of files in a single pod before rebalancing is triggered
145    #[arg(long, default_value_t = crate::deploy::cut::DEFAULT_MAX_POD_SIZE)]
146    pub max_pod_size: usize,
147
148    /// Diagnostic severity that makes the run exit with status 1
149    #[arg(long, value_enum, default_value_t = crate::interface::report::FailOn::Error)]
150    pub fail_on: crate::interface::report::FailOn,
151}
152
153/// The CLI owns the mapping from parsed arguments to the emitter configuration.
154impl From<&InspectArgs> for crate::output::emitter::EmitConfig {
155    fn from(args: &InspectArgs) -> Self {
156        Self {
157            output: args.output.clone(),
158            format: args.format,
159            html: false,
160            open_browser: false,
161        }
162    }
163}
164
165impl From<&GraphArgs> for crate::output::emitter::EmitConfig {
166    fn from(args: &GraphArgs) -> Self {
167        Self {
168            output: args.output.clone(),
169            format: args.format,
170            html: args.html,
171            open_browser: true,
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn parse_language_valid_lowercase() {
182        assert_eq!(
183            parse_language("python"),
184            Ok(crate::language::LangId::Python)
185        );
186        assert_eq!(
187            parse_language("typescript"),
188            Ok(crate::language::LangId::TypeScript)
189        );
190    }
191
192    #[test]
193    fn parse_language_case_insensitive() {
194        assert_eq!(
195            parse_language("Python"),
196            Ok(crate::language::LangId::Python)
197        );
198        assert_eq!(
199            parse_language("TYPESCRIPT"),
200            Ok(crate::language::LangId::TypeScript)
201        );
202    }
203
204    #[test]
205    fn parse_language_invalid_returns_all_names() {
206        let err = parse_language("pytho").unwrap_err();
207        for id in crate::language::LangId::all() {
208            let name: &str = id.as_ref();
209            assert!(
210                err.contains(name),
211                "error {err:?} should list the valid name {name:?}"
212            );
213        }
214    }
215
216    #[test]
217    fn parse_language_empty_returns_err() {
218        assert!(parse_language("").is_err());
219    }
220
221    #[test]
222    fn opening_the_dashboard_is_an_opt_in() {
223        let parsed = Cli::try_parse_from(["meta-ast", "graph", "demo", "--html", "--open"]);
224        assert!(
225            parsed.is_ok(),
226            "graph --html --open parses: {:?}",
227            parsed.as_ref().err()
228        );
229        assert!(
230            matches!(parsed, Ok(Cli::Graph(ref args)) if args.open),
231            "the flag reaches the parsed arguments"
232        );
233        assert!(
234            Cli::try_parse_from(["meta-ast", "graph", "demo", "--open"]).is_err(),
235            "--open without --html is a usage error"
236        );
237    }
238}