@epoch-agent/infra 0.1.0 → 0.2.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 CHANGED
@@ -1,12 +1,12 @@
1
1
  import { platform, homedir, tmpdir } from 'os';
2
- import { join, dirname, delimiter, resolve as resolve$1, isAbsolute, basename, win32, posix, relative } from 'path';
2
+ import { join, dirname, basename, delimiter, resolve as resolve$1, isAbsolute, win32, posix, relative } from 'path';
3
3
  import { existsSync, readFileSync, writeFileSync, mkdirSync, openSync, closeSync, writeSync, renameSync, rmSync, chmodSync, lstatSync, realpathSync, accessSync, constants } from 'fs';
4
4
  import Database from 'better-sqlite3';
5
5
  import { execFileSync, execFile, spawn } from 'child_process';
6
6
  import { TextDecoder } from 'util';
7
- import { createRequire } from 'module';
8
7
  import { fileURLToPath } from 'url';
9
8
  import { load } from 'js-yaml';
9
+ import { createRequire } from 'module';
10
10
  import { randomBytes, createCipheriv, createDecipheriv, createHash } from 'crypto';
11
11
 
12
12
  // src/paths.ts
@@ -132,6 +132,16 @@ function projectHooksPath(projectRoot) {
132
132
  function projectPoliciesDir(projectRoot) {
133
133
  return join(projectRoot, PROJECT_DIR_NAME, "policies");
134
134
  }
135
+ var CLAUDE_DIR_NAME = ".claude";
136
+ function claudeUserSettingsPath(home = homedir()) {
137
+ return join(home, CLAUDE_DIR_NAME, "settings.json");
138
+ }
139
+ function projectClaudeSettingsPath(projectRoot) {
140
+ return join(projectRoot, CLAUDE_DIR_NAME, "settings.json");
141
+ }
142
+ function projectClaudeLocalSettingsPath(projectRoot) {
143
+ return join(projectRoot, CLAUDE_DIR_NAME, "settings.local.json");
144
+ }
135
145
  function managedSettingsPath(os = platform()) {
136
146
  const FILE = "managed-settings.json";
137
147
  if (os === "win32") {
@@ -269,7 +279,6 @@ function writeArtifact(dir, name, extension, data, onExisting = "unique") {
269
279
  handle.close();
270
280
  return handle.path;
271
281
  }
272
-
273
282
  // src/text-slice.ts
274
283
  function isHighSurrogate(code) {
275
284
  return code >= 55296 && code <= 56319;
@@ -459,7 +468,6 @@ function schemaVersion(db, namespace) {
459
468
  const row = db.prepare("SELECT MAX(version) AS v FROM _epoch_migrations WHERE namespace = ?").get(namespace);
460
469
  return row?.v ?? 0;
461
470
  }
462
-
463
471
  // src/mask.ts
464
472
  function maskApiKey(key) {
465
473
  if (!key || key.length < 8) return "***";
@@ -491,7 +499,6 @@ function maskSensitive(data) {
491
499
  }
492
500
  return masked;
493
501
  }
494
-
495
502
  // src/logger.ts
496
503
  var currentLevel = "info";
497
504
  function setLogLevel(level) {
@@ -529,6 +536,177 @@ function createLogger(module) {
529
536
  }
530
537
  };
531
538
  }
539
+ var LANGS = ["zh", "en"];
540
+ var DEFAULT_LANG = "zh";
541
+ function isLang(value) {
542
+ return LANGS.includes(value);
543
+ }
544
+ var catalogs = /* @__PURE__ */ new Map();
545
+ var pending = [];
546
+ var localesDirCache;
547
+ var currentLangValue;
548
+ function localesDir() {
549
+ if (localesDirCache !== void 0) return localesDirCache ?? void 0;
550
+ const override = process.env.EPOCH_LOCALES_DIR;
551
+ if (override) {
552
+ localesDirCache = existsSync(override) ? override : null;
553
+ return localesDirCache ?? void 0;
554
+ }
555
+ let dir = dirname(fileURLToPath(import.meta.url));
556
+ for (let depth = 0; depth < 8; depth += 1) {
557
+ if (basename(dir) === "node_modules") break;
558
+ const candidate = join(dir, "locales");
559
+ if (existsSync(join(candidate, `${DEFAULT_LANG}.yaml`))) {
560
+ localesDirCache = candidate;
561
+ return candidate;
562
+ }
563
+ const parent = dirname(dir);
564
+ if (parent === dir) break;
565
+ dir = parent;
566
+ }
567
+ localesDirCache = null;
568
+ return void 0;
569
+ }
570
+ function flatten(value, prefix, out) {
571
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
572
+ for (const [key, child] of Object.entries(value)) {
573
+ const path = prefix ? `${prefix}.${key}` : key;
574
+ if (typeof child === "string") out[path] = child;
575
+ else flatten(child, path, out);
576
+ }
577
+ }
578
+ function fallbackDetail(key, vars) {
579
+ const text = t(key, vars);
580
+ if (text !== key) return text;
581
+ switch (key) {
582
+ case "i18n.locales_missing":
583
+ return "locales directory not found; UI text will show as key paths";
584
+ case "i18n.catalog_missing":
585
+ return `locales/${vars["file"]} is missing; ${vars["lang"]} text falls back to the default language`;
586
+ case "i18n.catalog_invalid":
587
+ return `locales/${vars["file"]} is not valid YAML (${vars["reason"]}); fell back to the built-in default`;
588
+ default:
589
+ return key;
590
+ }
591
+ }
592
+ function loadCatalog(lang) {
593
+ const cached2 = catalogs.get(lang);
594
+ if (cached2) return cached2;
595
+ const result = readCatalog(lang);
596
+ catalogs.set(lang, result);
597
+ return result;
598
+ }
599
+ function readCatalog(lang) {
600
+ const dir = localesDir();
601
+ if (!dir) {
602
+ pending.push({
603
+ code: "locales-missing",
604
+ render: () => fallbackDetail("i18n.locales_missing", {})
605
+ });
606
+ return { entries: {}, error: "\u627E\u4E0D\u5230 locales \u76EE\u5F55" };
607
+ }
608
+ const file = `${lang}.yaml`;
609
+ const path = join(dir, file);
610
+ if (!existsSync(path)) {
611
+ pending.push({
612
+ code: "catalog-missing",
613
+ render: () => fallbackDetail("i18n.catalog_missing", { file, lang })
614
+ });
615
+ return { entries: {}, error: `${path} \u4E0D\u5B58\u5728` };
616
+ }
617
+ let parsed;
618
+ try {
619
+ parsed = load(readFileSync(path, "utf-8"));
620
+ } catch (err) {
621
+ const reason = (err instanceof Error ? err.message : String(err)).split("\n")[0] ?? "";
622
+ pending.push({
623
+ code: "catalog-invalid",
624
+ render: () => fallbackDetail("i18n.catalog_invalid", { file, reason })
625
+ });
626
+ return { entries: {}, error: reason };
627
+ }
628
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
629
+ const reason = "\u9876\u5C42\u4E0D\u662F\u952E\u503C\u5BF9";
630
+ pending.push({
631
+ code: "catalog-invalid",
632
+ render: () => fallbackDetail("i18n.catalog_invalid", { file, reason })
633
+ });
634
+ return { entries: {}, error: reason };
635
+ }
636
+ const entries = {};
637
+ flatten(parsed, "", entries);
638
+ return { entries };
639
+ }
640
+ function resetI18n() {
641
+ catalogs.clear();
642
+ pending = [];
643
+ localesDirCache = void 0;
644
+ currentLangValue = void 0;
645
+ }
646
+ var PLACEHOLDER = /\{(\w+)\}/g;
647
+ function placeholdersOf(text) {
648
+ const found = [];
649
+ for (const match of text.matchAll(PLACEHOLDER)) {
650
+ const name = match[1];
651
+ if (name && !found.includes(name)) found.push(name);
652
+ }
653
+ return found;
654
+ }
655
+ function t(key, vars, lang) {
656
+ const target = lang ?? currentLang();
657
+ const text = loadCatalog(target).entries[key] ?? loadCatalog(DEFAULT_LANG).entries[key] ?? key;
658
+ if (!vars) return text;
659
+ return text.replace(PLACEHOLDER, (whole, name) => {
660
+ const value = vars[name];
661
+ return value === void 0 ? whole : String(value);
662
+ });
663
+ }
664
+ function currentLang() {
665
+ currentLangValue ??= resolveLang();
666
+ return currentLangValue;
667
+ }
668
+ function setLang(lang) {
669
+ currentLangValue = lang;
670
+ }
671
+ function uiDateLocale() {
672
+ switch (currentLang()) {
673
+ case "zh":
674
+ return "zh-CN";
675
+ case "en":
676
+ return "en-US";
677
+ }
678
+ }
679
+ function systemLocaleSignals() {
680
+ const env = process.env;
681
+ const signals = [env.LC_ALL, env.LC_MESSAGES, env.LANG, env.LANGUAGE].filter(
682
+ (v) => typeof v === "string" && v.length > 0
683
+ );
684
+ try {
685
+ signals.push(new Intl.DateTimeFormat().resolvedOptions().locale);
686
+ } catch {
687
+ }
688
+ return signals;
689
+ }
690
+ function langFromLocale(tag) {
691
+ const primary = tag.split(/[.@]/)[0]?.replace("_", "-").toLowerCase() ?? "";
692
+ if (!primary || primary === "c" || primary === "posix") return void 0;
693
+ if (primary === "zh" || primary.startsWith("zh-")) return "zh";
694
+ return "en";
695
+ }
696
+ function resolveLang(configured, signals) {
697
+ const fromEnv = process.env.EPOCH_LANGUAGE;
698
+ if (fromEnv && isLang(fromEnv)) return fromEnv;
699
+ if (configured && isLang(configured)) return configured;
700
+ for (const tag of signals ?? systemLocaleSignals()) {
701
+ const lang = langFromLocale(tag);
702
+ if (lang) return lang;
703
+ }
704
+ return DEFAULT_LANG;
705
+ }
706
+ function i18nDiagnostics() {
707
+ return pending.map((item) => ({ code: item.code, detail: item.render() }));
708
+ }
709
+ // src/platform.ts
532
710
  var IS_WINDOWS = platform() === "win32";
533
711
  var WINDOWS_HIDE_FLAGS = IS_WINDOWS ? { windowsHide: true } : {};
534
712
  function getPythonCommand() {
@@ -538,9 +716,7 @@ function getPythonCommand() {
538
716
  var SHELL_KINDS = ["cmd", "powershell", "pwsh"];
539
717
  var SHELL_EXECUTABLES = {
540
718
  cmd: "cmd.exe",
541
- // Windows PowerShell 5.1,随系统自带
542
719
  powershell: "powershell.exe",
543
- // PowerShell 7+,要自己装
544
720
  pwsh: "pwsh.exe"
545
721
  };
546
722
  var configuredShell = null;
@@ -563,7 +739,7 @@ function probeShell(kind) {
563
739
  for (const dir of dirs) {
564
740
  if (existsSync(join(dir, exe))) return null;
565
741
  }
566
- return `shell: \u914D\u7F6E\u7684 ${kind}\uFF08${exe}\uFF09\u4E0D\u5728 PATH \u4E0A\uFF0C\u5DF2\u56DE\u9000\u5230 cmd.exe`;
742
+ return t("infra_misc.shell_not_on_path", { kind, exe });
567
743
  }
568
744
  var PS_EXIT_TRAILER = [
569
745
  "$__epochOk = $?",
@@ -605,15 +781,10 @@ function encodingForCodePage(codePage) {
605
781
  const table = {
606
782
  65001: "utf-8",
607
783
  936: "gbk",
608
- // 简体中文
609
784
  950: "big5",
610
- // 繁体中文
611
785
  932: "shift_jis",
612
- // 日文
613
786
  949: "euc-kr",
614
- // 韩文
615
787
  866: "ibm866",
616
- // 西里尔 OEM
617
788
  874: "windows-874",
618
789
  1250: "windows-1250",
619
790
  1251: "windows-1251",
@@ -695,7 +866,10 @@ function createStreamDecoder(fallbackEncoding = detectConsoleEncoding()) {
695
866
  }
696
867
  };
697
868
  }
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";
869
+ function ripgrepInstallHint() {
870
+ if (IS_WINDOWS) return t("infra_misc.ripgrep_install_windows");
871
+ return process.platform === "darwin" ? "brew install ripgrep" : "apt install ripgrep / dnf install ripgrep / pacman -S ripgrep";
872
+ }
699
873
  function ensureExecutable(path) {
700
874
  try {
701
875
  accessSync(path, constants.X_OK);
@@ -777,7 +951,7 @@ function resolve(options) {
777
951
  }
778
952
  }
779
953
  }
780
- return { mode: "missing", command: "rg", hint: RIPGREP_INSTALL_HINT };
954
+ return { mode: "missing", command: "rg", hint: ripgrepInstallHint() };
781
955
  }
782
956
  var cached = null;
783
957
  function resolveRipgrep(options) {
@@ -812,24 +986,55 @@ async function killProcessTree(options) {
812
986
  const { pid, pty, detached = false, platform: platform10 = platform() } = options;
813
987
  if (!isRealChildPid(pid)) return;
814
988
  if (platform10 === "win32") {
815
- const descendants2 = await collectProcessTree(pid, platform10);
989
+ const snapshot = await windowsSnapshot();
990
+ if (snapshot.pids.size > 0 && !snapshot.pids.has(pid)) {
991
+ tryPtyKill(pty);
992
+ return;
993
+ }
994
+ const descendants2 = walkTree(pid, (parent) => snapshot.childrenOf.get(parent) ?? []);
995
+ await killPids(descendants2, { signal: "SIGKILL" });
996
+ const swept = isAlive(pid) ? await runProbe("taskkill", ["/pid", String(pid), "/f", "/t"]) : "";
816
997
  tryPtyKill(pty);
817
- await killPids([...descendants2, pid], { signal: "SIGKILL" });
818
- await runProbe("taskkill", ["/pid", String(pid), "/f", "/t"]);
998
+ if (!swept && isAlive(pid)) await killPids([pid], { signal: "SIGKILL" });
819
999
  return;
820
1000
  }
821
1001
  const descendants = await collectProcessTree(pid, platform10);
822
1002
  await killPids([...descendants, pid], options, {
823
- // 只有 detached 才允许碰 -pid —— 这一行就是文件头那条约束的落点
824
1003
  ...detached ? { groupLeaderPid: pid } : {},
825
1004
  ...pty ? { pty } : {}
826
1005
  });
827
1006
  }
1007
+ async function collectProcessTreeStamped(rootPid, platform10 = platform()) {
1008
+ if (!isRealChildPid(rootPid)) return [];
1009
+ if (platform10 !== "win32") {
1010
+ const pids = await collectProcessTree(rootPid, platform10);
1011
+ return pids.map((pid) => ({ pid, born: "" }));
1012
+ }
1013
+ const snapshot = await windowsSnapshot();
1014
+ return walkTree(rootPid, (parent) => snapshot.childrenOf.get(parent) ?? []).map((pid) => ({
1015
+ pid,
1016
+ born: snapshot.bornOf.get(pid) ?? ""
1017
+ }));
1018
+ }
1019
+ async function killStampedPids(stamped, options = {}, platform10 = platform()) {
1020
+ if (stamped.length === 0) return;
1021
+ if (platform10 !== "win32") {
1022
+ await killPids(
1023
+ stamped.map((s) => s.pid),
1024
+ options
1025
+ );
1026
+ return;
1027
+ }
1028
+ const snapshot = await windowsSnapshot();
1029
+ const targets = snapshot.pids.size === 0 ? stamped.map((s) => s.pid) : stamped.filter((s) => s.born === "" || snapshot.bornOf.get(s.pid) === s.born).map((s) => s.pid);
1030
+ if (targets.length > 0) await killPids(targets, options);
1031
+ }
828
1032
  async function killPids(pids, options = {}, extra = {}) {
829
1033
  const { escalate = false, isExited = () => false, killTimeoutMs = SIGKILL_TIMEOUT_MS } = options;
830
1034
  const targets = pids.filter(isRealChildPid);
831
1035
  const first = options.signal ?? (escalate ? "SIGTERM" : "SIGKILL");
832
1036
  sweep(targets, first, extra);
1037
+ tryPtyKill(extra.pty, first);
833
1038
  if (!escalate || isExited()) return;
834
1039
  await delay(killTimeoutMs);
835
1040
  if (isExited()) return;
@@ -838,7 +1043,14 @@ async function killPids(pids, options = {}, extra = {}) {
838
1043
  function sweep(targets, signal, extra) {
839
1044
  if (extra.groupLeaderPid !== void 0) sendSignal(-extra.groupLeaderPid, signal);
840
1045
  for (const target of targets) sendSignal(target, signal);
841
- tryPtyKill(extra.pty, signal);
1046
+ }
1047
+ function isAlive(pid) {
1048
+ try {
1049
+ process.kill(pid, 0);
1050
+ return true;
1051
+ } catch (err) {
1052
+ return err.code === "EPERM";
1053
+ }
842
1054
  }
843
1055
  function sendSignal(target, signal) {
844
1056
  try {
@@ -855,14 +1067,35 @@ function tryPtyKill(pty, signal) {
855
1067
  }
856
1068
  async function collectProcessTree(rootPid, platform10 = platform()) {
857
1069
  if (!isRealChildPid(rootPid)) return [];
858
- const listChildren = platform10 === "win32" ? await snapshotLister() : listChildrenViaPgrep;
1070
+ if (platform10 === "win32") {
1071
+ const snapshot = await windowsSnapshot();
1072
+ return walkTree(rootPid, (parent) => snapshot.childrenOf.get(parent) ?? []);
1073
+ }
1074
+ const found = [];
1075
+ const seen = /* @__PURE__ */ new Set([rootPid]);
1076
+ let frontier = [rootPid];
1077
+ for (let depth = 0; depth < MAX_TREE_DEPTH && frontier.length > 0; depth++) {
1078
+ const next = [];
1079
+ for (const parent of frontier) {
1080
+ for (const child of await listChildrenViaPgrep(parent)) {
1081
+ if (seen.has(child) || found.length >= MAX_TREE_SIZE) continue;
1082
+ seen.add(child);
1083
+ found.push(child);
1084
+ next.push(child);
1085
+ }
1086
+ }
1087
+ frontier = next;
1088
+ }
1089
+ return found.reverse();
1090
+ }
1091
+ function walkTree(rootPid, childrenOf) {
859
1092
  const found = [];
860
1093
  const seen = /* @__PURE__ */ new Set([rootPid]);
861
1094
  let frontier = [rootPid];
862
1095
  for (let depth = 0; depth < MAX_TREE_DEPTH && frontier.length > 0; depth++) {
863
1096
  const next = [];
864
1097
  for (const parent of frontier) {
865
- for (const child of await listChildren(parent)) {
1098
+ for (const child of childrenOf(parent)) {
866
1099
  if (seen.has(child) || found.length >= MAX_TREE_SIZE) continue;
867
1100
  seen.add(child);
868
1101
  found.push(child);
@@ -877,9 +1110,13 @@ var listChildrenViaPgrep = async (parentPid) => {
877
1110
  const stdout = await runProbe("pgrep", ["-P", String(parentPid)]);
878
1111
  return stdout.split("\n").map((line) => Number.parseInt(line.trim(), 10)).filter(isRealChildPid);
879
1112
  };
880
- async function snapshotLister() {
881
- const childrenOf = buildChildrenMap(await readWindowsSnapshot());
882
- return (parentPid) => Promise.resolve(childrenOf.get(parentPid) ?? []);
1113
+ async function windowsSnapshot() {
1114
+ const records = await readWindowsSnapshot();
1115
+ return {
1116
+ pids: new Set(records.map((record) => record.pid)),
1117
+ childrenOf: buildChildrenMap(records),
1118
+ bornOf: new Map(records.map((record) => [record.pid, record.born]))
1119
+ };
883
1120
  }
884
1121
  async function readWindowsSnapshot() {
885
1122
  const fast = parseWmicCsv(await runProbe("wmic", WMIC_SNAPSHOT_ARGS, SNAPSHOT_MAX_BUFFER));
@@ -917,7 +1154,6 @@ function parseWmicCsv(stdout) {
917
1154
  return {
918
1155
  pid: Number.parseInt(cells[atPid] ?? "", 10),
919
1156
  ppid: Number.parseInt(cells[atPpid] ?? "", 10),
920
- // `20260807071417.130820+480` → 留到秒的那 14 位就够比大小了
921
1157
  born: normalizeBorn((cells[atBorn] ?? "").slice(0, 14))
922
1158
  };
923
1159
  });
@@ -958,7 +1194,6 @@ function runProbe(file, args2, maxBuffer) {
958
1194
  function delay(ms) {
959
1195
  return new Promise((resolve5) => setTimeout(resolve5, ms));
960
1196
  }
961
-
962
1197
  // src/shell-parse.ts
963
1198
  function defaultFlavor() {
964
1199
  return IS_WINDOWS ? "windows" : "posix";
@@ -1111,7 +1346,7 @@ function parseShellCommand(command, flavor) {
1111
1346
  const hasSubstitution = kind === "windows" ? detectWindowsSubstitution(stripped) : detectPosixSubstitution(stripped);
1112
1347
  const tokens = tokenize(stripped, kind);
1113
1348
  if (tokens === null) {
1114
- return { segments: [], hasSubstitution, parseError: "\u5F15\u53F7\u4E0D\u95ED\u5408", stripped };
1349
+ return { segments: [], hasSubstitution, parseError: t("infra_misc.unclosed_quote"), stripped };
1115
1350
  }
1116
1351
  const segments = [];
1117
1352
  let words = [];
@@ -1156,7 +1391,6 @@ function hasSideEffectChannel(parsed) {
1156
1391
  if (parsed.segments.length > 1) return true;
1157
1392
  return parsed.segments.some((s) => s.redirects.length > 0);
1158
1393
  }
1159
-
1160
1394
  // src/command-safety.ts
1161
1395
  function rootMatches(rule, root) {
1162
1396
  if (typeof rule === "string") return root === rule;
@@ -1206,7 +1440,6 @@ function inlineCodeExecOf(seg) {
1206
1440
  return args(seg).some((a) => entry.flags.includes(a)) ? seg.root : null;
1207
1441
  }
1208
1442
  var CODE_EXEC_ROOTS = [
1209
- // 解释器
1210
1443
  "python",
1211
1444
  "python2",
1212
1445
  "python3",
@@ -1218,25 +1451,21 @@ var CODE_EXEC_ROOTS = [
1218
1451
  "perl",
1219
1452
  "php",
1220
1453
  "lua",
1221
- // 包运行器 —— 后面跟什么都能跑
1222
1454
  "npx",
1223
1455
  "bunx",
1224
1456
  "npm run",
1225
1457
  "yarn run",
1226
1458
  "pnpm run",
1227
1459
  "bun run",
1228
- // shell 与远程执行
1229
1460
  ...SHELLS,
1230
1461
  "ssh",
1231
1462
  "eval",
1232
1463
  "exec",
1233
1464
  "env",
1234
1465
  "xargs",
1235
- // 提权
1236
1466
  "sudo",
1237
1467
  "doas",
1238
1468
  "su",
1239
- // PowerShell
1240
1469
  "powershell",
1241
1470
  "pwsh"
1242
1471
  ];
@@ -1279,69 +1508,70 @@ var ENCODED_COMMAND_RULE = {
1279
1508
  const got = m[1].toLowerCase();
1280
1509
  return got.length > 0 && got.startsWith("e") && "encodedcommand".startsWith(got);
1281
1510
  }),
1282
- desc: "PowerShell \u7F16\u7801\u547D\u4EE4\uFF08-EncodedCommand\uFF09\uFF0C\u5185\u5BB9\u4E0D\u53EF\u8BFB"
1511
+ desc: () => t("danger.ps_encoded_command")
1283
1512
  };
1284
1513
  var POSIX_RULES = [
1285
1514
  {
1286
1515
  root: "rm",
1287
1516
  when: (s) => hasRecursiveForce(s) && args(s).some(isDangerousRmTarget),
1288
- desc: "\u9012\u5F52\u5220\u9664\u6839\u76EE\u5F55\u6216\u7CFB\u7EDF\u76EE\u5F55"
1517
+ desc: () => t("danger.rm_system_root")
1289
1518
  },
1290
1519
  {
1291
1520
  root: "rm",
1292
1521
  when: (s) => hasRecursiveForce(s) && args(s).some(isEmptyVarSlash),
1293
- desc: "rm -rf $VAR/ \u2014\u2014 \u53D8\u91CF\u4E3A\u7A7A\u65F6\u7B49\u4E8E\u5220\u6839"
1522
+ desc: () => t("danger.rm_empty_var")
1294
1523
  },
1295
1524
  {
1296
- // `find / -delete` / `find / -exec rm` —— 老实现整条漏了
1297
1525
  root: "find",
1298
1526
  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"
1527
+ desc: () => t("danger.find_delete_system")
1300
1528
  },
1301
- { root: /^mkfs(\.\w+)?$/, desc: "\u683C\u5F0F\u5316\u6587\u4EF6\u7CFB\u7EDF" },
1529
+ { root: /^mkfs(\.\w+)?$/, desc: () => t("danger.mkfs") },
1302
1530
  {
1303
1531
  root: "dd",
1304
1532
  when: (s) => args(s).some((a) => a.startsWith("of=/dev/")),
1305
- desc: "\u76F4\u63A5\u5199\u88F8\u8BBE\u5907"
1533
+ desc: () => t("danger.dd_raw_device")
1534
+ },
1535
+ {
1536
+ root: "dd",
1537
+ when: (s) => args(s).some((a) => a.startsWith("if=")),
1538
+ desc: () => t("danger.dd_disk_io")
1306
1539
  },
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" },
1540
+ { root: "shred", desc: () => t("danger.shred") },
1541
+ { root: ["fdisk", "parted"], desc: () => t("danger.partition") },
1310
1542
  {
1311
1543
  root: "diskutil",
1312
1544
  when: (s) => (args(s)[0] ?? "").toLowerCase().startsWith("erase"),
1313
- desc: "\u78C1\u76D8\u5206\u533A\u64CD\u4F5C"
1545
+ desc: () => t("danger.partition")
1314
1546
  },
1315
- { root: "chmod", when: (s) => args(s).includes("777"), desc: "\u5168\u6743\u9650\u5F00\u653E" },
1547
+ { root: "chmod", when: (s) => args(s).includes("777"), desc: () => t("danger.chmod_777") },
1316
1548
  {
1317
1549
  root: "chown",
1318
1550
  when: (s) => args(s).some((a) => a === "root" || a.startsWith("root:")),
1319
- desc: "\u6539\u5F52\u5C5E\u4E3A root"
1551
+ desc: () => t("danger.chown_root")
1320
1552
  },
1321
1553
  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" },
1554
+ { root: ["sudo", "doas"], desc: () => t("danger.privilege_posix") },
1555
+ { root: "su", when: (s) => args(s).includes("-"), desc: () => t("danger.privilege_posix") },
1324
1556
  {
1325
- // 下载后直接执行:上一段是下载工具,这一段是被管道喂进来的 shell
1326
1557
  root: SHELLS,
1327
1558
  when: (s, prev) => s.pipedInto && prev !== void 0 && FETCHERS.includes(prev.root),
1328
- desc: "\u4E0B\u8F7D\u540E\u76F4\u63A5\u6267\u884C\uFF08curl | sh\uFF09"
1559
+ desc: () => t("danger.curl_pipe_sh")
1329
1560
  },
1330
1561
  {
1331
1562
  root: INTERPRETERS,
1332
1563
  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"
1564
+ desc: () => t("danger.download_pipe_interpreter")
1334
1565
  },
1335
1566
  {
1336
- // 通用「管道进 shell」。放在上面两条之后,让来源明确的先给出更准确的描述
1337
1567
  root: SHELLS,
1338
1568
  when: (s) => s.pipedInto,
1339
- desc: "\u628A\u4EFB\u610F\u8F93\u51FA\u7BA1\u9053\u8FDB shell \u6267\u884C"
1569
+ desc: () => t("danger.pipe_to_shell")
1340
1570
  },
1341
1571
  {
1342
1572
  root: "git",
1343
1573
  when: (s) => args(s)[0] === "push" && args(s).some((a) => a === "--force" || a === "-f" || a === "+"),
1344
- desc: "\u5F3A\u5236\u63A8\u9001"
1574
+ desc: () => t("danger.force_push")
1345
1575
  },
1346
1576
  {
1347
1577
  root: "git",
@@ -1351,77 +1581,80 @@ var POSIX_RULES = [
1351
1581
  if (sub === "clean") return rest.some((a) => isShortCluster(a) && a.includes("f"));
1352
1582
  return false;
1353
1583
  },
1354
- desc: "\u4E22\u5F03\u672A\u63D0\u4EA4\u6539\u52A8"
1584
+ desc: () => t("danger.discard_changes")
1355
1585
  },
1356
- { root: ["shutdown", "reboot", "halt", "poweroff"], desc: "\u5173\u673A / \u91CD\u542F" },
1586
+ { root: ["shutdown", "reboot", "halt", "poweroff"], desc: () => t("danger.shutdown") },
1357
1587
  {
1358
1588
  root: ["kill", "killall"],
1359
1589
  when: (s) => args(s).includes("-9") && args(s).some((a) => a === "-1" || a === "1"),
1360
- desc: "\u6740\u6389\u5168\u90E8\u8FDB\u7A0B / init"
1590
+ desc: () => t("danger.kill_all")
1361
1591
  },
1362
1592
  {
1363
1593
  root: ["iptables", "ip6tables", "nft", "pfctl"],
1364
1594
  when: (s) => args(s).some((a) => a === "-F" || a === "--flush"),
1365
- desc: "\u6E05\u7A7A\u9632\u706B\u5899\u89C4\u5219"
1595
+ desc: () => t("danger.flush_firewall")
1366
1596
  },
1367
1597
  {
1368
1598
  root: ["launchctl", "systemctl"],
1369
1599
  when: (s) => ["disable", "unload"].includes(args(s)[0] ?? ""),
1370
- desc: "\u7981\u7528\u7CFB\u7EDF\u670D\u52A1"
1600
+ desc: () => t("danger.disable_service")
1371
1601
  },
1372
- { root: "history", when: (s) => args(s).includes("-c"), desc: "\u6E05\u9664\u547D\u4EE4\u5386\u53F2\uFF08\u63A9\u76D6\u75D5\u8FF9\uFF09" },
1602
+ { root: "history", when: (s) => args(s).includes("-c"), desc: () => t("danger.clear_history") },
1373
1603
  {
1374
1604
  root: "tee",
1375
1605
  when: (s) => args(s).some((a) => a.startsWith("/etc/")),
1376
- desc: "\u6539\u5199 /etc \u4E0B\u7684\u7CFB\u7EDF\u914D\u7F6E"
1606
+ desc: () => t("danger.write_etc")
1377
1607
  },
1378
1608
  {
1379
- // 重定向到裸设备 / 历史文件。这条不看 root,任何命令都可能这么写
1380
1609
  root: /.*/,
1381
1610
  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"
1611
+ desc: () => t("danger.write_device_or_history")
1383
1612
  }
1384
1613
  ];
1385
1614
  var WINDOWS_RULES = [
1386
1615
  {
1387
1616
  root: ["del", "erase", "rd", "rmdir"],
1388
1617
  when: hasDangerousWinTarget,
1389
- desc: "\u9012\u5F52\u5220\u9664\u76D8\u6839 / \u7CFB\u7EDF\u76EE\u5F55 / \u7528\u6237\u76EE\u5F55"
1618
+ desc: () => t("danger.win_rm_system")
1390
1619
  },
1391
1620
  {
1392
1621
  root: "remove-item",
1393
1622
  when: (s) => hasWinSwitch(s, "recurse") && hasDangerousWinTarget(s),
1394
- desc: "PowerShell \u9012\u5F52\u5220\u9664\u76D8\u6839 / \u7CFB\u7EDF\u76EE\u5F55"
1623
+ desc: () => t("danger.ps_rm_system")
1395
1624
  },
1396
1625
  {
1397
1626
  root: "format",
1398
1627
  when: (s) => args(s).some((a) => /^[a-z]:$/i.test(a)),
1399
- desc: "\u683C\u5F0F\u5316\u78C1\u76D8"
1628
+ desc: () => t("danger.win_format")
1400
1629
  },
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" },
1630
+ { root: "diskpart", desc: () => t("danger.partition") },
1631
+ { root: "cipher", when: (s) => hasWinSwitch(s, "w", true), desc: () => t("danger.cipher_wipe") },
1403
1632
  {
1404
1633
  root: "vssadmin",
1405
1634
  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"
1635
+ desc: () => t("danger.delete_shadow_copies")
1407
1636
  },
1408
1637
  {
1409
1638
  root: "wmic",
1410
1639
  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"
1640
+ desc: () => t("danger.delete_shadow_copies")
1412
1641
  },
1413
1642
  {
1414
1643
  root: "reg",
1415
1644
  when: (s) => (args(s)[0] ?? "").toLowerCase() === "delete" && hasWinSwitch(s, "f", true),
1416
- desc: "\u5F3A\u5236\u5220\u9664\u6CE8\u518C\u8868\u9879"
1645
+ desc: () => t("danger.reg_delete")
1417
1646
  },
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" },
1647
+ { root: "bcdedit", desc: () => t("danger.bcdedit") },
1648
+ {
1649
+ root: "sc",
1650
+ when: (s) => (args(s)[0] ?? "").toLowerCase() === "delete",
1651
+ desc: () => t("danger.sc_delete")
1652
+ },
1653
+ { root: "takeown", when: (s) => hasWinSwitch(s, "r", true), desc: () => t("danger.takeown") },
1421
1654
  {
1422
1655
  root: "icacls",
1423
1656
  when: (s) => hasWinSwitch(s, "grant") && args(s).some((a) => /^(everyone|users)(:|$)/i.test(a)),
1424
- desc: "\u7ED9 everyone / users \u6388\u6743"
1657
+ desc: () => t("danger.icacls_everyone")
1425
1658
  },
1426
1659
  {
1427
1660
  root: "netsh",
@@ -1429,75 +1662,82 @@ var WINDOWS_RULES = [
1429
1662
  const lower = args(s).map((a) => a.toLowerCase());
1430
1663
  return (lower[0] === "advfirewall" || lower[0] === "firewall") && lower.some((a) => a === "off" || a === "disable");
1431
1664
  },
1432
- desc: "\u5173\u95ED\u9632\u706B\u5899"
1665
+ desc: () => t("danger.disable_firewall")
1433
1666
  },
1434
1667
  {
1435
1668
  root: "set-mppreference",
1436
1669
  when: (s) => hasWinSwitch(s, "disablerealtimemonitoring"),
1437
- desc: "\u5173\u95ED Defender \u5B9E\u65F6\u9632\u62A4"
1670
+ desc: () => t("danger.disable_defender")
1438
1671
  },
1439
1672
  {
1440
1673
  root: "net",
1441
1674
  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"
1675
+ desc: () => t("danger.add_admin_user")
1443
1676
  },
1444
- { root: "runas", desc: "\u63D0\u6743\u6267\u884C\uFF08runas\uFF09" },
1677
+ { root: "runas", desc: () => t("danger.privilege_runas") },
1445
1678
  {
1446
1679
  root: "start-process",
1447
1680
  when: (s) => (winSwitchValue(s, "verb") ?? "").toLowerCase() === "runas",
1448
- desc: "\u63D0\u6743\u6267\u884C\uFF08Start-Process -Verb RunAs\uFF09"
1681
+ desc: () => t("danger.privilege_start_process")
1449
1682
  },
1450
1683
  {
1451
1684
  root: ["iex", "invoke-expression"],
1452
1685
  when: (s) => s.pipedInto,
1453
- desc: "\u4E0B\u8F7D\u540E\u76F4\u63A5\u6267\u884C\uFF08iwr | iex\uFF09"
1686
+ desc: () => t("danger.iwr_iex")
1454
1687
  },
1455
1688
  {
1456
1689
  root: ["powershell", "pwsh", "cmd"],
1457
1690
  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"
1691
+ desc: () => t("danger.download_pipe_shell")
1459
1692
  },
1460
1693
  {
1461
1694
  root: "certutil",
1462
1695
  when: (s) => hasWinSwitch(s, "urlcache"),
1463
- desc: "certutil \u4E0B\u8F7D\uFF08\u5E38\u89C1\u514D\u6740\u4E0B\u8F7D\u5668\uFF09"
1696
+ desc: () => t("danger.certutil_download")
1697
+ },
1698
+ {
1699
+ root: "bitsadmin",
1700
+ when: (s) => hasWinSwitch(s, "transfer"),
1701
+ desc: () => t("danger.bitsadmin")
1464
1702
  },
1465
- { root: "bitsadmin", when: (s) => hasWinSwitch(s, "transfer"), desc: "bitsadmin \u540E\u53F0\u4E0B\u8F7D" },
1466
1703
  ENCODED_COMMAND_RULE,
1467
1704
  {
1468
1705
  root: ["powershell", "pwsh"],
1469
1706
  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"
1707
+ desc: () => t("danger.ps_hidden_window")
1471
1708
  },
1472
1709
  {
1473
1710
  root: ["powershell", "pwsh"],
1474
1711
  when: (s) => ["bypass", "unrestricted"].includes(
1475
1712
  (winSwitchValue(s, "executionpolicy") ?? "").toLowerCase()
1476
1713
  ),
1477
- desc: "\u7ED5\u8FC7 PowerShell \u6267\u884C\u7B56\u7565"
1714
+ desc: () => t("danger.ps_bypass_policy")
1478
1715
  },
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" },
1716
+ { root: "mshta", desc: () => t("danger.mshta") },
1717
+ { root: "rundll32", desc: () => t("danger.rundll32") },
1481
1718
  {
1482
1719
  root: "regsvr32",
1483
1720
  when: (s) => args(s).some((a) => /^[/-]i:/i.test(a)),
1484
- desc: "regsvr32 \u8FDC\u7A0B scriptlet\uFF08LOLBin\uFF09"
1721
+ desc: () => t("danger.regsvr32")
1485
1722
  },
1486
1723
  {
1487
1724
  root: "shutdown",
1488
1725
  when: (s) => hasWinSwitch(s, "s", true) || hasWinSwitch(s, "r", true),
1489
- desc: "\u5173\u673A / \u91CD\u542F"
1726
+ desc: () => t("danger.shutdown")
1490
1727
  },
1491
- { root: ["restart-computer", "stop-computer"], desc: "\u5173\u673A / \u91CD\u542F" },
1728
+ { root: ["restart-computer", "stop-computer"], desc: () => t("danger.shutdown") },
1492
1729
  {
1493
1730
  root: "schtasks",
1494
1731
  when: (s) => hasWinSwitch(s, "create", true),
1495
- desc: "\u521B\u5EFA\u8BA1\u5212\u4EFB\u52A1\uFF08\u6301\u4E45\u5316\uFF09"
1732
+ desc: () => t("danger.schtasks_create")
1496
1733
  },
1497
- { root: ["new-scheduledtask", "register-scheduledtask"], desc: "\u6CE8\u518C\u8BA1\u5212\u4EFB\u52A1\uFF08\u6301\u4E45\u5316\uFF09" }
1734
+ {
1735
+ root: ["new-scheduledtask", "register-scheduledtask"],
1736
+ desc: () => t("danger.register_scheduled_task")
1737
+ }
1498
1738
  ];
1499
1739
  var WHOLE_STRING_RULES = [
1500
- { pattern: /:\s*\(\s*\)\s*\{.*\}\s*;?\s*:/, desc: "fork bomb" }
1740
+ { pattern: /:\s*\(\s*\)\s*\{.*\}\s*;?\s*:/, desc: () => t("danger.fork_bomb") }
1501
1741
  ];
1502
1742
  var WINDOWS_TABLE = [...WINDOWS_RULES, ...POSIX_RULES];
1503
1743
  function parseForSafety(command, platform10) {
@@ -1511,7 +1751,7 @@ function checkDangerousCommand(command, opts) {
1511
1751
  const platform10 = opts?.platform ?? currentPlatform();
1512
1752
  const parsed = parseForSafety(command, platform10);
1513
1753
  for (const { pattern, desc } of WHOLE_STRING_RULES) {
1514
- if (pattern.test(parsed.stripped)) return { dangerous: true, desc };
1754
+ if (pattern.test(parsed.stripped)) return { dangerous: true, desc: desc() };
1515
1755
  }
1516
1756
  const table = platform10 === "win32" ? WINDOWS_TABLE : POSIX_RULES;
1517
1757
  for (let i = 0; i < parsed.segments.length; i++) {
@@ -1520,7 +1760,7 @@ function checkDangerousCommand(command, opts) {
1520
1760
  for (const rule of table) {
1521
1761
  if (!rootMatches(rule.root, seg.root)) continue;
1522
1762
  if (rule.when && !rule.when(seg, prev)) continue;
1523
- return { dangerous: true, desc: rule.desc };
1763
+ return { dangerous: true, desc: rule.desc() };
1524
1764
  }
1525
1765
  }
1526
1766
  return { dangerous: false };
@@ -1598,29 +1838,31 @@ function isReadOnlyCommand(command) {
1598
1838
  });
1599
1839
  }
1600
1840
  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" }
1841
+ { pattern: /\beval\b/, desc: () => t("danger.obf_eval") },
1842
+ { pattern: /\bexec\s+\$/, desc: () => t("danger.obf_exec_var") },
1843
+ { pattern: /\bbase64\s+(-d|--decode)/, desc: () => t("danger.obf_base64") },
1844
+ { pattern: /\b(xxd|od)\s+-r/, desc: () => t("danger.obf_hex_restore") },
1845
+ { pattern: /\|\s*(ba|z|k|da)?sh\b/, desc: () => t("danger.obf_pipe_shell") },
1846
+ { pattern: /\\x[0-9a-f]{2}/i, desc: () => t("danger.obf_hex_escape") },
1847
+ { pattern: /\$\{[^}]*[:#%/][^}]*\}/, desc: () => t("danger.obf_var_expansion") },
1848
+ { pattern: /\bprintf\b[^|;&]*\|/, desc: () => t("danger.obf_printf_pipe") },
1849
+ { pattern: /\bcurl\b[^|;&]*\|/, desc: () => t("danger.obf_curl_pipe") }
1610
1850
  ];
1611
1851
  function checkObfuscation(command) {
1612
1852
  const parsed = parseForSafety(command, currentPlatform());
1613
- if (parsed.parseError !== void 0) return `\u65E0\u6CD5\u89E3\u6790\uFF08${parsed.parseError}\uFF09`;
1853
+ if (parsed.parseError !== void 0) {
1854
+ return t("infra_misc.parse_failed", { reason: parsed.parseError });
1855
+ }
1614
1856
  if (parsed.hasSubstitution) {
1615
- return /`/.test(parsed.stripped) && !/\$\(/.test(parsed.stripped) ? "\u53CD\u5F15\u53F7\u547D\u4EE4\u66FF\u6362" : "\u547D\u4EE4\u66FF\u6362 $(...)";
1857
+ return /`/.test(parsed.stripped) && !/\$\(/.test(parsed.stripped) ? t("infra_misc.backtick_substitution") : t("infra_misc.dollar_substitution");
1616
1858
  }
1617
1859
  for (const seg of parsed.segments) {
1618
1860
  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`;
1861
+ if (interpreter !== null) return t("infra_misc.inline_code", { interpreter });
1620
1862
  }
1621
1863
  const norm = command.replace(/\s+/g, " ").trim();
1622
1864
  for (const { pattern, desc } of OBFUSCATION_PATTERNS) {
1623
- if (pattern.test(norm)) return desc;
1865
+ if (pattern.test(norm)) return desc();
1624
1866
  }
1625
1867
  return null;
1626
1868
  }
@@ -1705,11 +1947,7 @@ var SYSTEM_READ_PATHS = [
1705
1947
  "/private/var/db",
1706
1948
  "/private/var/folders",
1707
1949
  "/private/var/select",
1708
- // xcode-select 要读它,否则 macOS 自带 python3 起不来
1709
1950
  "/private/etc",
1710
- // macOS 自带的 /usr/bin/python3 是个 Xcode shim:它会去跑 xcodebuild,
1711
- // 需要读整个 Xcode.app(Info.plist + SharedFrameworks)。
1712
- // Xcode.app 是公开的应用包,放行读取不泄露任何用户数据。
1713
1951
  "/Applications/Xcode.app",
1714
1952
  "/Library/Developer"
1715
1953
  ];
@@ -1745,9 +1983,6 @@ var SENSITIVE_HOME_PATHS = [
1745
1983
  "Library/Application Support/Firefox",
1746
1984
  "Library/Cookies",
1747
1985
  "Library/Messages",
1748
- // agent 自己的数据:会话、记忆、审批、API key 都在这。
1749
- // 注意沙箱目录本身就在 ~/.epoch/sandbox 下,所以 buildProfile 必须
1750
- // 在这条 deny **之后**重新 allow writableDirs,否则连自己的 code.js 都读不到。
1751
1986
  ".epoch"
1752
1987
  ];
1753
1988
  var SENSITIVE_ABS_PATHS = ["/etc/shadow", "/etc/sudoers", "/private/etc/master.passwd"];
@@ -1809,7 +2044,6 @@ function networkSection(allowNetwork) {
1809
2044
  return [
1810
2045
  ";; ---- \u7F51\u7EDC ----",
1811
2046
  "(deny network*)",
1812
- // 留一个本机 unix socket,否则解析库初始化就可能失败
1813
2047
  '(allow network-outbound (literal "/private/var/run/mDNSResponder"))'
1814
2048
  ];
1815
2049
  }
@@ -1885,7 +2119,6 @@ function isolateWithSeatbelt(command, args2, opts) {
1885
2119
  ]
1886
2120
  };
1887
2121
  }
1888
-
1889
2122
  // src/sandbox/backend.ts
1890
2123
  function detectBackend() {
1891
2124
  if (platform() === "darwin" && probeSeatbelt()) return "seatbelt";
@@ -1902,7 +2135,6 @@ function isolate(command, args2, opts) {
1902
2135
  return null;
1903
2136
  }
1904
2137
  }
1905
-
1906
2138
  // src/sandbox/classify.ts
1907
2139
  var DENIAL_SIGNATURES = {
1908
2140
  seatbelt: [/Operation not permitted/i, /\bEPERM\b/],
@@ -1912,13 +2144,10 @@ var DENIAL_SIGNATURES = {
1912
2144
  var RUNNER_FAILURE_RULES = {
1913
2145
  seatbelt: {
1914
2146
  fatal: [/^sandbox-exec:/],
1915
- // sandbox-exec 在 macOS 上已标记 deprecated,它可能(现在或将来)
1916
- // 往 stderr 打一句提示。那是**信息**不是失败,见方案 46 验收 7
1917
2147
  benign: [/^sandbox-exec:.*\bdeprecat/i]
1918
2148
  },
1919
2149
  bubblewrap: {
1920
2150
  fatal: [/^bwrap:/],
1921
- // bwrap 在部分内核上会为无害的降级打一行提示(比如某个 unshare 跳过了)
1922
2151
  benign: [/^bwrap:.*\b(?:skipping|ignoring|deprecat)/i]
1923
2152
  },
1924
2153
  none: { fatal: [], benign: [] }
@@ -1979,6 +2208,9 @@ function writableDirsFor(policy) {
1979
2208
  }
1980
2209
  function confine(command, args2, policy) {
1981
2210
  const backend = detectBackend();
2211
+ if (policy.enabled === false) {
2212
+ return { confined: false, backend, mode: policy.mode, reason: "config-disabled" };
2213
+ }
1982
2214
  if (policy.mode === "danger-full-access") {
1983
2215
  return { confined: false, backend, mode: policy.mode, reason: "mode-disabled" };
1984
2216
  }
@@ -2004,40 +2236,299 @@ function confine(command, args2, policy) {
2004
2236
  runnerFailureRules: RUNNER_FAILURE_RULES[backend]
2005
2237
  };
2006
2238
  }
2239
+ // src/jobs.ts
2240
+ var bySession2 = /* @__PURE__ */ new Map();
2241
+ var PREFIX = { command: "t", shell: "s", agent: "a" };
2242
+ var counters = { command: 0, shell: 0, agent: 0 };
2243
+ function nextId(kind) {
2244
+ counters[kind] += 1;
2245
+ return `${PREFIX[kind]}${counters[kind]}`;
2246
+ }
2247
+ function bucketOf(sessionId) {
2248
+ const existing = bySession2.get(sessionId);
2249
+ if (existing) return existing;
2250
+ const fresh = /* @__PURE__ */ new Map();
2251
+ bySession2.set(sessionId, fresh);
2252
+ return fresh;
2253
+ }
2254
+ function jobIn(sessionId, id) {
2255
+ return bySession2.get(sessionId)?.get(id);
2256
+ }
2257
+ function append(job, chunk) {
2258
+ job.info.outputBytes += chunk.length;
2259
+ job.buffer += chunk;
2260
+ if (job.buffer.length <= job.spec.maxOutput) return;
2261
+ if (job.spill === void 0) {
2262
+ job.spill = openArtifact(job.spec.artifactsDir, `${job.spec.artifactLabel}-${job.info.id}`, ".txt") ?? null;
2263
+ job.spill?.append(job.buffer);
2264
+ if (job.spill) job.info.artifact = job.spill.path;
2265
+ } else {
2266
+ job.spill?.append(chunk);
2267
+ }
2268
+ const drop = job.buffer.length - job.spec.maxOutput;
2269
+ job.buffer = job.buffer.slice(drop);
2270
+ job.droppedBytes += drop;
2271
+ job.info.truncated = true;
2272
+ }
2273
+ function endJob(job, status, exitCode) {
2274
+ if (job.info.status !== "running") return;
2275
+ job.info.status = status;
2276
+ if (exitCode !== void 0) job.info.exitCode = exitCode;
2277
+ job.info.endedAt = Date.now();
2278
+ job.spill?.close();
2279
+ job.settle();
2280
+ }
2281
+ function registerJob(spec) {
2282
+ const now = Date.now();
2283
+ let settle;
2284
+ const done = new Promise((resolve5) => {
2285
+ settle = resolve5;
2286
+ });
2287
+ const job = {
2288
+ spec,
2289
+ info: {
2290
+ id: nextId(spec.kind),
2291
+ kind: spec.kind,
2292
+ label: spec.label,
2293
+ ...spec.cwd ? { cwd: spec.cwd } : {},
2294
+ pid: 0,
2295
+ status: "running",
2296
+ startedAt: now,
2297
+ lastActivity: now,
2298
+ outputBytes: 0,
2299
+ truncated: false
2300
+ },
2301
+ buffer: "",
2302
+ droppedBytes: 0,
2303
+ done,
2304
+ settle
2305
+ };
2306
+ bucketOf(spec.sessionId).set(job.info.id, job);
2307
+ return {
2308
+ id: job.info.id,
2309
+ detail: spec.detail,
2310
+ info: () => ({ ...job.info }),
2311
+ status: () => job.info.status,
2312
+ setPid: (pid) => {
2313
+ job.info.pid = pid;
2314
+ },
2315
+ append: (chunk) => append(job, chunk),
2316
+ touch: () => {
2317
+ job.info.lastActivity = Date.now();
2318
+ },
2319
+ cursor: () => job.info.outputBytes,
2320
+ finish: (status, exitCode) => endJob(job, status, exitCode)
2321
+ };
2322
+ }
2323
+ function listJobs(sessionId, kind) {
2324
+ const bucket = bySession2.get(sessionId);
2325
+ if (!bucket) return [];
2326
+ const all = [...bucket.values()].map((job) => ({ ...job.info }));
2327
+ return kind ? all.filter((info) => info.kind === kind) : all;
2328
+ }
2329
+ function getJob(sessionId, id) {
2330
+ const job = jobIn(sessionId, id);
2331
+ return job ? { ...job.info } : void 0;
2332
+ }
2333
+ function jobDetail(sessionId, id) {
2334
+ return jobIn(sessionId, id)?.spec.detail;
2335
+ }
2336
+ function allJobs(kind) {
2337
+ const out = [];
2338
+ for (const [sessionId, bucket] of bySession2) {
2339
+ for (const job of bucket.values()) {
2340
+ if (kind && job.info.kind !== kind) continue;
2341
+ out.push({ sessionId, info: { ...job.info } });
2342
+ }
2343
+ }
2344
+ return out;
2345
+ }
2346
+ function readJob(sessionId, id, since, maxChunk) {
2347
+ const job = jobIn(sessionId, id);
2348
+ return job ? sliceFrom(job, since, maxChunk) : void 0;
2349
+ }
2350
+ function sliceFrom(job, since, maxChunk) {
2351
+ const from = Math.max(since, job.droppedBytes);
2352
+ const missed = Math.max(0, job.droppedBytes - since);
2353
+ const slice = job.buffer.slice(from - job.droppedBytes);
2354
+ const output = slice.slice(0, maxChunk);
2355
+ return {
2356
+ info: { ...job.info },
2357
+ output,
2358
+ nextCursor: from + output.length,
2359
+ missed,
2360
+ hasMore: output.length < slice.length
2361
+ };
2362
+ }
2363
+ function touchJob(sessionId, id) {
2364
+ const job = jobIn(sessionId, id);
2365
+ if (job) job.info.lastActivity = Date.now();
2366
+ }
2367
+ function appendJob(sessionId, id, chunk) {
2368
+ const job = jobIn(sessionId, id);
2369
+ if (job) append(job, chunk);
2370
+ }
2371
+ async function waitJob(sessionId, id, timeoutMs) {
2372
+ const job = jobIn(sessionId, id);
2373
+ if (!job) return void 0;
2374
+ const snapshot = () => ({ ...job.info });
2375
+ if (job.info.status !== "running") {
2376
+ return { info: snapshot(), timedOut: false, neverEnds: false };
2377
+ }
2378
+ if (!job.spec.terminates) return { info: snapshot(), timedOut: false, neverEnds: true };
2379
+ let timer;
2380
+ const timeout = new Promise((resolve5) => {
2381
+ timer = setTimeout(() => resolve5("timeout"), timeoutMs);
2382
+ timer.unref?.();
2383
+ });
2384
+ try {
2385
+ const winner = await Promise.race([job.done.then(() => "done"), timeout]);
2386
+ return { info: snapshot(), timedOut: winner === "timeout", neverEnds: false };
2387
+ } finally {
2388
+ if (timer) clearTimeout(timer);
2389
+ }
2390
+ }
2391
+ function stopJob(sessionId, id, status) {
2392
+ const job = jobIn(sessionId, id);
2393
+ if (!job || job.info.status !== "running") return Promise.resolve(false);
2394
+ const stop = job.spec.stop;
2395
+ endJob(job, status ?? job.spec.stoppedStatus ?? "killed");
2396
+ return stop().then(() => true);
2397
+ }
2398
+ function rekeyJobs(from, to, kinds) {
2399
+ if (from === to) return;
2400
+ const moving = bySession2.get(from);
2401
+ if (!moving) return;
2402
+ const wanted = [...moving].filter(([, job]) => kinds.includes(job.info.kind));
2403
+ if (wanted.length === 0) return;
2404
+ for (const [id] of wanted) moving.delete(id);
2405
+ if (moving.size === 0) bySession2.delete(from);
2406
+ const target = bucketOf(to);
2407
+ for (const [id, job] of wanted) {
2408
+ job.spec.sessionId = to;
2409
+ target.set(id, job);
2410
+ }
2411
+ }
2412
+ function clearAllJobs(kinds) {
2413
+ for (const [sessionId, bucket] of bySession2) {
2414
+ for (const [id, job] of bucket) {
2415
+ if (kinds && !kinds.includes(job.info.kind)) continue;
2416
+ job.spill?.close();
2417
+ job.settle();
2418
+ bucket.delete(id);
2419
+ }
2420
+ if (bucket.size === 0) bySession2.delete(sessionId);
2421
+ }
2422
+ for (const kind of kinds ?? ["command", "shell", "agent"]) counters[kind] = 0;
2423
+ }
2007
2424
  var EXEC_PATH_AS_NODE_ENV = {
2008
2425
  ELECTRON_RUN_AS_NODE: "1"
2009
2426
  };
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);
2427
+ var owners = /* @__PURE__ */ new Map();
2428
+ var bySession3 = /* @__PURE__ */ new Map();
2429
+ var TableImpl = class {
2430
+ constructor(fallback) {
2431
+ this.fallback = fallback;
2432
+ }
2433
+ fallback;
2434
+ entries = /* @__PURE__ */ new Map();
2435
+ sessions = /* @__PURE__ */ new Set();
2436
+ released = false;
2437
+ adopt(pid, entry) {
2438
+ this.entries.set(pid, entry);
2439
+ owners.set(pid, this);
2440
+ }
2441
+ forget(pid) {
2442
+ this.entries.delete(pid);
2443
+ if (owners.get(pid) === this) owners.delete(pid);
2444
+ }
2445
+ take(pid) {
2446
+ const entry = this.entries.get(pid);
2447
+ this.forget(pid);
2448
+ return entry;
2449
+ }
2450
+ start(opts) {
2451
+ const child = spawn(opts.file, [...opts.args], {
2452
+ stdio: ["ignore", "pipe", "pipe"],
2453
+ detached: false,
2454
+ ...opts.cwd ? { cwd: opts.cwd } : {},
2455
+ ...opts.spawnOptions
2024
2456
  });
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 };
2457
+ const pid = child.pid ?? 0;
2458
+ if (pid > 0) this.adopt(pid, { kind: "child", child });
2459
+ const pump = (stream) => {
2460
+ child[stream]?.on("data", (buf) => {
2461
+ opts.onOutput?.(buf.toString("utf8"), stream);
2462
+ });
2463
+ };
2464
+ pump("stdout");
2465
+ pump("stderr");
2466
+ child.on("exit", (code, signal) => {
2467
+ this.forget(pid);
2468
+ opts.onExit?.(code, signal);
2469
+ });
2470
+ child.on("error", () => {
2471
+ this.forget(pid);
2472
+ opts.onExit?.(null, null);
2473
+ });
2474
+ return { pid, child };
2475
+ }
2476
+ trackForeign(pid, kill) {
2477
+ if (pid <= 0) return () => {
2478
+ };
2479
+ this.adopt(pid, { kind: "foreign", kill });
2480
+ return () => {
2481
+ if (this.entries.get(pid)?.kind === "foreign") this.forget(pid);
2482
+ };
2483
+ }
2484
+ count() {
2485
+ return this.entries.size;
2486
+ }
2487
+ async killAll() {
2488
+ const pids = [...this.entries.keys()];
2489
+ await Promise.all(pids.map((pid) => killTrackedProcess(pid)));
2490
+ }
2491
+ claim(sessionId) {
2492
+ if (this.fallback || !sessionId) return;
2493
+ bySession3.set(sessionId, this);
2494
+ this.sessions.add(sessionId);
2495
+ }
2496
+ async release() {
2497
+ if (this.fallback || this.released) return;
2498
+ this.released = true;
2499
+ for (const sid of this.sessions) if (bySession3.get(sid) === this) bySession3.delete(sid);
2500
+ this.sessions.clear();
2501
+ const last = --fallbackRefs <= 0;
2502
+ if (last) fallbackRefs = 0;
2503
+ await Promise.all(last ? [this.killAll(), fallbackTable.killAll()] : [this.killAll()]);
2504
+ }
2505
+ };
2506
+ var fallbackTable = new TableImpl(true);
2507
+ var fallbackRefs = 0;
2508
+ function createProcessTable() {
2509
+ fallbackRefs++;
2510
+ return new TableImpl(false);
2511
+ }
2512
+ function processTableFor(sessionId) {
2513
+ if (!sessionId) return fallbackTable;
2514
+ return bySession3.get(sessionId) ?? fallbackTable;
2515
+ }
2516
+ function processFallbackTable() {
2517
+ return fallbackTable;
2518
+ }
2519
+ function startLongLivedProcess(opts) {
2520
+ return fallbackTable.start(opts);
2521
+ }
2522
+ function trackForeignProcess(pid, kill) {
2523
+ return fallbackTable.trackForeign(pid, kill);
2037
2524
  }
2038
2525
  async function killTrackedProcess(pid) {
2039
- const child = tracked.get(pid);
2040
- tracked.delete(pid);
2526
+ const entry = owners.get(pid)?.take(pid);
2527
+ if (entry?.kind === "foreign") {
2528
+ await entry.kill();
2529
+ return;
2530
+ }
2531
+ const child = entry?.child;
2041
2532
  await killProcessTree({
2042
2533
  pid,
2043
2534
  detached: false,
@@ -2046,10 +2537,10 @@ async function killTrackedProcess(pid) {
2046
2537
  });
2047
2538
  }
2048
2539
  function trackedProcessCount() {
2049
- return tracked.size;
2540
+ return owners.size;
2050
2541
  }
2051
2542
  async function killAllTrackedProcesses() {
2052
- const pids = [...tracked.keys()];
2543
+ const pids = [...owners.keys()];
2053
2544
  await Promise.all(pids.map((pid) => killTrackedProcess(pid)));
2054
2545
  }
2055
2546
  function within(root, abs) {
@@ -2063,7 +2554,6 @@ function isInWorkspace(target, workDir, extraRoots = []) {
2063
2554
  if (within(root, abs)) return true;
2064
2555
  return extraRoots.some((r) => within(resolve$1(r), abs));
2065
2556
  }
2066
-
2067
2557
  // src/schema.ts
2068
2558
  function parseLenient(schema, input, fallback) {
2069
2559
  const result = schema.safeParse(input);
@@ -2075,8 +2565,7 @@ function parseStrict(schema, input, label) {
2075
2565
  if (result.success) return result.data;
2076
2566
  const issues = toIssues(result.error.issues);
2077
2567
  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}`);
2568
+ throw new Error(t("infra_misc.schema_failed", { path: label, count: issues.length, detail }));
2080
2569
  }
2081
2570
  var DEFAULT_ISSUE_LIMIT = 8;
2082
2571
  function formatIssues(label, issues, limit = DEFAULT_ISSUE_LIMIT) {
@@ -2086,7 +2575,7 @@ function issueDetails(issues, limit = DEFAULT_ISSUE_LIMIT) {
2086
2575
  if (issues.length === 0) return [];
2087
2576
  const shown = issues.slice(0, limit).map((i) => `${i.path} \u2014\u2014 ${i.message}`);
2088
2577
  const rest = issues.length - shown.length;
2089
- if (rest > 0) shown.push(`\u8FD8\u6709 ${rest} \u5904\u95EE\u9898\u672A\u5217\u51FA`);
2578
+ if (rest > 0) shown.push(t("infra_misc.schema_more", { count: rest }));
2090
2579
  return shown;
2091
2580
  }
2092
2581
  function unknownKeyIssues(known, input, prefix = "") {
@@ -2094,7 +2583,7 @@ function unknownKeyIssues(known, input, prefix = "") {
2094
2583
  const knownSet = new Set(known);
2095
2584
  return Object.keys(input).filter((key) => !knownSet.has(key)).map((key) => ({
2096
2585
  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"
2586
+ message: t("infra_misc.schema_unknown_key")
2098
2587
  }));
2099
2588
  }
2100
2589
  var MAX_NEST_DEPTH = 5;
@@ -2129,7 +2618,6 @@ function dedupe(issues) {
2129
2618
  return true;
2130
2619
  });
2131
2620
  }
2132
-
2133
2621
  // src/async.ts
2134
2622
  async function withTimeout(promise, ms, fallback) {
2135
2623
  let timer;
@@ -2158,151 +2646,6 @@ function safeInit(name, factory, diags) {
2158
2646
  return null;
2159
2647
  }
2160
2648
  }
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
2649
  var SECRET_SERVICE = "epoch-agent";
2307
2650
  function secretServiceFor(homeDir) {
2308
2651
  if (homeDir === join(homedir(), ".epoch")) return SECRET_SERVICE;
@@ -2320,7 +2663,7 @@ var OP_TIMEOUT_MS = 1e4;
2320
2663
  var BACKEND_ENV_VAR = "EPOCH_SECRET_BACKEND";
2321
2664
  function assertValidSecretName(name) {
2322
2665
  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`);
2666
+ throw new Error(t("secret.bad_name", { name }));
2324
2667
  }
2325
2668
  }
2326
2669
  function secretIndexPath(homeDir) {
@@ -2329,7 +2672,6 @@ function secretIndexPath(homeDir) {
2329
2672
  function dpapiStorePath(homeDir) {
2330
2673
  return join(homeDir, "secrets.dpapi.json");
2331
2674
  }
2332
-
2333
2675
  // src/secret/catalog.ts
2334
2676
  var FILE_VERSION = 1;
2335
2677
  var SecretCatalog = class {
@@ -2338,7 +2680,6 @@ var SecretCatalog = class {
2338
2680
  }
2339
2681
  filePath;
2340
2682
  cache = null;
2341
- /** 已登记的名字,顺序稳定(排序过),方便 `epoch config secret list` 输出稳定 */
2342
2683
  read() {
2343
2684
  if (this.cache) return [...this.cache];
2344
2685
  this.cache = parse(this.filePath);
@@ -2354,22 +2695,14 @@ var SecretCatalog = class {
2354
2695
  if (!names.includes(name)) return;
2355
2696
  this.write(names.filter((n) => n !== name));
2356
2697
  }
2357
- /** 整体替换。预取自愈用:把「钥匙串里真读得出来」的那份写回去 */
2358
2698
  replace(names) {
2359
2699
  const next = [...new Set(names)].sort();
2360
2700
  if (sameList(this.read(), next)) return;
2361
2701
  this.write(next);
2362
2702
  }
2363
- /** 仅供测试:丢掉内存缓存,强制重新读盘 */
2364
2703
  invalidate() {
2365
2704
  this.cache = null;
2366
2705
  }
2367
- /**
2368
- * 原子写 + 建文件时就 0600。
2369
- *
2370
- * mode 在**创建时**生效,写完再 chmod 会留 TOCTOU 窗口
2371
- * [对标 hermes mcp_oauth.py:388 的 `_write_secure_json`]。
2372
- */
2373
2706
  write(names) {
2374
2707
  const sorted = [...names].sort();
2375
2708
  const file = { version: FILE_VERSION, names: sorted };
@@ -2384,7 +2717,11 @@ var SecretCatalog = class {
2384
2717
  } catch (err) {
2385
2718
  rmSync(tmp, { force: true });
2386
2719
  throw new Error(
2387
- `\u51ED\u636E\u7D22\u5F15 ${this.filePath} \u5199\u5165\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
2720
+ t("secret.op_failed", {
2721
+ op: t("secret.op_write"),
2722
+ name: t("secret.catalog_label"),
2723
+ detail: `${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
2724
+ })
2388
2725
  );
2389
2726
  }
2390
2727
  this.cache = sorted;
@@ -2411,10 +2748,8 @@ function run(file, args2, opts) {
2411
2748
  args2,
2412
2749
  {
2413
2750
  timeout: opts.timeoutMs,
2414
- // 凭据值可能几 KB(OAuth token 串),默认 1MB 够用但显式写出来更清楚
2415
2751
  maxBuffer: 1024 * 1024,
2416
2752
  encoding: "utf-8",
2417
- // Windows 上别弹黑框:PowerShell 冷启动会闪一个控制台窗口
2418
2753
  ...WINDOWS_HIDE_FLAGS
2419
2754
  },
2420
2755
  (err, stdout, stderr) => {
@@ -2444,12 +2779,15 @@ function classify(err) {
2444
2779
  };
2445
2780
  }
2446
2781
  function describeFailure(file, r) {
2447
- if (r.missing) return `\u627E\u4E0D\u5230 ${file}`;
2448
- if (r.timedOut) return `${file} \u8D85\u65F6\u672A\u8FD4\u56DE`;
2782
+ if (r.missing) return t("secret.exec_missing", { file });
2783
+ if (r.timedOut) return t("secret.exec_timeout", { file });
2449
2784
  const detail = r.stderr.trim().split("\n")[0]?.slice(0, 200);
2450
- return `${file} \u9000\u51FA\u7801 ${r.code}${detail ? `\uFF08${detail}\uFF09` : ""}`;
2785
+ return t("secret.exec_exit", {
2786
+ file,
2787
+ code: r.code ?? -1,
2788
+ detail: detail ? t("secret.exec_exit_detail", { detail }) : ""
2789
+ });
2451
2790
  }
2452
-
2453
2791
  // src/secret/dpapi.ts
2454
2792
  var FILE_VERSION2 = 1;
2455
2793
  var SHELLS2 = ["powershell.exe", "pwsh"];
@@ -2476,20 +2814,38 @@ var DpapiStore = class {
2476
2814
  shell;
2477
2815
  backend = "dpapi";
2478
2816
  encrypted = true;
2479
- detail = "Windows DPAPI\uFF08\u5DF2\u52A0\u5BC6\uFF0C\u7ED1\u5B9A\u5F53\u524D\u7528\u6237\u8D26\u6237\uFF09";
2817
+ get detail() {
2818
+ return t("secret.backend_dpapi");
2819
+ }
2480
2820
  cache = null;
2481
2821
  async get(name) {
2482
2822
  assertValidSecretName(name);
2483
2823
  const blob = this.load().secrets[name];
2484
2824
  if (!blob) return void 0;
2485
2825
  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)}`);
2826
+ if (r.code !== 0) {
2827
+ throw new Error(
2828
+ t("secret.op_failed", {
2829
+ op: t("secret.op_read"),
2830
+ name,
2831
+ detail: describeFailure(this.shell, r)
2832
+ })
2833
+ );
2834
+ }
2487
2835
  return Buffer.from(r.stdout.trim(), "base64").toString("utf-8");
2488
2836
  }
2489
2837
  async set(name, value) {
2490
2838
  assertValidSecretName(name);
2491
2839
  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)}`);
2840
+ if (r.code !== 0) {
2841
+ throw new Error(
2842
+ t("secret.op_failed", {
2843
+ op: t("secret.op_write"),
2844
+ name,
2845
+ detail: describeFailure(this.shell, r)
2846
+ })
2847
+ );
2848
+ }
2493
2849
  const file = this.load();
2494
2850
  file.secrets[name] = stripBlank(r.stdout);
2495
2851
  this.save(file);
@@ -2503,7 +2859,6 @@ var DpapiStore = class {
2503
2859
  }
2504
2860
  return Promise.resolve();
2505
2861
  }
2506
- /** 密文就在自己的文件里,名字直接是 key —— 这个后端不需要 catalog.ts 的索引 */
2507
2862
  list() {
2508
2863
  return Promise.resolve(Object.keys(this.load().secrets).sort());
2509
2864
  }
@@ -2515,7 +2870,6 @@ var DpapiStore = class {
2515
2870
  this.cache = readDpapiFile(this.filePath);
2516
2871
  return this.cache;
2517
2872
  }
2518
- /** 原子写 + 建文件时就 0600(Windows 上 mode 是空操作,但这份代码也跑在测试里) */
2519
2873
  save(file) {
2520
2874
  mkdirSync(dirname(this.filePath), { recursive: true });
2521
2875
  const tmp = `${this.filePath}.${process.pid}.tmp`;
@@ -2525,7 +2879,11 @@ var DpapiStore = class {
2525
2879
  } catch (err) {
2526
2880
  rmSync(tmp, { force: true });
2527
2881
  throw new Error(
2528
- `\u51ED\u636E\u6587\u4EF6 ${this.filePath} \u5199\u5165\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`
2882
+ t("secret.op_failed", {
2883
+ op: t("secret.op_write"),
2884
+ name: t("secret.dpapi_file_label"),
2885
+ detail: `${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
2886
+ })
2529
2887
  );
2530
2888
  }
2531
2889
  this.cache = file;
@@ -2548,10 +2906,10 @@ function readDpapiFile(filePath) {
2548
2906
  }
2549
2907
  }
2550
2908
  async function probeDpapi() {
2551
- if (platform() !== "win32") return { reason: "\u4E0D\u662F Windows" };
2909
+ if (platform() !== "win32") return { reason: t("secret.not_windows") };
2552
2910
  const expect = randomBytes(8).toString("hex");
2553
2911
  const payload = Buffer.from(expect, "utf-8").toString("base64");
2554
- let last = "\u672A\u627E\u5230 PowerShell\uFF08powershell.exe / pwsh \u90FD\u4E0D\u53EF\u7528\uFF09";
2912
+ let last = t("secret.no_powershell");
2555
2913
  for (const shell of SHELLS2) {
2556
2914
  const sealed = await run(shell, [...PS_FLAGS, PROTECT_SCRIPT], {
2557
2915
  timeoutMs: POWERSHELL_TIMEOUT_MS,
@@ -2559,7 +2917,7 @@ async function probeDpapi() {
2559
2917
  });
2560
2918
  if (sealed.missing) continue;
2561
2919
  if (sealed.code !== 0) {
2562
- last = `\u52A0\u5BC6\u63A2\u6D4B\u5931\u8D25\uFF08${describeFailure(shell, sealed)}\uFF09`;
2920
+ last = t("secret.probe_encrypt_failed", { detail: describeFailure(shell, sealed) });
2563
2921
  continue;
2564
2922
  }
2565
2923
  const back = await run(shell, [...PS_FLAGS, UNPROTECT_SCRIPT], {
@@ -2567,11 +2925,11 @@ async function probeDpapi() {
2567
2925
  stdin: stripBlank(sealed.stdout)
2568
2926
  });
2569
2927
  if (back.code !== 0) {
2570
- last = `\u89E3\u5BC6\u63A2\u6D4B\u5931\u8D25\uFF08${describeFailure(shell, back)}\uFF09`;
2928
+ last = t("secret.probe_decrypt_failed", { detail: describeFailure(shell, back) });
2571
2929
  continue;
2572
2930
  }
2573
2931
  if (Buffer.from(back.stdout.trim(), "base64").toString("utf-8") !== expect) {
2574
- last = "\u52A0\u89E3\u5BC6\u63A2\u6D4B\u7684\u503C\u5BF9\u4E0D\u4E0A";
2932
+ last = t("secret.probe_crypto_mismatch");
2575
2933
  continue;
2576
2934
  }
2577
2935
  return { shell };
@@ -2584,11 +2942,6 @@ function stripBlank(s) {
2584
2942
  var SECURITY = "/usr/bin/security";
2585
2943
  var NOT_FOUND = 44;
2586
2944
  var KeychainStore = class {
2587
- /**
2588
- * @param service 钥匙串里的服务名。**按家目录分命名空间**,见
2589
- * [types.ts](./types.js) 的 `secretServiceFor()` ——
2590
- * 钥匙串是全局的,写死一个名字会让多 profile 共用同一批凭据
2591
- */
2592
2945
  constructor(catalog, service) {
2593
2946
  this.catalog = catalog;
2594
2947
  this.service = service;
@@ -2597,14 +2950,24 @@ var KeychainStore = class {
2597
2950
  service;
2598
2951
  backend = "keychain";
2599
2952
  encrypted = true;
2600
- detail = "macOS Keychain\uFF08\u5DF2\u52A0\u5BC6\uFF09";
2953
+ get detail() {
2954
+ return t("secret.backend_keychain");
2955
+ }
2601
2956
  async get(name) {
2602
2957
  assertValidSecretName(name);
2603
2958
  const r = await run(SECURITY, ["find-generic-password", "-s", this.service, "-a", name, "-w"], {
2604
2959
  timeoutMs: OP_TIMEOUT_MS
2605
2960
  });
2606
2961
  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)}`);
2962
+ if (r.code !== 0) {
2963
+ throw new Error(
2964
+ t("secret.op_failed", {
2965
+ op: t("secret.op_read"),
2966
+ name,
2967
+ detail: describeFailure(SECURITY, r)
2968
+ })
2969
+ );
2970
+ }
2608
2971
  return decode(r.stdout);
2609
2972
  }
2610
2973
  async set(name, value) {
@@ -2625,7 +2988,15 @@ var KeychainStore = class {
2625
2988
  ],
2626
2989
  { timeoutMs: OP_TIMEOUT_MS }
2627
2990
  );
2628
- if (r.code !== 0) throw new Error(`\u5199\u5165\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(SECURITY, r)}`);
2991
+ if (r.code !== 0) {
2992
+ throw new Error(
2993
+ t("secret.op_failed", {
2994
+ op: t("secret.op_write"),
2995
+ name,
2996
+ detail: describeFailure(SECURITY, r)
2997
+ })
2998
+ );
2999
+ }
2629
3000
  this.catalog.add(name);
2630
3001
  }
2631
3002
  async delete(name) {
@@ -2634,24 +3005,28 @@ var KeychainStore = class {
2634
3005
  timeoutMs: OP_TIMEOUT_MS
2635
3006
  });
2636
3007
  if (r.code !== 0 && r.code !== NOT_FOUND) {
2637
- throw new Error(`\u5220\u9664\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(SECURITY, r)}`);
3008
+ throw new Error(
3009
+ t("secret.op_failed", {
3010
+ op: t("secret.op_delete"),
3011
+ name,
3012
+ detail: describeFailure(SECURITY, r)
3013
+ })
3014
+ );
2638
3015
  }
2639
3016
  this.catalog.remove(name);
2640
3017
  }
2641
- /** 见 catalog.ts:钥匙串本身没有便宜的列举方式,索引是提示、`get` 才是权威 */
2642
3018
  list() {
2643
3019
  return Promise.resolve(this.catalog.read());
2644
3020
  }
2645
- /** 预取时发现索引里有、钥匙串里没有的名字,就地剔掉(自愈) */
2646
3021
  healCatalog(available) {
2647
3022
  this.catalog.replace(available);
2648
3023
  }
2649
3024
  };
2650
3025
  async function probeKeychain(service) {
2651
- if (platform() !== "darwin") return "\u4E0D\u662F macOS";
3026
+ if (platform() !== "darwin") return t("secret.not_macos");
2652
3027
  const dk = await run(SECURITY, ["default-keychain"], { timeoutMs: PROBE_TIMEOUT_MS2 });
2653
3028
  if (dk.code !== 0 || dk.stdout.trim() === "") {
2654
- return `\u6CA1\u6709\u9ED8\u8BA4\u94A5\u5319\u4E32\uFF08${describeFailure(SECURITY, dk)}\uFF09`;
3029
+ return t("secret.no_default_keychain", { detail: describeFailure(SECURITY, dk) });
2655
3030
  }
2656
3031
  const account = `__probe__${randomBytes(8).toString("hex")}`;
2657
3032
  const expect = randomBytes(8).toString("hex");
@@ -2659,13 +3034,17 @@ async function probeKeychain(service) {
2659
3034
  const added = await run(SECURITY, ["add-generic-password", "-U", ...args2, "-w", encode(expect)], {
2660
3035
  timeoutMs: PROBE_TIMEOUT_MS2
2661
3036
  });
2662
- if (added.code !== 0) return `\u5199\u5165\u63A2\u6D4B\u5931\u8D25\uFF08${describeFailure(SECURITY, added)}\uFF09`;
3037
+ if (added.code !== 0) {
3038
+ return t("secret.probe_write_failed", { detail: describeFailure(SECURITY, added) });
3039
+ }
2663
3040
  const read = await run(SECURITY, ["find-generic-password", ...args2, "-w"], {
2664
3041
  timeoutMs: PROBE_TIMEOUT_MS2
2665
3042
  });
2666
3043
  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";
3044
+ if (read.code !== 0) {
3045
+ return t("secret.probe_read_failed", { detail: describeFailure(SECURITY, read) });
3046
+ }
3047
+ if (decode(read.stdout) !== expect) return t("secret.probe_mismatch");
2669
3048
  return null;
2670
3049
  }
2671
3050
  function encode(value) {
@@ -2676,10 +3055,6 @@ function decode(stdout) {
2676
3055
  }
2677
3056
  var SECRET_TOOL = "secret-tool";
2678
3057
  var LibsecretStore = class {
2679
- /**
2680
- * @param service Secret Service 里的 `service` 属性值。**按家目录分命名空间**,
2681
- * 见 [types.ts](./types.js) 的 `secretServiceFor()`
2682
- */
2683
3058
  constructor(catalog, service) {
2684
3059
  this.catalog = catalog;
2685
3060
  this.service = service;
@@ -2688,8 +3063,9 @@ var LibsecretStore = class {
2688
3063
  service;
2689
3064
  backend = "libsecret";
2690
3065
  encrypted = true;
2691
- detail = "Linux libsecret / Secret Service\uFF08\u5DF2\u52A0\u5BC6\uFF09";
2692
- /** 条目的属性对。用 service + account 两个属性,和 macOS 那边同一套心智 */
3066
+ get detail() {
3067
+ return t("secret.backend_libsecret");
3068
+ }
2693
3069
  attrs(name) {
2694
3070
  return ["service", this.service, "account", name];
2695
3071
  }
@@ -2698,7 +3074,13 @@ var LibsecretStore = class {
2698
3074
  const r = await run(SECRET_TOOL, ["lookup", ...this.attrs(name)], { timeoutMs: OP_TIMEOUT_MS });
2699
3075
  if (r.code === 0) return decode2(r.stdout);
2700
3076
  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)}`);
3077
+ throw new Error(
3078
+ t("secret.op_failed", {
3079
+ op: t("secret.op_read"),
3080
+ name,
3081
+ detail: describeFailure(SECRET_TOOL, r)
3082
+ })
3083
+ );
2702
3084
  }
2703
3085
  async set(name, value) {
2704
3086
  assertValidSecretName(name);
@@ -2707,31 +3089,40 @@ var LibsecretStore = class {
2707
3089
  ["store", `--label=${this.service}: ${name}`, ...this.attrs(name)],
2708
3090
  { timeoutMs: OP_TIMEOUT_MS, stdin: encode2(value) }
2709
3091
  );
2710
- if (r.code !== 0) throw new Error(`\u5199\u5165\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(SECRET_TOOL, r)}`);
3092
+ if (r.code !== 0) {
3093
+ throw new Error(
3094
+ t("secret.op_failed", {
3095
+ op: t("secret.op_write"),
3096
+ name,
3097
+ detail: describeFailure(SECRET_TOOL, r)
3098
+ })
3099
+ );
3100
+ }
2711
3101
  this.catalog.add(name);
2712
3102
  }
2713
3103
  async delete(name) {
2714
3104
  assertValidSecretName(name);
2715
3105
  const r = await run(SECRET_TOOL, ["clear", ...this.attrs(name)], { timeoutMs: OP_TIMEOUT_MS });
2716
3106
  if (r.code !== 0 && (r.timedOut || r.missing)) {
2717
- throw new Error(`\u5220\u9664\u51ED\u636E ${name} \u5931\u8D25\uFF1A${describeFailure(SECRET_TOOL, r)}`);
3107
+ throw new Error(
3108
+ t("secret.op_failed", {
3109
+ op: t("secret.op_delete"),
3110
+ name,
3111
+ detail: describeFailure(SECRET_TOOL, r)
3112
+ })
3113
+ );
2718
3114
  }
2719
3115
  this.catalog.remove(name);
2720
3116
  }
2721
- /**
2722
- * `secret-tool search --all` 能列,但输出格式随 libsecret 版本变,
2723
- * 而且要多走一次 D-Bus 往返。索引更便宜也更稳定 —— 见 catalog.ts。
2724
- */
2725
3117
  list() {
2726
3118
  return Promise.resolve(this.catalog.read());
2727
3119
  }
2728
- /** 预取时发现索引里有、keyring 里没有的名字,就地剔掉(自愈) */
2729
3120
  healCatalog(available) {
2730
3121
  this.catalog.replace(available);
2731
3122
  }
2732
3123
  };
2733
3124
  async function probeLibsecret(service) {
2734
- if (platform() === "darwin" || platform() === "win32") return "\u4E0D\u662F Linux";
3125
+ if (platform() === "darwin" || platform() === "win32") return t("secret.not_linux");
2735
3126
  const account = `__probe__${randomBytes(8).toString("hex")}`;
2736
3127
  const expect = randomBytes(8).toString("hex");
2737
3128
  const args2 = ["service", service, "account", account];
@@ -2740,16 +3131,20 @@ async function probeLibsecret(service) {
2740
3131
  stdin: encode2(expect)
2741
3132
  });
2742
3133
  if (stored.missing) {
2743
- return "\u672A\u627E\u5230 secret-tool\uFF08\u88C5\u6CD5\uFF1Aapt install libsecret-tools / dnf install libsecret\uFF09";
3134
+ return t("secret.no_secret_tool");
2744
3135
  }
2745
3136
  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";
3137
+ return t("secret.secret_tool_hang");
3138
+ }
3139
+ if (stored.code !== 0) {
3140
+ return t("secret.probe_write_failed", { detail: describeFailure(SECRET_TOOL, stored) });
2747
3141
  }
2748
- if (stored.code !== 0) return `\u5199\u5165\u63A2\u6D4B\u5931\u8D25\uFF08${describeFailure(SECRET_TOOL, stored)}\uFF09`;
2749
3142
  const read = await run(SECRET_TOOL, ["lookup", ...args2], { timeoutMs: PROBE_TIMEOUT_MS2 });
2750
3143
  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";
3144
+ if (read.code !== 0) {
3145
+ return t("secret.probe_read_failed", { detail: describeFailure(SECRET_TOOL, read) });
3146
+ }
3147
+ if (decode2(read.stdout) !== expect) return t("secret.probe_mismatch");
2753
3148
  return null;
2754
3149
  }
2755
3150
  function encode2(value) {
@@ -2759,18 +3154,17 @@ function decode2(stdout) {
2759
3154
  return Buffer.from(stdout.trim(), "base64").toString("utf-8");
2760
3155
  }
2761
3156
  var PlaintextStore = class {
2762
- /**
2763
- * @param reason 为什么降级。**必须带修复方法** —— 只说「明文」而不说怎么修,
2764
- * 用户除了忍着没有别的选择
2765
- */
2766
3157
  constructor(envFilePath, reason) {
2767
3158
  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`;
3159
+ this.reason = reason;
2769
3160
  }
2770
3161
  envFilePath;
3162
+ reason;
2771
3163
  backend = "plaintext";
2772
3164
  encrypted = false;
2773
- detail;
3165
+ get detail() {
3166
+ return t("secret.plaintext_note", { reason: this.reason, path: this.envFilePath });
3167
+ }
2774
3168
  get(name) {
2775
3169
  return Promise.resolve(parseEnvText(readText(this.envFilePath))[name]);
2776
3170
  }
@@ -2850,7 +3244,7 @@ var TAG_BYTES = 16;
2850
3244
  var dataKey = null;
2851
3245
  function setDataKey(key) {
2852
3246
  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}`);
3247
+ throw new Error(t("secret.bad_key_length", { expected: KEY_BYTES, actual: key.length }));
2854
3248
  }
2855
3249
  dataKey = key;
2856
3250
  }
@@ -2879,9 +3273,9 @@ function isEnvelope(text) {
2879
3273
  return text.startsWith(ENVELOPE_PREFIX);
2880
3274
  }
2881
3275
  function openEnvelope(key, blob) {
2882
- if (!isEnvelope(blob)) throw new Error("\u4E0D\u662F epoch \u4FE1\u5C01\u5BC6\u6587");
3276
+ if (!isEnvelope(blob)) throw new Error(t("secret.not_envelope"));
2883
3277
  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");
3278
+ if (raw.length < IV_BYTES + TAG_BYTES) throw new Error(t("secret.envelope_too_short"));
2885
3279
  const iv = raw.subarray(0, IV_BYTES);
2886
3280
  const tag = raw.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);
2887
3281
  const body = raw.subarray(IV_BYTES + TAG_BYTES);
@@ -2899,7 +3293,10 @@ async function migrateEnvSecrets(opts) {
2899
3293
  try {
2900
3294
  existing = new Set(await store.list());
2901
3295
  } catch (err) {
2902
- return { migrated: [], warning: `\u8BFB\u53D6\u5DF2\u6709\u51ED\u636E\u5931\u8D25\uFF0C\u8DF3\u8FC7\u8FC1\u79FB\uFF08${message(err)}\uFF09` };
3296
+ return {
3297
+ migrated: [],
3298
+ warning: t("secret.migrate_read_failed", { detail: message(err) })
3299
+ };
2903
3300
  }
2904
3301
  const todo = candidates.filter((n) => !existing.has(n));
2905
3302
  if (todo.length === 0) return { migrated: [] };
@@ -2913,7 +3310,11 @@ async function migrateEnvSecrets(opts) {
2913
3310
  } catch (err) {
2914
3311
  return {
2915
3312
  migrated,
2916
- warning: `\u8FC1\u79FB ${name} \u5931\u8D25\uFF0C\u5DF2\u505C\u6B62\u4E14\u4FDD\u7559 ${envFilePath}\uFF08${message(err)}\uFF09`
3313
+ warning: t("secret.migrate_failed", {
3314
+ name,
3315
+ path: envFilePath,
3316
+ detail: message(err)
3317
+ })
2917
3318
  };
2918
3319
  }
2919
3320
  }
@@ -2921,7 +3322,10 @@ async function migrateEnvSecrets(opts) {
2921
3322
  try {
2922
3323
  renameSync(envFilePath, backupPath);
2923
3324
  } catch (err) {
2924
- return { migrated, warning: `\u5DF2\u8FC1\u5165\u94A5\u5319\u4E32\uFF0C\u4F46 ${envFilePath} \u6539\u540D\u5931\u8D25\uFF08${message(err)}\uFF09` };
3325
+ return {
3326
+ migrated,
3327
+ warning: t("secret.migrate_partial", { path: envFilePath, detail: message(err) })
3328
+ };
2925
3329
  }
2926
3330
  return { migrated, backupPath };
2927
3331
  }
@@ -2943,14 +3347,16 @@ function readOrEmpty(path) {
2943
3347
  function message(err) {
2944
3348
  return err instanceof Error ? err.message : String(err);
2945
3349
  }
2946
-
2947
3350
  // src/secret/index.ts
2948
3351
  async function createSecretStore(opts = {}) {
2949
3352
  if (opts.inject) return opts.inject;
2950
3353
  const homeDir = opts.homeDir ?? resolveHomeDir();
2951
3354
  const forced = readForcedBackend();
2952
3355
  if (forced === "plaintext") {
2953
- return new PlaintextStore(envPath(homeDir), `${BACKEND_ENV_VAR}=plaintext \u663E\u5F0F\u6307\u5B9A`);
3356
+ return new PlaintextStore(
3357
+ envPath(homeDir),
3358
+ t("secret.backend_plaintext_forced", { envVar: BACKEND_ENV_VAR })
3359
+ );
2954
3360
  }
2955
3361
  const reason = await tryBackends(homeDir, forced);
2956
3362
  if (typeof reason !== "string") return reason;
@@ -2968,14 +3374,18 @@ async function tryBackends(homeDir, forced) {
2968
3374
  if (want("dpapi") && platform() === "win32") {
2969
3375
  const { shell, reason } = await probeDpapi();
2970
3376
  if (shell) return new DpapiStore(dpapiStorePath(homeDir), shell);
2971
- return reason ?? "DPAPI \u4E0D\u53EF\u7528";
3377
+ return reason ?? t("secret.dpapi_unavailable");
2972
3378
  }
2973
3379
  if (want("libsecret") && platform() !== "darwin" && platform() !== "win32") {
2974
3380
  const why = await probeLibsecret(service);
2975
3381
  if (why === null) return new LibsecretStore(catalog, service);
2976
3382
  return why;
2977
3383
  }
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`;
3384
+ return forced === null ? t("secret.no_backend", { platform: platform() }) : t("secret.forced_unavailable", {
3385
+ envVar: BACKEND_ENV_VAR,
3386
+ forced,
3387
+ platform: platform()
3388
+ });
2979
3389
  }
2980
3390
  function readForcedBackend() {
2981
3391
  const raw = process.env[BACKEND_ENV_VAR]?.trim().toLowerCase();
@@ -2988,7 +3398,7 @@ async function prefetchProviderSecrets(store, names) {
2988
3398
  try {
2989
3399
  stored = await store.list();
2990
3400
  } catch (err) {
2991
- return { values: {}, warning: `\u5217\u4E3E\u51ED\u636E\u5931\u8D25\uFF08${message2(err)}\uFF09` };
3401
+ return { values: {}, warning: t("secret.list_failed", { detail: message2(err) }) };
2992
3402
  }
2993
3403
  const wanted = stored.filter((n) => names.includes(n));
2994
3404
  const values2 = {};
@@ -2999,7 +3409,7 @@ async function prefetchProviderSecrets(store, names) {
2999
3409
  if (value) values2[name] = value;
3000
3410
  else missing.push(name);
3001
3411
  } catch (err) {
3002
- return { values: values2, warning: `\u8BFB\u53D6 ${name} \u5931\u8D25\uFF08${message2(err)}\uFF09` };
3412
+ return { values: values2, warning: t("secret.read_failed", { name, detail: message2(err) }) };
3003
3413
  }
3004
3414
  }
3005
3415
  if (missing.length > 0) {
@@ -3096,4 +3506,4 @@ function message2(err) {
3096
3506
  * Modifications Copyright 2024-2026 bowen
3097
3507
  */
3098
3508
 
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 };
3509
+ export { CLAUDE_DIR_NAME, CODE_EXEC_ROOTS, DEFAULT_LANG, DEFAULT_YAML, DENIAL_SIGNATURES, ENVELOPE_PREFIX, EXEC_PATH_AS_NODE_ENV, IS_WINDOWS, LANGS, PROJECT_DIR_NAME, PlaintextStore, RUNNER_FAILURE_RULES, SECRET_MODE, SECRET_SERVICE, SHELL_KINDS, SIGKILL_TIMEOUT_MS, SecretCatalog, TOOLCHAIN_CACHE_DIRS, WINDOWS_HIDE_FLAGS, agentsDir, allJobs, appendJob, approvalsPath, artifactsDir, assertValidSecretName, automationDir, automationLogsDir, automationWorkDir, budgetStatePath, buildBwrapArgs, buildProfile, checkDangerousCommand, checkObfuscation, checkpointsDir, classifyFailure, claudeUserSettingsPath, clearAllJobs, closeAllDatabases, collectProcessTree, collectProcessTreeStamped, commandsDir, configPath, confine, countCodePoints, createLogger, createProcessTable, createSecretStore, createStreamDecoder, currentLang, dbPath, detectBackend, detectConsoleEncoding, encodingForCodePage, envPath, formatIssues, generateDataKey, getDataKey, getDefaultShell, getJob, getPythonCommand, getSecretStore, getSecretValues, hasSideEffectChannel, headCodePoints, hooksPath, i18nDiagnostics, isEnvelope, isInWorkspace, isLang, isReadOnlyCommand, isSensitiveKey, isolate, issueDetails, jobDetail, keepHeadAndTail, keybindingsPath, killAllTrackedProcesses, killPids, killProcessTree, killStampedPids, killTrackedProcess, listJobs, 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, processFallbackTable, processTableFor, projectClaudeLocalSettingsPath, projectClaudeSettingsPath, projectCommandsDir, projectHooksPath, projectLocalSettingsPath, projectPoliciesDir, projectSchemasDir, projectSettingsPath, projectSkillsDir, readConfigYaml, readJob, readKey, refCount, registerJob, rekeyJobs, releaseDatabase, removeEnvVarText, resetI18n, resetRipgrepCache, resetSecretState, resolveHomeDir, resolveLang, resolveProfile, resolveRipgrep, resolveShellKind, ripgrepInstallHint, safeInit, sandboxModeForLevel, schemaVersion, sealEnvelope, setDataKey, setEnvVarText, setLang, setLogLevel, setScalar, setSecretStore, setSecretValues, setSectionField, setShell, shellPtyArgs, shellSpawnArgs, skillsDir, splitWords, startLongLivedProcess, stopJob, stripShellWrapper, systemLocaleSignals, t, tailCodePoints, touchJob, trackForeignProcess, trackedProcessCount, trustPath, trustedImportsPath, uiDateLocale, unknownKeyIssues, unsetScalar, unsetSectionField, waitJob, withTimeout, workspacesPath, worktreesDir, writeArtifact, writeConfigYaml, writeEnvFile };