finitemonkey 0.1.0-next.2 → 0.1.0-next.3

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.
@@ -16,10 +16,23 @@ var isEntryPoint = (moduleUrl, argv1, realpath, platform) => {
16
16
  // src/core/node-runtime.ts
17
17
  import { spawnSync as spawnSync2 } from "node:child_process";
18
18
  import { readFileSync, realpathSync } from "node:fs";
19
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
19
20
 
20
21
  // src/core/node-fs.ts
21
- import { closeSync, fsyncSync, mkdirSync, openSync, readdirSync, renameSync, rmSync, writeSync } from "node:fs";
22
- import { dirname, join, relative } from "node:path";
22
+ import { randomBytes } from "node:crypto";
23
+ import {
24
+ closeSync,
25
+ fsyncSync,
26
+ mkdirSync,
27
+ openSync,
28
+ readdirSync,
29
+ renameSync,
30
+ rmSync,
31
+ statSync,
32
+ unlinkSync,
33
+ writeSync
34
+ } from "node:fs";
35
+ import { basename, dirname, join, relative } from "node:path";
23
36
 
24
37
  // src/core/parsed.ts
25
38
  var accept = (value) => ({ ok: true, value });
@@ -43,8 +56,18 @@ var optionalString = (record2, key) => {
43
56
  };
44
57
 
45
58
  // src/core/node-fs.ts
59
+ var NEW_FILE_MODE = 420;
46
60
  var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
47
61
  var messageOf = (error) => error instanceof Error ? error.message : String(error);
62
+ var errnoOf = (error) => error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
63
+ var attempt = (work) => {
64
+ try {
65
+ work();
66
+ return accept(void 0);
67
+ } catch (error) {
68
+ return reject(messageOf(error));
69
+ }
70
+ };
48
71
  var nodeWriteText = (path, text2) => {
49
72
  const temp = `${path}.${String(process.pid)}.tmp`;
50
73
  try {
@@ -63,6 +86,14 @@ var nodeWriteText = (path, text2) => {
63
86
  return reject(messageOf(error));
64
87
  }
65
88
  };
89
+ var nodeStat = (path) => {
90
+ try {
91
+ const stats = statSync(path);
92
+ return { kind: stats.isDirectory() ? "dir" : "file", mtimeMs: stats.mtimeMs };
93
+ } catch {
94
+ return void 0;
95
+ }
96
+ };
66
97
  var nodeListDir = (path) => {
67
98
  try {
68
99
  return accept(readdirSync(path).sort());
@@ -70,6 +101,77 @@ var nodeListDir = (path) => {
70
101
  return reject(messageOf(error));
71
102
  }
72
103
  };
104
+ var modeOf = (path) => {
105
+ try {
106
+ return statSync(path).mode & 511;
107
+ } catch {
108
+ return NEW_FILE_MODE;
109
+ }
110
+ };
111
+ var writeAndSync = (file, content, mode) => {
112
+ const fd = openSync(file, "wx", mode);
113
+ try {
114
+ writeSync(fd, content, null, "utf8");
115
+ fsyncSync(fd);
116
+ } finally {
117
+ closeSync(fd);
118
+ }
119
+ };
120
+ var nodeWriteAtomic = (path, content) => {
121
+ const temp = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString("hex")}.tmp`);
122
+ return attempt(() => {
123
+ try {
124
+ writeAndSync(temp, content, modeOf(path));
125
+ renameSync(temp, path);
126
+ } catch (error) {
127
+ rmSync(temp, { force: true });
128
+ throw error;
129
+ }
130
+ });
131
+ };
132
+ var nodeRename = (from, to) => attempt(() => {
133
+ renameSync(from, to);
134
+ });
135
+ var nodeMkdir = (path) => attempt(() => {
136
+ mkdirSync(path, { recursive: true });
137
+ });
138
+ var nodeRemove = (path) => attempt(() => {
139
+ rmSync(path, { recursive: true, force: true });
140
+ });
141
+ var createLock = (path, owner, now) => {
142
+ const fd = openSync(path, "wx", 384);
143
+ try {
144
+ writeSync(fd, `${JSON.stringify({ owner, pid: process.pid, at: now })}
145
+ `, null, "utf8");
146
+ } finally {
147
+ closeSync(fd);
148
+ }
149
+ };
150
+ var release = (path) => () => {
151
+ try {
152
+ unlinkSync(path);
153
+ } catch {
154
+ return;
155
+ }
156
+ };
157
+ var isStale = (path, staleMs, now) => {
158
+ const stat = nodeStat(path);
159
+ return stat !== void 0 && now - stat.mtimeMs > staleMs;
160
+ };
161
+ var nodeLock = (path, options) => {
162
+ for (let round = 0; round < 2; round += 1) {
163
+ const now = Date.now();
164
+ try {
165
+ createLock(path, options.owner, now);
166
+ return accept(release(path));
167
+ } catch (error) {
168
+ if (errnoOf(error) !== "EEXIST") return reject(messageOf(error));
169
+ if (!isStale(path, options.staleMs, now)) return reject(`locked: ${path}`);
170
+ rmSync(path, { force: true });
171
+ }
172
+ }
173
+ return reject(`locked: ${path}`);
174
+ };
73
175
  var walkInto = (root, dir, out) => {
74
176
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
75
177
  const full = join(dir, entry.name);
@@ -183,7 +285,14 @@ var nodeRuntime = () => ({
183
285
  walkFiles: nodeWalkFiles,
184
286
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
185
287
  git: nodeGit,
186
- gitLines: nodeGitLines
288
+ gitLines: nodeGitLines,
289
+ stat: nodeStat,
290
+ writeAtomic: nodeWriteAtomic,
291
+ rename: nodeRename,
292
+ mkdir: nodeMkdir,
293
+ remove: nodeRemove,
294
+ lock: nodeLock,
295
+ selfPath: toAbsolutePath(fileURLToPath2(import.meta.url))
187
296
  });
188
297
 
189
298
  // src/doctor/system.ts
@@ -310,7 +419,7 @@ var doctorEnvFrom = (ctx) => ({
310
419
  cwd: ctx.cwd,
311
420
  runtime: ctx.runtime
312
421
  });
313
- var runChecks = (env, checks = CHECKS) => checks.flatMap((check) => check(env));
422
+ var runChecks = (env, checks = CHECKS) => checks.flatMap((check2) => check2(env));
314
423
  var toItem = (ctx, finding) => ({
315
424
  check: finding.check,
316
425
  status: finding.status,
@@ -407,10 +516,10 @@ var needsPersonProblem = (messageKey, fix, params = {}) => ({
407
516
  });
408
517
  var FmError = class extends Error {
409
518
  problem;
410
- constructor(problem) {
411
- super(problem.code);
519
+ constructor(problem2) {
520
+ super(problem2.code);
412
521
  this.name = "FmError";
413
- this.problem = problem;
522
+ this.problem = problem2;
414
523
  }
415
524
  };
416
525
 
@@ -481,11 +590,11 @@ var success = (data, human = []) => ({
481
590
  data,
482
591
  human
483
592
  });
484
- var failureFrom = (t, problem, human) => ({
593
+ var failureFrom = (t, problem2, human) => ({
485
594
  ok: false,
486
- exitCode: problem.exitCode,
487
- error: { code: problem.code, message: t(problem.messageKey, problem.params), fix: problem.fix },
488
- ...problem.data === void 0 ? {} : { data: problem.data },
595
+ exitCode: problem2.exitCode,
596
+ error: { code: problem2.code, message: t(problem2.messageKey, problem2.params), fix: problem2.fix },
597
+ ...problem2.data === void 0 ? {} : { data: problem2.data },
489
598
  ...human === void 0 ? {} : { human }
490
599
  });
491
600
  var toEnvelope = (command, result) => ({
@@ -557,10 +666,10 @@ var helpFor = (ctx, spec) => {
557
666
  var topicHelp = (ctx, registry, topic) => {
558
667
  const resolved = resolveCommand(registry, topic);
559
668
  if (resolved !== void 0) return helpFor(ctx, resolved.spec);
560
- const problem = usageProblem("unknown_command", "error.unknown_command", "finitemonkey help", {
669
+ const problem2 = usageProblem("unknown_command", "error.unknown_command", "finitemonkey help", {
561
670
  command: topic.join(" ")
562
671
  });
563
- return failureFrom(ctx.t, problem);
672
+ return failureFrom(ctx.t, problem2);
564
673
  };
565
674
  var helpCommand = {
566
675
  path: ["help"],
@@ -657,12 +766,12 @@ var saveBaseline = (runtime, root, baseline) => runtime.writeText(baselinePath(r
657
766
  var ratchet = (findings, baseline, now, inScope) => {
658
767
  const byId = new Map(baseline.entries.map((entry) => [entry.id, entry]));
659
768
  const seen = new Set(findings.map((finding) => finding.id));
660
- const entryOf2 = (finding) => byId.get(finding.id);
769
+ const entryOf3 = (finding) => byId.get(finding.id);
661
770
  return {
662
- fresh: findings.filter((finding) => entryOf2(finding) === void 0),
663
- known: findings.filter((finding) => (entryOf2(finding)?.expiresAt ?? "") >= now),
771
+ fresh: findings.filter((finding) => entryOf3(finding) === void 0),
772
+ known: findings.filter((finding) => (entryOf3(finding)?.expiresAt ?? "") >= now),
664
773
  expired: findings.filter((finding) => {
665
- const entry = entryOf2(finding);
774
+ const entry = entryOf3(finding);
666
775
  return entry !== void 0 && entry.expiresAt < now;
667
776
  }),
668
777
  fixed: inScope === void 0 ? [] : baseline.entries.filter((entry) => !seen.has(entry.id) && inScope(entry.file))
@@ -1297,7 +1406,7 @@ import { join as join4, relative as relative2 } from "node:path";
1297
1406
 
1298
1407
  // src/secrets/located.ts
1299
1408
  var findingId = (ruleId, file, fingerprint) => `fm-${sha256Hex(`${ruleId}\0${file}\0${fingerprint}`).slice(0, 16)}`;
1300
- var locate = (finding, rawFile, line, commit) => {
1409
+ var locate = (finding, rawFile, line, commit2) => {
1301
1410
  const file = toPosixPath(rawFile);
1302
1411
  return {
1303
1412
  id: findingId(finding.ruleId, file, finding.fingerprint),
@@ -1308,7 +1417,7 @@ var locate = (finding, rawFile, line, commit) => {
1308
1417
  column: finding.column,
1309
1418
  fingerprint: finding.fingerprint,
1310
1419
  tail: finding.tail,
1311
- ...commit === void 0 ? {} : { commit }
1420
+ ...commit2 === void 0 ? {} : { commit: commit2 }
1312
1421
  };
1313
1422
  };
1314
1423
  var whereOf = (finding) => `${finding.file}:${String(finding.line)}`;
@@ -1323,11 +1432,11 @@ var targetOf = (line) => {
1323
1432
  if (path === DEV_NULL) return void 0;
1324
1433
  return path.startsWith("b/") ? path.slice(2) : path;
1325
1434
  };
1326
- var collect = (chunk, commit, found) => {
1435
+ var collect = (chunk, commit2, found) => {
1327
1436
  const { file } = chunk;
1328
1437
  if (file === void 0 || chunk.text.length === 0) return;
1329
1438
  for (const finding of scanText(chunk.text.join("\n"), { path: file })) {
1330
- const located = locate(finding, file, chunk.lines[finding.line - 1] ?? finding.line, commit);
1439
+ const located = locate(finding, file, chunk.lines[finding.line - 1] ?? finding.line, commit2);
1331
1440
  found.set(located.id, located);
1332
1441
  }
1333
1442
  };
@@ -1348,17 +1457,17 @@ var addLine = (chunk, line) => {
1348
1457
  };
1349
1458
  var diffScanner = () => {
1350
1459
  const found = /* @__PURE__ */ new Map();
1351
- let commit;
1460
+ let commit2;
1352
1461
  let chunk = fresh();
1353
1462
  const flush2 = () => {
1354
- collect(chunk, commit, found);
1463
+ collect(chunk, commit2, found);
1355
1464
  chunk = fresh();
1356
1465
  };
1357
1466
  const header = (line) => {
1358
1467
  const match = COMMIT.exec(line);
1359
1468
  if (match !== null) {
1360
1469
  flush2();
1361
- commit = match[1];
1470
+ commit2 = match[1];
1362
1471
  return true;
1363
1472
  }
1364
1473
  if (line.startsWith("diff --git ")) {
@@ -1429,9 +1538,9 @@ var SAFE_ENV = /^\.env\.(?:example|sample|template|dist|defaults|schema)$/i;
1429
1538
  var ENV_FILE = /^\.env(?:\..+)?$/i;
1430
1539
  var KEY_FILE = /\.(?:pem|key|p12|pfx)$/i;
1431
1540
  var SSH_KEY = /^id_(?:rsa|dsa|ecdsa|ed25519)(?:_[A-Za-z0-9_-]+)?$/;
1432
- var basename = (path) => path.split(/[\\/]/).pop() ?? path;
1541
+ var basename2 = (path) => path.split(/[\\/]/).pop() ?? path;
1433
1542
  var isSensitivePath = (path) => {
1434
- const name = basename(path.replace(/^["']|["']$/g, ""));
1543
+ const name = basename2(path.replace(/^["']|["']$/g, ""));
1435
1544
  if (ENV_FILE.test(name)) return !SAFE_ENV.test(name);
1436
1545
  return KEY_FILE.test(name) || SSH_KEY.test(name);
1437
1546
  };
@@ -1619,8 +1728,8 @@ var bashDecision = (input) => {
1619
1728
  const items = found.map((finding) => ({ ruleId: finding.ruleId, where: lineWhere(input.t, finding) }));
1620
1729
  return { kind: "block", reason: input.t("secrets.block.bash", { found: describeFindings(input.t, items) }) };
1621
1730
  }
1622
- const commit = commitIntent(segments);
1623
- return commit === void 0 ? ALLOW : commitDecision(input, commit.includeUnstaged);
1731
+ const commit2 = commitIntent(segments);
1732
+ return commit2 === void 0 ? ALLOW : commitDecision(input, commit2.includeUnstaged);
1624
1733
  };
1625
1734
  var readDecision = (input) => {
1626
1735
  const inputs = toolInput(input.payload);
@@ -1793,6 +1902,185 @@ var runHook = async (event, options) => {
1793
1902
  }
1794
1903
  };
1795
1904
 
1905
+ // src/install/plugin-yield.ts
1906
+ import { join as join5 } from "node:path";
1907
+
1908
+ // src/install/layout.ts
1909
+ var STATE_DIR = ".finitemonkey";
1910
+ var SETTINGS_FILE = ".claude/settings.json";
1911
+ var GITIGNORE_FILE = ".gitignore";
1912
+ var CONFIG_FILE = ".finitemonkey/config.yml";
1913
+ var LOCK_FILE = ".finitemonkey/lock.json";
1914
+ var BACKUPS_DIR = ".finitemonkey/backups";
1915
+ var LOCKS_DIR = ".finitemonkey/locks";
1916
+ var INSTALL_LOCK = ".finitemonkey/locks/install.lock";
1917
+ var LOCAL_BIN = "node_modules/finitemonkey/dist/finitemonkey.mjs";
1918
+ var VENDOR_BIN = ".finitemonkey/runtime/dist/finitemonkey.mjs";
1919
+ var VENDOR_PACKAGE = ".finitemonkey/runtime/package.json";
1920
+ var LOCK_SCHEMA = "finitemonkey/lock@1";
1921
+ var PLAN_SCHEMA = "finitemonkey/install-plan@1";
1922
+ var GENERATED_MARKER = "finitemonkey:generated v=1";
1923
+ var BLOCK_BEGIN = "# finitemonkey:begin v=1";
1924
+ var BLOCK_END = "# finitemonkey:end";
1925
+ var INSTALL_LOCK_STALE_MS = 5 * 60 * 1e3;
1926
+ var PRESETS = ["neutral", "gabriel"];
1927
+ var parsePreset = (raw) => raw === void 0 ? "neutral" : PRESETS.find((preset) => preset === raw);
1928
+ var COMMITTED_STATE = ["config.yml", "runtime/", "tasks/", "rules.yml", "policy.yml", "findings/"];
1929
+
1930
+ // src/install/hook-entries.ts
1931
+ var INSTALLED_HOOKS = [
1932
+ { event: "SessionStart", timeout: 30, guard: false },
1933
+ { event: "UserPromptSubmit", timeout: 10, guard: true },
1934
+ { event: "PreToolUse", timeout: 10, guard: true },
1935
+ { event: "PostToolUse", timeout: 30, guard: false },
1936
+ { event: "Stop", timeout: 120, guard: false },
1937
+ { event: "SubagentStart", timeout: 30, guard: false },
1938
+ { event: "SubagentStop", timeout: 120, guard: false },
1939
+ { event: "PreCompact", timeout: 30, guard: false },
1940
+ { event: "ConfigChange", timeout: 10, guard: true }
1941
+ ];
1942
+ var PROJECT_BIN = `\${CLAUDE_PROJECT_DIR}/${LOCAL_BIN}`;
1943
+ var VENDORED_BIN = `\${CLAUDE_PROJECT_DIR}/${VENDOR_BIN}`;
1944
+ var isScript = (path) => /\.(c|m)?js$/i.test(path);
1945
+ var execForm = (runner, event) => {
1946
+ if (runner.kind === "local") return { command: "node", args: [PROJECT_BIN, "hook", event] };
1947
+ if (runner.kind === "vendored") return { command: "node", args: [VENDORED_BIN, "hook", event] };
1948
+ if (isScript(runner.path)) return { command: "node", args: [runner.path, "hook", event] };
1949
+ return { command: runner.path, args: ["hook", event] };
1950
+ };
1951
+ var hookHandler = (runner, spec, failClosedSupported) => {
1952
+ const failure2 = spec.guard && failClosedSupported ? { onFailure: "block" } : {};
1953
+ const { command, args } = execForm(runner, spec.event);
1954
+ return { type: "command", command, args, timeout: spec.timeout, ...failure2 };
1955
+ };
1956
+ var OWN_SHELL = /(^|[\s/"'])finitemonkey(@[^\s"]+|\.mjs|\.cmd|\.exe)?"?\s+hook\s/;
1957
+ var OWN_SCRIPT = /finitemonkey(\.mjs|\.cmd|\.exe)?$/;
1958
+ var stringsOf = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1959
+ var isOwnHandler = (handler) => {
1960
+ if (!isRecord(handler)) return false;
1961
+ const command = handler["command"];
1962
+ if (typeof command !== "string") return false;
1963
+ const args = stringsOf(handler["args"]);
1964
+ if (args.length === 0) return OWN_SHELL.test(`${command} `);
1965
+ const hookAt = args.indexOf("hook");
1966
+ if (hookAt < 0) return false;
1967
+ const target = hookAt === 0 ? command : args[hookAt - 1] ?? "";
1968
+ return OWN_SCRIPT.test(target);
1969
+ };
1970
+
1971
+ // src/install/json-text.ts
1972
+ var DEFAULT_STYLE = { indent: " ", finalNewline: true };
1973
+ var styleOf = (text2) => {
1974
+ const indented = /\n([ \t]+)\S/.exec(text2);
1975
+ return { indent: indented?.[1] ?? DEFAULT_STYLE.indent, finalNewline: text2.endsWith("\n") };
1976
+ };
1977
+ var renderJson = (value, style) => `${JSON.stringify(value, null, style.indent)}${style.finalNewline ? "\n" : ""}`;
1978
+ var parseSettings = (text2) => text2 === void 0 || text2.trim() === "" ? { ok: true, value: {} } : parseJsonObject(text2);
1979
+ var asList = (value) => Array.isArray(value) ? value : [];
1980
+ var asRecord = (value) => isRecord(value) ? value : {};
1981
+ var setKey = (record2, key, value) => {
1982
+ if (!(key in record2)) return value === void 0 ? record2 : { ...record2, [key]: value };
1983
+ return Object.fromEntries(
1984
+ Object.entries(record2).flatMap(([name, current]) => {
1985
+ if (name !== key) return [[name, current]];
1986
+ return value === void 0 ? [] : [[name, value]];
1987
+ })
1988
+ );
1989
+ };
1990
+
1991
+ // src/install/settings-merge.ts
1992
+ var SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
1993
+ var ATTRIBUTION_OFF = { commit: "", pr: "", sessionUrl: false };
1994
+ var MARKETPLACE_ENTRY = { source: { source: "github", repo: "gabrielvieiraf/finitemonkey" } };
1995
+ var MARKETPLACE_KEY = "finitemonkey";
1996
+ var PLUGIN_ID = "finitemonkey@finitemonkey";
1997
+ var withEntry = (settings, key, name, value) => {
1998
+ const inner = setKey(asRecord(settings[key]), name, value);
1999
+ return setKey(settings, key, Object.keys(inner).length === 0 ? void 0 : inner);
2000
+ };
2001
+ var hasEntry = (settings, key, name) => name in asRecord(settings[key]);
2002
+ var replaceInGroup = (group, isOwn, take) => {
2003
+ if (!isRecord(group)) return group;
2004
+ const handlers = asList(group["hooks"]).flatMap((handler) => {
2005
+ if (!isOwn(handler)) return [handler];
2006
+ const replacement = take();
2007
+ return replacement === void 0 ? [] : [replacement];
2008
+ });
2009
+ return handlers.length === 0 ? void 0 : { ...group, hooks: handlers };
2010
+ };
2011
+ var mergeEvent = (groups, isOwn, wanted) => {
2012
+ let pending = wanted;
2013
+ const take = () => {
2014
+ const next = pending;
2015
+ pending = void 0;
2016
+ return next;
2017
+ };
2018
+ const kept = groups.map((group) => replaceInGroup(group, isOwn, take)).filter((group) => group !== void 0);
2019
+ return pending === void 0 ? kept : [...kept, { hooks: [pending] }];
2020
+ };
2021
+ var mergeHooks = (hooks, isOwn, wanted) => {
2022
+ const events = [.../* @__PURE__ */ new Set([...Object.keys(hooks), ...Object.keys(wanted)])];
2023
+ const entries = events.flatMap((event) => {
2024
+ const groups = hooks[event];
2025
+ if (groups !== void 0 && !Array.isArray(groups)) return [[event, groups]];
2026
+ const merged = mergeEvent(asList(groups), isOwn, wanted[event]);
2027
+ return merged.length === 0 ? [] : [[event, merged]];
2028
+ });
2029
+ return Object.fromEntries(entries);
2030
+ };
2031
+ var withHooks = (settings, hooks) => {
2032
+ return setKey(settings, "hooks", Object.keys(hooks).length === 0 ? void 0 : hooks);
2033
+ };
2034
+ var withPermissions = (settings, deny) => {
2035
+ const permissions = asRecord(settings["permissions"]);
2036
+ const next = setKey(permissions, "deny", deny.length === 0 ? void 0 : deny);
2037
+ return setKey(settings, "permissions", Object.keys(next).length === 0 ? void 0 : next);
2038
+ };
2039
+ var ownHookMatcher = (recorded) => (handler) => isOwnHandler(handler) || recorded.some((own) => JSON.stringify(own) === JSON.stringify(handler));
2040
+ var mergeSettings = (current, wanted, recorded) => {
2041
+ const schema = Object.keys(current).length === 0;
2042
+ const base = schema ? { $schema: SETTINGS_SCHEMA_URL } : current;
2043
+ const hooks = mergeHooks(asRecord(base["hooks"]), ownHookMatcher(recorded), wanted.hooks);
2044
+ const existingDeny = asList(asRecord(base["permissions"])["deny"]);
2045
+ const deny = wanted.deny.filter((rule) => !existingDeny.includes(rule));
2046
+ const attribution = wanted.attributionOff && !("attribution" in base);
2047
+ const marketplace = wanted.marketplace && !hasEntry(base, "extraKnownMarketplaces", MARKETPLACE_KEY);
2048
+ const plugin = wanted.marketplace && !hasEntry(base, "enabledPlugins", PLUGIN_ID);
2049
+ let settings = withPermissions(withHooks(base, hooks), [...existingDeny, ...deny]);
2050
+ if (attribution) settings = { ...settings, attribution: ATTRIBUTION_OFF };
2051
+ if (marketplace) settings = withEntry(settings, "extraKnownMarketplaces", MARKETPLACE_KEY, MARKETPLACE_ENTRY);
2052
+ if (plugin) settings = withEntry(settings, "enabledPlugins", PLUGIN_ID, true);
2053
+ return { settings, added: { schema, deny, attribution, marketplace, plugin } };
2054
+ };
2055
+ var sameAttribution = (value) => JSON.stringify(value) === JSON.stringify(ATTRIBUTION_OFF);
2056
+ var unmergeSettings = (current, added, recorded) => {
2057
+ const hooks = mergeHooks(asRecord(current["hooks"]), ownHookMatcher(recorded), {});
2058
+ const deny = asList(asRecord(current["permissions"])["deny"]).filter(
2059
+ (rule) => typeof rule !== "string" || !added.deny.includes(rule)
2060
+ );
2061
+ let settings = withPermissions(withHooks(current, hooks), deny);
2062
+ if (added.attribution && sameAttribution(settings["attribution"])) {
2063
+ settings = setKey(settings, "attribution", void 0);
2064
+ }
2065
+ if (added.marketplace) settings = withEntry(settings, "extraKnownMarketplaces", MARKETPLACE_KEY, void 0);
2066
+ if (added.plugin) settings = withEntry(settings, "enabledPlugins", PLUGIN_ID, void 0);
2067
+ if (added.schema && Object.keys(settings).length === 1 && settings["$schema"] === SETTINGS_SCHEMA_URL) return {};
2068
+ return settings;
2069
+ };
2070
+ var ownHandlersIn = (settings) => Object.values(asRecord(settings["hooks"])).flatMap((groups) => asList(groups)).flatMap((group) => asList(asRecord(group)["hooks"])).filter(isOwnHandler).length;
2071
+
2072
+ // src/install/plugin-yield.ts
2073
+ var PLUGIN_VIA = "plugin";
2074
+ var YIELD_STDOUT = `${JSON.stringify({ continue: true })}
2075
+ `;
2076
+ var projectOwnsHooks = (ctx) => {
2077
+ const root = ctx.env["CLAUDE_PROJECT_DIR"] ?? ctx.cwd;
2078
+ const text2 = ctx.runtime.readText(toAbsolutePath(join5(root, ...SETTINGS_FILE.split("/"))));
2079
+ if (!text2.ok) return false;
2080
+ const parsed = parseJsonObject(text2.value);
2081
+ return parsed.ok && ownHandlersIn(parsed.value) > 0;
2082
+ };
2083
+
1796
2084
  // src/commands/hook.ts
1797
2085
  var hookCommand = {
1798
2086
  path: ["hook"],
@@ -1800,6 +2088,7 @@ var hookCommand = {
1800
2088
  usage: `finitemonkey hook <${HOOK_EVENTS.join("|")}>`,
1801
2089
  status: "ready",
1802
2090
  maxPositionals: 1,
2091
+ options: { via: { type: "string" } },
1803
2092
  run: async (ctx, args) => {
1804
2093
  const raw = args.positionals[0] ?? "";
1805
2094
  const event = parseHookEvent(raw);
@@ -1810,6 +2099,9 @@ var hookCommand = {
1810
2099
  usageProblem("unknown_hook_event", "error.unknown_hook_event", "finitemonkey help hook", params)
1811
2100
  );
1812
2101
  }
2102
+ if (stringOption(args.values, "via") === PLUGIN_VIA && projectOwnsHooks(ctx)) {
2103
+ return { ok: true, exitCode: 0, data: { event, verdict: "allow" }, raw: { stdout: YIELD_STDOUT, stderr: "" } };
2104
+ }
1813
2105
  const outcome = await runHook(event, {
1814
2106
  stdin: ctx.io.stdin,
1815
2107
  deadlineMs: parseDeadline(ctx.env["FINITEMONKEY_HOOK_DEADLINE_MS"]),
@@ -1834,7 +2126,7 @@ var hookCommand = {
1834
2126
  };
1835
2127
 
1836
2128
  // src/commands/i18n-check.ts
1837
- import { join as join5 } from "node:path";
2129
+ import { join as join6 } from "node:path";
1838
2130
 
1839
2131
  // src/i18n/en.json
1840
2132
  var en_default = {
@@ -1964,7 +2256,36 @@ var en_default = {
1964
2256
  "secrets.error.incident_not_found": "No incident has the id {id}",
1965
2257
  "secrets.error.not_rotated": "Incident {id} is {state}: rotate the key at the provider first, then mark it rotated",
1966
2258
  "secrets.error.bad_transition": "Incident {id} cannot go from {from} to {to}",
1967
- "secrets.error.write_failed": "Could not write {file}: {detail}"
2259
+ "secrets.error.write_failed": "Could not write {file}: {detail}",
2260
+ "install.config.header": "Finite Monkey project settings. Commit this file. `finitemonkey update` keeps your edits.",
2261
+ "install.plan.title": "Finite Monkey plan for {root} (preset {preset}):",
2262
+ "install.warning": "warning: {message}",
2263
+ "install.warn.not_git": "this folder is not inside a git repository; installing here anyway",
2264
+ "install.warn.fail_open": "Claude Code {version} cannot block an action when a hook crashes (needs 2.1.295 or newer); guards fail open until you update Claude Code",
2265
+ "install.warn.authority": "{name} ({source}) also decides when a session may stop; Finite Monkey works alongside it and never turns it off",
2266
+ "install.warn.plugin": "the finitemonkey plugin is enabled ({source}); its hooks step aside while this project has its own Finite Monkey hooks",
2267
+ "install.warn.foreign": "{path} already exists and was not written by Finite Monkey; left untouched (proposed content in the diff)",
2268
+ "install.warn.edited": "{path} was edited after install; your version is kept (the new template is in the diff)",
2269
+ "install.warn.edited_kept": "kept {path}: you edited it after install",
2270
+ "install.dry_run": "Dry run: nothing was written.",
2271
+ "install.nothing_to_do": "Already up to date. Nothing to write.",
2272
+ "install.up_to_date": "The files Finite Monkey manages match this version.",
2273
+ "install.confirm_question": "apply the plan above (every touched file is backed up first)?",
2274
+ "install.done_init": "Installed. Open Claude Code and keep working: one monkey, finite time, finished work.",
2275
+ "install.done_update": "Updated. Your edits were kept.",
2276
+ "install.uninstall.title": "Removing Finite Monkey from {root}:",
2277
+ "install.uninstall.keeps_state": ".finitemonkey/ stays (backups and evidence). Add --purge to delete it too.",
2278
+ "install.uninstall.purge": ".finitemonkey/ will be deleted, including backups and evidence.",
2279
+ "install.uninstall.outside": "Not touched here, remove it yourself if you installed it: {command}",
2280
+ "install.uninstall.question": "remove what Finite Monkey added and restore the backups?",
2281
+ "install.uninstall.done": "Removed. The project is back to how it was, except files you edited.",
2282
+ "error.settings_unreadable": ".claude/settings.json is not valid JSON ({detail}), so it was not changed",
2283
+ "error.lock_unreadable": ".finitemonkey/lock.json is unreadable ({detail})",
2284
+ "error.install_locked": "another finitemonkey run holds {path}",
2285
+ "error.install_failed": "could not write {path} ({detail}); every file written in this run was put back",
2286
+ "error.not_installed": "Finite Monkey is not installed in this project (no .finitemonkey/lock.json)",
2287
+ "error.install_outdated": "the files Finite Monkey manages differ from this version",
2288
+ "error.invalid_preset": 'Unknown preset "{preset}". Use neutral or gabriel'
1968
2289
  };
1969
2290
 
1970
2291
  // src/i18n/es.json
@@ -2095,7 +2416,36 @@ var es_default = {
2095
2416
  "secrets.error.incident_not_found": "Ningún incidente tiene el id {id}",
2096
2417
  "secrets.error.not_rotated": "El incidente {id} está {state}: rota la clave en el proveedor primero y luego márcalo como rotado",
2097
2418
  "secrets.error.bad_transition": "El incidente {id} no puede pasar de {from} a {to}",
2098
- "secrets.error.write_failed": "No se pudo escribir {file}: {detail}"
2419
+ "secrets.error.write_failed": "No se pudo escribir {file}: {detail}",
2420
+ "install.config.header": "Configuración de Finite Monkey en este proyecto. Versiona este archivo. `finitemonkey update` conserva tus cambios.",
2421
+ "install.plan.title": "Plan de Finite Monkey para {root} (preset {preset}):",
2422
+ "install.warning": "aviso: {message}",
2423
+ "install.warn.not_git": "esta carpeta no está dentro de un repositorio git; se instala aquí de todos modos",
2424
+ "install.warn.fail_open": "Claude Code {version} no puede bloquear la acción cuando un hook falla (necesita 2.1.295 o más nueva); los guardas fallan abiertos hasta que actualices Claude Code",
2425
+ "install.warn.authority": "{name} ({source}) también decide cuándo puede terminar la sesión; Finite Monkey convive con él y nunca lo desactiva",
2426
+ "install.warn.plugin": "el plugin finitemonkey está habilitado ({source}); sus hooks ceden el paso mientras este proyecto tenga sus propios hooks de Finite Monkey",
2427
+ "install.warn.foreign": "{path} ya existe y no lo escribió Finite Monkey; quedó intacto (contenido propuesto en el diff)",
2428
+ "install.warn.edited": "{path} se editó después de instalar; se conserva tu versión (la plantilla nueva está en el diff)",
2429
+ "install.warn.edited_kept": "conservado {path}: lo editaste después de instalar",
2430
+ "install.dry_run": "Simulación: no se escribió nada.",
2431
+ "install.nothing_to_do": "Ya está al día. Nada que escribir.",
2432
+ "install.up_to_date": "Los archivos que gestiona Finite Monkey coinciden con esta versión.",
2433
+ "install.confirm_question": "¿aplicar el plan de arriba (cada archivo tocado se respalda antes)?",
2434
+ "install.done_init": "Instalado. Abre Claude Code y sigue trabajando: un mono, tiempo finito, trabajo terminado.",
2435
+ "install.done_update": "Actualizado. Se conservaron tus cambios.",
2436
+ "install.uninstall.title": "Quitando Finite Monkey de {root}:",
2437
+ "install.uninstall.keeps_state": ".finitemonkey/ se queda (respaldos y evidencias). Usa --purge para borrarlo también.",
2438
+ "install.uninstall.purge": ".finitemonkey/ se borrará, incluidos respaldos y evidencias.",
2439
+ "install.uninstall.outside": "No se toca aquí, quítalo tú si lo instalaste: {command}",
2440
+ "install.uninstall.question": "¿quitar lo que agregó Finite Monkey y restaurar los respaldos?",
2441
+ "install.uninstall.done": "Quitado. El proyecto volvió a como estaba, salvo los archivos que editaste.",
2442
+ "error.settings_unreadable": ".claude/settings.json no es JSON válido ({detail}), así que no se cambió",
2443
+ "error.lock_unreadable": ".finitemonkey/lock.json no se puede leer ({detail})",
2444
+ "error.install_locked": "otra ejecución de finitemonkey tiene {path}",
2445
+ "error.install_failed": "no se pudo escribir {path} ({detail}); se restauró cada archivo escrito en esta ejecución",
2446
+ "error.not_installed": "Finite Monkey no está instalado en este proyecto (no hay .finitemonkey/lock.json)",
2447
+ "error.install_outdated": "los archivos que gestiona Finite Monkey difieren de esta versión",
2448
+ "error.invalid_preset": 'Preset desconocido "{preset}". Usa neutral o gabriel'
2099
2449
  };
2100
2450
 
2101
2451
  // src/i18n/pt-BR.json
@@ -2226,7 +2576,36 @@ var pt_BR_default = {
2226
2576
  "secrets.error.incident_not_found": "Nenhum incidente tem o id {id}",
2227
2577
  "secrets.error.not_rotated": "O incidente {id} está {state}: troque a chave no provedor primeiro e depois marque como rotacionado",
2228
2578
  "secrets.error.bad_transition": "O incidente {id} não pode ir de {from} para {to}",
2229
- "secrets.error.write_failed": "Não foi possível gravar {file}: {detail}"
2579
+ "secrets.error.write_failed": "Não foi possível gravar {file}: {detail}",
2580
+ "install.config.header": "Configuração do Finite Monkey neste projeto. Versione este arquivo. O `finitemonkey update` mantém suas edições.",
2581
+ "install.plan.title": "Plano do Finite Monkey para {root} (preset {preset}):",
2582
+ "install.warning": "aviso: {message}",
2583
+ "install.warn.not_git": "esta pasta não está dentro de um repositório git; instalando aqui mesmo assim",
2584
+ "install.warn.fail_open": "o Claude Code {version} não consegue barrar a ação quando um hook quebra (precisa da 2.1.295 ou mais nova); as travas falham abertas até você atualizar o Claude Code",
2585
+ "install.warn.authority": "{name} ({source}) também decide quando a sessão pode parar; o Finite Monkey convive com ele e nunca o desliga",
2586
+ "install.warn.plugin": "o plugin finitemonkey está habilitado ({source}); os hooks dele cedem a vez enquanto este projeto tiver os próprios hooks do Finite Monkey",
2587
+ "install.warn.foreign": "{path} já existe e não foi escrito pelo Finite Monkey; ficou intocado (conteúdo proposto no diff)",
2588
+ "install.warn.edited": "{path} foi editado depois da instalação; sua versão foi mantida (o modelo novo está no diff)",
2589
+ "install.warn.edited_kept": "mantido {path}: você o editou depois da instalação",
2590
+ "install.dry_run": "Simulação: nada foi gravado.",
2591
+ "install.nothing_to_do": "Já está em dia. Nada a gravar.",
2592
+ "install.up_to_date": "Os arquivos que o Finite Monkey gerencia batem com esta versão.",
2593
+ "install.confirm_question": "aplicar o plano acima (todo arquivo tocado ganha backup antes)?",
2594
+ "install.done_init": "Instalado. Abra o Claude Code e siga trabalhando: um macaco, tempo finito, trabalho terminado.",
2595
+ "install.done_update": "Atualizado. Suas edições foram mantidas.",
2596
+ "install.uninstall.title": "Removendo o Finite Monkey de {root}:",
2597
+ "install.uninstall.keeps_state": ".finitemonkey/ fica (backups e evidências). Use --purge para apagar também.",
2598
+ "install.uninstall.purge": ".finitemonkey/ será apagado, inclusive backups e evidências.",
2599
+ "install.uninstall.outside": "Não mexido aqui, remova você mesmo se instalou: {command}",
2600
+ "install.uninstall.question": "remover o que o Finite Monkey acrescentou e restaurar os backups?",
2601
+ "install.uninstall.done": "Removido. O projeto voltou ao que era, exceto os arquivos que você editou.",
2602
+ "error.settings_unreadable": ".claude/settings.json não é JSON válido ({detail}), então não foi alterado",
2603
+ "error.lock_unreadable": ".finitemonkey/lock.json está ilegível ({detail})",
2604
+ "error.install_locked": "outra execução do finitemonkey está segurando {path}",
2605
+ "error.install_failed": "não foi possível gravar {path} ({detail}); todo arquivo gravado nesta execução foi restaurado",
2606
+ "error.not_installed": "O Finite Monkey não está instalado neste projeto (sem .finitemonkey/lock.json)",
2607
+ "error.install_outdated": "os arquivos que o Finite Monkey gerencia diferem desta versão",
2608
+ "error.invalid_preset": 'Preset desconhecido "{preset}". Use neutral ou gabriel'
2230
2609
  };
2231
2610
 
2232
2611
  // src/i18n/catalogs.ts
@@ -2304,7 +2683,7 @@ var unreadable = (dir, file, detail) => ({
2304
2683
  exitCode: ExitCode.NotConfigured
2305
2684
  });
2306
2685
  var readCatalog = (ctx, dir, lang) => {
2307
- const file = toAbsolutePath(join5(dir, `${lang}.json`));
2686
+ const file = toAbsolutePath(join6(dir, `${lang}.json`));
2308
2687
  const text2 = ctx.runtime.readText(file);
2309
2688
  if (!text2.ok) return reject(unreadable(dir, file, text2.error));
2310
2689
  const parsed = parseJsonObject(text2.value);
@@ -2319,12 +2698,12 @@ var loadCatalogs = (ctx, dir) => {
2319
2698
  if (!es.ok) return es;
2320
2699
  return accept({ en: en.value, "pt-BR": ptBR.value, es: es.value });
2321
2700
  };
2322
- var describeProblem = (ctx, problem) => {
2323
- const kind = ctx.t(`i18n.problem.${problem.kind}`, {
2324
- expected: (problem.expected ?? []).join(", "),
2325
- actual: (problem.actual ?? []).join(", ")
2701
+ var describeProblem = (ctx, problem2) => {
2702
+ const kind = ctx.t(`i18n.problem.${problem2.kind}`, {
2703
+ expected: (problem2.expected ?? []).join(", "),
2704
+ actual: (problem2.actual ?? []).join(", ")
2326
2705
  });
2327
- return ` ${problem.lang} ${problem.key} ${kind}`;
2706
+ return ` ${problem2.lang} ${problem2.key} ${kind}`;
2328
2707
  };
2329
2708
  var runI18nCheck = (ctx, dir) => {
2330
2709
  const input = dir === void 0 ? accept(catalogs) : loadCatalogs(ctx, dir);
@@ -2337,7 +2716,7 @@ var runI18nCheck = (ctx, dir) => {
2337
2716
  ok: false,
2338
2717
  exitCode: ExitCode.CheckFailed,
2339
2718
  data,
2340
- human: [summary, ...report.problems.map((problem) => describeProblem(ctx, problem))],
2719
+ human: [summary, ...report.problems.map((problem2) => describeProblem(ctx, problem2))],
2341
2720
  error: {
2342
2721
  code: "i18n_mismatch",
2343
2722
  message: ctx.t("error.i18n_mismatch", { count: report.problems.length }),
@@ -2357,34 +2736,700 @@ var i18nCheckCommand = {
2357
2736
  }
2358
2737
  };
2359
2738
 
2360
- // src/commands/planned.ts
2361
- var planned = (name, summaryKey, options) => ({
2362
- path: [name],
2363
- summaryKey,
2364
- usage: `finitemonkey ${name}`,
2365
- status: "planned",
2366
- options,
2367
- maxPositionals: 8,
2368
- run: (ctx) => Promise.resolve(
2369
- failureFrom(ctx.t, {
2370
- code: "not_implemented",
2371
- messageKey: "error.not_implemented",
2372
- params: { command: name },
2373
- fix: "finitemonkey help",
2374
- exitCode: ExitCode.Usage,
2375
- data: { command: name, status: "planned" }
2739
+ // src/install/apply.ts
2740
+ import { dirname as dirname2 } from "node:path";
2741
+
2742
+ // src/install/hash.ts
2743
+ import { createHash as createHash2 } from "node:crypto";
2744
+ var hashText = (text2) => `sha256:${createHash2("sha256").update(text2, "utf8").digest("hex")}`;
2745
+
2746
+ // src/install/lock-file.ts
2747
+ var ENTRY_KINDS = ["settings", "gitignore", "generated"];
2748
+ var strings = (value) => Array.isArray(value) && value.every((item) => typeof item === "string") ? value : void 0;
2749
+ var parseAdded = (value) => {
2750
+ if (!isRecord(value)) return void 0;
2751
+ const deny = strings(value["deny"]);
2752
+ const { schema, attribution } = value;
2753
+ if (deny === void 0 || typeof schema !== "boolean" || typeof attribution !== "boolean") return void 0;
2754
+ const marketplace = value["marketplace"] === true;
2755
+ const plugin = value["plugin"] === true;
2756
+ return { schema, attribution, deny, marketplace, plugin };
2757
+ };
2758
+ var parseEntry2 = (value) => {
2759
+ if (!isRecord(value)) return void 0;
2760
+ const { path, kind, existed, hash, backup } = value;
2761
+ const known = ENTRY_KINDS.find((item) => item === kind);
2762
+ if (typeof path !== "string" || known === void 0 || typeof existed !== "boolean" || typeof hash !== "string") {
2763
+ return void 0;
2764
+ }
2765
+ const added = parseAdded(value["added"]);
2766
+ const handlers = Array.isArray(value["handlers"]) ? value["handlers"].filter(isRecord) : void 0;
2767
+ return {
2768
+ path,
2769
+ kind: known,
2770
+ existed,
2771
+ hash,
2772
+ ...typeof backup === "string" ? { backup } : {},
2773
+ ...added === void 0 ? {} : { added },
2774
+ ...handlers === void 0 ? {} : { handlers }
2775
+ };
2776
+ };
2777
+ var parseEntries = (value) => {
2778
+ if (!Array.isArray(value)) return void 0;
2779
+ const entries = value.map(parseEntry2);
2780
+ return entries.every((entry) => entry !== void 0) ? entries : void 0;
2781
+ };
2782
+ var fromRecord = (record2) => {
2783
+ const { schema, version, preset, installedAt, updatedAt } = record2;
2784
+ const known = PRESETS.find((item) => item === preset);
2785
+ const entries = parseEntries(record2["entries"]);
2786
+ const createdDirs = strings(record2["createdDirs"]);
2787
+ if (schema !== LOCK_SCHEMA) return reject(`schema is not ${LOCK_SCHEMA}`);
2788
+ if (typeof version !== "string" || typeof installedAt !== "string" || typeof updatedAt !== "string") {
2789
+ return reject("version, installedAt and updatedAt must be text");
2790
+ }
2791
+ if (known === void 0 || entries === void 0 || createdDirs === void 0) {
2792
+ return reject("preset, entries or createdDirs is malformed");
2793
+ }
2794
+ return accept({ schema, version, preset: known, installedAt, updatedAt, createdDirs, entries });
2795
+ };
2796
+ var parseLock = (text2) => {
2797
+ const parsed = parseJsonObject(text2);
2798
+ return parsed.ok ? fromRecord(parsed.value) : reject(parsed.error);
2799
+ };
2800
+ var renderLock = (lock) => `${JSON.stringify(lock, null, 2)}
2801
+ `;
2802
+
2803
+ // src/install/claude-version.ts
2804
+ var ON_FAILURE_MIN = [2, 1, 295];
2805
+ var PROBE_MS = 8e3;
2806
+ var parseClaudeVersion = (output) => {
2807
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(output);
2808
+ return match === null ? void 0 : [Number(match[1]), Number(match[2]), Number(match[3])];
2809
+ };
2810
+ var atLeast = (version, min) => {
2811
+ for (const [index, part] of min.entries()) {
2812
+ const value = version[index] ?? 0;
2813
+ if (value !== part) return value > part;
2814
+ }
2815
+ return true;
2816
+ };
2817
+ var detectClaude = (probe) => {
2818
+ const result = probe("claude", ["--version"], PROBE_MS);
2819
+ const parsed = result.ok ? parseClaudeVersion(result.stdout) : void 0;
2820
+ if (parsed === void 0) return { version: void 0, failClosed: true };
2821
+ return { version: parsed.join("."), failClosed: atLeast(parsed, ON_FAILURE_MIN) };
2822
+ };
2823
+
2824
+ // src/install/config-template.ts
2825
+ var configTemplate = (t, preset, lang) => [
2826
+ `# ${GENERATED_MARKER}`,
2827
+ `# ${t("install.config.header")}`,
2828
+ "schemaVersion: 1",
2829
+ `preset: ${preset}`,
2830
+ `lang: ${lang}`,
2831
+ ""
2832
+ ].join("\n");
2833
+ var hasMarker = (text2) => text2.includes(GENERATED_MARKER);
2834
+
2835
+ // src/install/deny-floor.ts
2836
+ var DENY_FLOOR = {
2837
+ secrets: ["Read(./.env)", "Read(./.env.*)", "Read(./**/.env)", "Read(./**/.env.*)", "Read(./**/*.pem)"],
2838
+ git: [
2839
+ "Bash(git push --force *)",
2840
+ "Bash(git push -f *)",
2841
+ "Bash(git clean -f*)",
2842
+ "Bash(git reset --hard*)",
2843
+ "Bash(git worktree remove --force*)"
2844
+ ],
2845
+ proof: [
2846
+ "Edit(./.finitemonkey/evidence/**)",
2847
+ "Edit(./.finitemonkey/runtime/**)",
2848
+ "Edit(./.finitemonkey/lock.json)",
2849
+ "Edit(./.finitemonkey/log.jsonl)",
2850
+ "Edit(./.finitemonkey/state.md)"
2851
+ ]
2852
+ };
2853
+ var DENY_RULES = [...DENY_FLOOR.secrets, ...DENY_FLOOR.git, ...DENY_FLOOR.proof];
2854
+
2855
+ // src/install/detect.ts
2856
+ import { join as join7 } from "node:path";
2857
+ var PACKAGE_MANAGERS = [
2858
+ { name: "pnpm", files: ["pnpm-lock.yaml"] },
2859
+ { name: "yarn", files: ["yarn.lock"] },
2860
+ { name: "bun", files: ["bun.lock", "bun.lockb"] },
2861
+ { name: "npm", files: ["package-lock.json", "npm-shrinkwrap.json"] }
2862
+ ];
2863
+ var LANGUAGE_MARKERS = [
2864
+ { name: "typescript", files: ["tsconfig.json"] },
2865
+ { name: "javascript", files: ["package.json"] },
2866
+ { name: "python", files: ["pyproject.toml", "requirements.txt", "setup.py"] },
2867
+ { name: "go", files: ["go.mod"] },
2868
+ { name: "rust", files: ["Cargo.toml"] },
2869
+ { name: "ruby", files: ["Gemfile"] },
2870
+ { name: "java", files: ["pom.xml", "build.gradle", "build.gradle.kts"] }
2871
+ ];
2872
+ var LOOP_PLUGINS = ["oh-my-claudecode", "ralph-loop", "ralph-wiggum", "everything-claude-code", "superclaude"];
2873
+ var WORKFLOW_DIRS = [
2874
+ { dir: ".planning", name: "gsd" },
2875
+ { dir: ".bmad-core", name: "bmad" },
2876
+ { dir: ".specify", name: "spec-kit" },
2877
+ { dir: "openspec", name: "openspec" }
2878
+ ];
2879
+ var fromPackageJson = (project) => {
2880
+ const text2 = project.read("package.json");
2881
+ const parsed = text2 === void 0 ? void 0 : parseJsonObject(text2);
2882
+ const field = parsed?.ok === true ? parsed.value["packageManager"] : void 0;
2883
+ if (typeof field !== "string") return void 0;
2884
+ return PACKAGE_MANAGERS.find(({ name }) => field.startsWith(`${name}@`))?.name;
2885
+ };
2886
+ var detectPackageManager = (project) => fromPackageJson(project) ?? PACKAGE_MANAGERS.find(({ files }) => files.some((file) => project.exists(file)))?.name;
2887
+ var detectLanguages = (project) => LANGUAGE_MARKERS.filter(({ files }) => files.some((file) => project.exists(file))).map(({ name }) => name);
2888
+ var handlersOf = (settings, event) => {
2889
+ const hooks = settings["hooks"];
2890
+ const groups = isRecord(hooks) ? hooks[event] : void 0;
2891
+ return asList(groups).flatMap((group) => asList(asRecord(group)["hooks"]));
2892
+ };
2893
+ var allHandlers = (settings) => {
2894
+ const hooks = settings["hooks"];
2895
+ return isRecord(hooks) ? Object.keys(hooks).flatMap((event) => handlersOf(settings, event)) : [];
2896
+ };
2897
+ var settingsInfo = (text2) => {
2898
+ if (text2 === void 0) return { exists: false, valid: true, userHooks: 0, ownHooks: 0 };
2899
+ const parsed = text2.trim() === "" ? { ok: true, value: {} } : parseJsonObject(text2);
2900
+ if (!parsed.ok) return { exists: true, valid: false, userHooks: 0, ownHooks: 0 };
2901
+ const handlers = allHandlers(parsed.value);
2902
+ const own = handlers.filter(isOwnHandler).length;
2903
+ return { exists: true, valid: true, userHooks: handlers.length - own, ownHooks: own };
2904
+ };
2905
+ var pluginAuthorities = (settings, source) => {
2906
+ const enabled = settings["enabledPlugins"];
2907
+ if (!isRecord(enabled)) return [];
2908
+ return Object.entries(enabled).filter(([, on]) => on === true).map(([id]) => id.split("@")[0] ?? id).flatMap((name) => {
2909
+ if (name === "finitemonkey") return [{ kind: "finitemonkey-plugin", source, name }];
2910
+ return LOOP_PLUGINS.includes(name) ? [{ kind: "loop-plugin", source, name }] : [];
2911
+ });
2912
+ };
2913
+ var scopeAuthorities = (text2, source) => {
2914
+ const parsed = text2 === void 0 ? void 0 : parseJsonObject(text2);
2915
+ if (parsed?.ok !== true) return [];
2916
+ const stops = ["Stop", "SubagentStop"].flatMap(
2917
+ (event) => handlersOf(parsed.value, event).filter((handler) => !isOwnHandler(handler)).map((handler) => {
2918
+ const command = isRecord(handler) ? handler["command"] : void 0;
2919
+ return { kind: "stop-hook", source, name: `${event}: ${typeof command === "string" ? command : "?"}` };
2376
2920
  })
2377
- )
2921
+ );
2922
+ return [...stops, ...pluginAuthorities(parsed.value, source)];
2923
+ };
2924
+ var userSettingsText = (project, env) => {
2925
+ const home = env["CLAUDE_CONFIG_DIR"] ?? env["HOME"] ?? env["USERPROFILE"];
2926
+ if (home === void 0) return void 0;
2927
+ const file = env["CLAUDE_CONFIG_DIR"] === void 0 ? join7(home, ".claude", "settings.json") : join7(home, "settings.json");
2928
+ const text2 = project.runtime.readText(toAbsolutePath(file));
2929
+ return text2.ok ? text2.value : void 0;
2930
+ };
2931
+ var detectAuthorities = (project, env) => [
2932
+ ...scopeAuthorities(project.read(SETTINGS_FILE), "project"),
2933
+ ...scopeAuthorities(project.read(".claude/settings.local.json"), "local"),
2934
+ ...scopeAuthorities(userSettingsText(project, env), "user"),
2935
+ ...WORKFLOW_DIRS.filter(({ dir }) => project.isDir(dir)).map(({ name }) => ({
2936
+ kind: "workflow",
2937
+ source: "project-dir",
2938
+ name
2939
+ }))
2940
+ ];
2941
+ var chooseRunner = (project, bin) => {
2942
+ if (bin !== void 0) return { kind: "path", path: bin };
2943
+ return project.exists(LOCAL_BIN) ? { kind: "local" } : { kind: "vendored" };
2944
+ };
2945
+
2946
+ // src/install/gitignore-block.ts
2947
+ var gitignoreBlock = () => [
2948
+ BLOCK_BEGIN,
2949
+ `${STATE_DIR}/*`,
2950
+ ...COMMITTED_STATE.map((path) => `!${STATE_DIR}/${path}`),
2951
+ BLOCK_END
2952
+ ];
2953
+ var lineBreak = (text2) => text2.includes("\r\n") ? "\r\n" : "\n";
2954
+ var blockRange = (lines) => {
2955
+ const start = lines.findIndex((line) => line.trimEnd() === BLOCK_BEGIN);
2956
+ if (start < 0) return void 0;
2957
+ const end = lines.findIndex((line, index) => index > start && line.trimEnd() === BLOCK_END);
2958
+ return end < 0 ? void 0 : { start, end };
2959
+ };
2960
+ var withBlock = (text2) => {
2961
+ const eol = text2 === void 0 ? "\n" : lineBreak(text2);
2962
+ const block2 = gitignoreBlock();
2963
+ if (text2 === void 0 || text2 === "") return `${block2.join(eol)}${eol}`;
2964
+ const lines = text2.split(eol);
2965
+ const range = blockRange(lines);
2966
+ if (range !== void 0) return [...lines.slice(0, range.start), ...block2, ...lines.slice(range.end + 1)].join(eol);
2967
+ const separator = text2.endsWith(eol) ? "" : eol;
2968
+ return `${text2}${separator}${block2.join(eol)}${eol}`;
2969
+ };
2970
+ var withoutBlock = (text2) => {
2971
+ const eol = lineBreak(text2);
2972
+ const lines = text2.split(eol);
2973
+ const range = blockRange(lines);
2974
+ if (range === void 0) return text2;
2975
+ return [...lines.slice(0, range.start), ...lines.slice(range.end + 1)].join(eol);
2976
+ };
2977
+
2978
+ // src/install/vendor.ts
2979
+ var isBundle = (text2) => text2.startsWith("#!/usr/bin/env node") && text2.includes("finitemonkey");
2980
+ var isOwnPackage = (text2) => {
2981
+ const parsed = parseJsonObject(text2);
2982
+ return parsed.ok && parsed.value["name"] === "finitemonkey";
2983
+ };
2984
+ var vendoredPackage = (version) => `${JSON.stringify({ name: "finitemonkey", version, private: true, type: "module" }, null, 2)}
2985
+ `;
2986
+ var vendoredFiles = (bundle, version) => [
2987
+ { path: VENDOR_BIN, template: bundle, isOurs: isBundle },
2988
+ { path: VENDOR_PACKAGE, template: vendoredPackage(version), isOurs: isOwnPackage }
2989
+ ];
2990
+ var isBundlePath = (path) => path.endsWith(".mjs");
2991
+
2992
+ // src/install/plan.ts
2993
+ var CHANGING = ["create", "merge", "update"];
2994
+ var detect = (project, gitRoot, input) => ({
2995
+ root: project.root,
2996
+ gitRoot,
2997
+ packageManager: detectPackageManager(project),
2998
+ languages: detectLanguages(project),
2999
+ settings: settingsInfo(project.read(SETTINGS_FILE)),
3000
+ authorities: detectAuthorities(project, input.env),
3001
+ runner: chooseRunner(project, input.bin),
3002
+ claude: detectClaude(project.runtime.probe)
2378
3003
  });
2379
- var initCommand = planned("init", "command.init.summary", {
2380
- preset: { type: "string" },
2381
- github: { type: "boolean" }
3004
+ var readPreviousLock = (project) => {
3005
+ const text2 = project.read(LOCK_FILE);
3006
+ if (text2 === void 0) return accept(void 0);
3007
+ const parsed = parseLock(text2);
3008
+ return parsed.ok ? accept(parsed.value) : reject({ kind: "lock", detail: parsed.error });
3009
+ };
3010
+ var actionFor = (before, after, existing) => {
3011
+ if (before === void 0) return "create";
3012
+ return before === after ? "unchanged" : existing;
3013
+ };
3014
+ var union = (a, b) => [.../* @__PURE__ */ new Set([...a, ...b])];
3015
+ var joinAdded = (previous, next) => previous === void 0 ? next : {
3016
+ schema: previous.schema || next.schema,
3017
+ attribution: previous.attribution || next.attribution,
3018
+ deny: union(previous.deny, next.deny),
3019
+ marketplace: previous.marketplace || next.marketplace,
3020
+ plugin: previous.plugin || next.plugin
3021
+ };
3022
+ var wantedHooks = (detection) => Object.fromEntries(
3023
+ INSTALLED_HOOKS.map((spec) => [spec.event, hookHandler(detection.runner, spec, detection.claude.failClosed)])
3024
+ );
3025
+ var settingsItem = (project, detection, input, previous) => {
3026
+ const before = project.read(SETTINGS_FILE);
3027
+ const current = parseSettings(before);
3028
+ if (!current.ok) return reject({ kind: "settings", detail: current.error });
3029
+ const hooks = wantedHooks(detection);
3030
+ const wanted = {
3031
+ hooks,
3032
+ deny: DENY_RULES,
3033
+ attributionOff: input.preset === "gabriel",
3034
+ marketplace: input.marketplace
3035
+ };
3036
+ const merged = mergeSettings(current.value, wanted, previous?.handlers ?? []);
3037
+ const after = renderJson(merged.settings, before === void 0 ? DEFAULT_STYLE : styleOf(before));
3038
+ return accept({
3039
+ path: SETTINGS_FILE,
3040
+ kind: "settings",
3041
+ action: actionFor(before, after, "merge"),
3042
+ before,
3043
+ after,
3044
+ added: joinAdded(previous?.added, merged.added),
3045
+ handlers: Object.values(hooks)
3046
+ });
3047
+ };
3048
+ var gitignoreItem = (project) => {
3049
+ const before = project.read(GITIGNORE_FILE);
3050
+ const after = withBlock(before);
3051
+ return { path: GITIGNORE_FILE, kind: "gitignore", action: actionFor(before, after, "merge"), before, after };
3052
+ };
3053
+ var generatedItem = (project, file, previous) => {
3054
+ const before = project.read(file.path);
3055
+ const { template } = file;
3056
+ const base = { path: file.path, kind: "generated", before };
3057
+ if (before === void 0) return { ...base, action: "create", after: template };
3058
+ if (previous === void 0 && !file.isOurs(before)) {
3059
+ return { ...base, action: "skip-foreign", after: before, proposal: template };
3060
+ }
3061
+ if (hashText(before) !== previous?.hash) {
3062
+ return { ...base, action: before === template ? "unchanged" : "keep-edited", after: before, proposal: template };
3063
+ }
3064
+ return { ...base, action: actionFor(before, template, "update"), after: template };
3065
+ };
3066
+ var generatedFiles = (input, vendor) => {
3067
+ const config = { path: CONFIG_FILE, template: configTemplate(input.t, input.preset, input.lang), isOurs: hasMarker };
3068
+ if (!vendor) return accept([config]);
3069
+ if (input.bundle === void 0) return reject({ kind: "bundle", detail: "the running finitemonkey is not a bundle" });
3070
+ return accept([config, ...vendoredFiles(input.bundle, input.version)]);
3071
+ };
3072
+ var entryFor = (lock, path) => lock?.entries.find((entry) => entry.path === path);
3073
+ var buildPlan = (project, detection, input, previous) => {
3074
+ const settings = settingsItem(project, detection, input, entryFor(previous, SETTINGS_FILE));
3075
+ if (!settings.ok) return settings;
3076
+ const vendor = detection.runner.kind === "vendored" || entryFor(previous, VENDOR_BIN) !== void 0;
3077
+ const files = generatedFiles(input, vendor);
3078
+ if (!files.ok) return files;
3079
+ const generated = files.value.map((file) => generatedItem(project, file, entryFor(previous, file.path)));
3080
+ return accept({
3081
+ detection,
3082
+ preset: input.preset,
3083
+ items: [settings.value, gitignoreItem(project), ...generated],
3084
+ previous
3085
+ });
3086
+ };
3087
+ var hasChanges = (plan) => plan.items.some((item) => CHANGING.includes(item.action));
3088
+ var needsWrite = (plan) => hasChanges(plan) || plan.previous === void 0;
3089
+
3090
+ // src/install/apply.ts
3091
+ var stamp = (iso) => iso.replace(/[:.]/g, "-");
3092
+ var ensureParent = (project, relative4) => project.runtime.mkdir(project.abs(dirname2(relative4)));
3093
+ var writeText = (project, relative4, text2) => {
3094
+ const parent = ensureParent(project, relative4);
3095
+ const written = parent.ok ? project.runtime.writeAtomic(project.abs(relative4), text2) : parent;
3096
+ return written.ok ? accept(void 0) : reject({ path: relative4, detail: written.error });
3097
+ };
3098
+ var restore = (project, items) => {
3099
+ for (const item of items) {
3100
+ if (item.before === void 0) project.runtime.remove(project.abs(item.path));
3101
+ else project.runtime.writeAtomic(project.abs(item.path), item.before);
3102
+ }
3103
+ };
3104
+ var backupItems = (project, dir, items) => {
3105
+ const manifest = items.map((item) => ({ path: item.path, hash: hashText(item.before ?? "") }));
3106
+ for (const item of items) {
3107
+ const saved = writeText(project, `${dir}/${item.path}`, item.before ?? "");
3108
+ if (!saved.ok) return saved;
3109
+ }
3110
+ return writeText(project, `${dir}/manifest.json`, `${JSON.stringify(manifest, null, 2)}
3111
+ `);
3112
+ };
3113
+ var entryOf = (plan, item, backupDir) => {
3114
+ const previous = plan.previous?.entries.find((entry) => entry.path === item.path);
3115
+ const existed = previous?.existed ?? item.before !== void 0;
3116
+ const backup = previous?.backup ?? (existed && backupDir !== void 0 ? `${backupDir}/${item.path}` : void 0);
3117
+ const hash = item.action === "keep-edited" ? previous?.hash ?? hashText(item.proposal ?? item.after) : hashText(item.after);
3118
+ return {
3119
+ path: item.path,
3120
+ kind: item.kind,
3121
+ existed,
3122
+ hash,
3123
+ ...backup === void 0 ? {} : { backup },
3124
+ ...item.added === void 0 ? {} : { added: item.added },
3125
+ ...item.handlers === void 0 ? {} : { handlers: item.handlers }
3126
+ };
3127
+ };
3128
+ var nextLock = (plan, version, now, backupDir, createdDirs) => ({
3129
+ schema: LOCK_SCHEMA,
3130
+ version,
3131
+ preset: plan.preset,
3132
+ installedAt: plan.previous?.installedAt ?? now,
3133
+ updatedAt: now,
3134
+ createdDirs: plan.previous?.createdDirs ?? createdDirs,
3135
+ entries: plan.items.filter((item) => item.action !== "skip-foreign").map((item) => entryOf(plan, item, backupDir))
2382
3136
  });
2383
- var updateCommand = planned("update", "command.update.summary", { check: { type: "boolean" } });
2384
- var uninstallCommand = planned("uninstall", "command.uninstall.summary", {
2385
- purge: { type: "boolean" },
2386
- "keep-generated": { type: "boolean" }
3137
+ var needsBackup = (plan, item) => item.before !== void 0 && plan.previous?.entries.find((entry) => entry.path === item.path)?.backup === void 0;
3138
+ var writeAll = (project, items) => {
3139
+ const done = [];
3140
+ for (const item of items) {
3141
+ const written = writeText(project, item.path, item.after);
3142
+ if (!written.ok) {
3143
+ restore(project, done);
3144
+ return written;
3145
+ }
3146
+ done.push(item);
3147
+ }
3148
+ return accept(void 0);
3149
+ };
3150
+ var createdBy = (project) => [".claude", STATE_DIR].filter((dir) => !project.exists(dir));
3151
+ var commit = (project, plan, version, createdDirs) => {
3152
+ const now = project.runtime.now();
3153
+ const changing = plan.items.filter((item) => CHANGING.includes(item.action));
3154
+ const toBackup = changing.filter((item) => needsBackup(plan, item));
3155
+ const backupDir = toBackup.length === 0 ? void 0 : `${BACKUPS_DIR}/${stamp(now)}`;
3156
+ if (backupDir !== void 0) {
3157
+ const saved = backupItems(project, backupDir, toBackup);
3158
+ if (!saved.ok) return saved;
3159
+ }
3160
+ const written = writeAll(project, changing);
3161
+ if (!written.ok) return written;
3162
+ const lock = nextLock(plan, version, now, backupDir, createdDirs);
3163
+ const locked = writeText(project, LOCK_FILE, renderLock(lock));
3164
+ if (!locked.ok) {
3165
+ restore(project, changing);
3166
+ return locked;
3167
+ }
3168
+ return accept({ lock, written: changing.map((item) => item.path), backup: backupDir });
3169
+ };
3170
+ var withInstallLock = (project, work) => {
3171
+ const dir = project.runtime.mkdir(project.abs(LOCKS_DIR));
3172
+ if (!dir.ok) return reject({ path: LOCKS_DIR, detail: dir.error });
3173
+ const lock = project.runtime.lock(project.abs(INSTALL_LOCK), {
3174
+ staleMs: INSTALL_LOCK_STALE_MS,
3175
+ owner: "finitemonkey"
3176
+ });
3177
+ if (!lock.ok) return reject({ path: INSTALL_LOCK, detail: lock.error });
3178
+ try {
3179
+ return work();
3180
+ } finally {
3181
+ lock.value();
3182
+ }
3183
+ };
3184
+ var applyPlan = (project, plan, version) => {
3185
+ const created2 = createdBy(project);
3186
+ return withInstallLock(project, () => commit(project, plan, version, created2));
3187
+ };
3188
+
3189
+ // src/install/diff.ts
3190
+ var MAX_DIFF_LINES = 120;
3191
+ var table = (a, b) => {
3192
+ const rows = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
3193
+ for (let i = a.length - 1; i >= 0; i -= 1) {
3194
+ const row = rows[i] ?? [];
3195
+ const below = rows[i + 1] ?? [];
3196
+ for (let j = b.length - 1; j >= 0; j -= 1) {
3197
+ row[j] = a[i] === b[j] ? (below[j + 1] ?? 0) + 1 : Math.max(below[j] ?? 0, row[j + 1] ?? 0);
3198
+ }
3199
+ }
3200
+ return rows;
3201
+ };
3202
+ var walk = (a, b) => {
3203
+ const lcs = table(a, b);
3204
+ const ops = [];
3205
+ let i = 0;
3206
+ let j = 0;
3207
+ while (i < a.length || j < b.length) {
3208
+ const left = a[i];
3209
+ const right = b[j];
3210
+ if (left !== void 0 && left === right) {
3211
+ ops.push(["=", left]);
3212
+ i += 1;
3213
+ j += 1;
3214
+ } else if (right !== void 0 && (left === void 0 || (lcs[i]?.[j + 1] ?? 0) >= (lcs[i + 1]?.[j] ?? 0))) {
3215
+ ops.push(["+", right]);
3216
+ j += 1;
3217
+ } else {
3218
+ ops.push(["-", left ?? ""]);
3219
+ i += 1;
3220
+ }
3221
+ }
3222
+ return ops;
3223
+ };
3224
+ var splitLines = (text2) => text2 === "" ? [] : text2.replace(/\r?\n$/, "").split(/\r?\n/);
3225
+ var lineDiff = (path, before, after) => {
3226
+ const changes = walk(splitLines(before), splitLines(after)).filter(([op]) => op !== "=");
3227
+ if (changes.length === 0) return [];
3228
+ const lines = changes.map(([op, line]) => `${op} ${line}`);
3229
+ const shown = lines.slice(0, MAX_DIFF_LINES);
3230
+ const hidden = lines.length - shown.length;
3231
+ return [`--- ${path}`, `+++ ${path}`, ...shown, ...hidden > 0 ? [`… ${String(hidden)} more`] : []];
3232
+ };
3233
+
3234
+ // src/install/report.ts
3235
+ var jsonDetection = (detection) => ({
3236
+ gitRoot: detection.gitRoot,
3237
+ packageManager: detection.packageManager ?? null,
3238
+ languages: detection.languages,
3239
+ settings: detection.settings,
3240
+ authorities: detection.authorities,
3241
+ runner: detection.runner.kind,
3242
+ claudeVersion: detection.claude.version ?? null,
3243
+ failClosedHooks: detection.claude.failClosed
2387
3244
  });
3245
+ var authorityWarning = (t, authority) => ({
3246
+ code: `authority_${authority.kind}`,
3247
+ message: t(`install.warn.${authority.kind === "finitemonkey-plugin" ? "plugin" : "authority"}`, {
3248
+ name: authority.name,
3249
+ source: authority.source
3250
+ })
3251
+ });
3252
+ var itemWarning = (t, item) => {
3253
+ if (item.action === "skip-foreign")
3254
+ return [{ code: "foreign_file", message: t("install.warn.foreign", { path: item.path }) }];
3255
+ if (item.action === "keep-edited")
3256
+ return [{ code: "edited_file", message: t("install.warn.edited", { path: item.path }) }];
3257
+ return [];
3258
+ };
3259
+ var planWarnings = (t, plan) => [
3260
+ ...plan.detection.gitRoot ? [] : [{ code: "not_git", message: t("install.warn.not_git") }],
3261
+ ...plan.detection.claude.failClosed ? [] : [{ code: "fail_open", message: t("install.warn.fail_open", { version: plan.detection.claude.version ?? "?" }) }],
3262
+ ...plan.detection.authorities.map((authority) => authorityWarning(t, authority)),
3263
+ ...plan.items.flatMap((item) => itemWarning(t, item))
3264
+ ];
3265
+ var planDiff = (plan) => plan.items.flatMap(
3266
+ (item) => item.action === "keep-edited" || item.action === "skip-foreign" ? lineDiff(item.path, item.before ?? "", item.proposal ?? item.after) : lineDiff(item.path, item.before ?? "", item.after)
3267
+ );
3268
+ var planData = (t, plan, mode, applied, backup) => ({
3269
+ schema: PLAN_SCHEMA,
3270
+ mode,
3271
+ applied,
3272
+ root: plan.detection.root,
3273
+ preset: plan.preset,
3274
+ detection: jsonDetection(plan.detection),
3275
+ actions: plan.items.map(({ path, action }) => ({ path, action })),
3276
+ warnings: planWarnings(t, plan),
3277
+ diff: planDiff(plan),
3278
+ ...backup === void 0 ? {} : { backup }
3279
+ });
3280
+ var pad2 = (text2, width) => text2.padEnd(width);
3281
+ var planLines = (t, data, withDiff) => [
3282
+ t("install.plan.title", { root: data.root, preset: data.preset }),
3283
+ ...data.actions.map(({ path, action }) => ` ${pad2(action, 13)}${path}`),
3284
+ ...data.warnings.map(({ message }) => t("install.warning", { message })),
3285
+ ...withDiff ? data.diff : []
3286
+ ];
3287
+
3288
+ // src/install/project.ts
3289
+ import { dirname as dirname3, join as join8 } from "node:path";
3290
+ var projectAt = (root, runtime) => {
3291
+ const abs = (relative4) => toAbsolutePath(join8(root, ...relative4.split("/")));
3292
+ return {
3293
+ root,
3294
+ runtime,
3295
+ abs,
3296
+ read: (relative4) => {
3297
+ const text2 = runtime.readText(abs(relative4));
3298
+ return text2.ok ? text2.value : void 0;
3299
+ },
3300
+ exists: (relative4) => runtime.stat(abs(relative4)) !== void 0,
3301
+ isDir: (relative4) => runtime.stat(abs(relative4))?.kind === "dir"
3302
+ };
3303
+ };
3304
+ var findGitRoot = (start, runtime) => {
3305
+ let current = start;
3306
+ for (; ; ) {
3307
+ if (runtime.stat(toAbsolutePath(join8(current, ".git"))) !== void 0) return toAbsolutePath(current);
3308
+ const parent = dirname3(current);
3309
+ if (parent === current) return void 0;
3310
+ current = parent;
3311
+ }
3312
+ };
3313
+
3314
+ // src/install/session.ts
3315
+ var openProject = (ctx) => {
3316
+ const root = findGitRoot(ctx.cwd, ctx.runtime);
3317
+ return { project: projectAt(root ?? ctx.cwd, ctx.runtime), gitRoot: root !== void 0 };
3318
+ };
3319
+ var problem = (code, messageKey, fix, exitCode, params) => ({
3320
+ code,
3321
+ messageKey,
3322
+ params,
3323
+ fix,
3324
+ exitCode
3325
+ });
3326
+ var planProblem = (error) => {
3327
+ if (error.kind === "bundle") {
3328
+ return problem("install_failed", "error.install_failed", "npx finitemonkey@latest init", ExitCode.NotConfigured, {
3329
+ path: ".finitemonkey/runtime/dist/finitemonkey.mjs",
3330
+ detail: error.detail
3331
+ });
3332
+ }
3333
+ return error.kind === "settings" ? problem(
3334
+ "settings_unreadable",
3335
+ "error.settings_unreadable",
3336
+ "fix the JSON in .claude/settings.json, then run finitemonkey init again",
3337
+ ExitCode.NotConfigured,
3338
+ { detail: error.detail }
3339
+ ) : problem(
3340
+ "lock_unreadable",
3341
+ "error.lock_unreadable",
3342
+ "finitemonkey uninstall --purge --yes",
3343
+ ExitCode.NotConfigured,
3344
+ { detail: error.detail }
3345
+ );
3346
+ };
3347
+ var applyProblem = (failure2) => failure2.detail.startsWith("locked:") ? problem(
3348
+ "install_locked",
3349
+ "error.install_locked",
3350
+ "wait for the other finitemonkey run to finish, then try again",
3351
+ ExitCode.CheckFailed,
3352
+ { path: failure2.path }
3353
+ ) : problem("install_failed", "error.install_failed", "finitemonkey doctor", ExitCode.NotConfigured, {
3354
+ path: failure2.path,
3355
+ detail: failure2.detail
3356
+ });
3357
+ var notInstalled = () => problem("not_installed", "error.not_installed", "finitemonkey init", ExitCode.NotConfigured, {});
3358
+ var confirmProblem = (command, question, fix) => problem("needs_confirmation", "error.needs_confirmation", fix, ExitCode.NeedsPerson, { command, question });
3359
+ var invalidPreset = (raw) => problem("invalid_arguments", "error.invalid_preset", "finitemonkey init --preset neutral", ExitCode.Usage, {
3360
+ preset: raw
3361
+ });
3362
+ var selfBundle = (ctx) => {
3363
+ if (!isBundlePath(ctx.runtime.selfPath)) return void 0;
3364
+ const text2 = ctx.runtime.readText(ctx.runtime.selfPath);
3365
+ return text2.ok ? text2.value : void 0;
3366
+ };
3367
+ var planInputFor = (ctx, preset, bin, marketplace) => ({
3368
+ preset,
3369
+ lang: ctx.lang,
3370
+ bin,
3371
+ version: ctx.version,
3372
+ marketplace,
3373
+ bundle: selfBundle(ctx),
3374
+ env: ctx.env,
3375
+ t: ctx.t
3376
+ });
3377
+
3378
+ // src/commands/init.ts
3379
+ var flagsFor = (request, preset) => [
3380
+ `finitemonkey ${request.mode} --yes`,
3381
+ ...request.mode === "init" && preset !== "neutral" ? [`--preset ${preset}`] : [],
3382
+ ...request.bin === void 0 ? [] : [`--bin "${request.bin}"`],
3383
+ ...request.marketplace ? ["--marketplace"] : []
3384
+ ].join(" ");
3385
+ var presentPlan = (ctx, request, plan) => {
3386
+ const data = planData(ctx.t, plan, request.mode, false, void 0);
3387
+ const human = planLines(ctx.t, data, request.showDiff || ctx.dryRun);
3388
+ if (ctx.dryRun) return success(data, [...human, ctx.t("install.dry_run")]);
3389
+ if (!needsWrite(plan)) return success(data, [...human, ctx.t("install.nothing_to_do")]);
3390
+ const next = flagsFor(request, plan.preset);
3391
+ const problem2 = confirmProblem(request.mode, ctx.t("install.confirm_question"), next);
3392
+ return { ...failureFrom(ctx.t, { ...problem2, data }, human), next };
3393
+ };
3394
+ var applyAndReport = (ctx, request, plan) => {
3395
+ const project = openProject(ctx).project;
3396
+ const applied = applyPlan(project, plan, ctx.version);
3397
+ if (!applied.ok) return failureFrom(ctx.t, applyProblem(applied.error));
3398
+ const data = planData(ctx.t, plan, request.mode, true, applied.value.backup);
3399
+ const done = ctx.t(request.mode === "init" ? "install.done_init" : "install.done_update");
3400
+ return success(data, [...planLines(ctx.t, data, request.showDiff), done]);
3401
+ };
3402
+ var runInstall = (ctx, request, presetFallback) => {
3403
+ const preset = request.preset === void 0 ? presetFallback : parsePreset(request.preset);
3404
+ if (preset === void 0) return failureFrom(ctx.t, invalidPreset(request.preset ?? ""));
3405
+ const { project, gitRoot } = openProject(ctx);
3406
+ const previous = readPreviousLock(project);
3407
+ if (!previous.ok) return failureFrom(ctx.t, planProblem(previous.error));
3408
+ const input = planInputFor(ctx, preset, request.bin, request.marketplace);
3409
+ const plan = buildPlan(project, detect(project, gitRoot, input), input, previous.value);
3410
+ if (!plan.ok) return failureFrom(ctx.t, planProblem(plan.error));
3411
+ if (ctx.dryRun || !ctx.yes || !needsWrite(plan.value)) return presentPlan(ctx, request, plan.value);
3412
+ return applyAndReport(ctx, request, plan.value);
3413
+ };
3414
+ var initRequest = (args) => ({
3415
+ mode: "init",
3416
+ preset: stringOption(args.values, "preset"),
3417
+ bin: stringOption(args.values, "bin"),
3418
+ marketplace: args.values["marketplace"] === true,
3419
+ showDiff: false
3420
+ });
3421
+ var initCommand = {
3422
+ path: ["init"],
3423
+ summaryKey: "command.init.summary",
3424
+ usage: "finitemonkey init [--preset neutral|gabriel] [--bin <path>] [--marketplace] [--dry-run] [--yes]",
3425
+ status: "ready",
3426
+ options: {
3427
+ preset: { type: "string" },
3428
+ bin: { type: "string" },
3429
+ marketplace: { type: "boolean" }
3430
+ },
3431
+ run: (ctx, args) => Promise.resolve(runInstall(ctx, initRequest(args), "neutral"))
3432
+ };
2388
3433
 
2389
3434
  // src/secrets/project.ts
2390
3435
  var projectRoot = (ctx) => {
@@ -2430,7 +3475,7 @@ var daysOf = (values) => {
2430
3475
  return Number.isInteger(days) && days > 0 && days <= EXCEPTION_MAX_DAYS ? days : void 0;
2431
3476
  };
2432
3477
  var plusDays = (iso, days) => new Date(Date.parse(iso) + days * DAY_MS).toISOString();
2433
- var failure = (ctx, problem) => failureFrom(ctx.t, problem);
3478
+ var failure = (ctx, problem2) => failureFrom(ctx.t, problem2);
2434
3479
  var inputProblem = (ctx, id, reason, days) => {
2435
3480
  const fix = "finitemonkey secrets accept <id> --reason <why> [--days 1-90] [--false-positive]";
2436
3481
  if (!isPerson(ctx)) {
@@ -2447,7 +3492,7 @@ var inputProblem = (ctx, id, reason, days) => {
2447
3492
  }
2448
3493
  return void 0;
2449
3494
  };
2450
- var entryOf = (ctx, finding, args) => {
3495
+ var entryOf2 = (ctx, finding, args) => {
2451
3496
  const now = ctx.runtime.now();
2452
3497
  return {
2453
3498
  id: finding.id,
@@ -2478,7 +3523,7 @@ var acceptEntry = (ctx, args) => {
2478
3523
  })
2479
3524
  );
2480
3525
  }
2481
- const entry = entryOf(ctx, finding, args);
3526
+ const entry = entryOf2(ctx, finding, args);
2482
3527
  const entries = [...baseline.value.entries.filter((item) => item.id !== entry.id), entry];
2483
3528
  const saved = saveBaseline(ctx.runtime, root, { schema: baseline.value.schema, entries });
2484
3529
  if (!saved.ok) {
@@ -2505,12 +3550,12 @@ var secretsAcceptCommand = {
2505
3550
  const id = args.positionals[0] ?? "";
2506
3551
  const reason = stringOption(args.values, "reason");
2507
3552
  const days = daysOf(args.values);
2508
- const problem = inputProblem(ctx, id, reason, days);
2509
- if (problem !== void 0 || reason === void 0 || days === void 0) {
3553
+ const problem2 = inputProblem(ctx, id, reason, days);
3554
+ if (problem2 !== void 0 || reason === void 0 || days === void 0) {
2510
3555
  return Promise.resolve(
2511
3556
  failure(
2512
3557
  ctx,
2513
- problem ?? usageProblem("invalid_arguments", "secrets.error.reason_required", "finitemonkey help secrets accept")
3558
+ problem2 ?? usageProblem("invalid_arguments", "secrets.error.reason_required", "finitemonkey help secrets accept")
2514
3559
  )
2515
3560
  );
2516
3561
  }
@@ -2531,7 +3576,7 @@ var PURGE_STEPS = [
2531
3576
  "secrets.purge.step4",
2532
3577
  "secrets.purge.step5"
2533
3578
  ];
2534
- var fail = (ctx, problem) => failureFrom(ctx.t, problem);
3579
+ var fail = (ctx, problem2) => failureFrom(ctx.t, problem2);
2535
3580
  var notFound2 = (ctx, id) => fail(
2536
3581
  ctx,
2537
3582
  usageProblem("secrets_incident_not_found", "secrets.error.incident_not_found", `finitemonkey secrets incident`, {
@@ -2650,7 +3695,7 @@ var view = (finding, status) => ({
2650
3695
  status,
2651
3696
  ...finding.commit === void 0 ? {} : { commit: finding.commit }
2652
3697
  });
2653
- var modeOf = (values) => {
3698
+ var modeOf2 = (values) => {
2654
3699
  const chosen = [values["staged"] === true, values["history"] === true, stringOption(values, "path") !== void 0];
2655
3700
  if (chosen.filter(Boolean).length > 1) return void 0;
2656
3701
  if (chosen[0] === true) return "staged";
@@ -2741,7 +3786,7 @@ var secretsScanCommand = {
2741
3786
  status: "ready",
2742
3787
  options: { staged: { type: "boolean" }, history: { type: "boolean" }, path: { type: "string" } },
2743
3788
  run: async (ctx, args) => {
2744
- const mode = modeOf(args.values);
3789
+ const mode = modeOf2(args.values);
2745
3790
  const fix = "finitemonkey help secrets scan";
2746
3791
  if (mode === void 0) return failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.error.one_mode", fix));
2747
3792
  const root = projectRoot(ctx);
@@ -2762,6 +3807,163 @@ var secretsScanCommand = {
2762
3807
  }
2763
3808
  };
2764
3809
 
3810
+ // src/install/uninstall.ts
3811
+ var structural = (entry, current) => {
3812
+ if (entry.kind === "gitignore") {
3813
+ const text2 = withoutBlock(current);
3814
+ return text2 === "" && !entry.existed ? void 0 : text2;
3815
+ }
3816
+ const parsed = parseSettings(current);
3817
+ if (!parsed.ok || entry.added === void 0) return current;
3818
+ const settings = unmergeSettings(parsed.value, entry.added, entry.handlers ?? []);
3819
+ return Object.keys(settings).length === 0 && !entry.existed ? void 0 : renderJson(settings, styleOf(current));
3820
+ };
3821
+ var unchanged = (project, entry) => {
3822
+ if (!entry.existed) return { path: entry.path, action: "delete", after: void 0 };
3823
+ const original = entry.backup === void 0 ? void 0 : project.read(entry.backup);
3824
+ return original === void 0 ? void 0 : { path: entry.path, action: "restore", after: original };
3825
+ };
3826
+ var removalFor = (project, entry, options) => {
3827
+ const current = project.read(entry.path);
3828
+ if (current === void 0) return { path: entry.path, action: "gone", after: void 0 };
3829
+ if (entry.kind === "generated" && options.keepGenerated) {
3830
+ return { path: entry.path, action: "keep-generated", after: current };
3831
+ }
3832
+ if (hashText(current) === entry.hash) {
3833
+ const exact2 = unchanged(project, entry);
3834
+ if (exact2 !== void 0) return exact2;
3835
+ }
3836
+ if (entry.kind === "generated") return { path: entry.path, action: "keep-edited", after: current };
3837
+ const after = structural(entry, current);
3838
+ return { path: entry.path, action: after === void 0 ? "delete" : "unmerge", after };
3839
+ };
3840
+ var planRemoval = (project, lock, options) => ({
3841
+ items: lock.entries.map((entry) => removalFor(project, entry, options)),
3842
+ purge: options.purge,
3843
+ removeDirs: lock.createdDirs.filter((dir) => dir !== STATE_DIR)
3844
+ });
3845
+ var applyItem = (project, item) => {
3846
+ if (item.action === "delete") {
3847
+ const removed = project.runtime.remove(project.abs(item.path));
3848
+ return removed.ok ? removed : reject({ path: item.path, detail: removed.error });
3849
+ }
3850
+ if (item.after === void 0 || item.action === "gone" || item.action.startsWith("keep")) return accept(void 0);
3851
+ return writeText(project, item.path, item.after);
3852
+ };
3853
+ var isEmptyDir = (project, relative4) => {
3854
+ const listed2 = project.runtime.listDir(project.abs(relative4));
3855
+ return listed2.ok && listed2.value.length === 0;
3856
+ };
3857
+ var finish = (project, plan) => {
3858
+ const target = plan.purge ? STATE_DIR : LOCK_FILE;
3859
+ const removed = project.runtime.remove(project.abs(target));
3860
+ if (!removed.ok) return reject({ path: target, detail: removed.error });
3861
+ for (const dir of plan.removeDirs) if (isEmptyDir(project, dir)) project.runtime.remove(project.abs(dir));
3862
+ return accept(void 0);
3863
+ };
3864
+ var applyRemoval = (project, plan) => {
3865
+ const changed = withInstallLock(project, () => {
3866
+ for (const item of plan.items) {
3867
+ const done = applyItem(project, item);
3868
+ if (!done.ok) return done;
3869
+ }
3870
+ return accept(void 0);
3871
+ });
3872
+ return changed.ok ? finish(project, plan) : changed;
3873
+ };
3874
+
3875
+ // src/commands/uninstall.ts
3876
+ var UNINSTALL_SCHEMA = "finitemonkey/uninstall-plan@1";
3877
+ var OUTSIDE = ["claude plugin uninstall finitemonkey@finitemonkey"];
3878
+ var dataOf2 = (root, plan, applied) => ({
3879
+ schema: UNINSTALL_SCHEMA,
3880
+ applied,
3881
+ purge: plan.purge,
3882
+ root,
3883
+ actions: plan.items.map(({ path, action }) => ({ path, action })),
3884
+ kept: plan.items.filter((item) => item.action.startsWith("keep")).map(({ path }) => path),
3885
+ outside: OUTSIDE
3886
+ });
3887
+ var linesOf = (ctx, data) => [
3888
+ ctx.t("install.uninstall.title", { root: data.root }),
3889
+ ...data.actions.map(({ path, action }) => ` ${action.padEnd(15)}${path}`),
3890
+ ...data.purge ? [ctx.t("install.uninstall.purge")] : [ctx.t("install.uninstall.keeps_state")],
3891
+ ...data.kept.map((path) => ctx.t("install.warn.edited_kept", { path })),
3892
+ ctx.t("install.uninstall.outside", { command: OUTSIDE.join(", ") })
3893
+ ];
3894
+ var fixFor = (args) => [
3895
+ "finitemonkey uninstall --yes",
3896
+ ...args.values["purge"] === true ? ["--purge"] : [],
3897
+ ...args.values["keep-generated"] === true ? ["--keep-generated"] : []
3898
+ ].join(" ");
3899
+ var runUninstall = (ctx, args) => {
3900
+ const { project } = openProject(ctx);
3901
+ const lock = readPreviousLock(project);
3902
+ if (!lock.ok) return failureFrom(ctx.t, planProblem(lock.error));
3903
+ if (lock.value === void 0) return failureFrom(ctx.t, notInstalled());
3904
+ const options = { purge: args.values["purge"] === true, keepGenerated: args.values["keep-generated"] === true };
3905
+ const plan = planRemoval(project, lock.value, options);
3906
+ const preview = dataOf2(project.root, plan, false);
3907
+ if (ctx.dryRun) return success(preview, [...linesOf(ctx, preview), ctx.t("install.dry_run")]);
3908
+ if (!ctx.yes) {
3909
+ const next = fixFor(args);
3910
+ const problem2 = confirmProblem("uninstall", ctx.t("install.uninstall.question"), next);
3911
+ return { ...failureFrom(ctx.t, { ...problem2, data: preview }, linesOf(ctx, preview)), next };
3912
+ }
3913
+ const removed = applyRemoval(project, plan);
3914
+ if (!removed.ok) return failureFrom(ctx.t, applyProblem(removed.error));
3915
+ const data = dataOf2(project.root, plan, true);
3916
+ return success(data, [...linesOf(ctx, data), ctx.t("install.uninstall.done")]);
3917
+ };
3918
+ var uninstallCommand = {
3919
+ path: ["uninstall"],
3920
+ summaryKey: "command.uninstall.summary",
3921
+ usage: "finitemonkey uninstall [--purge] [--keep-generated] [--dry-run] [--yes]",
3922
+ status: "ready",
3923
+ options: { purge: { type: "boolean" }, "keep-generated": { type: "boolean" } },
3924
+ run: (ctx, args) => Promise.resolve(runUninstall(ctx, args))
3925
+ };
3926
+
3927
+ // src/commands/update.ts
3928
+ var check = (ctx, bin) => {
3929
+ const { project, gitRoot } = openProject(ctx);
3930
+ const previous = readPreviousLock(project);
3931
+ if (!previous.ok) return failureFrom(ctx.t, planProblem(previous.error));
3932
+ if (previous.value === void 0) return failureFrom(ctx.t, notInstalled());
3933
+ const input = planInputFor(ctx, previous.value.preset, bin, false);
3934
+ const plan = buildPlan(project, detect(project, gitRoot, input), input, previous.value);
3935
+ if (!plan.ok) return failureFrom(ctx.t, planProblem(plan.error));
3936
+ const data = planData(ctx.t, plan.value, "update", false, void 0);
3937
+ const human = planLines(ctx.t, data, true);
3938
+ if (!hasChanges(plan.value)) return success(data, [...human, ctx.t("install.up_to_date")]);
3939
+ return {
3940
+ ok: false,
3941
+ exitCode: ExitCode.CheckFailed,
3942
+ data,
3943
+ human,
3944
+ error: { code: "install_outdated", message: ctx.t("error.install_outdated"), fix: "finitemonkey update --yes" }
3945
+ };
3946
+ };
3947
+ var runUpdate = (ctx, args) => {
3948
+ const bin = stringOption(args.values, "bin");
3949
+ if (args.values["check"] === true) return check(ctx, bin);
3950
+ const { project } = openProject(ctx);
3951
+ const previous = readPreviousLock(project);
3952
+ if (!previous.ok) return failureFrom(ctx.t, planProblem(previous.error));
3953
+ if (previous.value === void 0) return failureFrom(ctx.t, notInstalled());
3954
+ const preset = stringOption(args.values, "preset");
3955
+ const request = { mode: "update", preset, bin, marketplace: false, showDiff: true };
3956
+ return runInstall(ctx, request, previous.value.preset);
3957
+ };
3958
+ var updateCommand = {
3959
+ path: ["update"],
3960
+ summaryKey: "command.update.summary",
3961
+ usage: "finitemonkey update [--check] [--preset neutral|gabriel] [--bin <path>] [--yes]",
3962
+ status: "ready",
3963
+ options: { check: { type: "boolean" }, preset: { type: "string" }, bin: { type: "string" } },
3964
+ run: (ctx, args) => Promise.resolve(runUpdate(ctx, args))
3965
+ };
3966
+
2765
3967
  // src/commands/version.ts
2766
3968
  var versionCommand = {
2767
3969
  path: ["version"],
@@ -2824,9 +4026,9 @@ var unknownCommand = (registry, attempted) => {
2824
4026
  suggestion
2825
4027
  });
2826
4028
  };
2827
- var failWith = (ctx, name, problem) => ({
4029
+ var failWith = (ctx, name, problem2) => ({
2828
4030
  name,
2829
- execute: () => Promise.resolve(failureFrom(ctx.t, problem))
4031
+ execute: () => Promise.resolve(failureFrom(ctx.t, problem2))
2830
4032
  });
2831
4033
  var invoke = (ctx, argv, spec, registry) => {
2832
4034
  const name = commandName(spec);
@@ -2911,11 +4113,11 @@ var run = async (options) => {
2911
4113
  let result;
2912
4114
  try {
2913
4115
  if (loose.lang !== void 0 && normalizeLang(loose.lang) === void 0) {
2914
- const problem = usageProblem("invalid_lang", "error.invalid_lang", "finitemonkey --lang en", {
4116
+ const problem2 = usageProblem("invalid_lang", "error.invalid_lang", "finitemonkey --lang en", {
2915
4117
  lang: loose.lang,
2916
4118
  languages: LANGUAGES.join(", ")
2917
4119
  });
2918
- result = failureFrom(t, problem);
4120
+ result = failureFrom(t, problem2);
2919
4121
  } else {
2920
4122
  const dispatched = dispatch(ctx, options.argv, loose, options.registry ?? REGISTRY);
2921
4123
  name = dispatched.name;
@@ -2929,15 +4131,23 @@ var run = async (options) => {
2929
4131
  };
2930
4132
 
2931
4133
  // src/core/version.ts
2932
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4134
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
2933
4135
  var UNKNOWN_VERSION = "0.0.0-unknown";
2934
- var readPackageVersion = (readText, moduleUrl) => {
2935
- const text2 = readText(toAbsolutePath(fileURLToPath2(new URL("../package.json", moduleUrl))));
2936
- if (!text2.ok) return UNKNOWN_VERSION;
4136
+ var VERSION_SOURCES = ["../package.json", "../.claude-plugin/plugin.json"];
4137
+ var versionAt = (readText, moduleUrl, relative4) => {
4138
+ const text2 = readText(toAbsolutePath(fileURLToPath3(new URL(relative4, moduleUrl))));
4139
+ if (!text2.ok) return void 0;
2937
4140
  const parsed = parseJson(text2.value);
2938
- if (!parsed.ok || !isRecord(parsed.value)) return UNKNOWN_VERSION;
4141
+ if (!parsed.ok || !isRecord(parsed.value)) return void 0;
2939
4142
  const version = parsed.value["version"];
2940
- return typeof version === "string" ? version : UNKNOWN_VERSION;
4143
+ return typeof version === "string" ? version : void 0;
4144
+ };
4145
+ var readPackageVersion = (readText, moduleUrl) => {
4146
+ for (const relative4 of VERSION_SOURCES) {
4147
+ const version = versionAt(readText, moduleUrl, relative4);
4148
+ if (version !== void 0) return version;
4149
+ }
4150
+ return UNKNOWN_VERSION;
2941
4151
  };
2942
4152
 
2943
4153
  // src/cli.ts