Skip to main content

plan_issue/
lib.rs

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    // Task 1.5: `resolve-approval` text mode prints just the URL (or fails
139    // with a clear stderr message naming the count). JSON mode falls
140    // through to the standard envelope so consumers can read the candidate
141    // array.
142    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}