finitemonkey 0.0.0 → 0.1.0-next.2
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/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.es.md +142 -0
- package/README.md +140 -3
- package/README.pt-BR.md +142 -0
- package/dist/finitemonkey.mjs +2966 -0
- package/package.json +77 -5
|
@@ -0,0 +1,2966 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/core/brand.ts
|
|
4
|
+
import { isAbsolute, resolve } from "node:path";
|
|
5
|
+
var toAbsolutePath = (path, base) => isAbsolute(path) || !base ? resolve(path) : resolve(base, path);
|
|
6
|
+
|
|
7
|
+
// src/core/entry.ts
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
var isEntryPoint = (moduleUrl, argv1, realpath, platform) => {
|
|
10
|
+
if (argv1 === void 0) return false;
|
|
11
|
+
const self = realpath(fileURLToPath(moduleUrl));
|
|
12
|
+
const invoked = realpath(argv1);
|
|
13
|
+
return platform === "win32" ? self.toLowerCase() === invoked.toLowerCase() : self === invoked;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/core/node-runtime.ts
|
|
17
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
18
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
19
|
+
|
|
20
|
+
// 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";
|
|
23
|
+
|
|
24
|
+
// src/core/parsed.ts
|
|
25
|
+
var accept = (value) => ({ ok: true, value });
|
|
26
|
+
var reject = (error) => ({ ok: false, error });
|
|
27
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
var parseJson = (text2) => {
|
|
29
|
+
try {
|
|
30
|
+
return accept(JSON.parse(text2));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return reject(error instanceof Error ? error.message : String(error));
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var parseJsonObject = (text2) => {
|
|
36
|
+
const parsed = parseJson(text2);
|
|
37
|
+
if (!parsed.ok) return parsed;
|
|
38
|
+
return isRecord(parsed.value) ? accept(parsed.value) : reject("expected a JSON object");
|
|
39
|
+
};
|
|
40
|
+
var optionalString = (record2, key) => {
|
|
41
|
+
const value = record2[key];
|
|
42
|
+
return typeof value === "string" ? value : void 0;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/core/node-fs.ts
|
|
46
|
+
var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
47
|
+
var messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
48
|
+
var nodeWriteText = (path, text2) => {
|
|
49
|
+
const temp = `${path}.${String(process.pid)}.tmp`;
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
52
|
+
const fd = openSync(temp, "w", 384);
|
|
53
|
+
try {
|
|
54
|
+
writeSync(fd, text2);
|
|
55
|
+
fsyncSync(fd);
|
|
56
|
+
} finally {
|
|
57
|
+
closeSync(fd);
|
|
58
|
+
}
|
|
59
|
+
renameSync(temp, path);
|
|
60
|
+
return accept(true);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
rmSync(temp, { force: true });
|
|
63
|
+
return reject(messageOf(error));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var nodeListDir = (path) => {
|
|
67
|
+
try {
|
|
68
|
+
return accept(readdirSync(path).sort());
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return reject(messageOf(error));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var walkInto = (root, dir, out) => {
|
|
74
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
75
|
+
const full = join(dir, entry.name);
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
if (!SKIPPED_DIRS.has(entry.name)) walkInto(root, full, out);
|
|
78
|
+
} else if (entry.isFile()) {
|
|
79
|
+
out.push(relative(root, full).split("\\").join("/"));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var nodeWalkFiles = (root) => {
|
|
84
|
+
try {
|
|
85
|
+
const out = [];
|
|
86
|
+
walkInto(root, root, out);
|
|
87
|
+
return accept(out.sort());
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return reject(messageOf(error));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/core/node-git.ts
|
|
94
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
95
|
+
import { createInterface } from "node:readline";
|
|
96
|
+
var MAX_BUFFER = 256 * 1024 * 1024;
|
|
97
|
+
var GIT_ENV = { ...process.env, GIT_PAGER: "cat", GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" };
|
|
98
|
+
var notFound = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
99
|
+
var nodeGit = (args, cwd) => {
|
|
100
|
+
const result = spawnSync("git", [...args], {
|
|
101
|
+
cwd,
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
maxBuffer: MAX_BUFFER,
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
env: GIT_ENV
|
|
106
|
+
});
|
|
107
|
+
const stdout = result.stdout;
|
|
108
|
+
return {
|
|
109
|
+
found: !notFound(result.error),
|
|
110
|
+
ok: result.error === void 0 && result.status === 0,
|
|
111
|
+
stdout: typeof stdout === "string" ? stdout : ""
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
var nodeGitLines = (args, cwd, onLine) => new Promise((resolve2) => {
|
|
115
|
+
const child = spawn("git", [...args], {
|
|
116
|
+
cwd,
|
|
117
|
+
windowsHide: true,
|
|
118
|
+
env: GIT_ENV,
|
|
119
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
120
|
+
});
|
|
121
|
+
let found = true;
|
|
122
|
+
child.on("error", (error) => {
|
|
123
|
+
found = !notFound(error);
|
|
124
|
+
});
|
|
125
|
+
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
126
|
+
lines.on("line", onLine);
|
|
127
|
+
child.on("close", (code) => {
|
|
128
|
+
resolve2({ found, ok: found && code === 0, stdout: "" });
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// src/core/node-runtime.ts
|
|
133
|
+
var SAFE_TOKEN = /^[A-Za-z0-9._-]+$/;
|
|
134
|
+
var WINDOWS_NOT_FOUND = 9009;
|
|
135
|
+
var errnoCode = (error) => {
|
|
136
|
+
if (!(error instanceof Error) || !("code" in error)) return void 0;
|
|
137
|
+
return typeof error.code === "string" ? error.code : void 0;
|
|
138
|
+
};
|
|
139
|
+
var spawnProbe = (command, args, timeout) => {
|
|
140
|
+
const options = { encoding: "utf8", timeout, windowsHide: true };
|
|
141
|
+
return process.platform === "win32" ? spawnSync2([command, ...args].join(" "), { ...options, shell: true }) : spawnSync2(command, [...args], options);
|
|
142
|
+
};
|
|
143
|
+
var nodeProbe = (command, args, timeoutMs) => {
|
|
144
|
+
if (![command, ...args].every((token) => SAFE_TOKEN.test(token))) {
|
|
145
|
+
return { found: false, ok: false, stdout: "", timedOut: false };
|
|
146
|
+
}
|
|
147
|
+
const result = spawnProbe(command, args, timeoutMs);
|
|
148
|
+
const code = errnoCode(result.error);
|
|
149
|
+
const stdout = result.stdout;
|
|
150
|
+
return {
|
|
151
|
+
found: code !== "ENOENT" && result.status !== WINDOWS_NOT_FOUND,
|
|
152
|
+
ok: result.error === void 0 && result.status === 0,
|
|
153
|
+
stdout: typeof stdout === "string" ? stdout.trim() : "",
|
|
154
|
+
timedOut: code === "ETIMEDOUT"
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
var nodeReadText = (path) => {
|
|
158
|
+
try {
|
|
159
|
+
return accept(readFileSync(path, "utf8"));
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return reject(error instanceof Error ? error.message : String(error));
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
var nodeRealpath = (path) => {
|
|
165
|
+
try {
|
|
166
|
+
return realpathSync(path);
|
|
167
|
+
} catch {
|
|
168
|
+
return path;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
var nodeRuntime = () => ({
|
|
172
|
+
nodeVersion: process.versions.node,
|
|
173
|
+
readText: nodeReadText,
|
|
174
|
+
probe: nodeProbe,
|
|
175
|
+
startTimer: (ms, onFire) => {
|
|
176
|
+
const timer = setTimeout(onFire, ms);
|
|
177
|
+
return () => {
|
|
178
|
+
clearTimeout(timer);
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
writeText: nodeWriteText,
|
|
182
|
+
listDir: nodeListDir,
|
|
183
|
+
walkFiles: nodeWalkFiles,
|
|
184
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
185
|
+
git: nodeGit,
|
|
186
|
+
gitLines: nodeGitLines
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// src/doctor/system.ts
|
|
190
|
+
var detectOs = ({ platform, env, runtime }) => {
|
|
191
|
+
if (platform === "darwin") return "macos";
|
|
192
|
+
if (platform === "win32") return "windows";
|
|
193
|
+
if (platform !== "linux") return "other";
|
|
194
|
+
if (env["WSL_DISTRO_NAME"] !== void 0 || env["WSL_INTEROP"] !== void 0) return "wsl";
|
|
195
|
+
const version = runtime.readText(toAbsolutePath("/proc/version"));
|
|
196
|
+
return version.ok && /microsoft/i.test(version.value) ? "wsl" : "linux";
|
|
197
|
+
};
|
|
198
|
+
var WSL_MOUNT = /^\/mnt\/[a-z](\/|$)/i;
|
|
199
|
+
var SYNCED = /(OneDrive|Dropbox|iCloud ?Drive|Mobile Documents|Google ?Drive)/i;
|
|
200
|
+
var NON_ASCII = /[^\x20-\x7e]/;
|
|
201
|
+
var pathTraits = (path) => ({
|
|
202
|
+
spaces: path.includes(" "),
|
|
203
|
+
nonAscii: NON_ASCII.test(path),
|
|
204
|
+
synced: SYNCED.test(path)
|
|
205
|
+
});
|
|
206
|
+
var checkOs = (env) => {
|
|
207
|
+
const os = detectOs(env);
|
|
208
|
+
const findings = [
|
|
209
|
+
{ check: "os", status: "ok", messageKey: `doctor.os.${os}`, params: { platform: env.platform } }
|
|
210
|
+
];
|
|
211
|
+
if (os === "wsl" && WSL_MOUNT.test(env.cwd)) {
|
|
212
|
+
findings.push({
|
|
213
|
+
check: "path.wsl_mount",
|
|
214
|
+
status: "warn",
|
|
215
|
+
messageKey: "doctor.path.wsl_mount",
|
|
216
|
+
params: { path: env.cwd },
|
|
217
|
+
fixKey: "doctor.path.fix_wsl_mount"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return findings;
|
|
221
|
+
};
|
|
222
|
+
var checkPath = ({ cwd }) => {
|
|
223
|
+
const traits = pathTraits(cwd);
|
|
224
|
+
const chars = traits.spaces || traits.nonAscii ? {
|
|
225
|
+
check: "path.chars",
|
|
226
|
+
status: "warn",
|
|
227
|
+
messageKey: "doctor.path.chars",
|
|
228
|
+
params: { path: cwd },
|
|
229
|
+
fixKey: "doctor.path.fix_chars"
|
|
230
|
+
} : { check: "path.chars", status: "ok", messageKey: "doctor.path.plain", params: { path: cwd } };
|
|
231
|
+
const synced = traits.synced ? [
|
|
232
|
+
{
|
|
233
|
+
check: "path.synced",
|
|
234
|
+
status: "warn",
|
|
235
|
+
messageKey: "doctor.path.synced",
|
|
236
|
+
params: { path: cwd },
|
|
237
|
+
fixKey: "doctor.path.fix_synced"
|
|
238
|
+
}
|
|
239
|
+
] : [];
|
|
240
|
+
return [chars, ...synced];
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// src/doctor/tools.ts
|
|
244
|
+
var MIN_NODE_MAJOR = 20;
|
|
245
|
+
var MIN_GIT = { major: 2, minor: 38 };
|
|
246
|
+
var PROBE_TIMEOUT_MS = { git: 5e3, claude: 8e3 };
|
|
247
|
+
var parseGitVersion = (output) => {
|
|
248
|
+
const match = /(\d+)\.(\d+)/.exec(output);
|
|
249
|
+
if (match?.[1] === void 0 || match[2] === void 0) return void 0;
|
|
250
|
+
return { major: Number(match[1]), minor: Number(match[2]) };
|
|
251
|
+
};
|
|
252
|
+
var isOlder = (version, min) => version.major < min.major || version.major === min.major && version.minor < min.minor;
|
|
253
|
+
var checkNode = ({ runtime }) => {
|
|
254
|
+
const version = runtime.nodeVersion.replace(/^v/, "");
|
|
255
|
+
const major = Number.parseInt(version.split(".")[0] ?? "", 10);
|
|
256
|
+
if (Number.isNaN(major) || major < MIN_NODE_MAJOR) {
|
|
257
|
+
return [
|
|
258
|
+
{
|
|
259
|
+
check: "node",
|
|
260
|
+
status: "fail",
|
|
261
|
+
messageKey: "doctor.node.old",
|
|
262
|
+
params: { version, min: MIN_NODE_MAJOR },
|
|
263
|
+
fixKey: "doctor.node.fix"
|
|
264
|
+
}
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
return [{ check: "node", status: "ok", messageKey: "doctor.node.ok", params: { version } }];
|
|
268
|
+
};
|
|
269
|
+
var checkGit = ({ runtime }) => {
|
|
270
|
+
const result = runtime.probe("git", ["--version"], PROBE_TIMEOUT_MS.git);
|
|
271
|
+
if (!result.found || !result.ok) {
|
|
272
|
+
return [{ check: "git", status: "fail", messageKey: "doctor.git.missing", fixKey: "doctor.git.fix" }];
|
|
273
|
+
}
|
|
274
|
+
const parsed = parseGitVersion(result.stdout);
|
|
275
|
+
const version = parsed === void 0 ? result.stdout : `${String(parsed.major)}.${String(parsed.minor)}`;
|
|
276
|
+
if (parsed === void 0 || isOlder(parsed, MIN_GIT)) {
|
|
277
|
+
const min = `${String(MIN_GIT.major)}.${String(MIN_GIT.minor)}`;
|
|
278
|
+
return [
|
|
279
|
+
{
|
|
280
|
+
check: "git",
|
|
281
|
+
status: "warn",
|
|
282
|
+
messageKey: "doctor.git.old",
|
|
283
|
+
params: { version, min },
|
|
284
|
+
fixKey: "doctor.git.fix_old"
|
|
285
|
+
}
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
return [{ check: "git", status: "ok", messageKey: "doctor.git.ok", params: { version } }];
|
|
289
|
+
};
|
|
290
|
+
var checkClaude = ({ runtime }) => {
|
|
291
|
+
const result = runtime.probe("claude", ["--version"], PROBE_TIMEOUT_MS.claude);
|
|
292
|
+
if (result.timedOut) {
|
|
293
|
+
return [
|
|
294
|
+
{ check: "claude", status: "warn", messageKey: "doctor.claude.timeout", fixKey: "doctor.claude.fix_timeout" }
|
|
295
|
+
];
|
|
296
|
+
}
|
|
297
|
+
if (!result.found || !result.ok) {
|
|
298
|
+
return [{ check: "claude", status: "warn", messageKey: "doctor.claude.missing", fixKey: "doctor.claude.fix" }];
|
|
299
|
+
}
|
|
300
|
+
const version = /\d+\.\d+\.\d+/.exec(result.stdout)?.[0] ?? result.stdout;
|
|
301
|
+
return [{ check: "claude", status: "ok", messageKey: "doctor.claude.ok", params: { version } }];
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// src/doctor/run.ts
|
|
305
|
+
var DOCTOR_SCHEMA = "finitemonkey/doctor@1";
|
|
306
|
+
var CHECKS = [checkNode, checkGit, checkClaude, checkOs, checkPath];
|
|
307
|
+
var doctorEnvFrom = (ctx) => ({
|
|
308
|
+
platform: ctx.platform,
|
|
309
|
+
env: ctx.env,
|
|
310
|
+
cwd: ctx.cwd,
|
|
311
|
+
runtime: ctx.runtime
|
|
312
|
+
});
|
|
313
|
+
var runChecks = (env, checks = CHECKS) => checks.flatMap((check) => check(env));
|
|
314
|
+
var toItem = (ctx, finding) => ({
|
|
315
|
+
check: finding.check,
|
|
316
|
+
status: finding.status,
|
|
317
|
+
detail: ctx.t(finding.messageKey, finding.params),
|
|
318
|
+
...finding.fixKey === void 0 ? {} : { fix: ctx.t(finding.fixKey, finding.params) }
|
|
319
|
+
});
|
|
320
|
+
var count = (items, status) => items.filter((item) => item.status === status).length;
|
|
321
|
+
var buildReport = (ctx, findings) => {
|
|
322
|
+
const checks = findings.map((finding) => toItem(ctx, finding));
|
|
323
|
+
return {
|
|
324
|
+
schema: DOCTOR_SCHEMA,
|
|
325
|
+
checks,
|
|
326
|
+
summary: { ok: count(checks, "ok"), warn: count(checks, "warn"), fail: count(checks, "fail") }
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// src/exit-codes.ts
|
|
331
|
+
var ExitCode = {
|
|
332
|
+
Ok: 0,
|
|
333
|
+
CheckFailed: 1,
|
|
334
|
+
Usage: 2,
|
|
335
|
+
NotConfigured: 3,
|
|
336
|
+
NeedsPerson: 4,
|
|
337
|
+
Internal: 5
|
|
338
|
+
};
|
|
339
|
+
var EXIT_CODE_NAMES = [
|
|
340
|
+
"Ok",
|
|
341
|
+
"CheckFailed",
|
|
342
|
+
"Usage",
|
|
343
|
+
"NotConfigured",
|
|
344
|
+
"NeedsPerson",
|
|
345
|
+
"Internal"
|
|
346
|
+
];
|
|
347
|
+
var exitCodeDocs = {
|
|
348
|
+
0: "exit.ok",
|
|
349
|
+
1: "exit.check_failed",
|
|
350
|
+
2: "exit.usage",
|
|
351
|
+
3: "exit.not_configured",
|
|
352
|
+
4: "exit.needs_person",
|
|
353
|
+
5: "exit.internal"
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/commands/doctor.ts
|
|
357
|
+
var MARK = { ok: "ok ", warn: "warn", fail: "FAIL" };
|
|
358
|
+
var itemLines = (ctx, item) => [
|
|
359
|
+
`[${MARK[item.status]}] ${item.check}: ${item.detail}`,
|
|
360
|
+
...item.fix !== void 0 && item.status !== "ok" ? [` ${ctx.t("output.fix", { fix: item.fix })}`] : []
|
|
361
|
+
];
|
|
362
|
+
var doctorResult = (ctx, report) => {
|
|
363
|
+
const human = [
|
|
364
|
+
ctx.t("doctor.title", { version: ctx.version }),
|
|
365
|
+
...report.checks.flatMap((item) => itemLines(ctx, item)),
|
|
366
|
+
ctx.t("doctor.summary", report.summary)
|
|
367
|
+
];
|
|
368
|
+
const failed2 = report.checks.find((item) => item.status === "fail");
|
|
369
|
+
if (failed2 === void 0) return { ok: true, exitCode: ExitCode.Ok, data: report, human };
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
exitCode: ExitCode.CheckFailed,
|
|
373
|
+
data: report,
|
|
374
|
+
human,
|
|
375
|
+
error: {
|
|
376
|
+
code: "doctor_failed",
|
|
377
|
+
message: ctx.t("error.doctor_failed", { count: report.summary.fail }),
|
|
378
|
+
fix: failed2.fix ?? "finitemonkey doctor"
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
var doctorCommand = {
|
|
383
|
+
path: ["doctor"],
|
|
384
|
+
summaryKey: "command.doctor.summary",
|
|
385
|
+
usage: "finitemonkey doctor [--json]",
|
|
386
|
+
status: "ready",
|
|
387
|
+
run: (ctx) => Promise.resolve(doctorResult(ctx, buildReport(ctx, runChecks(doctorEnvFrom(ctx)))))
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/core/args.ts
|
|
391
|
+
import { parseArgs } from "node:util";
|
|
392
|
+
|
|
393
|
+
// src/core/problem.ts
|
|
394
|
+
var usageProblem = (code, messageKey, fix, params = {}) => ({
|
|
395
|
+
code,
|
|
396
|
+
messageKey,
|
|
397
|
+
params,
|
|
398
|
+
fix,
|
|
399
|
+
exitCode: ExitCode.Usage
|
|
400
|
+
});
|
|
401
|
+
var needsPersonProblem = (messageKey, fix, params = {}) => ({
|
|
402
|
+
code: "needs_confirmation",
|
|
403
|
+
messageKey,
|
|
404
|
+
params,
|
|
405
|
+
fix,
|
|
406
|
+
exitCode: ExitCode.NeedsPerson
|
|
407
|
+
});
|
|
408
|
+
var FmError = class extends Error {
|
|
409
|
+
problem;
|
|
410
|
+
constructor(problem) {
|
|
411
|
+
super(problem.code);
|
|
412
|
+
this.name = "FmError";
|
|
413
|
+
this.problem = problem;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// src/core/args.ts
|
|
418
|
+
var GLOBAL_OPTIONS = {
|
|
419
|
+
json: { type: "boolean" },
|
|
420
|
+
lang: { type: "string" },
|
|
421
|
+
yes: { type: "boolean", short: "y" },
|
|
422
|
+
"dry-run": { type: "boolean" },
|
|
423
|
+
version: { type: "boolean", short: "v" },
|
|
424
|
+
help: { type: "boolean", short: "h" }
|
|
425
|
+
};
|
|
426
|
+
var flag = (values, name) => values[name] === true;
|
|
427
|
+
var looseArgs = (argv) => {
|
|
428
|
+
const { values, positionals } = parseArgs({
|
|
429
|
+
args: [...argv],
|
|
430
|
+
options: GLOBAL_OPTIONS,
|
|
431
|
+
strict: false,
|
|
432
|
+
allowPositionals: true
|
|
433
|
+
});
|
|
434
|
+
const lang = values.lang;
|
|
435
|
+
return {
|
|
436
|
+
positionals,
|
|
437
|
+
json: flag(values, "json"),
|
|
438
|
+
yes: flag(values, "yes"),
|
|
439
|
+
dryRun: flag(values, "dry-run"),
|
|
440
|
+
version: flag(values, "version"),
|
|
441
|
+
help: flag(values, "help"),
|
|
442
|
+
lang: typeof lang === "string" ? lang : void 0
|
|
443
|
+
};
|
|
444
|
+
};
|
|
445
|
+
var resolveCommand = (registry, positionals) => registry.filter((spec) => spec.path.every((part, index) => positionals[index] === part)).reduce(
|
|
446
|
+
(best, spec) => best === void 0 || spec.path.length > best.consumed ? { spec, consumed: spec.path.length } : best,
|
|
447
|
+
void 0
|
|
448
|
+
);
|
|
449
|
+
var strictArgs = (argv, spec) => {
|
|
450
|
+
const fix = `finitemonkey help ${spec.path.join(" ")}`;
|
|
451
|
+
let parsed;
|
|
452
|
+
try {
|
|
453
|
+
parsed = parseArgs({
|
|
454
|
+
args: [...argv],
|
|
455
|
+
options: { ...GLOBAL_OPTIONS, ...spec.options },
|
|
456
|
+
strict: true,
|
|
457
|
+
allowPositionals: true
|
|
458
|
+
});
|
|
459
|
+
} catch (error) {
|
|
460
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
461
|
+
return reject(usageProblem("invalid_arguments", "error.invalid_arguments", fix, { detail }));
|
|
462
|
+
}
|
|
463
|
+
const rest = parsed.positionals.slice(spec.path.length);
|
|
464
|
+
const max = spec.maxPositionals ?? 0;
|
|
465
|
+
const extra = rest[max];
|
|
466
|
+
if (extra !== void 0) {
|
|
467
|
+
return reject(usageProblem("unexpected_argument", "error.unexpected_argument", fix, { argument: extra }));
|
|
468
|
+
}
|
|
469
|
+
return accept({ values: parsed.values, positionals: rest });
|
|
470
|
+
};
|
|
471
|
+
var stringOption = (values, name) => {
|
|
472
|
+
const value = values[name];
|
|
473
|
+
return typeof value === "string" ? value : void 0;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// src/core/result.ts
|
|
477
|
+
var SCHEMA_VERSION = 1;
|
|
478
|
+
var success = (data, human = []) => ({
|
|
479
|
+
ok: true,
|
|
480
|
+
exitCode: ExitCode.Ok,
|
|
481
|
+
data,
|
|
482
|
+
human
|
|
483
|
+
});
|
|
484
|
+
var failureFrom = (t, problem, human) => ({
|
|
485
|
+
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 },
|
|
489
|
+
...human === void 0 ? {} : { human }
|
|
490
|
+
});
|
|
491
|
+
var toEnvelope = (command, result) => ({
|
|
492
|
+
schemaVersion: SCHEMA_VERSION,
|
|
493
|
+
ok: result.ok,
|
|
494
|
+
command,
|
|
495
|
+
...result.data === void 0 ? {} : { data: result.data },
|
|
496
|
+
...result.ok ? {} : { error: result.error },
|
|
497
|
+
...result.next === void 0 ? {} : { next: result.next }
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// src/commands/types.ts
|
|
501
|
+
var commandName = (spec) => spec.path.join(" ");
|
|
502
|
+
|
|
503
|
+
// src/commands/help.ts
|
|
504
|
+
var GLOBAL_FLAGS = [
|
|
505
|
+
{ flag: "--json", key: "flag.json" },
|
|
506
|
+
{ flag: "--lang <pt-BR|en|es>", key: "flag.lang" },
|
|
507
|
+
{ flag: "--yes, -y", key: "flag.yes" },
|
|
508
|
+
{ flag: "--dry-run", key: "flag.dry_run" },
|
|
509
|
+
{ flag: "--version, -v", key: "flag.version" },
|
|
510
|
+
{ flag: "--help, -h", key: "flag.help" }
|
|
511
|
+
];
|
|
512
|
+
var exitCodeHelp = (ctx) => EXIT_CODE_NAMES.map((name) => ({ code: ExitCode[name], name, summary: ctx.t(exitCodeDocs[ExitCode[name]]) }));
|
|
513
|
+
var pad = (text2, width) => text2.padEnd(Math.max(width, text2.length + 1));
|
|
514
|
+
var describe = (ctx, spec) => ({
|
|
515
|
+
name: commandName(spec),
|
|
516
|
+
usage: spec.usage,
|
|
517
|
+
summary: ctx.t(spec.summaryKey),
|
|
518
|
+
status: spec.status,
|
|
519
|
+
options: Object.keys(spec.options ?? {}).map((name) => `--${name}`)
|
|
520
|
+
});
|
|
521
|
+
var summaryLine = (ctx, help) => help.status === "planned" ? `${help.summary} ${ctx.t("help.planned")}` : help.summary;
|
|
522
|
+
var overview = (ctx, registry) => {
|
|
523
|
+
const commands = registry.map((spec) => describe(ctx, spec));
|
|
524
|
+
const flags = GLOBAL_FLAGS.map(({ flag: flag2, key }) => ({ flag: flag2, summary: ctx.t(key) }));
|
|
525
|
+
const width = Math.max(...commands.map(({ name }) => name.length)) + 2;
|
|
526
|
+
const flagWidth = Math.max(...flags.map(({ flag: flag2 }) => flag2.length)) + 2;
|
|
527
|
+
const exitCodes = exitCodeHelp(ctx);
|
|
528
|
+
const human = [
|
|
529
|
+
`finitemonkey ${ctx.version}`,
|
|
530
|
+
ctx.t("help.tagline"),
|
|
531
|
+
"",
|
|
532
|
+
`${ctx.t("help.usage")}: finitemonkey <${ctx.t("help.command")}> [flags]`,
|
|
533
|
+
"",
|
|
534
|
+
`${ctx.t("help.commands")}:`,
|
|
535
|
+
...commands.map((help) => ` ${pad(help.name, width)}${summaryLine(ctx, help)}`),
|
|
536
|
+
"",
|
|
537
|
+
`${ctx.t("help.flags")}:`,
|
|
538
|
+
...flags.map(({ flag: flag2, summary }) => ` ${pad(flag2, flagWidth)}${summary}`),
|
|
539
|
+
"",
|
|
540
|
+
`${ctx.t("help.exit_codes")}:`,
|
|
541
|
+
...exitCodes.map(({ summary }) => ` ${summary}`),
|
|
542
|
+
"",
|
|
543
|
+
ctx.t("help.more")
|
|
544
|
+
];
|
|
545
|
+
return success({ commands, flags, exitCodes }, human);
|
|
546
|
+
};
|
|
547
|
+
var helpFor = (ctx, spec) => {
|
|
548
|
+
const help = describe(ctx, spec);
|
|
549
|
+
const human = [
|
|
550
|
+
`${ctx.t("help.usage")}: ${help.usage}`,
|
|
551
|
+
"",
|
|
552
|
+
summaryLine(ctx, help),
|
|
553
|
+
...help.options.length > 0 ? ["", `${ctx.t("help.options")}: ${help.options.join(", ")}`] : []
|
|
554
|
+
];
|
|
555
|
+
return success({ command: help }, human);
|
|
556
|
+
};
|
|
557
|
+
var topicHelp = (ctx, registry, topic) => {
|
|
558
|
+
const resolved = resolveCommand(registry, topic);
|
|
559
|
+
if (resolved !== void 0) return helpFor(ctx, resolved.spec);
|
|
560
|
+
const problem = usageProblem("unknown_command", "error.unknown_command", "finitemonkey help", {
|
|
561
|
+
command: topic.join(" ")
|
|
562
|
+
});
|
|
563
|
+
return failureFrom(ctx.t, problem);
|
|
564
|
+
};
|
|
565
|
+
var helpCommand = {
|
|
566
|
+
path: ["help"],
|
|
567
|
+
summaryKey: "command.help.summary",
|
|
568
|
+
usage: "finitemonkey help [command]",
|
|
569
|
+
status: "ready",
|
|
570
|
+
maxPositionals: 3,
|
|
571
|
+
run: (ctx, args, registry) => Promise.resolve(
|
|
572
|
+
args.positionals.length === 0 ? overview(ctx, registry) : topicHelp(ctx, registry, args.positionals)
|
|
573
|
+
)
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// src/hooks/events.ts
|
|
577
|
+
var HOOK_EVENTS = [
|
|
578
|
+
"SessionStart",
|
|
579
|
+
"UserPromptSubmit",
|
|
580
|
+
"PreToolUse",
|
|
581
|
+
"PostToolUse",
|
|
582
|
+
"Stop",
|
|
583
|
+
"SubagentStart",
|
|
584
|
+
"SubagentStop",
|
|
585
|
+
"PreCompact",
|
|
586
|
+
"ConfigChange",
|
|
587
|
+
"Notification",
|
|
588
|
+
"SessionEnd"
|
|
589
|
+
];
|
|
590
|
+
var HOOK_POLICIES = {
|
|
591
|
+
SessionStart: { failClosed: false },
|
|
592
|
+
UserPromptSubmit: { failClosed: true },
|
|
593
|
+
PreToolUse: { failClosed: true },
|
|
594
|
+
PostToolUse: { failClosed: false },
|
|
595
|
+
Stop: { failClosed: true },
|
|
596
|
+
SubagentStart: { failClosed: false },
|
|
597
|
+
SubagentStop: { failClosed: true },
|
|
598
|
+
PreCompact: { failClosed: false },
|
|
599
|
+
ConfigChange: { failClosed: true },
|
|
600
|
+
Notification: { failClosed: false },
|
|
601
|
+
SessionEnd: { failClosed: false }
|
|
602
|
+
};
|
|
603
|
+
var squash = (value) => value.replace(/[-_\s]/g, "").toLowerCase();
|
|
604
|
+
var parseHookEvent = (raw) => HOOK_EVENTS.find((event) => squash(event) === squash(raw));
|
|
605
|
+
var DEFAULT_HOOK_DEADLINE_MS = 3e3;
|
|
606
|
+
var parseDeadline = (raw) => {
|
|
607
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
608
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_HOOK_DEADLINE_MS;
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// src/secrets/baseline.ts
|
|
612
|
+
import { join as join2 } from "node:path";
|
|
613
|
+
var BASELINE_FILE = ".finitemonkey/findings/baseline.json";
|
|
614
|
+
var BASELINE_SCHEMA = "finitemonkey/secrets-baseline@1";
|
|
615
|
+
var BASELINE_STATES = ["aceito", "falso-positivo"];
|
|
616
|
+
var EXCEPTION_MAX_DAYS = 90;
|
|
617
|
+
var EMPTY_BASELINE = { schema: BASELINE_SCHEMA, entries: [] };
|
|
618
|
+
var baselinePath = (root) => toAbsolutePath(join2(root, BASELINE_FILE));
|
|
619
|
+
var text = (record2, key) => {
|
|
620
|
+
const value = record2[key];
|
|
621
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
622
|
+
};
|
|
623
|
+
var stateOf = (value) => BASELINE_STATES.find((state) => state === value);
|
|
624
|
+
var parseEntry = (value) => {
|
|
625
|
+
if (!isRecord(value)) return void 0;
|
|
626
|
+
const fields = ["id", "rule", "file", "fingerprint", "owner", "reason", "acceptedAt", "expiresAt"];
|
|
627
|
+
const [id, rule, file, fingerprint, owner, reason, acceptedAt, expiresAt] = fields.map((key) => text(value, key));
|
|
628
|
+
const state = stateOf(value["state"]);
|
|
629
|
+
const line = value["line"];
|
|
630
|
+
if (id === void 0 || rule === void 0 || file === void 0 || fingerprint === void 0) return void 0;
|
|
631
|
+
if (owner === void 0 || reason === void 0 || acceptedAt === void 0 || expiresAt === void 0)
|
|
632
|
+
return void 0;
|
|
633
|
+
if (state === void 0 || typeof line !== "number") return void 0;
|
|
634
|
+
return { id, rule, file, line, fingerprint, state, owner, reason, acceptedAt, expiresAt };
|
|
635
|
+
};
|
|
636
|
+
var parseBaseline = (raw) => {
|
|
637
|
+
const parsed = parseJsonObject(raw);
|
|
638
|
+
if (!parsed.ok) return parsed;
|
|
639
|
+
if (parsed.value["schema"] !== BASELINE_SCHEMA) return reject(`schema must be ${BASELINE_SCHEMA}`);
|
|
640
|
+
const list2 = parsed.value["entries"];
|
|
641
|
+
if (!Array.isArray(list2)) return reject("entries must be a list");
|
|
642
|
+
const entries = [];
|
|
643
|
+
for (const [index, item] of list2.entries()) {
|
|
644
|
+
const entry = parseEntry(item);
|
|
645
|
+
if (entry === void 0) return reject(`entry ${String(index)} is incomplete`);
|
|
646
|
+
entries.push(entry);
|
|
647
|
+
}
|
|
648
|
+
return accept({ schema: BASELINE_SCHEMA, entries });
|
|
649
|
+
};
|
|
650
|
+
var loadBaseline = (runtime, root) => {
|
|
651
|
+
const raw = runtime.readText(baselinePath(root));
|
|
652
|
+
return raw.ok ? parseBaseline(raw.value) : accept(EMPTY_BASELINE);
|
|
653
|
+
};
|
|
654
|
+
var serializeBaseline = (baseline) => `${JSON.stringify({ schema: baseline.schema, entries: [...baseline.entries].sort((a, b) => a.id.localeCompare(b.id)) }, null, 2)}
|
|
655
|
+
`;
|
|
656
|
+
var saveBaseline = (runtime, root, baseline) => runtime.writeText(baselinePath(root), serializeBaseline(baseline));
|
|
657
|
+
var ratchet = (findings, baseline, now, inScope) => {
|
|
658
|
+
const byId = new Map(baseline.entries.map((entry) => [entry.id, entry]));
|
|
659
|
+
const seen = new Set(findings.map((finding) => finding.id));
|
|
660
|
+
const entryOf2 = (finding) => byId.get(finding.id);
|
|
661
|
+
return {
|
|
662
|
+
fresh: findings.filter((finding) => entryOf2(finding) === void 0),
|
|
663
|
+
known: findings.filter((finding) => (entryOf2(finding)?.expiresAt ?? "") >= now),
|
|
664
|
+
expired: findings.filter((finding) => {
|
|
665
|
+
const entry = entryOf2(finding);
|
|
666
|
+
return entry !== void 0 && entry.expiresAt < now;
|
|
667
|
+
}),
|
|
668
|
+
fixed: inScope === void 0 ? [] : baseline.entries.filter((entry) => !seen.has(entry.id) && inScope(entry.file))
|
|
669
|
+
};
|
|
670
|
+
};
|
|
671
|
+
var withoutFixed = (baseline, fixed) => {
|
|
672
|
+
const gone = new Set(fixed.map((entry) => entry.id));
|
|
673
|
+
return { schema: baseline.schema, entries: baseline.entries.filter((entry) => !gone.has(entry.id)) };
|
|
674
|
+
};
|
|
675
|
+
var acceptedFingerprints = (baseline, now) => new Set(baseline.entries.filter((entry) => entry.expiresAt >= now).map((entry) => entry.fingerprint));
|
|
676
|
+
|
|
677
|
+
// src/core/posix-path.ts
|
|
678
|
+
var toPosixPath = (path) => path.replace(/\\/g, "/");
|
|
679
|
+
var withoutCarriageReturn = (line) => line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
680
|
+
|
|
681
|
+
// src/secrets/allowlist.ts
|
|
682
|
+
var PLACEHOLDER_WORDS = [
|
|
683
|
+
"example",
|
|
684
|
+
"placeholder",
|
|
685
|
+
"your_",
|
|
686
|
+
"your-",
|
|
687
|
+
"yourkey",
|
|
688
|
+
"yourtoken",
|
|
689
|
+
"yoursecret",
|
|
690
|
+
"_here",
|
|
691
|
+
"-here",
|
|
692
|
+
"changeme",
|
|
693
|
+
"change_me",
|
|
694
|
+
"change-me",
|
|
695
|
+
"replace",
|
|
696
|
+
"redacted",
|
|
697
|
+
"dummy",
|
|
698
|
+
"fake",
|
|
699
|
+
"sample",
|
|
700
|
+
"mock",
|
|
701
|
+
"fixture",
|
|
702
|
+
"test",
|
|
703
|
+
"todo",
|
|
704
|
+
"xxxx",
|
|
705
|
+
"****",
|
|
706
|
+
"....",
|
|
707
|
+
"…",
|
|
708
|
+
"<",
|
|
709
|
+
">",
|
|
710
|
+
"${",
|
|
711
|
+
"{{",
|
|
712
|
+
"%s",
|
|
713
|
+
"process.env",
|
|
714
|
+
"secrets.",
|
|
715
|
+
"env("
|
|
716
|
+
];
|
|
717
|
+
var PLACEHOLDER_VALUES = /* @__PURE__ */ new Set([
|
|
718
|
+
"password",
|
|
719
|
+
"passwd",
|
|
720
|
+
"secret",
|
|
721
|
+
"token",
|
|
722
|
+
"apikey",
|
|
723
|
+
"api_key",
|
|
724
|
+
"admin",
|
|
725
|
+
"root",
|
|
726
|
+
"postgres",
|
|
727
|
+
"changeit",
|
|
728
|
+
"user",
|
|
729
|
+
"pass"
|
|
730
|
+
]);
|
|
731
|
+
var ENV_REFERENCE = /^\$\{?[A-Z_][A-Z0-9_]*\}?$/;
|
|
732
|
+
var LOCKFILES = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|go\.sum|Cargo\.lock|poetry\.lock|uv\.lock|composer\.lock|Gemfile\.lock)$/;
|
|
733
|
+
var TEST_PATH = /(?:^|\/)(?:test|tests|__tests__|__fixtures__|fixtures|spec|testdata|examples?)\/|\.(?:test|spec)\.[A-Za-z0-9]+$/;
|
|
734
|
+
var isPlaceholder = (value) => {
|
|
735
|
+
const lower = value.toLowerCase();
|
|
736
|
+
if (PLACEHOLDER_VALUES.has(lower) || ENV_REFERENCE.test(value)) return true;
|
|
737
|
+
if (PLACEHOLDER_WORDS.some((word) => lower.includes(word))) return true;
|
|
738
|
+
return new Set(lower).size < 4;
|
|
739
|
+
};
|
|
740
|
+
var isTestOrLockPath = (path) => path !== void 0 && (LOCKFILES.test(toPosixPath(path)) || TEST_PATH.test(toPosixPath(path)));
|
|
741
|
+
|
|
742
|
+
// src/secrets/entropy.ts
|
|
743
|
+
var shannonEntropy = (text2) => {
|
|
744
|
+
if (text2.length === 0) return 0;
|
|
745
|
+
const counts = /* @__PURE__ */ new Map();
|
|
746
|
+
for (const char of text2) counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
747
|
+
let entropy = 0;
|
|
748
|
+
for (const count2 of counts.values()) {
|
|
749
|
+
const p = count2 / text2.length;
|
|
750
|
+
entropy -= p * Math.log2(p);
|
|
751
|
+
}
|
|
752
|
+
return entropy;
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// src/secrets/fingerprint.ts
|
|
756
|
+
import { createHash } from "node:crypto";
|
|
757
|
+
var sha256Hex = (text2) => createHash("sha256").update(text2).digest("hex");
|
|
758
|
+
var fingerprintOf = (secret) => sha256Hex(`finitemonkey:secret:${secret}`).slice(0, 16);
|
|
759
|
+
var ALNUM = /[A-Za-z0-9]/g;
|
|
760
|
+
var MIN_TAIL_LENGTH = 12;
|
|
761
|
+
var tailOf = (secret) => {
|
|
762
|
+
const visible = (secret.match(ALNUM) ?? []).join("");
|
|
763
|
+
return visible.length >= MIN_TAIL_LENGTH ? visible.slice(-4) : "****";
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// src/secrets/lines.ts
|
|
767
|
+
var lineLocator = (text2) => {
|
|
768
|
+
const starts = [0];
|
|
769
|
+
for (let index = text2.indexOf("\n"); index !== -1; index = text2.indexOf("\n", index + 1)) starts.push(index + 1);
|
|
770
|
+
return (offset) => {
|
|
771
|
+
let low = 0;
|
|
772
|
+
let high = starts.length - 1;
|
|
773
|
+
while (low < high) {
|
|
774
|
+
const mid = low + high + 1 >> 1;
|
|
775
|
+
if ((starts[mid] ?? 0) <= offset) low = mid;
|
|
776
|
+
else high = mid - 1;
|
|
777
|
+
}
|
|
778
|
+
return { line: low + 1, column: offset - (starts[low] ?? 0) + 1 };
|
|
779
|
+
};
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
// src/secrets/rules/types.ts
|
|
783
|
+
var defineRule = (spec) => ({
|
|
784
|
+
id: spec.id,
|
|
785
|
+
summary: spec.summary,
|
|
786
|
+
kind: spec.kind ?? "credential",
|
|
787
|
+
severity: spec.severity ?? "high",
|
|
788
|
+
keywords: spec.keywords.map((keyword) => keyword.toLowerCase()),
|
|
789
|
+
pattern: new RegExp(spec.source, `gd${spec.flags ?? ""}`),
|
|
790
|
+
minLength: spec.minLength ?? 8,
|
|
791
|
+
minEntropy: spec.minEntropy ?? 0,
|
|
792
|
+
requireDigit: spec.requireDigit ?? false,
|
|
793
|
+
minRandomRun: spec.minRandomRun ?? 0,
|
|
794
|
+
ignoredNames: (spec.ignoredNames ?? []).map((name) => name.toLowerCase()),
|
|
795
|
+
skipTestPaths: spec.skipTestPaths ?? false,
|
|
796
|
+
placeholders: spec.placeholders ?? true
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// src/secrets/rules/providers.ts
|
|
800
|
+
var PROVIDER_RULES = [
|
|
801
|
+
defineRule({
|
|
802
|
+
id: "aws-access-token",
|
|
803
|
+
summary: "AWS access key id (AKIA, ASIA, ABIA, ACCA, A3T)",
|
|
804
|
+
keywords: ["akia", "asia", "abia", "acca", "a3t"],
|
|
805
|
+
source: String.raw`\b(?<secret>(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b`,
|
|
806
|
+
minLength: 20,
|
|
807
|
+
minEntropy: 3
|
|
808
|
+
}),
|
|
809
|
+
defineRule({
|
|
810
|
+
id: "aws-secret-access-key",
|
|
811
|
+
summary: "AWS secret access key assigned to aws_secret_access_key or a similar name",
|
|
812
|
+
keywords: ["secret"],
|
|
813
|
+
source: String.raw`(?:aws_?)?secret_?access_?key["'\x60]?[ \t]*(?:=|:=|:|=>)[ \t]*["'\x60]?(?<secret>[A-Za-z0-9/+=]{40})(?![A-Za-z0-9/+=])`,
|
|
814
|
+
flags: "i",
|
|
815
|
+
minLength: 40,
|
|
816
|
+
minEntropy: 4
|
|
817
|
+
}),
|
|
818
|
+
defineRule({
|
|
819
|
+
id: "github-pat",
|
|
820
|
+
summary: "GitHub classic personal access token (ghp_)",
|
|
821
|
+
keywords: ["ghp_"],
|
|
822
|
+
source: String.raw`\b(?<secret>ghp_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
|
|
823
|
+
minEntropy: 3
|
|
824
|
+
}),
|
|
825
|
+
defineRule({
|
|
826
|
+
id: "github-oauth",
|
|
827
|
+
summary: "GitHub OAuth access token (gho_)",
|
|
828
|
+
keywords: ["gho_"],
|
|
829
|
+
source: String.raw`\b(?<secret>gho_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
|
|
830
|
+
minEntropy: 3
|
|
831
|
+
}),
|
|
832
|
+
defineRule({
|
|
833
|
+
id: "github-app-token",
|
|
834
|
+
summary: "GitHub App user or installation token (ghu_, ghs_)",
|
|
835
|
+
keywords: ["ghu_", "ghs_"],
|
|
836
|
+
source: String.raw`\b(?<secret>(?:ghu|ghs)_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
|
|
837
|
+
minEntropy: 3
|
|
838
|
+
}),
|
|
839
|
+
defineRule({
|
|
840
|
+
id: "github-refresh-token",
|
|
841
|
+
summary: "GitHub refresh token (ghr_)",
|
|
842
|
+
keywords: ["ghr_"],
|
|
843
|
+
source: String.raw`\b(?<secret>ghr_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
|
|
844
|
+
minEntropy: 3
|
|
845
|
+
}),
|
|
846
|
+
defineRule({
|
|
847
|
+
id: "github-fine-grained-pat",
|
|
848
|
+
summary: "GitHub fine-grained personal access token (github_pat_)",
|
|
849
|
+
keywords: ["github_pat_"],
|
|
850
|
+
source: String.raw`\b(?<secret>github_pat_[A-Za-z0-9_]{82})(?![A-Za-z0-9_])`,
|
|
851
|
+
minEntropy: 3
|
|
852
|
+
}),
|
|
853
|
+
defineRule({
|
|
854
|
+
id: "gitlab-pat",
|
|
855
|
+
summary: "GitLab personal access token (glpat-)",
|
|
856
|
+
keywords: ["glpat-"],
|
|
857
|
+
source: String.raw`\b(?<secret>glpat-[A-Za-z0-9_-]{20})(?![A-Za-z0-9_-])`,
|
|
858
|
+
minEntropy: 3
|
|
859
|
+
}),
|
|
860
|
+
defineRule({
|
|
861
|
+
id: "npm-access-token",
|
|
862
|
+
summary: "npm access token (npm_)",
|
|
863
|
+
keywords: ["npm_"],
|
|
864
|
+
source: String.raw`\b(?<secret>npm_[A-Za-z0-9]{36})(?![A-Za-z0-9])`,
|
|
865
|
+
flags: "i",
|
|
866
|
+
minEntropy: 3
|
|
867
|
+
}),
|
|
868
|
+
defineRule({
|
|
869
|
+
id: "slack-bot-token",
|
|
870
|
+
summary: "Slack bot token (xoxb-)",
|
|
871
|
+
keywords: ["xoxb-"],
|
|
872
|
+
source: String.raw`\b(?<secret>xoxb-[0-9]{10,13}-[0-9]{10,13}[A-Za-z0-9-]*)`,
|
|
873
|
+
minEntropy: 3
|
|
874
|
+
}),
|
|
875
|
+
defineRule({
|
|
876
|
+
id: "slack-user-token",
|
|
877
|
+
summary: "Slack user or export token (xoxp-, xoxe-)",
|
|
878
|
+
keywords: ["xoxp-", "xoxe-"],
|
|
879
|
+
source: String.raw`\b(?<secret>xox[pe](?:-[0-9]{10,13}){3}-[A-Za-z0-9-]{28,34})`,
|
|
880
|
+
minEntropy: 3
|
|
881
|
+
}),
|
|
882
|
+
defineRule({
|
|
883
|
+
id: "slack-app-token",
|
|
884
|
+
summary: "Slack app-level token (xapp-)",
|
|
885
|
+
keywords: ["xapp-"],
|
|
886
|
+
source: String.raw`\b(?<secret>xapp-\d-[A-Z0-9]+-\d+-[a-z0-9]+)`,
|
|
887
|
+
flags: "i",
|
|
888
|
+
minEntropy: 3
|
|
889
|
+
}),
|
|
890
|
+
defineRule({
|
|
891
|
+
id: "slack-webhook-url",
|
|
892
|
+
summary: "Slack incoming webhook URL",
|
|
893
|
+
keywords: ["hooks.slack.com"],
|
|
894
|
+
source: String.raw`(?<secret>hooks\.slack\.com/(?:services|workflows|triggers)/[A-Za-z0-9+/]{43,56})`,
|
|
895
|
+
minEntropy: 3
|
|
896
|
+
}),
|
|
897
|
+
defineRule({
|
|
898
|
+
id: "stripe-access-token",
|
|
899
|
+
summary: "Stripe live secret or restricted key (sk_live_, rk_live_)",
|
|
900
|
+
keywords: ["sk_live_", "rk_live_", "sk_prod_", "rk_prod_"],
|
|
901
|
+
source: String.raw`\b(?<secret>(?:sk|rk)_(?:live|prod)_[A-Za-z0-9]{20,99})(?![A-Za-z0-9])`,
|
|
902
|
+
minEntropy: 3
|
|
903
|
+
}),
|
|
904
|
+
defineRule({
|
|
905
|
+
id: "anthropic-api-key",
|
|
906
|
+
summary: "Anthropic API or admin key (sk-ant-)",
|
|
907
|
+
keywords: ["sk-ant-"],
|
|
908
|
+
source: String.raw`\b(?<secret>sk-ant-(?:api|admin|oat)\d{2}-[A-Za-z0-9_-]{80,})`,
|
|
909
|
+
minEntropy: 3.5
|
|
910
|
+
}),
|
|
911
|
+
defineRule({
|
|
912
|
+
id: "openai-api-key",
|
|
913
|
+
summary: "OpenAI API key (sk-proj-, sk-svcacct-, sk-admin- or the T3BlbkFJ marker)",
|
|
914
|
+
keywords: ["t3blbkfj", "sk-proj-", "sk-svcacct-", "sk-admin-"],
|
|
915
|
+
source: String.raw`\b(?<secret>sk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}T3BlbkFJ[A-Za-z0-9_-]{20,}|sk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{58,})`,
|
|
916
|
+
minEntropy: 3.5
|
|
917
|
+
}),
|
|
918
|
+
defineRule({
|
|
919
|
+
id: "gcp-api-key",
|
|
920
|
+
summary: "Google API key (AIza)",
|
|
921
|
+
keywords: ["aiza"],
|
|
922
|
+
source: String.raw`\b(?<secret>AIza[0-9A-Za-z_-]{35})(?![0-9A-Za-z_-])`,
|
|
923
|
+
minEntropy: 3
|
|
924
|
+
}),
|
|
925
|
+
defineRule({
|
|
926
|
+
id: "asaas-api-key",
|
|
927
|
+
summary: "Asaas API key ($aact_, $aact_prod_, $aact_hmlg_) with a body; the bare prefix is a placeholder",
|
|
928
|
+
keywords: ["$aact_"],
|
|
929
|
+
source: String.raw`(?<secret>\$aact_(?:prod_|hmlg_)?[A-Za-z0-9+/=_:$-]{32,})`,
|
|
930
|
+
minLength: 40,
|
|
931
|
+
minEntropy: 3.5
|
|
932
|
+
})
|
|
933
|
+
];
|
|
934
|
+
|
|
935
|
+
// src/secrets/rules/structural.ts
|
|
936
|
+
var STRUCTURAL_RULES = [
|
|
937
|
+
defineRule({
|
|
938
|
+
id: "private-key",
|
|
939
|
+
summary: "PEM private key block (RSA, EC, DSA, OPENSSH, PGP, PKCS#8)",
|
|
940
|
+
keywords: ["private key"],
|
|
941
|
+
source: String.raw`-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----(?<secret>[\s\S]{64,16384}?)-----END[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----`,
|
|
942
|
+
minLength: 64,
|
|
943
|
+
minEntropy: 4
|
|
944
|
+
}),
|
|
945
|
+
defineRule({
|
|
946
|
+
id: "jwt",
|
|
947
|
+
summary: "JSON Web Token with a header, a payload and a signature, checked for entropy",
|
|
948
|
+
keywords: ["eyj"],
|
|
949
|
+
source: String.raw`\b(?<secret>eyJ[A-Za-z0-9_-]{17,}\.eyJ[A-Za-z0-9_-]{17,}\.[A-Za-z0-9_-]{10,})`,
|
|
950
|
+
minLength: 40,
|
|
951
|
+
minEntropy: 3
|
|
952
|
+
}),
|
|
953
|
+
defineRule({
|
|
954
|
+
id: "credential-in-url",
|
|
955
|
+
summary: "Password or token inside a URL (scheme://user:secret@host), including git remotes",
|
|
956
|
+
keywords: ["://"],
|
|
957
|
+
source: String.raw`\b[a-z][a-z0-9+.-]{1,20}://[^\s:/@"'\x60]{1,64}:(?<secret>[^\s:/@"'\x60]{6,128})@[A-Za-z0-9.-]`,
|
|
958
|
+
flags: "i",
|
|
959
|
+
minLength: 6,
|
|
960
|
+
minEntropy: 2.5
|
|
961
|
+
}),
|
|
962
|
+
defineRule({
|
|
963
|
+
id: "default-secret-fallback",
|
|
964
|
+
summary: "JavaScript secret variable with a constant fallback (process.env.X ?? 'constant')",
|
|
965
|
+
kind: "practice",
|
|
966
|
+
severity: "medium",
|
|
967
|
+
keywords: ["process.env."],
|
|
968
|
+
source: String.raw`process\.env\.[A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD)[A-Z0-9_]*[ \t]*(?:\?\?|\|\|)[ \t]*["'\x60](?<secret>[^"'\x60\n]{4,})["'\x60]`,
|
|
969
|
+
minLength: 4,
|
|
970
|
+
placeholders: false,
|
|
971
|
+
skipTestPaths: true
|
|
972
|
+
}),
|
|
973
|
+
defineRule({
|
|
974
|
+
id: "default-secret-fallback-python",
|
|
975
|
+
summary: "Python secret variable with a constant default (os.getenv('X', 'constant'))",
|
|
976
|
+
kind: "practice",
|
|
977
|
+
severity: "medium",
|
|
978
|
+
keywords: ["getenv", "environ.get"],
|
|
979
|
+
source: String.raw`os\.(?:getenv|environ\.get)\([ \t]*["'][A-Za-z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD)[A-Za-z0-9_]*["'][ \t]*,[ \t]*["'](?<secret>[^"'\n]{4,})["']`,
|
|
980
|
+
minLength: 4,
|
|
981
|
+
placeholders: false,
|
|
982
|
+
skipTestPaths: true
|
|
983
|
+
}),
|
|
984
|
+
defineRule({
|
|
985
|
+
id: "generic-api-key",
|
|
986
|
+
summary: "High-entropy value assigned to a name with key, secret, token or credential",
|
|
987
|
+
keywords: ["key", "secret", "token", "credential"],
|
|
988
|
+
source: String.raw`(?:key|secret|token|credential)s?[A-Za-z0-9_.-]{0,30}["'\x60]?[ \t]*(?:=|:=|:|=>)[ \t]*["'\x60]?(?<secret>[A-Za-z0-9_\-+/=.~$!@#%^&*]{16,200})(?![A-Za-z0-9_\-+/=.~$!@#%^&*(])`,
|
|
989
|
+
flags: "i",
|
|
990
|
+
minLength: 16,
|
|
991
|
+
minEntropy: 3.5,
|
|
992
|
+
requireDigit: true,
|
|
993
|
+
minRandomRun: 12,
|
|
994
|
+
ignoredNames: [
|
|
995
|
+
"key",
|
|
996
|
+
"keys",
|
|
997
|
+
"cachekey",
|
|
998
|
+
"querykey",
|
|
999
|
+
"storagekey",
|
|
1000
|
+
"sortkey",
|
|
1001
|
+
"idempotencykey",
|
|
1002
|
+
"keyname",
|
|
1003
|
+
"tokentype"
|
|
1004
|
+
],
|
|
1005
|
+
skipTestPaths: true
|
|
1006
|
+
}),
|
|
1007
|
+
defineRule({
|
|
1008
|
+
id: "password-assignment",
|
|
1009
|
+
summary: "Literal value assigned to a password name (password, passwd, pwd, senha, contraseña)",
|
|
1010
|
+
keywords: ["passw", "pwd", "senha", "contrase"],
|
|
1011
|
+
source: String.raw`(?:password|passwd|pwd|senha|contrase[ñn]a)[A-Za-z0-9_.-]{0,20}["'\x60]?[ \t]*(?:=|:=|:|=>)[ \t]*["'\x60]?(?<secret>[^\s"'\x60,;()[\]{}<>]{8,128})`,
|
|
1012
|
+
flags: "i",
|
|
1013
|
+
minLength: 8,
|
|
1014
|
+
minEntropy: 3,
|
|
1015
|
+
requireDigit: true,
|
|
1016
|
+
skipTestPaths: true
|
|
1017
|
+
})
|
|
1018
|
+
];
|
|
1019
|
+
|
|
1020
|
+
// src/secrets/rules/catalog.ts
|
|
1021
|
+
var RULES = [...PROVIDER_RULES, ...STRUCTURAL_RULES];
|
|
1022
|
+
var CREDENTIAL_RULES = RULES.filter((rule) => rule.kind === "credential");
|
|
1023
|
+
var ruleById = (id) => RULES.find((rule) => rule.id === id);
|
|
1024
|
+
|
|
1025
|
+
// src/secrets/engine.ts
|
|
1026
|
+
var DIGIT = /[0-9]/;
|
|
1027
|
+
var LETTER = /[A-Za-z]/;
|
|
1028
|
+
var RUN_ENTROPY = 3;
|
|
1029
|
+
var hasRandomRun = (secret, length) => (secret.match(new RegExp(`[A-Za-z0-9]{${String(length)},}`, "g")) ?? []).some(
|
|
1030
|
+
(run2) => DIGIT.test(run2) && LETTER.test(run2) && shannonEntropy(run2) >= RUN_ENTROPY
|
|
1031
|
+
);
|
|
1032
|
+
var accepted = (rule, secret) => secret.length >= rule.minLength && (!rule.requireDigit || DIGIT.test(secret)) && (rule.minRandomRun === 0 || hasRandomRun(secret, rule.minRandomRun)) && (!rule.placeholders || !isPlaceholder(secret)) && (rule.minEntropy === 0 || shannonEntropy(secret) >= rule.minEntropy);
|
|
1033
|
+
var NAME_CHAR = /[A-Za-z0-9_.-]/;
|
|
1034
|
+
var nameAround = (text2, at) => {
|
|
1035
|
+
let start = at;
|
|
1036
|
+
while (start > 0 && NAME_CHAR.test(text2.charAt(start - 1))) start -= 1;
|
|
1037
|
+
let end = at;
|
|
1038
|
+
while (end < text2.length && NAME_CHAR.test(text2.charAt(end))) end += 1;
|
|
1039
|
+
return text2.slice(start, end).toLowerCase();
|
|
1040
|
+
};
|
|
1041
|
+
var ignoredName = (rule, match) => rule.ignoredNames.length > 0 && rule.ignoredNames.includes(nameAround(match.input, match.index));
|
|
1042
|
+
var spanOf = (rule, match) => {
|
|
1043
|
+
const range = match.indices?.groups?.["secret"] ?? match.indices?.[0];
|
|
1044
|
+
if (range === void 0 || ignoredName(rule, match)) return void 0;
|
|
1045
|
+
const [start, end] = range;
|
|
1046
|
+
const secret = match.input.slice(start, end);
|
|
1047
|
+
return accepted(rule, secret) ? { rule, start, end, secret } : void 0;
|
|
1048
|
+
};
|
|
1049
|
+
var ruleSpans = (rule, text2) => {
|
|
1050
|
+
const spans = [];
|
|
1051
|
+
for (const match of text2.matchAll(rule.pattern)) {
|
|
1052
|
+
const span = spanOf(rule, match);
|
|
1053
|
+
if (span !== void 0) spans.push(span);
|
|
1054
|
+
}
|
|
1055
|
+
return spans;
|
|
1056
|
+
};
|
|
1057
|
+
var overlaps = (taken, span) => taken.some((other) => span.start < other.end && other.start < span.end);
|
|
1058
|
+
var applicable = (rules, lower, path) => {
|
|
1059
|
+
const testPath = isTestOrLockPath(path);
|
|
1060
|
+
return rules.filter(
|
|
1061
|
+
(rule) => !(testPath && rule.skipTestPaths) && rule.keywords.some((keyword) => lower.includes(keyword))
|
|
1062
|
+
);
|
|
1063
|
+
};
|
|
1064
|
+
var toFinding = (span, locate2) => {
|
|
1065
|
+
const position = locate2(span.start);
|
|
1066
|
+
return {
|
|
1067
|
+
ruleId: span.rule.id,
|
|
1068
|
+
severity: span.rule.severity,
|
|
1069
|
+
line: position.line,
|
|
1070
|
+
column: position.column,
|
|
1071
|
+
start: span.start,
|
|
1072
|
+
end: span.end,
|
|
1073
|
+
fingerprint: fingerprintOf(span.secret),
|
|
1074
|
+
tail: tailOf(span.secret)
|
|
1075
|
+
};
|
|
1076
|
+
};
|
|
1077
|
+
var scanText = (text2, options = {}) => {
|
|
1078
|
+
if (text2.length === 0) return [];
|
|
1079
|
+
const rules = applicable(options.rules ?? RULES, text2.toLowerCase(), options.path);
|
|
1080
|
+
if (rules.length === 0) return [];
|
|
1081
|
+
const taken = [];
|
|
1082
|
+
for (const rule of rules) {
|
|
1083
|
+
for (const span of ruleSpans(rule, text2)) if (!overlaps(taken, span)) taken.push(span);
|
|
1084
|
+
}
|
|
1085
|
+
if (taken.length === 0) return [];
|
|
1086
|
+
const locate2 = lineLocator(text2);
|
|
1087
|
+
return taken.sort((a, b) => a.start - b.start).map((span) => toFinding(span, locate2));
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// src/secrets/incidents.ts
|
|
1091
|
+
import { join as join3 } from "node:path";
|
|
1092
|
+
var INCIDENTS_DIR = ".finitemonkey/incidents";
|
|
1093
|
+
var INCIDENT_STATES = ["aberto", "exposicao-local", "rotacionado", "fechado"];
|
|
1094
|
+
var INCIDENT_SOURCES = ["tool-output", "prompt", "history", "scan"];
|
|
1095
|
+
var ID = /^inc-[0-9a-f]{12}$/;
|
|
1096
|
+
var incidentId = (rule, fingerprint) => `inc-${sha256Hex(`${rule}\0${fingerprint}`).slice(0, 12)}`;
|
|
1097
|
+
var incidentsDir = (root) => toAbsolutePath(join3(root, INCIDENTS_DIR));
|
|
1098
|
+
var incidentFile = (root, id) => toAbsolutePath(join3(incidentsDir(root), `${id}.json`));
|
|
1099
|
+
var pick = (options, value) => options.find((option) => option === value);
|
|
1100
|
+
var str = (record2, key) => {
|
|
1101
|
+
const value = record2[key];
|
|
1102
|
+
return typeof value === "string" ? value : void 0;
|
|
1103
|
+
};
|
|
1104
|
+
var parseEvent = (value) => {
|
|
1105
|
+
if (!isRecord(value)) return void 0;
|
|
1106
|
+
const state = pick(INCIDENT_STATES, value["state"]);
|
|
1107
|
+
const at = str(value, "at");
|
|
1108
|
+
const by = str(value, "by");
|
|
1109
|
+
return state === void 0 || at === void 0 || by === void 0 ? void 0 : { state, at, by };
|
|
1110
|
+
};
|
|
1111
|
+
var parseHistory = (value) => {
|
|
1112
|
+
if (!Array.isArray(value)) return void 0;
|
|
1113
|
+
const events = value.map(parseEvent);
|
|
1114
|
+
return events.every((event) => event !== void 0) ? events : void 0;
|
|
1115
|
+
};
|
|
1116
|
+
var parseIncident = (raw) => {
|
|
1117
|
+
const parsed = parseJsonObject(raw);
|
|
1118
|
+
if (!parsed.ok) return parsed;
|
|
1119
|
+
const record2 = parsed.value;
|
|
1120
|
+
const [id, rule, where, fingerprint, createdAt, updatedAt] = ["id", "rule", "where", "fingerprint", "createdAt", "updatedAt"].map((key) => str(record2, key));
|
|
1121
|
+
const state = pick(INCIDENT_STATES, record2["state"]);
|
|
1122
|
+
const source = pick(INCIDENT_SOURCES, record2["source"]);
|
|
1123
|
+
const history = parseHistory(record2["history"]);
|
|
1124
|
+
const firstCommit = str(record2, "firstCommit");
|
|
1125
|
+
if (id === void 0 || !ID.test(id) || rule === void 0 || where === void 0) return reject("incomplete");
|
|
1126
|
+
if (fingerprint === void 0 || createdAt === void 0 || updatedAt === void 0) return reject("incomplete");
|
|
1127
|
+
if (state === void 0 || source === void 0 || history === void 0) return reject("incomplete");
|
|
1128
|
+
const base = {
|
|
1129
|
+
schemaVersion: 1,
|
|
1130
|
+
id,
|
|
1131
|
+
rule,
|
|
1132
|
+
where,
|
|
1133
|
+
fingerprint,
|
|
1134
|
+
source,
|
|
1135
|
+
state,
|
|
1136
|
+
createdAt,
|
|
1137
|
+
updatedAt,
|
|
1138
|
+
history
|
|
1139
|
+
};
|
|
1140
|
+
return accept(firstCommit === void 0 ? base : { ...base, firstCommit });
|
|
1141
|
+
};
|
|
1142
|
+
var readIncident = (runtime, root, id) => {
|
|
1143
|
+
if (!ID.test(id)) return reject("not an incident id");
|
|
1144
|
+
const raw = runtime.readText(incidentFile(root, id));
|
|
1145
|
+
return raw.ok ? parseIncident(raw.value) : reject(raw.error);
|
|
1146
|
+
};
|
|
1147
|
+
var listIncidents = (runtime, root) => {
|
|
1148
|
+
const names = runtime.listDir(incidentsDir(root));
|
|
1149
|
+
if (!names.ok) return [];
|
|
1150
|
+
return names.value.filter((name) => name.endsWith(".json")).map((name) => readIncident(runtime, root, name.slice(0, -".json".length))).flatMap((parsed) => parsed.ok ? [parsed.value] : []).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1151
|
+
};
|
|
1152
|
+
var saveIncident = (runtime, root, incident) => {
|
|
1153
|
+
const ignore = runtime.writeText(toAbsolutePath(join3(incidentsDir(root), ".gitignore")), "*\n");
|
|
1154
|
+
if (!ignore.ok) return ignore;
|
|
1155
|
+
return runtime.writeText(incidentFile(root, incident.id), `${JSON.stringify(incident, null, 2)}
|
|
1156
|
+
`);
|
|
1157
|
+
};
|
|
1158
|
+
var created = (draft, now, by) => ({
|
|
1159
|
+
schemaVersion: 1,
|
|
1160
|
+
id: incidentId(draft.rule, draft.fingerprint),
|
|
1161
|
+
rule: draft.rule,
|
|
1162
|
+
where: draft.where,
|
|
1163
|
+
fingerprint: draft.fingerprint,
|
|
1164
|
+
source: draft.source,
|
|
1165
|
+
state: draft.state,
|
|
1166
|
+
createdAt: now,
|
|
1167
|
+
updatedAt: now,
|
|
1168
|
+
...draft.firstCommit === void 0 ? {} : { firstCommit: draft.firstCommit },
|
|
1169
|
+
history: [{ state: draft.state, at: now, by }]
|
|
1170
|
+
});
|
|
1171
|
+
var recordIncident = (runtime, root, draft, by) => {
|
|
1172
|
+
const now = runtime.now();
|
|
1173
|
+
const existing = readIncident(runtime, root, incidentId(draft.rule, draft.fingerprint));
|
|
1174
|
+
if (existing.ok && existing.value.state !== "fechado") return existing;
|
|
1175
|
+
const incident = existing.ok ? {
|
|
1176
|
+
...existing.value,
|
|
1177
|
+
state: draft.state,
|
|
1178
|
+
updatedAt: now,
|
|
1179
|
+
history: [...existing.value.history, { state: draft.state, at: now, by }]
|
|
1180
|
+
} : created(draft, now, by);
|
|
1181
|
+
const saved = saveIncident(runtime, root, incident);
|
|
1182
|
+
return saved.ok ? accept(incident) : reject(saved.error);
|
|
1183
|
+
};
|
|
1184
|
+
var ALLOWED_FROM = {
|
|
1185
|
+
aberto: [],
|
|
1186
|
+
"exposicao-local": [],
|
|
1187
|
+
rotacionado: ["aberto", "exposicao-local"],
|
|
1188
|
+
fechado: ["rotacionado"]
|
|
1189
|
+
};
|
|
1190
|
+
var canMove = (from, to) => ALLOWED_FROM[to].includes(from);
|
|
1191
|
+
var moveIncident = (incident, to, now, by) => ({
|
|
1192
|
+
...incident,
|
|
1193
|
+
state: to,
|
|
1194
|
+
updatedAt: now,
|
|
1195
|
+
history: [...incident.history, { state: to, at: now, by }]
|
|
1196
|
+
});
|
|
1197
|
+
var isRotated = (incident) => incident.state === "rotacionado" || incident.state === "fechado";
|
|
1198
|
+
|
|
1199
|
+
// src/secrets/guards/describe.ts
|
|
1200
|
+
var MAX_LISTED = 5;
|
|
1201
|
+
var describeFindings = (t, items) => {
|
|
1202
|
+
const listed2 = items.slice(0, MAX_LISTED).map(({ ruleId, where }) => t("secrets.found_item", { rule: ruleId, where }));
|
|
1203
|
+
const rest = items.length - listed2.length;
|
|
1204
|
+
return rest > 0 ? `${listed2.join("; ")}; ${t("secrets.found_more", { count: rest })}` : listed2.join("; ");
|
|
1205
|
+
};
|
|
1206
|
+
var lineWhere = (t, finding) => t("secrets.where.line", { line: finding.line, column: finding.column });
|
|
1207
|
+
|
|
1208
|
+
// src/secrets/guards/tool-input.ts
|
|
1209
|
+
var toolName = (payload) => optionalString(payload.raw, "tool_name") ?? "";
|
|
1210
|
+
var toolInput = (payload) => {
|
|
1211
|
+
const input = payload.raw["tool_input"];
|
|
1212
|
+
return isRecord(input) ? input : {};
|
|
1213
|
+
};
|
|
1214
|
+
var editTexts = (input, file) => {
|
|
1215
|
+
const edits = input["edits"];
|
|
1216
|
+
if (!Array.isArray(edits)) return [];
|
|
1217
|
+
return edits.flatMap((edit) => {
|
|
1218
|
+
if (!isRecord(edit)) return [];
|
|
1219
|
+
const text2 = optionalString(edit, "new_string");
|
|
1220
|
+
return text2 === void 0 ? [] : [{ file, text: text2, whole: false }];
|
|
1221
|
+
});
|
|
1222
|
+
};
|
|
1223
|
+
var writtenTexts = (tool, input) => {
|
|
1224
|
+
const file = optionalString(input, "file_path") ?? optionalString(input, "notebook_path") ?? "";
|
|
1225
|
+
const single = (key, whole) => {
|
|
1226
|
+
const text2 = optionalString(input, key);
|
|
1227
|
+
return text2 === void 0 ? [] : [{ file, text: text2, whole }];
|
|
1228
|
+
};
|
|
1229
|
+
switch (tool) {
|
|
1230
|
+
case "Write":
|
|
1231
|
+
return single("content", true);
|
|
1232
|
+
case "Edit":
|
|
1233
|
+
return single("new_string", false);
|
|
1234
|
+
case "MultiEdit":
|
|
1235
|
+
return editTexts(input, file);
|
|
1236
|
+
case "NotebookEdit":
|
|
1237
|
+
return single("new_source", false);
|
|
1238
|
+
default:
|
|
1239
|
+
return [];
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
var stringLeaves = (value, out = []) => {
|
|
1243
|
+
if (typeof value === "string") out.push(value);
|
|
1244
|
+
else if (Array.isArray(value)) for (const item of value) stringLeaves(item, out);
|
|
1245
|
+
else if (isRecord(value)) for (const item of Object.values(value)) stringLeaves(item, out);
|
|
1246
|
+
return out;
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
// src/secrets/guards/exposure.ts
|
|
1250
|
+
var HOOK_ACTOR = "finitemonkey";
|
|
1251
|
+
var liveFindings = (input, texts) => {
|
|
1252
|
+
const baseline = loadBaseline(input.runtime, input.cwd);
|
|
1253
|
+
const accepted2 = acceptedFingerprints(baseline.ok ? baseline.value : EMPTY_BASELINE, input.runtime.now());
|
|
1254
|
+
return texts.flatMap((text2) => scanText(text2, { rules: CREDENTIAL_RULES })).filter((finding) => !accepted2.has(finding.fingerprint));
|
|
1255
|
+
};
|
|
1256
|
+
var record = (input, findings, draft, where) => {
|
|
1257
|
+
const ids = findings.map((finding) => {
|
|
1258
|
+
const saved = recordIncident(
|
|
1259
|
+
input.runtime,
|
|
1260
|
+
input.cwd,
|
|
1261
|
+
{ ...draft, rule: finding.ruleId, fingerprint: finding.fingerprint, where: `${where}:${String(finding.line)}` },
|
|
1262
|
+
HOOK_ACTOR
|
|
1263
|
+
);
|
|
1264
|
+
return saved.ok ? saved.value.id : input.t("secrets.incident.unsaved", { detail: saved.error });
|
|
1265
|
+
});
|
|
1266
|
+
return [...new Set(ids)].join(", ");
|
|
1267
|
+
};
|
|
1268
|
+
var secretsUserPromptSubmit = (input) => {
|
|
1269
|
+
const prompt = optionalString(input.payload.raw, "prompt") ?? "";
|
|
1270
|
+
const findings = liveFindings(input, [prompt]);
|
|
1271
|
+
if (findings.length === 0) return { kind: "allow" };
|
|
1272
|
+
const incident = record(input, findings, { source: "prompt", state: "exposicao-local" }, "prompt");
|
|
1273
|
+
const items = findings.map((finding) => ({ ruleId: finding.ruleId, where: lineWhere(input.t, finding) }));
|
|
1274
|
+
return {
|
|
1275
|
+
kind: "block",
|
|
1276
|
+
reason: input.t("secrets.block.prompt", { found: describeFindings(input.t, items), incident })
|
|
1277
|
+
};
|
|
1278
|
+
};
|
|
1279
|
+
var secretsPostToolUse = (input) => {
|
|
1280
|
+
const findings = liveFindings(input, stringLeaves(input.payload.raw["tool_response"]));
|
|
1281
|
+
if (findings.length === 0) return { kind: "allow" };
|
|
1282
|
+
const tool = toolName(input.payload) || "tool";
|
|
1283
|
+
const incident = record(input, findings, { source: "tool-output", state: "aberto" }, `tool-output:${tool}`);
|
|
1284
|
+
const items = findings.map((finding) => ({ ruleId: finding.ruleId, where: lineWhere(input.t, finding) }));
|
|
1285
|
+
const context = input.t("secrets.post.context", { tool, found: describeFindings(input.t, items), incident });
|
|
1286
|
+
return {
|
|
1287
|
+
kind: "allow",
|
|
1288
|
+
output: { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: context } }
|
|
1289
|
+
};
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
// src/secrets/guards/pre-tool.ts
|
|
1293
|
+
import { relative as relative3 } from "node:path";
|
|
1294
|
+
|
|
1295
|
+
// src/secrets/scan.ts
|
|
1296
|
+
import { join as join4, relative as relative2 } from "node:path";
|
|
1297
|
+
|
|
1298
|
+
// src/secrets/located.ts
|
|
1299
|
+
var findingId = (ruleId, file, fingerprint) => `fm-${sha256Hex(`${ruleId}\0${file}\0${fingerprint}`).slice(0, 16)}`;
|
|
1300
|
+
var locate = (finding, rawFile, line, commit) => {
|
|
1301
|
+
const file = toPosixPath(rawFile);
|
|
1302
|
+
return {
|
|
1303
|
+
id: findingId(finding.ruleId, file, finding.fingerprint),
|
|
1304
|
+
ruleId: finding.ruleId,
|
|
1305
|
+
severity: finding.severity,
|
|
1306
|
+
file,
|
|
1307
|
+
line,
|
|
1308
|
+
column: finding.column,
|
|
1309
|
+
fingerprint: finding.fingerprint,
|
|
1310
|
+
tail: finding.tail,
|
|
1311
|
+
...commit === void 0 ? {} : { commit }
|
|
1312
|
+
};
|
|
1313
|
+
};
|
|
1314
|
+
var whereOf = (finding) => `${finding.file}:${String(finding.line)}`;
|
|
1315
|
+
|
|
1316
|
+
// src/secrets/diff.ts
|
|
1317
|
+
var COMMIT = /^commit ([0-9a-f]{7,64})\b/;
|
|
1318
|
+
var HUNK = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
|
1319
|
+
var DEV_NULL = "/dev/null";
|
|
1320
|
+
var fresh = () => ({ file: void 0, inHunk: false, next: 0, text: [], lines: [] });
|
|
1321
|
+
var targetOf = (line) => {
|
|
1322
|
+
const path = line.slice(4).replace(/\t.*$/, "");
|
|
1323
|
+
if (path === DEV_NULL) return void 0;
|
|
1324
|
+
return path.startsWith("b/") ? path.slice(2) : path;
|
|
1325
|
+
};
|
|
1326
|
+
var collect = (chunk, commit, found) => {
|
|
1327
|
+
const { file } = chunk;
|
|
1328
|
+
if (file === void 0 || chunk.text.length === 0) return;
|
|
1329
|
+
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);
|
|
1331
|
+
found.set(located.id, located);
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
var addLine = (chunk, line) => {
|
|
1335
|
+
const hunk = HUNK.exec(line);
|
|
1336
|
+
if (hunk !== null) {
|
|
1337
|
+
chunk.inHunk = true;
|
|
1338
|
+
chunk.next = Number(hunk[1]);
|
|
1339
|
+
} else if (!chunk.inHunk && line.startsWith("+++ ")) {
|
|
1340
|
+
chunk.file = targetOf(line);
|
|
1341
|
+
} else if (chunk.inHunk && line.startsWith("+")) {
|
|
1342
|
+
chunk.text.push(line.slice(1));
|
|
1343
|
+
chunk.lines.push(chunk.next);
|
|
1344
|
+
chunk.next += 1;
|
|
1345
|
+
} else if (chunk.inHunk && line.startsWith(" ")) {
|
|
1346
|
+
chunk.next += 1;
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
var diffScanner = () => {
|
|
1350
|
+
const found = /* @__PURE__ */ new Map();
|
|
1351
|
+
let commit;
|
|
1352
|
+
let chunk = fresh();
|
|
1353
|
+
const flush2 = () => {
|
|
1354
|
+
collect(chunk, commit, found);
|
|
1355
|
+
chunk = fresh();
|
|
1356
|
+
};
|
|
1357
|
+
const header = (line) => {
|
|
1358
|
+
const match = COMMIT.exec(line);
|
|
1359
|
+
if (match !== null) {
|
|
1360
|
+
flush2();
|
|
1361
|
+
commit = match[1];
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
if (line.startsWith("diff --git ")) {
|
|
1365
|
+
flush2();
|
|
1366
|
+
return true;
|
|
1367
|
+
}
|
|
1368
|
+
return false;
|
|
1369
|
+
};
|
|
1370
|
+
return {
|
|
1371
|
+
push: (raw) => {
|
|
1372
|
+
const line = withoutCarriageReturn(raw);
|
|
1373
|
+
if (!header(line)) addLine(chunk, line);
|
|
1374
|
+
},
|
|
1375
|
+
end: () => {
|
|
1376
|
+
flush2();
|
|
1377
|
+
return [...found.values()];
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
// src/secrets/scan.ts
|
|
1383
|
+
var MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
1384
|
+
var SKIPPED = /(?:^|\/)(?:\.git|node_modules|\.finitemonkey)\//;
|
|
1385
|
+
var DIFF_FLAGS = ["-U0", "--no-color", "--no-ext-diff", "--no-renames"];
|
|
1386
|
+
var binary = (text2) => text2.slice(0, 8e3).includes("\0");
|
|
1387
|
+
var listed = (runtime, root, target) => {
|
|
1388
|
+
const git = runtime.git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", target], root);
|
|
1389
|
+
if (git.ok)
|
|
1390
|
+
return accept(
|
|
1391
|
+
git.stdout.split("\0").filter((name) => name !== "").map(toPosixPath)
|
|
1392
|
+
);
|
|
1393
|
+
const base = toAbsolutePath(target, root);
|
|
1394
|
+
const walked = runtime.walkFiles(base);
|
|
1395
|
+
if (!walked.ok) return walked;
|
|
1396
|
+
return accept(walked.value.map((name) => toPosixPath(relative2(root, join4(base, toPosixPath(name))))));
|
|
1397
|
+
};
|
|
1398
|
+
var scanFile = (runtime, root, file) => {
|
|
1399
|
+
const text2 = runtime.readText(toAbsolutePath(file, root));
|
|
1400
|
+
if (!text2.ok || text2.value.length > MAX_FILE_BYTES || binary(text2.value)) return [];
|
|
1401
|
+
return scanText(text2.value, { path: file }).map((finding) => locate(finding, file, finding.line));
|
|
1402
|
+
};
|
|
1403
|
+
var scanPath = (runtime, root, target) => {
|
|
1404
|
+
const files = listed(runtime, root, target);
|
|
1405
|
+
if (!files.ok) return files;
|
|
1406
|
+
return accept(files.value.filter((file) => !SKIPPED.test(file)).flatMap((file) => scanFile(runtime, root, file)));
|
|
1407
|
+
};
|
|
1408
|
+
var diffFindings = (diff) => {
|
|
1409
|
+
const scanner = diffScanner();
|
|
1410
|
+
for (const line of diff.split("\n")) scanner.push(line);
|
|
1411
|
+
return scanner.end().filter((finding) => !SKIPPED.test(finding.file));
|
|
1412
|
+
};
|
|
1413
|
+
var scanStaged = (runtime, root, includeUnstaged = false) => {
|
|
1414
|
+
const args = includeUnstaged ? ["diff", "HEAD", ...DIFF_FLAGS] : ["diff", "--cached", ...DIFF_FLAGS];
|
|
1415
|
+
const git = runtime.git(args, root);
|
|
1416
|
+
if (!git.ok) return reject(git.found ? "git diff failed" : "git not found");
|
|
1417
|
+
return accept(diffFindings(git.stdout));
|
|
1418
|
+
};
|
|
1419
|
+
var scanHistory = async (runtime, root) => {
|
|
1420
|
+
const scanner = diffScanner();
|
|
1421
|
+
const args = ["log", "-p", "--all", "--format=commit %H", ...DIFF_FLAGS];
|
|
1422
|
+
const git = await runtime.gitLines(args, root, scanner.push);
|
|
1423
|
+
if (!git.ok) return reject(git.found ? "git log failed" : "git not found");
|
|
1424
|
+
return accept(scanner.end().filter((finding) => !SKIPPED.test(finding.file)));
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
// src/secrets/guards/paths.ts
|
|
1428
|
+
var SAFE_ENV = /^\.env\.(?:example|sample|template|dist|defaults|schema)$/i;
|
|
1429
|
+
var ENV_FILE = /^\.env(?:\..+)?$/i;
|
|
1430
|
+
var KEY_FILE = /\.(?:pem|key|p12|pfx)$/i;
|
|
1431
|
+
var SSH_KEY = /^id_(?:rsa|dsa|ecdsa|ed25519)(?:_[A-Za-z0-9_-]+)?$/;
|
|
1432
|
+
var basename = (path) => path.split(/[\\/]/).pop() ?? path;
|
|
1433
|
+
var isSensitivePath = (path) => {
|
|
1434
|
+
const name = basename(path.replace(/^["']|["']$/g, ""));
|
|
1435
|
+
if (ENV_FILE.test(name)) return !SAFE_ENV.test(name);
|
|
1436
|
+
return KEY_FILE.test(name) || SSH_KEY.test(name);
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
// src/secrets/guards/bash-rules.ts
|
|
1440
|
+
var READERS = /* @__PURE__ */ new Set([
|
|
1441
|
+
"cat",
|
|
1442
|
+
"bat",
|
|
1443
|
+
"batcat",
|
|
1444
|
+
"less",
|
|
1445
|
+
"more",
|
|
1446
|
+
"head",
|
|
1447
|
+
"tail",
|
|
1448
|
+
"nl",
|
|
1449
|
+
"tac",
|
|
1450
|
+
"strings",
|
|
1451
|
+
"xxd",
|
|
1452
|
+
"hexdump",
|
|
1453
|
+
"od",
|
|
1454
|
+
"grep",
|
|
1455
|
+
"egrep",
|
|
1456
|
+
"fgrep",
|
|
1457
|
+
"rg",
|
|
1458
|
+
"ag",
|
|
1459
|
+
"ack",
|
|
1460
|
+
"awk",
|
|
1461
|
+
"gawk",
|
|
1462
|
+
"sed",
|
|
1463
|
+
"cut",
|
|
1464
|
+
"sort",
|
|
1465
|
+
"uniq",
|
|
1466
|
+
"jq",
|
|
1467
|
+
"yq",
|
|
1468
|
+
"base64",
|
|
1469
|
+
"type",
|
|
1470
|
+
"get-content",
|
|
1471
|
+
"gc",
|
|
1472
|
+
"select-string",
|
|
1473
|
+
"sls",
|
|
1474
|
+
"diff",
|
|
1475
|
+
"vim",
|
|
1476
|
+
"vi",
|
|
1477
|
+
"nano",
|
|
1478
|
+
"view",
|
|
1479
|
+
"dotenv"
|
|
1480
|
+
]);
|
|
1481
|
+
var SECRET_NAME = /(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)/i;
|
|
1482
|
+
var PERSON_ONLY = /\b(?:finitemonkey|fm)(?:\.mjs)?\s+secrets\s+(?:accept\b|incident\s+(?:rotated|close)\b)/;
|
|
1483
|
+
var LISTS_ENV = /* @__PURE__ */ new Set(["get-childitem", "gci", "dir", "ls"]);
|
|
1484
|
+
var onlyFlags = (args) => args.every((arg) => arg.startsWith("-"));
|
|
1485
|
+
var readTarget = (segment) => {
|
|
1486
|
+
const redirected = segment.redirectedFrom.find(isSensitivePath);
|
|
1487
|
+
if (redirected !== void 0) return redirected;
|
|
1488
|
+
const program = segment.program.toLowerCase();
|
|
1489
|
+
if (!READERS.has(program)) return void 0;
|
|
1490
|
+
if (program === "sed" && segment.args.some((arg) => arg.startsWith("-i"))) return void 0;
|
|
1491
|
+
return segment.args.find((arg) => !arg.startsWith("-") && isSensitivePath(arg));
|
|
1492
|
+
};
|
|
1493
|
+
var dumpsEnv = (segment) => {
|
|
1494
|
+
const program = segment.program.toLowerCase();
|
|
1495
|
+
if (program === "env" || program === "printenv") {
|
|
1496
|
+
if (onlyFlags(segment.args)) return true;
|
|
1497
|
+
return program === "printenv" && segment.args.some((arg) => SECRET_NAME.test(arg));
|
|
1498
|
+
}
|
|
1499
|
+
if (program === "set" || program === "export" || program === "declare") {
|
|
1500
|
+
return segment.args.length === 0 || segment.args.every((arg) => arg === "-p" || arg === "-x");
|
|
1501
|
+
}
|
|
1502
|
+
return LISTS_ENV.has(program) && segment.args.some((arg) => /^env:/i.test(arg));
|
|
1503
|
+
};
|
|
1504
|
+
var bashFindings = (command, segments) => {
|
|
1505
|
+
const person = PERSON_ONLY.exec(command);
|
|
1506
|
+
if (person !== null) return [{ kind: "person-only", command: person[0] }];
|
|
1507
|
+
return segments.flatMap((segment) => {
|
|
1508
|
+
const target = readTarget(segment);
|
|
1509
|
+
if (target !== void 0) return [{ kind: "read", target }];
|
|
1510
|
+
if (dumpsEnv(segment)) return [{ kind: "env-dump", command: [segment.program, ...segment.args].join(" ") }];
|
|
1511
|
+
return [];
|
|
1512
|
+
});
|
|
1513
|
+
};
|
|
1514
|
+
var GIT_GLOBALS_WITH_VALUE = /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree"]);
|
|
1515
|
+
var commitIntent = (segments) => {
|
|
1516
|
+
for (const segment of segments) {
|
|
1517
|
+
if (segment.program !== "git") continue;
|
|
1518
|
+
let index = 0;
|
|
1519
|
+
while (index < segment.args.length && (segment.args[index] ?? "").startsWith("-")) {
|
|
1520
|
+
index += GIT_GLOBALS_WITH_VALUE.has(segment.args[index] ?? "") ? 2 : 1;
|
|
1521
|
+
}
|
|
1522
|
+
if (segment.args[index] !== "commit") continue;
|
|
1523
|
+
const flags = segment.args.slice(index + 1);
|
|
1524
|
+
const all = flags.some((flag2) => flag2 === "--all" || /^-[A-Za-z]*a[A-Za-z]*$/.test(flag2));
|
|
1525
|
+
return { includeUnstaged: all };
|
|
1526
|
+
}
|
|
1527
|
+
return void 0;
|
|
1528
|
+
};
|
|
1529
|
+
|
|
1530
|
+
// src/secrets/guards/shell.ts
|
|
1531
|
+
var SEPARATORS = /\|\||&&|[|;&\n]/;
|
|
1532
|
+
var TOKEN = /"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g;
|
|
1533
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
1534
|
+
var OUTPUT_REDIRECT = /^(?:\d|&)?>{1,2}\|?$/;
|
|
1535
|
+
var OUTPUT_ATTACHED = /^(?:\d|&)?>{1,2}\|?\S/;
|
|
1536
|
+
var WRAPPERS = /* @__PURE__ */ new Set(["sudo", "command", "exec", "time", "nice", "nohup", "builtin"]);
|
|
1537
|
+
var tokensOf = (text2) => Array.from(text2.matchAll(TOKEN), (match) => match[1] ?? match[2] ?? match[3] ?? "");
|
|
1538
|
+
var splitRedirects = (tokens) => {
|
|
1539
|
+
const words = [];
|
|
1540
|
+
const inputs = [];
|
|
1541
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1542
|
+
const token = tokens[index] ?? "";
|
|
1543
|
+
if (OUTPUT_REDIRECT.test(token)) {
|
|
1544
|
+
index += 1;
|
|
1545
|
+
} else if (OUTPUT_ATTACHED.test(token)) {
|
|
1546
|
+
continue;
|
|
1547
|
+
} else if (token === "<") {
|
|
1548
|
+
inputs.push(tokens[index + 1] ?? "");
|
|
1549
|
+
index += 1;
|
|
1550
|
+
} else if (token.startsWith("<") && !token.startsWith("<<")) {
|
|
1551
|
+
inputs.push(token.slice(1));
|
|
1552
|
+
} else {
|
|
1553
|
+
words.push(token);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return { words, inputs };
|
|
1557
|
+
};
|
|
1558
|
+
var segmentOf = (text2) => {
|
|
1559
|
+
const { words, inputs } = splitRedirects(tokensOf(text2));
|
|
1560
|
+
let index = 0;
|
|
1561
|
+
while (index < words.length && (ASSIGNMENT.test(words[index] ?? "") || WRAPPERS.has(words[index] ?? ""))) index += 1;
|
|
1562
|
+
const program = words[index];
|
|
1563
|
+
if (program === void 0) return inputs.length > 0 ? { program: "", args: [], redirectedFrom: inputs } : void 0;
|
|
1564
|
+
return { program: program.split(/[\\/]/).pop() ?? program, args: words.slice(index + 1), redirectedFrom: inputs };
|
|
1565
|
+
};
|
|
1566
|
+
var segmentsOf = (command) => command.split(SEPARATORS).flatMap((part) => {
|
|
1567
|
+
const segment = segmentOf(part);
|
|
1568
|
+
return segment === void 0 ? [] : [segment];
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
// src/secrets/guards/pre-tool.ts
|
|
1572
|
+
var ALLOW = { kind: "allow" };
|
|
1573
|
+
var relativeFile = (cwd, file) => {
|
|
1574
|
+
if (file === "") return file;
|
|
1575
|
+
const rel = relative3(cwd, file).split("\\").join("/");
|
|
1576
|
+
return rel.startsWith("..") || rel === "" ? file : rel;
|
|
1577
|
+
};
|
|
1578
|
+
var writeDecision = (input, tool) => {
|
|
1579
|
+
const { t } = input;
|
|
1580
|
+
for (const written of writtenTexts(tool, toolInput(input.payload))) {
|
|
1581
|
+
const file = relativeFile(input.cwd, written.file);
|
|
1582
|
+
const found = scanText(written.text, { path: file });
|
|
1583
|
+
if (found.length === 0) continue;
|
|
1584
|
+
const items = found.map((finding) => ({
|
|
1585
|
+
ruleId: finding.ruleId,
|
|
1586
|
+
where: written.whole ? t("secrets.where.file", { file, line: finding.line, column: finding.column }) : t("secrets.where.new_text", { file, line: finding.line })
|
|
1587
|
+
}));
|
|
1588
|
+
return { kind: "block", reason: t("secrets.block.write", { file, found: describeFindings(t, items) }) };
|
|
1589
|
+
}
|
|
1590
|
+
return ALLOW;
|
|
1591
|
+
};
|
|
1592
|
+
var bashMessage = (t, finding) => {
|
|
1593
|
+
switch (finding.kind) {
|
|
1594
|
+
case "read":
|
|
1595
|
+
return t("secrets.block.read", { target: finding.target });
|
|
1596
|
+
case "env-dump":
|
|
1597
|
+
return t("secrets.block.env_dump", { command: finding.command });
|
|
1598
|
+
case "person-only":
|
|
1599
|
+
return t("secrets.block.person_only", { command: finding.command });
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
var commitDecision = (input, includeUnstaged) => {
|
|
1603
|
+
const staged = scanStaged(input.runtime, input.cwd, includeUnstaged);
|
|
1604
|
+
if (!staged.ok || staged.value.length === 0) return ALLOW;
|
|
1605
|
+
const items = staged.value.map((finding) => ({
|
|
1606
|
+
ruleId: finding.ruleId,
|
|
1607
|
+
where: input.t("secrets.where.file", { file: finding.file, line: finding.line, column: finding.column })
|
|
1608
|
+
}));
|
|
1609
|
+
return { kind: "block", reason: input.t("secrets.block.commit", { found: describeFindings(input.t, items) }) };
|
|
1610
|
+
};
|
|
1611
|
+
var bashDecision = (input) => {
|
|
1612
|
+
const command = optionalString(toolInput(input.payload), "command") ?? "";
|
|
1613
|
+
if (command === "") return ALLOW;
|
|
1614
|
+
const segments = segmentsOf(command);
|
|
1615
|
+
const [first] = bashFindings(command, segments);
|
|
1616
|
+
if (first !== void 0) return { kind: "block", reason: bashMessage(input.t, first) };
|
|
1617
|
+
const found = scanText(command);
|
|
1618
|
+
if (found.length > 0) {
|
|
1619
|
+
const items = found.map((finding) => ({ ruleId: finding.ruleId, where: lineWhere(input.t, finding) }));
|
|
1620
|
+
return { kind: "block", reason: input.t("secrets.block.bash", { found: describeFindings(input.t, items) }) };
|
|
1621
|
+
}
|
|
1622
|
+
const commit = commitIntent(segments);
|
|
1623
|
+
return commit === void 0 ? ALLOW : commitDecision(input, commit.includeUnstaged);
|
|
1624
|
+
};
|
|
1625
|
+
var readDecision = (input) => {
|
|
1626
|
+
const inputs = toolInput(input.payload);
|
|
1627
|
+
const target = optionalString(inputs, "file_path") ?? optionalString(inputs, "path") ?? "";
|
|
1628
|
+
if (target === "" || !isSensitivePath(target)) return ALLOW;
|
|
1629
|
+
return { kind: "block", reason: input.t("secrets.block.read", { target: relativeFile(input.cwd, target) }) };
|
|
1630
|
+
};
|
|
1631
|
+
var WRITERS = /* @__PURE__ */ new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
1632
|
+
var SHELLS = /* @__PURE__ */ new Set(["Bash", "PowerShell"]);
|
|
1633
|
+
var READS = /* @__PURE__ */ new Set(["Read", "Grep"]);
|
|
1634
|
+
var secretsPreToolUse = (input) => {
|
|
1635
|
+
const tool = toolName(input.payload);
|
|
1636
|
+
if (WRITERS.has(tool)) return writeDecision(input, tool);
|
|
1637
|
+
if (SHELLS.has(tool)) return bashDecision(input);
|
|
1638
|
+
if (READS.has(tool)) return readDecision(input);
|
|
1639
|
+
return ALLOW;
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
// src/hooks/handlers.ts
|
|
1643
|
+
var allow = () => ({ kind: "allow" });
|
|
1644
|
+
var DEFAULT_HANDLERS = {
|
|
1645
|
+
PreToolUse: [secretsPreToolUse],
|
|
1646
|
+
UserPromptSubmit: [secretsUserPromptSubmit],
|
|
1647
|
+
PostToolUse: [secretsPostToolUse]
|
|
1648
|
+
};
|
|
1649
|
+
|
|
1650
|
+
// src/hooks/payload.ts
|
|
1651
|
+
var EMPTY_PAYLOAD = {
|
|
1652
|
+
hookEventName: void 0,
|
|
1653
|
+
sessionId: void 0,
|
|
1654
|
+
cwd: void 0,
|
|
1655
|
+
stopHookActive: false,
|
|
1656
|
+
raw: {}
|
|
1657
|
+
};
|
|
1658
|
+
var parseHookPayload = (event, text2, interactive) => {
|
|
1659
|
+
if (interactive && text2.trim() === "") return accept(EMPTY_PAYLOAD);
|
|
1660
|
+
const parsed = parseJsonObject(text2);
|
|
1661
|
+
if (!parsed.ok) return reject(`stdin: ${parsed.error}`);
|
|
1662
|
+
const payload = {
|
|
1663
|
+
hookEventName: optionalString(parsed.value, "hook_event_name"),
|
|
1664
|
+
sessionId: optionalString(parsed.value, "session_id"),
|
|
1665
|
+
cwd: optionalString(parsed.value, "cwd"),
|
|
1666
|
+
stopHookActive: parsed.value["stop_hook_active"] === true,
|
|
1667
|
+
raw: parsed.value
|
|
1668
|
+
};
|
|
1669
|
+
if (payload.hookEventName !== void 0 && payload.hookEventName !== event) {
|
|
1670
|
+
return reject(`payload is for ${payload.hookEventName}, not ${event}`);
|
|
1671
|
+
}
|
|
1672
|
+
return accept(payload);
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1675
|
+
// src/hooks/stdin.ts
|
|
1676
|
+
var readAll = (source) => new Promise((resolve2, reject2) => {
|
|
1677
|
+
if (source.isTTY === true) {
|
|
1678
|
+
resolve2("");
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const chunks = [];
|
|
1682
|
+
source.setEncoding?.("utf8");
|
|
1683
|
+
source.on("data", (chunk) => {
|
|
1684
|
+
chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
1685
|
+
});
|
|
1686
|
+
source.on("end", () => {
|
|
1687
|
+
resolve2(chunks.join(""));
|
|
1688
|
+
});
|
|
1689
|
+
source.on("error", reject2);
|
|
1690
|
+
});
|
|
1691
|
+
var DeadlineError = class extends Error {
|
|
1692
|
+
ms;
|
|
1693
|
+
constructor(ms) {
|
|
1694
|
+
super(`deadline of ${String(ms)} ms exceeded`);
|
|
1695
|
+
this.name = "DeadlineError";
|
|
1696
|
+
this.ms = ms;
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
var withDeadline = (work, ms, startTimer) => new Promise((resolve2, reject2) => {
|
|
1700
|
+
const cancel = startTimer(ms, () => {
|
|
1701
|
+
reject2(new DeadlineError(ms));
|
|
1702
|
+
});
|
|
1703
|
+
work.then(
|
|
1704
|
+
(value) => {
|
|
1705
|
+
cancel();
|
|
1706
|
+
resolve2(value);
|
|
1707
|
+
},
|
|
1708
|
+
(error) => {
|
|
1709
|
+
cancel();
|
|
1710
|
+
reject2(error instanceof Error ? error : new Error(String(error)));
|
|
1711
|
+
}
|
|
1712
|
+
);
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
// src/hooks/runner.ts
|
|
1716
|
+
var messageOf2 = (error) => error instanceof Error ? error.message : String(error);
|
|
1717
|
+
var decide = async (input, handlers) => {
|
|
1718
|
+
let output = {};
|
|
1719
|
+
for (const handler of handlers[input.event] ?? [allow]) {
|
|
1720
|
+
const decision = await handler(input);
|
|
1721
|
+
if (decision.kind === "block") return decision;
|
|
1722
|
+
output = { ...output, ...decision.output };
|
|
1723
|
+
}
|
|
1724
|
+
return { kind: "allow", output };
|
|
1725
|
+
};
|
|
1726
|
+
var evaluate = async (event, options) => {
|
|
1727
|
+
const text2 = await readAll(options.stdin);
|
|
1728
|
+
const parsed = parseHookPayload(event, text2, options.stdin.isTTY === true);
|
|
1729
|
+
if (!parsed.ok) return { kind: "failed", payload: void 0, detail: parsed.error };
|
|
1730
|
+
const payload = parsed.value;
|
|
1731
|
+
try {
|
|
1732
|
+
return {
|
|
1733
|
+
kind: "decided",
|
|
1734
|
+
payload,
|
|
1735
|
+
decision: await decide(
|
|
1736
|
+
{ event, payload, cwd: options.cwd, runtime: options.runtime, t: options.t },
|
|
1737
|
+
options.handlers
|
|
1738
|
+
)
|
|
1739
|
+
};
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
return { kind: "failed", payload, detail: messageOf2(error) };
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
var ALLOW_STDOUT = `${JSON.stringify({ continue: true })}
|
|
1745
|
+
`;
|
|
1746
|
+
var allowed = (output) => ({
|
|
1747
|
+
exitCode: 0,
|
|
1748
|
+
stdout: `${JSON.stringify({ continue: true, ...output })}
|
|
1749
|
+
`,
|
|
1750
|
+
stderr: "",
|
|
1751
|
+
verdict: "allow"
|
|
1752
|
+
});
|
|
1753
|
+
var released = (event, detail, t) => ({
|
|
1754
|
+
exitCode: 0,
|
|
1755
|
+
stdout: ALLOW_STDOUT,
|
|
1756
|
+
stderr: `finitemonkey: ${t("hook.stop_released", { event, detail })}
|
|
1757
|
+
`,
|
|
1758
|
+
verdict: "stop_released"
|
|
1759
|
+
});
|
|
1760
|
+
var failed = (event, detail, t) => HOOK_POLICIES[event].failClosed ? {
|
|
1761
|
+
exitCode: 2,
|
|
1762
|
+
stdout: "",
|
|
1763
|
+
stderr: `finitemonkey: ${t("hook.failed_closed", { event, detail })}
|
|
1764
|
+
`,
|
|
1765
|
+
verdict: "failed_closed"
|
|
1766
|
+
} : {
|
|
1767
|
+
exitCode: 0,
|
|
1768
|
+
stdout: "",
|
|
1769
|
+
stderr: `finitemonkey: ${t("hook.failed_open", { event, detail })}
|
|
1770
|
+
`,
|
|
1771
|
+
verdict: "failed_open"
|
|
1772
|
+
};
|
|
1773
|
+
var conclude = (event, evaluation, t) => {
|
|
1774
|
+
const stopActive = evaluation.payload?.stopHookActive === true;
|
|
1775
|
+
if (evaluation.kind === "failed") {
|
|
1776
|
+
return stopActive ? released(event, evaluation.detail, t) : failed(event, evaluation.detail, t);
|
|
1777
|
+
}
|
|
1778
|
+
const { decision } = evaluation;
|
|
1779
|
+
if (decision.kind === "allow") return allowed(decision.output);
|
|
1780
|
+
if (stopActive) return released(event, decision.reason, t);
|
|
1781
|
+
return { exitCode: 2, stdout: "", stderr: `finitemonkey: ${decision.reason}
|
|
1782
|
+
`, verdict: "block" };
|
|
1783
|
+
};
|
|
1784
|
+
var runHook = async (event, options) => {
|
|
1785
|
+
try {
|
|
1786
|
+
return conclude(
|
|
1787
|
+
event,
|
|
1788
|
+
await withDeadline(evaluate(event, options), options.deadlineMs, options.startTimer),
|
|
1789
|
+
options.t
|
|
1790
|
+
);
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
return failed(event, messageOf2(error), options.t);
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1796
|
+
// src/commands/hook.ts
|
|
1797
|
+
var hookCommand = {
|
|
1798
|
+
path: ["hook"],
|
|
1799
|
+
summaryKey: "command.hook.summary",
|
|
1800
|
+
usage: `finitemonkey hook <${HOOK_EVENTS.join("|")}>`,
|
|
1801
|
+
status: "ready",
|
|
1802
|
+
maxPositionals: 1,
|
|
1803
|
+
run: async (ctx, args) => {
|
|
1804
|
+
const raw = args.positionals[0] ?? "";
|
|
1805
|
+
const event = parseHookEvent(raw);
|
|
1806
|
+
if (event === void 0) {
|
|
1807
|
+
const params = { event: raw, events: HOOK_EVENTS.join(", ") };
|
|
1808
|
+
return failureFrom(
|
|
1809
|
+
ctx.t,
|
|
1810
|
+
usageProblem("unknown_hook_event", "error.unknown_hook_event", "finitemonkey help hook", params)
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
const outcome = await runHook(event, {
|
|
1814
|
+
stdin: ctx.io.stdin,
|
|
1815
|
+
deadlineMs: parseDeadline(ctx.env["FINITEMONKEY_HOOK_DEADLINE_MS"]),
|
|
1816
|
+
handlers: DEFAULT_HANDLERS,
|
|
1817
|
+
cwd: ctx.cwd,
|
|
1818
|
+
t: ctx.t,
|
|
1819
|
+
startTimer: ctx.runtime.startTimer,
|
|
1820
|
+
runtime: ctx.runtime
|
|
1821
|
+
});
|
|
1822
|
+
const data = { event, verdict: outcome.verdict };
|
|
1823
|
+
const output = { stdout: outcome.stdout, stderr: outcome.stderr };
|
|
1824
|
+
if (outcome.exitCode === 0) return { ok: true, exitCode: 0, data, raw: output };
|
|
1825
|
+
const message = outcome.stderr.trim();
|
|
1826
|
+
return {
|
|
1827
|
+
ok: false,
|
|
1828
|
+
exitCode: 2,
|
|
1829
|
+
data,
|
|
1830
|
+
raw: output,
|
|
1831
|
+
error: { code: "hook_blocked", message, fix: "finitemonkey doctor" }
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
|
|
1836
|
+
// src/commands/i18n-check.ts
|
|
1837
|
+
import { join as join5 } from "node:path";
|
|
1838
|
+
|
|
1839
|
+
// src/i18n/en.json
|
|
1840
|
+
var en_default = {
|
|
1841
|
+
"command.init.summary": "Set up Finite Monkey in this project",
|
|
1842
|
+
"command.doctor.summary": "Check the environment and say how to fix each problem",
|
|
1843
|
+
"command.update.summary": "Update the files Finite Monkey generated",
|
|
1844
|
+
"command.uninstall.summary": "Remove Finite Monkey from this project",
|
|
1845
|
+
"command.i18n_check.summary": "Check that the pt-BR, en and es catalogs match",
|
|
1846
|
+
"command.hook.summary": "Answer a Claude Code hook event (reads JSON on stdin)",
|
|
1847
|
+
"command.version.summary": "Print the version",
|
|
1848
|
+
"command.help.summary": "Show commands and flags",
|
|
1849
|
+
"help.tagline": "Infinite monkeys not required.",
|
|
1850
|
+
"help.usage": "Usage",
|
|
1851
|
+
"help.command": "command",
|
|
1852
|
+
"help.commands": "Commands",
|
|
1853
|
+
"help.flags": "Flags",
|
|
1854
|
+
"help.options": "Options",
|
|
1855
|
+
"help.planned": "(not available yet)",
|
|
1856
|
+
"help.more": "Run `finitemonkey help <command>` for details. Add --json for machine-readable output.",
|
|
1857
|
+
"flag.json": "Print one JSON object (schemaVersion 1) instead of text",
|
|
1858
|
+
"flag.lang": "Language for messages",
|
|
1859
|
+
"flag.yes": "Answer yes to confirmations",
|
|
1860
|
+
"flag.dry_run": "Show what would change without changing anything",
|
|
1861
|
+
"flag.version": "Print the version",
|
|
1862
|
+
"flag.help": "Show help",
|
|
1863
|
+
"output.error": "error: {message}",
|
|
1864
|
+
"output.fix": "fix: {fix}",
|
|
1865
|
+
"output.next": "next: {next}",
|
|
1866
|
+
"error.internal": "Finite Monkey failed unexpectedly: {detail}",
|
|
1867
|
+
"error.invalid_arguments": "Invalid arguments: {detail}",
|
|
1868
|
+
"error.unexpected_argument": 'Unexpected argument "{argument}"',
|
|
1869
|
+
"error.unknown_command": 'Unknown command "{command}"',
|
|
1870
|
+
"error.unknown_command_suggest": 'Unknown command "{command}". Did you mean "{suggestion}"?',
|
|
1871
|
+
"error.not_implemented": '"{command}" is not available in this version yet',
|
|
1872
|
+
"error.invalid_lang": 'Unsupported language "{lang}". Use one of: {languages}',
|
|
1873
|
+
"error.i18n_catalog_unreadable": "Could not read catalog {file}: {detail}",
|
|
1874
|
+
"error.i18n_mismatch": "{count} catalog problem(s) found",
|
|
1875
|
+
"error.doctor_failed": "{count} check(s) failed",
|
|
1876
|
+
"error.unknown_hook_event": 'Unknown hook event "{event}". Use one of: {events}',
|
|
1877
|
+
"i18n.summary": "Languages: {languages}. Keys: {keys}.",
|
|
1878
|
+
"i18n.ok": "All catalogs match.",
|
|
1879
|
+
"i18n.fix": "Edit the catalogs in {file} so every language has the same keys and placeholders",
|
|
1880
|
+
"i18n.problem.missing": "missing key",
|
|
1881
|
+
"i18n.problem.extra": "key not in en.json",
|
|
1882
|
+
"i18n.problem.placeholder": "placeholders differ (expected: {expected}; found: {actual})",
|
|
1883
|
+
"i18n.problem.empty": "empty text",
|
|
1884
|
+
"i18n.problem.not_string": "value is not text",
|
|
1885
|
+
"doctor.title": "finitemonkey doctor ({version})",
|
|
1886
|
+
"doctor.summary": "{ok} ok, {warn} warning(s), {fail} failure(s)",
|
|
1887
|
+
"doctor.node.ok": "Node.js {version}",
|
|
1888
|
+
"doctor.node.old": "Node.js {version} is older than the minimum {min}",
|
|
1889
|
+
"doctor.node.fix": "Install Node.js 20 or newer from https://nodejs.org",
|
|
1890
|
+
"doctor.git.ok": "git {version}",
|
|
1891
|
+
"doctor.git.missing": "git was not found",
|
|
1892
|
+
"doctor.git.fix": "Install git from https://git-scm.com/downloads",
|
|
1893
|
+
"doctor.git.old": "git {version} is older than {min}; some commands will need a newer git",
|
|
1894
|
+
"doctor.git.fix_old": "Update git to {min} or newer",
|
|
1895
|
+
"doctor.claude.ok": "Claude Code {version}",
|
|
1896
|
+
"doctor.claude.missing": "Claude Code was not found; the CLI works without it, the hooks need it",
|
|
1897
|
+
"doctor.claude.fix": "Install Claude Code: npm install -g @anthropic-ai/claude-code",
|
|
1898
|
+
"doctor.claude.timeout": "`claude --version` did not answer in time",
|
|
1899
|
+
"doctor.claude.fix_timeout": "Run `claude --version` by hand and check that it answers",
|
|
1900
|
+
"doctor.os.linux": "Linux",
|
|
1901
|
+
"doctor.os.wsl": "Linux on WSL",
|
|
1902
|
+
"doctor.os.macos": "macOS",
|
|
1903
|
+
"doctor.os.windows": "Windows (native)",
|
|
1904
|
+
"doctor.os.other": "Unrecognized system ({platform})",
|
|
1905
|
+
"doctor.path.plain": "Project path has no spaces or accents",
|
|
1906
|
+
"doctor.path.chars": "Project path has spaces or accents: {path}",
|
|
1907
|
+
"doctor.path.fix_chars": "Finite Monkey handles this path; if another tool fails here, move the project to a path without spaces or accents",
|
|
1908
|
+
"doctor.path.synced": "Project is inside a synced folder (OneDrive, Dropbox, iCloud or Google Drive): {path}",
|
|
1909
|
+
"doctor.path.fix_synced": "Move the project out of the synced folder to avoid sync conflicts and slow git",
|
|
1910
|
+
"doctor.path.wsl_mount": "Project is on a Windows drive mounted in WSL: {path}",
|
|
1911
|
+
"doctor.path.fix_wsl_mount": "Move the project into the Linux home (for example ~/projects) for faster git and correct permissions",
|
|
1912
|
+
"hook.failed_closed": "the {event} hook could not run ({detail}), so it blocked to stay safe. Run `finitemonkey doctor`",
|
|
1913
|
+
"hook.failed_open": "the {event} hook could not run ({detail}); continuing without it. Run `finitemonkey doctor`",
|
|
1914
|
+
"exit.ok": "0 ok: the command did what it was asked",
|
|
1915
|
+
"exit.check_failed": "1 check failed: the command ran and found a problem",
|
|
1916
|
+
"exit.usage": "2 usage error: unknown command, unknown flag, bad value or command not available yet",
|
|
1917
|
+
"exit.not_configured": "3 not configured: setup or a prerequisite (node, git, files, permissions) is missing",
|
|
1918
|
+
"exit.needs_person": "4 needs a person: a decision or approval is required before going on",
|
|
1919
|
+
"exit.internal": "5 internal error: Finite Monkey itself failed; please report it",
|
|
1920
|
+
"help.exit_codes": "Exit codes",
|
|
1921
|
+
"hook.stop_released": "the {event} hook would block again ({detail}), but Claude Code is already continuing from a stop hook, so it lets the session stop. Run `finitemonkey doctor`",
|
|
1922
|
+
"error.needs_confirmation": '"{command}" needs your decision before it changes anything: {question}',
|
|
1923
|
+
"secrets.command.scan": "Find secrets in files, the staged changes or the git history (local only)",
|
|
1924
|
+
"secrets.command.accept": "Accept one finding into the baseline, with a reason and an expiry (the person only)",
|
|
1925
|
+
"secrets.command.incident": "List secret incidents, mark them rotated or closed, and show the history purge steps",
|
|
1926
|
+
"secrets.found_item": "{rule} at {where}",
|
|
1927
|
+
"secrets.found_more": "and {count} more",
|
|
1928
|
+
"secrets.where.line": "line {line}, column {column}",
|
|
1929
|
+
"secrets.where.file": "{file}:{line}:{column}",
|
|
1930
|
+
"secrets.where.new_text": "{file} (line {line} of the new text)",
|
|
1931
|
+
"secrets.block.write": "blocked: the new text for {file} has a credential ({found}). Nothing was written. Keep the value in .env (outside git) and read it by its variable name. If the key is real, treat it as exposed and rotate it.",
|
|
1932
|
+
"secrets.block.bash": "blocked: this command has a credential ({found}) and did not run. Read the value from an environment variable instead of writing it in the command. If the key is real, treat it as exposed and rotate it.",
|
|
1933
|
+
"secrets.block.commit": "blocked: the staged changes have a credential ({found}), so the commit did not run. Remove it from the staged files, keep it in .env (outside git) and rotate it if it is real.",
|
|
1934
|
+
"secrets.block.read": "blocked: {target} holds secrets, and reading it would put their values in the conversation. Ask the person for the variable names, or read .env.example.",
|
|
1935
|
+
"secrets.block.env_dump": 'blocked: `{command}` prints every environment variable, secrets included. Print only the variable you need by name, or check that it exists with `test -n "$NAME"`.',
|
|
1936
|
+
"secrets.block.person_only": "blocked: `{command}` is a decision for the person, not the model. Ask the person to run it in their own terminal.",
|
|
1937
|
+
"secrets.block.prompt": "your message was not sent to the model: it has a credential ({found}). If this key was pasted anywhere else, treat it as exposed and rotate it. To use it, put it in .env and tell me only the variable name. Never paste keys into the chat. Incident: {incident}.",
|
|
1938
|
+
"secrets.post.context": "Finite Monkey: the {tool} output contained a credential ({found}). Incident {incident} is open; rotating the key is the person's decision. Finite Monkey output never repeats the value.",
|
|
1939
|
+
"secrets.incident.unsaved": "not recorded ({detail})",
|
|
1940
|
+
"secrets.scan.title": "Secrets scan ({mode}): {total} found, {new} new, {known} known, {expired} expired, {fixed} fixed",
|
|
1941
|
+
"secrets.scan.item": " [{status}] {rule} {where} {mask} id {id}",
|
|
1942
|
+
"secrets.scan.clean": "No secrets found. The monkey keeps its bananas to itself.",
|
|
1943
|
+
"secrets.accept.done": "Accepted {id} ({rule} at {where}) as {state} until {expires}, by {owner}.",
|
|
1944
|
+
"secrets.action.accept": "accept a finding",
|
|
1945
|
+
"secrets.action.rotated": "mark an incident as rotated",
|
|
1946
|
+
"secrets.action.close": "close an incident",
|
|
1947
|
+
"secrets.incident.title": "{count} incident(s)",
|
|
1948
|
+
"secrets.incident.item": " {id} {state} {rule} {where} since {created}",
|
|
1949
|
+
"secrets.incident.none": "No secret incidents.",
|
|
1950
|
+
"secrets.incident.marked": "Incident {id} is now {state}.",
|
|
1951
|
+
"secrets.purge.title": "History purge for {id} ({where}, first commit {commit}). You run these steps; Finite Monkey never runs them:",
|
|
1952
|
+
"secrets.purge.step1": "1. Confirm the old key is revoked at the provider and the new one works in every consumer.",
|
|
1953
|
+
"secrets.purge.step2": "2. Outside the repository, write a replacements file with one line per leaked value: <value>==>REMOVED. Copy the value from your own records; Finite Monkey never stores or prints it.",
|
|
1954
|
+
"secrets.purge.step3": "3. In a fresh mirror clone, run: git filter-repo --replace-text <replacements file>",
|
|
1955
|
+
"secrets.purge.step4": "4. Relax branch protection yourself, push the rewritten refs, restore the protection and tell everyone with a clone to clone again.",
|
|
1956
|
+
"secrets.purge.step5": "5. Close the incident: finitemonkey secrets incident close {id}",
|
|
1957
|
+
"secrets.error.found": "{count} secret finding(s) outside the baseline or with an expired acceptance",
|
|
1958
|
+
"secrets.error.one_mode": "Choose only one of --staged, --history and --path",
|
|
1959
|
+
"secrets.error.git_unavailable": "The scan could not read git ({detail})",
|
|
1960
|
+
"secrets.error.baseline_invalid": "The baseline {file} is invalid: {detail}",
|
|
1961
|
+
"secrets.error.person_only": "Only the person can {action}, and this does not look like their terminal (no TTY, or running inside Claude Code)",
|
|
1962
|
+
"secrets.error.reason_required": "--reason is required: say why this finding is acceptable",
|
|
1963
|
+
"secrets.error.finding_not_found": "No current finding has the id {id}",
|
|
1964
|
+
"secrets.error.incident_not_found": "No incident has the id {id}",
|
|
1965
|
+
"secrets.error.not_rotated": "Incident {id} is {state}: rotate the key at the provider first, then mark it rotated",
|
|
1966
|
+
"secrets.error.bad_transition": "Incident {id} cannot go from {from} to {to}",
|
|
1967
|
+
"secrets.error.write_failed": "Could not write {file}: {detail}"
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
// src/i18n/es.json
|
|
1971
|
+
var es_default = {
|
|
1972
|
+
"command.init.summary": "Instala Finite Monkey en este proyecto",
|
|
1973
|
+
"command.doctor.summary": "Revisa el entorno y dice cómo corregir cada problema",
|
|
1974
|
+
"command.update.summary": "Actualiza los archivos que Finite Monkey generó",
|
|
1975
|
+
"command.uninstall.summary": "Quita Finite Monkey de este proyecto",
|
|
1976
|
+
"command.i18n_check.summary": "Revisa que los catálogos pt-BR, en y es coincidan",
|
|
1977
|
+
"command.hook.summary": "Responde a un evento de hook de Claude Code (lee JSON de la entrada estándar)",
|
|
1978
|
+
"command.version.summary": "Muestra la versión",
|
|
1979
|
+
"command.help.summary": "Muestra comandos y opciones",
|
|
1980
|
+
"help.tagline": "Infinite monkeys not required.",
|
|
1981
|
+
"help.usage": "Uso",
|
|
1982
|
+
"help.command": "comando",
|
|
1983
|
+
"help.commands": "Comandos",
|
|
1984
|
+
"help.flags": "Opciones globales",
|
|
1985
|
+
"help.options": "Opciones",
|
|
1986
|
+
"help.planned": "(aún no disponible)",
|
|
1987
|
+
"help.more": "Ejecuta `finitemonkey help <comando>` para más detalles. Usa --json para una salida legible por máquina.",
|
|
1988
|
+
"flag.json": "Imprime un objeto JSON (schemaVersion 1) en lugar de texto",
|
|
1989
|
+
"flag.lang": "Idioma de los mensajes",
|
|
1990
|
+
"flag.yes": "Responde sí a las confirmaciones",
|
|
1991
|
+
"flag.dry_run": "Muestra qué cambiaría sin cambiar nada",
|
|
1992
|
+
"flag.version": "Muestra la versión",
|
|
1993
|
+
"flag.help": "Muestra la ayuda",
|
|
1994
|
+
"output.error": "error: {message}",
|
|
1995
|
+
"output.fix": "cómo corregir: {fix}",
|
|
1996
|
+
"output.next": "siguiente paso: {next}",
|
|
1997
|
+
"error.internal": "Finite Monkey falló de forma inesperada: {detail}",
|
|
1998
|
+
"error.invalid_arguments": "Argumentos inválidos: {detail}",
|
|
1999
|
+
"error.unexpected_argument": 'Argumento inesperado "{argument}"',
|
|
2000
|
+
"error.unknown_command": 'Comando desconocido "{command}"',
|
|
2001
|
+
"error.unknown_command_suggest": 'Comando desconocido "{command}". ¿Quisiste decir "{suggestion}"?',
|
|
2002
|
+
"error.not_implemented": '"{command}" aún no está disponible en esta versión',
|
|
2003
|
+
"error.invalid_lang": 'Idioma no soportado "{lang}". Usa uno de estos: {languages}',
|
|
2004
|
+
"error.i18n_catalog_unreadable": "No se pudo leer el catálogo {file}: {detail}",
|
|
2005
|
+
"error.i18n_mismatch": "{count} problema(s) en los catálogos",
|
|
2006
|
+
"error.doctor_failed": "{count} revisión(es) fallaron",
|
|
2007
|
+
"error.unknown_hook_event": 'Evento de hook desconocido "{event}". Usa uno de estos: {events}',
|
|
2008
|
+
"i18n.summary": "Idiomas: {languages}. Claves: {keys}.",
|
|
2009
|
+
"i18n.ok": "Todos los catálogos coinciden.",
|
|
2010
|
+
"i18n.fix": "Edita los catálogos en {file} para que todos los idiomas tengan las mismas claves y los mismos marcadores",
|
|
2011
|
+
"i18n.problem.missing": "falta la clave",
|
|
2012
|
+
"i18n.problem.extra": "clave que no existe en en.json",
|
|
2013
|
+
"i18n.problem.placeholder": "marcadores distintos (esperado: {expected}; encontrado: {actual})",
|
|
2014
|
+
"i18n.problem.empty": "texto vacío",
|
|
2015
|
+
"i18n.problem.not_string": "el valor no es texto",
|
|
2016
|
+
"doctor.title": "finitemonkey doctor ({version})",
|
|
2017
|
+
"doctor.summary": "{ok} ok, {warn} aviso(s), {fail} falla(s)",
|
|
2018
|
+
"doctor.node.ok": "Node.js {version}",
|
|
2019
|
+
"doctor.node.old": "Node.js {version} es más antiguo que el mínimo {min}",
|
|
2020
|
+
"doctor.node.fix": "Instala Node.js 20 o más nuevo desde https://nodejs.org",
|
|
2021
|
+
"doctor.git.ok": "git {version}",
|
|
2022
|
+
"doctor.git.missing": "No se encontró git",
|
|
2023
|
+
"doctor.git.fix": "Instala git desde https://git-scm.com/downloads",
|
|
2024
|
+
"doctor.git.old": "git {version} es más antiguo que {min}; algunos comandos necesitarán un git más nuevo",
|
|
2025
|
+
"doctor.git.fix_old": "Actualiza git a {min} o más nuevo",
|
|
2026
|
+
"doctor.claude.ok": "Claude Code {version}",
|
|
2027
|
+
"doctor.claude.missing": "No se encontró Claude Code; la CLI funciona sin él, los hooks lo necesitan",
|
|
2028
|
+
"doctor.claude.fix": "Instala Claude Code: npm install -g @anthropic-ai/claude-code",
|
|
2029
|
+
"doctor.claude.timeout": "`claude --version` no respondió a tiempo",
|
|
2030
|
+
"doctor.claude.fix_timeout": "Ejecuta `claude --version` a mano y revisa que responda",
|
|
2031
|
+
"doctor.os.linux": "Linux",
|
|
2032
|
+
"doctor.os.wsl": "Linux en WSL",
|
|
2033
|
+
"doctor.os.macos": "macOS",
|
|
2034
|
+
"doctor.os.windows": "Windows (nativo)",
|
|
2035
|
+
"doctor.os.other": "Sistema no reconocido ({platform})",
|
|
2036
|
+
"doctor.path.plain": "La ruta del proyecto no tiene espacios ni acentos",
|
|
2037
|
+
"doctor.path.chars": "La ruta del proyecto tiene espacios o acentos: {path}",
|
|
2038
|
+
"doctor.path.fix_chars": "Finite Monkey maneja esta ruta; si otra herramienta falla aquí, mueve el proyecto a una ruta sin espacios ni acentos",
|
|
2039
|
+
"doctor.path.synced": "El proyecto está en una carpeta sincronizada (OneDrive, Dropbox, iCloud o Google Drive): {path}",
|
|
2040
|
+
"doctor.path.fix_synced": "Mueve el proyecto fuera de la carpeta sincronizada para evitar conflictos de sincronización y git lento",
|
|
2041
|
+
"doctor.path.wsl_mount": "El proyecto está en un disco de Windows montado en WSL: {path}",
|
|
2042
|
+
"doctor.path.fix_wsl_mount": "Mueve el proyecto a la home de Linux (por ejemplo ~/proyectos) para tener git rápido y permisos correctos",
|
|
2043
|
+
"hook.failed_closed": "el hook {event} no pudo ejecutarse ({detail}) y bloqueó por seguridad. Ejecuta `finitemonkey doctor`",
|
|
2044
|
+
"hook.failed_open": "el hook {event} no pudo ejecutarse ({detail}); se sigue sin él. Ejecuta `finitemonkey doctor`",
|
|
2045
|
+
"exit.ok": "0 ok: el comando hizo lo que se pidió",
|
|
2046
|
+
"exit.check_failed": "1 revisión fallida: el comando se ejecutó y encontró un problema",
|
|
2047
|
+
"exit.usage": "2 error de uso: comando u opción desconocidos, valor inválido o comando aún no disponible",
|
|
2048
|
+
"exit.not_configured": "3 no configurado: falta la instalación o un requisito previo (node, git, archivos, permisos)",
|
|
2049
|
+
"exit.needs_person": "4 necesita a la persona: se requiere una decisión o aprobación antes de seguir",
|
|
2050
|
+
"exit.internal": "5 error interno: el propio Finite Monkey falló; por favor, repórtalo",
|
|
2051
|
+
"help.exit_codes": "Códigos de salida",
|
|
2052
|
+
"hook.stop_released": "el hook {event} bloquearía otra vez ({detail}), pero Claude Code ya está continuando por un hook de parada, así que deja que la sesión termine. Ejecuta `finitemonkey doctor`",
|
|
2053
|
+
"error.needs_confirmation": '"{command}" necesita tu decisión antes de cambiar algo: {question}',
|
|
2054
|
+
"secrets.command.scan": "Busca secretos en los archivos, en los cambios preparados o en el historial de git (solo local)",
|
|
2055
|
+
"secrets.command.accept": "Acepta un hallazgo en el baseline, con motivo y vencimiento (solo la persona)",
|
|
2056
|
+
"secrets.command.incident": "Lista incidentes de secretos, los marca como rotados o cerrados y muestra los pasos para limpiar el historial",
|
|
2057
|
+
"secrets.found_item": "{rule} en {where}",
|
|
2058
|
+
"secrets.found_more": "y {count} más",
|
|
2059
|
+
"secrets.where.line": "línea {line}, columna {column}",
|
|
2060
|
+
"secrets.where.file": "{file}:{line}:{column}",
|
|
2061
|
+
"secrets.where.new_text": "{file} (línea {line} del texto nuevo)",
|
|
2062
|
+
"secrets.block.write": "bloqueado: el texto nuevo de {file} tiene una credencial ({found}). No se escribió nada. Deja el valor en .env (fuera de git) y léelo por el nombre de la variable. Si la clave es real, trátala como expuesta y rótala.",
|
|
2063
|
+
"secrets.block.bash": "bloqueado: este comando tiene una credencial ({found}) y no se ejecutó. Lee el valor de una variable de entorno en lugar de escribirlo en el comando. Si la clave es real, trátala como expuesta y rótala.",
|
|
2064
|
+
"secrets.block.commit": "bloqueado: los cambios preparados tienen una credencial ({found}), así que el commit no se ejecutó. Quítala de los archivos preparados, déjala en .env (fuera de git) y rótala si es real.",
|
|
2065
|
+
"secrets.block.read": "bloqueado: {target} guarda secretos, y leerlo pondría sus valores en la conversación. Pide a la persona los nombres de las variables, o lee .env.example.",
|
|
2066
|
+
"secrets.block.env_dump": 'bloqueado: `{command}` imprime todas las variables de entorno, secretos incluidos. Imprime solo la variable que necesitas por su nombre, o comprueba que existe con `test -n "$NAME"`.',
|
|
2067
|
+
"secrets.block.person_only": "bloqueado: `{command}` es una decisión de la persona, no del modelo. Pide a la persona que lo ejecute en su propia terminal.",
|
|
2068
|
+
"secrets.block.prompt": "tu mensaje no se envió al modelo: tiene una credencial ({found}). Si esta clave se pegó en otro lugar, trátala como expuesta y rótala. Para usarla, ponla en .env y dime solo el nombre de la variable. Nunca pegues claves en el chat. Incidente: {incident}.",
|
|
2069
|
+
"secrets.post.context": "Finite Monkey: la salida de {tool} contenía una credencial ({found}). El incidente {incident} está abierto; rotar la clave es decisión de la persona. La salida de Finite Monkey nunca repite el valor.",
|
|
2070
|
+
"secrets.incident.unsaved": "no registrado ({detail})",
|
|
2071
|
+
"secrets.scan.title": "Búsqueda de secretos ({mode}): {total} hallados, {new} nuevos, {known} conocidos, {expired} vencidos, {fixed} corregidos",
|
|
2072
|
+
"secrets.scan.item": " [{status}] {rule} {where} {mask} id {id}",
|
|
2073
|
+
"secrets.scan.clean": "No se encontraron secretos. El mono se guarda sus bananas.",
|
|
2074
|
+
"secrets.accept.done": "Aceptado {id} ({rule} en {where}) como {state} hasta {expires}, por {owner}.",
|
|
2075
|
+
"secrets.action.accept": "aceptar un hallazgo",
|
|
2076
|
+
"secrets.action.rotated": "marcar un incidente como rotado",
|
|
2077
|
+
"secrets.action.close": "cerrar un incidente",
|
|
2078
|
+
"secrets.incident.title": "{count} incidente(s)",
|
|
2079
|
+
"secrets.incident.item": " {id} {state} {rule} {where} desde {created}",
|
|
2080
|
+
"secrets.incident.none": "No hay incidentes de secretos.",
|
|
2081
|
+
"secrets.incident.marked": "El incidente {id} ahora está {state}.",
|
|
2082
|
+
"secrets.purge.title": "Limpieza del historial de {id} ({where}, primer commit {commit}). Estos pasos los ejecutas tú; Finite Monkey nunca los ejecuta:",
|
|
2083
|
+
"secrets.purge.step1": "1. Confirma que la clave vieja está revocada en el proveedor y que la nueva funciona en todos los consumidores.",
|
|
2084
|
+
"secrets.purge.step2": "2. Fuera del repositorio, escribe un archivo de reemplazos con una línea por valor filtrado: <valor>==>REMOVED. Copia el valor de tus propios registros; Finite Monkey nunca lo guarda ni lo imprime.",
|
|
2085
|
+
"secrets.purge.step3": "3. En un clon espejo nuevo, ejecuta: git filter-repo --replace-text <archivo de reemplazos>",
|
|
2086
|
+
"secrets.purge.step4": "4. Relaja tú la protección de la rama, envía las refs reescritas, restaura la protección y avisa a quien tenga un clon que vuelva a clonar.",
|
|
2087
|
+
"secrets.purge.step5": "5. Cierra el incidente: finitemonkey secrets incident close {id}",
|
|
2088
|
+
"secrets.error.found": "{count} hallazgo(s) de secretos fuera del baseline o con aceptación vencida",
|
|
2089
|
+
"secrets.error.one_mode": "Elige solo uno entre --staged, --history y --path",
|
|
2090
|
+
"secrets.error.git_unavailable": "La búsqueda no pudo leer git ({detail})",
|
|
2091
|
+
"secrets.error.baseline_invalid": "El baseline {file} no es válido: {detail}",
|
|
2092
|
+
"secrets.error.person_only": "Solo la persona puede {action}, y esto no parece su terminal (sin TTY, o dentro de Claude Code)",
|
|
2093
|
+
"secrets.error.reason_required": "--reason es obligatorio: di por qué este hallazgo es aceptable",
|
|
2094
|
+
"secrets.error.finding_not_found": "Ningún hallazgo actual tiene el id {id}",
|
|
2095
|
+
"secrets.error.incident_not_found": "Ningún incidente tiene el id {id}",
|
|
2096
|
+
"secrets.error.not_rotated": "El incidente {id} está {state}: rota la clave en el proveedor primero y luego márcalo como rotado",
|
|
2097
|
+
"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}"
|
|
2099
|
+
};
|
|
2100
|
+
|
|
2101
|
+
// src/i18n/pt-BR.json
|
|
2102
|
+
var pt_BR_default = {
|
|
2103
|
+
"command.init.summary": "Instala o Finite Monkey neste projeto",
|
|
2104
|
+
"command.doctor.summary": "Confere o ambiente e diz como corrigir cada problema",
|
|
2105
|
+
"command.update.summary": "Atualiza os arquivos que o Finite Monkey gerou",
|
|
2106
|
+
"command.uninstall.summary": "Remove o Finite Monkey deste projeto",
|
|
2107
|
+
"command.i18n_check.summary": "Confere se os catálogos pt-BR, en e es batem",
|
|
2108
|
+
"command.hook.summary": "Responde a um evento de hook do Claude Code (lê JSON da entrada padrão)",
|
|
2109
|
+
"command.version.summary": "Mostra a versão",
|
|
2110
|
+
"command.help.summary": "Mostra comandos e opções",
|
|
2111
|
+
"help.tagline": "Infinite monkeys not required.",
|
|
2112
|
+
"help.usage": "Uso",
|
|
2113
|
+
"help.command": "comando",
|
|
2114
|
+
"help.commands": "Comandos",
|
|
2115
|
+
"help.flags": "Opções globais",
|
|
2116
|
+
"help.options": "Opções",
|
|
2117
|
+
"help.planned": "(ainda não disponível)",
|
|
2118
|
+
"help.more": "Rode `finitemonkey help <comando>` para detalhes. Use --json para saída legível por máquina.",
|
|
2119
|
+
"flag.json": "Imprime um objeto JSON (schemaVersion 1) no lugar de texto",
|
|
2120
|
+
"flag.lang": "Idioma das mensagens",
|
|
2121
|
+
"flag.yes": "Responde sim às confirmações",
|
|
2122
|
+
"flag.dry_run": "Mostra o que mudaria sem mudar nada",
|
|
2123
|
+
"flag.version": "Mostra a versão",
|
|
2124
|
+
"flag.help": "Mostra a ajuda",
|
|
2125
|
+
"output.error": "erro: {message}",
|
|
2126
|
+
"output.fix": "como corrigir: {fix}",
|
|
2127
|
+
"output.next": "próximo passo: {next}",
|
|
2128
|
+
"error.internal": "O Finite Monkey falhou de forma inesperada: {detail}",
|
|
2129
|
+
"error.invalid_arguments": "Argumentos inválidos: {detail}",
|
|
2130
|
+
"error.unexpected_argument": 'Argumento inesperado "{argument}"',
|
|
2131
|
+
"error.unknown_command": 'Comando desconhecido "{command}"',
|
|
2132
|
+
"error.unknown_command_suggest": 'Comando desconhecido "{command}". Você quis dizer "{suggestion}"?',
|
|
2133
|
+
"error.not_implemented": '"{command}" ainda não está disponível nesta versão',
|
|
2134
|
+
"error.invalid_lang": 'Idioma não suportado "{lang}". Use um destes: {languages}',
|
|
2135
|
+
"error.i18n_catalog_unreadable": "Não foi possível ler o catálogo {file}: {detail}",
|
|
2136
|
+
"error.i18n_mismatch": "{count} problema(s) nos catálogos",
|
|
2137
|
+
"error.doctor_failed": "{count} verificação(ões) falharam",
|
|
2138
|
+
"error.unknown_hook_event": 'Evento de hook desconhecido "{event}". Use um destes: {events}',
|
|
2139
|
+
"i18n.summary": "Idiomas: {languages}. Chaves: {keys}.",
|
|
2140
|
+
"i18n.ok": "Todos os catálogos batem.",
|
|
2141
|
+
"i18n.fix": "Edite os catálogos em {file} para que todos os idiomas tenham as mesmas chaves e os mesmos marcadores",
|
|
2142
|
+
"i18n.problem.missing": "chave faltando",
|
|
2143
|
+
"i18n.problem.extra": "chave que não existe no en.json",
|
|
2144
|
+
"i18n.problem.placeholder": "marcadores diferentes (esperado: {expected}; encontrado: {actual})",
|
|
2145
|
+
"i18n.problem.empty": "texto vazio",
|
|
2146
|
+
"i18n.problem.not_string": "valor não é texto",
|
|
2147
|
+
"doctor.title": "finitemonkey doctor ({version})",
|
|
2148
|
+
"doctor.summary": "{ok} ok, {warn} aviso(s), {fail} falha(s)",
|
|
2149
|
+
"doctor.node.ok": "Node.js {version}",
|
|
2150
|
+
"doctor.node.old": "Node.js {version} é mais antigo que o mínimo {min}",
|
|
2151
|
+
"doctor.node.fix": "Instale o Node.js 20 ou mais novo em https://nodejs.org",
|
|
2152
|
+
"doctor.git.ok": "git {version}",
|
|
2153
|
+
"doctor.git.missing": "git não foi encontrado",
|
|
2154
|
+
"doctor.git.fix": "Instale o git em https://git-scm.com/downloads",
|
|
2155
|
+
"doctor.git.old": "git {version} é mais antigo que {min}; alguns comandos vão precisar de um git mais novo",
|
|
2156
|
+
"doctor.git.fix_old": "Atualize o git para {min} ou mais novo",
|
|
2157
|
+
"doctor.claude.ok": "Claude Code {version}",
|
|
2158
|
+
"doctor.claude.missing": "Claude Code não foi encontrado; a CLI funciona sem ele, os hooks precisam dele",
|
|
2159
|
+
"doctor.claude.fix": "Instale o Claude Code: npm install -g @anthropic-ai/claude-code",
|
|
2160
|
+
"doctor.claude.timeout": "`claude --version` não respondeu a tempo",
|
|
2161
|
+
"doctor.claude.fix_timeout": "Rode `claude --version` à mão e confira se ele responde",
|
|
2162
|
+
"doctor.os.linux": "Linux",
|
|
2163
|
+
"doctor.os.wsl": "Linux no WSL",
|
|
2164
|
+
"doctor.os.macos": "macOS",
|
|
2165
|
+
"doctor.os.windows": "Windows (nativo)",
|
|
2166
|
+
"doctor.os.other": "Sistema não reconhecido ({platform})",
|
|
2167
|
+
"doctor.path.plain": "O caminho do projeto não tem espaço nem acento",
|
|
2168
|
+
"doctor.path.chars": "O caminho do projeto tem espaço ou acento: {path}",
|
|
2169
|
+
"doctor.path.fix_chars": "O Finite Monkey lida com este caminho; se outra ferramenta falhar aqui, mova o projeto para um caminho sem espaço nem acento",
|
|
2170
|
+
"doctor.path.synced": "O projeto está numa pasta sincronizada (OneDrive, Dropbox, iCloud ou Google Drive): {path}",
|
|
2171
|
+
"doctor.path.fix_synced": "Mova o projeto para fora da pasta sincronizada para evitar conflito de sincronização e git lento",
|
|
2172
|
+
"doctor.path.wsl_mount": "O projeto está num disco do Windows montado no WSL: {path}",
|
|
2173
|
+
"doctor.path.fix_wsl_mount": "Mova o projeto para a home do Linux (por exemplo ~/projetos) para ter git rápido e permissões corretas",
|
|
2174
|
+
"hook.failed_closed": "o hook {event} não conseguiu rodar ({detail}) e bloqueou por segurança. Rode `finitemonkey doctor`",
|
|
2175
|
+
"hook.failed_open": "o hook {event} não conseguiu rodar ({detail}); seguindo sem ele. Rode `finitemonkey doctor`",
|
|
2176
|
+
"exit.ok": "0 ok: o comando fez o que foi pedido",
|
|
2177
|
+
"exit.check_failed": "1 verificação reprovada: o comando rodou e achou um problema",
|
|
2178
|
+
"exit.usage": "2 erro de uso: comando ou opção desconhecidos, valor inválido ou comando ainda não disponível",
|
|
2179
|
+
"exit.not_configured": "3 não configurado: falta instalação ou um pré-requisito (node, git, arquivos, permissões)",
|
|
2180
|
+
"exit.needs_person": "4 precisa da pessoa: uma decisão ou aprovação é necessária antes de seguir",
|
|
2181
|
+
"exit.internal": "5 erro interno: o próprio Finite Monkey falhou; por favor, reporte",
|
|
2182
|
+
"help.exit_codes": "Códigos de saída",
|
|
2183
|
+
"hook.stop_released": "o hook {event} bloquearia de novo ({detail}), mas o Claude Code já está continuando por causa de um hook de parada, então ele deixa a sessão parar. Rode `finitemonkey doctor`",
|
|
2184
|
+
"error.needs_confirmation": '"{command}" precisa da sua decisão antes de mudar qualquer coisa: {question}',
|
|
2185
|
+
"secrets.command.scan": "Procura segredos nos arquivos, nas mudanças preparadas ou no histórico do git (só local)",
|
|
2186
|
+
"secrets.command.accept": "Aceita um achado no baseline, com motivo e validade (só a pessoa)",
|
|
2187
|
+
"secrets.command.incident": "Lista incidentes de segredo, marca como rotacionado ou fechado e mostra os passos de limpeza do histórico",
|
|
2188
|
+
"secrets.found_item": "{rule} em {where}",
|
|
2189
|
+
"secrets.found_more": "e mais {count}",
|
|
2190
|
+
"secrets.where.line": "linha {line}, coluna {column}",
|
|
2191
|
+
"secrets.where.file": "{file}:{line}:{column}",
|
|
2192
|
+
"secrets.where.new_text": "{file} (linha {line} do texto novo)",
|
|
2193
|
+
"secrets.block.write": "bloqueado: o texto novo de {file} tem uma credencial ({found}). Nada foi gravado. Deixe o valor no .env (fora do git) e leia pelo nome da variável. Se a chave é real, trate como exposta e troque.",
|
|
2194
|
+
"secrets.block.bash": "bloqueado: este comando tem uma credencial ({found}) e não rodou. Leia o valor de uma variável de ambiente em vez de escrevê-lo no comando. Se a chave é real, trate como exposta e troque.",
|
|
2195
|
+
"secrets.block.commit": "bloqueado: as mudanças preparadas têm uma credencial ({found}), então o commit não rodou. Tire-a dos arquivos preparados, deixe no .env (fora do git) e troque a chave se for real.",
|
|
2196
|
+
"secrets.block.read": "bloqueado: {target} guarda segredos, e lê-lo colocaria os valores na conversa. Peça à pessoa os nomes das variáveis, ou leia o .env.example.",
|
|
2197
|
+
"secrets.block.env_dump": 'bloqueado: `{command}` imprime todas as variáveis de ambiente, segredos inclusive. Imprima só a variável que precisa, pelo nome, ou confira que ela existe com `test -n "$NAME"`.',
|
|
2198
|
+
"secrets.block.person_only": "bloqueado: `{command}` é decisão da pessoa, não do modelo. Peça à pessoa que rode no terminal dela.",
|
|
2199
|
+
"secrets.block.prompt": "sua mensagem não foi enviada ao modelo: ela tem uma credencial ({found}). Se essa chave foi colada em outro lugar, trate como exposta e troque. Para usá-la, ponha no .env e me diga só o nome da variável. Nunca cole chaves na conversa. Incidente: {incident}.",
|
|
2200
|
+
"secrets.post.context": "Finite Monkey: a saída de {tool} continha uma credencial ({found}). O incidente {incident} está aberto; trocar a chave é decisão da pessoa. A saída do Finite Monkey nunca repete o valor.",
|
|
2201
|
+
"secrets.incident.unsaved": "não registrado ({detail})",
|
|
2202
|
+
"secrets.scan.title": "Varredura de segredos ({mode}): {total} achados, {new} novos, {known} conhecidos, {expired} vencidos, {fixed} corrigidos",
|
|
2203
|
+
"secrets.scan.item": " [{status}] {rule} {where} {mask} id {id}",
|
|
2204
|
+
"secrets.scan.clean": "Nenhum segredo encontrado. O macaco guarda as bananas para si.",
|
|
2205
|
+
"secrets.accept.done": "Aceito {id} ({rule} em {where}) como {state} até {expires}, por {owner}.",
|
|
2206
|
+
"secrets.action.accept": "aceitar um achado",
|
|
2207
|
+
"secrets.action.rotated": "marcar um incidente como rotacionado",
|
|
2208
|
+
"secrets.action.close": "fechar um incidente",
|
|
2209
|
+
"secrets.incident.title": "{count} incidente(s)",
|
|
2210
|
+
"secrets.incident.item": " {id} {state} {rule} {where} desde {created}",
|
|
2211
|
+
"secrets.incident.none": "Nenhum incidente de segredo.",
|
|
2212
|
+
"secrets.incident.marked": "O incidente {id} agora está {state}.",
|
|
2213
|
+
"secrets.purge.title": "Limpeza do histórico de {id} ({where}, primeiro commit {commit}). Quem roda estes passos é você; o Finite Monkey nunca os roda:",
|
|
2214
|
+
"secrets.purge.step1": "1. Confirme que a chave antiga foi revogada no provedor e que a nova funciona em todos os consumidores.",
|
|
2215
|
+
"secrets.purge.step2": "2. Fora do repositório, escreva um arquivo de substituições com uma linha por valor vazado: <valor>==>REMOVED. Copie o valor dos seus registros; o Finite Monkey nunca o guarda nem imprime.",
|
|
2216
|
+
"secrets.purge.step3": "3. Num clone espelho novo, rode: git filter-repo --replace-text <arquivo de substituições>",
|
|
2217
|
+
"secrets.purge.step4": "4. Afrouxe a proteção da branch você mesmo, envie as refs reescritas, restaure a proteção e avise quem tem clone para clonar de novo.",
|
|
2218
|
+
"secrets.purge.step5": "5. Feche o incidente: finitemonkey secrets incident close {id}",
|
|
2219
|
+
"secrets.error.found": "{count} achado(s) de segredo fora do baseline ou com aceite vencido",
|
|
2220
|
+
"secrets.error.one_mode": "Escolha só um entre --staged, --history e --path",
|
|
2221
|
+
"secrets.error.git_unavailable": "A varredura não conseguiu ler o git ({detail})",
|
|
2222
|
+
"secrets.error.baseline_invalid": "O baseline {file} é inválido: {detail}",
|
|
2223
|
+
"secrets.error.person_only": "Só a pessoa pode {action}, e isto não parece o terminal dela (sem TTY, ou rodando dentro do Claude Code)",
|
|
2224
|
+
"secrets.error.reason_required": "--reason é obrigatório: diga por que este achado é aceitável",
|
|
2225
|
+
"secrets.error.finding_not_found": "Nenhum achado atual tem o id {id}",
|
|
2226
|
+
"secrets.error.incident_not_found": "Nenhum incidente tem o id {id}",
|
|
2227
|
+
"secrets.error.not_rotated": "O incidente {id} está {state}: troque a chave no provedor primeiro e depois marque como rotacionado",
|
|
2228
|
+
"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}"
|
|
2230
|
+
};
|
|
2231
|
+
|
|
2232
|
+
// src/i18n/catalogs.ts
|
|
2233
|
+
var exact = (catalog) => catalog;
|
|
2234
|
+
var catalogs = {
|
|
2235
|
+
en: exact(en_default),
|
|
2236
|
+
"pt-BR": exact(pt_BR_default),
|
|
2237
|
+
es: exact(es_default)
|
|
2238
|
+
};
|
|
2239
|
+
|
|
2240
|
+
// src/i18n/languages.ts
|
|
2241
|
+
var LANGUAGES = ["en", "pt-BR", "es"];
|
|
2242
|
+
var SOURCE_LANG = "en";
|
|
2243
|
+
var normalizeLang = (raw) => {
|
|
2244
|
+
if (raw === void 0) return void 0;
|
|
2245
|
+
const base = (raw.trim().split(/[.@:]/)[0] ?? "").replace(/_/g, "-").toLowerCase();
|
|
2246
|
+
if (base === "pt" || base.startsWith("pt-")) return "pt-BR";
|
|
2247
|
+
if (base === "es" || base.startsWith("es-")) return "es";
|
|
2248
|
+
if (base === "en" || base.startsWith("en-")) return "en";
|
|
2249
|
+
return void 0;
|
|
2250
|
+
};
|
|
2251
|
+
var LANG_ENV_ORDER = ["FINITEMONKEY_LANG", "LC_ALL", "LC_MESSAGES", "LANG"];
|
|
2252
|
+
var detectLang = (flag2, env) => {
|
|
2253
|
+
const fromFlag = normalizeLang(flag2);
|
|
2254
|
+
if (fromFlag) return { lang: fromFlag, source: "flag" };
|
|
2255
|
+
for (const name of LANG_ENV_ORDER) {
|
|
2256
|
+
const lang = normalizeLang(env[name]);
|
|
2257
|
+
if (lang) return { lang, source: name };
|
|
2258
|
+
}
|
|
2259
|
+
return { lang: SOURCE_LANG, source: "default" };
|
|
2260
|
+
};
|
|
2261
|
+
|
|
2262
|
+
// src/i18n/translate.ts
|
|
2263
|
+
var PLACEHOLDER = /\{([a-zA-Z][a-zA-Z0-9_]*)\}/g;
|
|
2264
|
+
var interpolate = (template, params = {}) => template.replace(PLACEHOLDER, (match, name) => {
|
|
2265
|
+
const value = params[name];
|
|
2266
|
+
return value === void 0 ? match : String(value);
|
|
2267
|
+
});
|
|
2268
|
+
var placeholdersOf = (template) => [...new Set(Array.from(template.matchAll(PLACEHOLDER), (match) => match[1] ?? ""))].sort();
|
|
2269
|
+
var createTranslator = (lang, source = catalogs) => (key, params) => interpolate(source[lang][key] ?? source[SOURCE_LANG][key] ?? key, params);
|
|
2270
|
+
|
|
2271
|
+
// src/i18n/check.ts
|
|
2272
|
+
var sameList = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
|
|
2273
|
+
var valueProblems = (lang, catalog, reference) => Object.keys(catalog).sort().flatMap((key) => {
|
|
2274
|
+
const value = catalog[key];
|
|
2275
|
+
if (typeof value !== "string") return [{ lang, key, kind: "not_string" }];
|
|
2276
|
+
const problems = [];
|
|
2277
|
+
if (value.trim() === "") problems.push({ lang, key, kind: "empty" });
|
|
2278
|
+
if (!(key in reference)) problems.push({ lang, key, kind: "extra" });
|
|
2279
|
+
return problems;
|
|
2280
|
+
});
|
|
2281
|
+
var parityProblems = (lang, catalog, reference) => Object.keys(reference).sort().flatMap((key) => {
|
|
2282
|
+
const value = catalog[key];
|
|
2283
|
+
const source = reference[key];
|
|
2284
|
+
if (value === void 0) return [{ lang, key, kind: "missing" }];
|
|
2285
|
+
if (typeof value !== "string" || typeof source !== "string") return [];
|
|
2286
|
+
const expected = placeholdersOf(source);
|
|
2287
|
+
const actual = placeholdersOf(value);
|
|
2288
|
+
return sameList(expected, actual) ? [] : [{ lang, key, kind: "placeholder", expected, actual }];
|
|
2289
|
+
});
|
|
2290
|
+
var checkCatalogs = (input) => {
|
|
2291
|
+
const reference = input[SOURCE_LANG];
|
|
2292
|
+
const problems = LANGUAGES.flatMap(
|
|
2293
|
+
(lang) => lang === SOURCE_LANG ? valueProblems(lang, input[lang], reference) : [...valueProblems(lang, input[lang], reference), ...parityProblems(lang, input[lang], reference)]
|
|
2294
|
+
);
|
|
2295
|
+
return { languages: LANGUAGES, keys: Object.keys(reference).length, problems };
|
|
2296
|
+
};
|
|
2297
|
+
|
|
2298
|
+
// src/commands/i18n-check.ts
|
|
2299
|
+
var unreadable = (dir, file, detail) => ({
|
|
2300
|
+
code: "i18n_catalog_unreadable",
|
|
2301
|
+
messageKey: "error.i18n_catalog_unreadable",
|
|
2302
|
+
params: { file, detail },
|
|
2303
|
+
fix: `finitemonkey i18n check --dir ${dir}`,
|
|
2304
|
+
exitCode: ExitCode.NotConfigured
|
|
2305
|
+
});
|
|
2306
|
+
var readCatalog = (ctx, dir, lang) => {
|
|
2307
|
+
const file = toAbsolutePath(join5(dir, `${lang}.json`));
|
|
2308
|
+
const text2 = ctx.runtime.readText(file);
|
|
2309
|
+
if (!text2.ok) return reject(unreadable(dir, file, text2.error));
|
|
2310
|
+
const parsed = parseJsonObject(text2.value);
|
|
2311
|
+
return parsed.ok ? parsed : reject(unreadable(dir, file, parsed.error));
|
|
2312
|
+
};
|
|
2313
|
+
var loadCatalogs = (ctx, dir) => {
|
|
2314
|
+
const en = readCatalog(ctx, dir, "en");
|
|
2315
|
+
if (!en.ok) return en;
|
|
2316
|
+
const ptBR = readCatalog(ctx, dir, "pt-BR");
|
|
2317
|
+
if (!ptBR.ok) return ptBR;
|
|
2318
|
+
const es = readCatalog(ctx, dir, "es");
|
|
2319
|
+
if (!es.ok) return es;
|
|
2320
|
+
return accept({ en: en.value, "pt-BR": ptBR.value, es: es.value });
|
|
2321
|
+
};
|
|
2322
|
+
var describeProblem = (ctx, problem) => {
|
|
2323
|
+
const kind = ctx.t(`i18n.problem.${problem.kind}`, {
|
|
2324
|
+
expected: (problem.expected ?? []).join(", "),
|
|
2325
|
+
actual: (problem.actual ?? []).join(", ")
|
|
2326
|
+
});
|
|
2327
|
+
return ` ${problem.lang} ${problem.key} ${kind}`;
|
|
2328
|
+
};
|
|
2329
|
+
var runI18nCheck = (ctx, dir) => {
|
|
2330
|
+
const input = dir === void 0 ? accept(catalogs) : loadCatalogs(ctx, dir);
|
|
2331
|
+
if (!input.ok) return failureFrom(ctx.t, input.error);
|
|
2332
|
+
const report = checkCatalogs(input.value);
|
|
2333
|
+
const data = { ...report, source: dir ?? "bundled" };
|
|
2334
|
+
const summary = ctx.t("i18n.summary", { languages: report.languages.join(", "), keys: report.keys });
|
|
2335
|
+
if (report.problems.length === 0) return success(data, [summary, ctx.t("i18n.ok")]);
|
|
2336
|
+
return {
|
|
2337
|
+
ok: false,
|
|
2338
|
+
exitCode: ExitCode.CheckFailed,
|
|
2339
|
+
data,
|
|
2340
|
+
human: [summary, ...report.problems.map((problem) => describeProblem(ctx, problem))],
|
|
2341
|
+
error: {
|
|
2342
|
+
code: "i18n_mismatch",
|
|
2343
|
+
message: ctx.t("error.i18n_mismatch", { count: report.problems.length }),
|
|
2344
|
+
fix: ctx.t("i18n.fix", { file: dir ?? "src/i18n" })
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
};
|
|
2348
|
+
var i18nCheckCommand = {
|
|
2349
|
+
path: ["i18n", "check"],
|
|
2350
|
+
summaryKey: "command.i18n_check.summary",
|
|
2351
|
+
usage: "finitemonkey i18n check [--dir <path>]",
|
|
2352
|
+
status: "ready",
|
|
2353
|
+
options: { dir: { type: "string" } },
|
|
2354
|
+
run: (ctx, args) => {
|
|
2355
|
+
const dir = stringOption(args.values, "dir");
|
|
2356
|
+
return Promise.resolve(runI18nCheck(ctx, dir === void 0 ? void 0 : toAbsolutePath(dir, ctx.cwd)));
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2359
|
+
|
|
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" }
|
|
2376
|
+
})
|
|
2377
|
+
)
|
|
2378
|
+
});
|
|
2379
|
+
var initCommand = planned("init", "command.init.summary", {
|
|
2380
|
+
preset: { type: "string" },
|
|
2381
|
+
github: { type: "boolean" }
|
|
2382
|
+
});
|
|
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" }
|
|
2387
|
+
});
|
|
2388
|
+
|
|
2389
|
+
// src/secrets/project.ts
|
|
2390
|
+
var projectRoot = (ctx) => {
|
|
2391
|
+
const top = ctx.runtime.git(["rev-parse", "--show-toplevel"], ctx.cwd);
|
|
2392
|
+
const path = top.stdout.trim();
|
|
2393
|
+
return top.ok && path !== "" ? toAbsolutePath(path) : ctx.cwd;
|
|
2394
|
+
};
|
|
2395
|
+
var isPerson = (ctx) => ctx.io.stdin.isTTY === true && ctx.env["CLAUDECODE"] === void 0 && ctx.env["CLAUDE_CODE_ENTRYPOINT"] === void 0;
|
|
2396
|
+
var personName = (ctx) => {
|
|
2397
|
+
const name = ctx.runtime.git(["config", "user.name"], ctx.cwd).stdout.trim();
|
|
2398
|
+
return name !== "" ? name : ctx.env["USER"] ?? ctx.env["USERNAME"] ?? "person";
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
// src/commands/secrets-problems.ts
|
|
2402
|
+
var baselineInvalid = (detail) => ({
|
|
2403
|
+
code: "secrets_baseline_invalid",
|
|
2404
|
+
messageKey: "secrets.error.baseline_invalid",
|
|
2405
|
+
params: { file: BASELINE_FILE, detail },
|
|
2406
|
+
fix: `git checkout -- ${BASELINE_FILE}`,
|
|
2407
|
+
exitCode: ExitCode.NotConfigured
|
|
2408
|
+
});
|
|
2409
|
+
var writeFailed = (file, detail) => ({
|
|
2410
|
+
code: "secrets_write_failed",
|
|
2411
|
+
messageKey: "secrets.error.write_failed",
|
|
2412
|
+
params: { file, detail },
|
|
2413
|
+
fix: "finitemonkey doctor",
|
|
2414
|
+
exitCode: ExitCode.Internal
|
|
2415
|
+
});
|
|
2416
|
+
var gitUnavailable = (detail) => ({
|
|
2417
|
+
code: "secrets_git_unavailable",
|
|
2418
|
+
messageKey: "secrets.error.git_unavailable",
|
|
2419
|
+
params: { detail },
|
|
2420
|
+
fix: "finitemonkey doctor",
|
|
2421
|
+
exitCode: ExitCode.NotConfigured
|
|
2422
|
+
});
|
|
2423
|
+
|
|
2424
|
+
// src/commands/secrets-accept.ts
|
|
2425
|
+
var DAY_MS = 864e5;
|
|
2426
|
+
var daysOf = (values) => {
|
|
2427
|
+
const raw = stringOption(values, "days");
|
|
2428
|
+
if (raw === void 0) return EXCEPTION_MAX_DAYS;
|
|
2429
|
+
const days = Number.parseInt(raw, 10);
|
|
2430
|
+
return Number.isInteger(days) && days > 0 && days <= EXCEPTION_MAX_DAYS ? days : void 0;
|
|
2431
|
+
};
|
|
2432
|
+
var plusDays = (iso, days) => new Date(Date.parse(iso) + days * DAY_MS).toISOString();
|
|
2433
|
+
var failure = (ctx, problem) => failureFrom(ctx.t, problem);
|
|
2434
|
+
var inputProblem = (ctx, id, reason, days) => {
|
|
2435
|
+
const fix = "finitemonkey secrets accept <id> --reason <why> [--days 1-90] [--false-positive]";
|
|
2436
|
+
if (!isPerson(ctx)) {
|
|
2437
|
+
return needsPersonProblem("secrets.error.person_only", fix, { action: ctx.t("secrets.action.accept") });
|
|
2438
|
+
}
|
|
2439
|
+
if (id === "") return usageProblem("invalid_arguments", "error.invalid_arguments", fix, { detail: "<id>" });
|
|
2440
|
+
if (reason === void 0 || reason.trim() === "") {
|
|
2441
|
+
return usageProblem("invalid_arguments", "secrets.error.reason_required", fix);
|
|
2442
|
+
}
|
|
2443
|
+
if (days === void 0) {
|
|
2444
|
+
return usageProblem("invalid_arguments", "error.invalid_arguments", fix, {
|
|
2445
|
+
detail: `--days 1-${String(EXCEPTION_MAX_DAYS)}`
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
return void 0;
|
|
2449
|
+
};
|
|
2450
|
+
var entryOf = (ctx, finding, args) => {
|
|
2451
|
+
const now = ctx.runtime.now();
|
|
2452
|
+
return {
|
|
2453
|
+
id: finding.id,
|
|
2454
|
+
rule: finding.ruleId,
|
|
2455
|
+
file: finding.file,
|
|
2456
|
+
line: finding.line,
|
|
2457
|
+
fingerprint: finding.fingerprint,
|
|
2458
|
+
state: args.falsePositive ? "falso-positivo" : "aceito",
|
|
2459
|
+
owner: personName(ctx),
|
|
2460
|
+
reason: args.reason,
|
|
2461
|
+
acceptedAt: now,
|
|
2462
|
+
expiresAt: plusDays(now, args.days)
|
|
2463
|
+
};
|
|
2464
|
+
};
|
|
2465
|
+
var acceptEntry = (ctx, args) => {
|
|
2466
|
+
const root = projectRoot(ctx);
|
|
2467
|
+
const baseline = loadBaseline(ctx.runtime, root);
|
|
2468
|
+
if (!baseline.ok) {
|
|
2469
|
+
return failure(ctx, baselineInvalid(baseline.error));
|
|
2470
|
+
}
|
|
2471
|
+
const scanned = scanPath(ctx.runtime, root, ".");
|
|
2472
|
+
const finding = scanned.ok ? scanned.value.find((item) => item.id === args.id) : void 0;
|
|
2473
|
+
if (finding === void 0) {
|
|
2474
|
+
return failure(
|
|
2475
|
+
ctx,
|
|
2476
|
+
usageProblem("secrets_finding_not_found", "secrets.error.finding_not_found", "finitemonkey secrets scan", {
|
|
2477
|
+
id: args.id
|
|
2478
|
+
})
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
const entry = entryOf(ctx, finding, args);
|
|
2482
|
+
const entries = [...baseline.value.entries.filter((item) => item.id !== entry.id), entry];
|
|
2483
|
+
const saved = saveBaseline(ctx.runtime, root, { schema: baseline.value.schema, entries });
|
|
2484
|
+
if (!saved.ok) {
|
|
2485
|
+
return failure(ctx, writeFailed(BASELINE_FILE, saved.error));
|
|
2486
|
+
}
|
|
2487
|
+
const line = ctx.t("secrets.accept.done", {
|
|
2488
|
+
id: entry.id,
|
|
2489
|
+
rule: entry.rule,
|
|
2490
|
+
where: whereOf(finding),
|
|
2491
|
+
state: entry.state,
|
|
2492
|
+
expires: entry.expiresAt.slice(0, 10),
|
|
2493
|
+
owner: entry.owner
|
|
2494
|
+
});
|
|
2495
|
+
return success(entry, [line]);
|
|
2496
|
+
};
|
|
2497
|
+
var secretsAcceptCommand = {
|
|
2498
|
+
path: ["secrets", "accept"],
|
|
2499
|
+
summaryKey: "secrets.command.accept",
|
|
2500
|
+
usage: "finitemonkey secrets accept <finding-id> --reason <why> [--days 1-90] [--false-positive]",
|
|
2501
|
+
status: "ready",
|
|
2502
|
+
maxPositionals: 1,
|
|
2503
|
+
options: { reason: { type: "string" }, days: { type: "string" }, "false-positive": { type: "boolean" } },
|
|
2504
|
+
run: (ctx, args) => {
|
|
2505
|
+
const id = args.positionals[0] ?? "";
|
|
2506
|
+
const reason = stringOption(args.values, "reason");
|
|
2507
|
+
const days = daysOf(args.values);
|
|
2508
|
+
const problem = inputProblem(ctx, id, reason, days);
|
|
2509
|
+
if (problem !== void 0 || reason === void 0 || days === void 0) {
|
|
2510
|
+
return Promise.resolve(
|
|
2511
|
+
failure(
|
|
2512
|
+
ctx,
|
|
2513
|
+
problem ?? usageProblem("invalid_arguments", "secrets.error.reason_required", "finitemonkey help secrets accept")
|
|
2514
|
+
)
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
return Promise.resolve(
|
|
2518
|
+
acceptEntry(ctx, { id, reason, days, falsePositive: args.values["false-positive"] === true })
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
};
|
|
2522
|
+
|
|
2523
|
+
// src/commands/secrets-incident.ts
|
|
2524
|
+
var INCIDENT_ACTIONS = ["list", "rotated", "close", "purge"];
|
|
2525
|
+
var USAGE = "finitemonkey secrets incident [list | rotated <id> | close <id> | purge <id>]";
|
|
2526
|
+
var TARGET = { rotated: "rotacionado", close: "fechado" };
|
|
2527
|
+
var PURGE_STEPS = [
|
|
2528
|
+
"secrets.purge.step1",
|
|
2529
|
+
"secrets.purge.step2",
|
|
2530
|
+
"secrets.purge.step3",
|
|
2531
|
+
"secrets.purge.step4",
|
|
2532
|
+
"secrets.purge.step5"
|
|
2533
|
+
];
|
|
2534
|
+
var fail = (ctx, problem) => failureFrom(ctx.t, problem);
|
|
2535
|
+
var notFound2 = (ctx, id) => fail(
|
|
2536
|
+
ctx,
|
|
2537
|
+
usageProblem("secrets_incident_not_found", "secrets.error.incident_not_found", `finitemonkey secrets incident`, {
|
|
2538
|
+
id
|
|
2539
|
+
})
|
|
2540
|
+
);
|
|
2541
|
+
var itemLine = (ctx, incident) => ctx.t("secrets.incident.item", {
|
|
2542
|
+
id: incident.id,
|
|
2543
|
+
state: incident.state,
|
|
2544
|
+
rule: incident.rule,
|
|
2545
|
+
where: incident.where,
|
|
2546
|
+
created: incident.createdAt.slice(0, 10)
|
|
2547
|
+
});
|
|
2548
|
+
var list = (ctx, root) => {
|
|
2549
|
+
const incidents = listIncidents(ctx.runtime, root);
|
|
2550
|
+
const human = incidents.length === 0 ? [ctx.t("secrets.incident.none")] : [ctx.t("secrets.incident.title", { count: incidents.length }), ...incidents.map((item) => itemLine(ctx, item))];
|
|
2551
|
+
return success({ schema: "finitemonkey/secrets-incidents@1", incidents }, human);
|
|
2552
|
+
};
|
|
2553
|
+
var notRotated = (ctx, incident) => fail(ctx, {
|
|
2554
|
+
code: "secrets_not_rotated",
|
|
2555
|
+
messageKey: "secrets.error.not_rotated",
|
|
2556
|
+
params: { id: incident.id, state: incident.state },
|
|
2557
|
+
fix: `finitemonkey secrets incident rotated ${incident.id}`,
|
|
2558
|
+
exitCode: ExitCode.NeedsPerson
|
|
2559
|
+
});
|
|
2560
|
+
var move = (ctx, root, incident, action) => {
|
|
2561
|
+
const to = TARGET[action];
|
|
2562
|
+
if (!isPerson(ctx)) {
|
|
2563
|
+
return fail(
|
|
2564
|
+
ctx,
|
|
2565
|
+
needsPersonProblem("secrets.error.person_only", `finitemonkey secrets incident ${action} ${incident.id}`, {
|
|
2566
|
+
action: ctx.t(`secrets.action.${action}`)
|
|
2567
|
+
})
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
if (!canMove(incident.state, to))
|
|
2571
|
+
return action === "close" ? notRotated(ctx, incident) : fail(
|
|
2572
|
+
ctx,
|
|
2573
|
+
usageProblem("invalid_arguments", "secrets.error.bad_transition", USAGE, {
|
|
2574
|
+
id: incident.id,
|
|
2575
|
+
from: incident.state,
|
|
2576
|
+
to
|
|
2577
|
+
})
|
|
2578
|
+
);
|
|
2579
|
+
const moved = moveIncident(incident, to, ctx.runtime.now(), personName(ctx));
|
|
2580
|
+
const saved = saveIncident(ctx.runtime, root, moved);
|
|
2581
|
+
if (!saved.ok) {
|
|
2582
|
+
return fail(ctx, writeFailed(incident.id, saved.error));
|
|
2583
|
+
}
|
|
2584
|
+
return success(moved, [ctx.t("secrets.incident.marked", { id: moved.id, state: moved.state })]);
|
|
2585
|
+
};
|
|
2586
|
+
var purge = (ctx, incident) => {
|
|
2587
|
+
if (!isRotated(incident)) return notRotated(ctx, incident);
|
|
2588
|
+
const params = { id: incident.id, where: incident.where, commit: incident.firstCommit ?? "?" };
|
|
2589
|
+
const steps = PURGE_STEPS.map((key) => ctx.t(key, params));
|
|
2590
|
+
return success({ id: incident.id, state: incident.state, steps }, [ctx.t("secrets.purge.title", params), ...steps]);
|
|
2591
|
+
};
|
|
2592
|
+
var actionOf = (raw) => INCIDENT_ACTIONS.find((action) => action === (raw ?? "list"));
|
|
2593
|
+
var secretsIncidentCommand = {
|
|
2594
|
+
path: ["secrets", "incident"],
|
|
2595
|
+
summaryKey: "secrets.command.incident",
|
|
2596
|
+
usage: USAGE,
|
|
2597
|
+
status: "ready",
|
|
2598
|
+
maxPositionals: 2,
|
|
2599
|
+
run: (ctx, args) => {
|
|
2600
|
+
const action = actionOf(args.positionals[0]);
|
|
2601
|
+
const root = projectRoot(ctx);
|
|
2602
|
+
if (action === void 0)
|
|
2603
|
+
return Promise.resolve(
|
|
2604
|
+
fail(
|
|
2605
|
+
ctx,
|
|
2606
|
+
usageProblem("invalid_arguments", "error.invalid_arguments", USAGE, { detail: args.positionals[0] ?? "" })
|
|
2607
|
+
)
|
|
2608
|
+
);
|
|
2609
|
+
if (action === "list") return Promise.resolve(list(ctx, root));
|
|
2610
|
+
const id = args.positionals[1] ?? "";
|
|
2611
|
+
const incident = readIncident(ctx.runtime, root, id);
|
|
2612
|
+
if (!incident.ok) return Promise.resolve(notFound2(ctx, id));
|
|
2613
|
+
return Promise.resolve(action === "purge" ? purge(ctx, incident.value) : move(ctx, root, incident.value, action));
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
|
|
2617
|
+
// src/mask/mask.ts
|
|
2618
|
+
var maskToken = (finding) => `«secret:${finding.ruleId}:${finding.tail}»`;
|
|
2619
|
+
var maskText = (text2) => {
|
|
2620
|
+
const findings = scanText(text2, { rules: CREDENTIAL_RULES });
|
|
2621
|
+
if (findings.length === 0) return text2;
|
|
2622
|
+
let out = "";
|
|
2623
|
+
let cursor = 0;
|
|
2624
|
+
for (const finding of findings) {
|
|
2625
|
+
out += text2.slice(cursor, finding.start) + maskToken(finding);
|
|
2626
|
+
cursor = finding.end;
|
|
2627
|
+
}
|
|
2628
|
+
return out + text2.slice(cursor);
|
|
2629
|
+
};
|
|
2630
|
+
var maskDeep = (value) => {
|
|
2631
|
+
if (typeof value === "string") return maskText(value);
|
|
2632
|
+
if (Array.isArray(value)) return value.map(maskDeep);
|
|
2633
|
+
if (typeof value === "object" && value !== null) {
|
|
2634
|
+
return Object.fromEntries(Object.entries(value).map(([key, inner]) => [maskText(key), maskDeep(inner)]));
|
|
2635
|
+
}
|
|
2636
|
+
return value;
|
|
2637
|
+
};
|
|
2638
|
+
|
|
2639
|
+
// src/commands/secrets-scan.ts
|
|
2640
|
+
var SECRETS_SCAN_SCHEMA = "finitemonkey/secrets-scan@1";
|
|
2641
|
+
var view = (finding, status) => ({
|
|
2642
|
+
id: finding.id,
|
|
2643
|
+
rule: finding.ruleId,
|
|
2644
|
+
severity: finding.severity,
|
|
2645
|
+
file: finding.file,
|
|
2646
|
+
line: finding.line,
|
|
2647
|
+
column: finding.column,
|
|
2648
|
+
fingerprint: finding.fingerprint,
|
|
2649
|
+
mask: maskToken(finding),
|
|
2650
|
+
status,
|
|
2651
|
+
...finding.commit === void 0 ? {} : { commit: finding.commit }
|
|
2652
|
+
});
|
|
2653
|
+
var modeOf = (values) => {
|
|
2654
|
+
const chosen = [values["staged"] === true, values["history"] === true, stringOption(values, "path") !== void 0];
|
|
2655
|
+
if (chosen.filter(Boolean).length > 1) return void 0;
|
|
2656
|
+
if (chosen[0] === true) return "staged";
|
|
2657
|
+
return chosen[1] === true ? "history" : "path";
|
|
2658
|
+
};
|
|
2659
|
+
var normalizedTarget = (target) => target.replace(/\\/g, "/").replace(/^\.\/?/, "").replace(/\/$/, "");
|
|
2660
|
+
var scopeOf = (target) => {
|
|
2661
|
+
const prefix = normalizedTarget(target);
|
|
2662
|
+
return (file) => prefix === "" || file === prefix || file.startsWith(`${prefix}/`);
|
|
2663
|
+
};
|
|
2664
|
+
var runScan = async (ctx, root, mode, target) => {
|
|
2665
|
+
if (mode === "staged") return scanStaged(ctx.runtime, root);
|
|
2666
|
+
if (mode === "history") return scanHistory(ctx.runtime, root);
|
|
2667
|
+
return scanPath(ctx.runtime, root, normalizedTarget(target) || ".");
|
|
2668
|
+
};
|
|
2669
|
+
var historyIncidents = (ctx, root, fresh2) => fresh2.flatMap((finding) => {
|
|
2670
|
+
if (ruleById(finding.ruleId)?.kind !== "credential") return [];
|
|
2671
|
+
const draft = {
|
|
2672
|
+
rule: finding.ruleId,
|
|
2673
|
+
fingerprint: finding.fingerprint,
|
|
2674
|
+
where: whereOf(finding),
|
|
2675
|
+
source: "history",
|
|
2676
|
+
state: "aberto",
|
|
2677
|
+
...finding.commit === void 0 ? {} : { firstCommit: finding.commit }
|
|
2678
|
+
};
|
|
2679
|
+
const saved = recordIncident(ctx.runtime, root, draft, HOOK_ACTOR);
|
|
2680
|
+
return saved.ok ? [saved.value.id] : [];
|
|
2681
|
+
});
|
|
2682
|
+
var dataOf = (mode, outcome, incidents) => {
|
|
2683
|
+
const findings = [
|
|
2684
|
+
...outcome.fresh.map((finding) => view(finding, "new")),
|
|
2685
|
+
...outcome.known.map((finding) => view(finding, "known")),
|
|
2686
|
+
...outcome.expired.map((finding) => view(finding, "expired"))
|
|
2687
|
+
];
|
|
2688
|
+
return {
|
|
2689
|
+
schema: SECRETS_SCAN_SCHEMA,
|
|
2690
|
+
mode,
|
|
2691
|
+
baseline: BASELINE_FILE,
|
|
2692
|
+
findings,
|
|
2693
|
+
fixed: outcome.fixed.map((entry) => entry.id),
|
|
2694
|
+
incidents,
|
|
2695
|
+
summary: {
|
|
2696
|
+
total: findings.length,
|
|
2697
|
+
new: outcome.fresh.length,
|
|
2698
|
+
known: outcome.known.length,
|
|
2699
|
+
expired: outcome.expired.length,
|
|
2700
|
+
fixed: outcome.fixed.length
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
};
|
|
2704
|
+
var humanOf = (ctx, data) => [
|
|
2705
|
+
ctx.t("secrets.scan.title", { mode: data.mode, ...data.summary }),
|
|
2706
|
+
...data.findings.map(
|
|
2707
|
+
(item) => ctx.t("secrets.scan.item", {
|
|
2708
|
+
status: item.status,
|
|
2709
|
+
rule: item.rule,
|
|
2710
|
+
where: `${item.file}:${String(item.line)}`,
|
|
2711
|
+
id: item.id,
|
|
2712
|
+
mask: item.mask
|
|
2713
|
+
})
|
|
2714
|
+
),
|
|
2715
|
+
...data.summary.total === 0 ? [ctx.t("secrets.scan.clean")] : []
|
|
2716
|
+
];
|
|
2717
|
+
var resultOf = (ctx, data) => {
|
|
2718
|
+
const human = humanOf(ctx, data);
|
|
2719
|
+
const failing = data.summary.new + data.summary.expired;
|
|
2720
|
+
if (failing === 0) return success(data, human);
|
|
2721
|
+
return failureFrom(
|
|
2722
|
+
ctx.t,
|
|
2723
|
+
{
|
|
2724
|
+
code: "secrets_found",
|
|
2725
|
+
messageKey: "secrets.error.found",
|
|
2726
|
+
params: { count: failing },
|
|
2727
|
+
fix: "finitemonkey secrets accept <id> --reason <why>",
|
|
2728
|
+
exitCode: ExitCode.CheckFailed,
|
|
2729
|
+
data
|
|
2730
|
+
},
|
|
2731
|
+
human
|
|
2732
|
+
);
|
|
2733
|
+
};
|
|
2734
|
+
var prune = (ctx, root, baseline, outcome) => {
|
|
2735
|
+
if (outcome.fixed.length > 0) saveBaseline(ctx.runtime, root, withoutFixed(baseline, outcome.fixed));
|
|
2736
|
+
};
|
|
2737
|
+
var secretsScanCommand = {
|
|
2738
|
+
path: ["secrets", "scan"],
|
|
2739
|
+
summaryKey: "secrets.command.scan",
|
|
2740
|
+
usage: "finitemonkey secrets scan [--staged | --history | --path <dir>] [--json]",
|
|
2741
|
+
status: "ready",
|
|
2742
|
+
options: { staged: { type: "boolean" }, history: { type: "boolean" }, path: { type: "string" } },
|
|
2743
|
+
run: async (ctx, args) => {
|
|
2744
|
+
const mode = modeOf(args.values);
|
|
2745
|
+
const fix = "finitemonkey help secrets scan";
|
|
2746
|
+
if (mode === void 0) return failureFrom(ctx.t, usageProblem("invalid_arguments", "secrets.error.one_mode", fix));
|
|
2747
|
+
const root = projectRoot(ctx);
|
|
2748
|
+
const target = stringOption(args.values, "path") ?? ".";
|
|
2749
|
+
const scanned = await runScan(ctx, root, mode, target);
|
|
2750
|
+
if (!scanned.ok) return failureFrom(ctx.t, gitUnavailable(scanned.error));
|
|
2751
|
+
const baseline = loadBaseline(ctx.runtime, root);
|
|
2752
|
+
if (!baseline.ok) return failureFrom(ctx.t, baselineInvalid(baseline.error));
|
|
2753
|
+
const outcome = ratchet(
|
|
2754
|
+
scanned.value,
|
|
2755
|
+
baseline.value,
|
|
2756
|
+
ctx.runtime.now(),
|
|
2757
|
+
mode === "path" ? scopeOf(target) : void 0
|
|
2758
|
+
);
|
|
2759
|
+
if (mode === "path") prune(ctx, root, baseline.value, outcome);
|
|
2760
|
+
const incidents = mode === "history" ? historyIncidents(ctx, root, outcome.fresh) : [];
|
|
2761
|
+
return resultOf(ctx, dataOf(mode, outcome, incidents));
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2765
|
+
// src/commands/version.ts
|
|
2766
|
+
var versionCommand = {
|
|
2767
|
+
path: ["version"],
|
|
2768
|
+
summaryKey: "command.version.summary",
|
|
2769
|
+
usage: "finitemonkey version",
|
|
2770
|
+
status: "ready",
|
|
2771
|
+
run: (ctx) => {
|
|
2772
|
+
const data = { version: ctx.version, node: ctx.runtime.nodeVersion, platform: ctx.platform };
|
|
2773
|
+
return Promise.resolve(success(data, [ctx.version]));
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
|
|
2777
|
+
// src/commands/index.ts
|
|
2778
|
+
var REGISTRY = [
|
|
2779
|
+
initCommand,
|
|
2780
|
+
doctorCommand,
|
|
2781
|
+
updateCommand,
|
|
2782
|
+
uninstallCommand,
|
|
2783
|
+
i18nCheckCommand,
|
|
2784
|
+
hookCommand,
|
|
2785
|
+
secretsScanCommand,
|
|
2786
|
+
secretsAcceptCommand,
|
|
2787
|
+
secretsIncidentCommand,
|
|
2788
|
+
versionCommand,
|
|
2789
|
+
helpCommand
|
|
2790
|
+
];
|
|
2791
|
+
|
|
2792
|
+
// src/core/suggest.ts
|
|
2793
|
+
var distance = (a, b) => {
|
|
2794
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
2795
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
2796
|
+
let diagonal = row[0] ?? 0;
|
|
2797
|
+
row[0] = i;
|
|
2798
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
2799
|
+
const above = row[j] ?? 0;
|
|
2800
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2801
|
+
row[j] = Math.min(above + 1, (row[j - 1] ?? 0) + 1, diagonal + cost);
|
|
2802
|
+
diagonal = above;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
return row[b.length] ?? 0;
|
|
2806
|
+
};
|
|
2807
|
+
var closest = (input, candidates) => {
|
|
2808
|
+
const group = candidates.find((name) => name.startsWith(`${input} `));
|
|
2809
|
+
if (group !== void 0) return group;
|
|
2810
|
+
const limit = Math.max(1, Math.floor(input.length / 3));
|
|
2811
|
+
let best;
|
|
2812
|
+
for (const name of candidates) {
|
|
2813
|
+
const score = distance(input, name.split(" ")[0] ?? name);
|
|
2814
|
+
if (score <= limit && (best === void 0 || score < best.score)) best = { name, score };
|
|
2815
|
+
}
|
|
2816
|
+
return best?.name;
|
|
2817
|
+
};
|
|
2818
|
+
|
|
2819
|
+
// src/core/dispatch.ts
|
|
2820
|
+
var unknownCommand = (registry, attempted) => {
|
|
2821
|
+
const suggestion = closest(attempted, registry.map(commandName));
|
|
2822
|
+
return suggestion === void 0 ? usageProblem("unknown_command", "error.unknown_command", "finitemonkey help", { command: attempted }) : usageProblem("unknown_command", "error.unknown_command_suggest", `finitemonkey ${suggestion}`, {
|
|
2823
|
+
command: attempted,
|
|
2824
|
+
suggestion
|
|
2825
|
+
});
|
|
2826
|
+
};
|
|
2827
|
+
var failWith = (ctx, name, problem) => ({
|
|
2828
|
+
name,
|
|
2829
|
+
execute: () => Promise.resolve(failureFrom(ctx.t, problem))
|
|
2830
|
+
});
|
|
2831
|
+
var invoke = (ctx, argv, spec, registry) => {
|
|
2832
|
+
const name = commandName(spec);
|
|
2833
|
+
const args = strictArgs(argv, spec);
|
|
2834
|
+
if (!args.ok) return failWith(ctx, name, args.error);
|
|
2835
|
+
return { name, execute: () => spec.run(ctx, args.value, registry) };
|
|
2836
|
+
};
|
|
2837
|
+
var builtin = (registry, name) => registry.find((spec) => commandName(spec) === name);
|
|
2838
|
+
var dispatch = (ctx, argv, loose, registry) => {
|
|
2839
|
+
const resolved = resolveCommand(registry, loose.positionals);
|
|
2840
|
+
const attempted = loose.positionals[0];
|
|
2841
|
+
if (resolved === void 0 && attempted !== void 0) {
|
|
2842
|
+
return failWith(ctx, attempted, unknownCommand(registry, attempted));
|
|
2843
|
+
}
|
|
2844
|
+
const wanted = loose.version && !loose.help ? "version" : "help";
|
|
2845
|
+
if (resolved === void 0 || loose.version && !loose.help) {
|
|
2846
|
+
const spec = builtin(registry, wanted);
|
|
2847
|
+
if (spec === void 0) return failWith(ctx, wanted, unknownCommand(registry, wanted));
|
|
2848
|
+
return { name: wanted, execute: () => spec.run(ctx, { values: {}, positionals: [] }, registry) };
|
|
2849
|
+
}
|
|
2850
|
+
if (loose.help) return { name: "help", execute: () => Promise.resolve(helpFor(ctx, resolved.spec)) };
|
|
2851
|
+
return invoke(ctx, argv, resolved.spec, registry);
|
|
2852
|
+
};
|
|
2853
|
+
|
|
2854
|
+
// src/core/output.ts
|
|
2855
|
+
var ISSUES_URL = "https://github.com/gabrielvieiraf/finitemonkey/issues";
|
|
2856
|
+
var resultFromThrown = (thrown, t) => {
|
|
2857
|
+
if (thrown instanceof FmError) return failureFrom(t, thrown.problem);
|
|
2858
|
+
const detail = thrown instanceof Error ? thrown.message : String(thrown);
|
|
2859
|
+
return failureFrom(t, {
|
|
2860
|
+
code: "internal_error",
|
|
2861
|
+
messageKey: "error.internal",
|
|
2862
|
+
params: { detail },
|
|
2863
|
+
fix: ISSUES_URL,
|
|
2864
|
+
exitCode: ExitCode.Internal
|
|
2865
|
+
});
|
|
2866
|
+
};
|
|
2867
|
+
var block = (items) => items.length > 0 ? `${items.join("\n")}
|
|
2868
|
+
` : "";
|
|
2869
|
+
var write = (sink, text2) => {
|
|
2870
|
+
if (text2 !== "") sink.write(maskText(text2));
|
|
2871
|
+
};
|
|
2872
|
+
var render = (ctx, command, result) => {
|
|
2873
|
+
if (result.raw !== void 0) {
|
|
2874
|
+
write(ctx.io.stdout, result.raw.stdout);
|
|
2875
|
+
write(ctx.io.stderr, result.raw.stderr);
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
if (ctx.json) {
|
|
2879
|
+
ctx.io.stdout.write(`${JSON.stringify(maskDeep(toEnvelope(command, result)))}
|
|
2880
|
+
`);
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
write(ctx.io.stdout, block(result.human ?? []));
|
|
2884
|
+
if (!result.ok) {
|
|
2885
|
+
const { message, fix } = result.error;
|
|
2886
|
+
write(ctx.io.stderr, block([ctx.t("output.error", { message }), ctx.t("output.fix", { fix })]));
|
|
2887
|
+
}
|
|
2888
|
+
if (result.next !== void 0) write(ctx.io.stdout, block([ctx.t("output.next", { next: result.next })]));
|
|
2889
|
+
};
|
|
2890
|
+
|
|
2891
|
+
// src/core/run.ts
|
|
2892
|
+
var run = async (options) => {
|
|
2893
|
+
const loose = looseArgs(options.argv);
|
|
2894
|
+
const detected = detectLang(loose.lang, options.env);
|
|
2895
|
+
const t = createTranslator(detected.lang);
|
|
2896
|
+
const ctx = {
|
|
2897
|
+
lang: detected.lang,
|
|
2898
|
+
langSource: detected.source,
|
|
2899
|
+
t,
|
|
2900
|
+
json: loose.json,
|
|
2901
|
+
yes: loose.yes,
|
|
2902
|
+
dryRun: loose.dryRun,
|
|
2903
|
+
cwd: options.cwd,
|
|
2904
|
+
env: options.env,
|
|
2905
|
+
platform: options.platform,
|
|
2906
|
+
version: options.version,
|
|
2907
|
+
io: options.io,
|
|
2908
|
+
runtime: options.runtime
|
|
2909
|
+
};
|
|
2910
|
+
let name = loose.positionals[0] ?? "";
|
|
2911
|
+
let result;
|
|
2912
|
+
try {
|
|
2913
|
+
if (loose.lang !== void 0 && normalizeLang(loose.lang) === void 0) {
|
|
2914
|
+
const problem = usageProblem("invalid_lang", "error.invalid_lang", "finitemonkey --lang en", {
|
|
2915
|
+
lang: loose.lang,
|
|
2916
|
+
languages: LANGUAGES.join(", ")
|
|
2917
|
+
});
|
|
2918
|
+
result = failureFrom(t, problem);
|
|
2919
|
+
} else {
|
|
2920
|
+
const dispatched = dispatch(ctx, options.argv, loose, options.registry ?? REGISTRY);
|
|
2921
|
+
name = dispatched.name;
|
|
2922
|
+
result = await dispatched.execute();
|
|
2923
|
+
}
|
|
2924
|
+
} catch (thrown) {
|
|
2925
|
+
result = resultFromThrown(thrown, t);
|
|
2926
|
+
}
|
|
2927
|
+
render(ctx, name, result);
|
|
2928
|
+
return result.exitCode;
|
|
2929
|
+
};
|
|
2930
|
+
|
|
2931
|
+
// src/core/version.ts
|
|
2932
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2933
|
+
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;
|
|
2937
|
+
const parsed = parseJson(text2.value);
|
|
2938
|
+
if (!parsed.ok || !isRecord(parsed.value)) return UNKNOWN_VERSION;
|
|
2939
|
+
const version = parsed.value["version"];
|
|
2940
|
+
return typeof version === "string" ? version : UNKNOWN_VERSION;
|
|
2941
|
+
};
|
|
2942
|
+
|
|
2943
|
+
// src/cli.ts
|
|
2944
|
+
var flush = (stream) => new Promise((resolve2) => {
|
|
2945
|
+
stream.write("", () => {
|
|
2946
|
+
resolve2();
|
|
2947
|
+
});
|
|
2948
|
+
});
|
|
2949
|
+
var main = async () => {
|
|
2950
|
+
const runtime = nodeRuntime();
|
|
2951
|
+
const code = await run({
|
|
2952
|
+
argv: process.argv.slice(2),
|
|
2953
|
+
env: process.env,
|
|
2954
|
+
cwd: toAbsolutePath(process.cwd()),
|
|
2955
|
+
platform: process.platform,
|
|
2956
|
+
io: { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin },
|
|
2957
|
+
version: readPackageVersion(runtime.readText, import.meta.url),
|
|
2958
|
+
runtime
|
|
2959
|
+
});
|
|
2960
|
+
await Promise.all([flush(process.stdout), flush(process.stderr)]);
|
|
2961
|
+
process.exit(code);
|
|
2962
|
+
};
|
|
2963
|
+
if (isEntryPoint(import.meta.url, process.argv[1], nodeRealpath, process.platform)) await main();
|
|
2964
|
+
export {
|
|
2965
|
+
main
|
|
2966
|
+
};
|