finitemonkey 0.0.0 → 0.1.0-next.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.
@@ -0,0 +1,1302 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/brand.ts
4
+ import { isAbsolute, resolve } from "node:path";
5
+ var toAbsolutePath = (path, base) => isAbsolute(path) || !base ? resolve(path) : resolve(base, path);
6
+
7
+ // src/core/entry.ts
8
+ import { fileURLToPath } from "node:url";
9
+ var isEntryPoint = (moduleUrl, argv1, realpath, platform) => {
10
+ if (argv1 === void 0) return false;
11
+ const self = realpath(fileURLToPath(moduleUrl));
12
+ const invoked = realpath(argv1);
13
+ return platform === "win32" ? self.toLowerCase() === invoked.toLowerCase() : self === invoked;
14
+ };
15
+
16
+ // src/core/node-runtime.ts
17
+ import { spawnSync } from "node:child_process";
18
+ import { readFileSync, realpathSync } from "node:fs";
19
+
20
+ // src/core/parsed.ts
21
+ var accept = (value) => ({ ok: true, value });
22
+ var reject = (error) => ({ ok: false, error });
23
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
24
+ var parseJson = (text) => {
25
+ try {
26
+ return accept(JSON.parse(text));
27
+ } catch (error) {
28
+ return reject(error instanceof Error ? error.message : String(error));
29
+ }
30
+ };
31
+ var parseJsonObject = (text) => {
32
+ const parsed = parseJson(text);
33
+ if (!parsed.ok) return parsed;
34
+ return isRecord(parsed.value) ? accept(parsed.value) : reject("expected a JSON object");
35
+ };
36
+ var optionalString = (record, key) => {
37
+ const value = record[key];
38
+ return typeof value === "string" ? value : void 0;
39
+ };
40
+
41
+ // src/core/node-runtime.ts
42
+ var SAFE_TOKEN = /^[A-Za-z0-9._-]+$/;
43
+ var WINDOWS_NOT_FOUND = 9009;
44
+ var errnoCode = (error) => {
45
+ if (!(error instanceof Error) || !("code" in error)) return void 0;
46
+ return typeof error.code === "string" ? error.code : void 0;
47
+ };
48
+ var spawnProbe = (command, args, timeout) => {
49
+ const options = { encoding: "utf8", timeout, windowsHide: true };
50
+ return process.platform === "win32" ? spawnSync([command, ...args].join(" "), { ...options, shell: true }) : spawnSync(command, [...args], options);
51
+ };
52
+ var nodeProbe = (command, args, timeoutMs) => {
53
+ if (![command, ...args].every((token) => SAFE_TOKEN.test(token))) {
54
+ return { found: false, ok: false, stdout: "", timedOut: false };
55
+ }
56
+ const result = spawnProbe(command, args, timeoutMs);
57
+ const code = errnoCode(result.error);
58
+ const stdout = result.stdout;
59
+ return {
60
+ found: code !== "ENOENT" && result.status !== WINDOWS_NOT_FOUND,
61
+ ok: result.error === void 0 && result.status === 0,
62
+ stdout: typeof stdout === "string" ? stdout.trim() : "",
63
+ timedOut: code === "ETIMEDOUT"
64
+ };
65
+ };
66
+ var nodeReadText = (path) => {
67
+ try {
68
+ return accept(readFileSync(path, "utf8"));
69
+ } catch (error) {
70
+ return reject(error instanceof Error ? error.message : String(error));
71
+ }
72
+ };
73
+ var nodeRealpath = (path) => {
74
+ try {
75
+ return realpathSync(path);
76
+ } catch {
77
+ return path;
78
+ }
79
+ };
80
+ var nodeRuntime = () => ({
81
+ nodeVersion: process.versions.node,
82
+ readText: nodeReadText,
83
+ probe: nodeProbe,
84
+ startTimer: (ms, onFire) => {
85
+ const timer = setTimeout(onFire, ms);
86
+ return () => {
87
+ clearTimeout(timer);
88
+ };
89
+ }
90
+ });
91
+
92
+ // src/doctor/system.ts
93
+ var detectOs = ({ platform, env, runtime }) => {
94
+ if (platform === "darwin") return "macos";
95
+ if (platform === "win32") return "windows";
96
+ if (platform !== "linux") return "other";
97
+ if (env["WSL_DISTRO_NAME"] !== void 0 || env["WSL_INTEROP"] !== void 0) return "wsl";
98
+ const version = runtime.readText(toAbsolutePath("/proc/version"));
99
+ return version.ok && /microsoft/i.test(version.value) ? "wsl" : "linux";
100
+ };
101
+ var WSL_MOUNT = /^\/mnt\/[a-z](\/|$)/i;
102
+ var SYNCED = /(OneDrive|Dropbox|iCloud ?Drive|Mobile Documents|Google ?Drive)/i;
103
+ var NON_ASCII = /[^\x20-\x7e]/;
104
+ var pathTraits = (path) => ({
105
+ spaces: path.includes(" "),
106
+ nonAscii: NON_ASCII.test(path),
107
+ synced: SYNCED.test(path)
108
+ });
109
+ var checkOs = (env) => {
110
+ const os = detectOs(env);
111
+ const findings = [
112
+ { check: "os", status: "ok", messageKey: `doctor.os.${os}`, params: { platform: env.platform } }
113
+ ];
114
+ if (os === "wsl" && WSL_MOUNT.test(env.cwd)) {
115
+ findings.push({
116
+ check: "path.wsl_mount",
117
+ status: "warn",
118
+ messageKey: "doctor.path.wsl_mount",
119
+ params: { path: env.cwd },
120
+ fixKey: "doctor.path.fix_wsl_mount"
121
+ });
122
+ }
123
+ return findings;
124
+ };
125
+ var checkPath = ({ cwd }) => {
126
+ const traits = pathTraits(cwd);
127
+ const chars = traits.spaces || traits.nonAscii ? {
128
+ check: "path.chars",
129
+ status: "warn",
130
+ messageKey: "doctor.path.chars",
131
+ params: { path: cwd },
132
+ fixKey: "doctor.path.fix_chars"
133
+ } : { check: "path.chars", status: "ok", messageKey: "doctor.path.plain", params: { path: cwd } };
134
+ const synced = traits.synced ? [
135
+ {
136
+ check: "path.synced",
137
+ status: "warn",
138
+ messageKey: "doctor.path.synced",
139
+ params: { path: cwd },
140
+ fixKey: "doctor.path.fix_synced"
141
+ }
142
+ ] : [];
143
+ return [chars, ...synced];
144
+ };
145
+
146
+ // src/doctor/tools.ts
147
+ var MIN_NODE_MAJOR = 20;
148
+ var MIN_GIT = { major: 2, minor: 38 };
149
+ var PROBE_TIMEOUT_MS = { git: 5e3, claude: 8e3 };
150
+ var parseGitVersion = (output) => {
151
+ const match = /(\d+)\.(\d+)/.exec(output);
152
+ if (match?.[1] === void 0 || match[2] === void 0) return void 0;
153
+ return { major: Number(match[1]), minor: Number(match[2]) };
154
+ };
155
+ var isOlder = (version, min) => version.major < min.major || version.major === min.major && version.minor < min.minor;
156
+ var checkNode = ({ runtime }) => {
157
+ const version = runtime.nodeVersion.replace(/^v/, "");
158
+ const major = Number.parseInt(version.split(".")[0] ?? "", 10);
159
+ if (Number.isNaN(major) || major < MIN_NODE_MAJOR) {
160
+ return [
161
+ {
162
+ check: "node",
163
+ status: "fail",
164
+ messageKey: "doctor.node.old",
165
+ params: { version, min: MIN_NODE_MAJOR },
166
+ fixKey: "doctor.node.fix"
167
+ }
168
+ ];
169
+ }
170
+ return [{ check: "node", status: "ok", messageKey: "doctor.node.ok", params: { version } }];
171
+ };
172
+ var checkGit = ({ runtime }) => {
173
+ const result = runtime.probe("git", ["--version"], PROBE_TIMEOUT_MS.git);
174
+ if (!result.found || !result.ok) {
175
+ return [{ check: "git", status: "fail", messageKey: "doctor.git.missing", fixKey: "doctor.git.fix" }];
176
+ }
177
+ const parsed = parseGitVersion(result.stdout);
178
+ const version = parsed === void 0 ? result.stdout : `${String(parsed.major)}.${String(parsed.minor)}`;
179
+ if (parsed === void 0 || isOlder(parsed, MIN_GIT)) {
180
+ const min = `${String(MIN_GIT.major)}.${String(MIN_GIT.minor)}`;
181
+ return [
182
+ {
183
+ check: "git",
184
+ status: "warn",
185
+ messageKey: "doctor.git.old",
186
+ params: { version, min },
187
+ fixKey: "doctor.git.fix_old"
188
+ }
189
+ ];
190
+ }
191
+ return [{ check: "git", status: "ok", messageKey: "doctor.git.ok", params: { version } }];
192
+ };
193
+ var checkClaude = ({ runtime }) => {
194
+ const result = runtime.probe("claude", ["--version"], PROBE_TIMEOUT_MS.claude);
195
+ if (result.timedOut) {
196
+ return [
197
+ { check: "claude", status: "warn", messageKey: "doctor.claude.timeout", fixKey: "doctor.claude.fix_timeout" }
198
+ ];
199
+ }
200
+ if (!result.found || !result.ok) {
201
+ return [{ check: "claude", status: "warn", messageKey: "doctor.claude.missing", fixKey: "doctor.claude.fix" }];
202
+ }
203
+ const version = /\d+\.\d+\.\d+/.exec(result.stdout)?.[0] ?? result.stdout;
204
+ return [{ check: "claude", status: "ok", messageKey: "doctor.claude.ok", params: { version } }];
205
+ };
206
+
207
+ // src/doctor/run.ts
208
+ var DOCTOR_SCHEMA = "finitemonkey/doctor@1";
209
+ var CHECKS = [checkNode, checkGit, checkClaude, checkOs, checkPath];
210
+ var doctorEnvFrom = (ctx) => ({
211
+ platform: ctx.platform,
212
+ env: ctx.env,
213
+ cwd: ctx.cwd,
214
+ runtime: ctx.runtime
215
+ });
216
+ var runChecks = (env, checks = CHECKS) => checks.flatMap((check) => check(env));
217
+ var toItem = (ctx, finding) => ({
218
+ check: finding.check,
219
+ status: finding.status,
220
+ detail: ctx.t(finding.messageKey, finding.params),
221
+ ...finding.fixKey === void 0 ? {} : { fix: ctx.t(finding.fixKey, finding.params) }
222
+ });
223
+ var count = (items, status) => items.filter((item) => item.status === status).length;
224
+ var buildReport = (ctx, findings) => {
225
+ const checks = findings.map((finding) => toItem(ctx, finding));
226
+ return {
227
+ schema: DOCTOR_SCHEMA,
228
+ checks,
229
+ summary: { ok: count(checks, "ok"), warn: count(checks, "warn"), fail: count(checks, "fail") }
230
+ };
231
+ };
232
+
233
+ // src/exit-codes.ts
234
+ var ExitCode = {
235
+ Ok: 0,
236
+ CheckFailed: 1,
237
+ Usage: 2,
238
+ NotConfigured: 3,
239
+ NeedsPerson: 4,
240
+ Internal: 5
241
+ };
242
+ var EXIT_CODE_NAMES = [
243
+ "Ok",
244
+ "CheckFailed",
245
+ "Usage",
246
+ "NotConfigured",
247
+ "NeedsPerson",
248
+ "Internal"
249
+ ];
250
+ var exitCodeDocs = {
251
+ 0: "exit.ok",
252
+ 1: "exit.check_failed",
253
+ 2: "exit.usage",
254
+ 3: "exit.not_configured",
255
+ 4: "exit.needs_person",
256
+ 5: "exit.internal"
257
+ };
258
+
259
+ // src/commands/doctor.ts
260
+ var MARK = { ok: "ok ", warn: "warn", fail: "FAIL" };
261
+ var itemLines = (ctx, item) => [
262
+ `[${MARK[item.status]}] ${item.check}: ${item.detail}`,
263
+ ...item.fix !== void 0 && item.status !== "ok" ? [` ${ctx.t("output.fix", { fix: item.fix })}`] : []
264
+ ];
265
+ var doctorResult = (ctx, report) => {
266
+ const human = [
267
+ ctx.t("doctor.title", { version: ctx.version }),
268
+ ...report.checks.flatMap((item) => itemLines(ctx, item)),
269
+ ctx.t("doctor.summary", report.summary)
270
+ ];
271
+ const failed2 = report.checks.find((item) => item.status === "fail");
272
+ if (failed2 === void 0) return { ok: true, exitCode: ExitCode.Ok, data: report, human };
273
+ return {
274
+ ok: false,
275
+ exitCode: ExitCode.CheckFailed,
276
+ data: report,
277
+ human,
278
+ error: {
279
+ code: "doctor_failed",
280
+ message: ctx.t("error.doctor_failed", { count: report.summary.fail }),
281
+ fix: failed2.fix ?? "finitemonkey doctor"
282
+ }
283
+ };
284
+ };
285
+ var doctorCommand = {
286
+ path: ["doctor"],
287
+ summaryKey: "command.doctor.summary",
288
+ usage: "finitemonkey doctor [--json]",
289
+ status: "ready",
290
+ run: (ctx) => Promise.resolve(doctorResult(ctx, buildReport(ctx, runChecks(doctorEnvFrom(ctx)))))
291
+ };
292
+
293
+ // src/core/args.ts
294
+ import { parseArgs } from "node:util";
295
+
296
+ // src/core/problem.ts
297
+ var usageProblem = (code, messageKey, fix, params = {}) => ({
298
+ code,
299
+ messageKey,
300
+ params,
301
+ fix,
302
+ exitCode: ExitCode.Usage
303
+ });
304
+ var FmError = class extends Error {
305
+ problem;
306
+ constructor(problem) {
307
+ super(problem.code);
308
+ this.name = "FmError";
309
+ this.problem = problem;
310
+ }
311
+ };
312
+
313
+ // src/core/args.ts
314
+ var GLOBAL_OPTIONS = {
315
+ json: { type: "boolean" },
316
+ lang: { type: "string" },
317
+ yes: { type: "boolean", short: "y" },
318
+ "dry-run": { type: "boolean" },
319
+ version: { type: "boolean", short: "v" },
320
+ help: { type: "boolean", short: "h" }
321
+ };
322
+ var flag = (values, name) => values[name] === true;
323
+ var looseArgs = (argv) => {
324
+ const { values, positionals } = parseArgs({
325
+ args: [...argv],
326
+ options: GLOBAL_OPTIONS,
327
+ strict: false,
328
+ allowPositionals: true
329
+ });
330
+ const lang = values.lang;
331
+ return {
332
+ positionals,
333
+ json: flag(values, "json"),
334
+ yes: flag(values, "yes"),
335
+ dryRun: flag(values, "dry-run"),
336
+ version: flag(values, "version"),
337
+ help: flag(values, "help"),
338
+ lang: typeof lang === "string" ? lang : void 0
339
+ };
340
+ };
341
+ var resolveCommand = (registry, positionals) => registry.filter((spec) => spec.path.every((part, index) => positionals[index] === part)).reduce(
342
+ (best, spec) => best === void 0 || spec.path.length > best.consumed ? { spec, consumed: spec.path.length } : best,
343
+ void 0
344
+ );
345
+ var strictArgs = (argv, spec) => {
346
+ const fix = `finitemonkey help ${spec.path.join(" ")}`;
347
+ let parsed;
348
+ try {
349
+ parsed = parseArgs({
350
+ args: [...argv],
351
+ options: { ...GLOBAL_OPTIONS, ...spec.options },
352
+ strict: true,
353
+ allowPositionals: true
354
+ });
355
+ } catch (error) {
356
+ const detail = error instanceof Error ? error.message : String(error);
357
+ return reject(usageProblem("invalid_arguments", "error.invalid_arguments", fix, { detail }));
358
+ }
359
+ const rest = parsed.positionals.slice(spec.path.length);
360
+ const max = spec.maxPositionals ?? 0;
361
+ const extra = rest[max];
362
+ if (extra !== void 0) {
363
+ return reject(usageProblem("unexpected_argument", "error.unexpected_argument", fix, { argument: extra }));
364
+ }
365
+ return accept({ values: parsed.values, positionals: rest });
366
+ };
367
+ var stringOption = (values, name) => {
368
+ const value = values[name];
369
+ return typeof value === "string" ? value : void 0;
370
+ };
371
+
372
+ // src/core/result.ts
373
+ var SCHEMA_VERSION = 1;
374
+ var success = (data, human = []) => ({
375
+ ok: true,
376
+ exitCode: ExitCode.Ok,
377
+ data,
378
+ human
379
+ });
380
+ var failureFrom = (t, problem, human) => ({
381
+ ok: false,
382
+ exitCode: problem.exitCode,
383
+ error: { code: problem.code, message: t(problem.messageKey, problem.params), fix: problem.fix },
384
+ ...problem.data === void 0 ? {} : { data: problem.data },
385
+ ...human === void 0 ? {} : { human }
386
+ });
387
+ var toEnvelope = (command, result) => ({
388
+ schemaVersion: SCHEMA_VERSION,
389
+ ok: result.ok,
390
+ command,
391
+ ...result.data === void 0 ? {} : { data: result.data },
392
+ ...result.ok ? {} : { error: result.error },
393
+ ...result.next === void 0 ? {} : { next: result.next }
394
+ });
395
+
396
+ // src/commands/types.ts
397
+ var commandName = (spec) => spec.path.join(" ");
398
+
399
+ // src/commands/help.ts
400
+ var GLOBAL_FLAGS = [
401
+ { flag: "--json", key: "flag.json" },
402
+ { flag: "--lang <pt-BR|en|es>", key: "flag.lang" },
403
+ { flag: "--yes, -y", key: "flag.yes" },
404
+ { flag: "--dry-run", key: "flag.dry_run" },
405
+ { flag: "--version, -v", key: "flag.version" },
406
+ { flag: "--help, -h", key: "flag.help" }
407
+ ];
408
+ var exitCodeHelp = (ctx) => EXIT_CODE_NAMES.map((name) => ({ code: ExitCode[name], name, summary: ctx.t(exitCodeDocs[ExitCode[name]]) }));
409
+ var pad = (text, width) => text.padEnd(Math.max(width, text.length + 1));
410
+ var describe = (ctx, spec) => ({
411
+ name: commandName(spec),
412
+ usage: spec.usage,
413
+ summary: ctx.t(spec.summaryKey),
414
+ status: spec.status,
415
+ options: Object.keys(spec.options ?? {}).map((name) => `--${name}`)
416
+ });
417
+ var summaryLine = (ctx, help) => help.status === "planned" ? `${help.summary} ${ctx.t("help.planned")}` : help.summary;
418
+ var overview = (ctx, registry) => {
419
+ const commands = registry.map((spec) => describe(ctx, spec));
420
+ const flags = GLOBAL_FLAGS.map(({ flag: flag2, key }) => ({ flag: flag2, summary: ctx.t(key) }));
421
+ const width = Math.max(...commands.map(({ name }) => name.length)) + 2;
422
+ const flagWidth = Math.max(...flags.map(({ flag: flag2 }) => flag2.length)) + 2;
423
+ const exitCodes = exitCodeHelp(ctx);
424
+ const human = [
425
+ `finitemonkey ${ctx.version}`,
426
+ ctx.t("help.tagline"),
427
+ "",
428
+ `${ctx.t("help.usage")}: finitemonkey <${ctx.t("help.command")}> [flags]`,
429
+ "",
430
+ `${ctx.t("help.commands")}:`,
431
+ ...commands.map((help) => ` ${pad(help.name, width)}${summaryLine(ctx, help)}`),
432
+ "",
433
+ `${ctx.t("help.flags")}:`,
434
+ ...flags.map(({ flag: flag2, summary }) => ` ${pad(flag2, flagWidth)}${summary}`),
435
+ "",
436
+ `${ctx.t("help.exit_codes")}:`,
437
+ ...exitCodes.map(({ summary }) => ` ${summary}`),
438
+ "",
439
+ ctx.t("help.more")
440
+ ];
441
+ return success({ commands, flags, exitCodes }, human);
442
+ };
443
+ var helpFor = (ctx, spec) => {
444
+ const help = describe(ctx, spec);
445
+ const human = [
446
+ `${ctx.t("help.usage")}: ${help.usage}`,
447
+ "",
448
+ summaryLine(ctx, help),
449
+ ...help.options.length > 0 ? ["", `${ctx.t("help.options")}: ${help.options.join(", ")}`] : []
450
+ ];
451
+ return success({ command: help }, human);
452
+ };
453
+ var topicHelp = (ctx, registry, topic) => {
454
+ const resolved = resolveCommand(registry, topic);
455
+ if (resolved !== void 0) return helpFor(ctx, resolved.spec);
456
+ const problem = usageProblem("unknown_command", "error.unknown_command", "finitemonkey help", {
457
+ command: topic.join(" ")
458
+ });
459
+ return failureFrom(ctx.t, problem);
460
+ };
461
+ var helpCommand = {
462
+ path: ["help"],
463
+ summaryKey: "command.help.summary",
464
+ usage: "finitemonkey help [command]",
465
+ status: "ready",
466
+ maxPositionals: 3,
467
+ run: (ctx, args, registry) => Promise.resolve(
468
+ args.positionals.length === 0 ? overview(ctx, registry) : topicHelp(ctx, registry, args.positionals)
469
+ )
470
+ };
471
+
472
+ // src/hooks/events.ts
473
+ var HOOK_EVENTS = [
474
+ "SessionStart",
475
+ "UserPromptSubmit",
476
+ "PreToolUse",
477
+ "PostToolUse",
478
+ "Stop",
479
+ "SubagentStart",
480
+ "SubagentStop",
481
+ "PreCompact",
482
+ "ConfigChange",
483
+ "Notification",
484
+ "SessionEnd"
485
+ ];
486
+ var HOOK_POLICIES = {
487
+ SessionStart: { failClosed: false },
488
+ UserPromptSubmit: { failClosed: true },
489
+ PreToolUse: { failClosed: true },
490
+ PostToolUse: { failClosed: false },
491
+ Stop: { failClosed: true },
492
+ SubagentStart: { failClosed: false },
493
+ SubagentStop: { failClosed: true },
494
+ PreCompact: { failClosed: false },
495
+ ConfigChange: { failClosed: true },
496
+ Notification: { failClosed: false },
497
+ SessionEnd: { failClosed: false }
498
+ };
499
+ var squash = (value) => value.replace(/[-_\s]/g, "").toLowerCase();
500
+ var parseHookEvent = (raw) => HOOK_EVENTS.find((event) => squash(event) === squash(raw));
501
+ var DEFAULT_HOOK_DEADLINE_MS = 3e3;
502
+ var parseDeadline = (raw) => {
503
+ const parsed = Number.parseInt(raw ?? "", 10);
504
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_HOOK_DEADLINE_MS;
505
+ };
506
+
507
+ // src/hooks/handlers.ts
508
+ var allow = () => ({ kind: "allow" });
509
+ var DEFAULT_HANDLERS = {};
510
+
511
+ // src/hooks/payload.ts
512
+ var EMPTY_PAYLOAD = {
513
+ hookEventName: void 0,
514
+ sessionId: void 0,
515
+ cwd: void 0,
516
+ stopHookActive: false,
517
+ raw: {}
518
+ };
519
+ var parseHookPayload = (event, text, interactive) => {
520
+ if (interactive && text.trim() === "") return accept(EMPTY_PAYLOAD);
521
+ const parsed = parseJsonObject(text);
522
+ if (!parsed.ok) return reject(`stdin: ${parsed.error}`);
523
+ const payload = {
524
+ hookEventName: optionalString(parsed.value, "hook_event_name"),
525
+ sessionId: optionalString(parsed.value, "session_id"),
526
+ cwd: optionalString(parsed.value, "cwd"),
527
+ stopHookActive: parsed.value["stop_hook_active"] === true,
528
+ raw: parsed.value
529
+ };
530
+ if (payload.hookEventName !== void 0 && payload.hookEventName !== event) {
531
+ return reject(`payload is for ${payload.hookEventName}, not ${event}`);
532
+ }
533
+ return accept(payload);
534
+ };
535
+
536
+ // src/hooks/stdin.ts
537
+ var readAll = (source) => new Promise((resolve2, reject2) => {
538
+ if (source.isTTY === true) {
539
+ resolve2("");
540
+ return;
541
+ }
542
+ const chunks = [];
543
+ source.setEncoding?.("utf8");
544
+ source.on("data", (chunk) => {
545
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
546
+ });
547
+ source.on("end", () => {
548
+ resolve2(chunks.join(""));
549
+ });
550
+ source.on("error", reject2);
551
+ });
552
+ var DeadlineError = class extends Error {
553
+ ms;
554
+ constructor(ms) {
555
+ super(`deadline of ${String(ms)} ms exceeded`);
556
+ this.name = "DeadlineError";
557
+ this.ms = ms;
558
+ }
559
+ };
560
+ var withDeadline = (work, ms, startTimer) => new Promise((resolve2, reject2) => {
561
+ const cancel = startTimer(ms, () => {
562
+ reject2(new DeadlineError(ms));
563
+ });
564
+ work.then(
565
+ (value) => {
566
+ cancel();
567
+ resolve2(value);
568
+ },
569
+ (error) => {
570
+ cancel();
571
+ reject2(error instanceof Error ? error : new Error(String(error)));
572
+ }
573
+ );
574
+ });
575
+
576
+ // src/hooks/runner.ts
577
+ var messageOf = (error) => error instanceof Error ? error.message : String(error);
578
+ var decide = async (input, handlers) => {
579
+ let output = {};
580
+ for (const handler of handlers[input.event] ?? [allow]) {
581
+ const decision = await handler(input);
582
+ if (decision.kind === "block") return decision;
583
+ output = { ...output, ...decision.output };
584
+ }
585
+ return { kind: "allow", output };
586
+ };
587
+ var evaluate = async (event, options) => {
588
+ const text = await readAll(options.stdin);
589
+ const parsed = parseHookPayload(event, text, options.stdin.isTTY === true);
590
+ if (!parsed.ok) return { kind: "failed", payload: void 0, detail: parsed.error };
591
+ const payload = parsed.value;
592
+ try {
593
+ return { kind: "decided", payload, decision: await decide({ event, payload, cwd: options.cwd }, options.handlers) };
594
+ } catch (error) {
595
+ return { kind: "failed", payload, detail: messageOf(error) };
596
+ }
597
+ };
598
+ var ALLOW_STDOUT = `${JSON.stringify({ continue: true })}
599
+ `;
600
+ var allowed = (output) => ({
601
+ exitCode: 0,
602
+ stdout: `${JSON.stringify({ continue: true, ...output })}
603
+ `,
604
+ stderr: "",
605
+ verdict: "allow"
606
+ });
607
+ var released = (event, detail, t) => ({
608
+ exitCode: 0,
609
+ stdout: ALLOW_STDOUT,
610
+ stderr: `finitemonkey: ${t("hook.stop_released", { event, detail })}
611
+ `,
612
+ verdict: "stop_released"
613
+ });
614
+ var failed = (event, detail, t) => HOOK_POLICIES[event].failClosed ? {
615
+ exitCode: 2,
616
+ stdout: "",
617
+ stderr: `finitemonkey: ${t("hook.failed_closed", { event, detail })}
618
+ `,
619
+ verdict: "failed_closed"
620
+ } : {
621
+ exitCode: 0,
622
+ stdout: "",
623
+ stderr: `finitemonkey: ${t("hook.failed_open", { event, detail })}
624
+ `,
625
+ verdict: "failed_open"
626
+ };
627
+ var conclude = (event, evaluation, t) => {
628
+ const stopActive = evaluation.payload?.stopHookActive === true;
629
+ if (evaluation.kind === "failed") {
630
+ return stopActive ? released(event, evaluation.detail, t) : failed(event, evaluation.detail, t);
631
+ }
632
+ const { decision } = evaluation;
633
+ if (decision.kind === "allow") return allowed(decision.output);
634
+ if (stopActive) return released(event, decision.reason, t);
635
+ return { exitCode: 2, stdout: "", stderr: `finitemonkey: ${decision.reason}
636
+ `, verdict: "block" };
637
+ };
638
+ var runHook = async (event, options) => {
639
+ try {
640
+ return conclude(
641
+ event,
642
+ await withDeadline(evaluate(event, options), options.deadlineMs, options.startTimer),
643
+ options.t
644
+ );
645
+ } catch (error) {
646
+ return failed(event, messageOf(error), options.t);
647
+ }
648
+ };
649
+
650
+ // src/commands/hook.ts
651
+ var hookCommand = {
652
+ path: ["hook"],
653
+ summaryKey: "command.hook.summary",
654
+ usage: `finitemonkey hook <${HOOK_EVENTS.join("|")}>`,
655
+ status: "ready",
656
+ maxPositionals: 1,
657
+ run: async (ctx, args) => {
658
+ const raw = args.positionals[0] ?? "";
659
+ const event = parseHookEvent(raw);
660
+ if (event === void 0) {
661
+ const params = { event: raw, events: HOOK_EVENTS.join(", ") };
662
+ return failureFrom(
663
+ ctx.t,
664
+ usageProblem("unknown_hook_event", "error.unknown_hook_event", "finitemonkey help hook", params)
665
+ );
666
+ }
667
+ const outcome = await runHook(event, {
668
+ stdin: ctx.io.stdin,
669
+ deadlineMs: parseDeadline(ctx.env["FINITEMONKEY_HOOK_DEADLINE_MS"]),
670
+ handlers: DEFAULT_HANDLERS,
671
+ cwd: ctx.cwd,
672
+ t: ctx.t,
673
+ startTimer: ctx.runtime.startTimer
674
+ });
675
+ const data = { event, verdict: outcome.verdict };
676
+ const output = { stdout: outcome.stdout, stderr: outcome.stderr };
677
+ if (outcome.exitCode === 0) return { ok: true, exitCode: 0, data, raw: output };
678
+ const message = outcome.stderr.trim();
679
+ return {
680
+ ok: false,
681
+ exitCode: 2,
682
+ data,
683
+ raw: output,
684
+ error: { code: "hook_blocked", message, fix: "finitemonkey doctor" }
685
+ };
686
+ }
687
+ };
688
+
689
+ // src/commands/i18n-check.ts
690
+ import { join } from "node:path";
691
+
692
+ // src/i18n/en.json
693
+ var en_default = {
694
+ "command.init.summary": "Set up Finite Monkey in this project",
695
+ "command.doctor.summary": "Check the environment and say how to fix each problem",
696
+ "command.update.summary": "Update the files Finite Monkey generated",
697
+ "command.uninstall.summary": "Remove Finite Monkey from this project",
698
+ "command.i18n_check.summary": "Check that the pt-BR, en and es catalogs match",
699
+ "command.hook.summary": "Answer a Claude Code hook event (reads JSON on stdin)",
700
+ "command.version.summary": "Print the version",
701
+ "command.help.summary": "Show commands and flags",
702
+ "help.tagline": "Infinite monkeys not required.",
703
+ "help.usage": "Usage",
704
+ "help.command": "command",
705
+ "help.commands": "Commands",
706
+ "help.flags": "Flags",
707
+ "help.options": "Options",
708
+ "help.planned": "(not available yet)",
709
+ "help.more": "Run `finitemonkey help <command>` for details. Add --json for machine-readable output.",
710
+ "flag.json": "Print one JSON object (schemaVersion 1) instead of text",
711
+ "flag.lang": "Language for messages",
712
+ "flag.yes": "Answer yes to confirmations",
713
+ "flag.dry_run": "Show what would change without changing anything",
714
+ "flag.version": "Print the version",
715
+ "flag.help": "Show help",
716
+ "output.error": "error: {message}",
717
+ "output.fix": "fix: {fix}",
718
+ "output.next": "next: {next}",
719
+ "error.internal": "Finite Monkey failed unexpectedly: {detail}",
720
+ "error.invalid_arguments": "Invalid arguments: {detail}",
721
+ "error.unexpected_argument": 'Unexpected argument "{argument}"',
722
+ "error.unknown_command": 'Unknown command "{command}"',
723
+ "error.unknown_command_suggest": 'Unknown command "{command}". Did you mean "{suggestion}"?',
724
+ "error.not_implemented": '"{command}" is not available in this version yet',
725
+ "error.invalid_lang": 'Unsupported language "{lang}". Use one of: {languages}',
726
+ "error.i18n_catalog_unreadable": "Could not read catalog {file}: {detail}",
727
+ "error.i18n_mismatch": "{count} catalog problem(s) found",
728
+ "error.doctor_failed": "{count} check(s) failed",
729
+ "error.unknown_hook_event": 'Unknown hook event "{event}". Use one of: {events}',
730
+ "i18n.summary": "Languages: {languages}. Keys: {keys}.",
731
+ "i18n.ok": "All catalogs match.",
732
+ "i18n.fix": "Edit the catalogs in {file} so every language has the same keys and placeholders",
733
+ "i18n.problem.missing": "missing key",
734
+ "i18n.problem.extra": "key not in en.json",
735
+ "i18n.problem.placeholder": "placeholders differ (expected: {expected}; found: {actual})",
736
+ "i18n.problem.empty": "empty text",
737
+ "i18n.problem.not_string": "value is not text",
738
+ "doctor.title": "finitemonkey doctor ({version})",
739
+ "doctor.summary": "{ok} ok, {warn} warning(s), {fail} failure(s)",
740
+ "doctor.node.ok": "Node.js {version}",
741
+ "doctor.node.old": "Node.js {version} is older than the minimum {min}",
742
+ "doctor.node.fix": "Install Node.js 20 or newer from https://nodejs.org",
743
+ "doctor.git.ok": "git {version}",
744
+ "doctor.git.missing": "git was not found",
745
+ "doctor.git.fix": "Install git from https://git-scm.com/downloads",
746
+ "doctor.git.old": "git {version} is older than {min}; some commands will need a newer git",
747
+ "doctor.git.fix_old": "Update git to {min} or newer",
748
+ "doctor.claude.ok": "Claude Code {version}",
749
+ "doctor.claude.missing": "Claude Code was not found; the CLI works without it, the hooks need it",
750
+ "doctor.claude.fix": "Install Claude Code: npm install -g @anthropic-ai/claude-code",
751
+ "doctor.claude.timeout": "`claude --version` did not answer in time",
752
+ "doctor.claude.fix_timeout": "Run `claude --version` by hand and check that it answers",
753
+ "doctor.os.linux": "Linux",
754
+ "doctor.os.wsl": "Linux on WSL",
755
+ "doctor.os.macos": "macOS",
756
+ "doctor.os.windows": "Windows (native)",
757
+ "doctor.os.other": "Unrecognized system ({platform})",
758
+ "doctor.path.plain": "Project path has no spaces or accents",
759
+ "doctor.path.chars": "Project path has spaces or accents: {path}",
760
+ "doctor.path.fix_chars": "Finite Monkey handles this path; if another tool fails here, move the project to a path without spaces or accents",
761
+ "doctor.path.synced": "Project is inside a synced folder (OneDrive, Dropbox, iCloud or Google Drive): {path}",
762
+ "doctor.path.fix_synced": "Move the project out of the synced folder to avoid sync conflicts and slow git",
763
+ "doctor.path.wsl_mount": "Project is on a Windows drive mounted in WSL: {path}",
764
+ "doctor.path.fix_wsl_mount": "Move the project into the Linux home (for example ~/projects) for faster git and correct permissions",
765
+ "hook.failed_closed": "the {event} hook could not run ({detail}), so it blocked to stay safe. Run `finitemonkey doctor`",
766
+ "hook.failed_open": "the {event} hook could not run ({detail}); continuing without it. Run `finitemonkey doctor`",
767
+ "exit.ok": "0 ok: the command did what it was asked",
768
+ "exit.check_failed": "1 check failed: the command ran and found a problem",
769
+ "exit.usage": "2 usage error: unknown command, unknown flag, bad value or command not available yet",
770
+ "exit.not_configured": "3 not configured: setup or a prerequisite (node, git, files, permissions) is missing",
771
+ "exit.needs_person": "4 needs a person: a decision or approval is required before going on",
772
+ "exit.internal": "5 internal error: Finite Monkey itself failed; please report it",
773
+ "help.exit_codes": "Exit codes",
774
+ "hook.stop_released": "the {event} hook would block again ({detail}), but Claude Code is already continuing from a stop hook, so it lets the session stop. Run `finitemonkey doctor`",
775
+ "error.needs_confirmation": '"{command}" needs your decision before it changes anything: {question}'
776
+ };
777
+
778
+ // src/i18n/es.json
779
+ var es_default = {
780
+ "command.init.summary": "Instala Finite Monkey en este proyecto",
781
+ "command.doctor.summary": "Revisa el entorno y dice cómo corregir cada problema",
782
+ "command.update.summary": "Actualiza los archivos que Finite Monkey generó",
783
+ "command.uninstall.summary": "Quita Finite Monkey de este proyecto",
784
+ "command.i18n_check.summary": "Revisa que los catálogos pt-BR, en y es coincidan",
785
+ "command.hook.summary": "Responde a un evento de hook de Claude Code (lee JSON de la entrada estándar)",
786
+ "command.version.summary": "Muestra la versión",
787
+ "command.help.summary": "Muestra comandos y opciones",
788
+ "help.tagline": "Infinite monkeys not required.",
789
+ "help.usage": "Uso",
790
+ "help.command": "comando",
791
+ "help.commands": "Comandos",
792
+ "help.flags": "Opciones globales",
793
+ "help.options": "Opciones",
794
+ "help.planned": "(aún no disponible)",
795
+ "help.more": "Ejecuta `finitemonkey help <comando>` para más detalles. Usa --json para una salida legible por máquina.",
796
+ "flag.json": "Imprime un objeto JSON (schemaVersion 1) en lugar de texto",
797
+ "flag.lang": "Idioma de los mensajes",
798
+ "flag.yes": "Responde sí a las confirmaciones",
799
+ "flag.dry_run": "Muestra qué cambiaría sin cambiar nada",
800
+ "flag.version": "Muestra la versión",
801
+ "flag.help": "Muestra la ayuda",
802
+ "output.error": "error: {message}",
803
+ "output.fix": "cómo corregir: {fix}",
804
+ "output.next": "siguiente paso: {next}",
805
+ "error.internal": "Finite Monkey falló de forma inesperada: {detail}",
806
+ "error.invalid_arguments": "Argumentos inválidos: {detail}",
807
+ "error.unexpected_argument": 'Argumento inesperado "{argument}"',
808
+ "error.unknown_command": 'Comando desconocido "{command}"',
809
+ "error.unknown_command_suggest": 'Comando desconocido "{command}". ¿Quisiste decir "{suggestion}"?',
810
+ "error.not_implemented": '"{command}" aún no está disponible en esta versión',
811
+ "error.invalid_lang": 'Idioma no soportado "{lang}". Usa uno de estos: {languages}',
812
+ "error.i18n_catalog_unreadable": "No se pudo leer el catálogo {file}: {detail}",
813
+ "error.i18n_mismatch": "{count} problema(s) en los catálogos",
814
+ "error.doctor_failed": "{count} revisión(es) fallaron",
815
+ "error.unknown_hook_event": 'Evento de hook desconocido "{event}". Usa uno de estos: {events}',
816
+ "i18n.summary": "Idiomas: {languages}. Claves: {keys}.",
817
+ "i18n.ok": "Todos los catálogos coinciden.",
818
+ "i18n.fix": "Edita los catálogos en {file} para que todos los idiomas tengan las mismas claves y los mismos marcadores",
819
+ "i18n.problem.missing": "falta la clave",
820
+ "i18n.problem.extra": "clave que no existe en en.json",
821
+ "i18n.problem.placeholder": "marcadores distintos (esperado: {expected}; encontrado: {actual})",
822
+ "i18n.problem.empty": "texto vacío",
823
+ "i18n.problem.not_string": "el valor no es texto",
824
+ "doctor.title": "finitemonkey doctor ({version})",
825
+ "doctor.summary": "{ok} ok, {warn} aviso(s), {fail} falla(s)",
826
+ "doctor.node.ok": "Node.js {version}",
827
+ "doctor.node.old": "Node.js {version} es más antiguo que el mínimo {min}",
828
+ "doctor.node.fix": "Instala Node.js 20 o más nuevo desde https://nodejs.org",
829
+ "doctor.git.ok": "git {version}",
830
+ "doctor.git.missing": "No se encontró git",
831
+ "doctor.git.fix": "Instala git desde https://git-scm.com/downloads",
832
+ "doctor.git.old": "git {version} es más antiguo que {min}; algunos comandos necesitarán un git más nuevo",
833
+ "doctor.git.fix_old": "Actualiza git a {min} o más nuevo",
834
+ "doctor.claude.ok": "Claude Code {version}",
835
+ "doctor.claude.missing": "No se encontró Claude Code; la CLI funciona sin él, los hooks lo necesitan",
836
+ "doctor.claude.fix": "Instala Claude Code: npm install -g @anthropic-ai/claude-code",
837
+ "doctor.claude.timeout": "`claude --version` no respondió a tiempo",
838
+ "doctor.claude.fix_timeout": "Ejecuta `claude --version` a mano y revisa que responda",
839
+ "doctor.os.linux": "Linux",
840
+ "doctor.os.wsl": "Linux en WSL",
841
+ "doctor.os.macos": "macOS",
842
+ "doctor.os.windows": "Windows (nativo)",
843
+ "doctor.os.other": "Sistema no reconocido ({platform})",
844
+ "doctor.path.plain": "La ruta del proyecto no tiene espacios ni acentos",
845
+ "doctor.path.chars": "La ruta del proyecto tiene espacios o acentos: {path}",
846
+ "doctor.path.fix_chars": "Finite Monkey maneja esta ruta; si otra herramienta falla aquí, mueve el proyecto a una ruta sin espacios ni acentos",
847
+ "doctor.path.synced": "El proyecto está en una carpeta sincronizada (OneDrive, Dropbox, iCloud o Google Drive): {path}",
848
+ "doctor.path.fix_synced": "Mueve el proyecto fuera de la carpeta sincronizada para evitar conflictos de sincronización y git lento",
849
+ "doctor.path.wsl_mount": "El proyecto está en un disco de Windows montado en WSL: {path}",
850
+ "doctor.path.fix_wsl_mount": "Mueve el proyecto a la home de Linux (por ejemplo ~/proyectos) para tener git rápido y permisos correctos",
851
+ "hook.failed_closed": "el hook {event} no pudo ejecutarse ({detail}) y bloqueó por seguridad. Ejecuta `finitemonkey doctor`",
852
+ "hook.failed_open": "el hook {event} no pudo ejecutarse ({detail}); se sigue sin él. Ejecuta `finitemonkey doctor`",
853
+ "exit.ok": "0 ok: el comando hizo lo que se pidió",
854
+ "exit.check_failed": "1 revisión fallida: el comando se ejecutó y encontró un problema",
855
+ "exit.usage": "2 error de uso: comando u opción desconocidos, valor inválido o comando aún no disponible",
856
+ "exit.not_configured": "3 no configurado: falta la instalación o un requisito previo (node, git, archivos, permisos)",
857
+ "exit.needs_person": "4 necesita a la persona: se requiere una decisión o aprobación antes de seguir",
858
+ "exit.internal": "5 error interno: el propio Finite Monkey falló; por favor, repórtalo",
859
+ "help.exit_codes": "Códigos de salida",
860
+ "hook.stop_released": "el hook {event} bloquearía otra vez ({detail}), pero Claude Code ya está continuando por un hook de parada, así que deja que la sesión termine. Ejecuta `finitemonkey doctor`",
861
+ "error.needs_confirmation": '"{command}" necesita tu decisión antes de cambiar algo: {question}'
862
+ };
863
+
864
+ // src/i18n/pt-BR.json
865
+ var pt_BR_default = {
866
+ "command.init.summary": "Instala o Finite Monkey neste projeto",
867
+ "command.doctor.summary": "Confere o ambiente e diz como corrigir cada problema",
868
+ "command.update.summary": "Atualiza os arquivos que o Finite Monkey gerou",
869
+ "command.uninstall.summary": "Remove o Finite Monkey deste projeto",
870
+ "command.i18n_check.summary": "Confere se os catálogos pt-BR, en e es batem",
871
+ "command.hook.summary": "Responde a um evento de hook do Claude Code (lê JSON da entrada padrão)",
872
+ "command.version.summary": "Mostra a versão",
873
+ "command.help.summary": "Mostra comandos e opções",
874
+ "help.tagline": "Infinite monkeys not required.",
875
+ "help.usage": "Uso",
876
+ "help.command": "comando",
877
+ "help.commands": "Comandos",
878
+ "help.flags": "Opções globais",
879
+ "help.options": "Opções",
880
+ "help.planned": "(ainda não disponível)",
881
+ "help.more": "Rode `finitemonkey help <comando>` para detalhes. Use --json para saída legível por máquina.",
882
+ "flag.json": "Imprime um objeto JSON (schemaVersion 1) no lugar de texto",
883
+ "flag.lang": "Idioma das mensagens",
884
+ "flag.yes": "Responde sim às confirmações",
885
+ "flag.dry_run": "Mostra o que mudaria sem mudar nada",
886
+ "flag.version": "Mostra a versão",
887
+ "flag.help": "Mostra a ajuda",
888
+ "output.error": "erro: {message}",
889
+ "output.fix": "como corrigir: {fix}",
890
+ "output.next": "próximo passo: {next}",
891
+ "error.internal": "O Finite Monkey falhou de forma inesperada: {detail}",
892
+ "error.invalid_arguments": "Argumentos inválidos: {detail}",
893
+ "error.unexpected_argument": 'Argumento inesperado "{argument}"',
894
+ "error.unknown_command": 'Comando desconhecido "{command}"',
895
+ "error.unknown_command_suggest": 'Comando desconhecido "{command}". Você quis dizer "{suggestion}"?',
896
+ "error.not_implemented": '"{command}" ainda não está disponível nesta versão',
897
+ "error.invalid_lang": 'Idioma não suportado "{lang}". Use um destes: {languages}',
898
+ "error.i18n_catalog_unreadable": "Não foi possível ler o catálogo {file}: {detail}",
899
+ "error.i18n_mismatch": "{count} problema(s) nos catálogos",
900
+ "error.doctor_failed": "{count} verificação(ões) falharam",
901
+ "error.unknown_hook_event": 'Evento de hook desconhecido "{event}". Use um destes: {events}',
902
+ "i18n.summary": "Idiomas: {languages}. Chaves: {keys}.",
903
+ "i18n.ok": "Todos os catálogos batem.",
904
+ "i18n.fix": "Edite os catálogos em {file} para que todos os idiomas tenham as mesmas chaves e os mesmos marcadores",
905
+ "i18n.problem.missing": "chave faltando",
906
+ "i18n.problem.extra": "chave que não existe no en.json",
907
+ "i18n.problem.placeholder": "marcadores diferentes (esperado: {expected}; encontrado: {actual})",
908
+ "i18n.problem.empty": "texto vazio",
909
+ "i18n.problem.not_string": "valor não é texto",
910
+ "doctor.title": "finitemonkey doctor ({version})",
911
+ "doctor.summary": "{ok} ok, {warn} aviso(s), {fail} falha(s)",
912
+ "doctor.node.ok": "Node.js {version}",
913
+ "doctor.node.old": "Node.js {version} é mais antigo que o mínimo {min}",
914
+ "doctor.node.fix": "Instale o Node.js 20 ou mais novo em https://nodejs.org",
915
+ "doctor.git.ok": "git {version}",
916
+ "doctor.git.missing": "git não foi encontrado",
917
+ "doctor.git.fix": "Instale o git em https://git-scm.com/downloads",
918
+ "doctor.git.old": "git {version} é mais antigo que {min}; alguns comandos vão precisar de um git mais novo",
919
+ "doctor.git.fix_old": "Atualize o git para {min} ou mais novo",
920
+ "doctor.claude.ok": "Claude Code {version}",
921
+ "doctor.claude.missing": "Claude Code não foi encontrado; a CLI funciona sem ele, os hooks precisam dele",
922
+ "doctor.claude.fix": "Instale o Claude Code: npm install -g @anthropic-ai/claude-code",
923
+ "doctor.claude.timeout": "`claude --version` não respondeu a tempo",
924
+ "doctor.claude.fix_timeout": "Rode `claude --version` à mão e confira se ele responde",
925
+ "doctor.os.linux": "Linux",
926
+ "doctor.os.wsl": "Linux no WSL",
927
+ "doctor.os.macos": "macOS",
928
+ "doctor.os.windows": "Windows (nativo)",
929
+ "doctor.os.other": "Sistema não reconhecido ({platform})",
930
+ "doctor.path.plain": "O caminho do projeto não tem espaço nem acento",
931
+ "doctor.path.chars": "O caminho do projeto tem espaço ou acento: {path}",
932
+ "doctor.path.fix_chars": "O Finite Monkey lida com este caminho; se outra ferramenta falhar aqui, mova o projeto para um caminho sem espaço nem acento",
933
+ "doctor.path.synced": "O projeto está numa pasta sincronizada (OneDrive, Dropbox, iCloud ou Google Drive): {path}",
934
+ "doctor.path.fix_synced": "Mova o projeto para fora da pasta sincronizada para evitar conflito de sincronização e git lento",
935
+ "doctor.path.wsl_mount": "O projeto está num disco do Windows montado no WSL: {path}",
936
+ "doctor.path.fix_wsl_mount": "Mova o projeto para a home do Linux (por exemplo ~/projetos) para ter git rápido e permissões corretas",
937
+ "hook.failed_closed": "o hook {event} não conseguiu rodar ({detail}) e bloqueou por segurança. Rode `finitemonkey doctor`",
938
+ "hook.failed_open": "o hook {event} não conseguiu rodar ({detail}); seguindo sem ele. Rode `finitemonkey doctor`",
939
+ "exit.ok": "0 ok: o comando fez o que foi pedido",
940
+ "exit.check_failed": "1 verificação reprovada: o comando rodou e achou um problema",
941
+ "exit.usage": "2 erro de uso: comando ou opção desconhecidos, valor inválido ou comando ainda não disponível",
942
+ "exit.not_configured": "3 não configurado: falta instalação ou um pré-requisito (node, git, arquivos, permissões)",
943
+ "exit.needs_person": "4 precisa da pessoa: uma decisão ou aprovação é necessária antes de seguir",
944
+ "exit.internal": "5 erro interno: o próprio Finite Monkey falhou; por favor, reporte",
945
+ "help.exit_codes": "Códigos de saída",
946
+ "hook.stop_released": "o hook {event} bloquearia de novo ({detail}), mas o Claude Code já está continuando por causa de um hook de parada, então ele deixa a sessão parar. Rode `finitemonkey doctor`",
947
+ "error.needs_confirmation": '"{command}" precisa da sua decisão antes de mudar qualquer coisa: {question}'
948
+ };
949
+
950
+ // src/i18n/catalogs.ts
951
+ var exact = (catalog) => catalog;
952
+ var catalogs = {
953
+ en: exact(en_default),
954
+ "pt-BR": exact(pt_BR_default),
955
+ es: exact(es_default)
956
+ };
957
+
958
+ // src/i18n/languages.ts
959
+ var LANGUAGES = ["en", "pt-BR", "es"];
960
+ var SOURCE_LANG = "en";
961
+ var normalizeLang = (raw) => {
962
+ if (raw === void 0) return void 0;
963
+ const base = (raw.trim().split(/[.@:]/)[0] ?? "").replace(/_/g, "-").toLowerCase();
964
+ if (base === "pt" || base.startsWith("pt-")) return "pt-BR";
965
+ if (base === "es" || base.startsWith("es-")) return "es";
966
+ if (base === "en" || base.startsWith("en-")) return "en";
967
+ return void 0;
968
+ };
969
+ var LANG_ENV_ORDER = ["FINITEMONKEY_LANG", "LC_ALL", "LC_MESSAGES", "LANG"];
970
+ var detectLang = (flag2, env) => {
971
+ const fromFlag = normalizeLang(flag2);
972
+ if (fromFlag) return { lang: fromFlag, source: "flag" };
973
+ for (const name of LANG_ENV_ORDER) {
974
+ const lang = normalizeLang(env[name]);
975
+ if (lang) return { lang, source: name };
976
+ }
977
+ return { lang: SOURCE_LANG, source: "default" };
978
+ };
979
+
980
+ // src/i18n/translate.ts
981
+ var PLACEHOLDER = /\{([a-zA-Z][a-zA-Z0-9_]*)\}/g;
982
+ var interpolate = (template, params = {}) => template.replace(PLACEHOLDER, (match, name) => {
983
+ const value = params[name];
984
+ return value === void 0 ? match : String(value);
985
+ });
986
+ var placeholdersOf = (template) => [...new Set(Array.from(template.matchAll(PLACEHOLDER), (match) => match[1] ?? ""))].sort();
987
+ var createTranslator = (lang, source = catalogs) => (key, params) => interpolate(source[lang][key] ?? source[SOURCE_LANG][key] ?? key, params);
988
+
989
+ // src/i18n/check.ts
990
+ var sameList = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
991
+ var valueProblems = (lang, catalog, reference) => Object.keys(catalog).sort().flatMap((key) => {
992
+ const value = catalog[key];
993
+ if (typeof value !== "string") return [{ lang, key, kind: "not_string" }];
994
+ const problems = [];
995
+ if (value.trim() === "") problems.push({ lang, key, kind: "empty" });
996
+ if (!(key in reference)) problems.push({ lang, key, kind: "extra" });
997
+ return problems;
998
+ });
999
+ var parityProblems = (lang, catalog, reference) => Object.keys(reference).sort().flatMap((key) => {
1000
+ const value = catalog[key];
1001
+ const source = reference[key];
1002
+ if (value === void 0) return [{ lang, key, kind: "missing" }];
1003
+ if (typeof value !== "string" || typeof source !== "string") return [];
1004
+ const expected = placeholdersOf(source);
1005
+ const actual = placeholdersOf(value);
1006
+ return sameList(expected, actual) ? [] : [{ lang, key, kind: "placeholder", expected, actual }];
1007
+ });
1008
+ var checkCatalogs = (input) => {
1009
+ const reference = input[SOURCE_LANG];
1010
+ const problems = LANGUAGES.flatMap(
1011
+ (lang) => lang === SOURCE_LANG ? valueProblems(lang, input[lang], reference) : [...valueProblems(lang, input[lang], reference), ...parityProblems(lang, input[lang], reference)]
1012
+ );
1013
+ return { languages: LANGUAGES, keys: Object.keys(reference).length, problems };
1014
+ };
1015
+
1016
+ // src/commands/i18n-check.ts
1017
+ var unreadable = (dir, file, detail) => ({
1018
+ code: "i18n_catalog_unreadable",
1019
+ messageKey: "error.i18n_catalog_unreadable",
1020
+ params: { file, detail },
1021
+ fix: `finitemonkey i18n check --dir ${dir}`,
1022
+ exitCode: ExitCode.NotConfigured
1023
+ });
1024
+ var readCatalog = (ctx, dir, lang) => {
1025
+ const file = toAbsolutePath(join(dir, `${lang}.json`));
1026
+ const text = ctx.runtime.readText(file);
1027
+ if (!text.ok) return reject(unreadable(dir, file, text.error));
1028
+ const parsed = parseJsonObject(text.value);
1029
+ return parsed.ok ? parsed : reject(unreadable(dir, file, parsed.error));
1030
+ };
1031
+ var loadCatalogs = (ctx, dir) => {
1032
+ const en = readCatalog(ctx, dir, "en");
1033
+ if (!en.ok) return en;
1034
+ const ptBR = readCatalog(ctx, dir, "pt-BR");
1035
+ if (!ptBR.ok) return ptBR;
1036
+ const es = readCatalog(ctx, dir, "es");
1037
+ if (!es.ok) return es;
1038
+ return accept({ en: en.value, "pt-BR": ptBR.value, es: es.value });
1039
+ };
1040
+ var describeProblem = (ctx, problem) => {
1041
+ const kind = ctx.t(`i18n.problem.${problem.kind}`, {
1042
+ expected: (problem.expected ?? []).join(", "),
1043
+ actual: (problem.actual ?? []).join(", ")
1044
+ });
1045
+ return ` ${problem.lang} ${problem.key} ${kind}`;
1046
+ };
1047
+ var runI18nCheck = (ctx, dir) => {
1048
+ const input = dir === void 0 ? accept(catalogs) : loadCatalogs(ctx, dir);
1049
+ if (!input.ok) return failureFrom(ctx.t, input.error);
1050
+ const report = checkCatalogs(input.value);
1051
+ const data = { ...report, source: dir ?? "bundled" };
1052
+ const summary = ctx.t("i18n.summary", { languages: report.languages.join(", "), keys: report.keys });
1053
+ if (report.problems.length === 0) return success(data, [summary, ctx.t("i18n.ok")]);
1054
+ return {
1055
+ ok: false,
1056
+ exitCode: ExitCode.CheckFailed,
1057
+ data,
1058
+ human: [summary, ...report.problems.map((problem) => describeProblem(ctx, problem))],
1059
+ error: {
1060
+ code: "i18n_mismatch",
1061
+ message: ctx.t("error.i18n_mismatch", { count: report.problems.length }),
1062
+ fix: ctx.t("i18n.fix", { file: dir ?? "src/i18n" })
1063
+ }
1064
+ };
1065
+ };
1066
+ var i18nCheckCommand = {
1067
+ path: ["i18n", "check"],
1068
+ summaryKey: "command.i18n_check.summary",
1069
+ usage: "finitemonkey i18n check [--dir <path>]",
1070
+ status: "ready",
1071
+ options: { dir: { type: "string" } },
1072
+ run: (ctx, args) => {
1073
+ const dir = stringOption(args.values, "dir");
1074
+ return Promise.resolve(runI18nCheck(ctx, dir === void 0 ? void 0 : toAbsolutePath(dir, ctx.cwd)));
1075
+ }
1076
+ };
1077
+
1078
+ // src/commands/planned.ts
1079
+ var planned = (name, summaryKey, options) => ({
1080
+ path: [name],
1081
+ summaryKey,
1082
+ usage: `finitemonkey ${name}`,
1083
+ status: "planned",
1084
+ options,
1085
+ maxPositionals: 8,
1086
+ run: (ctx) => Promise.resolve(
1087
+ failureFrom(ctx.t, {
1088
+ code: "not_implemented",
1089
+ messageKey: "error.not_implemented",
1090
+ params: { command: name },
1091
+ fix: "finitemonkey help",
1092
+ exitCode: ExitCode.Usage,
1093
+ data: { command: name, status: "planned" }
1094
+ })
1095
+ )
1096
+ });
1097
+ var initCommand = planned("init", "command.init.summary", {
1098
+ preset: { type: "string" },
1099
+ github: { type: "boolean" }
1100
+ });
1101
+ var updateCommand = planned("update", "command.update.summary", { check: { type: "boolean" } });
1102
+ var uninstallCommand = planned("uninstall", "command.uninstall.summary", {
1103
+ purge: { type: "boolean" },
1104
+ "keep-generated": { type: "boolean" }
1105
+ });
1106
+
1107
+ // src/commands/version.ts
1108
+ var versionCommand = {
1109
+ path: ["version"],
1110
+ summaryKey: "command.version.summary",
1111
+ usage: "finitemonkey version",
1112
+ status: "ready",
1113
+ run: (ctx) => {
1114
+ const data = { version: ctx.version, node: ctx.runtime.nodeVersion, platform: ctx.platform };
1115
+ return Promise.resolve(success(data, [ctx.version]));
1116
+ }
1117
+ };
1118
+
1119
+ // src/commands/index.ts
1120
+ var REGISTRY = [
1121
+ initCommand,
1122
+ doctorCommand,
1123
+ updateCommand,
1124
+ uninstallCommand,
1125
+ i18nCheckCommand,
1126
+ hookCommand,
1127
+ versionCommand,
1128
+ helpCommand
1129
+ ];
1130
+
1131
+ // src/core/suggest.ts
1132
+ var distance = (a, b) => {
1133
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
1134
+ for (let i = 1; i <= a.length; i += 1) {
1135
+ let diagonal = row[0] ?? 0;
1136
+ row[0] = i;
1137
+ for (let j = 1; j <= b.length; j += 1) {
1138
+ const above = row[j] ?? 0;
1139
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1140
+ row[j] = Math.min(above + 1, (row[j - 1] ?? 0) + 1, diagonal + cost);
1141
+ diagonal = above;
1142
+ }
1143
+ }
1144
+ return row[b.length] ?? 0;
1145
+ };
1146
+ var closest = (input, candidates) => {
1147
+ const group = candidates.find((name) => name.startsWith(`${input} `));
1148
+ if (group !== void 0) return group;
1149
+ const limit = Math.max(1, Math.floor(input.length / 3));
1150
+ let best;
1151
+ for (const name of candidates) {
1152
+ const score = distance(input, name.split(" ")[0] ?? name);
1153
+ if (score <= limit && (best === void 0 || score < best.score)) best = { name, score };
1154
+ }
1155
+ return best?.name;
1156
+ };
1157
+
1158
+ // src/core/dispatch.ts
1159
+ var unknownCommand = (registry, attempted) => {
1160
+ const suggestion = closest(attempted, registry.map(commandName));
1161
+ return suggestion === void 0 ? usageProblem("unknown_command", "error.unknown_command", "finitemonkey help", { command: attempted }) : usageProblem("unknown_command", "error.unknown_command_suggest", `finitemonkey ${suggestion}`, {
1162
+ command: attempted,
1163
+ suggestion
1164
+ });
1165
+ };
1166
+ var failWith = (ctx, name, problem) => ({
1167
+ name,
1168
+ execute: () => Promise.resolve(failureFrom(ctx.t, problem))
1169
+ });
1170
+ var invoke = (ctx, argv, spec, registry) => {
1171
+ const name = commandName(spec);
1172
+ const args = strictArgs(argv, spec);
1173
+ if (!args.ok) return failWith(ctx, name, args.error);
1174
+ return { name, execute: () => spec.run(ctx, args.value, registry) };
1175
+ };
1176
+ var builtin = (registry, name) => registry.find((spec) => commandName(spec) === name);
1177
+ var dispatch = (ctx, argv, loose, registry) => {
1178
+ const resolved = resolveCommand(registry, loose.positionals);
1179
+ const attempted = loose.positionals[0];
1180
+ if (resolved === void 0 && attempted !== void 0) {
1181
+ return failWith(ctx, attempted, unknownCommand(registry, attempted));
1182
+ }
1183
+ const wanted = loose.version && !loose.help ? "version" : "help";
1184
+ if (resolved === void 0 || loose.version && !loose.help) {
1185
+ const spec = builtin(registry, wanted);
1186
+ if (spec === void 0) return failWith(ctx, wanted, unknownCommand(registry, wanted));
1187
+ return { name: wanted, execute: () => spec.run(ctx, { values: {}, positionals: [] }, registry) };
1188
+ }
1189
+ if (loose.help) return { name: "help", execute: () => Promise.resolve(helpFor(ctx, resolved.spec)) };
1190
+ return invoke(ctx, argv, resolved.spec, registry);
1191
+ };
1192
+
1193
+ // src/core/output.ts
1194
+ var ISSUES_URL = "https://github.com/gabrielvieiraf/finitemonkey/issues";
1195
+ var resultFromThrown = (thrown, t) => {
1196
+ if (thrown instanceof FmError) return failureFrom(t, thrown.problem);
1197
+ const detail = thrown instanceof Error ? thrown.message : String(thrown);
1198
+ return failureFrom(t, {
1199
+ code: "internal_error",
1200
+ messageKey: "error.internal",
1201
+ params: { detail },
1202
+ fix: ISSUES_URL,
1203
+ exitCode: ExitCode.Internal
1204
+ });
1205
+ };
1206
+ var block = (items) => items.length > 0 ? `${items.join("\n")}
1207
+ ` : "";
1208
+ var render = (ctx, command, result) => {
1209
+ if (result.raw !== void 0) {
1210
+ ctx.io.stdout.write(result.raw.stdout);
1211
+ ctx.io.stderr.write(result.raw.stderr);
1212
+ return;
1213
+ }
1214
+ if (ctx.json) {
1215
+ ctx.io.stdout.write(`${JSON.stringify(toEnvelope(command, result))}
1216
+ `);
1217
+ return;
1218
+ }
1219
+ ctx.io.stdout.write(block(result.human ?? []));
1220
+ if (!result.ok) {
1221
+ const { message, fix } = result.error;
1222
+ ctx.io.stderr.write(block([ctx.t("output.error", { message }), ctx.t("output.fix", { fix })]));
1223
+ }
1224
+ if (result.next !== void 0) ctx.io.stdout.write(block([ctx.t("output.next", { next: result.next })]));
1225
+ };
1226
+
1227
+ // src/core/run.ts
1228
+ var run = async (options) => {
1229
+ const loose = looseArgs(options.argv);
1230
+ const detected = detectLang(loose.lang, options.env);
1231
+ const t = createTranslator(detected.lang);
1232
+ const ctx = {
1233
+ lang: detected.lang,
1234
+ langSource: detected.source,
1235
+ t,
1236
+ json: loose.json,
1237
+ yes: loose.yes,
1238
+ dryRun: loose.dryRun,
1239
+ cwd: options.cwd,
1240
+ env: options.env,
1241
+ platform: options.platform,
1242
+ version: options.version,
1243
+ io: options.io,
1244
+ runtime: options.runtime
1245
+ };
1246
+ let name = loose.positionals[0] ?? "";
1247
+ let result;
1248
+ try {
1249
+ if (loose.lang !== void 0 && normalizeLang(loose.lang) === void 0) {
1250
+ const problem = usageProblem("invalid_lang", "error.invalid_lang", "finitemonkey --lang en", {
1251
+ lang: loose.lang,
1252
+ languages: LANGUAGES.join(", ")
1253
+ });
1254
+ result = failureFrom(t, problem);
1255
+ } else {
1256
+ const dispatched = dispatch(ctx, options.argv, loose, options.registry ?? REGISTRY);
1257
+ name = dispatched.name;
1258
+ result = await dispatched.execute();
1259
+ }
1260
+ } catch (thrown) {
1261
+ result = resultFromThrown(thrown, t);
1262
+ }
1263
+ render(ctx, name, result);
1264
+ return result.exitCode;
1265
+ };
1266
+
1267
+ // src/core/version.ts
1268
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1269
+ var UNKNOWN_VERSION = "0.0.0-unknown";
1270
+ var readPackageVersion = (readText, moduleUrl) => {
1271
+ const text = readText(toAbsolutePath(fileURLToPath2(new URL("../package.json", moduleUrl))));
1272
+ if (!text.ok) return UNKNOWN_VERSION;
1273
+ const parsed = parseJson(text.value);
1274
+ if (!parsed.ok || !isRecord(parsed.value)) return UNKNOWN_VERSION;
1275
+ const version = parsed.value["version"];
1276
+ return typeof version === "string" ? version : UNKNOWN_VERSION;
1277
+ };
1278
+
1279
+ // src/cli.ts
1280
+ var flush = (stream) => new Promise((resolve2) => {
1281
+ stream.write("", () => {
1282
+ resolve2();
1283
+ });
1284
+ });
1285
+ var main = async () => {
1286
+ const runtime = nodeRuntime();
1287
+ const code = await run({
1288
+ argv: process.argv.slice(2),
1289
+ env: process.env,
1290
+ cwd: toAbsolutePath(process.cwd()),
1291
+ platform: process.platform,
1292
+ io: { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin },
1293
+ version: readPackageVersion(runtime.readText, import.meta.url),
1294
+ runtime
1295
+ });
1296
+ await Promise.all([flush(process.stdout), flush(process.stderr)]);
1297
+ process.exit(code);
1298
+ };
1299
+ if (isEntryPoint(import.meta.url, process.argv[1], nodeRealpath, process.platform)) await main();
1300
+ export {
1301
+ main
1302
+ };