1pub mod adapter;
2pub mod cli;
3pub mod commands;
4mod completion;
5pub mod dispatch_record;
6mod execute;
7mod forge_cli_adapter;
8pub mod issue_body;
9pub mod lifecycle_lock;
10pub mod lifecycle_record;
11pub mod lifecycle_vnext;
12pub mod output;
13mod provider;
14pub mod render;
15pub mod runtime_layout;
16pub mod state;
17pub mod task_spec;
18pub mod tracking;
19
20use std::ffi::OsString;
21
22use clap::{CommandFactory, FromArgMatches};
23use nils_common::cli_contract::exit;
24use serde_json::json;
25
26use crate::cli::Cli;
27use crate::commands::Command;
28
29pub const EXIT_SUCCESS: i32 = exit::SUCCESS;
30pub const EXIT_FAILURE: i32 = exit::RUNTIME;
31pub const EXIT_USAGE: i32 = exit::USAGE;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum BinaryFlavor {
35 PlanIssue,
36 PlanIssueLocal,
37}
38
39impl BinaryFlavor {
40 pub fn binary_name(self) -> &'static str {
41 match self {
42 Self::PlanIssue => "plan-issue",
43 Self::PlanIssueLocal => "plan-issue-local",
44 }
45 }
46
47 pub fn execution_mode(self) -> &'static str {
48 match self {
49 Self::PlanIssue => "live",
50 Self::PlanIssueLocal => "local",
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ValidationError {
57 pub code: &'static str,
58 pub message: String,
59}
60
61impl ValidationError {
62 pub fn new(code: &'static str, message: impl Into<String>) -> Self {
63 Self {
64 code,
65 message: message.into(),
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct CommandError {
72 pub code: &'static str,
73 pub message: String,
74 pub exit_code: i32,
75}
76
77impl CommandError {
78 pub fn new(code: &'static str, message: impl Into<String>, exit_code: i32) -> Self {
79 Self {
80 code,
81 message: message.into(),
82 exit_code,
83 }
84 }
85
86 pub fn runtime(code: &'static str, message: impl Into<String>) -> Self {
87 Self::new(code, message, EXIT_FAILURE)
88 }
89
90 pub fn usage(code: &'static str, message: impl Into<String>) -> Self {
91 Self::new(code, message, EXIT_USAGE)
92 }
93}
94
95pub fn run(binary: BinaryFlavor) -> i32 {
96 run_with_args(binary, std::env::args_os())
97}
98
99pub fn run_with_args<I, T>(binary: BinaryFlavor, args: I) -> i32
100where
101 I: IntoIterator<Item = T>,
102 T: Into<OsString> + Clone,
103{
104 let command = Cli::command().name(binary.binary_name());
105 let matches = match command.try_get_matches_from(args) {
106 Ok(matches) => matches,
107 Err(err) => {
108 let code = if err.use_stderr() {
109 EXIT_USAGE
110 } else {
111 EXIT_SUCCESS
112 };
113 let _ = err.print();
114 return code;
115 }
116 };
117 let cli = match Cli::from_arg_matches(&matches) {
118 Ok(cli) => cli,
119 Err(err) => {
120 let _ = err.print();
121 return EXIT_USAGE;
122 }
123 };
124
125 crate::state::set_state_dir_override(cli.state_dir.clone());
126
127 if let Command::Completion(args) = &cli.command {
128 return completion::run(binary, args.shell);
129 }
130
131 let output_format = match cli.resolve_output_format() {
132 Ok(format) => format,
133 Err(err) => {
134 eprintln!("error: {}", err.message);
135 return EXIT_USAGE;
136 }
137 };
138
139 if let Command::ResolveApproval(args) = &cli.command
144 && matches!(output_format, crate::cli::OutputFormat::Text)
145 {
146 return execute::run_resolve_approval_text(binary, cli.repo.as_deref(), args);
147 }
148
149 if let Err(err) = cli.validate() {
150 let schema_version = cli.command.schema_version();
151 if let Err(render_err) = output::emit_error(
152 output_format,
153 &schema_version,
154 cli.command.command_id(),
155 err.code,
156 &err.message,
157 ) {
158 eprintln!("error: {render_err}");
159 }
160 return EXIT_FAILURE;
161 }
162
163 let execution_result = match execute::execute(binary, &cli) {
164 Ok(result) => result,
165 Err(err) => {
166 let schema_version = cli.command.schema_version();
167 if let Err(render_err) = output::emit_error(
168 output_format,
169 &schema_version,
170 cli.command.command_id(),
171 err.code,
172 &err.message,
173 ) {
174 eprintln!("error: {render_err}");
175 }
176 return err.exit_code;
177 }
178 };
179
180 let schema_version = cli.command.schema_version();
181 let payload = json!({
182 "binary": binary.binary_name(),
183 "execution_mode": binary.execution_mode(),
184 "dry_run": cli.dry_run,
185 "repo": cli.repo,
186 "arguments": cli.command.payload(),
187 "result": execution_result,
188 });
189
190 if let Err(err) = output::emit_success(
191 output_format,
192 &schema_version,
193 cli.command.command_id(),
194 &payload,
195 ) {
196 eprintln!("error: {err}");
197 return EXIT_FAILURE;
198 }
199
200 EXIT_SUCCESS
201}