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.
- package/README.es.md +30 -1
- package/README.md +30 -1
- package/README.pt-BR.md +30 -1
- package/dist/finitemonkey.mjs +1292 -82
- package/package.json +6 -3
package/dist/finitemonkey.mjs
CHANGED
|
@@ -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 {
|
|
22
|
-
import {
|
|
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((
|
|
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(
|
|
411
|
-
super(
|
|
519
|
+
constructor(problem2) {
|
|
520
|
+
super(problem2.code);
|
|
412
521
|
this.name = "FmError";
|
|
413
|
-
this.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,
|
|
593
|
+
var failureFrom = (t, problem2, human) => ({
|
|
485
594
|
ok: false,
|
|
486
|
-
exitCode:
|
|
487
|
-
error: { code:
|
|
488
|
-
...
|
|
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
|
|
669
|
+
const problem2 = usageProblem("unknown_command", "error.unknown_command", "finitemonkey help", {
|
|
561
670
|
command: topic.join(" ")
|
|
562
671
|
});
|
|
563
|
-
return failureFrom(ctx.t,
|
|
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
|
|
769
|
+
const entryOf3 = (finding) => byId.get(finding.id);
|
|
661
770
|
return {
|
|
662
|
-
fresh: findings.filter((finding) =>
|
|
663
|
-
known: findings.filter((finding) => (
|
|
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 =
|
|
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,
|
|
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
|
-
...
|
|
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,
|
|
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,
|
|
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
|
|
1460
|
+
let commit2;
|
|
1352
1461
|
let chunk = fresh();
|
|
1353
1462
|
const flush2 = () => {
|
|
1354
|
-
collect(chunk,
|
|
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
|
-
|
|
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
|
|
1541
|
+
var basename2 = (path) => path.split(/[\\/]/).pop() ?? path;
|
|
1433
1542
|
var isSensitivePath = (path) => {
|
|
1434
|
-
const name =
|
|
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
|
|
1623
|
-
return
|
|
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
|
|
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(
|
|
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,
|
|
2323
|
-
const kind = ctx.t(`i18n.problem.${
|
|
2324
|
-
expected: (
|
|
2325
|
-
actual: (
|
|
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 ` ${
|
|
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((
|
|
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/
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
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
|
|
2380
|
-
|
|
2381
|
-
|
|
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
|
|
2384
|
-
var
|
|
2385
|
-
|
|
2386
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
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
|
|
2509
|
-
if (
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
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,
|
|
4029
|
+
var failWith = (ctx, name, problem2) => ({
|
|
2828
4030
|
name,
|
|
2829
|
-
execute: () => Promise.resolve(failureFrom(ctx.t,
|
|
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
|
|
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,
|
|
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
|
|
4134
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2933
4135
|
var UNKNOWN_VERSION = "0.0.0-unknown";
|
|
2934
|
-
var
|
|
2935
|
-
|
|
2936
|
-
|
|
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
|
|
4141
|
+
if (!parsed.ok || !isRecord(parsed.value)) return void 0;
|
|
2939
4142
|
const version = parsed.value["version"];
|
|
2940
|
-
return typeof version === "string" ? 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
|