1mod cli;
2pub mod commands;
3pub mod config;
4pub mod env;
5pub mod model;
6pub mod output;
7pub mod paths;
8pub mod resolver;
9
10use clap::Parser;
11
12use cli::{Cli, Command};
13use env::{PathOverrides, resolve_roots};
14use model::{ConfigErrorKind, OutputFormat};
15use output::{
16 render_baseline, render_contexts, render_resolve, render_scaffold_baseline, render_stub,
17};
18
19const EXIT_OK: i32 = 0;
20const EXIT_STRICT_MISSING_REQUIRED: i32 = 1;
21const EXIT_USAGE: i32 = 2;
22const EXIT_CONFIG: i32 = 3;
23const EXIT_RUNTIME: i32 = 4;
24
25pub fn run() -> i32 {
26 run_with_args(std::env::args_os())
27}
28
29pub fn run_with_args<I, T>(args: I) -> i32
30where
31 I: IntoIterator<Item = T>,
32 T: Into<std::ffi::OsString> + Clone,
33{
34 let cli = match Cli::try_parse_from(args) {
35 Ok(parsed) => parsed,
36 Err(err) => {
37 let code = err.exit_code();
38 let _ = err.print();
39 return code;
40 }
41 };
42
43 dispatch(cli)
44}
45
46fn dispatch(cli: Cli) -> i32 {
47 let fallback_mode = cli.worktree_fallback;
48 let overrides = PathOverrides {
49 agent_home: cli.agent_home,
50 project_path: cli.project_path,
51 };
52
53 match cli.command {
54 Command::Contexts(args) => print_rendered(
55 render_contexts(args.format, resolver::supported_contexts()),
56 EXIT_OK,
57 ),
58 Command::Resolve(args) => {
59 let roots = match resolve_roots_or_exit(&overrides) {
60 Ok(roots) => roots,
61 Err(code) => return code,
62 };
63
64 let report =
65 match resolver::resolve_with_mode(args.context, &roots, args.strict, fallback_mode)
66 {
67 Ok(report) => report,
68 Err(err) => {
69 eprintln!("error: {err}");
70 return config_error_exit_code(&err);
71 }
72 };
73 let exit_code = if args.strict && report.has_missing_required() {
74 EXIT_STRICT_MISSING_REQUIRED
75 } else {
76 EXIT_OK
77 };
78 print_rendered(render_resolve(args.format, &report), exit_code)
79 }
80 Command::Add(args) => {
81 let roots = match resolve_roots_or_exit(&overrides) {
82 Ok(roots) => roots,
83 Err(code) => return code,
84 };
85 let request = commands::add::AddDocumentRequest {
86 target: args.target,
87 context: args.context,
88 scope: args.scope,
89 path: args.path,
90 required: args.required,
91 when: args.when,
92 notes: args.notes,
93 };
94
95 match commands::add::upsert_document(&roots, request) {
96 Ok(report) => {
97 let message = format!(
98 "target={} action={} config={} entries={}",
99 report.target,
100 report.action,
101 report.config_path.display(),
102 report.document_count
103 );
104 print_rendered(render_stub(OutputFormat::Text, "add", message), EXIT_OK)
105 }
106 Err(err) => {
107 eprintln!("error: {err}");
108 config_error_exit_code(&err)
109 }
110 }
111 }
112 Command::ScaffoldAgents(args) => {
113 let roots = match resolve_roots_or_exit(&overrides) {
114 Ok(roots) => roots,
115 Err(code) => return code,
116 };
117 let request = commands::scaffold_agents::ScaffoldAgentsRequest {
118 target: args.target,
119 output: args.output,
120 force: args.force,
121 };
122
123 match commands::scaffold_agents::scaffold_agents(&request, &roots) {
124 Ok(report) => {
125 let message = format!(
126 "target={} mode={} output={}",
127 report.target,
128 report.write_mode,
129 report.output_path.display()
130 );
131 print_rendered(
132 render_stub(OutputFormat::Text, "scaffold-agents", message),
133 EXIT_OK,
134 )
135 }
136 Err(err) => {
137 eprintln!("error: {err}");
138 match err.kind {
139 commands::scaffold_agents::ScaffoldAgentsErrorKind::AlreadyExists => {
140 EXIT_STRICT_MISSING_REQUIRED
141 }
142 commands::scaffold_agents::ScaffoldAgentsErrorKind::Io => EXIT_RUNTIME,
143 }
144 }
145 }
146 }
147 Command::Baseline(args) => {
148 if !args.check {
149 eprintln!("error: baseline currently supports only --check");
150 return EXIT_USAGE;
151 }
152
153 let roots = match resolve_roots_or_exit(&overrides) {
154 Ok(roots) => roots,
155 Err(code) => return code,
156 };
157 let report = match commands::baseline::check_builtin_baseline_with_mode(
158 args.target,
159 &roots,
160 args.strict,
161 fallback_mode,
162 ) {
163 Ok(report) => report,
164 Err(err) => {
165 eprintln!("error: {err}");
166 return config_error_exit_code(&err);
167 }
168 };
169 let exit_code = if args.strict && report.has_missing_required() {
170 EXIT_STRICT_MISSING_REQUIRED
171 } else {
172 EXIT_OK
173 };
174 print_rendered(render_baseline(args.format, &report), exit_code)
175 }
176 Command::ScaffoldBaseline(args) => {
177 let roots = match resolve_roots_or_exit(&overrides) {
178 Ok(roots) => roots,
179 Err(code) => return code,
180 };
181 let request = commands::scaffold_baseline::ScaffoldBaselineRequest {
182 target: args.target,
183 missing_only: args.missing_only,
184 force: args.force,
185 dry_run: args.dry_run,
186 };
187
188 match commands::scaffold_baseline::scaffold_baseline(&request, &roots) {
189 Ok(report) => {
190 print_rendered(render_scaffold_baseline(args.format, &report), EXIT_OK)
191 }
192 Err(err) => {
193 eprintln!("error: {err}");
194 EXIT_RUNTIME
195 }
196 }
197 }
198 }
199}
200
201fn resolve_roots_or_exit(overrides: &PathOverrides) -> Result<env::ResolvedRoots, i32> {
202 resolve_roots(overrides).map_err(|err| {
203 eprintln!("error: {err:#}");
204 EXIT_RUNTIME
205 })
206}
207
208fn config_error_exit_code(err: &model::ConfigLoadError) -> i32 {
209 match err.kind {
210 ConfigErrorKind::Validation | ConfigErrorKind::Parse => EXIT_CONFIG,
211 ConfigErrorKind::Io => EXIT_RUNTIME,
212 }
213}
214
215fn print_rendered(rendered: anyhow::Result<String>, success_exit_code: i32) -> i32 {
216 match rendered {
217 Ok(body) => {
218 println!("{body}");
219 success_exit_code
220 }
221 Err(err) => {
222 eprintln!("error: {err:#}");
223 EXIT_RUNTIME
224 }
225 }
226}