Skip to main content

plan_issue/
lib.rs

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