@clossys/butler 0.1.1

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/LICENSE +21 -0
  3. package/README.md +260 -0
  4. package/dist/audit-shape.check.d.ts +32 -0
  5. package/dist/audit-shape.check.d.ts.map +1 -0
  6. package/dist/audit-shape.check.js +7 -0
  7. package/dist/audit-shape.check.js.map +1 -0
  8. package/dist/cli.d.ts +54 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +426 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/contract.d.ts +256 -0
  13. package/dist/contract.d.ts.map +1 -0
  14. package/dist/contract.js +377 -0
  15. package/dist/contract.js.map +1 -0
  16. package/dist/inbound/index.d.ts +120 -0
  17. package/dist/inbound/index.d.ts.map +1 -0
  18. package/dist/inbound/index.js +125 -0
  19. package/dist/inbound/index.js.map +1 -0
  20. package/dist/index.d.ts +50 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +47 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/schema.d.ts +374 -0
  25. package/dist/schema.d.ts.map +1 -0
  26. package/dist/schema.js +304 -0
  27. package/dist/schema.js.map +1 -0
  28. package/dist/validation.d.ts +74 -0
  29. package/dist/validation.d.ts.map +1 -0
  30. package/dist/validation.js +140 -0
  31. package/dist/validation.js.map +1 -0
  32. package/dist/web/index.d.ts +5 -0
  33. package/dist/web/index.d.ts.map +1 -0
  34. package/dist/web/index.js +25 -0
  35. package/dist/web/index.js.map +1 -0
  36. package/dist/web/internal/peer-version.d.ts +53 -0
  37. package/dist/web/internal/peer-version.d.ts.map +1 -0
  38. package/dist/web/internal/peer-version.js +136 -0
  39. package/dist/web/internal/peer-version.js.map +1 -0
  40. package/dist/web/useStandingWants.d.ts +75 -0
  41. package/dist/web/useStandingWants.d.ts.map +1 -0
  42. package/dist/web/useStandingWants.js +66 -0
  43. package/dist/web/useStandingWants.js.map +1 -0
  44. package/package.json +93 -0
  45. package/src/audit-shape.check.ts +37 -0
  46. package/src/cli.ts +445 -0
  47. package/src/contract.ts +534 -0
  48. package/src/inbound/index.ts +190 -0
  49. package/src/index.ts +113 -0
  50. package/src/schema.ts +622 -0
  51. package/src/validation.ts +172 -0
  52. package/src/web/index.ts +27 -0
  53. package/src/web/internal/peer-version.ts +159 -0
  54. package/src/web/useStandingWants.ts +139 -0
package/src/cli.ts ADDED
@@ -0,0 +1,445 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `butler-check` — one bin, three gates, dispatched on `argv[0]` matching a
4
+ * subcommand name EXACTLY.
5
+ *
6
+ * Dispatch is on `argv[0]`, never on `basename(process.argv[1])`. A bin-name
7
+ * dispatch would see `cli.js` wherever this gate is invoked by its compiled
8
+ * path — which is exactly how this repository's own `npm run check` invokes
9
+ * other packages' gates — and would silently run the wrong command, or none.
10
+ *
11
+ * Presentation only: parse argv, load JSON files, run the pure checkers in
12
+ * `contract.ts`, print a report, pick an exit code. Every decision worth
13
+ * testing lives in `contract.ts` and is tested there directly, against
14
+ * plain values, with no filesystem involved.
15
+ *
16
+ * EXIT CODES — the contract a consumer's CI depends on (this repository's
17
+ * own contribution guide, "Gate CLIs exit `0` clean, `1` findings, `2`
18
+ * could not run"):
19
+ *
20
+ * 0 — ran cleanly against a non-empty record set and found nothing.
21
+ * 1 — ran cleanly and found at least one real violation.
22
+ * 2 — could not run. Kept strictly distinct from `1`, because "I checked
23
+ * and it is fine" and "I never checked" are different answers and a
24
+ * gate that reports the second as the first is worse than no gate.
25
+ *
26
+ * `2` is genuinely reachable here, on every subcommand, and each route is
27
+ * tested:
28
+ * - a record store that cannot be read — missing file, unreadable file,
29
+ * a directory where a file was named, invalid JSON, or JSON that does
30
+ * not validate against this package's own schema;
31
+ * - nothing to scan — an empty record set, which is not a clean run, it
32
+ * is a run that examined nothing;
33
+ * - a required declared value that was not supplied — the confidence
34
+ * floor, or the denial-invalidation policy. Neither has a default
35
+ * anywhere in this package, and a run missing one declines rather than
36
+ * inventing one of the consumer's own values;
37
+ * - NO GATE SELECTED AT ALL. A bare `butler-check` with no subcommand is
38
+ * a run that never happened, and it exits `2`, not `0` — see `main()`'s
39
+ * own comment for why an explicitly requested `--help` is the one
40
+ * argument-shaped `0` here and a dropped argument is not.
41
+ */
42
+
43
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
44
+ import { resolve } from "node:path";
45
+ import { fileURLToPath } from "node:url";
46
+ import {
47
+ checkConfirmationCompleteness,
48
+ checkCurrency,
49
+ checkWithdrawalParity,
50
+ type ConfirmationCompletenessResult,
51
+ type CurrencyResult,
52
+ type WithdrawalParityResult,
53
+ } from "./contract.js";
54
+ import {
55
+ validateConfirmationRecords,
56
+ validateInstructionUsages,
57
+ validateIntentRecords,
58
+ validatePreferencePaths,
59
+ validateStandingInstructions,
60
+ } from "./schema.js";
61
+ import type { ValidationResult } from "./validation.js";
62
+
63
+ const USAGE = `Usage: butler-check <gate> [arguments]
64
+
65
+ Gates:
66
+ confirmation-completeness <intents-file> <confirmations-file> --floor <0..1>
67
+ Fails when an acted-on intent has no confirmation record, when it was
68
+ acted on against a "misread" or "unclear" read-back, or when a reading
69
+ below the declared floor was acted on with no explicit hand-off.
70
+
71
+ currency <instructions-file> <usages-file> --invalidate-denial-on-policy-bump <true|false>
72
+ Fails when a standing instruction was relied on past its own declared
73
+ window, after the policy version it answered was superseded, or while
74
+ there was no answer on record at all.
75
+
76
+ withdrawal-parity <paths-file>
77
+ Fails when withdrawing takes more steps than granting, demands a
78
+ contact or an account granting did not, or is not offered at all.
79
+
80
+ Options:
81
+ --help Print this message and exit 0. Also available per gate.
82
+
83
+ Exit codes: 0 = clean, 1 = at least one finding, 2 = could not run (bad
84
+ arguments, a missing/unreadable/unparseable/invalid file, an empty record
85
+ set, or a required declared value that was not supplied).
86
+ `;
87
+
88
+ const CONFIRMATION_USAGE = `Usage: butler-check confirmation-completeness <intents-file> <confirmations-file> --floor <0..1>
89
+
90
+ intents-file JSON array of IntentRecord objects. Required.
91
+ confirmations-file JSON array of ConfirmationRecord objects. Required. May be an empty array — an empty CONFIRMATION set is a real, checkable state (every acted-on intent is then unconfirmed); an empty INTENT set is not, and exits 2.
92
+ --floor <0..1> The declared confidence floor. Required, with no default: the number below which a reading is too weak to act on is one of the consumer's own values, and this gate will not invent one.
93
+
94
+ Exit codes: 0 = every acted-on intent is accounted for, 1 = at least one is not, 2 = could not run.
95
+ `;
96
+
97
+ const CURRENCY_USAGE = `Usage: butler-check currency <instructions-file> <usages-file> --invalidate-denial-on-policy-bump <true|false>
98
+
99
+ instructions-file JSON array of StandingInstruction objects. Required.
100
+ usages-file JSON array of InstructionUsage objects — one entry per occasion an instruction was actually relied on. Required.
101
+ --invalidate-denial-on-policy-bump <true|false>
102
+ Whether a policy-version bump also invalidates a stored denial. Required, with no default in either direction: that is a jurisdiction judgment this package does not make.
103
+
104
+ Exit codes: 0 = every usage relied on a current answer, 1 = at least one did not, 2 = could not run.
105
+ `;
106
+
107
+ const WITHDRAWAL_USAGE = `Usage: butler-check withdrawal-parity <paths-file>
108
+
109
+ paths-file JSON array of PreferencePath objects: for one surface and one topic, the measured cost of the grant route and of the withdraw route. Required.
110
+
111
+ Exit codes: 0 = withdrawing is no harder than granting anywhere, 1 = somewhere it is, 2 = could not run.
112
+ `;
113
+
114
+ const GATES = ["confirmation-completeness", "currency", "withdrawal-parity"] as const;
115
+
116
+ /** Exported for `cli.test.ts` — anything wrong with the arguments themselves always maps to exit code 2, never 1. */
117
+ export class CliInputError extends Error {}
118
+
119
+ interface ParsedArgs {
120
+ positional: string[];
121
+ options: Map<string, string>;
122
+ help: boolean;
123
+ }
124
+
125
+ /**
126
+ * One argv parser for all three gates. `--name value` and `--name=value`
127
+ * are both accepted; an unknown flag is a `CliInputError`, never a silently
128
+ * ignored argument — a typo in a flag that carries a required declared
129
+ * value must not read as that value being absent by choice.
130
+ */
131
+ function parseArgs(argv: string[], knownOptions: readonly string[]): ParsedArgs {
132
+ const positional: string[] = [];
133
+ const options = new Map<string, string>();
134
+ let help = false;
135
+
136
+ for (let i = 0; i < argv.length; i++) {
137
+ const arg = argv[i] as string;
138
+ if (arg === "--help" || arg === "-h") {
139
+ help = true;
140
+ continue;
141
+ }
142
+ if (!arg.startsWith("-")) {
143
+ positional.push(arg);
144
+ continue;
145
+ }
146
+ const equals = arg.indexOf("=");
147
+ const name = equals === -1 ? arg : arg.slice(0, equals);
148
+ if (!knownOptions.includes(name)) throw new CliInputError(`unknown flag "${name}"`);
149
+ if (equals !== -1) {
150
+ options.set(name, arg.slice(equals + 1));
151
+ continue;
152
+ }
153
+ const next = argv[i + 1];
154
+ if (next === undefined || next.startsWith("-")) throw new CliInputError(`flag "${name}" requires a value`);
155
+ options.set(name, next);
156
+ i++;
157
+ }
158
+
159
+ return { positional, options, help };
160
+ }
161
+
162
+ /**
163
+ * Why this record store cannot be read, or `undefined` if it can be.
164
+ *
165
+ * Deliberately NOT a `CliInputError`. A malformed argument is the caller's
166
+ * mistake and belongs with the other argv errors; a record store that is
167
+ * missing, unreadable, or is a directory where a file was named is the
168
+ * store's condition, and it is exactly the "could not run" state this
169
+ * gate's `2` exists for. Reporting it through the normal return path
170
+ * rather than an exception is what lets `main()` itself be observed
171
+ * returning `2` for it — a decline path nothing ever exercises is a
172
+ * decline path nobody knows works.
173
+ */
174
+ function fileProblem(label: string, path: string): string | undefined {
175
+ if (!existsSync(path)) return `${label} "${path}" does not exist`;
176
+ let stat;
177
+ try {
178
+ stat = statSync(path);
179
+ } catch (error) {
180
+ return `cannot stat ${label} "${path}": ${error instanceof Error ? error.message : String(error)}`;
181
+ }
182
+ if (!stat.isFile()) return `${label} "${path}" is not a file`;
183
+ return undefined;
184
+ }
185
+
186
+ type JsonReadResult = { ok: true; value: unknown } | { ok: false; detail: string };
187
+
188
+ /**
189
+ * Reads and JSON-parses a file, never throwing: an I/O failure or a syntax
190
+ * error comes back as `{ ok: false, detail }`. This CLI is the only place
191
+ * in this package that reads anything off disk — every checker in
192
+ * `contract.ts` is pure.
193
+ */
194
+ function readJsonFile(label: string, path: string): JsonReadResult {
195
+ let raw: string;
196
+ try {
197
+ raw = readFileSync(path, "utf8");
198
+ } catch (error) {
199
+ return { ok: false, detail: `cannot read ${label} "${path}": ${error instanceof Error ? error.message : String(error)}` };
200
+ }
201
+ try {
202
+ return { ok: true, value: JSON.parse(raw) };
203
+ } catch (error) {
204
+ return { ok: false, detail: `${label} "${path}" is not valid JSON: ${error instanceof Error ? error.message : String(error)}` };
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Load one record file and validate it against this package's own schema.
210
+ * Returns the records, or `undefined` having already printed why — a file
211
+ * that is missing, unreadable, unparseable, or schema-invalid never
212
+ * reaches a checker at all: it is "could not run", decided here.
213
+ */
214
+ function loadRecords<T>(label: string, path: string, validate: (value: unknown) => ValidationResult<T[]>): T[] | undefined {
215
+ const problem = fileProblem(label, path);
216
+ if (problem !== undefined) {
217
+ console.error(`\n${label} could not be loaded: ${problem}`);
218
+ console.error("Refusing to report a pass against a record store that could not be read.");
219
+ return undefined;
220
+ }
221
+ const json = readJsonFile(label, path);
222
+ if (!json.ok) {
223
+ console.error(`\n${label} could not be loaded: ${json.detail}`);
224
+ console.error("Refusing to report a pass against a record store that could not be read.");
225
+ return undefined;
226
+ }
227
+ const shape = validate(json.value);
228
+ if (!shape.ok) {
229
+ console.error(`\n${label} "${path}" did not validate:`);
230
+ for (const issue of shape.issues) console.error(` ${issue.path}: ${issue.message}`);
231
+ console.error("Refusing to report a pass against records this package cannot trust.");
232
+ return undefined;
233
+ }
234
+ return shape.value;
235
+ }
236
+
237
+ /** `--floor 0.8`. No default: a missing or unparseable floor is a decline, never an assumed number. */
238
+ function parseFloor(raw: string | undefined): number {
239
+ if (raw === undefined) throw new CliInputError("--floor is required; this gate has no default confidence floor");
240
+ const value = Number(raw);
241
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
242
+ throw new CliInputError(`--floor must be a number between 0 and 1 inclusive, got "${raw}"`);
243
+ }
244
+ return value;
245
+ }
246
+
247
+ /** Exactly `true` or `false`. No default in either direction, and no truthiness: "yes", "1" and "" are all input errors. */
248
+ function parseBooleanFlag(name: string, raw: string | undefined): boolean {
249
+ if (raw === undefined) throw new CliInputError(`${name} is required; this gate has no default in either direction`);
250
+ if (raw === "true") return true;
251
+ if (raw === "false") return false;
252
+ throw new CliInputError(`${name} must be exactly "true" or "false", got "${raw}"`);
253
+ }
254
+
255
+ function printConfirmationReport(result: ConfirmationCompletenessResult): void {
256
+ console.log(
257
+ `${result.intentsChecked} intent(s) checked against ${result.confirmationsChecked} confirmation(s), floor ${result.floorApplied}.`,
258
+ );
259
+ for (const finding of result.findings) {
260
+ console.log(` [${finding.kind}] ${finding.intentId}${finding.actorId ? ` (actor ${finding.actorId})` : ""} — ${finding.message}`);
261
+ }
262
+ if (result.ok) console.log("Confirmation completeness: satisfied.");
263
+ else if (result.reason === "unconfirmed-intents") console.log("Confirmation completeness: violated.");
264
+ else console.log(`Confirmation completeness: indeterminate (${result.reason}).`);
265
+ }
266
+
267
+ function printCurrencyReport(result: CurrencyResult): void {
268
+ console.log(`${result.instructionsChecked} standing instruction(s) checked against ${result.usagesChecked} usage(s).`);
269
+ for (const finding of result.findings) {
270
+ console.log(` [${finding.kind}] ${finding.instructionId} (actor ${finding.actorId}, used ${finding.usedAt}) — ${finding.message}`);
271
+ }
272
+ if (result.ok) console.log("Currency: satisfied.");
273
+ else if (result.reason === "stale-instructions-used") console.log("Currency: violated.");
274
+ else console.log(`Currency: indeterminate (${result.reason}).`);
275
+ }
276
+
277
+ function printWithdrawalReport(result: WithdrawalParityResult): void {
278
+ console.log(`${result.pathsChecked} preference path(s) checked.`);
279
+ for (const finding of result.findings) {
280
+ console.log(` [${finding.kind}] ${finding.surfaceId} / ${finding.topic} — ${finding.message}`);
281
+ }
282
+ if (result.ok) console.log("Withdrawal parity: satisfied.");
283
+ else if (result.reason === "withdrawal-harder-than-granting") console.log("Withdrawal parity: violated.");
284
+ else console.log(`Withdrawal parity: indeterminate (${result.reason}).`);
285
+ }
286
+
287
+ /**
288
+ * The one mapping every gate shares: a real violation is `1`; an
289
+ * indeterminate reason — nothing was compared — is `2`, never `0` and never
290
+ * conflated with a violation.
291
+ */
292
+ function exitCodeFor(ok: boolean, reason: string | undefined, violationReason: string): number {
293
+ if (ok) return 0;
294
+ return reason === violationReason ? 1 : 2;
295
+ }
296
+
297
+ function runConfirmationCompleteness(argv: string[]): number {
298
+ const args = parseArgs(argv, ["--floor"]);
299
+ if (args.help) {
300
+ console.log(CONFIRMATION_USAGE);
301
+ return 0;
302
+ }
303
+ const [intentsArg, confirmationsArg, ...extra] = args.positional;
304
+ if (intentsArg === undefined) throw new CliInputError("intents-file is required");
305
+ if (confirmationsArg === undefined) throw new CliInputError("confirmations-file is required");
306
+ if (extra.length > 0) throw new CliInputError(`unexpected extra argument "${extra[0]}"`);
307
+ const minimumConfidence = parseFloor(args.options.get("--floor"));
308
+
309
+ const intentsFile = resolve(intentsArg);
310
+ const confirmationsFile = resolve(confirmationsArg);
311
+ console.log(`Intents file: ${intentsFile}`);
312
+ console.log(`Confirmations file: ${confirmationsFile}`);
313
+
314
+ const intents = loadRecords("intents-file", intentsFile, validateIntentRecords);
315
+ if (intents === undefined) return 2;
316
+ const confirmations = loadRecords("confirmations-file", confirmationsFile, validateConfirmationRecords);
317
+ if (confirmations === undefined) return 2;
318
+
319
+ const result = checkConfirmationCompleteness(intents, confirmations, { minimumConfidence });
320
+ printConfirmationReport(result);
321
+ return exitCodeFor(result.ok, result.reason, "unconfirmed-intents");
322
+ }
323
+
324
+ function runCurrency(argv: string[]): number {
325
+ const args = parseArgs(argv, ["--invalidate-denial-on-policy-bump"]);
326
+ if (args.help) {
327
+ console.log(CURRENCY_USAGE);
328
+ return 0;
329
+ }
330
+ const [instructionsArg, usagesArg, ...extra] = args.positional;
331
+ if (instructionsArg === undefined) throw new CliInputError("instructions-file is required");
332
+ if (usagesArg === undefined) throw new CliInputError("usages-file is required");
333
+ if (extra.length > 0) throw new CliInputError(`unexpected extra argument "${extra[0]}"`);
334
+ const invalidateDenialOnPolicyBump = parseBooleanFlag(
335
+ "--invalidate-denial-on-policy-bump",
336
+ args.options.get("--invalidate-denial-on-policy-bump"),
337
+ );
338
+
339
+ const instructionsFile = resolve(instructionsArg);
340
+ const usagesFile = resolve(usagesArg);
341
+ console.log(`Instructions file: ${instructionsFile}`);
342
+ console.log(`Usages file: ${usagesFile}`);
343
+
344
+ const instructions = loadRecords("instructions-file", instructionsFile, validateStandingInstructions);
345
+ if (instructions === undefined) return 2;
346
+ const usages = loadRecords("usages-file", usagesFile, validateInstructionUsages);
347
+ if (usages === undefined) return 2;
348
+
349
+ const result = checkCurrency(instructions, usages, { invalidateDenialOnPolicyBump });
350
+ printCurrencyReport(result);
351
+ return exitCodeFor(result.ok, result.reason, "stale-instructions-used");
352
+ }
353
+
354
+ function runWithdrawalParity(argv: string[]): number {
355
+ const args = parseArgs(argv, []);
356
+ if (args.help) {
357
+ console.log(WITHDRAWAL_USAGE);
358
+ return 0;
359
+ }
360
+ const [pathsArg, ...extra] = args.positional;
361
+ if (pathsArg === undefined) throw new CliInputError("paths-file is required");
362
+ if (extra.length > 0) throw new CliInputError(`unexpected extra argument "${extra[0]}"`);
363
+
364
+ const pathsFile = resolve(pathsArg);
365
+ console.log(`Paths file: ${pathsFile}`);
366
+
367
+ const paths = loadRecords("paths-file", pathsFile, validatePreferencePaths);
368
+ if (paths === undefined) return 2;
369
+
370
+ const result = checkWithdrawalParity(paths);
371
+ printWithdrawalReport(result);
372
+ return exitCodeFor(result.ok, result.reason, "withdrawal-harder-than-granting");
373
+ }
374
+
375
+ /**
376
+ * Exported (unlike a typical CLI `main`) so `cli.test.ts` can exercise the
377
+ * whole argv-to-exit-code contract directly, against real `mkdtemp` temp
378
+ * directories, without spawning a subprocess per case. Takes `argv` as a
379
+ * parameter rather than reading `process.argv` itself for exactly that
380
+ * reason — `run()` below is the only caller that reads the real
381
+ * `process.argv`.
382
+ */
383
+ export function main(argv: string[]): number {
384
+ const first = argv[0];
385
+
386
+ // An EXPLICITLY requested `--help` is a run that did exactly what was
387
+ // asked, so it is `0`. A BARE invocation is not: no gate was selected, so
388
+ // nothing was checked, and reporting that clean is the precise fail-open
389
+ // shape this repository's own contribution guide forbids ("a check that
390
+ // cannot run must fail (`2`), never pass (`0`)"). A CI step with a dropped
391
+ // argument, a wrapper that loses `$1`, or a gate renamed out from under
392
+ // its caller all arrive here, and all three must go red rather than green
393
+ // on the strength of having examined nothing. Usage still prints — on
394
+ // stderr, because it is now a diagnostic rather than the thing asked for.
395
+ if (first === "--help" || first === "-h") {
396
+ console.log(USAGE);
397
+ return 0;
398
+ }
399
+ if (first === undefined) {
400
+ console.error("butler-check: no gate selected, so nothing was checked.");
401
+ console.error(`\n${USAGE}`);
402
+ return 2;
403
+ }
404
+ if (first === "confirmation-completeness") return runConfirmationCompleteness(argv.slice(1));
405
+ if (first === "currency") return runCurrency(argv.slice(1));
406
+ if (first === "withdrawal-parity") return runWithdrawalParity(argv.slice(1));
407
+ throw new CliInputError(`unknown gate "${first}"; expected one of ${GATES.join(", ")}`);
408
+ }
409
+
410
+ function run(): void {
411
+ try {
412
+ process.exitCode = main(process.argv.slice(2));
413
+ } catch (error) {
414
+ if (error instanceof CliInputError) {
415
+ console.error(`butler-check: ${error.message}`);
416
+ console.error(`\n${USAGE}`);
417
+ process.exitCode = 2;
418
+ } else {
419
+ console.error(`butler-check: unexpected error: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
420
+ process.exitCode = 2;
421
+ }
422
+ }
423
+ }
424
+
425
+ /**
426
+ * `npm install` publishes `bin` entries as symlinks, so comparing
427
+ * `process.argv[1]` to `import.meta.url` without resolving symlinks on both
428
+ * sides fails the moment this file is invoked the only way it ships — as an
429
+ * installed CLI — and fails silently: `run()` never fires, nothing prints,
430
+ * and the exit code is 0. Resolving both real paths is the fix.
431
+ */
432
+ function detectMainModule(): boolean {
433
+ const argvPath = process.argv[1];
434
+ if (argvPath === undefined) return false;
435
+ const modulePath = fileURLToPath(import.meta.url);
436
+ try {
437
+ return realpathSync(resolve(argvPath)) === realpathSync(modulePath);
438
+ } catch {
439
+ return resolve(argvPath) === modulePath;
440
+ }
441
+ }
442
+
443
+ if (detectMainModule()) {
444
+ run();
445
+ }