@epoch-agent/infra 0.1.0 → 0.3.1

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