nojibake 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1054 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync as readFileSync4, realpathSync as realpathSync2 } from "fs";
5
+ import { fileURLToPath as fileURLToPath2 } from "url";
6
+ import { Command } from "commander";
7
+
8
+ // src/config.ts
9
+ import { existsSync, readFileSync } from "fs";
10
+ import { join, resolve } from "path";
11
+ var allowedPolicies = ["unsafe", "ambiguous", "mixed-eol", "non-utf8", "disallowed-encoding"];
12
+ function isRecord(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+ function parsePositiveInteger(value, key) {
16
+ if (value === void 0) return void 0;
17
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
18
+ throw new Error(`${key} must be a positive integer.`);
19
+ }
20
+ return value;
21
+ }
22
+ function parseBoolean(value, key) {
23
+ if (value === void 0) return void 0;
24
+ if (typeof value !== "boolean") throw new Error(`${key} must be a boolean.`);
25
+ return value;
26
+ }
27
+ function parseStringArray(value, key) {
28
+ if (value === void 0) return void 0;
29
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
30
+ throw new Error(`${key} must be an array of strings.`);
31
+ }
32
+ return value.map((item) => item.trim()).filter(Boolean);
33
+ }
34
+ function parsePolicies(value) {
35
+ const items = parseStringArray(value, "failOn");
36
+ if (items === void 0) return void 0;
37
+ const policies = [];
38
+ for (const item of items) {
39
+ if (!allowedPolicies.includes(item)) {
40
+ throw new Error(`Unknown guard policy in config: ${item}`);
41
+ }
42
+ policies.push(item);
43
+ }
44
+ return [...new Set(policies)];
45
+ }
46
+ function parseConfigText(text, source) {
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(text);
50
+ } catch (error) {
51
+ const message = error instanceof Error ? error.message : "Invalid JSON.";
52
+ throw new Error(`Could not parse ${source}: ${message}`);
53
+ }
54
+ if (!isRecord(parsed)) throw new Error(`${source} must contain a JSON object.`);
55
+ const config = {};
56
+ const maxFiles = parsePositiveInteger(parsed.maxFiles, "maxFiles");
57
+ if (maxFiles !== void 0) config.maxFiles = maxFiles;
58
+ const maxBytes = parsePositiveInteger(parsed.maxBytes, "maxBytes");
59
+ if (maxBytes !== void 0) config.maxBytes = maxBytes;
60
+ const includeIgnored = parseBoolean(parsed.includeIgnored, "includeIgnored");
61
+ if (includeIgnored !== void 0) config.includeIgnored = includeIgnored;
62
+ const ignore = parseStringArray(parsed.ignore, "ignore");
63
+ if (ignore !== void 0) config.ignore = ignore;
64
+ const failOn = parsePolicies(parsed.failOn);
65
+ if (failOn !== void 0) config.failOn = failOn;
66
+ const allowEncodings = parseStringArray(parsed.allowEncodings, "allowEncodings");
67
+ if (allowEncodings !== void 0) config.allowEncodings = allowEncodings.map((encoding) => encoding.toLowerCase());
68
+ return config;
69
+ }
70
+ function loadConfig(root) {
71
+ const configPath = join(resolve(root), ".nojibakerc.json");
72
+ if (!existsSync(configPath)) return {};
73
+ return parseConfigText(readFileSync(configPath, "utf8"), configPath);
74
+ }
75
+
76
+ // src/encoding.ts
77
+ import { createHash } from "crypto";
78
+ import { TextDecoder } from "util";
79
+ import iconv from "iconv-lite";
80
+
81
+ // src/result.ts
82
+ import { randomUUID } from "crypto";
83
+ import { readFileSync as readFileSync2 } from "fs";
84
+ import { dirname, join as join2 } from "path";
85
+ import { fileURLToPath } from "url";
86
+
87
+ // src/types.ts
88
+ var schemaVersion = "1.0.0";
89
+
90
+ // src/result.ts
91
+ function loadPackage() {
92
+ const here = dirname(fileURLToPath(import.meta.url));
93
+ const candidates = [join2(here, "..", "package.json"), join2(here, "..", "..", "package.json")];
94
+ for (const candidate of candidates) {
95
+ try {
96
+ const parsed = JSON.parse(readFileSync2(candidate, "utf8"));
97
+ if (typeof parsed.name === "string" && typeof parsed.version === "string") {
98
+ return { name: parsed.name, version: parsed.version };
99
+ }
100
+ } catch (error) {
101
+ if (!(error instanceof Error)) throw error;
102
+ continue;
103
+ }
104
+ }
105
+ return { name: "nojibake", version: "0.0.0" };
106
+ }
107
+ var packageInfo = loadPackage();
108
+ function makeError(code, message, details) {
109
+ return details === void 0 ? { code, message } : { code, message, details };
110
+ }
111
+ function envelope(input) {
112
+ return {
113
+ schemaVersion,
114
+ toolVersion: packageInfo.version,
115
+ invocationId: randomUUID(),
116
+ ok: input.ok,
117
+ command: input.command,
118
+ summary: input.summary,
119
+ data: input.data,
120
+ errors: input.errors ?? [],
121
+ warnings: input.warnings ?? []
122
+ };
123
+ }
124
+
125
+ // src/encoding.ts
126
+ function hasPrefix(bytes, prefix) {
127
+ return prefix.every((value, index) => bytes[index] === value);
128
+ }
129
+ function detectBom(bytes) {
130
+ if (hasPrefix(bytes, [239, 187, 191])) return { bom: "utf-8", offset: 3, encoding: "utf-8" };
131
+ if (hasPrefix(bytes, [255, 254])) return { bom: "utf-16le", offset: 2, encoding: "utf-16le" };
132
+ if (hasPrefix(bytes, [254, 255])) return { bom: "utf-16be", offset: 2, encoding: "utf-16be" };
133
+ return { bom: "none", offset: 0, encoding: null };
134
+ }
135
+ function strictDecode(bytes, encoding) {
136
+ return new TextDecoder(encoding, { fatal: true, ignoreBOM: true }).decode(bytes);
137
+ }
138
+ function firstInvalidUtf8Offset(bytes) {
139
+ for (let i = 0; i < bytes.length; i += 1) {
140
+ const value = bytes[i];
141
+ if (value === void 0) return i;
142
+ if (value <= 127) continue;
143
+ let needed = 0;
144
+ let min = 0;
145
+ if (value >= 194 && value <= 223) {
146
+ needed = 1;
147
+ min = 128;
148
+ } else if (value >= 224 && value <= 239) {
149
+ needed = 2;
150
+ min = value === 224 ? 160 : 128;
151
+ } else if (value >= 240 && value <= 244) {
152
+ needed = 3;
153
+ min = value === 240 ? 144 : 128;
154
+ } else {
155
+ return i;
156
+ }
157
+ if (i + needed >= bytes.length) return i;
158
+ const firstContinuation = bytes[i + 1];
159
+ if (firstContinuation === void 0 || firstContinuation < min || firstContinuation > 191) return i + 1;
160
+ for (let j = 2; j <= needed; j += 1) {
161
+ const continuation = bytes[i + j];
162
+ if (continuation === void 0 || continuation < 128 || continuation > 191) return i + j;
163
+ }
164
+ if (value === 237) {
165
+ const second = bytes[i + 1];
166
+ if (second !== void 0 && second >= 160) return i;
167
+ }
168
+ if (value === 244) {
169
+ const second = bytes[i + 1];
170
+ if (second !== void 0 && second > 143) return i;
171
+ }
172
+ i += needed;
173
+ }
174
+ return -1;
175
+ }
176
+ function isCp949RoundTrip(bytes) {
177
+ const decoded = iconv.decode(bytes, "windows-949");
178
+ if (decoded.includes("\uFFFD")) return false;
179
+ return iconv.encode(decoded, "windows-949").equals(bytes);
180
+ }
181
+ function makeEolSummary(text) {
182
+ let crlf = 0;
183
+ let lf = 0;
184
+ let cr = 0;
185
+ for (let i = 0; i < text.length; i += 1) {
186
+ const char = text[i];
187
+ if (char === "\r") {
188
+ if (text[i + 1] === "\n") {
189
+ crlf += 1;
190
+ i += 1;
191
+ } else {
192
+ cr += 1;
193
+ }
194
+ } else if (char === "\n") {
195
+ lf += 1;
196
+ }
197
+ }
198
+ const kinds = [crlf, lf, cr].filter((count) => count > 0).length;
199
+ return { crlf, lf, cr, mixed: kinds > 1, finalNewline: text.endsWith("\n") || text.endsWith("\r") };
200
+ }
201
+ function emptyEol() {
202
+ return { crlf: 0, lf: 0, cr: 0, mixed: false, finalNewline: false };
203
+ }
204
+ function isAscii(bytes) {
205
+ return bytes.every((byte) => byte <= 127);
206
+ }
207
+ function sha256(bytes) {
208
+ return createHash("sha256").update(bytes).digest("hex");
209
+ }
210
+ function analyzeBytes(bytes) {
211
+ const bom = detectBom(bytes);
212
+ const payload = bytes.subarray(bom.offset);
213
+ const nulOffset = bytes.indexOf(0);
214
+ if (nulOffset >= 0 && bom.encoding === null) {
215
+ return {
216
+ bom: bom.bom,
217
+ encoding: "binary",
218
+ decision: "binary",
219
+ asciiCompatible: false,
220
+ safeRead: false,
221
+ eol: emptyEol(),
222
+ candidates: [],
223
+ errors: [makeError("NOJIBAKE_BINARY_NUL", "Binary NUL byte detected.", { offset: nulOffset })]
224
+ };
225
+ }
226
+ if (bom.encoding !== null) {
227
+ if ((bom.encoding === "utf-16le" || bom.encoding === "utf-16be") && payload.length % 2 !== 0) {
228
+ return {
229
+ bom: bom.bom,
230
+ encoding: bom.encoding,
231
+ decision: "invalid",
232
+ asciiCompatible: false,
233
+ safeRead: false,
234
+ eol: emptyEol(),
235
+ candidates: [{ encoding: bom.encoding, valid: false, confidence: "confirmed" }],
236
+ errors: [makeError("NOJIBAKE_INVALID_UTF16_TRUNCATED", "UTF-16 payload has an odd byte length.", { encoding: bom.encoding })]
237
+ };
238
+ }
239
+ try {
240
+ const text = strictDecode(payload, bom.encoding);
241
+ return {
242
+ bom: bom.bom,
243
+ encoding: bom.encoding,
244
+ decision: "confirmed",
245
+ asciiCompatible: bom.encoding === "utf-8",
246
+ safeRead: true,
247
+ eol: makeEolSummary(text),
248
+ candidates: [{ encoding: bom.encoding, valid: true, confidence: "confirmed" }],
249
+ errors: []
250
+ };
251
+ } catch {
252
+ return {
253
+ bom: bom.bom,
254
+ encoding: bom.encoding,
255
+ decision: "invalid",
256
+ asciiCompatible: false,
257
+ safeRead: false,
258
+ eol: emptyEol(),
259
+ candidates: [{ encoding: bom.encoding, valid: false, confidence: "confirmed" }],
260
+ errors: [makeError("NOJIBAKE_INVALID_CONFIRMED_ENCODING", "BOM-confirmed text failed strict validation.", { encoding: bom.encoding })]
261
+ };
262
+ }
263
+ }
264
+ if (isAscii(bytes)) {
265
+ const text = strictDecode(bytes, "utf-8");
266
+ return {
267
+ bom: "none",
268
+ encoding: "ascii",
269
+ decision: "ascii",
270
+ asciiCompatible: true,
271
+ safeRead: true,
272
+ eol: makeEolSummary(text),
273
+ candidates: [
274
+ { encoding: "utf-8", valid: true, confidence: "candidate" },
275
+ { encoding: "windows-949", valid: true, confidence: "candidate" }
276
+ ],
277
+ errors: []
278
+ };
279
+ }
280
+ const candidates = [];
281
+ let utf8Text = null;
282
+ try {
283
+ utf8Text = strictDecode(bytes, "utf-8");
284
+ candidates.push({ encoding: "utf-8", valid: true, confidence: "candidate" });
285
+ } catch {
286
+ candidates.push({ encoding: "utf-8", valid: false, confidence: "candidate" });
287
+ }
288
+ const cp949Valid = isCp949RoundTrip(bytes);
289
+ candidates.push({ encoding: "windows-949", valid: cp949Valid, confidence: "candidate" });
290
+ const validCandidates = candidates.filter((candidate) => candidate.valid);
291
+ if (validCandidates.length > 1) {
292
+ return {
293
+ bom: "none",
294
+ encoding: "ambiguous",
295
+ decision: "ambiguous",
296
+ asciiCompatible: true,
297
+ safeRead: true,
298
+ eol: makeEolSummary(utf8Text ?? iconv.decode(bytes, "windows-949")),
299
+ candidates,
300
+ errors: []
301
+ };
302
+ }
303
+ if (utf8Text !== null) {
304
+ return {
305
+ bom: "none",
306
+ encoding: "utf-8",
307
+ decision: "candidate",
308
+ asciiCompatible: true,
309
+ safeRead: true,
310
+ eol: makeEolSummary(utf8Text),
311
+ candidates,
312
+ errors: []
313
+ };
314
+ }
315
+ if (cp949Valid) {
316
+ return {
317
+ bom: "none",
318
+ encoding: "windows-949",
319
+ decision: "candidate",
320
+ asciiCompatible: true,
321
+ safeRead: true,
322
+ eol: makeEolSummary(iconv.decode(bytes, "windows-949")),
323
+ candidates,
324
+ errors: []
325
+ };
326
+ }
327
+ return {
328
+ bom: "none",
329
+ encoding: "unknown",
330
+ decision: "invalid",
331
+ asciiCompatible: false,
332
+ safeRead: false,
333
+ eol: emptyEol(),
334
+ candidates,
335
+ errors: [
336
+ makeError("NOJIBAKE_INVALID_BYTES", "Bytes are not valid UTF-8 or windows-949.", {
337
+ utf8InvalidOffset: firstInvalidUtf8Offset(bytes)
338
+ })
339
+ ]
340
+ };
341
+ }
342
+ function buildInspectData(input) {
343
+ const analysis = analyzeBytes(input.bytes);
344
+ return {
345
+ path: input.path,
346
+ root: input.root,
347
+ length: input.bytes.length,
348
+ sha256: sha256(input.bytes),
349
+ bom: analysis.bom,
350
+ encoding: analysis.encoding,
351
+ decision: analysis.decision,
352
+ asciiCompatible: analysis.asciiCompatible,
353
+ eol: analysis.eol,
354
+ safeRead: analysis.safeRead,
355
+ safeRewrite: false,
356
+ candidates: analysis.candidates
357
+ };
358
+ }
359
+ function encodingErrors(bytes) {
360
+ return analyzeBytes(bytes).errors;
361
+ }
362
+
363
+ // src/format.ts
364
+ import { relative, sep } from "path";
365
+ function portable(path) {
366
+ return path.split(sep).join("/");
367
+ }
368
+ function displayInspectPath(data) {
369
+ if (data.root === null) return data.path;
370
+ const rel = relative(data.root, data.path);
371
+ return rel === "" ? "." : portable(rel);
372
+ }
373
+ function compactInspectData(data) {
374
+ return {
375
+ p: displayInspectPath(data),
376
+ l: data.length,
377
+ h: data.sha256,
378
+ bom: data.bom,
379
+ e: data.encoding,
380
+ d: data.decision,
381
+ sr: data.safeRead,
382
+ sw: data.safeRewrite,
383
+ mix: data.eol.mixed
384
+ };
385
+ }
386
+ function compactSummary(summary) {
387
+ return {
388
+ ok: summary.ok,
389
+ n: summary.totalFiles,
390
+ bytes: summary.totalBytes,
391
+ safe: summary.safeRead,
392
+ unsafe: summary.unsafeRead,
393
+ amb: summary.ambiguous,
394
+ mix: summary.mixedEol,
395
+ err: summary.errorFiles,
396
+ skip: summary.skipped,
397
+ d: summary.byDecision,
398
+ e: summary.byEncoding,
399
+ why: summary.byReason
400
+ };
401
+ }
402
+ function compactFile(file) {
403
+ const output = {
404
+ p: file.path,
405
+ l: file.length,
406
+ e: file.encoding,
407
+ d: file.decision,
408
+ sr: file.safeRead,
409
+ sw: file.safeRewrite
410
+ };
411
+ if (file.eol?.mixed === true) output.mix = true;
412
+ if (file.reasons.length > 0) output.why = file.reasons;
413
+ if (file.errors.length > 0) output.err = file.errors.map((error) => error.code);
414
+ return output;
415
+ }
416
+ function compactScanData(data) {
417
+ return { r: data.root, s: compactSummary(data.summary), f: data.files.map(compactFile) };
418
+ }
419
+ function compactGuardData(data) {
420
+ return {
421
+ r: data.root,
422
+ p: data.policies,
423
+ s: compactSummary(data.summary),
424
+ fail: data.failures.map((failure) => ({ p: failure.path, pol: failure.policies, why: failure.reasons }))
425
+ };
426
+ }
427
+ function formatInspectLine(data, errors) {
428
+ const status = errors.length === 0 && data.safeRead ? "OK" : "RISK";
429
+ const eol = data.eol.mixed ? "mixed-eol" : "stable-eol";
430
+ return `${status} ${data.encoding} ${data.decision} ${data.length}B ${eol} ${displayInspectPath(data)}`;
431
+ }
432
+ function formatScanLines(data) {
433
+ const lines = [
434
+ `root: ${data.root}`,
435
+ `files: ${data.summary.totalFiles}, safe: ${data.summary.safeRead}, unsafe: ${data.summary.unsafeRead}, ambiguous: ${data.summary.ambiguous}, mixed-eol: ${data.summary.mixedEol}, skipped: ${data.summary.skipped}, bytes: ${data.summary.totalBytes}`
436
+ ];
437
+ for (const file of data.files) {
438
+ const status = file.errors.length === 0 && file.safeRead ? "OK" : "RISK";
439
+ const reasons = file.reasons.length > 0 ? ` ${file.reasons.join(",")}` : "";
440
+ lines.push(`${status} ${file.encoding ?? "unknown"} ${file.decision} ${file.length ?? 0}B ${file.path}${reasons}`);
441
+ }
442
+ return lines;
443
+ }
444
+ function formatGuardLines(data) {
445
+ const lines = [
446
+ `root: ${data.root}`,
447
+ `policies: ${data.policies.join(",")}`,
448
+ `failures: ${data.failures.length}`
449
+ ];
450
+ for (const failure of data.failures) {
451
+ lines.push(`FAIL ${failure.policies.join(",")} ${failure.reasons.join(",")} ${failure.path}`);
452
+ }
453
+ return lines;
454
+ }
455
+
456
+ // src/pathSafety.ts
457
+ import { lstatSync, realpathSync, statSync } from "fs";
458
+ import { isAbsolute, parse, resolve as resolve2, relative as relative2, sep as sep2 } from "path";
459
+ function hasAdsNotation(inputPath) {
460
+ const parsed = parse(inputPath);
461
+ const withoutDrive = parsed.root.startsWith(parsed.root[0] ?? "") && /^[A-Za-z]:[\\/]?$/.test(parsed.root) ? inputPath.slice(2) : inputPath;
462
+ return withoutDrive.includes(":");
463
+ }
464
+ function isInsideRoot(realPath, realRoot) {
465
+ const rel = relative2(realRoot, realPath);
466
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
467
+ }
468
+ function linkedComponent(path) {
469
+ const parsed = parse(path);
470
+ const parts = relative2(parsed.root, path).split(/[\\/]+/).filter((part) => part.length > 0);
471
+ let current = parsed.root;
472
+ for (const part of parts) {
473
+ current = resolve2(current, part);
474
+ if (lstatSync(current).isSymbolicLink()) return current;
475
+ }
476
+ return null;
477
+ }
478
+ function linkedDescendant(path, root) {
479
+ const rel = relative2(root, path);
480
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return null;
481
+ let current = root;
482
+ for (const part of rel.split(/[\\/]+/).filter((entry) => entry.length > 0)) {
483
+ current = resolve2(current, part);
484
+ if (lstatSync(current).isSymbolicLink()) return current;
485
+ }
486
+ return null;
487
+ }
488
+ function resolveSafePath(inputPath, root) {
489
+ const resolvedRoot = root === void 0 ? null : resolve2(root);
490
+ const base = resolvedRoot ?? process.cwd();
491
+ const resolvedPath = resolve2(base, inputPath);
492
+ const errors = [];
493
+ if (hasAdsNotation(inputPath)) {
494
+ errors.push(makeError("NOJIBAKE_PATH_ADS_REJECTED", "Windows alternate data stream notation is rejected."));
495
+ }
496
+ if (resolvedRoot !== null) {
497
+ try {
498
+ if (lstatSync(resolvedRoot).isSymbolicLink()) {
499
+ errors.push(makeError("NOJIBAKE_ROOT_LINK_REJECTED", "Symlink or reparse root is rejected for MVP safety.", { root: resolvedRoot }));
500
+ return { ok: false, path: resolvedPath, root: resolvedRoot, errors };
501
+ }
502
+ } catch {
503
+ errors.push(makeError("NOJIBAKE_ROOT_NOT_FOUND", "Root does not exist.", { root: resolvedRoot }));
504
+ return { ok: false, path: resolvedPath, root: resolvedRoot, errors };
505
+ }
506
+ }
507
+ let pathLstat;
508
+ try {
509
+ pathLstat = lstatSync(resolvedPath);
510
+ } catch {
511
+ errors.push(makeError("NOJIBAKE_PATH_NOT_FOUND", "Path does not exist.", { path: resolvedPath }));
512
+ return { ok: false, path: resolvedPath, root: resolvedRoot, errors };
513
+ }
514
+ if (pathLstat.isSymbolicLink()) {
515
+ errors.push(makeError("NOJIBAKE_PATH_LINK_REJECTED", "Symlink or reparse traversal is rejected for MVP safety.", { path: resolvedPath }));
516
+ return { ok: false, path: resolvedPath, root: resolvedRoot, errors };
517
+ }
518
+ const pathLink = resolvedRoot === null ? linkedComponent(resolvedPath) : linkedDescendant(resolvedPath, resolvedRoot);
519
+ if (pathLink !== null) {
520
+ errors.push(makeError("NOJIBAKE_PATH_LINK_REJECTED", "Symlink or reparse traversal is rejected for MVP safety.", { path: pathLink }));
521
+ return { ok: false, path: resolvedPath, root: resolvedRoot, errors };
522
+ }
523
+ const pathStat = statSync(resolvedPath);
524
+ if (pathStat.isDirectory()) {
525
+ errors.push(makeError("NOJIBAKE_PATH_DIRECTORY_REJECTED", "Directories cannot be inspected as files.", { path: resolvedPath }));
526
+ } else if (!pathStat.isFile()) {
527
+ errors.push(makeError("NOJIBAKE_PATH_NOT_FILE", "Only regular files can be inspected.", { path: resolvedPath }));
528
+ }
529
+ if (resolvedRoot !== null) {
530
+ const realRoot = realpathSync(resolvedRoot);
531
+ const realTarget = realpathSync(resolvedPath);
532
+ if (!isInsideRoot(realTarget, realRoot)) {
533
+ errors.push(makeError("NOJIBAKE_PATH_OUTSIDE_ROOT", "Path resolves outside the configured root boundary.", { path: realTarget, root: realRoot }));
534
+ }
535
+ }
536
+ return { ok: errors.length === 0, path: resolvedPath.split(sep2).join("/"), root: resolvedRoot, errors };
537
+ }
538
+
539
+ // src/scan.ts
540
+ import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "fs";
541
+ import { relative as relative3, resolve as resolve3, sep as sep3 } from "path";
542
+ var defaultIgnoredDirs = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "coverage"]);
543
+ var allowedPolicies2 = /* @__PURE__ */ new Set(["unsafe", "ambiguous", "mixed-eol", "non-utf8", "disallowed-encoding"]);
544
+ function portable2(path) {
545
+ return path.split(sep3).join("/");
546
+ }
547
+ function relativeToRoot(root, path) {
548
+ const rel = relative3(root, path);
549
+ return rel === "" ? "." : portable2(rel);
550
+ }
551
+ function emptySummary(skipped) {
552
+ return {
553
+ ok: true,
554
+ totalFiles: 0,
555
+ totalBytes: 0,
556
+ safeRead: 0,
557
+ unsafeRead: 0,
558
+ safeRewrite: 0,
559
+ ambiguous: 0,
560
+ mixedEol: 0,
561
+ errorFiles: 0,
562
+ skipped,
563
+ byDecision: {},
564
+ byEncoding: {},
565
+ byReason: {}
566
+ };
567
+ }
568
+ function addCount(record, key) {
569
+ record[key] = (record[key] ?? 0) + 1;
570
+ }
571
+ function summarize(files, skipped) {
572
+ const summary = emptySummary(skipped.length);
573
+ for (const file of files) {
574
+ summary.totalFiles += 1;
575
+ summary.totalBytes += file.length ?? 0;
576
+ if (file.safeRead && file.errors.length === 0) {
577
+ summary.safeRead += 1;
578
+ } else {
579
+ summary.unsafeRead += 1;
580
+ }
581
+ if (file.safeRewrite) summary.safeRewrite += 1;
582
+ if (file.decision === "ambiguous") summary.ambiguous += 1;
583
+ if (file.eol?.mixed === true) summary.mixedEol += 1;
584
+ if (file.errors.length > 0) summary.errorFiles += 1;
585
+ addCount(summary.byDecision, file.decision);
586
+ addCount(summary.byEncoding, file.encoding ?? "unknown");
587
+ for (const reason of file.reasons) addCount(summary.byReason, reason);
588
+ }
589
+ summary.ok = summary.unsafeRead === 0;
590
+ return summary;
591
+ }
592
+ function normalizeIgnorePattern(pattern) {
593
+ return pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\//, "");
594
+ }
595
+ function escapeRegexChar(char) {
596
+ return /[|\\{}()[\]^$+?.]/.test(char) ? `\\${char}` : char;
597
+ }
598
+ function globToRegex(pattern) {
599
+ let source = "^";
600
+ for (let index = 0; index < pattern.length; index += 1) {
601
+ const char = pattern[index];
602
+ if (char === "*") {
603
+ if (pattern[index + 1] === "*") {
604
+ source += ".*";
605
+ index += 1;
606
+ } else {
607
+ source += "[^/]*";
608
+ }
609
+ } else {
610
+ source += escapeRegexChar(char ?? "");
611
+ }
612
+ }
613
+ return new RegExp(`${source}$`);
614
+ }
615
+ function isIgnored(path, patterns) {
616
+ const normalizedPath = normalizeIgnorePattern(path);
617
+ return patterns.some((rawPattern) => {
618
+ const pattern = normalizeIgnorePattern(rawPattern);
619
+ if (pattern === "") return false;
620
+ if (pattern.endsWith("/**")) {
621
+ const prefix = pattern.slice(0, -3);
622
+ return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`);
623
+ }
624
+ if (!pattern.includes("*")) {
625
+ return normalizedPath === pattern || normalizedPath.startsWith(`${pattern}/`);
626
+ }
627
+ return globToRegex(pattern).test(normalizedPath);
628
+ });
629
+ }
630
+ function collectRecursive(root, maxFiles, includeIgnored, ignore) {
631
+ const paths = [];
632
+ const skipped = [];
633
+ const pending = [root];
634
+ while (pending.length > 0) {
635
+ const current = pending.pop();
636
+ if (current === void 0) continue;
637
+ const entries = readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
638
+ for (const entry of entries) {
639
+ const fullPath = resolve3(current, entry.name);
640
+ const relPath = relativeToRoot(root, fullPath);
641
+ if (isIgnored(relPath, ignore)) {
642
+ skipped.push({ path: relPath, reason: "ignored-path" });
643
+ continue;
644
+ }
645
+ if (entry.isSymbolicLink()) {
646
+ skipped.push({ path: relPath, reason: "symlink" });
647
+ continue;
648
+ }
649
+ if (entry.isDirectory()) {
650
+ if (!includeIgnored && defaultIgnoredDirs.has(entry.name)) {
651
+ skipped.push({ path: relPath, reason: "ignored-directory" });
652
+ continue;
653
+ }
654
+ pending.push(fullPath);
655
+ continue;
656
+ }
657
+ if (!entry.isFile()) {
658
+ skipped.push({ path: relPath, reason: "not-regular-file" });
659
+ continue;
660
+ }
661
+ if (paths.length >= maxFiles) {
662
+ skipped.push({ path: relPath, reason: "max-files-exceeded" });
663
+ continue;
664
+ }
665
+ paths.push(relPath);
666
+ }
667
+ }
668
+ return { paths: paths.sort(), skipped: skipped.sort((a, b) => a.path.localeCompare(b.path)) };
669
+ }
670
+ function uniqueReasons(reasons) {
671
+ return [...new Set(reasons)];
672
+ }
673
+ function reasonCodesFor(file, allowEncodings) {
674
+ const reasons = [];
675
+ if (file.errors.some((error) => error.code.startsWith("NOJIBAKE_PATH_"))) reasons.push("path:error");
676
+ if (file.errors.some((error) => error.code === "NOJIBAKE_FILE_READ_FAILED")) reasons.push("file:read-failed");
677
+ if (file.errors.some((error) => error.code === "NOJIBAKE_FILE_TOO_LARGE")) reasons.push("large:file");
678
+ if (!file.safeRead || file.errors.length > 0) reasons.push("read:unsafe");
679
+ if (file.decision === "binary" || file.errors.some((error) => error.code === "NOJIBAKE_BINARY_NUL")) reasons.push("encoding:binary");
680
+ if (file.decision === "invalid") reasons.push("encoding:invalid");
681
+ if (file.decision === "ambiguous") reasons.push("encoding:ambiguous");
682
+ if (file.encoding !== null && file.encoding !== "utf-8" && file.encoding !== "ascii") reasons.push("encoding:non-utf8");
683
+ if (allowEncodings !== null && file.encoding !== null && !allowEncodings.has(file.encoding)) reasons.push("encoding:disallowed");
684
+ if (file.eol?.mixed === true) reasons.push("eol:mixed");
685
+ return uniqueReasons(reasons);
686
+ }
687
+ function withReasons(file, allowEncodings) {
688
+ const complete = { ...file, reasons: [] };
689
+ return { ...complete, reasons: reasonCodesFor(complete, allowEncodings) };
690
+ }
691
+ function inspectOne(root, inputPath, options) {
692
+ const safe = resolveSafePath(inputPath, root);
693
+ const displayPath = safe.root === null ? safe.path : relativeToRoot(safe.root, safe.path);
694
+ if (!safe.ok) {
695
+ return withReasons({
696
+ path: displayPath,
697
+ ok: false,
698
+ length: null,
699
+ sha256: null,
700
+ bom: null,
701
+ encoding: null,
702
+ decision: "error",
703
+ asciiCompatible: null,
704
+ eol: null,
705
+ safeRead: false,
706
+ safeRewrite: false,
707
+ candidates: [],
708
+ errors: safe.errors
709
+ }, options.allowEncodings);
710
+ }
711
+ const fileSize = statSync2(safe.path).size;
712
+ if (options.maxBytes !== void 0 && fileSize > options.maxBytes) {
713
+ return withReasons({
714
+ path: displayPath,
715
+ ok: false,
716
+ length: fileSize,
717
+ sha256: null,
718
+ bom: null,
719
+ encoding: null,
720
+ decision: "error",
721
+ asciiCompatible: null,
722
+ eol: null,
723
+ safeRead: false,
724
+ safeRewrite: false,
725
+ candidates: [],
726
+ errors: [makeError("NOJIBAKE_FILE_TOO_LARGE", "File exceeds maxBytes and was not read.", { path: displayPath, length: fileSize, maxBytes: options.maxBytes })]
727
+ }, options.allowEncodings);
728
+ }
729
+ let bytes;
730
+ try {
731
+ bytes = readFileSync3(safe.path);
732
+ } catch (error) {
733
+ const message = error instanceof Error ? error.message : "File read failed.";
734
+ return withReasons({
735
+ path: displayPath,
736
+ ok: false,
737
+ length: null,
738
+ sha256: null,
739
+ bom: null,
740
+ encoding: null,
741
+ decision: "error",
742
+ asciiCompatible: null,
743
+ eol: null,
744
+ safeRead: false,
745
+ safeRewrite: false,
746
+ candidates: [],
747
+ errors: [makeError("NOJIBAKE_FILE_READ_FAILED", message, { path: displayPath })]
748
+ }, options.allowEncodings);
749
+ }
750
+ const data = buildInspectData({ path: safe.path, root: safe.root, bytes });
751
+ const errors = encodingErrors(bytes);
752
+ return withReasons({
753
+ path: displayPath,
754
+ ok: errors.length === 0,
755
+ length: data.length,
756
+ sha256: data.sha256,
757
+ bom: data.bom,
758
+ encoding: data.encoding,
759
+ decision: data.decision,
760
+ asciiCompatible: data.asciiCompatible,
761
+ eol: data.eol,
762
+ safeRead: data.safeRead,
763
+ safeRewrite: data.safeRewrite,
764
+ candidates: data.candidates,
765
+ errors
766
+ }, options.allowEncodings);
767
+ }
768
+ function normalizeInputPaths(paths) {
769
+ return [...new Set((paths ?? []).flatMap((value) => value.split(/\r?\n/)).map((value) => value.trim()).filter(Boolean))];
770
+ }
771
+ function parsePositiveInteger2(value) {
772
+ if (value === void 0) return void 0;
773
+ const parsed = Number.parseInt(value, 10);
774
+ if (!Number.isFinite(parsed) || parsed < 1) return void 0;
775
+ return parsed;
776
+ }
777
+ function scanPaths(options = {}) {
778
+ const root = resolve3(options.root ?? process.cwd());
779
+ const rootStat = statSync2(root);
780
+ if (!rootStat.isDirectory()) {
781
+ throw new Error(`Scan root is not a directory: ${root}`);
782
+ }
783
+ const ignore = (options.ignore ?? []).map(normalizeIgnorePattern).filter(Boolean);
784
+ const inputPaths = normalizeInputPaths(options.paths);
785
+ const skippedInputs = [];
786
+ const activeInputPaths = inputPaths.filter((path) => {
787
+ if (!isIgnored(path, ignore)) return true;
788
+ skippedInputs.push({ path: normalizeIgnorePattern(path), reason: "ignored-path" });
789
+ return false;
790
+ });
791
+ const useExplicitPaths = options.useExplicitPaths === true || activeInputPaths.length > 0;
792
+ const collected = useExplicitPaths ? { paths: activeInputPaths, skipped: skippedInputs } : collectRecursive(root, options.maxFiles ?? 5e3, options.includeIgnored === true, ignore);
793
+ const allowEncodings = options.allowEncodings === void 0 ? null : new Set(options.allowEncodings.map((encoding) => encoding.toLowerCase()));
794
+ const inspectOptions = { allowEncodings };
795
+ if (options.maxBytes !== void 0) inspectOptions.maxBytes = options.maxBytes;
796
+ const files = collected.paths.map((path) => inspectOne(root, path, inspectOptions));
797
+ return { root: portable2(root), files, skipped: collected.skipped, summary: summarize(files, collected.skipped) };
798
+ }
799
+ function policyReasons(policy, file) {
800
+ if (policy === "unsafe") return file.reasons.filter((reason) => ["path:error", "file:read-failed", "large:file", "read:unsafe", "encoding:binary", "encoding:invalid"].includes(reason));
801
+ if (policy === "ambiguous" && file.reasons.includes("encoding:ambiguous")) return ["encoding:ambiguous"];
802
+ if (policy === "mixed-eol" && file.reasons.includes("eol:mixed")) return ["eol:mixed"];
803
+ if (policy === "non-utf8" && file.reasons.includes("encoding:non-utf8")) return ["encoding:non-utf8"];
804
+ if (policy === "disallowed-encoding" && file.reasons.includes("encoding:disallowed")) return ["encoding:disallowed"];
805
+ return [];
806
+ }
807
+ function parseGuardPolicies(value) {
808
+ const raw = Array.isArray(value) ? value : value === void 0 ? ["unsafe"] : value.split(",").map((item) => item.trim()).filter(Boolean);
809
+ const policies = [];
810
+ for (const item of raw) {
811
+ if (!allowedPolicies2.has(item)) {
812
+ throw new Error(`Unknown guard policy: ${item}`);
813
+ }
814
+ policies.push(item);
815
+ }
816
+ return policies.length > 0 ? [...new Set(policies)] : ["unsafe"];
817
+ }
818
+ function guardScan(scan, policies) {
819
+ const failures = [];
820
+ for (const file of scan.files) {
821
+ const matchedPolicies = policies.filter((policy) => policyReasons(policy, file).length > 0);
822
+ const reasons = uniqueReasons(matchedPolicies.flatMap((policy) => policyReasons(policy, file)));
823
+ if (matchedPolicies.length > 0) failures.push({ path: file.path, policies: matchedPolicies, reasons });
824
+ }
825
+ return { root: scan.root, policies, summary: scan.summary, failures };
826
+ }
827
+
828
+ // src/schema.ts
829
+ var resultSchema = {
830
+ $schema: "https://json-schema.org/draft/2020-12/schema",
831
+ $id: "https://github.com/don9x2E/nojibake/schemas/result-envelope.json",
832
+ title: "Nojibake Result Envelope",
833
+ type: "object",
834
+ required: ["schemaVersion", "toolVersion", "invocationId", "ok", "command", "summary", "data", "errors", "warnings"],
835
+ properties: {
836
+ schemaVersion: { type: "string" },
837
+ toolVersion: { type: "string" },
838
+ invocationId: { type: "string" },
839
+ ok: { type: "boolean" },
840
+ command: { type: "string" },
841
+ summary: { type: "string" },
842
+ data: {},
843
+ errors: { type: "array" },
844
+ warnings: { type: "array" }
845
+ },
846
+ additionalProperties: false
847
+ };
848
+
849
+ // src/cli.ts
850
+ function writeJson(value, compact = false) {
851
+ process.stdout.write(`${JSON.stringify(value, null, compact ? 0 : 2)}
852
+ `);
853
+ }
854
+ function writeLines(lines) {
855
+ process.stdout.write(`${lines.join("\n")}
856
+ `);
857
+ }
858
+ function writeUsageError(command, code, message) {
859
+ writeJson(envelope({
860
+ ok: false,
861
+ command,
862
+ summary: "CLI usage error.",
863
+ data: null,
864
+ errors: [{ code, message }]
865
+ }));
866
+ process.exitCode = 2;
867
+ }
868
+ function requireJsonOption(json, command) {
869
+ if (json !== true) {
870
+ writeUsageError(command, "NOJIBAKE_JSON_REQUIRED", "Pass --json.");
871
+ return false;
872
+ }
873
+ return true;
874
+ }
875
+ function requireStructuredOutput(options, command) {
876
+ if (options.pretty === true) return "pretty";
877
+ if (options.json === true) return "json";
878
+ writeUsageError(command, "NOJIBAKE_JSON_REQUIRED", "Pass --json, or pass --pretty for human-readable output.");
879
+ return null;
880
+ }
881
+ function parseCommand(argv) {
882
+ return argv.slice(2).filter((arg) => !arg.startsWith("-")).join(" ");
883
+ }
884
+ function configureCommand(command) {
885
+ return command.helpOption(false).showHelpAfterError(false).showSuggestionAfterError(false);
886
+ }
887
+ function collectPath(value, previous) {
888
+ previous.push(value);
889
+ return previous;
890
+ }
891
+ function readStdinPaths() {
892
+ if (process.stdin.isTTY === true) return [];
893
+ return readFileSync4(0, "utf8").split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
894
+ }
895
+ function mergeStrings(configValues, cliValues) {
896
+ const merged = [...configValues ?? [], ...cliValues ?? []].map((value) => value.trim()).filter(Boolean);
897
+ return merged.length > 0 ? [...new Set(merged)] : void 0;
898
+ }
899
+ function applyNumberOption(target, key, cliValue, configValue) {
900
+ const parsed = parsePositiveInteger2(cliValue);
901
+ const value = parsed ?? configValue;
902
+ if (value !== void 0) target[key] = value;
903
+ }
904
+ function scanCommandOptions(options) {
905
+ const root = options.root ?? process.cwd();
906
+ const config = loadConfig(root);
907
+ const scanOptions = { root };
908
+ const paths = [...options.path ?? []];
909
+ const useStdinPaths = options.stdinPaths === true;
910
+ if (useStdinPaths) paths.push(...readStdinPaths());
911
+ if (paths.length > 0) scanOptions.paths = paths;
912
+ if (paths.length > 0 || useStdinPaths) scanOptions.useExplicitPaths = true;
913
+ applyNumberOption(scanOptions, "maxFiles", options.maxFiles, config.maxFiles);
914
+ applyNumberOption(scanOptions, "maxBytes", options.maxBytes, config.maxBytes);
915
+ if (options.includeIgnored === true || config.includeIgnored === true) scanOptions.includeIgnored = true;
916
+ const ignore = mergeStrings(config.ignore, options.ignore);
917
+ if (ignore !== void 0) scanOptions.ignore = ignore;
918
+ const allowEncodings = mergeStrings(config.allowEncodings, options.allowEncoding);
919
+ if (allowEncodings !== void 0) scanOptions.allowEncodings = allowEncodings;
920
+ return { scanOptions, config };
921
+ }
922
+ function runScanCommand(options) {
923
+ const command = "scan";
924
+ const output = requireStructuredOutput(options, command);
925
+ if (output === null) return;
926
+ try {
927
+ const { scanOptions } = scanCommandOptions(options);
928
+ const scan = scanPaths(scanOptions);
929
+ if (output === "pretty") {
930
+ writeLines(formatScanLines(scan));
931
+ return;
932
+ }
933
+ writeJson(envelope({ ok: true, command, summary: `Scanned ${scan.summary.totalFiles} files.`, data: options.compact ? compactScanData(scan) : scan }), options.compact === true);
934
+ } catch (error) {
935
+ const message = error instanceof Error ? error.message : "Scan failed.";
936
+ writeUsageError(command, "NOJIBAKE_SCAN_FAILED", message);
937
+ }
938
+ }
939
+ function runGuardCommand(options) {
940
+ const command = "guard";
941
+ const output = requireStructuredOutput(options, command);
942
+ if (output === null) return;
943
+ try {
944
+ const { scanOptions, config } = scanCommandOptions(options);
945
+ const scan = scanPaths(scanOptions);
946
+ const policies = parseGuardPolicies(options.failOn ?? config.failOn);
947
+ const guard = guardScan(scan, policies);
948
+ const ok = guard.failures.length === 0;
949
+ if (output === "pretty") {
950
+ writeLines(formatGuardLines(guard));
951
+ } else {
952
+ writeJson(envelope({ ok, command, summary: ok ? "Guard passed." : `Guard failed for ${guard.failures.length} files.`, data: options.compact ? compactGuardData(guard) : guard }), options.compact === true);
953
+ }
954
+ if (!ok) process.exitCode = 1;
955
+ } catch (error) {
956
+ const message = error instanceof Error ? error.message : "Guard failed.";
957
+ writeUsageError(command, "NOJIBAKE_GUARD_FAILED", message);
958
+ }
959
+ }
960
+ function createProgram() {
961
+ const program = configureCommand(new Command());
962
+ program.name("nojibake").description("Read-only text encoding inspection CLI.").exitOverride().configureOutput({ writeOut: () => void 0, writeErr: () => void 0 });
963
+ program.action(() => {
964
+ writeUsageError("", "NOJIBAKE_CLI_USAGE", "Missing command.");
965
+ });
966
+ configureCommand(program.command("version").option("--json", "emit JSON").action((options) => {
967
+ if (!requireJsonOption(options.json, "version")) return;
968
+ const data = { name: packageInfo.name, version: packageInfo.version };
969
+ writeJson(envelope({ ok: true, command: "version", summary: `Nojibake ${packageInfo.version}`, data }));
970
+ }));
971
+ const schema = configureCommand(program.command("schema"));
972
+ schema.action(() => {
973
+ writeUsageError("schema", "NOJIBAKE_CLI_USAGE", "Missing schema command.");
974
+ });
975
+ configureCommand(schema.command("result").option("--json", "emit JSON").action((options) => {
976
+ if (!requireJsonOption(options.json, "schema result")) return;
977
+ writeJson(envelope({ ok: true, command: "schema result", summary: "Result envelope schema.", data: resultSchema }));
978
+ }));
979
+ const inspect = configureCommand(program.command("inspect"));
980
+ inspect.action(() => {
981
+ writeUsageError("inspect", "NOJIBAKE_CLI_USAGE", "Missing inspect command.");
982
+ });
983
+ configureCommand(inspect.command("path").option("--root <root>", "optional root boundary").option("--path <file>", "file to inspect").option("--json", "emit JSON").option("--compact", "emit compact JSON for agent contexts").option("--pretty", "emit human-readable one-line summary").action((options) => {
984
+ const command = "inspect path";
985
+ const output = requireStructuredOutput(options, command);
986
+ if (output === null) return;
987
+ if (options.path === void 0) {
988
+ writeUsageError(command, "NOJIBAKE_PATH_REQUIRED", "Pass --path <file>.");
989
+ return;
990
+ }
991
+ const safe = resolveSafePath(options.path, options.root);
992
+ if (!safe.ok) {
993
+ writeJson(envelope({ ok: false, command, summary: "Path rejected.", data: null, errors: safe.errors }), options.compact === true);
994
+ process.exitCode = 2;
995
+ return;
996
+ }
997
+ let bytes;
998
+ try {
999
+ bytes = readFileSync4(safe.path);
1000
+ } catch (error) {
1001
+ const message = error instanceof Error ? error.message : "File read failed.";
1002
+ writeJson(envelope({
1003
+ ok: false,
1004
+ command,
1005
+ summary: "File could not be read.",
1006
+ data: null,
1007
+ errors: [{ code: "NOJIBAKE_FILE_READ_FAILED", message, details: { path: safe.path } }]
1008
+ }), options.compact === true);
1009
+ process.exitCode = 2;
1010
+ return;
1011
+ }
1012
+ const data = buildInspectData({ path: safe.path, root: safe.root, bytes });
1013
+ const errors = encodingErrors(bytes);
1014
+ if (output === "pretty") {
1015
+ writeLines([formatInspectLine(data, errors)]);
1016
+ } else {
1017
+ writeJson(envelope({
1018
+ ok: errors.length === 0,
1019
+ command,
1020
+ summary: errors.length === 0 ? `Inspected ${data.length} bytes.` : "File bytes failed encoding validation.",
1021
+ data: options.compact === true ? compactInspectData(data) : data,
1022
+ errors
1023
+ }), options.compact === true);
1024
+ }
1025
+ if (errors.length > 0) process.exitCode = 1;
1026
+ }));
1027
+ configureCommand(program.command("scan").option("--root <root>", "root directory to scan", process.cwd()).option("--path <file>", "specific file to scan; can be repeated", collectPath, []).option("--stdin-paths", "read newline-separated paths from stdin").option("--max-files <n>", "maximum files for recursive scan").option("--max-bytes <n>", "maximum bytes to read per file").option("--include-ignored", "include default ignored directories such as node_modules and dist").option("--ignore <pattern>", "ignore pattern; can be repeated", collectPath, []).option("--allow-encoding <encoding>", "allowed encoding; can be repeated", collectPath, []).option("--json", "emit JSON").option("--compact", "emit compact JSON for agent contexts").option("--pretty", "emit human-readable summary").action(runScanCommand));
1028
+ configureCommand(program.command("guard").option("--root <root>", "root directory to scan", process.cwd()).option("--path <file>", "specific file to guard; can be repeated", collectPath, []).option("--stdin-paths", "read newline-separated paths from stdin").option("--max-files <n>", "maximum files for recursive scan").option("--max-bytes <n>", "maximum bytes to read per file").option("--include-ignored", "include default ignored directories such as node_modules and dist").option("--ignore <pattern>", "ignore pattern; can be repeated", collectPath, []).option("--allow-encoding <encoding>", "allowed encoding; can be repeated", collectPath, []).option("--fail-on <policies>", "comma-separated policies: unsafe,ambiguous,mixed-eol,non-utf8,disallowed-encoding").option("--json", "emit JSON").option("--compact", "emit compact JSON for agent contexts").option("--pretty", "emit human-readable summary").action(runGuardCommand));
1029
+ return program;
1030
+ }
1031
+ function run(argv = process.argv) {
1032
+ try {
1033
+ createProgram().parse(argv);
1034
+ } catch (error) {
1035
+ const message = error instanceof Error ? error.message : "CLI parse failed.";
1036
+ writeUsageError(parseCommand(argv), "NOJIBAKE_CLI_USAGE", message);
1037
+ }
1038
+ }
1039
+ function isDirectCliInvocation(argv = process.argv) {
1040
+ const argvPath = argv[1];
1041
+ if (argvPath === void 0) return false;
1042
+ try {
1043
+ return realpathSync2(fileURLToPath2(import.meta.url)) === realpathSync2(argvPath);
1044
+ } catch {
1045
+ return import.meta.url === `file://${argvPath.replace(/\\/g, "/")}` || argvPath.endsWith("cli.js");
1046
+ }
1047
+ }
1048
+ if (isDirectCliInvocation()) {
1049
+ run();
1050
+ }
1051
+ export {
1052
+ createProgram,
1053
+ run
1054
+ };