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