@epoch-agent/infra 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,3099 @@
1
+ import { platform, homedir, tmpdir } from 'os';
2
+ import { join, dirname, delimiter, resolve as resolve$1, isAbsolute, basename, win32, posix, relative } from 'path';
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, openSync, closeSync, writeSync, renameSync, rmSync, chmodSync, lstatSync, realpathSync, accessSync, constants } from 'fs';
4
+ import Database from 'better-sqlite3';
5
+ import { execFileSync, execFile, spawn } from 'child_process';
6
+ import { TextDecoder } from 'util';
7
+ import { createRequire } from 'module';
8
+ import { fileURLToPath } from 'url';
9
+ import { load } from 'js-yaml';
10
+ import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
11
+
12
+ // src/paths.ts
13
+ function resolveHomeDir() {
14
+ const explicit = process.env.EPOCH_HOME;
15
+ if (explicit) return explicit;
16
+ const profile = process.env.EPOCH_PROFILE || "default";
17
+ if (profile !== "default") return join(homedir(), ".epoch", "profiles", profile);
18
+ return join(homedir(), ".epoch");
19
+ }
20
+ function resolveProfile() {
21
+ return process.env.EPOCH_PROFILE || "default";
22
+ }
23
+ function configPath(homeDir = resolveHomeDir()) {
24
+ return join(homeDir, "config.yaml");
25
+ }
26
+ function envPath(homeDir = resolveHomeDir()) {
27
+ return join(homeDir, ".env");
28
+ }
29
+ function dbPath(homeDir = resolveHomeDir()) {
30
+ return join(homeDir, "sessions.db");
31
+ }
32
+ function memoriesDir(homeDir = resolveHomeDir()) {
33
+ return join(homeDir, "memories");
34
+ }
35
+ function skillsDir(homeDir = resolveHomeDir()) {
36
+ return join(homeDir, "skills");
37
+ }
38
+ function policiesDir(homeDir = resolveHomeDir()) {
39
+ return join(homeDir, "policies");
40
+ }
41
+ function hooksPath(homeDir = resolveHomeDir()) {
42
+ return join(homeDir, "hooks.json");
43
+ }
44
+ function keybindingsPath(homeDir = resolveHomeDir()) {
45
+ return join(homeDir, "keybindings.json");
46
+ }
47
+ function agentsDir(homeDir = resolveHomeDir()) {
48
+ return join(homeDir, "agents");
49
+ }
50
+ function commandsDir(homeDir = resolveHomeDir()) {
51
+ return join(homeDir, "commands");
52
+ }
53
+ function mcpConfigPath(homeDir = resolveHomeDir()) {
54
+ return join(homeDir, "mcp.json");
55
+ }
56
+ function pluginsDir(homeDir = resolveHomeDir()) {
57
+ return join(homeDir, "plugins");
58
+ }
59
+ function pluginsStatePath(homeDir = resolveHomeDir()) {
60
+ return join(homeDir, "plugins.json");
61
+ }
62
+ function marketplacesPath(homeDir = resolveHomeDir()) {
63
+ return join(homeDir, "marketplaces.json");
64
+ }
65
+ function approvalsPath(homeDir = resolveHomeDir()) {
66
+ return join(homeDir, "approvals.json");
67
+ }
68
+ function trustPath(homeDir = resolveHomeDir()) {
69
+ return join(homeDir, "trusted-folders.json");
70
+ }
71
+ function trustedImportsPath(homeDir = resolveHomeDir()) {
72
+ return join(homeDir, "trusted-imports.json");
73
+ }
74
+ function workspacesPath(homeDir = resolveHomeDir()) {
75
+ return join(homeDir, "workspaces.json");
76
+ }
77
+ function budgetStatePath(homeDir = resolveHomeDir()) {
78
+ return join(homeDir, "budget.json");
79
+ }
80
+ function mcpAuthPath(homeDir = resolveHomeDir()) {
81
+ return join(homeDir, "mcp-auth.json");
82
+ }
83
+ function mcpSchemaCachePath(homeDir = resolveHomeDir()) {
84
+ return join(homeDir, "cache", "mcp-schema-cache.json");
85
+ }
86
+ function artifactsDir(sessionId, homeDir = resolveHomeDir()) {
87
+ return bySession(join(homeDir, "artifacts"), sessionId);
88
+ }
89
+ function checkpointsDir(sessionId, homeDir = resolveHomeDir()) {
90
+ return bySession(join(homeDir, "checkpoints"), sessionId);
91
+ }
92
+ function worktreesDir(homeDir = resolveHomeDir()) {
93
+ return join(homeDir, "worktrees");
94
+ }
95
+ function automationDir(homeDir = resolveHomeDir()) {
96
+ return join(homeDir, "automation");
97
+ }
98
+ function automationWorkDir(taskId, homeDir = resolveHomeDir()) {
99
+ return byTask(automationDir(homeDir), taskId);
100
+ }
101
+ function automationLogsDir(taskId, homeDir = resolveHomeDir()) {
102
+ return byTask(join(automationDir(homeDir), "logs"), taskId);
103
+ }
104
+ function byTask(base, taskId) {
105
+ const safe = taskId.replace(/[^A-Za-z0-9_-]/g, "_");
106
+ return join(base, safe.length > 0 ? safe : "unknown");
107
+ }
108
+ function bySession(base, sessionId) {
109
+ if (sessionId === void 0) return base;
110
+ const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, "_");
111
+ return join(base, safe.length > 0 ? safe : "unknown");
112
+ }
113
+ var PROJECT_DIR_NAME = ".epoch";
114
+ function projectSettingsPath(projectRoot) {
115
+ return join(projectRoot, PROJECT_DIR_NAME, "settings.json");
116
+ }
117
+ function projectLocalSettingsPath(projectRoot) {
118
+ return join(projectRoot, PROJECT_DIR_NAME, "settings.local.json");
119
+ }
120
+ function projectSchemasDir(projectRoot) {
121
+ return join(projectRoot, PROJECT_DIR_NAME, "schemas");
122
+ }
123
+ function projectCommandsDir(projectRoot) {
124
+ return join(projectRoot, PROJECT_DIR_NAME, "commands");
125
+ }
126
+ function projectSkillsDir(projectRoot) {
127
+ return join(projectRoot, PROJECT_DIR_NAME, "skills");
128
+ }
129
+ function projectHooksPath(projectRoot) {
130
+ return join(projectRoot, PROJECT_DIR_NAME, "hooks.json");
131
+ }
132
+ function projectPoliciesDir(projectRoot) {
133
+ return join(projectRoot, PROJECT_DIR_NAME, "policies");
134
+ }
135
+ function managedSettingsPath(os = platform()) {
136
+ const FILE = "managed-settings.json";
137
+ if (os === "win32") {
138
+ return join(process.env.ProgramData || "C:\\ProgramData", "epoch", FILE);
139
+ }
140
+ if (os === "darwin") return join("/Library", "Application Support", "epoch", FILE);
141
+ return join("/etc", "epoch", FILE);
142
+ }
143
+ var DEFAULT_YAML = "provider:\n type: openai\n apiKey: ''\nmodel: gpt-4o-mini\nmaxTurns: 50\n";
144
+ function setScalar(yaml, key, value) {
145
+ const pattern = new RegExp(`^${key}:.*$`, "m");
146
+ if (pattern.test(yaml)) return yaml.replace(pattern, `${key}: ${value}`);
147
+ return yaml.trimEnd() + `
148
+ ${key}: ${value}
149
+ `;
150
+ }
151
+ function setSectionField(yaml, section, field, value) {
152
+ const entry = ` ${field}: ${value}`;
153
+ const header = new RegExp(`^${section}:[ \\t]*$`, "m");
154
+ if (!header.test(yaml)) {
155
+ return `${yaml.trimEnd()}
156
+ ${section}:
157
+ ${entry}
158
+ `;
159
+ }
160
+ const existing = new RegExp(`^(${section}:[ \\t]*\\n(?:[ \\t]+.*\\n)*?)[ \\t]+${field}:.*$`, "m");
161
+ if (existing.test(yaml)) return yaml.replace(existing, `$1${entry}`);
162
+ return yaml.replace(header, `${section}:
163
+ ${entry}`);
164
+ }
165
+ function readKey(yaml, key) {
166
+ const dot = key.indexOf(".");
167
+ if (dot === -1) {
168
+ return cleanValue(new RegExp(`^${key}:[ \\t]*(.*)$`, "m").exec(yaml)?.[1]);
169
+ }
170
+ const section = key.slice(0, dot);
171
+ const field = key.slice(dot + 1);
172
+ const block = new RegExp(`^${section}:[ \\t]*\\n((?:[ \\t]+.*\\n?)*)`, "m").exec(yaml)?.[1];
173
+ if (block === void 0) return void 0;
174
+ return cleanValue(new RegExp(`^[ \\t]+${field}:[ \\t]*(.*)$`, "m").exec(block)?.[1]);
175
+ }
176
+ function cleanValue(raw) {
177
+ if (raw === void 0) return void 0;
178
+ const trimmed = raw.trim();
179
+ const quoted = /^(['"])(.*)\1$/.exec(trimmed);
180
+ const value = quoted ? quoted[2] ?? "" : trimmed.replace(/\s+#.*$/, "").trim();
181
+ return value === "" ? void 0 : value;
182
+ }
183
+ function unsetScalar(yaml, key) {
184
+ const pattern = new RegExp(`^${key}:.*\\n?`, "m");
185
+ return yaml.replace(pattern, "");
186
+ }
187
+ function unsetSectionField(yaml, section, field) {
188
+ const pattern = new RegExp(
189
+ `^(${section}:[ \\t]*\\n(?:[ \\t]+.*\\n)*?)[ \\t]+${field}:.*\\n?`,
190
+ "m"
191
+ );
192
+ return dropEmptySection(yaml.replace(pattern, "$1"), section);
193
+ }
194
+ function dropEmptySection(yaml, section) {
195
+ const empty = new RegExp(`^${section}:[ \\t]*(?:\\n(?![ \\t]+\\S)|(?![\\s\\S]))`, "m");
196
+ return yaml.replace(empty, "");
197
+ }
198
+ function readConfigYaml(path) {
199
+ return existsSync(path) ? readFileSync(path, "utf-8") : DEFAULT_YAML;
200
+ }
201
+ function writeConfigYaml(path, yaml) {
202
+ writeFileSync(path, yaml, "utf-8");
203
+ }
204
+ var DIR_MODE = 448;
205
+ var FILE_MODE = 384;
206
+ var MAX_NAME_ATTEMPTS = 8;
207
+ function sanitize(name) {
208
+ return name.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 64);
209
+ }
210
+ function resolveExisting(path, onExisting) {
211
+ if (onExisting !== "reuse") return void 0;
212
+ try {
213
+ return lstatSync(path).isFile() ? path : void 0;
214
+ } catch {
215
+ return void 0;
216
+ }
217
+ }
218
+ function openArtifact(dir, name, extension, onExisting = "unique") {
219
+ try {
220
+ mkdirSync(dir, { recursive: true, mode: DIR_MODE });
221
+ } catch {
222
+ return void 0;
223
+ }
224
+ const stem = sanitize(name);
225
+ for (let attempt = 0; attempt < MAX_NAME_ATTEMPTS; attempt += 1) {
226
+ const suffix = attempt === 0 ? "" : `-${attempt}`;
227
+ const path = join(dir, `${stem}${suffix}${extension}`);
228
+ let fd;
229
+ try {
230
+ fd = openSync(path, "wx", FILE_MODE);
231
+ } catch (err) {
232
+ if (err.code !== "EEXIST") return void 0;
233
+ const reused = resolveExisting(path, onExisting);
234
+ if (reused) return { path: reused, append: () => {
235
+ }, close: () => {
236
+ } };
237
+ continue;
238
+ }
239
+ return makeHandle(path, fd);
240
+ }
241
+ return void 0;
242
+ }
243
+ function makeHandle(path, fd) {
244
+ let open = true;
245
+ return {
246
+ path,
247
+ append(data) {
248
+ if (!open) return;
249
+ try {
250
+ writeSync(fd, typeof data === "string" ? Buffer.from(data, "utf-8") : data);
251
+ } catch {
252
+ open = false;
253
+ }
254
+ },
255
+ close() {
256
+ if (!open) return;
257
+ open = false;
258
+ try {
259
+ closeSync(fd);
260
+ } catch {
261
+ }
262
+ }
263
+ };
264
+ }
265
+ function writeArtifact(dir, name, extension, data, onExisting = "unique") {
266
+ const handle = openArtifact(dir, name, extension, onExisting);
267
+ if (!handle) return void 0;
268
+ handle.append(data);
269
+ handle.close();
270
+ return handle.path;
271
+ }
272
+
273
+ // src/text-slice.ts
274
+ function isHighSurrogate(code) {
275
+ return code >= 55296 && code <= 56319;
276
+ }
277
+ function isLowSurrogate(code) {
278
+ return code >= 56320 && code <= 57343;
279
+ }
280
+ function headEnd(text, count) {
281
+ let index = 0;
282
+ let seen = 0;
283
+ while (index < text.length && seen < count) {
284
+ const isPair = isHighSurrogate(text.charCodeAt(index)) && index + 1 < text.length;
285
+ index += isPair ? 2 : 1;
286
+ seen += 1;
287
+ }
288
+ return index;
289
+ }
290
+ function tailStart(text, count) {
291
+ let index = text.length;
292
+ let seen = 0;
293
+ while (index > 0 && seen < count) {
294
+ const isPair = isLowSurrogate(text.charCodeAt(index - 1)) && index - 2 >= 0;
295
+ index -= isPair ? 2 : 1;
296
+ seen += 1;
297
+ }
298
+ return index;
299
+ }
300
+ function countCodePoints(text) {
301
+ let count = 0;
302
+ for (let i = 0; i < text.length; i += 1) {
303
+ if (isHighSurrogate(text.charCodeAt(i)) && i + 1 < text.length) i += 1;
304
+ count += 1;
305
+ }
306
+ return count;
307
+ }
308
+ function headCodePoints(text, count) {
309
+ if (count <= 0) return "";
310
+ return text.slice(0, headEnd(text, count));
311
+ }
312
+ function tailCodePoints(text, count) {
313
+ if (count <= 0) return "";
314
+ return text.slice(tailStart(text, count));
315
+ }
316
+ function keepHeadAndTail(text, head, tail, join9) {
317
+ const total = countCodePoints(text);
318
+ if (total <= head + tail) return text;
319
+ const front = headCodePoints(text, head);
320
+ const back = tailCodePoints(text, tail);
321
+ return `${front}${join9(total - head - tail)}${back}`;
322
+ }
323
+ var pool = /* @__PURE__ */ new Map();
324
+ function openDatabase(path) {
325
+ if (path === ":memory:") {
326
+ const db2 = new Database(path);
327
+ db2.pragma("foreign_keys = ON");
328
+ return db2;
329
+ }
330
+ const existing = pool.get(path);
331
+ if (existing) {
332
+ existing.refs++;
333
+ return existing.db;
334
+ }
335
+ mkdirSync(dirname(path), { recursive: true });
336
+ const db = new Database(path);
337
+ db.pragma("journal_mode = WAL");
338
+ db.pragma("foreign_keys = ON");
339
+ pool.set(path, { db, refs: 1 });
340
+ return db;
341
+ }
342
+ function releaseDatabase(path) {
343
+ const entry = pool.get(path);
344
+ if (!entry) return;
345
+ entry.refs--;
346
+ if (entry.refs > 0) return;
347
+ pool.delete(path);
348
+ try {
349
+ entry.db.close();
350
+ } catch {
351
+ }
352
+ }
353
+ function refCount(path) {
354
+ return pool.get(path)?.refs ?? 0;
355
+ }
356
+ function closeAllDatabases() {
357
+ for (const [path, entry] of pool) {
358
+ try {
359
+ entry.db.close();
360
+ } catch {
361
+ }
362
+ pool.delete(path);
363
+ }
364
+ }
365
+ var MIGRATION_TABLE = `
366
+ CREATE TABLE IF NOT EXISTS _epoch_migrations (
367
+ namespace TEXT NOT NULL,
368
+ version INTEGER NOT NULL,
369
+ description TEXT NOT NULL,
370
+ applied_at TEXT NOT NULL,
371
+ id TEXT,
372
+ PRIMARY KEY (namespace, version)
373
+ );
374
+ `;
375
+ function ensureIdColumn(db) {
376
+ const columns = db.prepare("PRAGMA table_info(_epoch_migrations)").all();
377
+ if (!columns.some((c) => c.name === "id")) {
378
+ db.exec("ALTER TABLE _epoch_migrations ADD COLUMN id TEXT");
379
+ }
380
+ }
381
+ function reconcile(db, namespace, migrations) {
382
+ const rows = db.prepare("SELECT version, id, description FROM _epoch_migrations WHERE namespace = ?").all(namespace);
383
+ const byId = /* @__PURE__ */ new Map();
384
+ const legacyByDescription = /* @__PURE__ */ new Map();
385
+ for (const row of rows) {
386
+ if (row.id !== null) byId.set(row.id, row);
387
+ else if (!legacyByDescription.has(row.description))
388
+ legacyByDescription.set(row.description, row);
389
+ }
390
+ const pending2 = [];
391
+ const repairs = [];
392
+ const claimed = /* @__PURE__ */ new Set();
393
+ for (const m of migrations) {
394
+ const row = byId.get(m.id) ?? legacyByDescription.get(m.description);
395
+ if (!row) {
396
+ pending2.push(m);
397
+ continue;
398
+ }
399
+ claimed.add(row.version);
400
+ if (row.id !== m.id || row.version !== m.version || row.description !== m.description) {
401
+ repairs.push({ from: row.version, to: m });
402
+ }
403
+ }
404
+ const unclaimed = rows.filter((r) => !claimed.has(r.version));
405
+ for (const m of pending2) {
406
+ const blocker = unclaimed.find((r) => r.version === m.version);
407
+ if (blocker) {
408
+ throw new Error(
409
+ `\u8FC1\u79FB\u7248\u672C\u53F7\u649E\u4E86\uFF1A${namespace} \u7684 v${m.version} \u60F3\u7ED9\u300C${m.id}\u300D\u7528\uFF0C\u800C\u5E93\u91CC v${m.version} \u8BB0\u7684\u662F\u300C${blocker.description}\u300D\uFF08id=${blocker.id ?? "\u65E0\uFF0C\u8001\u5E93"}\uFF09\u3002\u8FD9\u4E2A\u5E93\u8DD1\u8FC7\u522B\u7684\u5206\u652F\uFF0C\u6216\u8005\u67D0\u6761\u8FC1\u79FB\u7684 id \u88AB\u6539\u8FC7\u4E86\u3002\u522B\u9760\u6539\u7248\u672C\u53F7\u7ED5\u8FC7\u53BB \u2014\u2014 \u5148\u5F04\u6E05\u5E93\u91CC\u90A3\u6761\u5230\u5E95\u505A\u8FC7\u4EC0\u4E48`
410
+ );
411
+ }
412
+ }
413
+ if (repairs.length > 0) {
414
+ const park = db.prepare(
415
+ "UPDATE _epoch_migrations SET version = ? WHERE namespace = ? AND version = ?"
416
+ );
417
+ const place = db.prepare(
418
+ "UPDATE _epoch_migrations SET version = ?, id = ?, description = ? WHERE namespace = ? AND version = ?"
419
+ );
420
+ const parkBase = Math.min(0, ...rows.map((r) => r.version)) - 1;
421
+ db.transaction(() => {
422
+ repairs.forEach((r, i) => park.run(parkBase - i, namespace, r.from));
423
+ repairs.forEach(
424
+ (r, i) => place.run(r.to.version, r.to.id, r.to.description, namespace, parkBase - i)
425
+ );
426
+ })();
427
+ }
428
+ return pending2;
429
+ }
430
+ function migrate(db, namespace, migrations) {
431
+ db.exec(MIGRATION_TABLE);
432
+ ensureIdColumn(db);
433
+ const ordered = [...migrations].sort((a, b) => a.version - b.version);
434
+ for (const key of ["id", "version"]) {
435
+ const seen = /* @__PURE__ */ new Map();
436
+ for (const m of ordered) {
437
+ const other = seen.get(m[key]);
438
+ if (other) {
439
+ throw new Error(
440
+ `${namespace} \u6709\u4E24\u6761\u8FC1\u79FB\u7684 ${key} \u90FD\u662F ${String(m[key])}\uFF1A\u300C${other.description}\u300D\u548C\u300C${m.description}\u300D\u3002\u4E24\u4E2A\u5206\u652F\u5404\u5199\u4E86\u4E00\u6761\u662F\u5E38\u4E8B \u2014\u2014 version \u76F4\u63A5\u6539\u53F7\uFF08\u8BA4\u8D26\u9760 id\uFF0C\u6539\u53F7\u5B89\u5168\uFF09\uFF1Bid \u649E\u4E86\u8BF4\u660E\u662F\u590D\u5236\u7C98\u8D34\u6F0F\u6539\uFF0C\u6362\u4E00\u4E2A`
441
+ );
442
+ }
443
+ seen.set(m[key], m);
444
+ }
445
+ }
446
+ const record = db.prepare(
447
+ "INSERT INTO _epoch_migrations (namespace, version, description, applied_at, id) VALUES (?, ?, ?, ?, ?)"
448
+ );
449
+ for (const m of reconcile(db, namespace, ordered)) {
450
+ const run2 = db.transaction(() => {
451
+ m.up(db);
452
+ record.run(namespace, m.version, m.description, (/* @__PURE__ */ new Date()).toISOString(), m.id);
453
+ });
454
+ run2();
455
+ }
456
+ }
457
+ function schemaVersion(db, namespace) {
458
+ db.exec(MIGRATION_TABLE);
459
+ const row = db.prepare("SELECT MAX(version) AS v FROM _epoch_migrations WHERE namespace = ?").get(namespace);
460
+ return row?.v ?? 0;
461
+ }
462
+
463
+ // src/mask.ts
464
+ function maskApiKey(key) {
465
+ if (!key || key.length < 8) return "***";
466
+ return `${key.slice(0, 3)}-***${key.slice(-4)}`;
467
+ }
468
+ var SENSITIVE_WORDS = [
469
+ "key",
470
+ "token",
471
+ "secret",
472
+ "password",
473
+ "passwd",
474
+ "credential",
475
+ "auth",
476
+ "bearer",
477
+ "apikey"
478
+ ];
479
+ function isSensitiveKey(name) {
480
+ const lower = name.toLowerCase();
481
+ return SENSITIVE_WORDS.some((word) => lower.includes(word));
482
+ }
483
+ function maskSensitive(data) {
484
+ const masked = {};
485
+ for (const [k, v] of Object.entries(data)) {
486
+ if (isSensitiveKey(k)) {
487
+ masked[k] = typeof v === "string" ? maskApiKey(v) : "***";
488
+ } else {
489
+ masked[k] = v;
490
+ }
491
+ }
492
+ return masked;
493
+ }
494
+
495
+ // src/logger.ts
496
+ var currentLevel = "info";
497
+ function setLogLevel(level) {
498
+ currentLevel = level;
499
+ }
500
+ var LEVEL_RANK = { debug: 0, info: 1, warn: 2, error: 3 };
501
+ function shouldLog(level) {
502
+ return LEVEL_RANK[level] >= LEVEL_RANK[currentLevel];
503
+ }
504
+ function emit(entry) {
505
+ const line = JSON.stringify({
506
+ ...entry,
507
+ data: entry.data ? maskSensitive(entry.data) : void 0
508
+ });
509
+ if (entry.level === "error") process.stderr.write(line + "\n");
510
+ else process.stdout.write(line + "\n");
511
+ }
512
+ function createLogger(module) {
513
+ return {
514
+ debug(msg, data) {
515
+ if (shouldLog("debug"))
516
+ emit({ level: "debug", module, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data });
517
+ },
518
+ info(msg, data) {
519
+ if (shouldLog("info"))
520
+ emit({ level: "info", module, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data });
521
+ },
522
+ warn(msg, data) {
523
+ if (shouldLog("warn"))
524
+ emit({ level: "warn", module, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data });
525
+ },
526
+ error(msg, data) {
527
+ if (shouldLog("error"))
528
+ emit({ level: "error", module, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data });
529
+ }
530
+ };
531
+ }
532
+ var IS_WINDOWS = platform() === "win32";
533
+ var WINDOWS_HIDE_FLAGS = IS_WINDOWS ? { windowsHide: true } : {};
534
+ function getPythonCommand() {
535
+ if (IS_WINDOWS) return "python";
536
+ return "python3";
537
+ }
538
+ var SHELL_KINDS = ["cmd", "powershell", "pwsh"];
539
+ var SHELL_EXECUTABLES = {
540
+ cmd: "cmd.exe",
541
+ // Windows PowerShell 5.1,随系统自带
542
+ powershell: "powershell.exe",
543
+ // PowerShell 7+,要自己装
544
+ pwsh: "pwsh.exe"
545
+ };
546
+ var configuredShell = null;
547
+ function setShell(kind) {
548
+ if (!IS_WINDOWS) return;
549
+ configuredShell = kind ?? null;
550
+ }
551
+ function resolveShellKind() {
552
+ if (!IS_WINDOWS) return "cmd";
553
+ const fromEnv = SHELL_KINDS.find((k) => k === process.env["EPOCH_SHELL"]);
554
+ return fromEnv ?? configuredShell ?? "cmd";
555
+ }
556
+ function getDefaultShell() {
557
+ return IS_WINDOWS ? SHELL_EXECUTABLES[resolveShellKind()] : "/bin/sh";
558
+ }
559
+ function probeShell(kind) {
560
+ if (!IS_WINDOWS) return null;
561
+ const exe = SHELL_EXECUTABLES[kind];
562
+ const dirs = (process.env["PATH"] ?? "").split(delimiter).filter(Boolean);
563
+ for (const dir of dirs) {
564
+ if (existsSync(join(dir, exe))) return null;
565
+ }
566
+ return `shell: \u914D\u7F6E\u7684 ${kind}\uFF08${exe}\uFF09\u4E0D\u5728 PATH \u4E0A\uFF0C\u5DF2\u56DE\u9000\u5230 cmd.exe`;
567
+ }
568
+ var PS_EXIT_TRAILER = [
569
+ "$__epochOk = $?",
570
+ "$__epochCode = $LASTEXITCODE",
571
+ "if ($null -eq $__epochCode) { if ($__epochOk) { exit 0 } else { exit 1 } }",
572
+ "exit $__epochCode"
573
+ ].join("\r\n");
574
+ function powershellCommandArg(command) {
575
+ const escape = (s) => s.replaceAll('"', String.raw`\"`);
576
+ return `-NoProfile -NonInteractive -Command "${escape(command)}\r
577
+ ${escape(PS_EXIT_TRAILER)}"`;
578
+ }
579
+ function shellSpawnArgs(command) {
580
+ if (!IS_WINDOWS) return { file: getDefaultShell(), args: ["-c", command], options: {} };
581
+ const file = getDefaultShell();
582
+ if (resolveShellKind() === "cmd") {
583
+ return {
584
+ file,
585
+ args: ["/d", "/s", "/c", `"${command}"`],
586
+ options: { windowsVerbatimArguments: true }
587
+ };
588
+ }
589
+ return {
590
+ file,
591
+ args: [powershellCommandArg(command)],
592
+ options: { windowsVerbatimArguments: true }
593
+ };
594
+ }
595
+ function shellPtyArgs(command) {
596
+ if (!IS_WINDOWS) return { file: getDefaultShell(), args: ["-c", command] };
597
+ const file = getDefaultShell();
598
+ if (resolveShellKind() === "cmd") return { file, args: `/d /s /c "${command}"` };
599
+ return { file, args: powershellCommandArg(command) };
600
+ }
601
+ function normalizeForMatch(path) {
602
+ return path.replaceAll("\\", "/").toLowerCase();
603
+ }
604
+ function encodingForCodePage(codePage) {
605
+ const table = {
606
+ 65001: "utf-8",
607
+ 936: "gbk",
608
+ // 简体中文
609
+ 950: "big5",
610
+ // 繁体中文
611
+ 932: "shift_jis",
612
+ // 日文
613
+ 949: "euc-kr",
614
+ // 韩文
615
+ 866: "ibm866",
616
+ // 西里尔 OEM
617
+ 874: "windows-874",
618
+ 1250: "windows-1250",
619
+ 1251: "windows-1251",
620
+ 1252: "windows-1252",
621
+ 1253: "windows-1253",
622
+ 1254: "windows-1254",
623
+ 1255: "windows-1255",
624
+ 1256: "windows-1256",
625
+ 1257: "windows-1257",
626
+ 1258: "windows-1258"
627
+ };
628
+ return table[codePage] ?? "utf-8";
629
+ }
630
+ var cachedConsoleEncoding = null;
631
+ function detectConsoleEncoding() {
632
+ if (!IS_WINDOWS) return "utf-8";
633
+ if (cachedConsoleEncoding !== null) return cachedConsoleEncoding;
634
+ let encoding = "utf-8";
635
+ try {
636
+ const out = execFileSync("chcp.com", { encoding: "latin1", windowsHide: true, timeout: 3e3 });
637
+ const nums = out.match(/\d+/g);
638
+ const last = nums?.[nums.length - 1];
639
+ if (last) encoding = encodingForCodePage(Number(last));
640
+ } catch {
641
+ }
642
+ cachedConsoleEncoding = encoding;
643
+ return encoding;
644
+ }
645
+ var EMPTY = Buffer.alloc(0);
646
+ function makeDecoder(encoding) {
647
+ try {
648
+ return new TextDecoder(encoding);
649
+ } catch {
650
+ return new TextDecoder("utf-8");
651
+ }
652
+ }
653
+ function asciiPrefixLength(bytes) {
654
+ let i = 0;
655
+ while (i < bytes.length && bytes[i] < 128) i++;
656
+ return i;
657
+ }
658
+ function createStreamDecoder(fallbackEncoding = detectConsoleEncoding()) {
659
+ let decoder = null;
660
+ let pending2 = EMPTY;
661
+ const decide = (rest) => {
662
+ try {
663
+ const probe = new TextDecoder("utf-8", { fatal: true }).decode(rest, { stream: true });
664
+ return probe.length === 0 ? null : makeDecoder("utf-8");
665
+ } catch {
666
+ return makeDecoder(fallbackEncoding);
667
+ }
668
+ };
669
+ return {
670
+ write(chunk) {
671
+ if (decoder) return decoder.decode(chunk, { stream: true });
672
+ const buf = pending2.length === 0 ? chunk : Buffer.concat([pending2, chunk]);
673
+ const cut = asciiPrefixLength(buf);
674
+ const ascii = buf.subarray(0, cut).toString("latin1");
675
+ const rest = buf.subarray(cut);
676
+ if (rest.length === 0) {
677
+ pending2 = EMPTY;
678
+ return ascii;
679
+ }
680
+ const chosen = decide(rest);
681
+ if (!chosen) {
682
+ pending2 = rest;
683
+ return ascii;
684
+ }
685
+ decoder = chosen;
686
+ pending2 = EMPTY;
687
+ return ascii + chosen.decode(rest, { stream: true });
688
+ },
689
+ end() {
690
+ const final = decoder ?? makeDecoder("utf-8");
691
+ decoder = final;
692
+ const tail = pending2;
693
+ pending2 = EMPTY;
694
+ return (tail.length > 0 ? final.decode(tail, { stream: true }) : "") + final.decode();
695
+ }
696
+ };
697
+ }
698
+ var RIPGREP_INSTALL_HINT = IS_WINDOWS ? "winget install BurntSushi.ripgrep.MSVC\uFF08\u6216 scoop install ripgrep\uFF09" : process.platform === "darwin" ? "brew install ripgrep" : "apt install ripgrep / dnf install ripgrep / pacman -S ripgrep";
699
+ function ensureExecutable(path) {
700
+ try {
701
+ accessSync(path, constants.X_OK);
702
+ return true;
703
+ } catch {
704
+ }
705
+ try {
706
+ chmodSync(path, 493);
707
+ accessSync(path, constants.X_OK);
708
+ return true;
709
+ } catch {
710
+ return false;
711
+ }
712
+ }
713
+ function probeSystem(env, windows, exists) {
714
+ const api = windows ? win32 : posix;
715
+ const dirs = (env["PATH"] ?? "").split(api.delimiter).filter(Boolean);
716
+ const exts = windows ? (env["PATHEXT"] ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
717
+ for (const dir of dirs) {
718
+ for (const ext of exts) {
719
+ if (exists(api.join(dir, `rg${ext.toLowerCase()}`))) return true;
720
+ }
721
+ }
722
+ return false;
723
+ }
724
+ var RIPGREP_VENDOR_TARGETS = [
725
+ { platform: "win32", arch: "x64", pkg: "@epoch-agent/vendor-ripgrep-win32-x64" },
726
+ { platform: "win32", arch: "arm64", pkg: "@epoch-agent/vendor-ripgrep-win32-x64" },
727
+ { platform: "darwin", arch: "x64", pkg: "@epoch-agent/vendor-ripgrep-darwin-x64" },
728
+ { platform: "darwin", arch: "arm64", pkg: "@epoch-agent/vendor-ripgrep-darwin-arm64" }
729
+ ];
730
+ function vendorPackageFor(platform10, arch) {
731
+ return RIPGREP_VENDOR_TARGETS.find((t2) => t2.platform === platform10 && t2.arch === arch)?.pkg ?? null;
732
+ }
733
+ var require_ = createRequire(import.meta.url);
734
+ function defaultResolveVendor(specifier) {
735
+ const manifest = `${specifier}/package.json`;
736
+ const roots = [require_, createRequire(join(process.cwd(), "noop.js"))];
737
+ for (const resolver of roots) {
738
+ try {
739
+ return resolver.resolve(manifest);
740
+ } catch {
741
+ }
742
+ }
743
+ return null;
744
+ }
745
+ function readVendorVersion(manifestPath) {
746
+ try {
747
+ const pkg = require_(manifestPath);
748
+ return typeof pkg.version === "string" ? pkg.version : void 0;
749
+ } catch {
750
+ return void 0;
751
+ }
752
+ }
753
+ function resolve(options) {
754
+ const env = options.env ?? process.env;
755
+ const platform10 = options.platform ?? process.platform;
756
+ const arch = options.arch ?? process.arch;
757
+ const exists = options.fileExists ?? existsSync;
758
+ const resolveVendor = options.resolveVendor ?? defaultResolveVendor;
759
+ const makeExecutable = options.makeExecutable ?? ensureExecutable;
760
+ if (probeSystem(env, platform10 === "win32", exists)) {
761
+ return { mode: "system", command: "rg" };
762
+ }
763
+ const vendor = vendorPackageFor(platform10, arch);
764
+ if (vendor) {
765
+ let manifest = null;
766
+ try {
767
+ manifest = resolveVendor(vendor);
768
+ } catch {
769
+ manifest = null;
770
+ }
771
+ if (manifest) {
772
+ const api = platform10 === "win32" ? win32 : posix;
773
+ const binary = api.join(api.dirname(manifest), "bin", platform10 === "win32" ? "rg.exe" : "rg");
774
+ if (exists(binary) && (platform10 === "win32" || makeExecutable(binary))) {
775
+ const version = readVendorVersion(manifest);
776
+ return { mode: "builtin", command: binary, ...version ? { version } : {} };
777
+ }
778
+ }
779
+ }
780
+ return { mode: "missing", command: "rg", hint: RIPGREP_INSTALL_HINT };
781
+ }
782
+ var cached = null;
783
+ function resolveRipgrep(options) {
784
+ if (options) return resolve(options);
785
+ cached ??= resolve({});
786
+ return cached;
787
+ }
788
+ function resetRipgrepCache() {
789
+ cached = null;
790
+ }
791
+ var SIGKILL_TIMEOUT_MS = 200;
792
+ var PROBE_TIMEOUT_MS = 5e3;
793
+ var MAX_TREE_DEPTH = 32;
794
+ var MAX_TREE_SIZE = 512;
795
+ var SNAPSHOT_MAX_BUFFER = 8 * 1024 * 1024;
796
+ var WMIC_SNAPSHOT_ARGS = [
797
+ "process",
798
+ "get",
799
+ "CreationDate,ParentProcessId,ProcessId",
800
+ "/format:csv"
801
+ ];
802
+ var POWERSHELL_SNAPSHOT_ARGS = [
803
+ "-NoProfile",
804
+ "-NonInteractive",
805
+ "-Command",
806
+ 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId) $($_.CreationDate.Ticks)" }'
807
+ ];
808
+ function isRealChildPid(pid) {
809
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 1;
810
+ }
811
+ async function killProcessTree(options) {
812
+ const { pid, pty, detached = false, platform: platform10 = platform() } = options;
813
+ if (!isRealChildPid(pid)) return;
814
+ if (platform10 === "win32") {
815
+ const descendants2 = await collectProcessTree(pid, platform10);
816
+ tryPtyKill(pty);
817
+ await killPids([...descendants2, pid], { signal: "SIGKILL" });
818
+ await runProbe("taskkill", ["/pid", String(pid), "/f", "/t"]);
819
+ return;
820
+ }
821
+ const descendants = await collectProcessTree(pid, platform10);
822
+ await killPids([...descendants, pid], options, {
823
+ // 只有 detached 才允许碰 -pid —— 这一行就是文件头那条约束的落点
824
+ ...detached ? { groupLeaderPid: pid } : {},
825
+ ...pty ? { pty } : {}
826
+ });
827
+ }
828
+ async function killPids(pids, options = {}, extra = {}) {
829
+ const { escalate = false, isExited = () => false, killTimeoutMs = SIGKILL_TIMEOUT_MS } = options;
830
+ const targets = pids.filter(isRealChildPid);
831
+ const first = options.signal ?? (escalate ? "SIGTERM" : "SIGKILL");
832
+ sweep(targets, first, extra);
833
+ if (!escalate || isExited()) return;
834
+ await delay(killTimeoutMs);
835
+ if (isExited()) return;
836
+ sweep(targets, "SIGKILL", extra);
837
+ }
838
+ function sweep(targets, signal, extra) {
839
+ if (extra.groupLeaderPid !== void 0) sendSignal(-extra.groupLeaderPid, signal);
840
+ for (const target of targets) sendSignal(target, signal);
841
+ tryPtyKill(extra.pty, signal);
842
+ }
843
+ function sendSignal(target, signal) {
844
+ try {
845
+ process.kill(target, signal);
846
+ } catch {
847
+ }
848
+ }
849
+ function tryPtyKill(pty, signal) {
850
+ if (!pty) return;
851
+ try {
852
+ pty.kill(signal);
853
+ } catch {
854
+ }
855
+ }
856
+ async function collectProcessTree(rootPid, platform10 = platform()) {
857
+ if (!isRealChildPid(rootPid)) return [];
858
+ const listChildren = platform10 === "win32" ? await snapshotLister() : listChildrenViaPgrep;
859
+ const found = [];
860
+ const seen = /* @__PURE__ */ new Set([rootPid]);
861
+ let frontier = [rootPid];
862
+ for (let depth = 0; depth < MAX_TREE_DEPTH && frontier.length > 0; depth++) {
863
+ const next = [];
864
+ for (const parent of frontier) {
865
+ for (const child of await listChildren(parent)) {
866
+ if (seen.has(child) || found.length >= MAX_TREE_SIZE) continue;
867
+ seen.add(child);
868
+ found.push(child);
869
+ next.push(child);
870
+ }
871
+ }
872
+ frontier = next;
873
+ }
874
+ return found.reverse();
875
+ }
876
+ var listChildrenViaPgrep = async (parentPid) => {
877
+ const stdout = await runProbe("pgrep", ["-P", String(parentPid)]);
878
+ return stdout.split("\n").map((line) => Number.parseInt(line.trim(), 10)).filter(isRealChildPid);
879
+ };
880
+ async function snapshotLister() {
881
+ const childrenOf = buildChildrenMap(await readWindowsSnapshot());
882
+ return (parentPid) => Promise.resolve(childrenOf.get(parentPid) ?? []);
883
+ }
884
+ async function readWindowsSnapshot() {
885
+ const fast = parseWmicCsv(await runProbe("wmic", WMIC_SNAPSHOT_ARGS, SNAPSHOT_MAX_BUFFER));
886
+ if (fast.length > 0) return fast;
887
+ const slow = await runProbe("powershell", POWERSHELL_SNAPSHOT_ARGS, SNAPSHOT_MAX_BUFFER);
888
+ return parseCimLines(slow);
889
+ }
890
+ function buildChildrenMap(records) {
891
+ const bornOf = /* @__PURE__ */ new Map();
892
+ for (const record of records) bornOf.set(record.pid, record.born);
893
+ const childrenOf = /* @__PURE__ */ new Map();
894
+ for (const { pid, ppid, born } of records) {
895
+ if (!isRealChildPid(pid) || !isRealChildPid(ppid)) continue;
896
+ const parentBorn = bornOf.get(ppid);
897
+ if (parentBorn === void 0 || parentBorn === "" || born === "" || born < parentBorn) continue;
898
+ const siblings = childrenOf.get(ppid);
899
+ if (siblings) siblings.push(pid);
900
+ else childrenOf.set(ppid, [pid]);
901
+ }
902
+ return childrenOf;
903
+ }
904
+ function parseWmicCsv(stdout) {
905
+ const lines = stdout.split("\n").map((line) => line.trim()).filter(Boolean);
906
+ const header = lines[lines.findIndex((line) => line.includes("ProcessId"))];
907
+ if (header === void 0) return [];
908
+ const columns = header.split(",").map((column) => column.trim());
909
+ const [atPid, atPpid, atBorn] = [
910
+ columns.indexOf("ProcessId"),
911
+ columns.indexOf("ParentProcessId"),
912
+ columns.indexOf("CreationDate")
913
+ ];
914
+ if (atPid < 0 || atPpid < 0 || atBorn < 0) return [];
915
+ return lines.slice(lines.indexOf(header) + 1).map((line) => {
916
+ const cells = line.split(",");
917
+ return {
918
+ pid: Number.parseInt(cells[atPid] ?? "", 10),
919
+ ppid: Number.parseInt(cells[atPpid] ?? "", 10),
920
+ // `20260807071417.130820+480` → 留到秒的那 14 位就够比大小了
921
+ born: normalizeBorn((cells[atBorn] ?? "").slice(0, 14))
922
+ };
923
+ });
924
+ }
925
+ function parseCimLines(stdout) {
926
+ const records = [];
927
+ for (const line of stdout.split("\n")) {
928
+ const [pid, ppid, born, ...rest] = line.trim().split(/\s+/);
929
+ if (born === void 0 || rest.length > 0) continue;
930
+ records.push({
931
+ pid: Number.parseInt(pid ?? "", 10),
932
+ ppid: Number.parseInt(ppid ?? "", 10),
933
+ born: normalizeBorn(born)
934
+ });
935
+ }
936
+ return records;
937
+ }
938
+ function normalizeBorn(raw) {
939
+ const trimmed = raw.trim();
940
+ return /^\d+$/.test(trimmed) ? trimmed.padStart(20, "0") : "";
941
+ }
942
+ function runProbe(file, args2, maxBuffer) {
943
+ return new Promise((resolve5) => {
944
+ try {
945
+ const options = {
946
+ timeout: PROBE_TIMEOUT_MS,
947
+ windowsHide: true,
948
+ ...maxBuffer === void 0 ? {} : { maxBuffer }
949
+ };
950
+ execFile(file, args2, options, (err, stdout) => {
951
+ resolve5(err ? "" : String(stdout));
952
+ });
953
+ } catch {
954
+ resolve5("");
955
+ }
956
+ });
957
+ }
958
+ function delay(ms) {
959
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
960
+ }
961
+
962
+ // src/shell-parse.ts
963
+ function defaultFlavor() {
964
+ return IS_WINDOWS ? "windows" : "posix";
965
+ }
966
+ var WRAPPER_RE = /^\s*(?:(?:(?:\S+[/\\])?(?:sh|bash|zsh|dash|ksh))\s+-c|cmd(?:\.exe)?\s+\/[cCkK]|(?:powershell|pwsh)(?:\.exe)?\s+(?:-NoProfile\s+)?-Command)\s+/i;
967
+ function stripShellWrapper(command) {
968
+ const match = command.match(WRAPPER_RE);
969
+ if (!match) return command.trim();
970
+ let rest = command.slice(match[0].length).trim();
971
+ if (rest.length >= 2) {
972
+ const first = rest[0];
973
+ const last = rest[rest.length - 1];
974
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
975
+ rest = rest.slice(1, -1);
976
+ }
977
+ }
978
+ return rest;
979
+ }
980
+ function detectPosixSubstitution(command) {
981
+ let inSingle = false;
982
+ let inDouble = false;
983
+ for (let i = 0; i < command.length; i++) {
984
+ const char = command[i];
985
+ if (char === "'" && !inDouble) {
986
+ inSingle = !inSingle;
987
+ continue;
988
+ }
989
+ if (char === '"' && !inSingle) {
990
+ inDouble = !inDouble;
991
+ continue;
992
+ }
993
+ if (inSingle) continue;
994
+ if (char === "\\" && i + 1 < command.length) {
995
+ if (!inDouble || ["$", "`", '"', "\\", "\n"].includes(command[i + 1] ?? "")) {
996
+ i++;
997
+ continue;
998
+ }
999
+ }
1000
+ if (char === "$" && command[i + 1] === "(") return true;
1001
+ if (!inDouble && (char === "<" || char === ">") && command[i + 1] === "(") return true;
1002
+ if (char === "`") return true;
1003
+ }
1004
+ return false;
1005
+ }
1006
+ var PS_KEYWORD_RE = /\b(if|elseif|else|foreach|for|while|do|switch|try|catch|finally|until|trap|function|filter)(\s+[-\w]+)*\s*$/i;
1007
+ function detectWindowsSubstitution(command) {
1008
+ let inSingle = false;
1009
+ let inDouble = false;
1010
+ for (let i = 0; i < command.length; i++) {
1011
+ const char = command[i];
1012
+ if (char === "'" && !inDouble) {
1013
+ inSingle = !inSingle;
1014
+ continue;
1015
+ }
1016
+ if (char === '"' && !inSingle) {
1017
+ inDouble = !inDouble;
1018
+ continue;
1019
+ }
1020
+ if (inSingle) continue;
1021
+ if (char === "`" && i + 1 < command.length) {
1022
+ i++;
1023
+ continue;
1024
+ }
1025
+ if (char === "$" && command[i + 1] === "(") return true;
1026
+ if (!inDouble && char === "@" && command[i + 1] === "(") return true;
1027
+ if (!inDouble && char === "(") {
1028
+ const before = command.slice(0, i).trimEnd();
1029
+ if (before.endsWith("(")) continue;
1030
+ if (PS_KEYWORD_RE.test(before)) continue;
1031
+ return true;
1032
+ }
1033
+ }
1034
+ return false;
1035
+ }
1036
+ var SEPARATORS = ["&&", "||", ";", "|", "&"];
1037
+ var REDIRECTS = ["2>&1", ">>", "2>", ">", "<"];
1038
+ function tokenize(input, flavor) {
1039
+ const tokens = [];
1040
+ let cur = "";
1041
+ let curHasContent = false;
1042
+ let inSingle = false;
1043
+ let inDouble = false;
1044
+ const flush = () => {
1045
+ if (curHasContent) tokens.push({ text: cur });
1046
+ cur = "";
1047
+ curHasContent = false;
1048
+ };
1049
+ for (let i = 0; i < input.length; i++) {
1050
+ const char = input[i];
1051
+ if (char === "'" && !inDouble) {
1052
+ inSingle = !inSingle;
1053
+ curHasContent = true;
1054
+ continue;
1055
+ }
1056
+ if (char === '"' && !inSingle) {
1057
+ inDouble = !inDouble;
1058
+ curHasContent = true;
1059
+ continue;
1060
+ }
1061
+ if (char === "\\" && flavor === "posix" && !inSingle && i + 1 < input.length) {
1062
+ cur += input[i + 1];
1063
+ curHasContent = true;
1064
+ i++;
1065
+ continue;
1066
+ }
1067
+ if (inSingle || inDouble) {
1068
+ cur += char;
1069
+ curHasContent = true;
1070
+ continue;
1071
+ }
1072
+ if (/\s/.test(char)) {
1073
+ flush();
1074
+ continue;
1075
+ }
1076
+ const rest = input.slice(i);
1077
+ const redirect = REDIRECTS.find((r) => rest.startsWith(r));
1078
+ if (redirect) {
1079
+ flush();
1080
+ tokens.push({ text: redirect, op: redirect });
1081
+ i += redirect.length - 1;
1082
+ continue;
1083
+ }
1084
+ const sep = SEPARATORS.find((s) => rest.startsWith(s));
1085
+ if (sep) {
1086
+ flush();
1087
+ tokens.push({ text: sep, op: sep });
1088
+ i += sep.length - 1;
1089
+ continue;
1090
+ }
1091
+ cur += char;
1092
+ curHasContent = true;
1093
+ }
1094
+ if (inSingle || inDouble) return null;
1095
+ flush();
1096
+ return tokens;
1097
+ }
1098
+ var ENV_ASSIGN_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
1099
+ function deriveRoot(words) {
1100
+ let i = 0;
1101
+ while (i < words.length && ENV_ASSIGN_RE.test(words[i])) i++;
1102
+ const assignments = words.slice(0, i);
1103
+ const name = words[i];
1104
+ if (name === void 0) return { root: "", argv: [], assignments };
1105
+ const base = name.split(/[/\\]/).pop() ?? name;
1106
+ return { root: base.toLowerCase(), argv: words.slice(i), assignments };
1107
+ }
1108
+ function parseShellCommand(command, flavor) {
1109
+ const kind = flavor ?? defaultFlavor();
1110
+ const stripped = stripShellWrapper(command);
1111
+ const hasSubstitution = kind === "windows" ? detectWindowsSubstitution(stripped) : detectPosixSubstitution(stripped);
1112
+ const tokens = tokenize(stripped, kind);
1113
+ if (tokens === null) {
1114
+ return { segments: [], hasSubstitution, parseError: "\u5F15\u53F7\u4E0D\u95ED\u5408", stripped };
1115
+ }
1116
+ const segments = [];
1117
+ let words = [];
1118
+ let redirects = [];
1119
+ let pipedInto = false;
1120
+ let nextPiped = false;
1121
+ const closeSegment = () => {
1122
+ if (words.length > 0 || redirects.length > 0) {
1123
+ const { root, argv, assignments } = deriveRoot(words);
1124
+ segments.push({ root, argv, assignments, raw: words.join(" "), pipedInto, redirects });
1125
+ }
1126
+ words = [];
1127
+ redirects = [];
1128
+ pipedInto = nextPiped;
1129
+ nextPiped = false;
1130
+ };
1131
+ for (let i = 0; i < tokens.length; i++) {
1132
+ const tok = tokens[i];
1133
+ if (!tok.op) {
1134
+ words.push(tok.text);
1135
+ continue;
1136
+ }
1137
+ if (REDIRECTS.includes(tok.op)) {
1138
+ const target = tokens[i + 1];
1139
+ if (target && !target.op) {
1140
+ redirects.push(target.text);
1141
+ i++;
1142
+ }
1143
+ continue;
1144
+ }
1145
+ nextPiped = tok.op === "|";
1146
+ closeSegment();
1147
+ }
1148
+ closeSegment();
1149
+ return { segments, hasSubstitution, stripped };
1150
+ }
1151
+ function splitWords(input, flavor) {
1152
+ const tokens = tokenize(input, flavor ?? defaultFlavor());
1153
+ return tokens === null ? null : tokens.map((t2) => t2.text);
1154
+ }
1155
+ function hasSideEffectChannel(parsed) {
1156
+ if (parsed.segments.length > 1) return true;
1157
+ return parsed.segments.some((s) => s.redirects.length > 0);
1158
+ }
1159
+
1160
+ // src/command-safety.ts
1161
+ function rootMatches(rule, root) {
1162
+ if (typeof rule === "string") return root === rule;
1163
+ if (rule instanceof RegExp) return rule.test(root);
1164
+ return rule.includes(root);
1165
+ }
1166
+ var args = (seg) => seg.argv.slice(1);
1167
+ var isShortCluster = (a) => /^-[a-zA-Z]+$/.test(a);
1168
+ function hasRecursiveForce(seg) {
1169
+ let recursive = false;
1170
+ let force = false;
1171
+ for (const a of args(seg)) {
1172
+ if (a === "--recursive") recursive = true;
1173
+ else if (a === "--force") force = true;
1174
+ else if (isShortCluster(a)) {
1175
+ if (/[rR]/.test(a)) recursive = true;
1176
+ if (/f/.test(a)) force = true;
1177
+ }
1178
+ }
1179
+ return recursive && force;
1180
+ }
1181
+ function isDangerousRmTarget(t2) {
1182
+ if (t2 === "/" || t2 === "/*") return true;
1183
+ if (t2 === "~" || t2 === "~/" || t2 === "~/*") return true;
1184
+ if (/^\$\{?HOME\}?$/i.test(t2)) return true;
1185
+ return /^\/(bin|boot|dev|etc|lib|proc|root|sbin|sys|usr|var)(\/\*)?\/?$/.test(t2);
1186
+ }
1187
+ var isEmptyVarSlash = (t2) => /^\$\{?\w+\}?\/$/.test(t2);
1188
+ var isRawDevice = (t2) => /^\/dev\/(sd[a-z]|nvme\d|disk\d|hd[a-z])/.test(t2);
1189
+ var SHELLS = ["sh", "bash", "zsh", "ksh", "dash", "ash", "fish"];
1190
+ var INTERPRETERS = ["python", "python2", "python3", "perl", "ruby", "node"];
1191
+ var FETCHERS = ["curl", "wget", "fetch"];
1192
+ var INLINE_CODE_EXEC = [
1193
+ { root: "python", flags: ["-c"] },
1194
+ { root: "python2", flags: ["-c"] },
1195
+ { root: "python3", flags: ["-c"] },
1196
+ { root: "node", flags: ["-e", "--eval", "-p", "--print"] },
1197
+ { root: "deno", flags: ["eval"] },
1198
+ { root: "bun", flags: ["-e", "--eval"] },
1199
+ { root: "perl", flags: ["-e", "-E"] },
1200
+ { root: "ruby", flags: ["-e"] },
1201
+ { root: "php", flags: ["-r"] }
1202
+ ];
1203
+ function inlineCodeExecOf(seg) {
1204
+ const entry = INLINE_CODE_EXEC.find((e) => e.root === seg.root);
1205
+ if (!entry) return null;
1206
+ return args(seg).some((a) => entry.flags.includes(a)) ? seg.root : null;
1207
+ }
1208
+ var CODE_EXEC_ROOTS = [
1209
+ // 解释器
1210
+ "python",
1211
+ "python2",
1212
+ "python3",
1213
+ "node",
1214
+ "deno",
1215
+ "bun",
1216
+ "tsx",
1217
+ "ruby",
1218
+ "perl",
1219
+ "php",
1220
+ "lua",
1221
+ // 包运行器 —— 后面跟什么都能跑
1222
+ "npx",
1223
+ "bunx",
1224
+ "npm run",
1225
+ "yarn run",
1226
+ "pnpm run",
1227
+ "bun run",
1228
+ // shell 与远程执行
1229
+ ...SHELLS,
1230
+ "ssh",
1231
+ "eval",
1232
+ "exec",
1233
+ "env",
1234
+ "xargs",
1235
+ // 提权
1236
+ "sudo",
1237
+ "doas",
1238
+ "su",
1239
+ // PowerShell
1240
+ "powershell",
1241
+ "pwsh"
1242
+ ];
1243
+ function hasWinSwitch(seg, name, exact = false) {
1244
+ const want = name.toLowerCase();
1245
+ return args(seg).some((a) => {
1246
+ const m = /^[/-](.+)$/.exec(a);
1247
+ if (!m) return false;
1248
+ const got = m[1].toLowerCase().split(":")[0] ?? "";
1249
+ if (exact) return got === want;
1250
+ return got.length > 0 && want.startsWith(got);
1251
+ });
1252
+ }
1253
+ function winSwitchValue(seg, name) {
1254
+ const want = name.toLowerCase();
1255
+ const list = args(seg);
1256
+ for (let i = 0; i < list.length; i++) {
1257
+ const a = list[i];
1258
+ const m = /^[/-](.+)$/.exec(a);
1259
+ if (!m) continue;
1260
+ const [head, ...tail] = m[1].split(":");
1261
+ if (head.toLowerCase() !== want) continue;
1262
+ return tail.length > 0 ? tail.join(":") : list[i + 1];
1263
+ }
1264
+ return void 0;
1265
+ }
1266
+ function isDangerousWinTarget(t2) {
1267
+ const s = t2.toLowerCase();
1268
+ if (/^[a-z]:\\?\*?$/.test(s)) return true;
1269
+ if (/^%(userprofile|systemdrive|systemroot|windir|appdata|programfiles)%/.test(s)) return true;
1270
+ if (/^\$env:(userprofile|systemroot|appdata)/.test(s)) return true;
1271
+ return /^[a-z]:\\(windows|users|program files)(\\|$)/.test(s);
1272
+ }
1273
+ var hasDangerousWinTarget = (seg) => args(seg).some(isDangerousWinTarget);
1274
+ var ENCODED_COMMAND_RULE = {
1275
+ root: ["powershell", "pwsh"],
1276
+ when: (s) => args(s).some((a) => {
1277
+ const m = /^[/-](.+)$/.exec(a);
1278
+ if (!m) return false;
1279
+ const got = m[1].toLowerCase();
1280
+ return got.length > 0 && got.startsWith("e") && "encodedcommand".startsWith(got);
1281
+ }),
1282
+ desc: "PowerShell \u7F16\u7801\u547D\u4EE4\uFF08-EncodedCommand\uFF09\uFF0C\u5185\u5BB9\u4E0D\u53EF\u8BFB"
1283
+ };
1284
+ var POSIX_RULES = [
1285
+ {
1286
+ root: "rm",
1287
+ when: (s) => hasRecursiveForce(s) && args(s).some(isDangerousRmTarget),
1288
+ desc: "\u9012\u5F52\u5220\u9664\u6839\u76EE\u5F55\u6216\u7CFB\u7EDF\u76EE\u5F55"
1289
+ },
1290
+ {
1291
+ root: "rm",
1292
+ when: (s) => hasRecursiveForce(s) && args(s).some(isEmptyVarSlash),
1293
+ desc: "rm -rf $VAR/ \u2014\u2014 \u53D8\u91CF\u4E3A\u7A7A\u65F6\u7B49\u4E8E\u5220\u6839"
1294
+ },
1295
+ {
1296
+ // `find / -delete` / `find / -exec rm` —— 老实现整条漏了
1297
+ root: "find",
1298
+ when: (s) => args(s).some(isDangerousRmTarget) && args(s).some((a) => a === "-delete" || a === "-exec" || a === "-execdir"),
1299
+ desc: "\u5728\u7CFB\u7EDF\u76EE\u5F55\u4E0A\u6279\u91CF\u5220\u9664\uFF08find -delete / -exec\uFF09"
1300
+ },
1301
+ { root: /^mkfs(\.\w+)?$/, desc: "\u683C\u5F0F\u5316\u6587\u4EF6\u7CFB\u7EDF" },
1302
+ {
1303
+ root: "dd",
1304
+ when: (s) => args(s).some((a) => a.startsWith("of=/dev/")),
1305
+ desc: "\u76F4\u63A5\u5199\u88F8\u8BBE\u5907"
1306
+ },
1307
+ { root: "dd", when: (s) => args(s).some((a) => a.startsWith("if=")), desc: "\u78C1\u76D8\u7EA7\u8BFB\u5199" },
1308
+ { root: "shred", desc: "\u4E0D\u53EF\u6062\u590D\u5730\u64E6\u9664\u6587\u4EF6" },
1309
+ { root: ["fdisk", "parted"], desc: "\u78C1\u76D8\u5206\u533A\u64CD\u4F5C" },
1310
+ {
1311
+ root: "diskutil",
1312
+ when: (s) => (args(s)[0] ?? "").toLowerCase().startsWith("erase"),
1313
+ desc: "\u78C1\u76D8\u5206\u533A\u64CD\u4F5C"
1314
+ },
1315
+ { root: "chmod", when: (s) => args(s).includes("777"), desc: "\u5168\u6743\u9650\u5F00\u653E" },
1316
+ {
1317
+ root: "chown",
1318
+ when: (s) => args(s).some((a) => a === "root" || a.startsWith("root:")),
1319
+ desc: "\u6539\u5F52\u5C5E\u4E3A root"
1320
+ },
1321
+ ENCODED_COMMAND_RULE,
1322
+ { root: ["sudo", "doas"], desc: "\u63D0\u6743\u6267\u884C\uFF08sudo / doas / su -\uFF09" },
1323
+ { root: "su", when: (s) => args(s).includes("-"), desc: "\u63D0\u6743\u6267\u884C\uFF08sudo / doas / su -\uFF09" },
1324
+ {
1325
+ // 下载后直接执行:上一段是下载工具,这一段是被管道喂进来的 shell
1326
+ root: SHELLS,
1327
+ when: (s, prev) => s.pipedInto && prev !== void 0 && FETCHERS.includes(prev.root),
1328
+ desc: "\u4E0B\u8F7D\u540E\u76F4\u63A5\u6267\u884C\uFF08curl | sh\uFF09"
1329
+ },
1330
+ {
1331
+ root: INTERPRETERS,
1332
+ when: (s, prev) => s.pipedInto && prev !== void 0 && FETCHERS.includes(prev.root),
1333
+ desc: "\u4E0B\u8F7D\u540E\u76F4\u63A5\u4EA4\u7ED9\u89E3\u91CA\u5668\u6267\u884C"
1334
+ },
1335
+ {
1336
+ // 通用「管道进 shell」。放在上面两条之后,让来源明确的先给出更准确的描述
1337
+ root: SHELLS,
1338
+ when: (s) => s.pipedInto,
1339
+ desc: "\u628A\u4EFB\u610F\u8F93\u51FA\u7BA1\u9053\u8FDB shell \u6267\u884C"
1340
+ },
1341
+ {
1342
+ root: "git",
1343
+ when: (s) => args(s)[0] === "push" && args(s).some((a) => a === "--force" || a === "-f" || a === "+"),
1344
+ desc: "\u5F3A\u5236\u63A8\u9001"
1345
+ },
1346
+ {
1347
+ root: "git",
1348
+ when: (s) => {
1349
+ const [sub, ...rest] = args(s);
1350
+ if (sub === "reset") return rest.includes("--hard");
1351
+ if (sub === "clean") return rest.some((a) => isShortCluster(a) && a.includes("f"));
1352
+ return false;
1353
+ },
1354
+ desc: "\u4E22\u5F03\u672A\u63D0\u4EA4\u6539\u52A8"
1355
+ },
1356
+ { root: ["shutdown", "reboot", "halt", "poweroff"], desc: "\u5173\u673A / \u91CD\u542F" },
1357
+ {
1358
+ root: ["kill", "killall"],
1359
+ when: (s) => args(s).includes("-9") && args(s).some((a) => a === "-1" || a === "1"),
1360
+ desc: "\u6740\u6389\u5168\u90E8\u8FDB\u7A0B / init"
1361
+ },
1362
+ {
1363
+ root: ["iptables", "ip6tables", "nft", "pfctl"],
1364
+ when: (s) => args(s).some((a) => a === "-F" || a === "--flush"),
1365
+ desc: "\u6E05\u7A7A\u9632\u706B\u5899\u89C4\u5219"
1366
+ },
1367
+ {
1368
+ root: ["launchctl", "systemctl"],
1369
+ when: (s) => ["disable", "unload"].includes(args(s)[0] ?? ""),
1370
+ desc: "\u7981\u7528\u7CFB\u7EDF\u670D\u52A1"
1371
+ },
1372
+ { root: "history", when: (s) => args(s).includes("-c"), desc: "\u6E05\u9664\u547D\u4EE4\u5386\u53F2\uFF08\u63A9\u76D6\u75D5\u8FF9\uFF09" },
1373
+ {
1374
+ root: "tee",
1375
+ when: (s) => args(s).some((a) => a.startsWith("/etc/")),
1376
+ desc: "\u6539\u5199 /etc \u4E0B\u7684\u7CFB\u7EDF\u914D\u7F6E"
1377
+ },
1378
+ {
1379
+ // 重定向到裸设备 / 历史文件。这条不看 root,任何命令都可能这么写
1380
+ root: /.*/,
1381
+ when: (s) => s.redirects.some((r) => isRawDevice(r) || /(^|\/)\.?\w*history$/.test(r)),
1382
+ desc: "\u5199\u5165\u78C1\u76D8\u8BBE\u5907\u6216\u7BE1\u6539\u5386\u53F2\u6587\u4EF6"
1383
+ }
1384
+ ];
1385
+ var WINDOWS_RULES = [
1386
+ {
1387
+ root: ["del", "erase", "rd", "rmdir"],
1388
+ when: hasDangerousWinTarget,
1389
+ desc: "\u9012\u5F52\u5220\u9664\u76D8\u6839 / \u7CFB\u7EDF\u76EE\u5F55 / \u7528\u6237\u76EE\u5F55"
1390
+ },
1391
+ {
1392
+ root: "remove-item",
1393
+ when: (s) => hasWinSwitch(s, "recurse") && hasDangerousWinTarget(s),
1394
+ desc: "PowerShell \u9012\u5F52\u5220\u9664\u76D8\u6839 / \u7CFB\u7EDF\u76EE\u5F55"
1395
+ },
1396
+ {
1397
+ root: "format",
1398
+ when: (s) => args(s).some((a) => /^[a-z]:$/i.test(a)),
1399
+ desc: "\u683C\u5F0F\u5316\u78C1\u76D8"
1400
+ },
1401
+ { root: "diskpart", desc: "\u78C1\u76D8\u5206\u533A\u64CD\u4F5C" },
1402
+ { root: "cipher", when: (s) => hasWinSwitch(s, "w", true), desc: "\u64E6\u9664\u78C1\u76D8\u7A7A\u95F2\u7A7A\u95F4\uFF08\u4E0D\u53EF\u6062\u590D\uFF09" },
1403
+ {
1404
+ root: "vssadmin",
1405
+ when: (s) => (args(s)[0] ?? "").toLowerCase() === "delete",
1406
+ desc: "\u5220\u9664\u5377\u5F71\u526F\u672C\uFF08\u52D2\u7D22\u8F6F\u4EF6\u6807\u51C6\u52A8\u4F5C\uFF0C\u5220\u5B8C\u65E0\u6CD5\u56DE\u6EDA\uFF09"
1407
+ },
1408
+ {
1409
+ root: "wmic",
1410
+ when: (s) => args(s).map((a) => a.toLowerCase()).join(" ").includes("shadowcopy delete"),
1411
+ desc: "\u5220\u9664\u5377\u5F71\u526F\u672C\uFF08\u52D2\u7D22\u8F6F\u4EF6\u6807\u51C6\u52A8\u4F5C\uFF0C\u5220\u5B8C\u65E0\u6CD5\u56DE\u6EDA\uFF09"
1412
+ },
1413
+ {
1414
+ root: "reg",
1415
+ when: (s) => (args(s)[0] ?? "").toLowerCase() === "delete" && hasWinSwitch(s, "f", true),
1416
+ desc: "\u5F3A\u5236\u5220\u9664\u6CE8\u518C\u8868\u9879"
1417
+ },
1418
+ { root: "bcdedit", desc: "\u6539\u542F\u52A8\u914D\u7F6E\uFF08\u53EF\u80FD\u5BFC\u81F4\u7CFB\u7EDF\u8D77\u4E0D\u6765\uFF09" },
1419
+ { root: "sc", when: (s) => (args(s)[0] ?? "").toLowerCase() === "delete", desc: "\u5220\u9664\u7CFB\u7EDF\u670D\u52A1" },
1420
+ { root: "takeown", when: (s) => hasWinSwitch(s, "r", true), desc: "\u9012\u5F52\u593A\u53D6\u6587\u4EF6\u6240\u6709\u6743" },
1421
+ {
1422
+ root: "icacls",
1423
+ when: (s) => hasWinSwitch(s, "grant") && args(s).some((a) => /^(everyone|users)(:|$)/i.test(a)),
1424
+ desc: "\u7ED9 everyone / users \u6388\u6743"
1425
+ },
1426
+ {
1427
+ root: "netsh",
1428
+ when: (s) => {
1429
+ const lower = args(s).map((a) => a.toLowerCase());
1430
+ return (lower[0] === "advfirewall" || lower[0] === "firewall") && lower.some((a) => a === "off" || a === "disable");
1431
+ },
1432
+ desc: "\u5173\u95ED\u9632\u706B\u5899"
1433
+ },
1434
+ {
1435
+ root: "set-mppreference",
1436
+ when: (s) => hasWinSwitch(s, "disablerealtimemonitoring"),
1437
+ desc: "\u5173\u95ED Defender \u5B9E\u65F6\u9632\u62A4"
1438
+ },
1439
+ {
1440
+ root: "net",
1441
+ when: (s) => ["user", "localgroup"].includes((args(s)[0] ?? "").toLowerCase()) && hasWinSwitch(s, "add", true),
1442
+ desc: "\u65B0\u589E\u8D26\u6237 / \u52A0\u5165\u7BA1\u7406\u5458\u7EC4"
1443
+ },
1444
+ { root: "runas", desc: "\u63D0\u6743\u6267\u884C\uFF08runas\uFF09" },
1445
+ {
1446
+ root: "start-process",
1447
+ when: (s) => (winSwitchValue(s, "verb") ?? "").toLowerCase() === "runas",
1448
+ desc: "\u63D0\u6743\u6267\u884C\uFF08Start-Process -Verb RunAs\uFF09"
1449
+ },
1450
+ {
1451
+ root: ["iex", "invoke-expression"],
1452
+ when: (s) => s.pipedInto,
1453
+ desc: "\u4E0B\u8F7D\u540E\u76F4\u63A5\u6267\u884C\uFF08iwr | iex\uFF09"
1454
+ },
1455
+ {
1456
+ root: ["powershell", "pwsh", "cmd"],
1457
+ when: (s, prev) => s.pipedInto && prev !== void 0 && ["curl", "wget", "iwr", "irm", "invoke-webrequest", "invoke-restmethod"].includes(prev.root),
1458
+ desc: "\u4E0B\u8F7D\u540E\u76F4\u63A5\u4EA4\u7ED9 shell \u6267\u884C"
1459
+ },
1460
+ {
1461
+ root: "certutil",
1462
+ when: (s) => hasWinSwitch(s, "urlcache"),
1463
+ desc: "certutil \u4E0B\u8F7D\uFF08\u5E38\u89C1\u514D\u6740\u4E0B\u8F7D\u5668\uFF09"
1464
+ },
1465
+ { root: "bitsadmin", when: (s) => hasWinSwitch(s, "transfer"), desc: "bitsadmin \u540E\u53F0\u4E0B\u8F7D" },
1466
+ ENCODED_COMMAND_RULE,
1467
+ {
1468
+ root: ["powershell", "pwsh"],
1469
+ when: (s) => hasWinSwitch(s, "noprofile") || (winSwitchValue(s, "windowstyle") ?? "").toLowerCase() === "hidden" || (winSwitchValue(s, "w") ?? "").toLowerCase() === "hidden",
1470
+ desc: "PowerShell \u9690\u85CF\u7A97\u53E3 / \u8DF3\u8FC7\u914D\u7F6E\u6267\u884C"
1471
+ },
1472
+ {
1473
+ root: ["powershell", "pwsh"],
1474
+ when: (s) => ["bypass", "unrestricted"].includes(
1475
+ (winSwitchValue(s, "executionpolicy") ?? "").toLowerCase()
1476
+ ),
1477
+ desc: "\u7ED5\u8FC7 PowerShell \u6267\u884C\u7B56\u7565"
1478
+ },
1479
+ { root: "mshta", desc: "mshta \u6267\u884C\u8FDC\u7A0B\u811A\u672C\uFF08LOLBin\uFF09" },
1480
+ { root: "rundll32", desc: "rundll32 \u6267\u884C\u4EFB\u610F DLL \u5BFC\u51FA\uFF08LOLBin\uFF09" },
1481
+ {
1482
+ root: "regsvr32",
1483
+ when: (s) => args(s).some((a) => /^[/-]i:/i.test(a)),
1484
+ desc: "regsvr32 \u8FDC\u7A0B scriptlet\uFF08LOLBin\uFF09"
1485
+ },
1486
+ {
1487
+ root: "shutdown",
1488
+ when: (s) => hasWinSwitch(s, "s", true) || hasWinSwitch(s, "r", true),
1489
+ desc: "\u5173\u673A / \u91CD\u542F"
1490
+ },
1491
+ { root: ["restart-computer", "stop-computer"], desc: "\u5173\u673A / \u91CD\u542F" },
1492
+ {
1493
+ root: "schtasks",
1494
+ when: (s) => hasWinSwitch(s, "create", true),
1495
+ desc: "\u521B\u5EFA\u8BA1\u5212\u4EFB\u52A1\uFF08\u6301\u4E45\u5316\uFF09"
1496
+ },
1497
+ { root: ["new-scheduledtask", "register-scheduledtask"], desc: "\u6CE8\u518C\u8BA1\u5212\u4EFB\u52A1\uFF08\u6301\u4E45\u5316\uFF09" }
1498
+ ];
1499
+ var WHOLE_STRING_RULES = [
1500
+ { pattern: /:\s*\(\s*\)\s*\{.*\}\s*;?\s*:/, desc: "fork bomb" }
1501
+ ];
1502
+ var WINDOWS_TABLE = [...WINDOWS_RULES, ...POSIX_RULES];
1503
+ function parseForSafety(command, platform10) {
1504
+ const parsed = parseShellCommand(command, platform10 === "win32" ? "windows" : "posix");
1505
+ if (platform10 !== "win32" || parsed.hasSubstitution) return parsed;
1506
+ const asPosix = parseShellCommand(command, "posix");
1507
+ return asPosix.hasSubstitution ? { ...parsed, hasSubstitution: true } : parsed;
1508
+ }
1509
+ var currentPlatform = () => IS_WINDOWS ? "win32" : "posix";
1510
+ function checkDangerousCommand(command, opts) {
1511
+ const platform10 = opts?.platform ?? currentPlatform();
1512
+ const parsed = parseForSafety(command, platform10);
1513
+ for (const { pattern, desc } of WHOLE_STRING_RULES) {
1514
+ if (pattern.test(parsed.stripped)) return { dangerous: true, desc };
1515
+ }
1516
+ const table = platform10 === "win32" ? WINDOWS_TABLE : POSIX_RULES;
1517
+ for (let i = 0; i < parsed.segments.length; i++) {
1518
+ const seg = parsed.segments[i];
1519
+ const prev = i > 0 ? parsed.segments[i - 1] : void 0;
1520
+ for (const rule of table) {
1521
+ if (!rootMatches(rule.root, seg.root)) continue;
1522
+ if (rule.when && !rule.when(seg, prev)) continue;
1523
+ return { dangerous: true, desc: rule.desc };
1524
+ }
1525
+ }
1526
+ return { dangerous: false };
1527
+ }
1528
+ var READONLY_ROOTS = /* @__PURE__ */ new Set([
1529
+ "ls",
1530
+ "ll",
1531
+ "cat",
1532
+ "bat",
1533
+ "head",
1534
+ "tail",
1535
+ "wc",
1536
+ "grep",
1537
+ "rg",
1538
+ "ag",
1539
+ "find",
1540
+ "fd",
1541
+ "tree",
1542
+ "file",
1543
+ "stat",
1544
+ "type",
1545
+ "which",
1546
+ "whereis",
1547
+ "pwd",
1548
+ "echo",
1549
+ "date",
1550
+ "whoami",
1551
+ "id",
1552
+ "uname",
1553
+ "hostname",
1554
+ "df",
1555
+ "du",
1556
+ "free",
1557
+ "uptime",
1558
+ "ps",
1559
+ "top",
1560
+ "htop",
1561
+ "env",
1562
+ "printenv"
1563
+ ]);
1564
+ var READONLY_SUBCOMMANDS = {
1565
+ node: ["-v", "--version"],
1566
+ npm: ["ls", "list", "view", "info", "outdated"],
1567
+ pnpm: ["ls", "list", "why", "outdated"],
1568
+ yarn: ["list", "info"],
1569
+ git: [
1570
+ "status",
1571
+ "log",
1572
+ "diff",
1573
+ "show",
1574
+ "branch",
1575
+ "remote",
1576
+ "blame",
1577
+ "describe",
1578
+ "rev-parse",
1579
+ "ls-files",
1580
+ "shortlog",
1581
+ "tag"
1582
+ ]
1583
+ };
1584
+ function isReadOnlyCommand(command) {
1585
+ const parsed = parseForSafety(command, currentPlatform());
1586
+ if (parsed.parseError !== void 0 || parsed.hasSubstitution) return false;
1587
+ if (hasSideEffectChannel(parsed)) return false;
1588
+ if (parsed.segments.length === 0) return false;
1589
+ return parsed.segments.every((seg) => {
1590
+ if (READONLY_ROOTS.has(seg.root)) return true;
1591
+ const subs = READONLY_SUBCOMMANDS[seg.root];
1592
+ if (!subs) return false;
1593
+ const first = seg.argv[1];
1594
+ if (first === void 0) return false;
1595
+ if (seg.root === "git" && first === "stash") return seg.argv[2] === "list";
1596
+ if (seg.root === "git" && first === "config") return seg.argv.includes("--get");
1597
+ return subs.includes(first);
1598
+ });
1599
+ }
1600
+ var OBFUSCATION_PATTERNS = [
1601
+ { pattern: /\beval\b/, desc: "eval \u52A8\u6001\u6267\u884C" },
1602
+ { pattern: /\bexec\s+\$/, desc: "exec \u6267\u884C\u53D8\u91CF" },
1603
+ { pattern: /\bbase64\s+(-d|--decode)/, desc: "base64 \u89E3\u7801" },
1604
+ { pattern: /\b(xxd|od)\s+-r/, desc: "\u5341\u516D\u8FDB\u5236\u8FD8\u539F" },
1605
+ { pattern: /\|\s*(ba|z|k|da)?sh\b/, desc: "\u7BA1\u9053\u8FDB shell" },
1606
+ { pattern: /\\x[0-9a-f]{2}/i, desc: "\u5341\u516D\u8FDB\u5236\u8F6C\u4E49\u5B57\u7B26" },
1607
+ { pattern: /\$\{[^}]*[:#%/][^}]*\}/, desc: "\u53D8\u91CF\u5C55\u5F00\u505A\u5B57\u7B26\u4E32\u62FC\u88C5" },
1608
+ { pattern: /\bprintf\b[^|;&]*\|/, desc: "printf \u6784\u9020\u540E\u7BA1\u9053" },
1609
+ { pattern: /\bcurl\b[^|;&]*\|/, desc: "\u4E0B\u8F7D\u540E\u7BA1\u9053" }
1610
+ ];
1611
+ function checkObfuscation(command) {
1612
+ const parsed = parseForSafety(command, currentPlatform());
1613
+ if (parsed.parseError !== void 0) return `\u65E0\u6CD5\u89E3\u6790\uFF08${parsed.parseError}\uFF09`;
1614
+ if (parsed.hasSubstitution) {
1615
+ return /`/.test(parsed.stripped) && !/\$\(/.test(parsed.stripped) ? "\u53CD\u5F15\u53F7\u547D\u4EE4\u66FF\u6362" : "\u547D\u4EE4\u66FF\u6362 $(...)";
1616
+ }
1617
+ for (const seg of parsed.segments) {
1618
+ const interpreter = inlineCodeExecOf(seg);
1619
+ if (interpreter !== null) return `${interpreter} \u6267\u884C\u5185\u8054\u4EE3\u7801\uFF08\u5185\u5BB9\u65E0\u6CD5\u9759\u6001\u5206\u6790\uFF09`;
1620
+ }
1621
+ const norm = command.replace(/\s+/g, " ").trim();
1622
+ for (const { pattern, desc } of OBFUSCATION_PATTERNS) {
1623
+ if (pattern.test(norm)) return desc;
1624
+ }
1625
+ return null;
1626
+ }
1627
+ var BWRAP_CANDIDATES = ["/usr/bin/bwrap", "/bin/bwrap", "/usr/local/bin/bwrap"];
1628
+ var bwrapPath;
1629
+ function realpathOrSelf(p) {
1630
+ try {
1631
+ return realpathSync(p);
1632
+ } catch {
1633
+ return p;
1634
+ }
1635
+ }
1636
+ function isInside(target, dir) {
1637
+ const rel = relative(resolve$1(dir), resolve$1(target));
1638
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1639
+ }
1640
+ function probeBubblewrap() {
1641
+ if (bwrapPath !== void 0) return bwrapPath;
1642
+ if (platform() !== "linux") {
1643
+ bwrapPath = null;
1644
+ return bwrapPath;
1645
+ }
1646
+ for (const candidate of BWRAP_CANDIDATES) {
1647
+ if (!existsSync(candidate)) continue;
1648
+ try {
1649
+ execFileSync(candidate, ["--ro-bind", "/", "/", "--", "/bin/true"], {
1650
+ stdio: "ignore",
1651
+ timeout: 5e3
1652
+ });
1653
+ bwrapPath = candidate;
1654
+ return bwrapPath;
1655
+ } catch {
1656
+ }
1657
+ }
1658
+ bwrapPath = null;
1659
+ return bwrapPath;
1660
+ }
1661
+ function buildBwrapArgs(command, commandArgs, opts) {
1662
+ const home = resolve$1(homedir());
1663
+ const args2 = [];
1664
+ args2.push("--ro-bind", "/", "/");
1665
+ args2.push("--dev", "/dev");
1666
+ args2.push("--proc", "/proc");
1667
+ if (opts.reads !== "unrestricted" && home !== "/" && existsSync(home)) {
1668
+ args2.push("--tmpfs", home);
1669
+ }
1670
+ const real = realpathOrSelf(command);
1671
+ for (const p of [dirname(dirname(real)), dirname(real), real]) {
1672
+ if (isInside(p, home) && existsSync(p)) {
1673
+ args2.push("--ro-bind", p, p);
1674
+ }
1675
+ }
1676
+ for (const dir of opts.readableDirs ?? []) {
1677
+ const abs = resolve$1(dir);
1678
+ if (abs === home || abs === "/") continue;
1679
+ if (existsSync(abs)) args2.push("--ro-bind", abs, abs);
1680
+ }
1681
+ for (const abs of new Set(opts.writableDirs.map((d) => resolve$1(d)))) {
1682
+ if (existsSync(abs)) args2.push("--bind", abs, abs);
1683
+ }
1684
+ if (!opts.allowNetwork) args2.push("--unshare-net");
1685
+ args2.push("--unshare-pid", "--unshare-uts", "--unshare-ipc");
1686
+ args2.push("--die-with-parent");
1687
+ args2.push("--new-session");
1688
+ args2.push("--cap-drop", "ALL");
1689
+ if (opts.cwd) args2.push("--chdir", resolve$1(opts.cwd));
1690
+ args2.push("--", command, ...commandArgs);
1691
+ return args2;
1692
+ }
1693
+ function isolateWithBubblewrap(command, commandArgs, opts) {
1694
+ const bwrap = probeBubblewrap();
1695
+ if (!bwrap) return null;
1696
+ return { command: bwrap, args: buildBwrapArgs(command, commandArgs, opts) };
1697
+ }
1698
+ var SYSTEM_READ_PATHS = [
1699
+ "/usr",
1700
+ "/System",
1701
+ "/Library",
1702
+ "/bin",
1703
+ "/sbin",
1704
+ "/opt",
1705
+ "/private/var/db",
1706
+ "/private/var/folders",
1707
+ "/private/var/select",
1708
+ // xcode-select 要读它,否则 macOS 自带 python3 起不来
1709
+ "/private/etc",
1710
+ // macOS 自带的 /usr/bin/python3 是个 Xcode shim:它会去跑 xcodebuild,
1711
+ // 需要读整个 Xcode.app(Info.plist + SharedFrameworks)。
1712
+ // Xcode.app 是公开的应用包,放行读取不泄露任何用户数据。
1713
+ "/Applications/Xcode.app",
1714
+ "/Library/Developer"
1715
+ ];
1716
+ function ancestorsOf(paths) {
1717
+ const out = /* @__PURE__ */ new Set(["/"]);
1718
+ for (const p of paths) {
1719
+ let cur = p;
1720
+ for (; ; ) {
1721
+ const parent = dirname(cur);
1722
+ if (parent === cur) break;
1723
+ out.add(parent);
1724
+ cur = parent;
1725
+ }
1726
+ }
1727
+ return [...out];
1728
+ }
1729
+ var READ_DEVICES = ["/dev/null", "/dev/urandom", "/dev/random", "/dev/tty"];
1730
+ var SENSITIVE_HOME_PATHS = [
1731
+ ".ssh",
1732
+ ".gnupg",
1733
+ ".aws",
1734
+ ".azure",
1735
+ ".kube",
1736
+ ".docker",
1737
+ ".config/gcloud",
1738
+ ".npmrc",
1739
+ ".pypirc",
1740
+ ".netrc",
1741
+ ".git-credentials",
1742
+ ".gitconfig",
1743
+ "Library/Keychains",
1744
+ "Library/Application Support/Google/Chrome",
1745
+ "Library/Application Support/Firefox",
1746
+ "Library/Cookies",
1747
+ "Library/Messages",
1748
+ // agent 自己的数据:会话、记忆、审批、API key 都在这。
1749
+ // 注意沙箱目录本身就在 ~/.epoch/sandbox 下,所以 buildProfile 必须
1750
+ // 在这条 deny **之后**重新 allow writableDirs,否则连自己的 code.js 都读不到。
1751
+ ".epoch"
1752
+ ];
1753
+ var SENSITIVE_ABS_PATHS = ["/etc/shadow", "/etc/sudoers", "/private/etc/master.passwd"];
1754
+ function sb(value) {
1755
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1756
+ }
1757
+ function realpathOrSelf2(p) {
1758
+ try {
1759
+ return realpathSync(p);
1760
+ } catch {
1761
+ return p;
1762
+ }
1763
+ }
1764
+ function pathVariants(p) {
1765
+ const real = realpathOrSelf2(p);
1766
+ const set = /* @__PURE__ */ new Set([p, real]);
1767
+ if (real.startsWith("/private/")) set.add(real.slice("/private".length));
1768
+ else set.add(join("/private", real));
1769
+ return [...set];
1770
+ }
1771
+ function expandSubpaths(paths) {
1772
+ return paths.flatMap(pathVariants).map((d) => `(subpath ${sb(d)})`).join(" ");
1773
+ }
1774
+ function readSection(opts, readable) {
1775
+ if (opts.reads === "unrestricted") {
1776
+ return [
1777
+ ";; ---- \u8BFB\u53D6\uFF1A\u4E0D\u8BBE\u9650\uFF08\u6C99\u7BB1\u6A21\u5F0F\u53EA\u7BA1\u5199\u5165\uFF0C\u5224\u636E\u89C1 SeatbeltOptions.reads\uFF09----",
1778
+ "(allow file-read*)",
1779
+ ""
1780
+ ];
1781
+ }
1782
+ const readableLiterals = READ_DEVICES.map((d) => `(literal ${sb(d)})`).join(" ");
1783
+ const ancestorLiterals = ancestorsOf(readable.flatMap(pathVariants)).map((d) => `(literal ${sb(d)})`).join(" ");
1784
+ return [
1785
+ ";; ---- \u8BFB\u53D6\uFF1A\u9ED8\u8BA4\u5168\u7981\uFF0C\u53EA\u653E\u5F00\u7CFB\u7EDF\u5E93 / \u89E3\u91CA\u5668 / \u6C99\u7BB1 / \u5DE5\u4F5C\u533A ----",
1786
+ "(deny file-read*)",
1787
+ `(allow file-read* ${expandSubpaths(readable)} ${readableLiterals})`,
1788
+ ";; \u7956\u5148\u76EE\u5F55\u672C\u8EAB\u653E\u884C\uFF08\u8DEF\u5F84\u89E3\u6790\u9700\u8981\uFF09\u2014\u2014\u53EA\u662F\u76EE\u5F55\u8282\u70B9\uFF0C\u4E0D\u542B\u5176\u5B50\u6811",
1789
+ `(allow file-read* ${ancestorLiterals})`,
1790
+ ""
1791
+ ];
1792
+ }
1793
+ function sensitiveReadSection(opts) {
1794
+ if (opts.reads === "unrestricted") return [];
1795
+ const home = homedir();
1796
+ const sensitiveReads = [
1797
+ ...SENSITIVE_HOME_PATHS.flatMap((rel) => pathVariants(join(home, rel))),
1798
+ ...SENSITIVE_ABS_PATHS.flatMap(pathVariants)
1799
+ ].map((p) => `(subpath ${sb(p)})`).join("\n ");
1800
+ return [
1801
+ ";; ---- \u515C\u5E95\uFF1A\u51ED\u8BC1 / \u9690\u79C1\u76EE\u5F55\u59CB\u7EC8\u4E0D\u53EF\u8BFB\uFF08\u540E\u5339\u914D\u4F18\u5148\uFF0C\u538B\u5728 allow \u4E4B\u540E\uFF09----",
1802
+ `(deny file-read*
1803
+ ${sensitiveReads})`,
1804
+ ""
1805
+ ];
1806
+ }
1807
+ function networkSection(allowNetwork) {
1808
+ if (allowNetwork) return [";; ---- \u7F51\u7EDC ----", "(allow network*)"];
1809
+ return [
1810
+ ";; ---- \u7F51\u7EDC ----",
1811
+ "(deny network*)",
1812
+ // 留一个本机 unix socket,否则解析库初始化就可能失败
1813
+ '(allow network-outbound (literal "/private/var/run/mDNSResponder"))'
1814
+ ];
1815
+ }
1816
+ function buildProfile(opts) {
1817
+ const readable = [...SYSTEM_READ_PATHS, ...opts.writableDirs, ...opts.readableDirs ?? []];
1818
+ const writableLiterals = ["/dev/null", "/dev/zero", ...READ_DEVICES, "/dev/dtracehelper"].map((d) => `(literal ${sb(d)})`).join(" ");
1819
+ const lines = [
1820
+ "(version 1)",
1821
+ ";; \u975E\u6587\u4EF6\u64CD\u4F5C\u653E\u884C\u2014\u2014\u6574\u4F53 deny-default \u4F1A\u8BA9 Node \u76F4\u63A5 SIGABRT\uFF0C\u89C1\u6A21\u5757\u6CE8\u91CA\u3002",
1822
+ "(allow default)",
1823
+ "",
1824
+ ...readSection(opts, readable),
1825
+ ";; ---- \u5199\u5165\uFF1A\u9ED8\u8BA4\u5168\u7981\uFF0C\u53EA\u653E\u5F00\u6C99\u7BB1\u76EE\u5F55\u4E0E\u4E34\u65F6\u76EE\u5F55 ----",
1826
+ "(deny file-write*)",
1827
+ `(allow file-write* ${expandSubpaths(opts.writableDirs)} ${writableLiterals})`,
1828
+ "",
1829
+ ...sensitiveReadSection(opts),
1830
+ ...networkSection(opts.allowNetwork),
1831
+ "",
1832
+ ";; ---- \u660E\u786E\u7981\u6B62\u7684\u9AD8\u5371\u52A8\u4F5C ----",
1833
+ "(deny file-write-setugid)",
1834
+ '(deny process-exec* (literal "/usr/bin/sudo") (literal "/usr/bin/su"))',
1835
+ "(deny mach-priv-host-port)",
1836
+ ""
1837
+ ];
1838
+ return lines.join("\n");
1839
+ }
1840
+ var probed = null;
1841
+ function probeSeatbelt() {
1842
+ if (probed !== null) return probed;
1843
+ if (platform() !== "darwin") {
1844
+ probed = false;
1845
+ return probed;
1846
+ }
1847
+ try {
1848
+ execFileSync("/usr/bin/sandbox-exec", ["-p", "(version 1)(allow default)", "/usr/bin/true"], {
1849
+ stdio: "ignore",
1850
+ timeout: 5e3
1851
+ });
1852
+ probed = true;
1853
+ } catch {
1854
+ probed = false;
1855
+ }
1856
+ return probed;
1857
+ }
1858
+ function interpreterReadPaths(command) {
1859
+ const real = realpathOrSelf2(command);
1860
+ const binDir = dirname(real);
1861
+ const prefix = dirname(binDir);
1862
+ const paths = [real, binDir, prefix];
1863
+ let cur = prefix;
1864
+ for (let i = 0; i < 6; i++) {
1865
+ const parent = dirname(cur);
1866
+ if (parent === cur) break;
1867
+ if (/\.(framework|app)$/.test(cur)) {
1868
+ paths.push(cur);
1869
+ break;
1870
+ }
1871
+ cur = parent;
1872
+ }
1873
+ return paths;
1874
+ }
1875
+ function isolateWithSeatbelt(command, args2, opts) {
1876
+ if (!probeSeatbelt()) return null;
1877
+ const readableDirs = opts.reads === "unrestricted" ? opts.readableDirs : [...opts.readableDirs ?? [], ...interpreterReadPaths(command)];
1878
+ return {
1879
+ command: "/usr/bin/sandbox-exec",
1880
+ args: [
1881
+ "-p",
1882
+ buildProfile({ ...opts, ...readableDirs ? { readableDirs } : {} }),
1883
+ command,
1884
+ ...args2
1885
+ ]
1886
+ };
1887
+ }
1888
+
1889
+ // src/sandbox/backend.ts
1890
+ function detectBackend() {
1891
+ if (platform() === "darwin" && probeSeatbelt()) return "seatbelt";
1892
+ if (platform() === "linux" && probeBubblewrap()) return "bubblewrap";
1893
+ return "none";
1894
+ }
1895
+ function isolate(command, args2, opts) {
1896
+ switch (detectBackend()) {
1897
+ case "seatbelt":
1898
+ return isolateWithSeatbelt(command, args2, opts);
1899
+ case "bubblewrap":
1900
+ return isolateWithBubblewrap(command, args2, opts);
1901
+ default:
1902
+ return null;
1903
+ }
1904
+ }
1905
+
1906
+ // src/sandbox/classify.ts
1907
+ var DENIAL_SIGNATURES = {
1908
+ seatbelt: [/Operation not permitted/i, /\bEPERM\b/],
1909
+ bubblewrap: [/Read-only file system/i, /\bEROFS\b/],
1910
+ none: []
1911
+ };
1912
+ var RUNNER_FAILURE_RULES = {
1913
+ seatbelt: {
1914
+ fatal: [/^sandbox-exec:/],
1915
+ // sandbox-exec 在 macOS 上已标记 deprecated,它可能(现在或将来)
1916
+ // 往 stderr 打一句提示。那是**信息**不是失败,见方案 46 验收 7
1917
+ benign: [/^sandbox-exec:.*\bdeprecat/i]
1918
+ },
1919
+ bubblewrap: {
1920
+ fatal: [/^bwrap:/],
1921
+ // bwrap 在部分内核上会为无害的降级打一行提示(比如某个 unshare 跳过了)
1922
+ benign: [/^bwrap:.*\b(?:skipping|ignoring|deprecat)/i]
1923
+ },
1924
+ none: { fatal: [], benign: [] }
1925
+ };
1926
+ function matchesAny(line, patterns) {
1927
+ return patterns.some((re) => re.test(line));
1928
+ }
1929
+ function classifyFailure(input) {
1930
+ const { exitCode, stderr, denialSignatures, runnerFailureRules: rules } = input;
1931
+ const allowed = rules.allowedExitCodes ?? [0];
1932
+ if (allowed.includes(exitCode)) return { kind: "command-failed" };
1933
+ const lines = stderr.split("\n").map((l) => l.trimEnd());
1934
+ const survivors = lines.filter((l) => l.trim() !== "" && !matchesAny(l, rules.benign));
1935
+ const fatalLine = survivors.find((l) => matchesAny(l, rules.fatal));
1936
+ if (fatalLine !== void 0) return { kind: "runner-failed", evidence: fatalLine };
1937
+ const deniedLine = lines.find((l) => matchesAny(l, denialSignatures));
1938
+ if (deniedLine !== void 0) return { kind: "sandbox-denied", evidence: deniedLine };
1939
+ return { kind: "command-failed" };
1940
+ }
1941
+ function sandboxModeForLevel(level) {
1942
+ switch (level) {
1943
+ case "plan":
1944
+ return "read-only";
1945
+ case "bypass":
1946
+ return "danger-full-access";
1947
+ case "auto":
1948
+ case "default":
1949
+ case "acceptEdits":
1950
+ return "workspace-write";
1951
+ default:
1952
+ return "workspace-write";
1953
+ }
1954
+ }
1955
+ var TOOLCHAIN_CACHE_DIRS = [
1956
+ ".cache",
1957
+ ".npm",
1958
+ ".pnpm-store",
1959
+ ".local/share/pnpm",
1960
+ ".yarn",
1961
+ ".bun/install/cache",
1962
+ ".deno",
1963
+ ".cargo/registry",
1964
+ ".gradle/caches",
1965
+ ".m2/repository",
1966
+ "Library/Caches",
1967
+ "Library/pnpm"
1968
+ ];
1969
+ function enforcementFor(mode) {
1970
+ if (mode === "read-only") return "partial";
1971
+ return "full";
1972
+ }
1973
+ function writableDirsFor(policy) {
1974
+ if (policy.mode === "read-only") return [];
1975
+ const home = homedir();
1976
+ const roots = [policy.workspaceRoot, ...policy.extraRoots ?? []].filter((d) => typeof d === "string" && d !== "").map((d) => resolve$1(d));
1977
+ const caches = TOOLCHAIN_CACHE_DIRS.map((rel) => join(home, rel));
1978
+ return [.../* @__PURE__ */ new Set([...roots, resolve$1(tmpdir()), ...caches])];
1979
+ }
1980
+ function confine(command, args2, policy) {
1981
+ const backend = detectBackend();
1982
+ if (policy.mode === "danger-full-access") {
1983
+ return { confined: false, backend, mode: policy.mode, reason: "mode-disabled" };
1984
+ }
1985
+ const opts = {
1986
+ writableDirs: writableDirsFor(policy),
1987
+ allowNetwork: policy.allowNetwork,
1988
+ reads: "unrestricted",
1989
+ ...policy.cwd ? { cwd: policy.cwd } : {}
1990
+ };
1991
+ const wrapped = backend === "seatbelt" ? isolateWithSeatbelt(command, args2, opts) : backend === "bubblewrap" ? isolateWithBubblewrap(command, args2, opts) : null;
1992
+ if (!wrapped) {
1993
+ return { confined: false, backend, mode: policy.mode, reason: "no-backend" };
1994
+ }
1995
+ return {
1996
+ confined: true,
1997
+ command: wrapped.command,
1998
+ args: wrapped.args,
1999
+ backend,
2000
+ mode: policy.mode,
2001
+ enforcement: enforcementFor(policy.mode),
2002
+ writableDirs: opts.writableDirs,
2003
+ denialSignatures: DENIAL_SIGNATURES[backend],
2004
+ runnerFailureRules: RUNNER_FAILURE_RULES[backend]
2005
+ };
2006
+ }
2007
+ var EXEC_PATH_AS_NODE_ENV = {
2008
+ ELECTRON_RUN_AS_NODE: "1"
2009
+ };
2010
+ var tracked = /* @__PURE__ */ new Map();
2011
+ function startLongLivedProcess(opts) {
2012
+ const child = spawn(opts.file, [...opts.args], {
2013
+ stdio: ["ignore", "pipe", "pipe"],
2014
+ // 见文件头:和前台命令同一个决定,不 detach
2015
+ detached: false,
2016
+ ...opts.cwd ? { cwd: opts.cwd } : {},
2017
+ ...opts.spawnOptions
2018
+ });
2019
+ const pid = child.pid ?? 0;
2020
+ if (pid > 0) tracked.set(pid, child);
2021
+ const pump = (stream) => {
2022
+ child[stream]?.on("data", (buf) => {
2023
+ opts.onOutput?.(buf.toString("utf8"), stream);
2024
+ });
2025
+ };
2026
+ pump("stdout");
2027
+ pump("stderr");
2028
+ child.on("exit", (code, signal) => {
2029
+ tracked.delete(pid);
2030
+ opts.onExit?.(code, signal);
2031
+ });
2032
+ child.on("error", () => {
2033
+ tracked.delete(pid);
2034
+ opts.onExit?.(null, null);
2035
+ });
2036
+ return { pid, child };
2037
+ }
2038
+ async function killTrackedProcess(pid) {
2039
+ const child = tracked.get(pid);
2040
+ tracked.delete(pid);
2041
+ await killProcessTree({
2042
+ pid,
2043
+ detached: false,
2044
+ escalate: true,
2045
+ ...child ? { isExited: () => child.exitCode !== null || child.signalCode !== null } : {}
2046
+ });
2047
+ }
2048
+ function trackedProcessCount() {
2049
+ return tracked.size;
2050
+ }
2051
+ async function killAllTrackedProcesses() {
2052
+ const pids = [...tracked.keys()];
2053
+ await Promise.all(pids.map((pid) => killTrackedProcess(pid)));
2054
+ }
2055
+ function within(root, abs) {
2056
+ const rel = relative(root, abs);
2057
+ return !rel.startsWith("..") && !isAbsolute(rel);
2058
+ }
2059
+ function isInWorkspace(target, workDir, extraRoots = []) {
2060
+ if (!target) return true;
2061
+ const root = resolve$1(workDir);
2062
+ const abs = isAbsolute(target) ? resolve$1(target) : resolve$1(root, target);
2063
+ if (within(root, abs)) return true;
2064
+ return extraRoots.some((r) => within(resolve$1(r), abs));
2065
+ }
2066
+
2067
+ // src/schema.ts
2068
+ function parseLenient(schema, input, fallback) {
2069
+ const result = schema.safeParse(input);
2070
+ if (result.success) return { value: result.data, issues: [] };
2071
+ return { value: fallback, issues: toIssues(result.error.issues) };
2072
+ }
2073
+ function parseStrict(schema, input, label) {
2074
+ const result = schema.safeParse(input);
2075
+ if (result.success) return result.data;
2076
+ const issues = toIssues(result.error.issues);
2077
+ const detail = issues.map((i) => ` - ${i.path}: ${i.message}`).join("\n");
2078
+ throw new Error(`${label} \u6821\u9A8C\u5931\u8D25\uFF08${issues.length} \u5904\uFF09\uFF1A
2079
+ ${detail}`);
2080
+ }
2081
+ var DEFAULT_ISSUE_LIMIT = 8;
2082
+ function formatIssues(label, issues, limit = DEFAULT_ISSUE_LIMIT) {
2083
+ return issueDetails(issues, limit).map((d) => `${label}: ${d}`);
2084
+ }
2085
+ function issueDetails(issues, limit = DEFAULT_ISSUE_LIMIT) {
2086
+ if (issues.length === 0) return [];
2087
+ const shown = issues.slice(0, limit).map((i) => `${i.path} \u2014\u2014 ${i.message}`);
2088
+ const rest = issues.length - shown.length;
2089
+ if (rest > 0) shown.push(`\u8FD8\u6709 ${rest} \u5904\u95EE\u9898\u672A\u5217\u51FA`);
2090
+ return shown;
2091
+ }
2092
+ function unknownKeyIssues(known, input, prefix = "") {
2093
+ if (!input || typeof input !== "object" || Array.isArray(input)) return [];
2094
+ const knownSet = new Set(known);
2095
+ return Object.keys(input).filter((key) => !knownSet.has(key)).map((key) => ({
2096
+ path: prefix ? `${prefix}.${key}` : key,
2097
+ message: "\u672A\u77E5\u914D\u7F6E\u9879\uFF0C\u5DF2\u5FFD\u7565\uFF08\u4E0D\u5F71\u54CD\u5176\u4ED6\u914D\u7F6E\u751F\u6548\uFF09"
2098
+ }));
2099
+ }
2100
+ var MAX_NEST_DEPTH = 5;
2101
+ function toIssues(issues, prefix = [], depth = 0) {
2102
+ const out = [];
2103
+ for (const issue of issues) {
2104
+ const fullPath = [...prefix, ...issue.path];
2105
+ const nested = depth < MAX_NEST_DEPTH ? expandNested(issue, fullPath, depth) : [];
2106
+ if (nested.length > 0) out.push(...nested);
2107
+ else out.push({ path: joinPath(fullPath), message: issue.message });
2108
+ }
2109
+ return dedupe(out);
2110
+ }
2111
+ function expandNested(issue, fullPath, depth) {
2112
+ if (issue.code === "invalid_union") {
2113
+ return issue.errors.flatMap((branch) => toIssues(branch, fullPath, depth + 1));
2114
+ }
2115
+ if (issue.code === "invalid_key" || issue.code === "invalid_element") {
2116
+ return toIssues(issue.issues, fullPath, depth + 1);
2117
+ }
2118
+ return [];
2119
+ }
2120
+ function joinPath(path) {
2121
+ return path.length > 0 ? path.map((seg) => String(seg)).join(".") : "(root)";
2122
+ }
2123
+ function dedupe(issues) {
2124
+ const seen = /* @__PURE__ */ new Set();
2125
+ return issues.filter((issue) => {
2126
+ const key = `${issue.path} ${issue.message}`;
2127
+ if (seen.has(key)) return false;
2128
+ seen.add(key);
2129
+ return true;
2130
+ });
2131
+ }
2132
+
2133
+ // src/async.ts
2134
+ async function withTimeout(promise, ms, fallback) {
2135
+ let timer;
2136
+ try {
2137
+ return await Promise.race([
2138
+ promise,
2139
+ new Promise((resolve5) => {
2140
+ timer = setTimeout(() => resolve5(fallback), ms);
2141
+ })
2142
+ ]);
2143
+ } finally {
2144
+ if (timer) clearTimeout(timer);
2145
+ }
2146
+ }
2147
+ function safeInit(name, factory, diags) {
2148
+ try {
2149
+ const instance = factory();
2150
+ diags.ok(name);
2151
+ return instance;
2152
+ } catch (err) {
2153
+ diags.failed(
2154
+ name,
2155
+ `FAILED (${err instanceof Error ? err.message : String(err)})`,
2156
+ "init-failed"
2157
+ );
2158
+ return null;
2159
+ }
2160
+ }
2161
+ var LANGS = ["zh", "en"];
2162
+ var DEFAULT_LANG = "zh";
2163
+ function isLang(value) {
2164
+ return LANGS.includes(value);
2165
+ }
2166
+ var catalogs = /* @__PURE__ */ new Map();
2167
+ var pending = [];
2168
+ var localesDirCache;
2169
+ var currentLangValue;
2170
+ function localesDir() {
2171
+ if (localesDirCache !== void 0) return localesDirCache ?? void 0;
2172
+ const override = process.env.EPOCH_LOCALES_DIR;
2173
+ if (override) {
2174
+ localesDirCache = existsSync(override) ? override : null;
2175
+ return localesDirCache ?? void 0;
2176
+ }
2177
+ let dir = dirname(fileURLToPath(import.meta.url));
2178
+ for (let depth = 0; depth < 8; depth += 1) {
2179
+ if (basename(dir) === "node_modules") break;
2180
+ const candidate = join(dir, "locales");
2181
+ if (existsSync(join(candidate, `${DEFAULT_LANG}.yaml`))) {
2182
+ localesDirCache = candidate;
2183
+ return candidate;
2184
+ }
2185
+ const parent = dirname(dir);
2186
+ if (parent === dir) break;
2187
+ dir = parent;
2188
+ }
2189
+ localesDirCache = null;
2190
+ return void 0;
2191
+ }
2192
+ function flatten(value, prefix, out) {
2193
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
2194
+ for (const [key, child] of Object.entries(value)) {
2195
+ const path = prefix ? `${prefix}.${key}` : key;
2196
+ if (typeof child === "string") out[path] = child;
2197
+ else flatten(child, path, out);
2198
+ }
2199
+ }
2200
+ function loadCatalog(lang) {
2201
+ const cached2 = catalogs.get(lang);
2202
+ if (cached2) return cached2;
2203
+ const result = readCatalog(lang);
2204
+ catalogs.set(lang, result);
2205
+ return result;
2206
+ }
2207
+ function readCatalog(lang) {
2208
+ const dir = localesDir();
2209
+ if (!dir) {
2210
+ pending.push({ code: "locales-missing", render: () => t("i18n.locales_missing") });
2211
+ return { entries: {}, error: "\u627E\u4E0D\u5230 locales \u76EE\u5F55" };
2212
+ }
2213
+ const file = `${lang}.yaml`;
2214
+ const path = join(dir, file);
2215
+ if (!existsSync(path)) {
2216
+ pending.push({
2217
+ code: "catalog-missing",
2218
+ render: () => t("i18n.catalog_missing", { file, lang })
2219
+ });
2220
+ return { entries: {}, error: `${path} \u4E0D\u5B58\u5728` };
2221
+ }
2222
+ let parsed;
2223
+ try {
2224
+ parsed = load(readFileSync(path, "utf-8"));
2225
+ } catch (err) {
2226
+ const reason = (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? "";
2227
+ pending.push({
2228
+ code: "catalog-invalid",
2229
+ render: () => t("i18n.catalog_invalid", { file, reason })
2230
+ });
2231
+ return { entries: {}, error: reason };
2232
+ }
2233
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2234
+ const reason = "\u9876\u5C42\u4E0D\u662F\u952E\u503C\u5BF9";
2235
+ pending.push({
2236
+ code: "catalog-invalid",
2237
+ render: () => t("i18n.catalog_invalid", { file, reason })
2238
+ });
2239
+ return { entries: {}, error: reason };
2240
+ }
2241
+ const entries = {};
2242
+ flatten(parsed, "", entries);
2243
+ return { entries };
2244
+ }
2245
+ function resetI18n() {
2246
+ catalogs.clear();
2247
+ pending = [];
2248
+ localesDirCache = void 0;
2249
+ currentLangValue = void 0;
2250
+ }
2251
+ var PLACEHOLDER = /\{(\w+)\}/g;
2252
+ function placeholdersOf(text) {
2253
+ const found = [];
2254
+ for (const match of text.matchAll(PLACEHOLDER)) {
2255
+ const name = match[1];
2256
+ if (name && !found.includes(name)) found.push(name);
2257
+ }
2258
+ return found;
2259
+ }
2260
+ function t(key, vars, lang) {
2261
+ const target = lang ?? currentLang();
2262
+ const text = loadCatalog(target).entries[key] ?? loadCatalog(DEFAULT_LANG).entries[key] ?? key;
2263
+ if (!vars) return text;
2264
+ return text.replace(PLACEHOLDER, (whole, name) => {
2265
+ const value = vars[name];
2266
+ return value === void 0 ? whole : String(value);
2267
+ });
2268
+ }
2269
+ function currentLang() {
2270
+ currentLangValue ??= resolveLang();
2271
+ return currentLangValue;
2272
+ }
2273
+ function setLang(lang) {
2274
+ currentLangValue = lang;
2275
+ }
2276
+ function systemLocaleSignals() {
2277
+ const env = process.env;
2278
+ const signals = [env.LC_ALL, env.LC_MESSAGES, env.LANG, env.LANGUAGE].filter(
2279
+ (v) => typeof v === "string" && v.length > 0
2280
+ );
2281
+ try {
2282
+ signals.push(new Intl.DateTimeFormat().resolvedOptions().locale);
2283
+ } catch {
2284
+ }
2285
+ return signals;
2286
+ }
2287
+ function langFromLocale(tag) {
2288
+ const primary = tag.split(/[.@]/)[0]?.replace("_", "-").toLowerCase() ?? "";
2289
+ if (!primary || primary === "c" || primary === "posix") return void 0;
2290
+ if (primary === "zh" || primary.startsWith("zh-")) return "zh";
2291
+ return "en";
2292
+ }
2293
+ function resolveLang(configured, signals) {
2294
+ const fromEnv = process.env.EPOCH_LANGUAGE;
2295
+ if (fromEnv && isLang(fromEnv)) return fromEnv;
2296
+ if (configured && isLang(configured)) return configured;
2297
+ for (const tag of signals ?? systemLocaleSignals()) {
2298
+ const lang = langFromLocale(tag);
2299
+ if (lang) return lang;
2300
+ }
2301
+ return DEFAULT_LANG;
2302
+ }
2303
+ function i18nDiagnostics() {
2304
+ return pending.map((item) => ({ code: item.code, detail: item.render() }));
2305
+ }
2306
+ var SECRET_SERVICE = "epoch-agent";
2307
+ function secretServiceFor(homeDir) {
2308
+ if (homeDir === join(homedir(), ".epoch")) return SECRET_SERVICE;
2309
+ const profile = process.env.EPOCH_PROFILE;
2310
+ if (!process.env.EPOCH_HOME && profile && profile !== "default") {
2311
+ const safe = profile.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 40);
2312
+ return `${SECRET_SERVICE}:${safe}`;
2313
+ }
2314
+ return `${SECRET_SERVICE}:${createHash("sha256").update(homeDir).digest("hex").slice(0, 12)}`;
2315
+ }
2316
+ var SECRET_MODE = 384;
2317
+ var PROBE_TIMEOUT_MS2 = 3e3;
2318
+ var POWERSHELL_TIMEOUT_MS = 15e3;
2319
+ var OP_TIMEOUT_MS = 1e4;
2320
+ var BACKEND_ENV_VAR = "EPOCH_SECRET_BACKEND";
2321
+ function assertValidSecretName(name) {
2322
+ if (!/^[A-Za-z0-9._:-]{1,128}$/.test(name)) {
2323
+ throw new Error(`\u975E\u6CD5\u7684\u51ED\u636E\u540D "${name}"\uFF1A\u53EA\u5141\u8BB8 A-Z a-z 0-9 . _ : - \uFF0C\u4E14 1~128 \u5B57\u7B26`);
2324
+ }
2325
+ }
2326
+ function secretIndexPath(homeDir) {
2327
+ return join(homeDir, "secrets-index.json");
2328
+ }
2329
+ function dpapiStorePath(homeDir) {
2330
+ return join(homeDir, "secrets.dpapi.json");
2331
+ }
2332
+
2333
+ // src/secret/catalog.ts
2334
+ var FILE_VERSION = 1;
2335
+ var SecretCatalog = class {
2336
+ constructor(filePath) {
2337
+ this.filePath = filePath;
2338
+ }
2339
+ filePath;
2340
+ cache = null;
2341
+ /** 已登记的名字,顺序稳定(排序过),方便 `epoch config secret list` 输出稳定 */
2342
+ read() {
2343
+ if (this.cache) return [...this.cache];
2344
+ this.cache = parse(this.filePath);
2345
+ return [...this.cache];
2346
+ }
2347
+ add(name) {
2348
+ const names = this.read();
2349
+ if (names.includes(name)) return;
2350
+ this.write([...names, name]);
2351
+ }
2352
+ remove(name) {
2353
+ const names = this.read();
2354
+ if (!names.includes(name)) return;
2355
+ this.write(names.filter((n) => n !== name));
2356
+ }
2357
+ /** 整体替换。预取自愈用:把「钥匙串里真读得出来」的那份写回去 */
2358
+ replace(names) {
2359
+ const next = [...new Set(names)].sort();
2360
+ if (sameList(this.read(), next)) return;
2361
+ this.write(next);
2362
+ }
2363
+ /** 仅供测试:丢掉内存缓存,强制重新读盘 */
2364
+ invalidate() {
2365
+ this.cache = null;
2366
+ }
2367
+ /**
2368
+ * 原子写 + 建文件时就 0600。
2369
+ *
2370
+ * mode 在**创建时**生效,写完再 chmod 会留 TOCTOU 窗口
2371
+ * [对标 hermes mcp_oauth.py:388 的 `_write_secure_json`]。
2372
+ */
2373
+ write(names) {
2374
+ const sorted = [...names].sort();
2375
+ const file = { version: FILE_VERSION, names: sorted };
2376
+ mkdirSync(dirname(this.filePath), { recursive: true });
2377
+ const tmp = `${this.filePath}.${process.pid}.tmp`;
2378
+ try {
2379
+ writeFileSync(tmp, JSON.stringify(file, null, 2), {
2380
+ encoding: "utf-8",
2381
+ mode: SECRET_MODE
2382
+ });
2383
+ renameSync(tmp, this.filePath);
2384
+ } catch (err) {
2385
+ rmSync(tmp, { force: true });
2386
+ throw new Error(
2387
+ `\u51ED\u636E\u7D22\u5F15 ${this.filePath} \u5199\u5165\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
2388
+ );
2389
+ }
2390
+ this.cache = sorted;
2391
+ }
2392
+ };
2393
+ function parse(filePath) {
2394
+ if (!existsSync(filePath)) return [];
2395
+ try {
2396
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
2397
+ const names = raw.names;
2398
+ if (!Array.isArray(names)) return [];
2399
+ return [...new Set(names.filter((n) => typeof n === "string" && n.length > 0))].sort().slice(0, 512);
2400
+ } catch {
2401
+ return [];
2402
+ }
2403
+ }
2404
+ function sameList(a, b) {
2405
+ return a.length === b.length && a.every((v, i) => v === b[i]);
2406
+ }
2407
+ function run(file, args2, opts) {
2408
+ return new Promise((resolve5) => {
2409
+ const child = execFile(
2410
+ file,
2411
+ args2,
2412
+ {
2413
+ timeout: opts.timeoutMs,
2414
+ // 凭据值可能几 KB(OAuth token 串),默认 1MB 够用但显式写出来更清楚
2415
+ maxBuffer: 1024 * 1024,
2416
+ encoding: "utf-8",
2417
+ // Windows 上别弹黑框:PowerShell 冷启动会闪一个控制台窗口
2418
+ ...WINDOWS_HIDE_FLAGS
2419
+ },
2420
+ (err, stdout, stderr) => {
2421
+ resolve5({
2422
+ ...classify(err),
2423
+ stdout: String(stdout ?? ""),
2424
+ stderr: String(stderr ?? "")
2425
+ });
2426
+ }
2427
+ );
2428
+ if (opts.stdin !== void 0) {
2429
+ child.stdin?.on("error", () => {
2430
+ });
2431
+ child.stdin?.end(opts.stdin);
2432
+ }
2433
+ });
2434
+ }
2435
+ function classify(err) {
2436
+ if (!err) return { code: 0, timedOut: false, missing: false };
2437
+ const e = err;
2438
+ const missing = e.code === "ENOENT";
2439
+ const timedOut = Boolean(e.killed) || e.signal === "SIGTERM";
2440
+ return {
2441
+ code: typeof e.code === "number" ? e.code : null,
2442
+ timedOut: timedOut && !missing,
2443
+ missing
2444
+ };
2445
+ }
2446
+ function describeFailure(file, r) {
2447
+ if (r.missing) return `\u627E\u4E0D\u5230 ${file}`;
2448
+ if (r.timedOut) return `${file} \u8D85\u65F6\u672A\u8FD4\u56DE`;
2449
+ const detail = r.stderr.trim().split("\n")[0]?.slice(0, 200);
2450
+ return `${file} \u9000\u51FA\u7801 ${r.code}${detail ? `\uFF08${detail}\uFF09` : ""}`;
2451
+ }
2452
+
2453
+ // src/secret/dpapi.ts
2454
+ var FILE_VERSION2 = 1;
2455
+ var SHELLS2 = ["powershell.exe", "pwsh"];
2456
+ var PS_FLAGS = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command"];
2457
+ var PROTECT_SCRIPT = [
2458
+ '$ErrorActionPreference="Stop";',
2459
+ "$in=[Console]::In.ReadToEnd().Trim();",
2460
+ "(ConvertTo-SecureString -String $in -AsPlainText -Force | ConvertFrom-SecureString)"
2461
+ ].join("");
2462
+ var UNPROTECT_SCRIPT = [
2463
+ '$ErrorActionPreference="Stop";',
2464
+ "$in=[Console]::In.ReadToEnd().Trim();",
2465
+ "$s=ConvertTo-SecureString -String $in;",
2466
+ "$b=[Runtime.InteropServices.Marshal]::SecureStringToBSTR($s);",
2467
+ "try{[Runtime.InteropServices.Marshal]::PtrToStringBSTR($b)}",
2468
+ "finally{[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($b)}"
2469
+ ].join("");
2470
+ var DpapiStore = class {
2471
+ constructor(filePath, shell) {
2472
+ this.filePath = filePath;
2473
+ this.shell = shell;
2474
+ }
2475
+ filePath;
2476
+ shell;
2477
+ backend = "dpapi";
2478
+ encrypted = true;
2479
+ detail = "Windows DPAPI\uFF08\u5DF2\u52A0\u5BC6\uFF0C\u7ED1\u5B9A\u5F53\u524D\u7528\u6237\u8D26\u6237\uFF09";
2480
+ cache = null;
2481
+ async get(name) {
2482
+ assertValidSecretName(name);
2483
+ const blob = this.load().secrets[name];
2484
+ if (!blob) return void 0;
2485
+ const r = await this.ps(UNPROTECT_SCRIPT, blob);
2486
+ if (r.code !== 0) throw new Error(`\u8BFB\u53D6\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(this.shell, r)}`);
2487
+ return Buffer.from(r.stdout.trim(), "base64").toString("utf-8");
2488
+ }
2489
+ async set(name, value) {
2490
+ assertValidSecretName(name);
2491
+ const r = await this.ps(PROTECT_SCRIPT, Buffer.from(value, "utf-8").toString("base64"));
2492
+ if (r.code !== 0) throw new Error(`\u5199\u5165\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(this.shell, r)}`);
2493
+ const file = this.load();
2494
+ file.secrets[name] = stripBlank(r.stdout);
2495
+ this.save(file);
2496
+ }
2497
+ delete(name) {
2498
+ assertValidSecretName(name);
2499
+ const file = this.load();
2500
+ if (name in file.secrets) {
2501
+ delete file.secrets[name];
2502
+ this.save(file);
2503
+ }
2504
+ return Promise.resolve();
2505
+ }
2506
+ /** 密文就在自己的文件里,名字直接是 key —— 这个后端不需要 catalog.ts 的索引 */
2507
+ list() {
2508
+ return Promise.resolve(Object.keys(this.load().secrets).sort());
2509
+ }
2510
+ ps(script, stdin) {
2511
+ return run(this.shell, [...PS_FLAGS, script], { timeoutMs: POWERSHELL_TIMEOUT_MS, stdin });
2512
+ }
2513
+ load() {
2514
+ if (this.cache) return this.cache;
2515
+ this.cache = readDpapiFile(this.filePath);
2516
+ return this.cache;
2517
+ }
2518
+ /** 原子写 + 建文件时就 0600(Windows 上 mode 是空操作,但这份代码也跑在测试里) */
2519
+ save(file) {
2520
+ mkdirSync(dirname(this.filePath), { recursive: true });
2521
+ const tmp = `${this.filePath}.${process.pid}.tmp`;
2522
+ try {
2523
+ writeFileSync(tmp, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: SECRET_MODE });
2524
+ renameSync(tmp, this.filePath);
2525
+ } catch (err) {
2526
+ rmSync(tmp, { force: true });
2527
+ throw new Error(
2528
+ `\u51ED\u636E\u6587\u4EF6 ${this.filePath} \u5199\u5165\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
2529
+ );
2530
+ }
2531
+ this.cache = file;
2532
+ }
2533
+ };
2534
+ function readDpapiFile(filePath) {
2535
+ const empty = { version: FILE_VERSION2, secrets: {} };
2536
+ if (!existsSync(filePath)) return empty;
2537
+ try {
2538
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
2539
+ const secrets = raw.secrets;
2540
+ if (typeof secrets !== "object" || secrets === null) return empty;
2541
+ const out = {};
2542
+ for (const [k, v] of Object.entries(secrets)) {
2543
+ if (typeof v === "string" && v.length > 0) out[k] = v;
2544
+ }
2545
+ return { version: FILE_VERSION2, secrets: out };
2546
+ } catch {
2547
+ return empty;
2548
+ }
2549
+ }
2550
+ async function probeDpapi() {
2551
+ if (platform() !== "win32") return { reason: "\u4E0D\u662F Windows" };
2552
+ const expect = randomBytes(8).toString("hex");
2553
+ const payload = Buffer.from(expect, "utf-8").toString("base64");
2554
+ let last = "\u672A\u627E\u5230 PowerShell\uFF08powershell.exe / pwsh \u90FD\u4E0D\u53EF\u7528\uFF09";
2555
+ for (const shell of SHELLS2) {
2556
+ const sealed = await run(shell, [...PS_FLAGS, PROTECT_SCRIPT], {
2557
+ timeoutMs: POWERSHELL_TIMEOUT_MS,
2558
+ stdin: payload
2559
+ });
2560
+ if (sealed.missing) continue;
2561
+ if (sealed.code !== 0) {
2562
+ last = `\u52A0\u5BC6\u63A2\u6D4B\u5931\u8D25\uFF08${describeFailure(shell, sealed)}\uFF09`;
2563
+ continue;
2564
+ }
2565
+ const back = await run(shell, [...PS_FLAGS, UNPROTECT_SCRIPT], {
2566
+ timeoutMs: POWERSHELL_TIMEOUT_MS,
2567
+ stdin: stripBlank(sealed.stdout)
2568
+ });
2569
+ if (back.code !== 0) {
2570
+ last = `\u89E3\u5BC6\u63A2\u6D4B\u5931\u8D25\uFF08${describeFailure(shell, back)}\uFF09`;
2571
+ continue;
2572
+ }
2573
+ if (Buffer.from(back.stdout.trim(), "base64").toString("utf-8") !== expect) {
2574
+ last = "\u52A0\u89E3\u5BC6\u63A2\u6D4B\u7684\u503C\u5BF9\u4E0D\u4E0A";
2575
+ continue;
2576
+ }
2577
+ return { shell };
2578
+ }
2579
+ return { reason: last };
2580
+ }
2581
+ function stripBlank(s) {
2582
+ return s.replace(/\s+/g, "");
2583
+ }
2584
+ var SECURITY = "/usr/bin/security";
2585
+ var NOT_FOUND = 44;
2586
+ var KeychainStore = class {
2587
+ /**
2588
+ * @param service 钥匙串里的服务名。**按家目录分命名空间**,见
2589
+ * [types.ts](./types.js) 的 `secretServiceFor()` ——
2590
+ * 钥匙串是全局的,写死一个名字会让多 profile 共用同一批凭据
2591
+ */
2592
+ constructor(catalog, service) {
2593
+ this.catalog = catalog;
2594
+ this.service = service;
2595
+ }
2596
+ catalog;
2597
+ service;
2598
+ backend = "keychain";
2599
+ encrypted = true;
2600
+ detail = "macOS Keychain\uFF08\u5DF2\u52A0\u5BC6\uFF09";
2601
+ async get(name) {
2602
+ assertValidSecretName(name);
2603
+ const r = await run(SECURITY, ["find-generic-password", "-s", this.service, "-a", name, "-w"], {
2604
+ timeoutMs: OP_TIMEOUT_MS
2605
+ });
2606
+ if (r.code === NOT_FOUND) return void 0;
2607
+ if (r.code !== 0) throw new Error(`\u8BFB\u53D6\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(SECURITY, r)}`);
2608
+ return decode(r.stdout);
2609
+ }
2610
+ async set(name, value) {
2611
+ assertValidSecretName(name);
2612
+ const r = await run(
2613
+ SECURITY,
2614
+ [
2615
+ "add-generic-password",
2616
+ "-U",
2617
+ "-s",
2618
+ this.service,
2619
+ "-a",
2620
+ name,
2621
+ "-D",
2622
+ "epoch-agent credential",
2623
+ "-w",
2624
+ encode(value)
2625
+ ],
2626
+ { timeoutMs: OP_TIMEOUT_MS }
2627
+ );
2628
+ if (r.code !== 0) throw new Error(`\u5199\u5165\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(SECURITY, r)}`);
2629
+ this.catalog.add(name);
2630
+ }
2631
+ async delete(name) {
2632
+ assertValidSecretName(name);
2633
+ const r = await run(SECURITY, ["delete-generic-password", "-s", this.service, "-a", name], {
2634
+ timeoutMs: OP_TIMEOUT_MS
2635
+ });
2636
+ if (r.code !== 0 && r.code !== NOT_FOUND) {
2637
+ throw new Error(`\u5220\u9664\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(SECURITY, r)}`);
2638
+ }
2639
+ this.catalog.remove(name);
2640
+ }
2641
+ /** 见 catalog.ts:钥匙串本身没有便宜的列举方式,索引是提示、`get` 才是权威 */
2642
+ list() {
2643
+ return Promise.resolve(this.catalog.read());
2644
+ }
2645
+ /** 预取时发现索引里有、钥匙串里没有的名字,就地剔掉(自愈) */
2646
+ healCatalog(available) {
2647
+ this.catalog.replace(available);
2648
+ }
2649
+ };
2650
+ async function probeKeychain(service) {
2651
+ if (platform() !== "darwin") return "\u4E0D\u662F macOS";
2652
+ const dk = await run(SECURITY, ["default-keychain"], { timeoutMs: PROBE_TIMEOUT_MS2 });
2653
+ if (dk.code !== 0 || dk.stdout.trim() === "") {
2654
+ return `\u6CA1\u6709\u9ED8\u8BA4\u94A5\u5319\u4E32\uFF08${describeFailure(SECURITY, dk)}\uFF09`;
2655
+ }
2656
+ const account = `__probe__${randomBytes(8).toString("hex")}`;
2657
+ const expect = randomBytes(8).toString("hex");
2658
+ const args2 = ["-s", service, "-a", account];
2659
+ const added = await run(SECURITY, ["add-generic-password", "-U", ...args2, "-w", encode(expect)], {
2660
+ timeoutMs: PROBE_TIMEOUT_MS2
2661
+ });
2662
+ if (added.code !== 0) return `\u5199\u5165\u63A2\u6D4B\u5931\u8D25\uFF08${describeFailure(SECURITY, added)}\uFF09`;
2663
+ const read = await run(SECURITY, ["find-generic-password", ...args2, "-w"], {
2664
+ timeoutMs: PROBE_TIMEOUT_MS2
2665
+ });
2666
+ await run(SECURITY, ["delete-generic-password", ...args2], { timeoutMs: PROBE_TIMEOUT_MS2 });
2667
+ if (read.code !== 0) return `\u8BFB\u53D6\u63A2\u6D4B\u5931\u8D25\uFF08${describeFailure(SECURITY, read)}\uFF09`;
2668
+ if (decode(read.stdout) !== expect) return "\u8BFB\u5199\u63A2\u6D4B\u7684\u503C\u5BF9\u4E0D\u4E0A";
2669
+ return null;
2670
+ }
2671
+ function encode(value) {
2672
+ return Buffer.from(value, "utf-8").toString("base64");
2673
+ }
2674
+ function decode(stdout) {
2675
+ return Buffer.from(stdout.trim(), "base64").toString("utf-8");
2676
+ }
2677
+ var SECRET_TOOL = "secret-tool";
2678
+ var LibsecretStore = class {
2679
+ /**
2680
+ * @param service Secret Service 里的 `service` 属性值。**按家目录分命名空间**,
2681
+ * 见 [types.ts](./types.js) 的 `secretServiceFor()`
2682
+ */
2683
+ constructor(catalog, service) {
2684
+ this.catalog = catalog;
2685
+ this.service = service;
2686
+ }
2687
+ catalog;
2688
+ service;
2689
+ backend = "libsecret";
2690
+ encrypted = true;
2691
+ detail = "Linux libsecret / Secret Service\uFF08\u5DF2\u52A0\u5BC6\uFF09";
2692
+ /** 条目的属性对。用 service + account 两个属性,和 macOS 那边同一套心智 */
2693
+ attrs(name) {
2694
+ return ["service", this.service, "account", name];
2695
+ }
2696
+ async get(name) {
2697
+ assertValidSecretName(name);
2698
+ const r = await run(SECRET_TOOL, ["lookup", ...this.attrs(name)], { timeoutMs: OP_TIMEOUT_MS });
2699
+ if (r.code === 0) return decode2(r.stdout);
2700
+ if (r.stdout.trim() === "" && !r.timedOut && !r.missing) return void 0;
2701
+ throw new Error(`\u8BFB\u53D6\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(SECRET_TOOL, r)}`);
2702
+ }
2703
+ async set(name, value) {
2704
+ assertValidSecretName(name);
2705
+ const r = await run(
2706
+ SECRET_TOOL,
2707
+ ["store", `--label=${this.service}: ${name}`, ...this.attrs(name)],
2708
+ { timeoutMs: OP_TIMEOUT_MS, stdin: encode2(value) }
2709
+ );
2710
+ if (r.code !== 0) throw new Error(`\u5199\u5165\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(SECRET_TOOL, r)}`);
2711
+ this.catalog.add(name);
2712
+ }
2713
+ async delete(name) {
2714
+ assertValidSecretName(name);
2715
+ const r = await run(SECRET_TOOL, ["clear", ...this.attrs(name)], { timeoutMs: OP_TIMEOUT_MS });
2716
+ if (r.code !== 0 && (r.timedOut || r.missing)) {
2717
+ throw new Error(`\u5220\u9664\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(SECRET_TOOL, r)}`);
2718
+ }
2719
+ this.catalog.remove(name);
2720
+ }
2721
+ /**
2722
+ * `secret-tool search --all` 能列,但输出格式随 libsecret 版本变,
2723
+ * 而且要多走一次 D-Bus 往返。索引更便宜也更稳定 —— 见 catalog.ts。
2724
+ */
2725
+ list() {
2726
+ return Promise.resolve(this.catalog.read());
2727
+ }
2728
+ /** 预取时发现索引里有、keyring 里没有的名字,就地剔掉(自愈) */
2729
+ healCatalog(available) {
2730
+ this.catalog.replace(available);
2731
+ }
2732
+ };
2733
+ async function probeLibsecret(service) {
2734
+ if (platform() === "darwin" || platform() === "win32") return "\u4E0D\u662F Linux";
2735
+ const account = `__probe__${randomBytes(8).toString("hex")}`;
2736
+ const expect = randomBytes(8).toString("hex");
2737
+ const args2 = ["service", service, "account", account];
2738
+ const stored = await run(SECRET_TOOL, ["store", `--label=${service} probe`, ...args2], {
2739
+ timeoutMs: PROBE_TIMEOUT_MS2,
2740
+ stdin: encode2(expect)
2741
+ });
2742
+ if (stored.missing) {
2743
+ return "\u672A\u627E\u5230 secret-tool\uFF08\u88C5\u6CD5\uFF1Aapt install libsecret-tools / dnf install libsecret\uFF09";
2744
+ }
2745
+ if (stored.timedOut) {
2746
+ return "secret-tool \u65E0\u54CD\u5E94\uFF0C\u591A\u534A\u662F\u6CA1\u6709 D-Bus \u6216 keyring \u672A\u89E3\u9501\uFF08headless / SSH \u4F1A\u8BDD\u5E38\u89C1\uFF09";
2747
+ }
2748
+ if (stored.code !== 0) return `\u5199\u5165\u63A2\u6D4B\u5931\u8D25\uFF08${describeFailure(SECRET_TOOL, stored)}\uFF09`;
2749
+ const read = await run(SECRET_TOOL, ["lookup", ...args2], { timeoutMs: PROBE_TIMEOUT_MS2 });
2750
+ await run(SECRET_TOOL, ["clear", ...args2], { timeoutMs: PROBE_TIMEOUT_MS2 });
2751
+ if (read.code !== 0) return `\u8BFB\u53D6\u63A2\u6D4B\u5931\u8D25\uFF08${describeFailure(SECRET_TOOL, read)}\uFF09`;
2752
+ if (decode2(read.stdout) !== expect) return "\u8BFB\u5199\u63A2\u6D4B\u7684\u503C\u5BF9\u4E0D\u4E0A";
2753
+ return null;
2754
+ }
2755
+ function encode2(value) {
2756
+ return Buffer.from(value, "utf-8").toString("base64");
2757
+ }
2758
+ function decode2(stdout) {
2759
+ return Buffer.from(stdout.trim(), "base64").toString("utf-8");
2760
+ }
2761
+ var PlaintextStore = class {
2762
+ /**
2763
+ * @param reason 为什么降级。**必须带修复方法** —— 只说「明文」而不说怎么修,
2764
+ * 用户除了忍着没有别的选择
2765
+ */
2766
+ constructor(envFilePath, reason) {
2767
+ this.envFilePath = envFilePath;
2768
+ this.detail = `\u660E\u6587\u964D\u7EA7 \u2014\u2014 ${reason}\uFF0C\u51ED\u636E\u4ECD\u662F\u660E\u6587\uFF08${envFilePath}\uFF0C\u6743\u9650 0600\uFF09`;
2769
+ }
2770
+ envFilePath;
2771
+ backend = "plaintext";
2772
+ encrypted = false;
2773
+ detail;
2774
+ get(name) {
2775
+ return Promise.resolve(parseEnvText(readText(this.envFilePath))[name]);
2776
+ }
2777
+ set(name, value) {
2778
+ writeEnvFile(this.envFilePath, setEnvVarText(readText(this.envFilePath), name, value));
2779
+ return Promise.resolve();
2780
+ }
2781
+ delete(name) {
2782
+ const text = readText(this.envFilePath);
2783
+ if (!existsSync(this.envFilePath)) return Promise.resolve();
2784
+ writeEnvFile(this.envFilePath, removeEnvVarText(text, name));
2785
+ return Promise.resolve();
2786
+ }
2787
+ list() {
2788
+ return Promise.resolve(Object.keys(parseEnvText(readText(this.envFilePath))).sort());
2789
+ }
2790
+ };
2791
+ function isAssignmentTo(line, name) {
2792
+ const trimmed = line.trim();
2793
+ if (!trimmed || trimmed.startsWith("#")) return false;
2794
+ const eq = trimmed.indexOf("=");
2795
+ if (eq === -1) return false;
2796
+ return trimmed.slice(0, eq).trim() === name;
2797
+ }
2798
+ function setEnvVarText(text, name, value) {
2799
+ const entry = `${name}=${value}`;
2800
+ let replaced = false;
2801
+ const kept = text.split("\n").filter((line) => {
2802
+ if (!isAssignmentTo(line, name)) return true;
2803
+ if (replaced) return false;
2804
+ replaced = true;
2805
+ return true;
2806
+ });
2807
+ const out = kept.map((line) => isAssignmentTo(line, name) ? entry : line);
2808
+ if (!replaced) {
2809
+ while (out.length > 0 && out[out.length - 1]?.trim() === "") out.pop();
2810
+ out.push(entry);
2811
+ }
2812
+ while (out.length > 0 && out[out.length - 1]?.trim() === "") out.pop();
2813
+ return out.join("\n") + "\n";
2814
+ }
2815
+ function removeEnvVarText(text, name) {
2816
+ const out = text.split("\n").filter((line) => !isAssignmentTo(line, name));
2817
+ while (out.length > 0 && out[out.length - 1]?.trim() === "") out.pop();
2818
+ return out.length === 0 ? "" : out.join("\n") + "\n";
2819
+ }
2820
+ function parseEnvText(text) {
2821
+ const result = {};
2822
+ for (const line of text.split("\n")) {
2823
+ const trimmed = line.trim();
2824
+ if (!trimmed || trimmed.startsWith("#")) continue;
2825
+ const eq = trimmed.indexOf("=");
2826
+ if (eq === -1) continue;
2827
+ result[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
2828
+ }
2829
+ return result;
2830
+ }
2831
+ function writeEnvFile(path, text) {
2832
+ writeFileSync(path, text, { encoding: "utf-8", mode: SECRET_MODE });
2833
+ try {
2834
+ chmodSync(path, SECRET_MODE);
2835
+ } catch {
2836
+ }
2837
+ }
2838
+ function readText(path) {
2839
+ try {
2840
+ return readFileSync(path, "utf-8");
2841
+ } catch {
2842
+ return "";
2843
+ }
2844
+ }
2845
+ var ENVELOPE_PREFIX = "epoch-enc:v1:";
2846
+ var ALGORITHM = "aes-256-gcm";
2847
+ var KEY_BYTES = 32;
2848
+ var IV_BYTES = 12;
2849
+ var TAG_BYTES = 16;
2850
+ var dataKey = null;
2851
+ function setDataKey(key) {
2852
+ if (key !== null && key.length !== KEY_BYTES) {
2853
+ throw new Error(`\u6570\u636E\u5BC6\u94A5\u957F\u5EA6\u5FC5\u987B\u662F ${KEY_BYTES} \u5B57\u8282\uFF0C\u5B9E\u9645 ${key.length}`);
2854
+ }
2855
+ dataKey = key;
2856
+ }
2857
+ function getDataKey() {
2858
+ return dataKey;
2859
+ }
2860
+ function generateDataKey() {
2861
+ return randomBytes(KEY_BYTES).toString("base64");
2862
+ }
2863
+ function parseDataKey(encoded) {
2864
+ if (!encoded) return null;
2865
+ try {
2866
+ const buf = Buffer.from(encoded, "base64");
2867
+ return buf.length === KEY_BYTES ? buf : null;
2868
+ } catch {
2869
+ return null;
2870
+ }
2871
+ }
2872
+ function sealEnvelope(key, plaintext) {
2873
+ const iv = randomBytes(IV_BYTES);
2874
+ const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: TAG_BYTES });
2875
+ const body = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
2876
+ return ENVELOPE_PREFIX + Buffer.concat([iv, cipher.getAuthTag(), body]).toString("base64");
2877
+ }
2878
+ function isEnvelope(text) {
2879
+ return text.startsWith(ENVELOPE_PREFIX);
2880
+ }
2881
+ function openEnvelope(key, blob) {
2882
+ if (!isEnvelope(blob)) throw new Error("\u4E0D\u662F epoch \u4FE1\u5C01\u5BC6\u6587");
2883
+ const raw = Buffer.from(blob.slice(ENVELOPE_PREFIX.length), "base64");
2884
+ if (raw.length < IV_BYTES + TAG_BYTES) throw new Error("\u4FE1\u5C01\u5BC6\u6587\u957F\u5EA6\u4E0D\u8DB3");
2885
+ const iv = raw.subarray(0, IV_BYTES);
2886
+ const tag = raw.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);
2887
+ const body = raw.subarray(IV_BYTES + TAG_BYTES);
2888
+ const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: TAG_BYTES });
2889
+ decipher.setAuthTag(tag);
2890
+ return Buffer.concat([decipher.update(body), decipher.final()]).toString("utf-8");
2891
+ }
2892
+ async function migrateEnvSecrets(opts) {
2893
+ const { envFilePath, store, names, stamp } = opts;
2894
+ if (!existsSync(envFilePath)) return { migrated: [] };
2895
+ const fromEnv = parseEnvText(readOrEmpty(envFilePath));
2896
+ const candidates = names.filter((n) => Boolean(fromEnv[n]));
2897
+ if (candidates.length === 0) return { migrated: [] };
2898
+ let existing;
2899
+ try {
2900
+ existing = new Set(await store.list());
2901
+ } catch (err) {
2902
+ return { migrated: [], warning: `\u8BFB\u53D6\u5DF2\u6709\u51ED\u636E\u5931\u8D25\uFF0C\u8DF3\u8FC7\u8FC1\u79FB\uFF08${message(err)}\uFF09` };
2903
+ }
2904
+ const todo = candidates.filter((n) => !existing.has(n));
2905
+ if (todo.length === 0) return { migrated: [] };
2906
+ const migrated = [];
2907
+ for (const name of todo) {
2908
+ const value = fromEnv[name];
2909
+ if (!value) continue;
2910
+ try {
2911
+ await store.set(name, value);
2912
+ migrated.push(name);
2913
+ } catch (err) {
2914
+ return {
2915
+ migrated,
2916
+ warning: `\u8FC1\u79FB ${name} \u5931\u8D25\uFF0C\u5DF2\u505C\u6B62\u4E14\u4FDD\u7559 ${envFilePath}\uFF08${message(err)}\uFF09`
2917
+ };
2918
+ }
2919
+ }
2920
+ const backupPath = uniquePath(`${envFilePath}.migrated-${stamp}`);
2921
+ try {
2922
+ renameSync(envFilePath, backupPath);
2923
+ } catch (err) {
2924
+ return { migrated, warning: `\u5DF2\u8FC1\u5165\u94A5\u5319\u4E32\uFF0C\u4F46 ${envFilePath} \u6539\u540D\u5931\u8D25\uFF08${message(err)}\uFF09` };
2925
+ }
2926
+ return { migrated, backupPath };
2927
+ }
2928
+ function uniquePath(base) {
2929
+ if (!existsSync(base)) return base;
2930
+ for (let i = 2; i < 100; i++) {
2931
+ const candidate = `${base}.${i}`;
2932
+ if (!existsSync(candidate)) return candidate;
2933
+ }
2934
+ return `${base}.${process.pid}`;
2935
+ }
2936
+ function readOrEmpty(path) {
2937
+ try {
2938
+ return readFileSync(path, "utf-8");
2939
+ } catch {
2940
+ return "";
2941
+ }
2942
+ }
2943
+ function message(err) {
2944
+ return err instanceof Error ? err.message : String(err);
2945
+ }
2946
+
2947
+ // src/secret/index.ts
2948
+ async function createSecretStore(opts = {}) {
2949
+ if (opts.inject) return opts.inject;
2950
+ const homeDir = opts.homeDir ?? resolveHomeDir();
2951
+ const forced = readForcedBackend();
2952
+ if (forced === "plaintext") {
2953
+ return new PlaintextStore(envPath(homeDir), `${BACKEND_ENV_VAR}=plaintext \u663E\u5F0F\u6307\u5B9A`);
2954
+ }
2955
+ const reason = await tryBackends(homeDir, forced);
2956
+ if (typeof reason !== "string") return reason;
2957
+ return new PlaintextStore(envPath(homeDir), reason);
2958
+ }
2959
+ async function tryBackends(homeDir, forced) {
2960
+ const want = (id) => forced === null || forced === id;
2961
+ const catalog = new SecretCatalog(secretIndexPath(homeDir));
2962
+ const service = secretServiceFor(homeDir);
2963
+ if (want("keychain") && platform() === "darwin") {
2964
+ const why = await probeKeychain(service);
2965
+ if (why === null) return new KeychainStore(catalog, service);
2966
+ return why;
2967
+ }
2968
+ if (want("dpapi") && platform() === "win32") {
2969
+ const { shell, reason } = await probeDpapi();
2970
+ if (shell) return new DpapiStore(dpapiStorePath(homeDir), shell);
2971
+ return reason ?? "DPAPI \u4E0D\u53EF\u7528";
2972
+ }
2973
+ if (want("libsecret") && platform() !== "darwin" && platform() !== "win32") {
2974
+ const why = await probeLibsecret(service);
2975
+ if (why === null) return new LibsecretStore(catalog, service);
2976
+ return why;
2977
+ }
2978
+ return forced === null ? `\u672C\u5E73\u53F0\uFF08${platform()}\uFF09\u6CA1\u6709\u53EF\u7528\u7684\u51ED\u636E\u540E\u7AEF` : `${BACKEND_ENV_VAR}=${forced} \u5728\u672C\u5E73\u53F0\uFF08${platform()}\uFF09\u7528\u4E0D\u4E86`;
2979
+ }
2980
+ function readForcedBackend() {
2981
+ const raw = process.env[BACKEND_ENV_VAR]?.trim().toLowerCase();
2982
+ if (!raw || raw === "auto") return null;
2983
+ const known = ["keychain", "dpapi", "libsecret", "plaintext"];
2984
+ return known.find((k) => k === raw) ?? null;
2985
+ }
2986
+ async function prefetchProviderSecrets(store, names) {
2987
+ let stored;
2988
+ try {
2989
+ stored = await store.list();
2990
+ } catch (err) {
2991
+ return { values: {}, warning: `\u5217\u4E3E\u51ED\u636E\u5931\u8D25\uFF08${message2(err)}\uFF09` };
2992
+ }
2993
+ const wanted = stored.filter((n) => names.includes(n));
2994
+ const values2 = {};
2995
+ const missing = [];
2996
+ for (const name of wanted) {
2997
+ try {
2998
+ const value = await store.get(name);
2999
+ if (value) values2[name] = value;
3000
+ else missing.push(name);
3001
+ } catch (err) {
3002
+ return { values: values2, warning: `\u8BFB\u53D6 ${name} \u5931\u8D25\uFF08${message2(err)}\uFF09` };
3003
+ }
3004
+ }
3005
+ if (missing.length > 0) {
3006
+ try {
3007
+ store.healCatalog?.(stored.filter((n) => !missing.includes(n)));
3008
+ } catch {
3009
+ }
3010
+ }
3011
+ return { values: values2 };
3012
+ }
3013
+ function listStoredSecretNames(homeDir = resolveHomeDir()) {
3014
+ const names = new Set(new SecretCatalog(secretIndexPath(homeDir)).read());
3015
+ try {
3016
+ const raw = JSON.parse(readFileSync(dpapiStorePath(homeDir), "utf-8"));
3017
+ const secrets = raw.secrets;
3018
+ if (secrets) for (const k of Object.keys(secrets)) names.add(k);
3019
+ } catch {
3020
+ }
3021
+ return [...names].sort();
3022
+ }
3023
+ var current = null;
3024
+ function setSecretStore(store) {
3025
+ current = store;
3026
+ }
3027
+ function getSecretStore() {
3028
+ return current;
3029
+ }
3030
+ var values = {};
3031
+ function setSecretValues(next) {
3032
+ values = { ...next };
3033
+ }
3034
+ function getSecretValues() {
3035
+ return values;
3036
+ }
3037
+ function resetSecretState() {
3038
+ current = null;
3039
+ values = {};
3040
+ }
3041
+ function message2(err) {
3042
+ return err instanceof Error ? err.message : String(err);
3043
+ }
3044
+ /**
3045
+ * @license
3046
+ * Copyright 2025 Google LLC
3047
+ * SPDX-License-Identifier: Apache-2.0
3048
+ *
3049
+ * 衍生自 gemini-cli `packages/core/src/utils/process-utils.ts` 的
3050
+ * `killProcessGroup()`。已做的修改:
3051
+ * - 增加 `detached` 开关且**默认 false**:只有调用方明确声明子进程是
3052
+ * `detached: true` 起的,才允许 `process.kill(-pid)`。原版无条件杀进程组,
3053
+ * 对非 detached 的子进程等于杀自己(见下方「为什么默认不杀进程组」)。
3054
+ * - `platform` 改成可注入入参,让 Windows 分支在 Mac 上也能被用例覆盖到。
3055
+ * - 入口挡死 `pid <= 1`(原版没挡,`kill(-0)` / `kill(-1)` 的后果见下)。
3056
+ * - 后代收集从递归 DFS 改成带 `seen` 去重的 BFS,并加深度 / 数量上限,
3057
+ * 且拆成独立导出的 `collectProcessTree()` —— 「先优雅关闭、关完再清扫」
3058
+ * 的场景必须能把收集和下手分成两步(见 `killPids`)。
3059
+ * - 不引 gemini-cli 的 `spawnAsync`(它挂着沙箱管理器依赖,跟 infra 的
3060
+ * 零内部依赖定位冲突),改用 `execFile` 的手写 promise 包装。
3061
+ * - Windows 的进程树采集(`readWindowsSnapshot` / `buildChildrenMap` 及其两个
3062
+ * 解析器)是**原创**,gemini-cli 没有对应实现 —— 它在 Windows 上只有
3063
+ * `taskkill /t` 一条路。
3064
+ * Modifications Copyright 2024-2026 bowen.
3065
+ */
3066
+ /**
3067
+ * 剥掉 `bash -c "真正的命令"` 这类外壳。
3068
+ *
3069
+ * 不剥的话 `bash -c "rm -rf /"` 的 root 是 `bash`,危险表一条都命不中。
3070
+ *
3071
+ * @license Apache-2.0 (adapted from google-gemini/gemini-cli shell-utils.ts stripShellWrapper)
3072
+ * Copyright 2026 Google LLC
3073
+ * SPDX-License-Identifier: Apache-2.0
3074
+ * Modifications Copyright 2024-2026 bowen
3075
+ */
3076
+ /**
3077
+ * 引号感知地判断有没有命令替换。
3078
+ *
3079
+ * 逐字符走一遍状态机而不是打正则:`echo '$(not a substitution)'` 里那对括号
3080
+ * 在单引号内,bash 不会执行它,正则区分不了。
3081
+ *
3082
+ * @license Apache-2.0 (adapted from google-gemini/gemini-cli shell-utils.ts detectBashSubstitution)
3083
+ * Copyright 2026 Google LLC
3084
+ * SPDX-License-Identifier: Apache-2.0
3085
+ * Modifications Copyright 2024-2026 bowen
3086
+ */
3087
+ /**
3088
+ * PowerShell 的子表达式:`$( )` / `@( )` / 裸 `( )`。
3089
+ *
3090
+ * 裸括号那一条要排掉控制流关键字(`if (...)`、`foreach (...)`),
3091
+ * 否则任何带条件的脚本都会被判成「有替换」。
3092
+ *
3093
+ * @license Apache-2.0 (adapted from google-gemini/gemini-cli shell-utils.ts detectPowerShellSubstitution)
3094
+ * Copyright 2026 Google LLC
3095
+ * SPDX-License-Identifier: Apache-2.0
3096
+ * Modifications Copyright 2024-2026 bowen
3097
+ */
3098
+
3099
+ export { CODE_EXEC_ROOTS, DEFAULT_LANG, DEFAULT_YAML, DENIAL_SIGNATURES, ENVELOPE_PREFIX, EXEC_PATH_AS_NODE_ENV, IS_WINDOWS, LANGS, PROJECT_DIR_NAME, PlaintextStore, RIPGREP_INSTALL_HINT, RUNNER_FAILURE_RULES, SECRET_MODE, SECRET_SERVICE, SHELL_KINDS, SIGKILL_TIMEOUT_MS, SecretCatalog, TOOLCHAIN_CACHE_DIRS, WINDOWS_HIDE_FLAGS, agentsDir, approvalsPath, artifactsDir, assertValidSecretName, automationDir, automationLogsDir, automationWorkDir, budgetStatePath, buildBwrapArgs, buildProfile, checkDangerousCommand, checkObfuscation, checkpointsDir, classifyFailure, closeAllDatabases, collectProcessTree, commandsDir, configPath, confine, countCodePoints, createLogger, createSecretStore, createStreamDecoder, currentLang, dbPath, detectBackend, detectConsoleEncoding, encodingForCodePage, envPath, formatIssues, generateDataKey, getDataKey, getDefaultShell, getPythonCommand, getSecretStore, getSecretValues, hasSideEffectChannel, headCodePoints, hooksPath, i18nDiagnostics, isEnvelope, isInWorkspace, isLang, isReadOnlyCommand, isSensitiveKey, isolate, issueDetails, keepHeadAndTail, keybindingsPath, killAllTrackedProcesses, killPids, killProcessTree, killTrackedProcess, listStoredSecretNames, loadCatalog, localesDir, managedSettingsPath, marketplacesPath, maskApiKey, maskSensitive, mcpAuthPath, mcpConfigPath, mcpSchemaCachePath, memoriesDir, migrate, migrateEnvSecrets, normalizeForMatch, openArtifact, openDatabase, openEnvelope, parseDataKey, parseEnvText, parseLenient, parseShellCommand, parseStrict, placeholdersOf, pluginsDir, pluginsStatePath, policiesDir, powershellCommandArg, prefetchProviderSecrets, probeBubblewrap, probeSeatbelt, probeShell, projectCommandsDir, projectHooksPath, projectLocalSettingsPath, projectPoliciesDir, projectSchemasDir, projectSettingsPath, projectSkillsDir, readConfigYaml, readKey, refCount, releaseDatabase, removeEnvVarText, resetI18n, resetRipgrepCache, resetSecretState, resolveHomeDir, resolveLang, resolveProfile, resolveRipgrep, resolveShellKind, safeInit, sandboxModeForLevel, schemaVersion, sealEnvelope, setDataKey, setEnvVarText, setLang, setLogLevel, setScalar, setSecretStore, setSecretValues, setSectionField, setShell, shellPtyArgs, shellSpawnArgs, skillsDir, splitWords, startLongLivedProcess, stripShellWrapper, systemLocaleSignals, t, tailCodePoints, trackedProcessCount, trustPath, trustedImportsPath, unknownKeyIssues, unsetScalar, unsetSectionField, withTimeout, workspacesPath, worktreesDir, writeArtifact, writeConfigYaml, writeEnvFile };