Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,22 @@ npx @jeremyfellaz/maximus fix

```text
Maximus audit
Target: /workspace/my-app

Status: attention needed
Findings: 1 error, 2 warnings, 1 info
Fixes available: 1

Findings
- [error] Path alias target does not exist
file: packages/web/tsconfig.json
detail: @ui/* points to src/missing/*
hint: Update or remove the stale alias target.

- [warn] Missing .env.example contract
file: .env
detail: Runtime env files exist, but .env.example is missing.
hint: Run `maximus fix` to create a blank contract file.
대상: /workspace/my-app

상태: 조치 필요
발견 항목: 오류 1개, 경고 2개, 정보 1개
적용 가능한 수정: 1개

발견 항목
- [오류] 경로 alias 대상이 존재하지 않음
파일: packages/web/tsconfig.json
상세: @ui/*src/missing/*를 가리키지만 해석된 경로를 찾을 수 없습니다.
힌트: editor와 build 해석을 깨기 전에 오래된 alias를 수정하거나 제거하세요.

- [경고] .env.example 계약 파일 누락
파일: .env
상세: 실행 env 파일은 있지만 .env.example이 없습니다.
힌트: `maximus fix`를 실행해 빈 계약 파일을 생성하세요.
```

## GitHub Action
Expand Down
28 changes: 14 additions & 14 deletions bin/maximus.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const require = createRequire(import.meta.url);
const cliArgs = process.argv.slice(2);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const frozenJsReferenceNote =
"Rust is the canonical Maximus runtime. The bundled JS reference is frozen and only kept as a temporary compatibility bridge for legacy-compatible commands.";
"Rust가 Maximus의 canonical runtime입니다. 포함된 JS reference는 frozen 상태이며 legacy 호환 명령을 위한 임시 compatibility bridge로만 유지됩니다.";

try {
const runtime = await resolveRuntime(cliArgs);
Expand All @@ -28,7 +28,7 @@ try {
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Maximus failed: ${message}`);
console.error(`Maximus 실패: ${message}`);
process.exitCode = process.exitCode || 1;
}

Expand All @@ -52,7 +52,7 @@ async function resolveRuntime(args) {
}
if (fallback.reason) {
throw new Error(
`${fallback.reason} Build the Rust CLI with \`cargo build -p maximus-cli\`, or install a supported native runtime package before using this command.`,
`${fallback.reason} 이 명령을 사용하기 전에 \`cargo build -p maximus-cli\`로 Rust CLI를 빌드하거나 지원되는 native runtime package를 설치하세요.`,
);
}

Expand Down Expand Up @@ -109,10 +109,10 @@ function hasGlibcRuntime() {

function formatUnsupportedPlatformMessage() {
if (process.platform === "linux" && !hasGlibcRuntime()) {
return `Linux musl is not supported yet. Maximus currently ships prebuilt Rust binaries only for Linux glibc and macOS. ${frozenJsReferenceNote}`;
return `Linux musl은 아직 지원하지 않습니다. Maximus는 현재 Linux glibc와 macOS용 prebuilt Rust binary만 제공합니다. ${frozenJsReferenceNote}`;
}

return `Unsupported platform ${process.platform}-${process.arch}. Maximus currently ships prebuilt Rust binaries only for darwin-arm64, darwin-x64, linux-arm64-gnu, and linux-x64-gnu. ${frozenJsReferenceNote}`;
return `지원하지 않는 플랫폼입니다: ${process.platform}-${process.arch}. Maximus는 현재 darwin-arm64, darwin-x64, linux-arm64-gnu, linux-x64-gnu용 prebuilt Rust binary만 제공합니다. ${frozenJsReferenceNote}`;
}

async function resolveInstalledBinary(packageName) {
Expand Down Expand Up @@ -168,15 +168,15 @@ async function evaluateFrozenJsFallback(args) {
if (parsed.unsupportedFlags.length > 0) {
return {
allowed: false,
reason: `A Rust runtime is required for options not supported by the frozen JS compatibility path: ${parsed.unsupportedFlags.join(", ")}. ${frozenJsReferenceNote}`,
reason: `frozen JS compatibility path에서 지원하지 않는 옵션에는 Rust runtime이 필요합니다: ${parsed.unsupportedFlags.join(", ")}. ${frozenJsReferenceNote}`,
};
}

const configPath = await findNearestConfigPath(parsed.targetDir);
if (configPath) {
return {
allowed: false,
reason: `A Rust runtime is required when a Maximus config file is present (${configPath}). ${frozenJsReferenceNote}`,
reason: `Maximus config file이 있을 때는 Rust runtime이 필요합니다 (${configPath}). ${frozenJsReferenceNote}`,
};
}

Expand Down Expand Up @@ -322,10 +322,10 @@ async function isPlaceholderRuntime(binaryPath) {

function formatMissingRuntimeMessage(platformPackage) {
return [
`No Rust runtime is available for ${platformPackage.label}.`,
`Expected optional dependency "${platformPackage.packageName}" to be installed or a local Cargo build to exist in target/debug or target/release.`,
`${platformPackage.label}에서 사용할 수 있는 Rust runtime이 없습니다.`,
`optional dependency "${platformPackage.packageName}"가 설치되어 있거나 target/debug 또는 target/release에 local Cargo build가 있어야 합니다.`,
frozenJsReferenceNote,
"If you are developing inside the repository, build the Rust CLI with `cargo build -p maximus-cli` first.",
"repository 안에서 개발 중이라면 먼저 `cargo build -p maximus-cli`로 Rust CLI를 빌드하세요.",
].join(" ");
}

Expand All @@ -336,7 +336,7 @@ async function runBinary(command, args) {
});

child.on("error", (error) => {
reject(new Error(`Failed to launch Rust CLI at "${command}": ${error.message}`));
reject(new Error(`"${command}"에서 Rust CLI를 실행하지 못했습니다: ${error.message}`));
});

child.on("exit", (code, signal) => {
Expand All @@ -362,15 +362,15 @@ function formatCompatHelp() {
return [
"Maximus",
"",
"Bring order to chaotic configs.",
"혼란스러운 설정을 정리합니다.",
"",
"Usage",
"사용법",
" maximus audit [path] [--json]",
" maximus doctor [path] [--json]",
" maximus fix [path] --dry-run [--json]",
" maximus help",
"",
"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`, `--format`, and `--output` require the Rust runtime, and `fix` is only available with `--dry-run`.",
"Rust가 Maximus의 canonical runtime입니다. Rust runtime이 없을 때 포함된 JS compatibility path는 Maximus config file이나 Rust 전용 flag가 없는 legacy 호환 명령을 위한 frozen reference fallback으로만 동작합니다. `--only`, `--skip`, `--fail-on`, `--diff`, `--fix-id`, `--fix-prefix`, `--format`, `--output`에는 Rust runtime이 필요하고, `fix``--dry-run`과 함께 사용할 때만 가능합니다.",
].join("\n");
}

Expand Down
8 changes: 4 additions & 4 deletions crates/maximus-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,16 @@ pub enum ArgsError {
impl Display for ArgsError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyValue(flag) => write!(f, "Option \"{flag}\" requires a non-empty value."),
Self::EmptyValue(flag) => write!(f, "\"{flag}\" 옵션에는 비어 있지 않은 값이 필요합니다."),
Self::ConflictingValue(left, right) => write!(
f,
"Option \"{left}\" cannot be combined with option \"{right}\"."
"\"{left}\" 옵션은 \"{right}\" 옵션과 함께 사용할 수 없습니다."
),
Self::InvalidValue(flag, value, expected) => write!(
f,
"Option \"{flag}\" received unsupported value \"{value}\". Use one of: {expected}."
"\"{flag}\" 옵션에 지원하지 않는 값 \"{value}\"가 전달되었습니다. 사용 가능한 값: {expected}."
),
Self::MissingValue(flag) => write!(f, "Option \"{flag}\" requires a value."),
Self::MissingValue(flag) => write!(f, "\"{flag}\" 옵션에는 값이 필요합니다."),
}
}
}
Expand Down
95 changes: 75 additions & 20 deletions crates/maximus-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod exit_codes;
mod fail_policy;
mod report_diff;
mod report_json;
mod report_ko;
mod report_markdown;
mod report_sarif;
mod report_text;
Expand Down Expand Up @@ -38,7 +39,7 @@ enum CliError {
Args(ArgsError),
Config(LoadConfigError),
Io(io::Error),
Json(serde_json::Error),
Json,
InvalidArguments(String),
UnknownCommand(String),
}
Expand All @@ -47,16 +48,14 @@ impl Display for CliError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Args(error) => write!(f, "{error}"),
Self::Config(error) => write!(f, "{error}"),
Self::Io(error) => write!(f, "{error}"),
Self::Json(error) => write!(f, "{error}"),
Self::Config(error) => write!(f, "{}", format_config_error(error)),
Self::Io(error) => write!(f, "{}", format_io_error(error)),
Self::Json => write!(f, "JSON 출력을 생성할 수 없습니다."),
Self::InvalidArguments(message) => write!(f, "{message}"),
Self::UnknownCommand(command) => {
write!(
f,
"Unknown command \"{command}\". Run \"maximus help\" for usage."
)
}
Self::UnknownCommand(command) => write!(
f,
"알 수 없는 명령 \"{command}\"입니다. 사용법은 \"maximus help\"를 실행하세요."
),
}
}
}
Expand All @@ -82,16 +81,71 @@ impl From<LoadConfigError> for CliError {
}

impl From<serde_json::Error> for CliError {
fn from(value: serde_json::Error) -> Self {
Self::Json(value)
fn from(_: serde_json::Error) -> Self {
Self::Json
}
}

fn format_config_error(error: &LoadConfigError) -> String {
match error {
LoadConfigError::Io(error) => {
format!("설정 파일을 읽을 수 없습니다: {}", format_io_error(error))
}
LoadConfigError::Parse(error) => format!(
"설정 파일 {}을 파싱할 수 없습니다. JSONC 문법을 확인하세요.",
error.label()
),
}
}

fn format_io_error(error: &io::Error) -> String {
let message = error.to_string();
if let Some(rest) = message.strip_prefix("IO error for operation on ") {
if let Some((path, reason)) = rest.split_once(": ") {
return format!(
"{path}에서 I/O 작업을 수행할 수 없습니다: {}",
korean_io_reason(reason)
);
}
}

korean_io_reason_for_kind(error.kind()).to_string()
}

fn korean_io_reason(reason: &str) -> &'static str {
if reason.contains("No such file or directory") {
"파일이나 디렉터리가 없습니다."
} else if reason.contains("Permission denied") {
"권한이 거부되었습니다."
} else if reason.contains("not a directory") {
"디렉터리가 아닙니다."
} else if reason.contains("Is a directory") {
"디렉터리입니다."
} else {
"운영체제 I/O 오류가 발생했습니다."
}
}

fn korean_io_reason_for_kind(kind: io::ErrorKind) -> &'static str {
match kind {
io::ErrorKind::NotFound => "파일이나 디렉터리가 없습니다.",
io::ErrorKind::PermissionDenied => "권한이 거부되었습니다.",
io::ErrorKind::AlreadyExists => "이미 존재합니다.",
io::ErrorKind::InvalidInput => "입력값이 올바르지 않습니다.",
io::ErrorKind::InvalidData => "데이터가 올바르지 않습니다.",
io::ErrorKind::Unsupported => "지원하지 않는 작업입니다.",
io::ErrorKind::UnexpectedEof => "예상보다 일찍 파일 끝에 도달했습니다.",
io::ErrorKind::WriteZero => "파일에 쓸 수 없습니다.",
io::ErrorKind::BrokenPipe => "출력 대상 연결이 끊어졌습니다.",
_ => "I/O 작업을 완료할 수 없습니다.",
}
}

fn main() {
let exit_code = match run_cli(env::args_os().skip(1)) {
Ok(exit_code) => exit_code,
Err(error) => {
eprintln!("Maximus failed: {error}");
eprintln!("Maximus 실패: {error}");
exit_codes::FAILURE
}
};
Expand Down Expand Up @@ -176,7 +230,7 @@ fn run_fix_command(
if initial.planned_fixes.len() != initial.result.fixes.len() {
return Err(CliError::Io(io::Error::new(
io::ErrorKind::Unsupported,
"one or more fixes are not executable from the Rust runtime yet",
"하나 이상의 수정 항목은 아직 Rust runtime에서 실행할 수 없습니다",
)));
}

Expand All @@ -187,7 +241,7 @@ fn run_fix_command(
let planned = select_planned_fixes(&initial.planned_fixes, &selector);
if !selector.is_empty() && planned.is_empty() {
return Err(CliError::InvalidArguments(
"No matching fixes for the requested selector.".to_string(),
"요청한 selector와 일치하는 수정 항목이 없습니다.".to_string(),
));
}
let selected_fixes = select_fix_plans(&initial.result.fixes, &selector);
Expand Down Expand Up @@ -462,7 +516,7 @@ fn parse_fail_on_level(value: &str) -> Result<FailOnLevel, CliError> {
"info" => Ok(FailOnLevel::Info),
"none" => Ok(FailOnLevel::None),
_ => Err(CliError::InvalidArguments(format!(
"Unknown fail-on level \"{value}\". Use one of: error, warn, info, none."
"알 수 없는 fail-on level \"{value}\"입니다. 사용 가능한 값: error, warn, info, none."
))),
}
}
Expand All @@ -473,7 +527,7 @@ fn validate_check_ids(source: &str, ids: &[String]) -> Result<(), CliError> {
for id in ids {
if !known_ids.contains(&id.as_str()) {
return Err(CliError::InvalidArguments(format!(
"Unknown check id \"{id}\" in {source}. Use one of: {}.",
"{source}에 알 수 없는 check id \"{id}\"가 있습니다. 사용 가능한 값: {}.",
known_ids.join(", ")
)));
}
Expand All @@ -490,19 +544,20 @@ fn validate_command_flags(command: Option<&str>, flags: &Flags) -> Result<(), Cl

if uses_fix_only_flags && (flags.help || command != Some("fix")) {
return Err(CliError::InvalidArguments(
"Options \"--diff\", \"--env-source-comments\", \"--fix-id\", and \"--fix-prefix\" are only available for \"fix\".".to_string(),
"\"--diff\", \"--env-source-comments\", \"--fix-id\", \"--fix-prefix\" 옵션은 \"fix\"에서만 사용할 수 있습니다.".to_string(),
));
}

if flags.diff && !flags.dry_run {
return Err(CliError::InvalidArguments(
"Option \"--diff\" requires \"fix --dry-run\".".to_string(),
"\"--diff\" 옵션은 \"fix --dry-run\"이 필요합니다.".to_string(),
));
}

if command == Some("fix") && matches!(flags.output_format, OutputFormat::Sarif) {
return Err(CliError::InvalidArguments(
"Option \"--format sarif\" is only available for \"audit\" and \"doctor\".".to_string(),
"\"--format sarif\" 옵션은 \"audit\"과 \"doctor\"에서만 사용할 수 있습니다."
.to_string(),
));
}

Expand Down
13 changes: 11 additions & 2 deletions crates/maximus-cli/src/report_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ use std::path::Path;

use maximus_core::{FixFilePreview, PreviewedFix};

use crate::report_ko as ko;

pub fn render_fix_preview(target_dir: &Path, previews: &[PreviewedFix]) -> String {
let mut blocks = Vec::new();

for preview in previews {
blocks.push(format!("- {}", preview.title));
blocks.push(format!("- {}", ko::fix_title(&preview.title)));

for file_preview in &preview.previews {
blocks.push(render_file_diff(target_dir, file_preview));
Expand Down Expand Up @@ -97,7 +99,10 @@ fn diff_lines(text: &str) -> Vec<String> {
return Vec::new();
}

let mut lines = text.split('\n').map(ToString::to_string).collect::<Vec<_>>();
let mut lines = text
.split('\n')
.map(ToString::to_string)
.collect::<Vec<_>>();
if text.ends_with('\n') {
lines.pop();
}
Expand Down Expand Up @@ -152,6 +157,8 @@ mod tests {
);

assert!(preview.contains("--- /dev/null"));
assert!(preview.contains("- .env.example 생성"));
assert!(preview.contains("- .env.example에 누락된 키 추가"));
assert!(preview.contains("+++ .env.example"));
assert!(preview.contains("+API_URL="));
assert!(preview.contains("@@ -1,1 +1,2 @@"));
Expand Down Expand Up @@ -179,6 +186,8 @@ mod tests {
assert!(preview.contains("--- .env.example"));
assert!(preview.contains("+++ .env.example"));
assert!(preview.contains("@@ -0,0 +1,1 @@"));
assert!(preview.contains("- .env.example에 누락된 키 추가"));
assert!(!preview.contains("Append missing keys to .env.example"));
assert!(!preview.contains("/dev/null"));
}
}
Loading