|
2 | 2 |
|
3 | 3 | import { readFileSync } from "node:fs"; |
4 | 4 | import { resolve, dirname, join } from "node:path"; |
5 | | -import { fileURLToPath } from "node:url"; |
| 5 | +import { fileURLToPath, pathToFileURL } from "node:url"; |
6 | 6 | import { cmdAnalyze } from "./analyze.js"; |
7 | 7 | import { cmdSplit } from "./split.js"; |
8 | 8 | import { cmdValidate } from "./validate.js"; |
9 | 9 | import { cmdStats } from "./stats.js"; |
10 | 10 | import { cmdInitSignals } from "./signals.js"; |
| 11 | +import { log, fail, BOLD, DIM, RESET } from "./console.js"; |
11 | 12 |
|
12 | 13 | // ── Package metadata ─────────────────────────────────────────── |
13 | 14 | const __dirname = dirname(fileURLToPath(import.meta.url)); |
14 | 15 | const PKG_ROOT = resolve(__dirname, ".."); |
15 | 16 | const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")); |
16 | 17 | const VERSION: string = pkg.version; |
17 | 18 |
|
18 | | -// ── Colors ───────────────────────────────────────────────────── |
19 | | -export const BOLD = "\x1b[1m"; |
20 | | -export const GREEN = "\x1b[32m"; |
21 | | -export const YELLOW = "\x1b[33m"; |
22 | | -export const RED = "\x1b[31m"; |
23 | | -export const CYAN = "\x1b[36m"; |
24 | | -export const DIM = "\x1b[2m"; |
25 | | -export const RESET = "\x1b[0m"; |
26 | | - |
27 | | -export function log(msg: string): void { |
28 | | - console.log(msg); |
29 | | -} |
30 | | -export function ok(msg: string): void { |
31 | | - log(`${GREEN}✓${RESET} ${msg}`); |
32 | | -} |
33 | | -export function warn(msg: string): void { |
34 | | - log(`${YELLOW}!${RESET} ${msg}`); |
35 | | -} |
36 | | -export function skip(msg: string): void { |
37 | | - log(`${DIM} skip${RESET} ${msg}`); |
38 | | -} |
39 | | -export function info(msg: string): void { |
40 | | - log(`${CYAN}i${RESET} ${msg}`); |
41 | | -} |
42 | | - |
43 | | -// ── Structured error + exit ──────────────────────────────────── |
44 | | -export function fail( |
45 | | - code: string, |
46 | | - message: string, |
47 | | - hint: string, |
48 | | - exitCode = 1, |
49 | | -): never { |
50 | | - console.error(`${RED}Error [${code}]:${RESET} ${message}`); |
51 | | - console.error(`${DIM}Hint: ${hint}${RESET}`); |
52 | | - process.exit(exitCode); |
53 | | -} |
54 | | - |
55 | | -// ── Flag parsing helpers ─────────────────────────────────────── |
56 | | -export function hasFlag(args: string[], flag: string): boolean { |
57 | | - return args.includes(flag); |
58 | | -} |
59 | | - |
60 | | -export function flagValue( |
61 | | - args: string[], |
62 | | - flag: string, |
63 | | -): string | undefined { |
64 | | - const idx = args.indexOf(flag); |
65 | | - if (idx === -1 || idx + 1 >= args.length) return undefined; |
66 | | - const next = args[idx + 1]; |
67 | | - // Guard against a missing value: `split --rules-dir --dry-run` must not treat |
68 | | - // the following flag as a directory name (which would create a dir literally |
69 | | - // named "--dry-run"). A value-flag whose next token is itself a flag has no |
70 | | - // value — return undefined so callers fall back to their default. |
71 | | - if (next.startsWith("--")) return undefined; |
72 | | - return next; |
73 | | -} |
74 | | - |
75 | | -// Every flag in the CLI surface that consumes the following argument as its |
76 | | -// value. Kept here as the single source of truth so positionalArgs can always |
77 | | -// skip a value-flag's argument even if a caller forgets to list it — otherwise |
78 | | -// `--rules-dir foo path` would mis-parse `foo` as a positional. |
79 | | -const VALUE_FLAGS = ["--rules-dir", "--signals"]; |
80 | | -// Boolean flags take no argument. |
81 | | -const BOOL_FLAGS = ["--memory", "--dry-run", "--help", "-h", "--yes", "--lazy", "--json"]; |
82 | | - |
83 | | -export function positionalArgs( |
84 | | - args: string[], |
85 | | - flags: string[], |
86 | | -): string[] { |
87 | | - const flagIndices = new Set<number>(); |
88 | | - // Union of the caller-supplied value-flags and the known CLI value-flags, |
89 | | - // so a value-flag's argument can never be mis-parsed as a positional. |
90 | | - const valueFlags = new Set([...flags, ...VALUE_FLAGS]); |
91 | | - for (let i = 0; i < args.length; i++) { |
92 | | - if (valueFlags.has(args[i])) { |
93 | | - flagIndices.add(i); |
94 | | - flagIndices.add(i + 1); // skip the flag's value (if present) |
95 | | - } else if (BOOL_FLAGS.includes(args[i])) { |
96 | | - flagIndices.add(i); |
97 | | - } |
98 | | - } |
99 | | - return args.filter((_, i) => !flagIndices.has(i) && !args[i].startsWith("--")); |
100 | | -} |
101 | | - |
102 | 19 | // ── Help text ────────────────────────────────────────────────── |
103 | 20 | function helpCommand(): void { |
104 | 21 | log(` |
@@ -182,6 +99,18 @@ async function main(): Promise<void> { |
182 | 99 | } |
183 | 100 | } |
184 | 101 |
|
185 | | -main().catch((err: Error) => { |
186 | | - fail("RUNTIME_FATAL", err.message, "This is a bug. Please report it.", 2); |
187 | | -}); |
| 102 | +// ── Entrypoint guard ─────────────────────────────────────────── |
| 103 | +// Only run the CLI when this module is executed directly as a binary |
| 104 | +// (`claude-rules ...` / `node dist/cli.js ...`), NOT when imported. The library |
| 105 | +// barrel (src/index.ts) re-exports the pure logic modules, which import shared |
| 106 | +// helpers from ./console.js — but the dispatch chain still reaches cli.ts via |
| 107 | +// the bin. Without this guard, `import "@mcptoolshop/claude-rules"` would parse |
| 108 | +// argv and run a command as an import side effect. |
| 109 | +if ( |
| 110 | + process.argv[1] && |
| 111 | + import.meta.url === pathToFileURL(process.argv[1]).href |
| 112 | +) { |
| 113 | + main().catch((err: Error) => { |
| 114 | + fail("RUNTIME_FATAL", err.message, "This is a bug. Please report it.", 2); |
| 115 | + }); |
| 116 | +} |
0 commit comments