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