Skip to content

Commit 374f202

Browse files
committed
feat: CLI 출력 포맷 추가
1 parent 18cb510 commit 374f202

8 files changed

Lines changed: 1229 additions & 60 deletions

File tree

bin/maximus.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ function parseCompatInvocation(args) {
209209

210210
let pathArg;
211211
const unsupportedFlags = [];
212-
const valueFlags = new Set(["--only", "--skip", "--fail-on", "--fix-id", "--fix-prefix"]);
212+
const valueFlags = new Set(["--only", "--skip", "--fail-on", "--fix-id", "--fix-prefix", "--format"]);
213213
const passthroughFlags = new Set(["--dry-run", "--json"]);
214214
const isFixCommand = command === "fix";
215215
const hasDryRun = rest.includes("--dry-run");
@@ -359,7 +359,7 @@ function formatCompatHelp() {
359359
" maximus fix [path] --dry-run [--json]",
360360
" maximus help",
361361
"",
362-
"Rust is the canonical Maximus runtime. When no Rust runtime is available, the bundled JS compatibility path stays as frozen reference-only fallback for legacy-compatible commands without Maximus config files or Rust-only flags. `--only`, `--skip`, `--fail-on`, `--diff`, `--fix-id`, and `--fix-prefix` require the Rust runtime, and `fix` is only available with `--dry-run`.",
362+
"Rust is the canonical Maximus runtime. When no Rust runtime is available, the bundled JS compatibility path stays as frozen reference-only fallback for legacy-compatible commands without Maximus config files or Rust-only flags. `--only`, `--skip`, `--fail-on`, `--diff`, `--fix-id`, `--fix-prefix`, and `--format` require the Rust runtime, and `fix` is only available with `--dry-run`.",
363363
].join("\n");
364364
}
365365

crates/maximus-cli/src/args.rs

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,25 @@ pub struct Flags {
99
pub fix_ids: Vec<String>,
1010
pub fix_prefixes: Vec<String>,
1111
pub help: bool,
12-
pub json: bool,
12+
pub output_format: OutputFormat,
1313
pub only_checks: Option<Vec<String>>,
1414
pub skip_checks: Option<Vec<String>>,
1515
}
1616

17+
#[derive(Debug, Clone, PartialEq, Eq)]
18+
pub enum OutputFormat {
19+
Text,
20+
Json,
21+
Markdown,
22+
Sarif,
23+
}
24+
25+
impl Default for OutputFormat {
26+
fn default() -> Self {
27+
Self::Text
28+
}
29+
}
30+
1731
#[derive(Debug, Clone, PartialEq, Eq, Default)]
1832
pub struct ParsedArgs {
1933
pub command: Option<String>,
@@ -24,13 +38,23 @@ pub struct ParsedArgs {
2438
#[derive(Debug, Clone, PartialEq, Eq)]
2539
pub enum ArgsError {
2640
EmptyValue(&'static str),
41+
ConflictingValue(&'static str, &'static str),
42+
InvalidValue(&'static str, String, &'static str),
2743
MissingValue(&'static str),
2844
}
2945

3046
impl Display for ArgsError {
3147
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
3248
match self {
3349
Self::EmptyValue(flag) => write!(f, "Option \"{flag}\" requires a non-empty value."),
50+
Self::ConflictingValue(left, right) => write!(
51+
f,
52+
"Option \"{left}\" cannot be combined with option \"{right}\"."
53+
),
54+
Self::InvalidValue(flag, value, expected) => write!(
55+
f,
56+
"Option \"{flag}\" received unsupported value \"{value}\". Use one of: {expected}."
57+
),
3458
Self::MissingValue(flag) => write!(f, "Option \"{flag}\" requires a value."),
3559
}
3660
}
@@ -43,6 +67,7 @@ where
4367
{
4468
let mut args = Vec::new();
4569
let mut flags = Flags::default();
70+
let mut output_format_source = None;
4671
let mut tokens = argv.into_iter().map(Into::into);
4772

4873
while let Some(token) = tokens.next() {
@@ -71,6 +96,16 @@ where
7196
.get_or_insert_with(Vec::new)
7297
.extend(values);
7398
}
99+
Some("--format") => {
100+
let value = next_option_value(tokens.next(), "--format")?;
101+
let output_format = parse_output_format(&value)?;
102+
set_output_format(
103+
&mut flags,
104+
&mut output_format_source,
105+
output_format,
106+
"--format",
107+
)?;
108+
}
74109
Some("--skip") => {
75110
let value = next_option_value(tokens.next(), "--skip")?;
76111
let values = split_csv_values(&value, "--skip")?;
@@ -79,7 +114,14 @@ where
79114
.get_or_insert_with(Vec::new)
80115
.extend(values);
81116
}
82-
Some("--json") => flags.json = true,
117+
Some("--json") => {
118+
set_output_format(
119+
&mut flags,
120+
&mut output_format_source,
121+
OutputFormat::Json,
122+
"--json",
123+
)?;
124+
}
83125
Some("--help") | Some("-h") => flags.help = true,
84126
_ => args.push(token),
85127
}
@@ -125,11 +167,43 @@ fn split_csv_values(value: &OsString, flag: &'static str) -> Result<Vec<String>,
125167
Ok(values)
126168
}
127169

170+
fn parse_output_format(value: &OsString) -> Result<OutputFormat, ArgsError> {
171+
match value.to_string_lossy().as_ref() {
172+
"text" => Ok(OutputFormat::Text),
173+
"json" => Ok(OutputFormat::Json),
174+
"markdown" => Ok(OutputFormat::Markdown),
175+
"sarif" => Ok(OutputFormat::Sarif),
176+
value => Err(ArgsError::InvalidValue(
177+
"--format",
178+
value.to_string(),
179+
"text, json, markdown, sarif",
180+
)),
181+
}
182+
}
183+
184+
fn set_output_format(
185+
flags: &mut Flags,
186+
source: &mut Option<&'static str>,
187+
output_format: OutputFormat,
188+
flag: &'static str,
189+
) -> Result<(), ArgsError> {
190+
if source.is_some() && flags.output_format != output_format {
191+
return Err(ArgsError::ConflictingValue(
192+
source.unwrap_or("--format"),
193+
flag,
194+
));
195+
}
196+
197+
flags.output_format = output_format;
198+
*source = Some(flag);
199+
Ok(())
200+
}
201+
128202
#[cfg(test)]
129203
mod tests {
130204
use std::ffi::OsString;
131205

132-
use super::{parse_args, ArgsError, Flags, ParsedArgs};
206+
use super::{parse_args, ArgsError, Flags, OutputFormat, ParsedArgs};
133207

134208
#[test]
135209
fn parse_args_collects_known_flags_and_positionals() {
@@ -148,7 +222,7 @@ mod tests {
148222
fix_ids: Vec::new(),
149223
fix_prefixes: Vec::new(),
150224
help: false,
151-
json: true,
225+
output_format: OutputFormat::Json,
152226
only_checks: None,
153227
skip_checks: None,
154228
},
@@ -205,6 +279,40 @@ mod tests {
205279
assert!(parsed.flags.diff);
206280
}
207281

282+
#[test]
283+
fn parse_args_collects_format_output_values() {
284+
let parsed = parse_args(["audit", "--format", "markdown"]).expect("args should parse");
285+
assert_eq!(parsed.flags.output_format, OutputFormat::Markdown);
286+
287+
let parsed = parse_args(["audit", "--format", "sarif"]).expect("args should parse");
288+
assert_eq!(parsed.flags.output_format, OutputFormat::Sarif);
289+
290+
let parsed = parse_args(["audit", "--format", "json"]).expect("args should parse");
291+
assert_eq!(parsed.flags.output_format, OutputFormat::Json);
292+
293+
let parsed = parse_args(["audit", "--format", "text"]).expect("args should parse");
294+
assert_eq!(parsed.flags.output_format, OutputFormat::Text);
295+
}
296+
297+
#[test]
298+
fn parse_args_errors_when_output_format_flags_conflict() {
299+
let error = parse_args(["audit", "--json", "--format", "markdown"])
300+
.expect_err("conflicting output formats should fail");
301+
302+
assert_eq!(error, ArgsError::ConflictingValue("--json", "--format"));
303+
}
304+
305+
#[test]
306+
fn parse_args_errors_when_output_format_value_is_invalid() {
307+
let error = parse_args(["audit", "--format", "xml"])
308+
.expect_err("unknown output format should fail");
309+
310+
assert_eq!(
311+
error,
312+
ArgsError::InvalidValue("--format", "xml".to_string(), "text, json, markdown, sarif")
313+
);
314+
}
315+
208316
#[test]
209317
fn parse_args_errors_when_fix_selector_value_is_missing() {
210318
let error = parse_args(["fix", "--fix-id"]).expect_err("missing selector should fail");

0 commit comments

Comments
 (0)