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