1use crate::commands::{
8 cmd_coverage, cmd_decisions_for, cmd_diff, cmd_doctor, cmd_eval, cmd_export, cmd_find,
9 cmd_gate, cmd_herald, cmd_hook, cmd_improve, cmd_index, cmd_init, cmd_inspect, cmd_mcp_stats, cmd_migrate,
10 cmd_new, cmd_portfolio, cmd_quickstart, cmd_relationships, cmd_rename, cmd_resolve,
11 cmd_retrieve, cmd_review, cmd_schema, cmd_sentry, cmd_skill, cmd_stats, cmd_telemetry,
12 cmd_templates, cmd_usage, cmd_validate, CoverageArgs, DecisionsForArgs, DiffArgs, DoctorArgs,
13 EvalArgs, ExportArgs, FindArgs, GateArgs, HeraldArgs, HookArgs, ImproveArgs, IndexArgs, InitArgs,
14 InspectArgs, McpStatsArgs, MigrateArgs, NewArgs, PortfolioArgs, QuickstartArgs,
15 RelationshipsArgs, RenameArgs, ResolveArgs, RetrieveArgs, ReviewArgs, SchemaArgs, SentryArgs,
16 SkillArgs, StatsArgs, TelemetryArgs, TemplatesArgs, UsageArgs, ValidateArgs, WatchkeeperArgs,
17};
18use crate::commands::cmd_watchkeeper;
19use crate::output::rac_version;
20
21const SUBCOMMANDS: [&str; 32] = [
24 "validate",
25 "diff",
26 "stats",
27 "ingest",
28 "inspect",
29 "improve",
30 "schema",
31 "relationships",
32 "rename",
33 "review",
34 "doctor",
35 "coverage",
36 "gate",
37 "watchkeeper",
38 "portfolio",
39 "index",
40 "export",
41 "mcp",
42 "mcp-stats",
43 "telemetry",
44 "usage",
45 "new",
46 "templates",
47 "init",
48 "quickstart",
49 "resolve",
50 "find",
51 "decisions-for",
52 "eval",
53 "migrate",
54 "skill",
55 "hook",
56];
57
58fn version_line() -> String {
59 format!("decided {}", rac_version())
60}
61
62fn print_stdout(text: &str) {
63 use std::io::Write;
64 let mut out = std::io::stdout().lock();
65 let _ = out.write_all(text.as_bytes());
66 let _ = out.write_all(b"\n");
67 let _ = out.flush();
68}
69
70static PARSE_LEVEL_EXIT: std::sync::atomic::AtomicBool =
76 std::sync::atomic::AtomicBool::new(false);
77
78fn skip_usage_record() {
79 PARSE_LEVEL_EXIT.store(true, std::sync::atomic::Ordering::Relaxed);
80}
81
82fn argparse_error(prog: &str, message: &str) -> u8 {
86 skip_usage_record();
87 eprintln!("usage: {prog} ...");
88 eprintln!("{prog}: error: {message}");
89 2
90}
91
92fn unrecognized(extras: &[String]) -> u8 {
95 argparse_error(
96 "decided",
97 &format!("unrecognized arguments: {}", extras.join(" ")),
98 )
99}
100
101fn invalid_choice_message(token: &str) -> String {
102 let choices = SUBCOMMANDS
103 .iter()
104 .map(|s| format!("'{s}'"))
105 .collect::<Vec<_>>()
106 .join(", ");
107 format!("argument command: invalid choice: '{token}' (choose from {choices})")
108}
109
110pub fn run(args: &[String]) -> u8 {
115 let start = std::time::Instant::now();
116 let code = run_dispatch(args);
117 if !PARSE_LEVEL_EXIT.load(std::sync::atomic::Ordering::Relaxed) {
118 if let Some(command) = args.first().filter(|a| !a.starts_with('-')) {
119 let outcome = if code == 0 {
120 crate::usage::OUTCOME_OK
121 } else {
122 crate::usage::OUTCOME_ERROR
123 };
124 crate::usage::record_command(
125 command,
126 outcome,
127 start.elapsed().as_millis() as i64,
128 );
129 }
130 }
131 code
132}
133
134fn run_dispatch(args: &[String]) -> u8 {
135 let mut it = args.iter();
136 let first = match it.next() {
137 None => return argparse_error("decided", "the following arguments are required: command"),
138 Some(a) if a == "--version" => {
139 skip_usage_record();
140 print_stdout(&version_line());
141 return 0;
142 }
143 Some(a) if a == "-h" || a == "--help" => {
144 skip_usage_record();
146 print_stdout("usage: decided [-h] [--version] <command> ...");
147 return 0;
148 }
149 Some(a) => a,
150 };
151
152 if first.starts_with('-') {
153 return argparse_error("decided", &format!("unrecognized arguments: {first}"));
154 }
155 if !matches!(first.as_str(), "retrieve" | "sentry" | "herald")
159 && !SUBCOMMANDS.contains(&first.as_str())
160 {
161 return argparse_error("decided", &invalid_choice_message(first));
162 }
163
164 let rest: Vec<&String> = it.collect();
165
166 let order_aware = matches!(
182 first.as_str(),
183 "mcp-stats" | "telemetry" | "usage" | "skill" | "hook" | "eval" | "init" | "quickstart"
184 | "migrate" | "watchkeeper"
185 );
186 if !order_aware {
187 if rest.iter().any(|a| a.as_str() == "--version") {
188 skip_usage_record();
189 print_stdout(&version_line());
190 return 0;
191 }
192 if rest.iter().any(|a| a.as_str() == "-h" || a.as_str() == "--help") {
193 skip_usage_record();
194 print_stdout(&format!("usage: decided {first} ..."));
195 return 0;
196 }
197 }
198
199 match first.as_str() {
200 "validate" => run_validate(&rest),
201 "diff" => run_diff(&rest),
202 "inspect" => run_inspect(&rest),
203 "improve" => run_improve(&rest),
204 "relationships" => run_relationships(&rest),
205 "stats" => run_stats(&rest),
206 "schema" => run_schema(&rest),
207 "templates" => run_templates(&rest),
208 "resolve" => run_resolve(&rest),
209 "find" => run_find(&rest),
210 "retrieve" => run_retrieve(&rest),
211 "review" => run_review(&rest),
212 "export" => run_export(&rest),
213 "index" => run_index(&rest),
214 "portfolio" => run_portfolio(&rest),
215 "coverage" => run_coverage(&rest),
216 "decisions-for" => run_decisions_for(&rest),
217 "gate" => run_gate(&rest),
218 "sentry" => run_sentry(&rest),
219 "herald" => run_herald(&rest),
220 "doctor" => run_doctor(&rest),
221 "watchkeeper" => run_watchkeeper(&rest),
222 "mcp-stats" => run_mcp_stats(&rest),
223 "usage" => run_usage(&rest),
224 "telemetry" => run_telemetry(&rest),
225 "skill" => run_skill(&rest),
226 "hook" => run_hook(&rest),
227 "eval" => run_eval(&rest),
228 "new" => run_new(&rest),
229 "init" => run_init(&rest),
230 "quickstart" => run_quickstart(&rest),
231 "rename" => run_rename(&rest),
232 "migrate" => run_migrate(&rest),
233 other => {
234 eprintln!("decided-rs: subcommand '{other}' is not yet implemented");
235 2
236 }
237 }
238}
239
240struct FlagError(u8);
241
242fn mutex_check(prog: &str, new_flag: &str, other_flag: &str, other_set: bool) -> Option<u8> {
245 if other_set {
246 Some(argparse_error(
247 prog,
248 &format!("argument {new_flag}: not allowed with argument {other_flag}"),
249 ))
250 } else {
251 None
252 }
253}
254
255fn take_opt_value(
260 prog: &str,
261 flag: &str,
262 arg: &str,
263 rest: &[&String],
264 i: &mut usize,
265) -> Result<String, u8> {
266 if let Some(inline) = arg.strip_prefix(flag).and_then(|r| r.strip_prefix('=')) {
267 return Ok(inline.to_string());
268 }
269 *i += 1;
270 match rest.get(*i) {
271 Some(v) if !v.starts_with('-') || v.as_str() == "-" => Ok(v.to_string()),
272 _ => Err(argparse_error(
273 prog,
274 &format!("argument {flag}: expected one argument"),
275 )),
276 }
277}
278
279fn run_validate(rest: &[&String]) -> u8 {
280 let prog = "decided validate";
281 let mut file: Option<String> = None;
282 let mut json = false;
283 let mut sarif = false;
284 let mut top_level = false;
285 let mut corpus: Option<String> = None;
286 let mut cache = true;
287 let mut verify = false;
288 let mut extras: Vec<String> = Vec::new();
289 let mut positional_only = false;
290
291 let mut i = 0;
292 while i < rest.len() {
293 let arg = rest[i].as_str();
294 if positional_only || arg == "-" || !arg.starts_with('-') {
295 if file.is_none() {
296 file = Some(arg.to_string());
297 } else {
298 extras.push(arg.to_string());
299 }
300 i += 1;
301 continue;
302 }
303 match arg {
304 "--" => positional_only = true,
305 "--json" => {
306 if let Some(code) = mutex_check(prog, "--json", "--sarif", sarif) {
307 return code;
308 }
309 json = true;
310 }
311 "--sarif" => {
312 if let Some(code) = mutex_check(prog, "--sarif", "--json", json) {
313 return code;
314 }
315 sarif = true;
316 }
317 "--top-level" => top_level = true,
318 "--recursive" => {} "--cache" => cache = true,
320 "--no-cache" => cache = false,
321 "--verify" => verify = true,
322 other if other == "--corpus" || other.starts_with("--corpus=") => {
323 match take_opt_value(prog, "--corpus", other, rest, &mut i) {
324 Ok(v) => corpus = Some(v),
325 Err(code) => return code,
326 }
327 }
328 other => extras.push(other.to_string()),
329 }
330 i += 1;
331 }
332
333 let Some(file) = file else {
334 return argparse_error(prog, "the following arguments are required: file");
335 };
336 if !extras.is_empty() {
337 return unrecognized(&extras);
338 }
339
340 cmd_validate(&ValidateArgs {
341 file,
342 json,
343 sarif,
344 top_level,
345 corpus,
346 cache,
347 verify,
348 }) as u8
349}
350
351fn run_diff(rest: &[&String]) -> u8 {
352 let prog = "decided diff";
353 let mut old: Option<String> = None;
354 let mut new: Option<String> = None;
355 let mut json = false;
356 let mut extras: Vec<String> = Vec::new();
357 let mut positional_only = false;
358
359 for arg in rest {
360 let arg = arg.as_str();
361 if positional_only || arg == "-" || !arg.starts_with('-') {
362 if old.is_none() {
363 old = Some(arg.to_string());
364 } else if new.is_none() {
365 new = Some(arg.to_string());
366 } else {
367 extras.push(arg.to_string());
368 }
369 continue;
370 }
371 match arg {
372 "--" => positional_only = true,
373 "--json" => json = true,
374 other => extras.push(other.to_string()),
375 }
376 }
377
378 let (Some(old), Some(new)) = (old.clone(), new) else {
381 let missing = if old.is_none() { "old, new" } else { "new" };
382 return argparse_error(
383 prog,
384 &format!("the following arguments are required: {missing}"),
385 );
386 };
387 if !extras.is_empty() {
388 return unrecognized(&extras);
390 }
391
392 cmd_diff(&DiffArgs { old, new, json }) as u8
393}
394
395fn run_inspect(rest: &[&String]) -> u8 {
396 let prog = "decided inspect";
397 let mut file: Option<String> = None;
398 let mut verbose = false;
399 let mut top_level = false;
400 let mut json = false;
401 let mut extras: Vec<String> = Vec::new();
402 let mut positional_only = false;
403
404 for arg in rest {
405 let arg = arg.as_str();
406 if positional_only || arg == "-" || !arg.starts_with('-') {
407 if file.is_none() {
408 file = Some(arg.to_string());
409 } else {
410 extras.push(arg.to_string());
411 }
412 continue;
413 }
414 match arg {
415 "--" => positional_only = true,
416 "--verbose" => verbose = true,
417 "--top-level" => top_level = true,
418 "--recursive" => {} "--json" => json = true,
420 other => extras.push(other.to_string()),
421 }
422 }
423
424 let Some(file) = file else {
425 return argparse_error(prog, "the following arguments are required: file");
426 };
427 if !extras.is_empty() {
428 return unrecognized(&extras);
429 }
430
431 cmd_inspect(&InspectArgs {
432 file,
433 verbose,
434 top_level,
435 json,
436 }) as u8
437}
438
439fn run_improve(rest: &[&String]) -> u8 {
440 let prog = "decided improve";
441 let mut file: Option<String> = None;
442 let mut json = false;
443 let mut template = false;
444 let mut extras: Vec<String> = Vec::new();
445 let mut positional_only = false;
446
447 for arg in rest {
448 let arg = arg.as_str();
449 if positional_only || arg == "-" || !arg.starts_with('-') {
450 if file.is_none() {
451 file = Some(arg.to_string());
452 } else {
453 extras.push(arg.to_string());
454 }
455 continue;
456 }
457 match arg {
458 "--" => positional_only = true,
459 "--json" => {
462 if let Some(code) = mutex_check(prog, "--json", "--template", template) {
463 return code;
464 }
465 json = true;
466 }
467 "--template" => {
468 if let Some(code) = mutex_check(prog, "--template", "--json", json) {
469 return code;
470 }
471 template = true;
472 }
473 other => extras.push(other.to_string()),
474 }
475 }
476
477 let Some(file) = file else {
478 return argparse_error(prog, "the following arguments are required: file");
479 };
480 if !extras.is_empty() {
481 return unrecognized(&extras);
482 }
483
484 cmd_improve(&ImproveArgs {
485 file,
486 json,
487 template,
488 }) as u8
489}
490
491fn run_relationships(rest: &[&String]) -> u8 {
492 let prog = "decided relationships";
493 let mut path: Option<String> = None;
494 let mut validate = false;
495 let mut sarif = false;
496 let mut json = false;
497 let mut top_level = false;
498 let mut extras: Vec<String> = Vec::new();
499 let mut positional_only = false;
500
501 for arg in rest {
502 let arg = arg.as_str();
503 if positional_only || !arg.starts_with('-') {
504 if path.is_none() {
505 path = Some(arg.to_string());
506 } else {
507 extras.push(arg.to_string());
508 }
509 continue;
510 }
511 match arg {
512 "--" => positional_only = true,
513 "--validate" => validate = true,
514 "--sarif" => sarif = true,
515 "--json" => json = true,
516 "--top-level" => top_level = true,
517 "--recursive" => {}
518 other => extras.push(other.to_string()),
519 }
520 }
521
522 let Some(path) = path else {
523 return argparse_error(prog, "the following arguments are required: path");
524 };
525 if !extras.is_empty() {
526 return unrecognized(&extras);
527 }
528
529 cmd_relationships(&RelationshipsArgs {
530 path,
531 validate,
532 sarif,
533 json,
534 top_level,
535 }) as u8
536}
537
538fn run_stats(rest: &[&String]) -> u8 {
539 let prog = "decided stats";
540 let mut directory: Option<String> = None;
541 let mut json = false;
542 let mut extras: Vec<String> = Vec::new();
543 let mut positional_only = false;
544
545 for arg in rest {
546 let arg = arg.as_str();
547 if positional_only || !arg.starts_with('-') {
548 if directory.is_none() {
549 directory = Some(arg.to_string());
550 } else {
551 extras.push(arg.to_string());
552 }
553 continue;
554 }
555 match arg {
556 "--" => positional_only = true,
557 "--json" => json = true,
558 other => extras.push(other.to_string()),
559 }
560 }
561
562 let Some(directory) = directory else {
563 return argparse_error(prog, "the following arguments are required: directory");
564 };
565 if !extras.is_empty() {
566 return unrecognized(&extras);
567 }
568
569 cmd_stats(&StatsArgs { directory, json }) as u8
570}
571
572fn run_portfolio(rest: &[&String]) -> u8 {
573 let prog = "decided portfolio";
574 let mut directory: Option<String> = None;
575 let mut json = false;
576 let mut top_level = false;
577 let mut extras: Vec<String> = Vec::new();
578 let mut positional_only = false;
579
580 for arg in rest {
581 let arg = arg.as_str();
582 if positional_only || arg == "-" || !arg.starts_with('-') {
583 if directory.is_none() {
584 directory = Some(arg.to_string());
585 } else {
586 extras.push(arg.to_string());
587 }
588 continue;
589 }
590 match arg {
591 "--" => positional_only = true,
592 "--json" => json = true,
593 "--top-level" => top_level = true,
594 "--recursive" => {} other => extras.push(other.to_string()),
596 }
597 }
598
599 let Some(directory) = directory else {
601 return argparse_error(prog, "the following arguments are required: directory");
602 };
603 if !extras.is_empty() {
604 return unrecognized(&extras);
605 }
606
607 cmd_portfolio(&PortfolioArgs {
608 directory,
609 json,
610 top_level,
611 }) as u8
612}
613
614fn run_index(rest: &[&String]) -> u8 {
618 let mut directory: Option<String> = None;
619 let mut json = false;
620 let mut top_level = false;
621 let mut extras: Vec<String> = Vec::new();
622 let mut positional_only = false;
623
624 for arg in rest {
625 let arg = arg.as_str();
626 if positional_only || arg == "-" || !arg.starts_with('-') {
627 if directory.is_none() {
628 directory = Some(arg.to_string());
629 } else {
630 extras.push(arg.to_string());
631 }
632 continue;
633 }
634 match arg {
635 "--" => positional_only = true,
636 "--json" => json = true,
637 "--top-level" => top_level = true,
638 "--recursive" => {} other => extras.push(other.to_string()),
640 }
641 }
642
643 if !extras.is_empty() {
644 return unrecognized(&extras);
645 }
646
647 cmd_index(&IndexArgs {
648 directory: directory.unwrap_or_else(|| ".".to_string()),
649 json,
650 top_level,
651 }) as u8
652}
653
654fn run_coverage(rest: &[&String]) -> u8 {
655 let mut directory: Option<String> = None;
658 let mut json = false;
659 let mut extras: Vec<String> = Vec::new();
660 let mut positional_only = false;
661
662 for arg in rest {
663 let arg = arg.as_str();
664 if positional_only || arg == "-" || !arg.starts_with('-') {
665 if directory.is_none() {
666 directory = Some(arg.to_string());
667 } else {
668 extras.push(arg.to_string());
669 }
670 continue;
671 }
672 match arg {
673 "--" => positional_only = true,
674 "--json" => json = true,
675 other => extras.push(other.to_string()),
676 }
677 }
678
679 if !extras.is_empty() {
680 return unrecognized(&extras);
681 }
682
683 cmd_coverage(&CoverageArgs {
684 directory: directory.unwrap_or_else(|| ".".to_string()),
685 json,
686 }) as u8
687}
688
689fn run_decisions_for(rest: &[&String]) -> u8 {
690 let prog = "decided decisions-for";
691 let mut path: Option<String> = None;
692 let mut directory: Option<String> = None;
693 let mut json = false;
694 let mut top_level = false;
695 let mut extras: Vec<String> = Vec::new();
696 let mut positional_only = false;
697
698 for arg in rest {
699 let arg = arg.as_str();
700 if positional_only || arg == "-" || !arg.starts_with('-') {
701 if path.is_none() {
702 path = Some(arg.to_string());
703 } else if directory.is_none() {
704 directory = Some(arg.to_string());
705 } else {
706 extras.push(arg.to_string());
707 }
708 continue;
709 }
710 match arg {
711 "--" => positional_only = true,
712 "--json" => json = true,
713 "--top-level" => top_level = true,
714 "--recursive" => {} other => extras.push(other.to_string()),
716 }
717 }
718
719 let Some(path) = path else {
720 return argparse_error(prog, "the following arguments are required: path");
721 };
722 if !extras.is_empty() {
723 return unrecognized(&extras);
724 }
725
726 cmd_decisions_for(&DecisionsForArgs {
727 path,
728 directory: directory.unwrap_or_else(|| ".".to_string()),
729 json,
730 top_level,
731 }) as u8
732}
733
734fn run_gate(rest: &[&String]) -> u8 {
735 let prog = "decided gate";
736 let mut directory: Option<String> = None;
737 let mut json = false;
738 let mut sarif = false;
739 let mut top_level = false;
740 let mut code = false;
741 let mut repository = ".".to_string();
742 let mut base: Option<String> = None;
743 let mut full = false;
744 let mut extras: Vec<String> = Vec::new();
745 let mut positional_only = false;
746
747 let mut i = 0;
748 while i < rest.len() {
749 let arg = rest[i].as_str();
750 if positional_only || arg == "-" || !arg.starts_with('-') {
751 if directory.is_none() {
752 directory = Some(arg.to_string());
753 } else {
754 extras.push(arg.to_string());
755 }
756 i += 1;
757 continue;
758 }
759 match arg {
760 "--" => positional_only = true,
761 "--json" => {
763 if let Some(code) = mutex_check(prog, "--json", "--sarif", sarif) {
764 return code;
765 }
766 json = true;
767 }
768 "--sarif" => {
769 if let Some(code) = mutex_check(prog, "--sarif", "--json", json) {
770 return code;
771 }
772 sarif = true;
773 }
774 "--top-level" => top_level = true,
775 "--code" => code = true,
776 "--full" => full = true,
777 "--repository" | "--base" => {
778 i += 1;
779 if i >= rest.len() {
780 return argparse_error(prog, &format!("argument {arg}: expected one argument"));
781 }
782 if arg == "--repository" {
783 repository = rest[i].to_string();
784 } else {
785 base = Some(rest[i].to_string());
786 }
787 }
788 other => extras.push(other.to_string()),
791 }
792 i += 1;
793 }
794
795 let Some(directory) = directory else {
797 return argparse_error(prog, "the following arguments are required: directory");
798 };
799 if !extras.is_empty() {
800 return unrecognized(&extras);
801 }
802
803 cmd_gate(&GateArgs {
804 directory,
805 json,
806 sarif,
807 top_level,
808 code,
809 repository,
810 base,
811 full,
812 }) as u8
813}
814
815fn run_sentry(rest: &[&String]) -> u8 {
816 let prog = "decided sentry";
817 let mut directory: Option<String> = None;
818 let mut repository = ".".to_string();
819 let mut base: Option<String> = None;
820 let mut full = false;
821 let mut json = false;
822 let mut sarif = false;
823 let mut top_level = false;
824 let mut extras = Vec::new();
825 let mut positional_only = false;
826 let mut i = 0;
827 while i < rest.len() {
828 let arg = rest[i].as_str();
829 if positional_only || arg == "-" || !arg.starts_with('-') {
830 if directory.is_none() {
831 directory = Some(arg.to_string());
832 } else {
833 extras.push(arg.to_string());
834 }
835 i += 1;
836 continue;
837 }
838 match arg {
839 "--" => positional_only = true,
840 "--json" => {
841 if let Some(code) = mutex_check(prog, "--json", "--sarif", sarif) {
842 return code;
843 }
844 json = true;
845 }
846 "--sarif" => {
847 if let Some(code) = mutex_check(prog, "--sarif", "--json", json) {
848 return code;
849 }
850 sarif = true;
851 }
852 "--full" => full = true,
853 "--top-level" => top_level = true,
854 "--repository" | "--base" => {
855 i += 1;
856 if i >= rest.len() {
857 return argparse_error(prog, &format!("argument {arg}: expected one argument"));
858 }
859 if arg == "--repository" {
860 repository = rest[i].to_string();
861 } else {
862 base = Some(rest[i].to_string());
863 }
864 }
865 other => extras.push(other.to_string()),
866 }
867 i += 1;
868 }
869 let Some(directory) = directory else {
870 return argparse_error(prog, "the following arguments are required: directory");
871 };
872 if !extras.is_empty() {
873 return unrecognized(&extras);
874 }
875 cmd_sentry(&SentryArgs {
876 directory,
877 repository,
878 base,
879 full,
880 json,
881 sarif,
882 top_level,
883 }) as u8
884}
885
886fn run_herald(rest: &[&String]) -> u8 {
887 let prog = "decided herald";
888 let mut directory = None;
889 let mut paths_file = None;
890 let mut link_base = String::new();
891 let mut max_inline = 5i64;
892 let mut out = None;
893 let mut github_output = None;
894 let mut top_level = false;
895 let mut extras = Vec::new();
896 let mut i = 0;
897 while i < rest.len() {
898 let arg = rest[i].as_str();
899 if arg == "-" || !arg.starts_with('-') {
900 if directory.is_none() {
901 directory = Some(arg.to_string());
902 } else {
903 extras.push(arg.to_string());
904 }
905 i += 1;
906 continue;
907 }
908 match arg {
909 "--top-level" => top_level = true,
910 "--paths-file" | "--link-base" | "--max-inline" | "--out" | "--github-output" => {
911 i += 1;
912 if i >= rest.len() {
913 return argparse_error(prog, &format!("argument {arg}: expected one argument"));
914 }
915 let value = rest[i].to_string();
916 match arg {
917 "--paths-file" => paths_file = Some(value),
918 "--link-base" => link_base = value,
919 "--max-inline" => {
920 max_inline = match value.parse() {
921 Ok(value) => value,
922 Err(_) => {
923 return argparse_error(
924 prog,
925 &format!("argument --max-inline: invalid int value: '{value}'"),
926 )
927 }
928 }
929 }
930 "--out" => out = Some(value),
931 "--github-output" => github_output = Some(value),
932 _ => unreachable!(),
933 }
934 }
935 other => extras.push(other.to_string()),
936 }
937 i += 1;
938 }
939 let Some(directory) = directory else {
940 return argparse_error(prog, "the following arguments are required: directory");
941 };
942 let Some(paths_file) = paths_file else {
943 return argparse_error(prog, "the following arguments are required: --paths-file");
944 };
945 let Some(out) = out else {
946 return argparse_error(prog, "the following arguments are required: --out");
947 };
948 if !extras.is_empty() {
949 return unrecognized(&extras);
950 }
951 cmd_herald(&HeraldArgs {
952 directory,
953 paths_file,
954 link_base,
955 max_inline,
956 out,
957 github_output,
958 top_level,
959 }) as u8
960}
961
962fn run_doctor(rest: &[&String]) -> u8 {
963 let prog = "decided doctor";
964 let mut directory: Option<String> = None;
965 let mut json = false;
966 let mut top_level = false;
967 let mut hub_threshold: i64 = 20; let mut extras: Vec<String> = Vec::new();
969 let mut positional_only = false;
970
971 let mut i = 0;
972 while i < rest.len() {
973 let arg = rest[i].as_str();
974 if positional_only || arg == "-" || !arg.starts_with('-') || looks_like_negative_number(arg)
975 {
976 if directory.is_none() {
977 directory = Some(arg.to_string());
978 } else {
979 extras.push(arg.to_string());
980 }
981 i += 1;
982 continue;
983 }
984 match arg {
985 "--" => positional_only = true,
986 "--json" => json = true,
987 "--top-level" => top_level = true,
988 "--recursive" => {} "--hub-threshold" => {
990 i += 1;
993 let raw = match rest.get(i) {
994 Some(v)
995 if !v.starts_with('-')
996 || v.as_str() == "-"
997 || looks_like_negative_number(v) =>
998 {
999 v.as_str()
1000 }
1001 _ => {
1002 return argparse_error(
1003 prog,
1004 "argument --hub-threshold: expected one argument",
1005 )
1006 }
1007 };
1008 match py_parse_int(raw) {
1009 Some(v) => hub_threshold = v,
1010 None => {
1011 return argparse_error(
1012 prog,
1013 &format!("argument --hub-threshold: invalid int value: '{raw}'"),
1014 )
1015 }
1016 }
1017 }
1018 other if other.starts_with("--hub-threshold=") => {
1019 let raw = &other["--hub-threshold=".len()..];
1020 match py_parse_int(raw) {
1021 Some(v) => hub_threshold = v,
1022 None => {
1023 return argparse_error(
1024 prog,
1025 &format!("argument --hub-threshold: invalid int value: '{raw}'"),
1026 )
1027 }
1028 }
1029 }
1030 other => extras.push(other.to_string()),
1031 }
1032 i += 1;
1033 }
1034
1035 if !extras.is_empty() {
1036 return unrecognized(&extras);
1037 }
1038
1039 cmd_doctor(&DoctorArgs {
1040 directory: directory.unwrap_or_else(|| ".".to_string()),
1041 json,
1042 top_level,
1043 hub_threshold,
1044 }) as u8
1045}
1046
1047fn parse_json_share_group(prog: &str, rest: &[&String]) -> Result<(bool, bool), u8> {
1054 let mut json = false;
1055 let mut share = false;
1056 let mut extras: Vec<String> = Vec::new();
1057 let mut positional_only = false;
1058
1059 for arg in rest {
1060 let arg = arg.as_str();
1061 if positional_only {
1062 extras.push(arg.to_string());
1063 continue;
1064 }
1065 match arg {
1066 "--" => positional_only = true,
1067 "--version" => {
1068 skip_usage_record();
1069 print_stdout(&version_line());
1070 return Err(0);
1071 }
1072 "-h" | "--help" => {
1073 skip_usage_record();
1074 print_stdout(&format!("usage: {prog} ..."));
1075 return Err(0);
1076 }
1077 "--json" => {
1078 if let Some(code) = mutex_check(prog, "--json", "--share", share) {
1079 return Err(code);
1080 }
1081 json = true;
1082 }
1083 "--share" => {
1084 if let Some(code) = mutex_check(prog, "--share", "--json", json) {
1085 return Err(code);
1086 }
1087 share = true;
1088 }
1089 other => extras.push(other.to_string()),
1090 }
1091 }
1092
1093 if !extras.is_empty() {
1094 return Err(unrecognized(&extras));
1095 }
1096 Ok((json, share))
1097}
1098
1099fn run_mcp_stats(rest: &[&String]) -> u8 {
1100 match parse_json_share_group("decided mcp-stats", rest) {
1101 Ok((json, share)) => cmd_mcp_stats(&McpStatsArgs { json, share }) as u8,
1102 Err(code) => code,
1103 }
1104}
1105
1106fn run_usage(rest: &[&String]) -> u8 {
1107 match parse_json_share_group("decided usage", rest) {
1108 Ok((json, share)) => cmd_usage(&UsageArgs { json, share }) as u8,
1109 Err(code) => code,
1110 }
1111}
1112
1113fn run_telemetry(rest: &[&String]) -> u8 {
1114 let prog = "decided telemetry";
1115 let mut action: Option<String> = None;
1116 let mut enterprise = false;
1117 let mut unlock = false;
1118 let mut extras: Vec<String> = Vec::new();
1119 let mut positional_only = false;
1120
1121 for arg in rest {
1122 let arg = arg.as_str();
1123 if positional_only || arg == "-" || !arg.starts_with('-') {
1124 if action.is_none() {
1125 if !matches!(arg, "on" | "off" | "status") {
1130 return argparse_error(
1131 prog,
1132 &format!(
1133 "argument action: invalid choice: '{arg}' (choose from 'on', 'off', 'status')"
1134 ),
1135 );
1136 }
1137 action = Some(arg.to_string());
1138 } else {
1139 extras.push(arg.to_string());
1142 }
1143 continue;
1144 }
1145 match arg {
1146 "--" => positional_only = true,
1147 "--version" => {
1148 skip_usage_record();
1149 print_stdout(&version_line());
1150 return 0;
1151 }
1152 "-h" | "--help" => {
1153 skip_usage_record();
1154 print_stdout(&format!("usage: {prog} ..."));
1155 return 0;
1156 }
1157 "--enterprise" => enterprise = true,
1158 "--unlock" => unlock = true,
1159 other => extras.push(other.to_string()),
1160 }
1161 }
1162
1163 if !extras.is_empty() {
1164 return unrecognized(&extras);
1165 }
1166
1167 cmd_telemetry(&TelemetryArgs {
1168 action: action.unwrap_or_else(|| "status".to_string()),
1169 enterprise,
1170 unlock,
1171 }) as u8
1172}
1173
1174fn run_skill(rest: &[&String]) -> u8 {
1180 let prog = "decided skill";
1181 let mut action: Option<String> = None;
1182 let mut name: Option<String> = None;
1183 let mut dir: String = ".".to_string();
1184 let mut json = false;
1185 let mut extras: Vec<String> = Vec::new();
1186 let mut positional_only = false;
1187
1188 let mut i = 0;
1189 while i < rest.len() {
1190 let arg = rest[i].as_str();
1191 if positional_only || arg == "-" || !arg.starts_with('-') {
1192 if action.is_none() {
1193 if !matches!(arg, "install" | "list") {
1194 return argparse_error(
1195 prog,
1196 &format!(
1197 "argument action: invalid choice: '{arg}' (choose from 'install', 'list')"
1198 ),
1199 );
1200 }
1201 action = Some(arg.to_string());
1202 } else if name.is_none() {
1203 name = Some(arg.to_string());
1204 } else {
1205 extras.push(arg.to_string());
1206 }
1207 i += 1;
1208 continue;
1209 }
1210 match arg {
1211 "--" => positional_only = true,
1212 "--version" => {
1213 skip_usage_record();
1214 print_stdout(&version_line());
1215 return 0;
1216 }
1217 "-h" | "--help" => {
1218 skip_usage_record();
1219 print_stdout(&format!("usage: {prog} ..."));
1220 return 0;
1221 }
1222 "--json" => json = true,
1223 other if other == "--dir" || other.starts_with("--dir=") => {
1224 match take_opt_value(prog, "--dir", other, rest, &mut i) {
1225 Ok(v) => dir = v,
1226 Err(code) => return code,
1227 }
1228 }
1229 other => extras.push(other.to_string()),
1230 }
1231 i += 1;
1232 }
1233
1234 let Some(action) = action else {
1235 return argparse_error(prog, "the following arguments are required: action");
1236 };
1237 if !extras.is_empty() {
1238 return unrecognized(&extras);
1239 }
1240
1241 cmd_skill(&SkillArgs {
1242 action,
1243 name,
1244 dir,
1245 json,
1246 }) as u8
1247}
1248
1249fn run_hook(rest: &[&String]) -> u8 {
1255 let prog = "decided hook";
1256 let mut action: Option<String> = None;
1257 let mut style: String = "post-commit".to_string(); let mut dir: String = ".".to_string();
1259 let mut json = false;
1260 let mut extras: Vec<String> = Vec::new();
1261 let mut positional_only = false;
1262
1263 let style_choice = |prog: &str, v: &str| -> Option<u8> {
1264 if matches!(v, "post-commit" | "pre-commit") {
1265 None
1266 } else {
1267 Some(argparse_error(
1268 prog,
1269 &format!(
1270 "argument --style: invalid choice: '{v}' (choose from 'post-commit', 'pre-commit')"
1271 ),
1272 ))
1273 }
1274 };
1275
1276 let mut i = 0;
1277 while i < rest.len() {
1278 let arg = rest[i].as_str();
1279 if positional_only || arg == "-" || !arg.starts_with('-') {
1280 if action.is_none() {
1281 if !matches!(arg, "install" | "list") {
1282 return argparse_error(
1283 prog,
1284 &format!(
1285 "argument action: invalid choice: '{arg}' (choose from 'install', 'list')"
1286 ),
1287 );
1288 }
1289 action = Some(arg.to_string());
1290 } else {
1291 extras.push(arg.to_string());
1292 }
1293 i += 1;
1294 continue;
1295 }
1296 match arg {
1297 "--" => positional_only = true,
1298 "--version" => {
1299 skip_usage_record();
1300 print_stdout(&version_line());
1301 return 0;
1302 }
1303 "-h" | "--help" => {
1304 skip_usage_record();
1305 print_stdout(&format!("usage: {prog} ..."));
1306 return 0;
1307 }
1308 "--json" => json = true,
1309 other if other == "--style" || other.starts_with("--style=") => {
1310 match take_opt_value(prog, "--style", other, rest, &mut i) {
1311 Ok(v) => {
1312 if let Some(code) = style_choice(prog, &v) {
1313 return code;
1314 }
1315 style = v;
1316 }
1317 Err(code) => return code,
1318 }
1319 }
1320 other if other == "--dir" || other.starts_with("--dir=") => {
1321 match take_opt_value(prog, "--dir", other, rest, &mut i) {
1322 Ok(v) => dir = v,
1323 Err(code) => return code,
1324 }
1325 }
1326 other => extras.push(other.to_string()),
1327 }
1328 i += 1;
1329 }
1330
1331 let Some(action) = action else {
1332 return argparse_error(prog, "the following arguments are required: action");
1333 };
1334 if !extras.is_empty() {
1335 return unrecognized(&extras);
1336 }
1337
1338 cmd_hook(&HookArgs {
1339 action,
1340 style,
1341 dir,
1342 json,
1343 }) as u8
1344}
1345
1346fn run_watchkeeper(rest: &[&String]) -> u8 {
1356 let prog = "decided watchkeeper";
1357 let mut directory: Option<String> = None;
1358 let mut base: String = "main".to_string();
1359 let mut head: Option<String> = None;
1360 let mut format: String = "human".to_string();
1361 let mut json = false;
1362 let mut fail_on: String = "error".to_string();
1363 let mut annotate = true;
1364 let mut extras: Vec<String> = Vec::new();
1365 let mut positional_only = false;
1366
1367 let format_choice = |v: &str| -> Option<u8> {
1368 if matches!(v, "human" | "json" | "github") {
1369 None
1370 } else {
1371 Some(argparse_error(
1372 prog,
1373 &format!(
1374 "argument --format: invalid choice: '{v}' (choose from 'human', 'json', 'github')"
1375 ),
1376 ))
1377 }
1378 };
1379 let fail_on_choice = |v: &str| -> Option<u8> {
1380 if matches!(v, "error" | "warning" | "none") {
1381 None
1382 } else {
1383 Some(argparse_error(
1384 prog,
1385 &format!(
1386 "argument --fail-on: invalid choice: '{v}' (choose from 'error', 'warning', 'none')"
1387 ),
1388 ))
1389 }
1390 };
1391
1392 let mut i = 0;
1393 while i < rest.len() {
1394 let arg = rest[i].as_str();
1395 if positional_only || arg == "-" || !arg.starts_with('-') {
1396 if directory.is_none() {
1397 directory = Some(arg.to_string());
1398 } else {
1399 extras.push(arg.to_string());
1400 }
1401 i += 1;
1402 continue;
1403 }
1404 match arg {
1405 "--" => positional_only = true,
1406 "--version" => {
1407 skip_usage_record();
1408 print_stdout(&version_line());
1409 return 0;
1410 }
1411 "-h" | "--help" => {
1412 skip_usage_record();
1413 print_stdout(&format!("usage: {prog} ..."));
1414 return 0;
1415 }
1416 "--json" => json = true,
1417 "--no-annotate" => annotate = false,
1418 other if other == "--base" || other.starts_with("--base=") => {
1419 match take_opt_value(prog, "--base", other, rest, &mut i) {
1420 Ok(v) => base = v,
1421 Err(code) => return code,
1422 }
1423 }
1424 other if other == "--head" || other.starts_with("--head=") => {
1425 match take_opt_value(prog, "--head", other, rest, &mut i) {
1426 Ok(v) => head = Some(v),
1427 Err(code) => return code,
1428 }
1429 }
1430 other if other == "--format" || other.starts_with("--format=") => {
1431 match take_opt_value(prog, "--format", other, rest, &mut i) {
1432 Ok(v) => {
1433 if let Some(code) = format_choice(&v) {
1434 return code;
1435 }
1436 format = v;
1437 }
1438 Err(code) => return code,
1439 }
1440 }
1441 other if other == "--fail-on" || other.starts_with("--fail-on=") => {
1442 match take_opt_value(prog, "--fail-on", other, rest, &mut i) {
1443 Ok(v) => {
1444 if let Some(code) = fail_on_choice(&v) {
1445 return code;
1446 }
1447 fail_on = v;
1448 }
1449 Err(code) => return code,
1450 }
1451 }
1452 other => extras.push(other.to_string()),
1453 }
1454 i += 1;
1455 }
1456
1457 if !extras.is_empty() {
1458 return unrecognized(&extras);
1459 }
1460
1461 cmd_watchkeeper(&WatchkeeperArgs {
1462 directory,
1463 base,
1464 head,
1465 format,
1466 json,
1467 fail_on,
1468 annotate,
1469 }) as u8
1470}
1471
1472fn run_eval(rest: &[&String]) -> u8 {
1477 let prog = "decided eval";
1478 let mut check = false;
1479 let mut update_baseline = false;
1480 let mut json = false;
1481 let mut root: String = "rust/fixtures/eval/corpus".to_string();
1482 let mut queries: String = "rust/fixtures/eval/queries.json".to_string();
1483 let mut baseline: String = "rust/fixtures/eval/baseline.json".to_string();
1484 let mut config: String = "rust/fixtures/eval/eval-config.json".to_string();
1485 let mut extras: Vec<String> = Vec::new();
1486 let mut positional_only = false;
1487
1488 let mut i = 0;
1489 while i < rest.len() {
1490 let arg = rest[i].as_str();
1491 if positional_only || arg == "-" || !arg.starts_with('-') {
1492 extras.push(arg.to_string());
1493 i += 1;
1494 continue;
1495 }
1496 match arg {
1497 "--" => positional_only = true,
1498 "--version" => {
1499 skip_usage_record();
1500 print_stdout(&version_line());
1501 return 0;
1502 }
1503 "-h" | "--help" => {
1504 skip_usage_record();
1505 print_stdout(&format!("usage: {prog} ..."));
1506 return 0;
1507 }
1508 "--check" => {
1509 if let Some(code) =
1510 mutex_check(prog, "--check", "--update-baseline", update_baseline)
1511 {
1512 return code;
1513 }
1514 check = true;
1515 }
1516 "--update-baseline" => {
1517 if let Some(code) = mutex_check(prog, "--update-baseline", "--check", check) {
1518 return code;
1519 }
1520 update_baseline = true;
1521 }
1522 "--json" => json = true,
1523 other if other == "--root" || other.starts_with("--root=") => {
1524 match take_opt_value(prog, "--root", other, rest, &mut i) {
1525 Ok(v) => root = v,
1526 Err(code) => return code,
1527 }
1528 }
1529 other if other == "--queries" || other.starts_with("--queries=") => {
1530 match take_opt_value(prog, "--queries", other, rest, &mut i) {
1531 Ok(v) => queries = v,
1532 Err(code) => return code,
1533 }
1534 }
1535 other if other == "--baseline" || other.starts_with("--baseline=") => {
1536 match take_opt_value(prog, "--baseline", other, rest, &mut i) {
1537 Ok(v) => baseline = v,
1538 Err(code) => return code,
1539 }
1540 }
1541 other if other == "--config" || other.starts_with("--config=") => {
1542 match take_opt_value(prog, "--config", other, rest, &mut i) {
1543 Ok(v) => config = v,
1544 Err(code) => return code,
1545 }
1546 }
1547 other => extras.push(other.to_string()),
1548 }
1549 i += 1;
1550 }
1551
1552 if !extras.is_empty() {
1553 return unrecognized(&extras);
1554 }
1555
1556 cmd_eval(&EvalArgs {
1557 check,
1558 update_baseline,
1559 json,
1560 root,
1561 queries,
1562 baseline,
1563 config,
1564 }) as u8
1565}
1566
1567fn run_resolve(rest: &[&String]) -> u8 {
1568 let prog = "decided resolve";
1569 let mut id: Option<String> = None;
1570 let mut directory: Option<String> = None;
1571 let mut json = false;
1572 let mut top_level = false;
1573 let mut extras: Vec<String> = Vec::new();
1574 let mut positional_only = false;
1575
1576 for arg in rest {
1577 let arg = arg.as_str();
1578 if positional_only || arg == "-" || !arg.starts_with('-') {
1579 if id.is_none() {
1580 id = Some(arg.to_string());
1581 } else if directory.is_none() {
1582 directory = Some(arg.to_string());
1583 } else {
1584 extras.push(arg.to_string());
1585 }
1586 continue;
1587 }
1588 match arg {
1589 "--" => positional_only = true,
1590 "--json" => json = true,
1591 "--top-level" => top_level = true,
1592 "--recursive" => {} other => extras.push(other.to_string()),
1594 }
1595 }
1596
1597 let Some(id) = id else {
1598 return argparse_error(prog, "the following arguments are required: id");
1599 };
1600 if !extras.is_empty() {
1601 return unrecognized(&extras);
1602 }
1603
1604 cmd_resolve(&ResolveArgs {
1605 id,
1606 directory: directory.unwrap_or_else(|| ".".to_string()),
1607 json,
1608 top_level,
1609 }) as u8
1610}
1611
1612fn run_find(rest: &[&String]) -> u8 {
1613 let prog = "decided find";
1614 let mut query: Option<String> = None;
1615 let mut directory: Option<String> = None;
1616 let mut artifact_type: Option<String> = None;
1617 let mut decisions = false;
1618 let mut tags: Vec<String> = Vec::new();
1619 let mut json = false;
1620 let mut explain = false;
1621 let mut top_level = false;
1622 let mut live = false;
1623 let mut cache = true;
1624 let mut verify = false;
1625 let mut extras: Vec<String> = Vec::new();
1626 let mut positional_only = false;
1627
1628 let mut i = 0;
1629 while i < rest.len() {
1630 let arg = rest[i].as_str();
1631 if positional_only || arg == "-" || !arg.starts_with('-') {
1632 if query.is_none() {
1633 query = Some(arg.to_string());
1634 } else if directory.is_none() {
1635 directory = Some(arg.to_string());
1636 } else {
1637 extras.push(arg.to_string());
1638 }
1639 i += 1;
1640 continue;
1641 }
1642 match arg {
1643 "--" => positional_only = true,
1644 "--json" => json = true,
1645 "--explain" => explain = true,
1646 "--top-level" => top_level = true,
1647 "--live" => live = true, "--recursive" => {} "--cache" => cache = true,
1650 "--no-cache" => cache = false,
1651 "--verify" => verify = true,
1652 "--decisions" => {
1653 if let Some(code) =
1655 mutex_check(prog, "--decisions", "--type", artifact_type.is_some())
1656 {
1657 return code;
1658 }
1659 decisions = true;
1660 }
1661 other if other == "--type" || other.starts_with("--type=") => {
1662 if let Some(code) = mutex_check(prog, "--type", "--decisions", decisions) {
1663 return code;
1664 }
1665 match take_opt_value(prog, "--type", other, rest, &mut i) {
1666 Ok(v) => artifact_type = Some(v),
1667 Err(code) => return code,
1668 }
1669 }
1670 other if other == "--tag" || other.starts_with("--tag=") => {
1671 match take_opt_value(prog, "--tag", other, rest, &mut i) {
1672 Ok(v) => tags.push(v),
1673 Err(code) => return code,
1674 }
1675 }
1676 other => extras.push(other.to_string()),
1677 }
1678 i += 1;
1679 }
1680
1681 let Some(query) = query else {
1682 return argparse_error(prog, "the following arguments are required: query");
1683 };
1684 if !extras.is_empty() {
1685 return unrecognized(&extras);
1686 }
1687
1688 cmd_find(&FindArgs {
1689 query,
1690 directory: directory.unwrap_or_else(|| ".".to_string()),
1691 artifact_type,
1692 decisions,
1693 tags,
1694 json,
1695 explain,
1696 top_level,
1697 live,
1698 cache,
1699 verify,
1700 }) as u8
1701}
1702
1703fn py_parse_int(value: &str) -> Option<i64> {
1707 let text = crate::pycompat::py_strip(value);
1708 let (neg, digits) = match text.strip_prefix('-') {
1709 Some(rest) => (true, rest),
1710 None => (false, text.strip_prefix('+').unwrap_or(text)),
1711 };
1712 if digits.is_empty() {
1713 return None;
1714 }
1715 let bytes = digits.as_bytes();
1716 if bytes[0] == b'_' || bytes[bytes.len() - 1] == b'_' {
1717 return None;
1718 }
1719 let mut out: i64 = 0;
1720 let mut prev_underscore = false;
1721 for &b in bytes {
1722 if b == b'_' {
1723 if prev_underscore {
1724 return None;
1725 }
1726 prev_underscore = true;
1727 continue;
1728 }
1729 prev_underscore = false;
1730 if !b.is_ascii_digit() {
1731 return None;
1732 }
1733 out = out
1734 .saturating_mul(10)
1735 .saturating_add(i64::from(b - b'0'));
1736 }
1737 Some(if neg { -out } else { out })
1738}
1739
1740fn run_retrieve(rest: &[&String]) -> u8 {
1741 let prog = "decided retrieve";
1742 let mut task: Option<String> = None;
1743 let mut directory: Option<String> = None;
1744 let mut scope: Option<String> = None;
1745 let mut top_k: i64 = 5;
1746 let mut budget: i64 = 10_000;
1747 let mut live = false;
1748 let mut all = false;
1749 let mut json = false;
1750 let mut extras: Vec<String> = Vec::new();
1751 let mut positional_only = false;
1752
1753 enum IntErr {
1755 Missing,
1756 Invalid(String),
1757 }
1758 let parse_int_flag = |raw: Option<&&String>| -> Result<i64, IntErr> {
1759 match raw {
1760 Some(v)
1761 if !v.starts_with('-')
1762 || v.as_str() == "-"
1763 || looks_like_negative_number(v) =>
1764 {
1765 py_parse_int(v).ok_or_else(|| IntErr::Invalid(v.to_string()))
1766 }
1767 _ => Err(IntErr::Missing),
1768 }
1769 };
1770 let int_flag_error = |flag: &str, err: IntErr| -> u8 {
1771 match err {
1772 IntErr::Missing => {
1773 argparse_error(prog, &format!("argument {flag}: expected one argument"))
1774 }
1775 IntErr::Invalid(v) => argparse_error(
1776 prog,
1777 &format!("argument {flag}: invalid int value: '{v}'"),
1778 ),
1779 }
1780 };
1781
1782 let mut i = 0;
1783 while i < rest.len() {
1784 let arg = rest[i].as_str();
1785 if positional_only || !arg.starts_with('-') || arg == "-" || looks_like_negative_number(arg)
1786 {
1787 if task.is_none() {
1788 task = Some(arg.to_string());
1789 } else if directory.is_none() {
1790 directory = Some(arg.to_string());
1791 } else {
1792 extras.push(arg.to_string());
1793 }
1794 i += 1;
1795 continue;
1796 }
1797 match arg {
1798 "--" => positional_only = true,
1799 "--json" => json = true,
1800 "--live" => {
1801 if let Some(code) = mutex_check(prog, "--live", "--all", all) {
1802 return code;
1803 }
1804 live = true;
1805 }
1806 "--all" => {
1807 if let Some(code) = mutex_check(prog, "--all", "--live", live) {
1808 return code;
1809 }
1810 all = true;
1811 }
1812 other if other == "--scope" || other.starts_with("--scope=") => {
1813 match take_opt_value(prog, "--scope", other, rest, &mut i) {
1814 Ok(v) => scope = Some(v),
1815 Err(code) => return code,
1816 }
1817 }
1818 "--top-k" => {
1819 i += 1;
1820 match parse_int_flag(rest.get(i)) {
1821 Ok(v) => top_k = v,
1822 Err(e) => return int_flag_error("--top-k", e),
1823 }
1824 }
1825 other if other.starts_with("--top-k=") => {
1826 let v = &other["--top-k=".len()..];
1827 match py_parse_int(v) {
1828 Some(parsed) => top_k = parsed,
1829 None => return int_flag_error("--top-k", IntErr::Invalid(v.to_string())),
1830 }
1831 }
1832 "--budget" => {
1833 i += 1;
1834 match parse_int_flag(rest.get(i)) {
1835 Ok(v) => budget = v,
1836 Err(e) => return int_flag_error("--budget", e),
1837 }
1838 }
1839 other if other.starts_with("--budget=") => {
1840 let v = &other["--budget=".len()..];
1841 match py_parse_int(v) {
1842 Some(parsed) => budget = parsed,
1843 None => return int_flag_error("--budget", IntErr::Invalid(v.to_string())),
1844 }
1845 }
1846 other => extras.push(other.to_string()),
1847 }
1848 i += 1;
1849 }
1850
1851 let Some(task) = task else {
1852 return argparse_error(prog, "the following arguments are required: task");
1853 };
1854 if !extras.is_empty() {
1855 return unrecognized(&extras);
1856 }
1857
1858 cmd_retrieve(&RetrieveArgs {
1859 task,
1860 directory: directory.unwrap_or_else(|| ".".to_string()),
1861 scope,
1862 top_k,
1863 budget,
1864 all,
1865 json,
1866 }) as u8
1867}
1868
1869fn looks_like_negative_number(s: &str) -> bool {
1872 let Some(rest) = s.strip_prefix('-') else {
1873 return false;
1874 };
1875 if rest.is_empty() {
1876 return false;
1877 }
1878 let mut seen_dot = false;
1879 let mut seen_digit = false;
1880 for ch in rest.chars() {
1881 if ch == '.' {
1882 if seen_dot {
1883 return false;
1884 }
1885 seen_dot = true;
1886 } else if ch.is_ascii_digit() {
1887 seen_digit = true;
1888 } else {
1889 return false;
1890 }
1891 }
1892 seen_digit
1893}
1894
1895fn run_review(rest: &[&String]) -> u8 {
1896 let prog = "decided review";
1897 let mut directory: Option<String> = None;
1898 let mut json = false;
1899 let mut sarif = false;
1900 let mut top_level = false;
1901 let mut stale_after: Option<i64> = None;
1902 let mut extras: Vec<String> = Vec::new();
1903 let mut positional_only = false;
1904
1905 let mut i = 0;
1906 while i < rest.len() {
1907 let arg = rest[i].as_str();
1908 if positional_only || !arg.starts_with('-') || arg == "-" {
1909 if directory.is_none() {
1910 directory = Some(arg.to_string());
1911 } else {
1912 extras.push(arg.to_string());
1913 }
1914 i += 1;
1915 continue;
1916 }
1917 match arg {
1918 "--" => positional_only = true,
1919 "--json" => json = true,
1920 "--sarif" => sarif = true,
1921 "--top-level" => top_level = true,
1922 "--recursive" => {}
1923 "--stale-after" => {
1924 let consume = match rest.get(i + 1) {
1927 Some(v) => !v.starts_with('-') || looks_like_negative_number(v),
1928 None => false,
1929 };
1930 if consume {
1931 i += 1;
1932 let raw = rest[i].as_str();
1933 match raw.trim().parse::<i64>() {
1934 Ok(v) => stale_after = Some(v),
1935 Err(_) => {
1936 return argparse_error(
1937 prog,
1938 &format!("argument --stale-after: invalid int value: '{raw}'"),
1939 )
1940 }
1941 }
1942 } else {
1943 stale_after = Some(14);
1944 }
1945 }
1946 other if other.starts_with("--stale-after=") => {
1947 let raw = &other["--stale-after=".len()..];
1948 match raw.trim().parse::<i64>() {
1949 Ok(v) => stale_after = Some(v),
1950 Err(_) => {
1951 return argparse_error(
1952 prog,
1953 &format!("argument --stale-after: invalid int value: '{raw}'"),
1954 )
1955 }
1956 }
1957 }
1958 other => extras.push(other.to_string()),
1959 }
1960 i += 1;
1961 }
1962
1963 let Some(directory) = directory else {
1964 return argparse_error(prog, "the following arguments are required: directory");
1965 };
1966 if !extras.is_empty() {
1967 return unrecognized(&extras);
1968 }
1969
1970 cmd_review(&ReviewArgs {
1971 directory,
1972 json,
1973 sarif,
1974 top_level,
1975 stale_after,
1976 }) as u8
1977}
1978
1979fn run_export(rest: &[&String]) -> u8 {
1980 let prog = "decided export";
1981 let mut directory: Option<String> = None;
1982 let mut json = false;
1983 let mut html = false;
1984 let mut okf = false;
1985 let mut documents = false;
1986 let mut graph = false;
1987 let mut agent_rules = false;
1988 let mut check = false;
1989 let mut client: Vec<String> = Vec::new();
1990 let mut out: Option<String> = None;
1991 let mut extras: Vec<String> = Vec::new();
1992 let mut positional_only = false;
1993
1994 let mut last_mode: Option<&'static str> = None;
1996 let set_mode = |flag: &'static str,
1997 slot: &mut bool,
1998 last_mode: &mut Option<&'static str>|
1999 -> Result<(), FlagError> {
2000 if let Some(prev) = *last_mode {
2001 if prev != flag {
2002 return Err(FlagError(argparse_error(
2003 prog,
2004 &format!("argument {flag}: not allowed with argument {prev}"),
2005 )));
2006 }
2007 }
2008 *slot = true;
2009 *last_mode = Some(flag);
2010 Ok(())
2011 };
2012
2013 let mut i = 0;
2014 while i < rest.len() {
2015 let arg = rest[i].as_str();
2016 if positional_only || !arg.starts_with('-') || arg == "-" {
2017 if directory.is_none() {
2018 directory = Some(arg.to_string());
2019 } else {
2020 extras.push(arg.to_string());
2021 }
2022 i += 1;
2023 continue;
2024 }
2025 match arg {
2026 "--" => positional_only = true,
2027 "--json" => json = true,
2028 "--html" => {
2029 if let Err(FlagError(c)) = set_mode("--html", &mut html, &mut last_mode) {
2030 return c;
2031 }
2032 }
2033 "--okf" => {
2034 if let Err(FlagError(c)) = set_mode("--okf", &mut okf, &mut last_mode) {
2035 return c;
2036 }
2037 }
2038 "--documents" => {
2039 if let Err(FlagError(c)) = set_mode("--documents", &mut documents, &mut last_mode) {
2040 return c;
2041 }
2042 }
2043 "--graph" => {
2044 if let Err(FlagError(c)) = set_mode("--graph", &mut graph, &mut last_mode) {
2045 return c;
2046 }
2047 }
2048 "--agent-rules" => {
2049 if let Err(FlagError(c)) =
2050 set_mode("--agent-rules", &mut agent_rules, &mut last_mode)
2051 {
2052 return c;
2053 }
2054 }
2055 "--check" => check = true,
2056 "--client" => {
2057 i += 1;
2058 match rest.get(i) {
2059 Some(v) if is_client_choice(v) => client.push(v.to_string()),
2060 Some(v) if !v.starts_with('-') => {
2061 return argparse_error(
2062 prog,
2063 &format!(
2064 "argument --client: invalid choice: '{v}' (choose from 'claude', 'agents', 'cursor', 'copilot')"
2065 ),
2066 )
2067 }
2068 _ => return argparse_error(prog, "argument --client: expected one argument"),
2069 }
2070 }
2071 other if other.starts_with("--client=") => {
2072 let v = &other["--client=".len()..];
2073 if is_client_choice(v) {
2074 client.push(v.to_string());
2075 } else {
2076 return argparse_error(
2077 prog,
2078 &format!(
2079 "argument --client: invalid choice: '{v}' (choose from 'claude', 'agents', 'cursor', 'copilot')"
2080 ),
2081 );
2082 }
2083 }
2084 other if other == "--out" || other.starts_with("--out=") => {
2085 match take_opt_value(prog, "--out", other, rest, &mut i) {
2086 Ok(v) => out = Some(v),
2087 Err(code) => return code,
2088 }
2089 }
2090 other => extras.push(other.to_string()),
2091 }
2092 i += 1;
2093 }
2094
2095 if !extras.is_empty() {
2096 return unrecognized(&extras);
2097 }
2098
2099 cmd_export(&ExportArgs {
2100 directory: directory.unwrap_or_else(|| ".".to_string()),
2101 json,
2102 graph,
2103 documents,
2104 html,
2105 okf,
2106 agent_rules,
2107 check,
2108 client,
2109 out,
2110 }) as u8
2111}
2112
2113fn is_client_choice(v: &str) -> bool {
2114 matches!(v, "claude" | "agents" | "cursor" | "copilot")
2115}
2116
2117fn run_schema(rest: &[&String]) -> u8 {
2118 let prog = "decided schema";
2119 let mut schema: Option<String> = None;
2120 let mut list = false;
2121 let mut json = false;
2122 let mut template = false;
2123 let mut extras: Vec<String> = Vec::new();
2124 let mut positional_only = false;
2125
2126 for arg in rest {
2127 let arg = arg.as_str();
2128 if positional_only || !arg.starts_with('-') {
2129 if schema.is_none() {
2130 schema = Some(arg.to_string());
2131 } else {
2132 extras.push(arg.to_string());
2133 }
2134 continue;
2135 }
2136 match arg {
2137 "--" => positional_only = true,
2138 "--list" => list = true,
2139 "--json" => {
2140 if let Some(code) = mutex_check(prog, "--json", "--template", template) {
2141 return code;
2142 }
2143 json = true;
2144 }
2145 "--template" => {
2146 if let Some(code) = mutex_check(prog, "--template", "--json", json) {
2147 return code;
2148 }
2149 template = true;
2150 }
2151 other => extras.push(other.to_string()),
2152 }
2153 }
2154
2155 if !extras.is_empty() {
2156 return unrecognized(&extras);
2157 }
2158
2159 cmd_schema(&SchemaArgs {
2160 schema,
2161 list,
2162 json,
2163 template,
2164 }) as u8
2165}
2166
2167fn run_templates(rest: &[&String]) -> u8 {
2168 let mut json = false;
2169 let mut extras: Vec<String> = Vec::new();
2170 let mut positional_only = false;
2171
2172 for arg in rest {
2173 let arg = arg.as_str();
2174 if positional_only || !arg.starts_with('-') {
2175 extras.push(arg.to_string());
2176 continue;
2177 }
2178 match arg {
2179 "--" => positional_only = true,
2180 "--json" => json = true,
2181 other => extras.push(other.to_string()),
2182 }
2183 }
2184
2185 if !extras.is_empty() {
2186 return unrecognized(&extras);
2187 }
2188
2189 cmd_templates(&TemplatesArgs { json }) as u8
2190}
2191
2192fn run_new(rest: &[&String]) -> u8 {
2193 let prog = "decided new";
2194 let mut artifact_type: Option<String> = None;
2195 let mut output_path: Option<String> = None;
2196 let mut json = false;
2197 let mut extras: Vec<String> = Vec::new();
2198 let mut positional_only = false;
2199
2200 for arg in rest {
2201 let arg = arg.as_str();
2202 if positional_only || arg == "-" || !arg.starts_with('-') {
2203 if artifact_type.is_none() {
2204 artifact_type = Some(arg.to_string());
2205 } else if output_path.is_none() {
2206 output_path = Some(arg.to_string());
2207 } else {
2208 extras.push(arg.to_string());
2209 }
2210 continue;
2211 }
2212 match arg {
2213 "--" => positional_only = true,
2214 "--json" => json = true,
2215 other => extras.push(other.to_string()),
2216 }
2217 }
2218
2219 let (Some(artifact_type), Some(output_path)) = (artifact_type.clone(), output_path) else {
2221 let missing = if artifact_type.is_none() {
2222 "type, output_path"
2223 } else {
2224 "output_path"
2225 };
2226 return argparse_error(
2227 prog,
2228 &format!("the following arguments are required: {missing}"),
2229 );
2230 };
2231 if !extras.is_empty() {
2232 return unrecognized(&extras);
2233 }
2234
2235 cmd_new(&NewArgs {
2236 artifact_type,
2237 output_path,
2238 json,
2239 }) as u8
2240}
2241
2242fn run_init(rest: &[&String]) -> u8 {
2248 let prog = "decided init";
2249 let mut directory: Option<String> = None;
2250 let mut key: String = "RAC".to_string(); let mut ticketing: Option<String> = None;
2252 let mut profile: Option<String> = None;
2253 let mut org_endpoint: Option<String> = None;
2254 let mut json = false;
2255 let mut extras: Vec<String> = Vec::new();
2256 let mut positional_only = false;
2257
2258 let ticketing_choice = |v: &str| -> Option<u8> {
2259 if matches!(v, "jira" | "github" | "linear" | "azure-devops" | "servicenow" | "none") {
2260 None
2261 } else {
2262 Some(argparse_error(
2263 prog,
2264 &format!(
2265 "argument --ticketing: invalid choice: '{v}' (choose from 'jira', 'github', 'linear', 'azure-devops', 'servicenow', 'none')"
2266 ),
2267 ))
2268 }
2269 };
2270 let profile_choice = |v: &str| -> Option<u8> {
2271 if matches!(v, "default" | "enterprise") {
2272 None
2273 } else {
2274 Some(argparse_error(
2275 prog,
2276 &format!(
2277 "argument --profile: invalid choice: '{v}' (choose from 'default', 'enterprise')"
2278 ),
2279 ))
2280 }
2281 };
2282
2283 let mut i = 0;
2284 while i < rest.len() {
2285 let arg = rest[i].as_str();
2286 if positional_only || arg == "-" || !arg.starts_with('-') {
2287 if directory.is_none() {
2288 directory = Some(arg.to_string());
2289 } else {
2290 extras.push(arg.to_string());
2291 }
2292 i += 1;
2293 continue;
2294 }
2295 match arg {
2296 "--" => positional_only = true,
2297 "--version" => {
2298 skip_usage_record();
2299 print_stdout(&version_line());
2300 return 0;
2301 }
2302 "-h" | "--help" => {
2303 skip_usage_record();
2304 print_stdout(&format!("usage: {prog} ..."));
2305 return 0;
2306 }
2307 "--json" => json = true,
2308 other if other == "--key" || other.starts_with("--key=") => {
2309 match take_opt_value(prog, "--key", other, rest, &mut i) {
2310 Ok(v) => key = v,
2311 Err(code) => return code,
2312 }
2313 }
2314 other if other == "--ticketing" || other.starts_with("--ticketing=") => {
2315 match take_opt_value(prog, "--ticketing", other, rest, &mut i) {
2316 Ok(v) => {
2317 if let Some(code) = ticketing_choice(&v) {
2318 return code;
2319 }
2320 ticketing = Some(v);
2321 }
2322 Err(code) => return code,
2323 }
2324 }
2325 other if other == "--profile" || other.starts_with("--profile=") => {
2326 match take_opt_value(prog, "--profile", other, rest, &mut i) {
2327 Ok(v) => {
2328 if let Some(code) = profile_choice(&v) {
2329 return code;
2330 }
2331 profile = Some(v);
2332 }
2333 Err(code) => return code,
2334 }
2335 }
2336 other if other == "--org-endpoint" || other.starts_with("--org-endpoint=") => {
2337 match take_opt_value(prog, "--org-endpoint", other, rest, &mut i) {
2340 Ok(v) => org_endpoint = Some(v),
2341 Err(code) => return code,
2342 }
2343 }
2344 other => extras.push(other.to_string()),
2345 }
2346 i += 1;
2347 }
2348
2349 if !extras.is_empty() {
2350 return unrecognized(&extras);
2351 }
2352
2353 cmd_init(&InitArgs {
2354 directory: directory.unwrap_or_else(|| ".".to_string()),
2355 key,
2356 ticketing,
2357 profile,
2358 org_endpoint,
2359 json,
2360 }) as u8
2361}
2362
2363fn run_quickstart(rest: &[&String]) -> u8 {
2369 let prog = "decided quickstart";
2370 let mut directory: Option<String> = None;
2371 let mut key: String = "RAC".to_string(); let mut artifact_type: String = "requirement".to_string(); let mut json = false;
2374 let mut extras: Vec<String> = Vec::new();
2375 let mut positional_only = false;
2376
2377 let mut i = 0;
2378 while i < rest.len() {
2379 let arg = rest[i].as_str();
2380 if positional_only || arg == "-" || !arg.starts_with('-') {
2381 if directory.is_none() {
2382 directory = Some(arg.to_string());
2383 } else {
2384 extras.push(arg.to_string());
2385 }
2386 i += 1;
2387 continue;
2388 }
2389 match arg {
2390 "--" => positional_only = true,
2391 "--version" => {
2392 skip_usage_record();
2393 print_stdout(&version_line());
2394 return 0;
2395 }
2396 "-h" | "--help" => {
2397 skip_usage_record();
2398 print_stdout(&format!("usage: {prog} ..."));
2399 return 0;
2400 }
2401 "--json" => json = true,
2402 other if other == "--key" || other.starts_with("--key=") => {
2403 match take_opt_value(prog, "--key", other, rest, &mut i) {
2404 Ok(v) => key = v,
2405 Err(code) => return code,
2406 }
2407 }
2408 other if other == "--type" || other.starts_with("--type=") => {
2409 match take_opt_value(prog, "--type", other, rest, &mut i) {
2410 Ok(v) => artifact_type = v,
2411 Err(code) => return code,
2412 }
2413 }
2414 other => extras.push(other.to_string()),
2415 }
2416 i += 1;
2417 }
2418
2419 if !extras.is_empty() {
2420 return unrecognized(&extras);
2421 }
2422
2423 cmd_quickstart(&QuickstartArgs {
2424 directory: directory.unwrap_or_else(|| ".".to_string()),
2425 key,
2426 artifact_type,
2427 json,
2428 }) as u8
2429}
2430
2431fn run_rename(rest: &[&String]) -> u8 {
2432 let prog = "decided rename";
2433 let mut old: Option<String> = None;
2434 let mut new: Option<String> = None;
2435 let mut directory: Option<String> = None;
2436 let mut apply = false;
2437 let mut top_level = false;
2438 let mut json = false;
2439 let mut extras: Vec<String> = Vec::new();
2440 let mut positional_only = false;
2441
2442 for arg in rest {
2443 let arg = arg.as_str();
2444 if positional_only || arg == "-" || !arg.starts_with('-') {
2445 if old.is_none() {
2446 old = Some(arg.to_string());
2447 } else if new.is_none() {
2448 new = Some(arg.to_string());
2449 } else if directory.is_none() {
2450 directory = Some(arg.to_string());
2451 } else {
2452 extras.push(arg.to_string());
2453 }
2454 continue;
2455 }
2456 match arg {
2457 "--" => positional_only = true,
2458 "--apply" => apply = true,
2459 "--top-level" => top_level = true,
2460 "--json" => json = true,
2461 other => extras.push(other.to_string()),
2462 }
2463 }
2464
2465 let (Some(old), Some(new), Some(directory)) = (old.clone(), new.clone(), directory) else {
2468 let mut missing: Vec<&str> = Vec::new();
2469 if old.is_none() {
2470 missing.push("old");
2471 }
2472 if new.is_none() {
2473 missing.push("new");
2474 }
2475 missing.push("directory");
2476 return argparse_error(
2477 prog,
2478 &format!("the following arguments are required: {}", missing.join(", ")),
2479 );
2480 };
2481 if !extras.is_empty() {
2482 return unrecognized(&extras);
2483 }
2484
2485 cmd_rename(&RenameArgs {
2486 old,
2487 new,
2488 directory,
2489 apply,
2490 top_level,
2491 json,
2492 }) as u8
2493}
2494
2495fn run_migrate(rest: &[&String]) -> u8 {
2500 let prog = "decided migrate";
2501 let mut target: Option<String> = None;
2502 let mut directory: Option<String> = None;
2503 let mut dry_run = false;
2504 let mut top_level = false;
2505 let mut json = false;
2506 let mut extras: Vec<String> = Vec::new();
2507 let mut positional_only = false;
2508
2509 for arg in rest {
2510 let arg = arg.as_str();
2511 if positional_only || arg == "-" || !arg.starts_with('-') {
2512 if target.is_none() {
2513 if arg != "metadata" && arg != "layout" {
2514 return argparse_error(
2515 prog,
2516 &format!(
2517 "argument target: invalid choice: '{arg}' (choose from 'metadata', 'layout')"
2518 ),
2519 );
2520 }
2521 target = Some(arg.to_string());
2522 } else if directory.is_none() {
2523 directory = Some(arg.to_string());
2524 } else {
2525 extras.push(arg.to_string());
2526 }
2527 continue;
2528 }
2529 match arg {
2530 "--" => positional_only = true,
2531 "--version" => {
2532 skip_usage_record();
2533 print_stdout(&version_line());
2534 return 0;
2535 }
2536 "-h" | "--help" => {
2537 skip_usage_record();
2538 print_stdout(&format!("usage: {prog} ..."));
2539 return 0;
2540 }
2541 "--dry-run" => dry_run = true,
2542 "--top-level" => top_level = true,
2543 "--recursive" => {} "--json" => json = true,
2545 other => extras.push(other.to_string()),
2546 }
2547 }
2548
2549 let (Some(target), Some(directory)) = (target.clone(), directory) else {
2550 let missing = if target.is_none() {
2551 "target, directory"
2552 } else {
2553 "directory"
2554 };
2555 return argparse_error(
2556 prog,
2557 &format!("the following arguments are required: {missing}"),
2558 );
2559 };
2560 if !extras.is_empty() {
2561 return unrecognized(&extras);
2562 }
2563
2564 cmd_migrate(&MigrateArgs {
2565 target,
2566 directory,
2567 dry_run,
2568 top_level,
2569 json,
2570 }) as u8
2571}