From fde6313e3a83db2204f1678e5c973fc8a10a3cf8 Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Fri, 8 May 2026 22:33:11 -0400 Subject: [PATCH 1/4] fix(B-0272): accept hyphen-prefixed paths as option values Remove the startsWith("-") guard from readOptionValue so paths like --dir -roms are parsed correctly instead of rejected. Update test to use a truly missing value (trailing --datfile with no argument). Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/roms/canonicalize.test.ts | 2 +- tools/roms/canonicalize.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/roms/canonicalize.test.ts b/tools/roms/canonicalize.test.ts index 7f9e851af..2da1efc54 100644 --- a/tools/roms/canonicalize.test.ts +++ b/tools/roms/canonicalize.test.ts @@ -203,7 +203,7 @@ describe("main", () => { }) as typeof process.exit; try { - expect(() => main(["--datfile", "--dir", "/tmp"])).toThrow("exit:64"); + expect(() => main(["--datfile"])).toThrow("exit:64"); expect(stderr).toContain("missing value for --datfile"); } finally { process.stderr.write = originalStderrWrite; diff --git a/tools/roms/canonicalize.ts b/tools/roms/canonicalize.ts index 01e920ac7..948f626d7 100644 --- a/tools/roms/canonicalize.ts +++ b/tools/roms/canonicalize.ts @@ -260,7 +260,7 @@ function parseArgs(argv: readonly string[]): Args { function readOptionValue(index: number, flag: string): string { const value = argv[index + 1]; - if (value === undefined || value.startsWith("-")) { + if (value === undefined) { process.stderr.write(`missing value for ${flag}\n`); process.exit(64); } From bec5b649994f44b6685c7d165efaa88b99700afd Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Fri, 8 May 2026 22:34:49 -0400 Subject: [PATCH 2/4] refactor(B-0272): replace process.exit in parseArgs with thrown ArgError parseArgs now throws ArgError instead of calling process.exit, so main() catches and returns the exit code. Tests no longer need to monkeypatch process.exit. Co-Authored-By: Claude Opus 4.6 (1M context) --- tools/roms/canonicalize.test.ts | 8 ++------ tools/roms/canonicalize.ts | 34 +++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/tools/roms/canonicalize.test.ts b/tools/roms/canonicalize.test.ts index 2da1efc54..b4ba40d18 100644 --- a/tools/roms/canonicalize.test.ts +++ b/tools/roms/canonicalize.test.ts @@ -191,23 +191,19 @@ describe("matchAndReport", () => { describe("main", () => { test("reports missing datfile value clearly", () => { const originalStderrWrite = process.stderr.write; - const originalExit = process.exit; let stderr = ""; process.stderr.write = ((chunk: string | Uint8Array) => { stderr += String(chunk); return true; }) as typeof process.stderr.write; - process.exit = ((code?: number) => { - throw new Error(`exit:${code}`); - }) as typeof process.exit; try { - expect(() => main(["--datfile"])).toThrow("exit:64"); + const code = main(["--datfile"]); + expect(code).toBe(64); expect(stderr).toContain("missing value for --datfile"); } finally { process.stderr.write = originalStderrWrite; - process.exit = originalExit; } }); }); diff --git a/tools/roms/canonicalize.ts b/tools/roms/canonicalize.ts index 948f626d7..6977936b4 100644 --- a/tools/roms/canonicalize.ts +++ b/tools/roms/canonicalize.ts @@ -253,6 +253,15 @@ interface Args { readonly apply: boolean; } +class ArgError extends Error { + constructor( + message: string, + readonly exitCode: number, + ) { + super(message); + } +} + function parseArgs(argv: readonly string[]): Args { let datfile: string | undefined; let dir: string | undefined; @@ -261,8 +270,7 @@ function parseArgs(argv: readonly string[]): Args { function readOptionValue(index: number, flag: string): string { const value = argv[index + 1]; if (value === undefined) { - process.stderr.write(`missing value for ${flag}\n`); - process.exit(64); + throw new ArgError(`missing value for ${flag}`, 64); } return value; } @@ -284,26 +292,32 @@ function parseArgs(argv: readonly string[]): Args { " --dir Directory containing ROM files to match.\n" + " --apply Actually rename files (default: dry-run report).\n", ); - process.exit(0); + throw new ArgError("", 0); } else { - process.stderr.write(`unknown arg: ${arg}\n`); - process.exit(64); + throw new ArgError(`unknown arg: ${arg}`, 64); } } if (!datfile) { - process.stderr.write("--datfile is required\n"); - process.exit(64); + throw new ArgError("--datfile is required", 64); } if (!dir) { - process.stderr.write("--dir is required\n"); - process.exit(64); + throw new ArgError("--dir is required", 64); } return { datfile, dir, apply }; } export function main(argv: readonly string[]): number { - const args = parseArgs(argv); + let args: Args; + try { + args = parseArgs(argv); + } catch (e) { + if (e instanceof ArgError) { + if (e.message) process.stderr.write(`${e.message}\n`); + return e.exitCode; + } + throw e; + } if (!existsSync(args.datfile)) { process.stderr.write(`datfile not found: ${args.datfile}\n`); From 903333dc8cbc5d033b9f299e8956ef32e1f024b0 Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Fri, 8 May 2026 22:38:42 -0400 Subject: [PATCH 3/4] test(B-0272): cover hyphen-prefixed ROM option values Co-Authored-By: Codex --- tools/roms/canonicalize.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tools/roms/canonicalize.test.ts b/tools/roms/canonicalize.test.ts index b4ba40d18..fbf424bd7 100644 --- a/tools/roms/canonicalize.test.ts +++ b/tools/roms/canonicalize.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { existsSync, mkdtempSync, writeFileSync, readFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, writeFileSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { @@ -206,4 +206,19 @@ describe("main", () => { process.stderr.write = originalStderrWrite; } }); + + test("accepts hyphen-prefixed datfile and directory values", () => { + const tmp = mkdtempSync(join(tmpdir(), "rom-cli-hyphen-")); + const originalCwd = process.cwd(); + writeFileSync(join(tmp, "-set.dat"), FIXTURE_DATFILE); + mkdirSync(join(tmp, "-roms")); + + try { + process.chdir(tmp); + const code = main(["--datfile", "-set.dat", "--dir", "-roms"]); + expect(code).toBe(0); + } finally { + process.chdir(originalCwd); + } + }); }); From 6e35c3bc576f25d54a067d6bca555425e23e670f Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Fri, 8 May 2026 22:43:24 -0400 Subject: [PATCH 4/4] fix(B-0272): avoid parameter property in ROM ArgError Co-Authored-By: Codex --- tools/roms/canonicalize.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/roms/canonicalize.ts b/tools/roms/canonicalize.ts index 6977936b4..fe622bac4 100644 --- a/tools/roms/canonicalize.ts +++ b/tools/roms/canonicalize.ts @@ -254,11 +254,11 @@ interface Args { } class ArgError extends Error { - constructor( - message: string, - readonly exitCode: number, - ) { + readonly exitCode: number; + + constructor(message: string, exitCode: number) { super(message); + this.exitCode = exitCode; } }