dsh-composer-ux 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -419,8 +419,8 @@ for (const key of [
419
419
  return schema;
420
420
  } });
421
421
  var resolvers = {};
422
- Schema.extend = function extend(type, resolve2) {
423
- resolvers[type] = resolve2;
422
+ Schema.extend = function extend(type, resolve3) {
423
+ resolvers[type] = resolve3;
424
424
  };
425
425
  Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
426
426
  if (!schema) return [data];
@@ -786,9 +786,253 @@ defineMethod("transform", [
786
786
  "preserve"
787
787
  ], ({ inner }, isInner) => inner.toString(isInner));
788
788
 
789
+ // src/host.ts
790
+ import { spawn } from "node:child_process";
791
+ import { existsSync as existsSync2, readFileSync } from "node:fs";
792
+ import { tmpdir } from "node:os";
793
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
794
+ import inspector from "node:inspector";
795
+
796
+ // src/terminal/discover.ts
797
+ var WSL_REASON = "WSL \u7684 bash\uFF1A\u5B83\u6309 Linux \u89C4\u5219\u89E3\u91CA\u8DEF\u5F84\uFF08D:\\x \u8981\u5199\u6210 /mnt/d/x\uFF09\uFF0C\u4E0E\u6A21\u578B\u624B\u91CC\u7684 Windows \u5DE5\u4F5C\u76EE\u5F55\u4E0D\u517C\u5BB9\uFF0C\u6545\u4E0D\u91C7\u7528";
798
+ function normalizePath(value) {
799
+ const trimmed = value.trim().replace(/^"|"$/g, "");
800
+ const slashed = trimmed.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
801
+ return slashed.length > 1 ? slashed.replace(/\/+$/, "") : slashed;
802
+ }
803
+ function isWslBash(value) {
804
+ const path = normalizePath(value).toLowerCase();
805
+ return /\/windows\/system32\/bash\.exe$/.test(path) || /\/windowsapps\/bash\.exe$/.test(path);
806
+ }
807
+ function candidateKindOf(value) {
808
+ const path = normalizePath(value).toLowerCase();
809
+ if (path.endsWith("/niu.exe") || path.includes("/niubash/")) return "niubash";
810
+ if (path.includes("/msys64/") || path.includes("/msys32/") || path.includes("/msys2/")) return "msys2";
811
+ if (path.includes("/cygwin")) return "cygwin";
812
+ if (/(^|\/)(portable)?git(\/|$)/.test(path)) return "git";
813
+ return "path";
814
+ }
815
+ function kindLabel(kind) {
816
+ switch (kind) {
817
+ case "git":
818
+ return "Git for Windows";
819
+ case "msys2":
820
+ return "MSYS2";
821
+ case "cygwin":
822
+ return "Cygwin";
823
+ case "niubash":
824
+ return "Niubash\uFF08\u725B bash\uFF09";
825
+ default:
826
+ return "PATH";
827
+ }
828
+ }
829
+ var KIND_RANK = { git: 0, msys2: 1, cygwin: 2, niubash: 3, path: 4 };
830
+ var DRIVE_LETTERS = "CDEFGHIJKLMNOPQRSTUVWXYZ";
831
+ function gitRootOf(entry) {
832
+ const path = normalizePath(entry);
833
+ if (path === "") return void 0;
834
+ const lower = path.toLowerCase();
835
+ for (const suffix of ["/mingw64/bin", "/mingw32/bin", "/usr/bin", "/cmd", "/bin"]) {
836
+ if (lower.endsWith(suffix)) return path.slice(0, path.length - suffix.length);
837
+ }
838
+ return path;
839
+ }
840
+ function bashPathsUnder(root) {
841
+ return [`${root}/bin/bash.exe`, `${root}/usr/bin/bash.exe`, `${root}/mingw64/bin/bash.exe`];
842
+ }
843
+ function unpreparedBashRoot(value) {
844
+ const match = /^(.*)\/(usr|mingw32|mingw64)\/bin\/bash\.exe$/i.exec(normalizePath(value));
845
+ return match?.[1];
846
+ }
847
+ function discoverBashCandidates(input) {
848
+ const seen = /* @__PURE__ */ new Set();
849
+ const excluded = [];
850
+ const ordered = [];
851
+ const dirsOf = (parent) => {
852
+ if (input.listDirs === void 0) return [];
853
+ try {
854
+ return input.listDirs(parent);
855
+ } catch {
856
+ return [];
857
+ }
858
+ };
859
+ const consider = (raw, label, isExplicit = false) => {
860
+ const path = normalizePath(raw);
861
+ if (path === "") return;
862
+ if (isWslBash(path)) {
863
+ if (!input.exists(path)) return;
864
+ if (!excluded.some((item) => item.path.toLowerCase() === path.toLowerCase())) {
865
+ excluded.push({ path, reason: WSL_REASON });
866
+ }
867
+ return;
868
+ }
869
+ if (!isExplicit) {
870
+ const root = unpreparedBashRoot(path);
871
+ if (root !== void 0 && input.exists(`${root}/bin/bash.exe`)) return;
872
+ }
873
+ const key = path.toLowerCase();
874
+ if (seen.has(key)) return;
875
+ if (!input.exists(path)) return;
876
+ seen.add(key);
877
+ const kind = candidateKindOf(path);
878
+ ordered.push({ path, kind, label: label ?? kindLabel(kind), explicit: isExplicit });
879
+ };
880
+ const addGroup = (group) => {
881
+ for (const root of group.roots) {
882
+ if (root === "" || !input.exists(root)) continue;
883
+ for (const path of bashPathsUnder(root)) consider(path, group.label);
884
+ }
885
+ };
886
+ const explicitRaw = normalizePath(input.explicitPath ?? "");
887
+ let explicit;
888
+ if (explicitRaw !== "") {
889
+ if (isWslBash(explicitRaw)) {
890
+ explicit = { path: explicitRaw, state: "wsl" };
891
+ if (input.exists(explicitRaw)) excluded.push({ path: explicitRaw, reason: WSL_REASON });
892
+ } else if (input.exists(explicitRaw)) explicit = { path: explicitRaw, state: "ok" };
893
+ else explicit = { path: explicitRaw, state: "missing" };
894
+ }
895
+ const entries = input.pathEntries.map((entry) => normalizePath(entry)).filter((entry) => entry !== "");
896
+ const env = (name2) => {
897
+ const value = input.env(name2);
898
+ return value === void 0 || value === "" ? void 0 : normalizePath(value);
899
+ };
900
+ const programFiles = env("ProgramFiles") ?? "C:/Program Files";
901
+ const programFilesX86 = env("ProgramFiles(x86)") ?? "C:/Program Files (x86)";
902
+ const programW6432 = env("ProgramW6432");
903
+ const localAppData = env("LOCALAPPDATA");
904
+ const home = env("USERPROFILE") ?? env("HOME");
905
+ const scoop = env("SCOOP") ?? (home === void 0 ? void 0 : `${home}/scoop`);
906
+ const choco = env("ChocolateyInstall") ?? (env("ProgramData") === void 0 ? "C:/ProgramData/chocolatey" : `${env("ProgramData")}/chocolatey`);
907
+ const versioned = [];
908
+ if (localAppData !== void 0) {
909
+ const desktop = `${localAppData}/GitHubDesktop`;
910
+ for (const dir of [...dirsOf(desktop)].sort().reverse()) {
911
+ if (dir.startsWith("app-")) versioned.push(`${desktop}/${dir}/resources/app/git`);
912
+ }
913
+ const github = `${localAppData}/GitHub`;
914
+ for (const dir of [...dirsOf(github)].sort().reverse()) {
915
+ if (dir.startsWith("PortableGit")) versioned.push(`${github}/${dir}`);
916
+ }
917
+ }
918
+ const vsRoot = `${programFiles}/Microsoft Visual Studio`;
919
+ for (const year of dirsOf(vsRoot)) {
920
+ const yearRoot = `${vsRoot}/${year}`;
921
+ for (const edition of dirsOf(yearRoot)) {
922
+ versioned.push(`${yearRoot}/${edition}/Common7/IDE/CommonExtensions/Microsoft/TeamFoundation/Team Explorer/Git`);
923
+ }
924
+ }
925
+ const drives = [];
926
+ for (const letter of DRIVE_LETTERS) {
927
+ const root = `${letter}:`;
928
+ if (input.exists(`${root}/`)) drives.push(root);
929
+ }
930
+ const driveRoots = (names) => drives.flatMap((root) => names.map((name2) => `${root}/${name2}`));
931
+ const groups = [
932
+ // 2) PATH 上认出来的 git 根 —— 那才是用户实际在用的那份。
933
+ ...entries.map((entry) => gitRootOf(entry)).filter((root) => root !== void 0).map((root) => ({ roots: [root] })),
934
+ // 3) 官方安装器的两个默认落点 + 32 位 / 重定向后的 Program Files。
935
+ { roots: [programFiles, programFilesX86, programW6432, localAppData === void 0 ? void 0 : `${localAppData}/Programs`].filter((base) => base !== void 0).map((base) => `${base}/Git`) },
936
+ // 4) Scoop(`scoop install git` 会带一份 bash)。
937
+ ...scoop === void 0 ? [] : [{ roots: [`${scoop}/apps/git/current`, `${scoop}/apps/git`], label: "Scoop \u7684 git" }],
938
+ // 5) Chocolatey 的便携包(`git.install` 装的仍在 Program Files,上面那条已覆盖)。
939
+ { roots: [`${choco}/lib/git.portable/tools`, `${choco}/lib/git.portable/tools/git`], label: "Chocolatey \u4FBF\u643A\u5305" },
940
+ // 6) GitHub Desktop / 旧 GitHub for Windows / Visual Studio 自带(都带版本号,靠 listDirs)。
941
+ { roots: versioned.filter((path) => path.includes("GitHubDesktop")), label: "GitHub Desktop \u5185\u5D4C" },
942
+ { roots: versioned.filter((path) => path.includes("/GitHub/PortableGit")), label: "GitHub PortableGit" },
943
+ { roots: versioned.filter((path) => path.includes("/Microsoft Visual Studio/")), label: "Visual Studio \u5185\u5D4C" },
944
+ // 7) MSYS2 / Cygwin 的常见位置(含各盘符根下的同名目录)。
945
+ { roots: [`${programFiles}/msys64`, "C:/msys64", ...driveRoots(["msys64"])], label: "MSYS2" },
946
+ { roots: [`${programFiles}/cygwin64`, "C:/cygwin64", "C:/cygwin", ...driveRoots(["cygwin64", "cygwin"])], label: "Cygwin" },
947
+ // 8) 各盘符根下的 Git / PortableGit(装在 D:\Git、D:\PortableGit 这类)。
948
+ { roots: driveRoots(["Git", "Program Files/Git", "Program Files (x86)/Git"]), label: "Git for Windows" },
949
+ { roots: driveRoots(["PortableGit"]), label: "PortableGit\uFF08\u81EA\u89E3\u538B\uFF09" }
950
+ ];
951
+ for (const group of groups) addGroup(group);
952
+ if (localAppData !== void 0) consider(`${localAppData}/Programs/Niubash/niu.exe`, "Niubash\uFF08\u725B bash\uFF09");
953
+ for (const entry of entries) consider(`${entry}/bash.exe`);
954
+ if (explicit?.state === "ok") consider(explicit.path, void 0, true);
955
+ const ranked = ordered.map((candidate, index) => ({ candidate, index })).sort((a, b) => {
956
+ if (a.candidate.explicit !== b.candidate.explicit) return a.candidate.explicit ? -1 : 1;
957
+ const rank = KIND_RANK[a.candidate.kind] - KIND_RANK[b.candidate.kind];
958
+ return rank !== 0 ? rank : a.index - b.index;
959
+ }).map((item) => item.candidate);
960
+ return {
961
+ candidates: ranked,
962
+ excluded,
963
+ ...explicit === void 0 ? {} : { explicit }
964
+ };
965
+ }
966
+ function defaultBashPath(result) {
967
+ return result.candidates[0]?.path ?? "";
968
+ }
969
+
970
+ // src/terminal/contracts.ts
971
+ var TERMINAL_MODE_FIELD = "terminalMode";
972
+ var TERMINAL_BASH_PATH_FIELD = "terminalBashPath";
973
+ var TERMINAL_CANDIDATES_FIELD = "terminalCandidates";
974
+ var TERMINAL_STATUS_FIELD = "terminalStatus";
975
+ var TERMINAL_EFFECTIVE_FIELD = "terminalEffective";
976
+ var DEFAULT_TERMINAL_MODE = "auto";
977
+ function terminalModeFrom(value) {
978
+ return value === "auto" || value === "gitbash" || value === "pwsh" ? value : DEFAULT_TERMINAL_MODE;
979
+ }
980
+ function candidatesToStored(candidates) {
981
+ return candidates.map((candidate) => ({
982
+ path: candidate.path,
983
+ label: candidate.label,
984
+ kind: candidate.kind,
985
+ explicit: candidate.explicit
986
+ }));
987
+ }
988
+ function activeBashPath(mode, bashPath, candidates, explicitState) {
989
+ if (mode === "pwsh") return "";
990
+ const explicit = normalizePath(bashPath);
991
+ if (explicit !== "" && (explicitState === void 0 || explicitState.state === "ok")) return explicit;
992
+ return defaultBashPath({ candidates, excluded: [] });
993
+ }
994
+ function probeBash(explicitPath, pathValue, env, exists, listDirs) {
995
+ return discoverBashCandidates({
996
+ exists,
997
+ pathEntries: pathValue.split(";"),
998
+ env,
999
+ explicitPath,
1000
+ ...listDirs === void 0 ? {} : { listDirs }
1001
+ });
1002
+ }
1003
+ function terminalStatusText(input) {
1004
+ const text = (() => {
1005
+ if (input.platform !== "win32") {
1006
+ return `\u672C\u63D2\u4EF6\u53EA\u5728 Windows \u4E0A\u63A5\u7BA1\u7EC8\u7AEF\uFF08\u5F53\u524D\u5E73\u53F0 ${input.platform}\uFF09\uFF0C\u6B64\u5904\u4E0D\u751F\u6548`;
1007
+ }
1008
+ if (input.mode === "pwsh") {
1009
+ return "\u4FDD\u6301 DSH \u9ED8\u8BA4\uFF08PowerShell\uFF09\u2014\u2014\u672C\u63D2\u4EF6\u4E0D\u4ECB\u5165\u7EC8\u7AEF\u5DE5\u5177\u9762";
1010
+ }
1011
+ if (input.effective === "bash") {
1012
+ const path = input.effectivePath ?? "";
1013
+ const candidate = input.candidates.find((item) => item.path === path);
1014
+ const suffix = input.explicit?.state === "missing" ? `\uFF08\u4F60\u586B\u7684\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${input.explicit.path}\uFF09` : input.explicit?.state === "wsl" ? `\uFF08\u4F60\u586B\u7684\u662F WSL \u7684 bash\uFF1A${input.explicit.path}\uFF09` : "";
1015
+ return `\u5DF2\u751F\u6548\uFF1A${path}${candidate === void 0 ? "" : `\uFF08${candidate.label}\uFF09`}${suffix}`;
1016
+ }
1017
+ if (input.candidates.length === 0) {
1018
+ const excluded = input.excludedCount > 0 ? `\uFF08\u5DF2\u6392\u9664 ${input.excludedCount} \u4E2A WSL \u7684 bash.exe\uFF09` : "";
1019
+ return `\u6CA1\u627E\u5230\u53EF\u7528\u7684 bash${excluded}\uFF0C\u6682\u65F6\u4FDD\u6301 PowerShell`;
1020
+ }
1021
+ return `\u5019\u9009\u4E0D\u53EF\u7528\uFF0C\u6682\u65F6\u4FDD\u6301 PowerShell`;
1022
+ })();
1023
+ const failure = input.failure === void 0 || input.failure === "" ? "" : `\uFF1B\u4E0B\u53D1\u5931\u8D25\uFF1A${input.failure}`;
1024
+ return `${text}${failure}`.slice(0, 400);
1025
+ }
1026
+
789
1027
  // src/settings-contract.ts
790
1028
  var NAMESPACE = "composer-ux";
1029
+ var RESTART_API_PATH = "/composer-ux/restart";
791
1030
  var ENABLED_FIELD = "enabled";
1031
+ var KEYS_ENABLED_FIELD = "keysEnabled";
1032
+ var MENU_ENABLED_FIELD = "menuEnabled";
1033
+ var QUICK_ENABLED_FIELD = "quickEnabled";
1034
+ var PANEL_ENABLED_FIELD = "panelEnabled";
1035
+ var TERMINAL_ENABLED_FIELD = "terminalEnabled";
792
1036
  var SEND_KEY_FIELD = "sendKey";
793
1037
  var NEWLINE_KEY_FIELD = "newlineKey";
794
1038
  var PANEL_SCROLL_FIELD = "panelScroll";
@@ -804,7 +1048,14 @@ var MENU_FIELDS = [
804
1048
  "menuDelete",
805
1049
  "menuSelectAll"
806
1050
  ];
1051
+ var MENU_MODE_FIELD = "menuMode";
807
1052
  var MENU_NATIVE_FIELD = "menuNative";
1053
+ function menuModeFrom(storedMode, legacyNative) {
1054
+ if (storedMode === "official" || storedMode === "browser" || storedMode === "custom") return storedMode;
1055
+ if (legacyNative === true) return "browser";
1056
+ if (legacyNative === false) return "custom";
1057
+ return DEFAULT_SETTINGS.menuMode;
1058
+ }
808
1059
  var QUICK_PROMPTS_FIELD = "quickPrompts";
809
1060
  var OPTIMIZER_TIER_FIELD = "optimizerTier";
810
1061
  var QUICK_PROMPT_MAX = 60;
@@ -820,15 +1071,15 @@ var OPTIMIZE_OUTPUT_MAX = 12e3;
820
1071
  var OPTIMIZER_API_PATH = "/composer-ux/optimize";
821
1072
  var DEFAULT_OPTIMIZER_TIER = "advanced";
822
1073
  var DEFAULT_QUICK_PROMPTS = [
823
- { id: "builtin-1", label: "\u4E00\u95EE\u4E00\u7B54", prompt: "\u4F60\u4E0D\u61C2\u7684\u5C31\u95EE\u6211\uFF0C\u4E00\u95EE\u4E00\u7B54\uFF1B\u540C\u65F6\u8BF4\u6E05\u695A\u4F60\u4E3A\u4EC0\u4E48\u8981\u95EE\u8BE5\u95EE\u9898\uFF1B\u76F4\u5230\u4F60\u5BF9\u6211\u7684\u76EE\u6807\u6709\u660E\u786E\u8BA4\u77E5\u540E\u518D\u5F00\u59CB\u5E72\u6D3B\u3002", always: false },
824
- { id: "builtin-2", label: "\u4EA4\u63A5\u6587\u6863", prompt: "\u628A\u8FD9\u6B21\u4EFB\u52A1\u3001\u5DF2\u5B8C\u6210\u5185\u5BB9\u3001\u5F53\u524D\u5361\u70B9\u3001\u4E0B\u4E00\u6B65\u8BA1\u5212\u3001\u8E29\u8FC7\u7684\u5751\uFF0C\u6574\u7406\u6210\u4E00\u4EFD\u4EA4\u63A5\u6587\u6863\uFF0C\u5199\u7ED9\u65B0\u4F1A\u8BDD\u770B\u3002", always: false },
825
- { id: "builtin-3", label: "\u4EC5\u8BF4\u660E\u539F\u56E0", prompt: "\u4EC5\u8BF4\u660E\u539F\u56E0\uFF0C\u4E0D\u8981\u505A\u5176\u4ED6\u52A8\u4F5C\u3002", always: false },
826
- { id: "builtin-4", label: "\u5206\u6790\u540E\u76F4\u63A5\u5E72", prompt: "\u5206\u6790\u539F\u56E0\uFF0C\u7136\u540E\u76F4\u63A5\u5F00\u59CB\u5E72\u6D3B\uFF0C\u4E0D\u9700\u8981\u8FC7\u95EE\u6211\u3002", always: false },
827
- { id: "builtin-5", label: "\u63D0\u4EA4\u4EE3\u7801", prompt: "\u8BF7\u5E2E\u6211\u63D0\u4EA4\u4EE3\u7801\uFF1A\u68C0\u67E5\u5F53\u524D git \u53D8\u66F4\uFF0C\u751F\u6210\u89C4\u8303\u7684 commit message \u5E76\u6267\u884C\u63D0\u4EA4\u3002", always: false },
828
- { id: "builtin-6", label: "\u7ED9\u65B9\u6848", prompt: "\u8BF7\u9488\u5BF9\u4E0A\u9762\u7684\u95EE\u9898\u7ED9\u51FA\u4E00\u4E2A\u5B8C\u6574\u65B9\u6848\uFF0C\u5305\u62EC\u601D\u8DEF\u3001\u6B65\u9AA4\u3001\u6CE8\u610F\u4E8B\u9879\u548C\u98CE\u9669\u3002", always: false },
829
- { id: "builtin-7", label: "\u89E3\u91CA\u4EE3\u7801", prompt: "\u8BF7\u89E3\u91CA\u8FD9\u6BB5\u4EE3\u7801\u7684\u4F5C\u7528\u548C\u5B9E\u73B0\u601D\u8DEF\u3002", always: false },
830
- { id: "builtin-8", label: "\u5199\u6D4B\u8BD5", prompt: "\u8BF7\u4E3A\u4E0B\u9762\u7684\u4EE3\u7801\u7F16\u5199\u5355\u5143\u6D4B\u8BD5\u3002", always: false },
831
- { id: "builtin-9", label: "\u4EE3\u7801\u5BA1\u67E5", prompt: "\u8BF7\u5BF9\u4E0B\u9762\u7684\u4EE3\u7801\u8FDB\u884C\u4EE3\u7801\u5BA1\u67E5\uFF0C\u6307\u51FA\u95EE\u9898\u5E76\u7ED9\u51FA\u6539\u8FDB\u5EFA\u8BAE\u3002", always: false }
1074
+ { id: "builtin-1", label: "\u4E00\u95EE\u4E00\u7B54", prompt: "\u4F60\u4E0D\u61C2\u7684\u5C31\u95EE\u6211\uFF0C\u4E00\u95EE\u4E00\u7B54\uFF1B\u540C\u65F6\u8BF4\u6E05\u695A\u4F60\u4E3A\u4EC0\u4E48\u8981\u95EE\u8BE5\u95EE\u9898\uFF1B\u76F4\u5230\u4F60\u5BF9\u6211\u7684\u76EE\u6807\u6709\u660E\u786E\u8BA4\u77E5\u540E\u518D\u5F00\u59CB\u5E72\u6D3B\u3002", always: false, firstOnly: false },
1075
+ { id: "builtin-2", label: "\u4EA4\u63A5\u6587\u6863", prompt: "\u628A\u8FD9\u6B21\u4EFB\u52A1\u3001\u5DF2\u5B8C\u6210\u5185\u5BB9\u3001\u5F53\u524D\u5361\u70B9\u3001\u4E0B\u4E00\u6B65\u8BA1\u5212\u3001\u8E29\u8FC7\u7684\u5751\uFF0C\u6574\u7406\u6210\u4E00\u4EFD\u4EA4\u63A5\u6587\u6863\uFF0C\u5199\u7ED9\u65B0\u4F1A\u8BDD\u770B\u3002", always: false, firstOnly: false },
1076
+ { id: "builtin-3", label: "\u4EC5\u8BF4\u660E\u539F\u56E0", prompt: "\u4EC5\u8BF4\u660E\u539F\u56E0\uFF0C\u4E0D\u8981\u505A\u5176\u4ED6\u52A8\u4F5C\u3002", always: false, firstOnly: false },
1077
+ { id: "builtin-4", label: "\u5206\u6790\u540E\u76F4\u63A5\u5E72", prompt: "\u5206\u6790\u539F\u56E0\uFF0C\u7136\u540E\u76F4\u63A5\u5F00\u59CB\u5E72\u6D3B\uFF0C\u4E0D\u9700\u8981\u8FC7\u95EE\u6211\u3002", always: false, firstOnly: false },
1078
+ { id: "builtin-5", label: "\u63D0\u4EA4\u4EE3\u7801", prompt: "\u8BF7\u5E2E\u6211\u63D0\u4EA4\u4EE3\u7801\uFF1A\u68C0\u67E5\u5F53\u524D git \u53D8\u66F4\uFF0C\u751F\u6210\u89C4\u8303\u7684 commit message \u5E76\u6267\u884C\u63D0\u4EA4\u3002", always: false, firstOnly: false },
1079
+ { id: "builtin-6", label: "\u7ED9\u65B9\u6848", prompt: "\u8BF7\u9488\u5BF9\u4E0A\u9762\u7684\u95EE\u9898\u7ED9\u51FA\u4E00\u4E2A\u5B8C\u6574\u65B9\u6848\uFF0C\u5305\u62EC\u601D\u8DEF\u3001\u6B65\u9AA4\u3001\u6CE8\u610F\u4E8B\u9879\u548C\u98CE\u9669\u3002", always: false, firstOnly: false },
1080
+ { id: "builtin-7", label: "\u89E3\u91CA\u4EE3\u7801", prompt: "\u8BF7\u89E3\u91CA\u8FD9\u6BB5\u4EE3\u7801\u7684\u4F5C\u7528\u548C\u5B9E\u73B0\u601D\u8DEF\u3002", always: false, firstOnly: false },
1081
+ { id: "builtin-8", label: "\u5199\u6D4B\u8BD5", prompt: "\u8BF7\u4E3A\u4E0B\u9762\u7684\u4EE3\u7801\u7F16\u5199\u5355\u5143\u6D4B\u8BD5\u3002", always: false, firstOnly: false },
1082
+ { id: "builtin-9", label: "\u4EE3\u7801\u5BA1\u67E5", prompt: "\u8BF7\u5BF9\u4E0B\u9762\u7684\u4EE3\u7801\u8FDB\u884C\u4EE3\u7801\u5BA1\u67E5\uFF0C\u6307\u51FA\u95EE\u9898\u5E76\u7ED9\u51FA\u6539\u8FDB\u5EFA\u8BAE\u3002", always: false, firstOnly: false }
832
1083
  ];
833
1084
  function newQuickPromptId() {
834
1085
  return `qp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
@@ -855,7 +1106,13 @@ function asPromptRow(value) {
855
1106
  const id = typeof row.id === "string" && row.id !== "" ? row.id.slice(0, 64) : newQuickPromptId();
856
1107
  const order = typeof row.order === "number" && Number.isFinite(row.order) ? Math.max(1, Math.round(row.order)) : void 0;
857
1108
  return {
858
- prompt: { id, label: label === "" ? prompt.slice(0, 12) : label, prompt, always: row.always === true || row.autoSend === true },
1109
+ prompt: {
1110
+ id,
1111
+ label: label === "" ? prompt.slice(0, 12) : label,
1112
+ prompt,
1113
+ always: row.always === true || row.autoSend === true,
1114
+ firstOnly: row.firstOnly === true || row.autoSendFirst === true
1115
+ },
859
1116
  order
860
1117
  };
861
1118
  }
@@ -918,6 +1175,8 @@ function bookToFile(book) {
918
1175
  title: prompt.label,
919
1176
  text: prompt.prompt,
920
1177
  autoSend: prompt.always,
1178
+ // 「仅首次」是我们加的键,只在为真时写出来:文件形状尽量贴近参考实现。
1179
+ ...prompt.firstOnly ? { autoSendFirst: true } : {},
921
1180
  order: index + 1
922
1181
  }))
923
1182
  }))
@@ -937,7 +1196,14 @@ var DEFAULT_HEADER_NAME = "x-opencode-session";
937
1196
  var HEADER_VALUE_MAX = 200;
938
1197
  var HEADER_NAME_MAX = 64;
939
1198
  var DEFAULT_SETTINGS = {
1199
+ // 总开关默认开:它只是总闸,真正"要不要用这一栏"由下面五个开关决定(默认关)。
940
1200
  enabled: true,
1201
+ // 五栏默认关(老文档由 sectionEnabledOf 迁移成"碰过就开")。
1202
+ keysEnabled: false,
1203
+ menuEnabled: false,
1204
+ quickEnabled: false,
1205
+ panelEnabled: false,
1206
+ terminalEnabled: false,
941
1207
  sendKey: "Enter",
942
1208
  newlineKey: "Shift+Enter",
943
1209
  menuUndo: true,
@@ -947,7 +1213,7 @@ var DEFAULT_SETTINGS = {
947
1213
  menuPaste: true,
948
1214
  menuDelete: true,
949
1215
  menuSelectAll: true,
950
- menuNative: false,
1216
+ menuMode: "official",
951
1217
  panelScroll: true,
952
1218
  panelResize: true,
953
1219
  headerEnabled: false,
@@ -958,7 +1224,12 @@ var DEFAULT_SETTINGS = {
958
1224
  headerAppliedValue: "",
959
1225
  headerStatus: "",
960
1226
  quickPrompts: DEFAULT_QUICK_PROMPTS,
961
- optimizerTier: DEFAULT_OPTIMIZER_TIER
1227
+ optimizerTier: DEFAULT_OPTIMIZER_TIER,
1228
+ terminalMode: DEFAULT_TERMINAL_MODE,
1229
+ terminalBashPath: "",
1230
+ terminalCandidates: [],
1231
+ terminalStatus: "",
1232
+ terminalEffective: ""
962
1233
  };
963
1234
  function newSessionId() {
964
1235
  const bag = globalThis;
@@ -973,6 +1244,950 @@ function parseRouteList(text) {
973
1244
  const parts = text.split(/[\s,,、]+/).map((item) => item.trim()).filter((item) => item !== "");
974
1245
  return [...new Set(parts)];
975
1246
  }
1247
+ var SECTION_SIGNALS = {
1248
+ [KEYS_ENABLED_FIELD]: (source) => touched(source, SEND_KEY_FIELD, DEFAULT_SETTINGS.sendKey) || touched(source, NEWLINE_KEY_FIELD, DEFAULT_SETTINGS.newlineKey),
1249
+ // 「官方」=本插件不介入,等价于关;所以只有选过另外两档才算"碰过"。
1250
+ // (`menuModeFrom` 对"两个键都没有"给的就是 'official',天然满足"缺省 = 默认"。)
1251
+ [MENU_ENABLED_FIELD]: (source) => menuModeFrom(source[MENU_MODE_FIELD], source[MENU_NATIVE_FIELD]) !== "official",
1252
+ [QUICK_ENABLED_FIELD]: (source) => Array.isArray(source[QUICK_PROMPTS_FIELD]) && source[QUICK_PROMPTS_FIELD].length !== DEFAULT_QUICK_PROMPTS.length || touched(source, OPTIMIZER_TIER_FIELD, DEFAULT_OPTIMIZER_TIER),
1253
+ [PANEL_ENABLED_FIELD]: (source) => touched(source, PANEL_SCROLL_FIELD, true) || touched(source, PANEL_RESIZE_FIELD, true) || source[PANEL_WIDTH_FIELD] !== void 0 || source[PANEL_HEIGHT_FIELD] !== void 0,
1254
+ [TERMINAL_ENABLED_FIELD]: (source) => touched(source, TERMINAL_MODE_FIELD, DEFAULT_TERMINAL_MODE) || touched(source, TERMINAL_BASH_PATH_FIELD, "")
1255
+ };
1256
+ function touched(source, field, untouched) {
1257
+ const value = source[field];
1258
+ return value !== void 0 && value !== untouched;
1259
+ }
1260
+ function sectionEnabledOf(field, source) {
1261
+ const explicit = source[field];
1262
+ if (typeof explicit === "boolean") return explicit;
1263
+ const signal = SECTION_SIGNALS[field];
1264
+ return signal === void 0 ? false : signal(source);
1265
+ }
1266
+
1267
+ // src/terminal/host.ts
1268
+ import { existsSync, readdirSync } from "node:fs";
1269
+ import { isAbsolute, sep } from "node:path";
1270
+
1271
+ // src/terminal/sandbox.ts
1272
+ var WIDER_MODES = {
1273
+ "read-only": ["workspace-write", "danger-full-access"],
1274
+ "workspace-write": ["danger-full-access"]
1275
+ };
1276
+ var ESCALATION_TARGETS = ["workspace-write", "danger-full-access"];
1277
+ function sandboxDenialMarker(mode) {
1278
+ return `[sandbox: file access denied under ${mode} mode]`;
1279
+ }
1280
+ function escalationHintMarker(subject) {
1281
+ return `[sandbox: escalation available \u2014 retry this exact ${subject} once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]`;
1282
+ }
1283
+ function validateEscalationArgs(sandboxPermissions, justification) {
1284
+ if (sandboxPermissions !== void 0 && justification === void 0) {
1285
+ throw new Error("invalid escalation: sandbox_permissions requires a justification");
1286
+ }
1287
+ if (justification !== void 0 && sandboxPermissions === void 0) {
1288
+ throw new Error("invalid escalation: justification is only valid together with sandbox_permissions");
1289
+ }
1290
+ if (justification !== void 0 && justification.trim().length === 0) {
1291
+ throw new Error("invalid justification: expected a non-empty sentence");
1292
+ }
1293
+ }
1294
+ async function approveEscalation(request, approval) {
1295
+ const { requestedMode: mode, effectiveMode, justification, subject } = request;
1296
+ if (mode === effectiveMode) return effectiveMode;
1297
+ if (!(WIDER_MODES[effectiveMode] ?? []).includes(mode)) {
1298
+ throw new Error(`sandbox escalation to "${mode}" is not strictly wider than this call's current "${effectiveMode}" mode`);
1299
+ }
1300
+ if (approval.approver === void 0) {
1301
+ throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval service is composed`);
1302
+ }
1303
+ if (approval.agent === void 0) {
1304
+ throw new Error(`sandbox escalation to "${mode}" requires approval, but the call has no agent to route it through`);
1305
+ }
1306
+ const outcome = await approval.approver.request({
1307
+ agent: approval.agent,
1308
+ toolName: approval.toolName,
1309
+ callId: approval.callId,
1310
+ reason: `escalate sandbox to ${mode}: ${justification}`,
1311
+ ...approval.signal === void 0 ? {} : { signal: approval.signal }
1312
+ });
1313
+ switch (outcome) {
1314
+ case "allowed-once":
1315
+ return mode;
1316
+ case "rejected":
1317
+ throw new Error(`the user rejected escalating this ${subject} to "${mode}"`);
1318
+ case "cancelled":
1319
+ throw new Error(`approval for escalating to "${mode}" was cancelled`);
1320
+ case "unavailable":
1321
+ throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval channel is available`);
1322
+ default:
1323
+ throw new Error(`unexpected escalation outcome ${JSON.stringify(outcome)}`);
1324
+ }
1325
+ }
1326
+
1327
+ // src/terminal/render.ts
1328
+ function parseExitStatus(text) {
1329
+ const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text);
1330
+ if (signal?.[1] !== void 0) return { body: text.slice(0, signal.index), signal: signal[1] };
1331
+ const exit = /\n\[exit code: (\d+)\]$/.exec(text);
1332
+ if (exit?.[1] !== void 0) return { body: text.slice(0, exit.index), exitCode: Number(exit[1]) };
1333
+ return { body: text, exitCode: 0 };
1334
+ }
1335
+ function streamText(output) {
1336
+ if (!output.truncated) return output.text;
1337
+ return `${output.text}
1338
+ [output truncated; full output: ${output.spillPath ?? "(unavailable)"}]`;
1339
+ }
1340
+ function renderBashResult(result, escalationModes = []) {
1341
+ const out = streamText(result.stdout);
1342
+ const err = streamText(result.stderr);
1343
+ let body = out;
1344
+ if (err.length > 0) {
1345
+ if (body.length > 0 && !body.endsWith("\n")) body += "\n";
1346
+ body += `[stderr]
1347
+ ${err}`;
1348
+ }
1349
+ if (body.length === 0) body = "(no output)";
1350
+ const markers = [];
1351
+ if (result.sandbox?.denied === true) {
1352
+ markers.push(sandboxDenialMarker(result.sandbox.mode));
1353
+ if (escalationModes.length > 0) markers.push(escalationHintMarker("command"));
1354
+ }
1355
+ if (result.timedOut) markers.push(`[timed out after ${result.timeoutMs}ms]`);
1356
+ if (result.signal !== null) markers.push(`[killed by signal: ${result.signal}]`);
1357
+ else if (result.exitCode !== 0) markers.push(`[exit code: ${result.exitCode}]`);
1358
+ if (markers.length === 0) return body;
1359
+ if (!body.endsWith("\n")) body += "\n";
1360
+ return body + markers.join("\n");
1361
+ }
1362
+ function renderProcessRead(read, sandbox, escalationModes = []) {
1363
+ const notices = [];
1364
+ if (read.lossy) {
1365
+ const paths = [read.stdoutSpillPath, read.stderrSpillPath].filter((path) => path !== void 0);
1366
+ notices.push(`[some output was dropped from memory; full output: ${paths.length > 0 ? paths.join(", ") : "(unavailable)"}]`);
1367
+ }
1368
+ if (sandbox?.runnerFailed === true) {
1369
+ notices.push(`[sandbox: the sandbox runner itself failed under ${sandbox.mode} mode \u2014 the command did not run; this is a sandbox problem, not a command failure]`);
1370
+ } else if (sandbox?.denied === true) {
1371
+ notices.push(sandboxDenialMarker(sandbox.mode));
1372
+ if (escalationModes.length > 0) notices.push(escalationHintMarker("command"));
1373
+ }
1374
+ if (notices.length === 0) return read.delta;
1375
+ const separator = read.delta.length > 0 && !read.delta.endsWith("\n") ? "\n" : "";
1376
+ return `${read.delta}${separator}${notices.join("\n")}`;
1377
+ }
1378
+
1379
+ // src/terminal/tool.ts
1380
+ var BASH_TOOL_NAME = "bash";
1381
+ var TOOL_BASH_SECTION_ORDER = 1e3;
1382
+ var BASH_SECTION_TEXT = "Check the [exit code: N] marker on every bash result; investigate failures before moving on.";
1383
+ var DEFAULT_TIMEOUT_MS = 12e4;
1384
+ var MAX_TIMEOUT_MS = 6e5;
1385
+ var MAX_OUTPUT_BYTES = 64e3;
1386
+ var MAX_SPILL_BYTES = 64 * 1024 * 1024;
1387
+ var GRACE_MS = 3e3;
1388
+ var TOOL_ABORTED = "ABORTED";
1389
+ var ENV_OVERRIDES = {
1390
+ NO_COLOR: "1",
1391
+ TERM: "dumb",
1392
+ PAGER: "cat",
1393
+ GIT_PAGER: "cat"
1394
+ };
1395
+ function escalationModesOf(deps) {
1396
+ return deps.sandbox !== void 0 && deps.sandboxPolicy !== void 0 ? ESCALATION_TARGETS : [];
1397
+ }
1398
+ function bashDescription(backgroundEnabled, escalationModes) {
1399
+ const background = backgroundEnabled ? "Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`." : "Background execution is not available; long-running commands must finish within the timeout.";
1400
+ const base = "Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls \u2014 pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` \u2014 a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. " + background;
1401
+ if (escalationModes.length === 0) return base;
1402
+ return base + " Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn \u2014 the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first \u2014 the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final \u2014 do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial \u2014 normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command \u2014 stop and explain, never work around it \u2014 but it does not forbid attempting or escalating other commands later.";
1403
+ }
1404
+ function bashParameters(escalationModes) {
1405
+ return {
1406
+ type: "object",
1407
+ additionalProperties: false,
1408
+ properties: {
1409
+ command: { type: "string", description: "The bash command to execute." },
1410
+ description: {
1411
+ type: "string",
1412
+ description: 'Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: "ls" \u2192 "List files in current directory"; "git status" \u2192 "Show working tree status"; "npm install" \u2192 "Install package dependencies".'
1413
+ },
1414
+ timeoutMs: {
1415
+ type: "number",
1416
+ description: "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
1417
+ },
1418
+ workdir: {
1419
+ type: "string",
1420
+ description: "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
1421
+ },
1422
+ ...escalationModes.length === 0 ? {} : {
1423
+ sandbox_permissions: {
1424
+ type: "string",
1425
+ enum: [...escalationModes],
1426
+ description: "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval."
1427
+ },
1428
+ justification: {
1429
+ type: "string",
1430
+ description: "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access."
1431
+ }
1432
+ }
1433
+ },
1434
+ required: ["command", "description"]
1435
+ };
1436
+ }
1437
+ function bashOutputSchema() {
1438
+ const stream = {
1439
+ type: "object",
1440
+ additionalProperties: false,
1441
+ required: ["text", "truncated"],
1442
+ properties: {
1443
+ text: { type: "string" },
1444
+ truncated: { type: "boolean" },
1445
+ spillPath: { type: "string" }
1446
+ }
1447
+ };
1448
+ return {
1449
+ oneOf: [
1450
+ {
1451
+ type: "object",
1452
+ additionalProperties: false,
1453
+ required: ["kind", "jobId"],
1454
+ properties: {
1455
+ kind: { type: "string", const: "background" },
1456
+ jobId: { type: "string" }
1457
+ }
1458
+ },
1459
+ {
1460
+ type: "object",
1461
+ additionalProperties: false,
1462
+ required: ["kind", "exitCode", "signal", "timedOut", "aborted", "timeoutMs", "stdout", "stderr"],
1463
+ properties: {
1464
+ kind: { type: "string", const: "foreground" },
1465
+ exitCode: { oneOf: [{ type: "integer" }, { type: "null" }] },
1466
+ signal: { oneOf: [{ type: "string" }, { type: "null" }] },
1467
+ timedOut: { type: "boolean" },
1468
+ aborted: { type: "boolean" },
1469
+ timeoutMs: { type: "number" },
1470
+ stdout: stream,
1471
+ stderr: stream,
1472
+ sandbox: {
1473
+ type: "object",
1474
+ additionalProperties: false,
1475
+ required: ["mode", "denied"],
1476
+ properties: {
1477
+ mode: { type: "string" },
1478
+ denied: { type: "boolean" },
1479
+ enforcement: { type: "string" },
1480
+ runnerFailed: { type: "boolean" }
1481
+ }
1482
+ }
1483
+ }
1484
+ }
1485
+ ]
1486
+ };
1487
+ }
1488
+ function clampTimeout(requested, min, max) {
1489
+ if (requested === void 0 || !Number.isFinite(requested) || requested <= 0) return min;
1490
+ return Math.min(Math.max(requested, min), max);
1491
+ }
1492
+ function resolveWorkdir(modelWorkdir, exec, policyWorkspaceRoot, sep2, isAbsolute2) {
1493
+ const headerCwd = exec.agent?.session.header.cwd;
1494
+ const sessionCwd = policyWorkspaceRoot ?? headerCwd;
1495
+ if (modelWorkdir === void 0) return sessionCwd;
1496
+ if (sessionCwd !== void 0 && !isAbsolute2(modelWorkdir)) return `${sessionCwd}${sep2}${modelWorkdir}`;
1497
+ return modelWorkdir;
1498
+ }
1499
+ function abortedError() {
1500
+ const error = new Error("tool call aborted");
1501
+ error.name = "AbortError";
1502
+ error.code = TOOL_ABORTED;
1503
+ return error;
1504
+ }
1505
+ function createBashTool(deps, options) {
1506
+ const escalationModes = escalationModesOf(deps);
1507
+ const sep2 = options.sep ?? "\\";
1508
+ const isAbsolute2 = options.isAbsolute ?? ((path) => /^([a-zA-Z]:[\\/]|[\\/])/.test(path));
1509
+ const minTimeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1510
+ const maxTimeout = options.maxTimeoutMs ?? MAX_TIMEOUT_MS;
1511
+ const spawnSpec = (argv, cwd, dshEnv, signal) => ({
1512
+ argv,
1513
+ cwd,
1514
+ stdio: {
1515
+ stdin: "ignore",
1516
+ stdout: { maxBytes: MAX_OUTPUT_BYTES, spill: { maxBytes: MAX_SPILL_BYTES } },
1517
+ stderr: { maxBytes: MAX_OUTPUT_BYTES, spill: { maxBytes: MAX_SPILL_BYTES } }
1518
+ },
1519
+ graceMs: GRACE_MS,
1520
+ ...signal === void 0 ? {} : { signal },
1521
+ // dshEnv 必须显式带上:subprocess 会先按凭据形状清洗父环境(含清掉所有 DSH_*)。
1522
+ env: { ...ENV_OVERRIDES, ...dshEnv }
1523
+ });
1524
+ const readFrame = (handle, offsets) => {
1525
+ const outRead = handle.collected.stdout?.readFrom(offsets.stdout);
1526
+ const errRead = handle.collected.stderr?.readFrom(offsets.stderr);
1527
+ offsets.stdout = outRead?.nextOffset ?? offsets.stdout;
1528
+ offsets.stderr = errRead?.nextOffset ?? offsets.stderr;
1529
+ const outText = outRead?.text ?? "";
1530
+ const errText = errRead?.text ?? "";
1531
+ const separator = outText.length > 0 && !outText.endsWith("\n") ? "\n" : "";
1532
+ return {
1533
+ delta: outText + (errText.length > 0 ? `${separator}[stderr]
1534
+ ${errText}` : ""),
1535
+ lossy: (outRead?.lossy ?? false) || (errRead?.lossy ?? false),
1536
+ ...outRead?.spillPath === void 0 ? {} : { stdoutSpillPath: outRead.spillPath },
1537
+ ...errRead?.spillPath === void 0 ? {} : { stderrSpillPath: errRead.spillPath }
1538
+ };
1539
+ };
1540
+ const execute = async (rawArgs, exec) => {
1541
+ const args = rawArgs;
1542
+ if (args.command.trim().length === 0) throw new Error("invalid command: expected a non-empty string");
1543
+ if (args.description.trim().length === 0) throw new Error("invalid description: expected a non-empty string");
1544
+ if (args.timeoutMs !== void 0 && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) {
1545
+ throw new Error(`invalid timeoutMs: expected a positive number, got ${JSON.stringify(args.timeoutMs)}`);
1546
+ }
1547
+ validateEscalationArgs(args.sandbox_permissions, args.justification);
1548
+ const standingPolicy = deps.sandboxPolicy?.resolve(
1549
+ exec.agent === void 0 ? {} : { session: exec.agent.session }
1550
+ );
1551
+ const approvedMode = args.sandbox_permissions !== void 0 && args.justification !== void 0 ? await approveEscalation(
1552
+ {
1553
+ requestedMode: args.sandbox_permissions,
1554
+ justification: args.justification,
1555
+ effectiveMode: standingPolicy.mode,
1556
+ subject: "command"
1557
+ },
1558
+ {
1559
+ approver: deps.approval,
1560
+ agent: exec.agent,
1561
+ callId: exec.callId,
1562
+ toolName: BASH_TOOL_NAME,
1563
+ signal: exec.signal
1564
+ }
1565
+ ) : void 0;
1566
+ const policy = approvedMode === void 0 ? standingPolicy : { ...standingPolicy, mode: approvedMode };
1567
+ const workdir = resolveWorkdir(args.workdir, exec, standingPolicy?.workspaceRoot, sep2, isAbsolute2);
1568
+ const dshEnv = deps.shellEnv?.collect(exec) ?? {};
1569
+ let argv = [options.bashPath, "-c", args.command];
1570
+ let confined;
1571
+ if (policy !== void 0 && policy.mode !== "danger-full-access" && deps.sandbox !== void 0) {
1572
+ confined = await deps.sandbox.confine(argv, { ...policy, mode: policy.mode });
1573
+ argv = confined.argv;
1574
+ }
1575
+ const timeoutMs = clampTimeout(args.timeoutMs, minTimeout, maxTimeout);
1576
+ if (args.run_in_background === true) {
1577
+ const jobs = deps.jobs;
1578
+ if (jobs === void 0) {
1579
+ throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
1580
+ }
1581
+ if (exec.signal.aborted) throw abortedError();
1582
+ const id = jobs.start({
1583
+ kind: "bash",
1584
+ label: args.command,
1585
+ ...exec.agent === void 0 ? {} : { owner: exec.agent },
1586
+ run: () => {
1587
+ const controller2 = new AbortController();
1588
+ let handle2;
1589
+ const offsets = { stdout: 0, stderr: 0 };
1590
+ const done = (async () => {
1591
+ try {
1592
+ handle2 = deps.subprocess.spawn(spawnSpec(argv, workdir ?? process.cwd(), dshEnv, controller2.signal));
1593
+ if (controller2.signal.aborted) handle2.terminate();
1594
+ const outcome = await handle2.done;
1595
+ if (controller2.signal.aborted) {
1596
+ return {
1597
+ status: "killed",
1598
+ detail: outcome.signal !== null ? `signal: ${outcome.signal}` : "killed before exit"
1599
+ };
1600
+ }
1601
+ return { status: "completed", detail: `exit code: ${outcome.exitCode ?? 0}` };
1602
+ } catch (error) {
1603
+ return {
1604
+ status: controller2.signal.aborted && handle2 === void 0 ? "killed" : "failed",
1605
+ detail: error instanceof Error ? error.message : String(error)
1606
+ };
1607
+ }
1608
+ })();
1609
+ return {
1610
+ cancel: () => {
1611
+ if (controller2.signal.aborted) return;
1612
+ controller2.abort();
1613
+ handle2?.terminate();
1614
+ },
1615
+ done,
1616
+ readOutput: () => handle2 === void 0 ? "" : renderProcessRead(readFrame(handle2, offsets), void 0, escalationModes)
1617
+ };
1618
+ }
1619
+ });
1620
+ return { kind: "background", jobId: id };
1621
+ }
1622
+ const controller = new AbortController();
1623
+ let timedOut = false;
1624
+ const timer = setTimeout(() => {
1625
+ timedOut = true;
1626
+ controller.abort();
1627
+ }, timeoutMs);
1628
+ const onAbort = () => {
1629
+ controller.abort();
1630
+ };
1631
+ exec.signal.addEventListener("abort", onAbort);
1632
+ let handle;
1633
+ try {
1634
+ handle = deps.subprocess.spawn(spawnSpec(argv, workdir ?? process.cwd(), dshEnv, controller.signal));
1635
+ } catch (error) {
1636
+ clearTimeout(timer);
1637
+ exec.signal.removeEventListener("abort", onAbort);
1638
+ throw error;
1639
+ }
1640
+ const onControllerAbort = () => {
1641
+ try {
1642
+ handle?.terminate();
1643
+ } catch {
1644
+ }
1645
+ };
1646
+ controller.signal.addEventListener("abort", onControllerAbort);
1647
+ try {
1648
+ const outcome = await handle.done;
1649
+ const read = readFrame(handle, { stdout: 0, stderr: 0 });
1650
+ const aborted = exec.signal.aborted && !timedOut;
1651
+ const denied = confined !== void 0 && outcome.exitCode !== 0 && confined.denialSignatures.some((signature) => read.delta.toLowerCase().includes(signature.toLowerCase()));
1652
+ const result = {
1653
+ exitCode: outcome.exitCode,
1654
+ signal: outcome.signal,
1655
+ timedOut,
1656
+ aborted,
1657
+ timeoutMs,
1658
+ stdout: { text: read.delta, truncated: read.lossy, ...read.stdoutSpillPath === void 0 ? {} : { spillPath: read.stdoutSpillPath } },
1659
+ stderr: { text: "", truncated: false },
1660
+ ...denied ? { sandbox: { mode: policy?.mode ?? "read-only", denied: true } } : {}
1661
+ };
1662
+ if (aborted) throw abortedError();
1663
+ return { kind: "foreground", ...result };
1664
+ } finally {
1665
+ clearTimeout(timer);
1666
+ exec.signal.removeEventListener("abort", onAbort);
1667
+ controller.signal.removeEventListener("abort", onControllerAbort);
1668
+ }
1669
+ };
1670
+ return {
1671
+ name: BASH_TOOL_NAME,
1672
+ description: bashDescription(true, escalationModes),
1673
+ parameters: bashParameters(escalationModes),
1674
+ output: {
1675
+ schema: bashOutputSchema(),
1676
+ render: (_args, value) => {
1677
+ const v = value;
1678
+ const text = v.kind === "background" ? `started background job ${v.jobId}` : renderBashResult(v, escalationModes);
1679
+ return [{ type: "text", text }];
1680
+ }
1681
+ },
1682
+ execute,
1683
+ presentCall: (args) => {
1684
+ const a = args;
1685
+ if (a.run_in_background === true) {
1686
+ return {
1687
+ card: "generic",
1688
+ title: a.command,
1689
+ kind: "execute",
1690
+ rawInput: a.command,
1691
+ content: [{ type: "text", text: a.description }]
1692
+ };
1693
+ }
1694
+ return {
1695
+ card: "terminal",
1696
+ title: a.command,
1697
+ description: a.description,
1698
+ ...a.workdir === void 0 ? {} : { cwd: a.workdir }
1699
+ };
1700
+ },
1701
+ presentResult: (args, result) => {
1702
+ const block = result.content.length === 1 ? result.content[0] : void 0;
1703
+ if (block === void 0 || block.type !== "text") return void 0;
1704
+ const raw = block.text;
1705
+ const isBackground = typeof args === "object" && args !== null && args.run_in_background === true;
1706
+ if (isBackground || result.isError) {
1707
+ return { card: "generic", content: [{ type: "text", text: `\`\`\`console
1708
+ ${raw.replace(/\n+$/, "")}
1709
+ \`\`\`` }] };
1710
+ }
1711
+ const { body, ...exit } = parseExitStatus(raw);
1712
+ return { card: "terminal", output: body, ...exit };
1713
+ }
1714
+ };
1715
+ }
1716
+
1717
+ // src/terminal/host.ts
1718
+ var TERMINAL_OFF_TEXT = "\u672A\u542F\u7528\uFF1A\u6253\u5F00\u8FD9\u4E00\u680F\u7684\u5F00\u5173\u540E\u624D\u4F1A\u63A5\u7BA1\uFF08\u73B0\u5728\u4FDD\u6301 PowerShell\uFF09";
1719
+ function objectOf(value) {
1720
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1721
+ }
1722
+ function textOf(value) {
1723
+ return typeof value === "string" ? value : "";
1724
+ }
1725
+ function errorText(error) {
1726
+ return error instanceof Error && error.message !== "" ? error.message : String(error);
1727
+ }
1728
+ function installTerminalPolicy(ctx, namespace, settings, readToolDeps, options = {}) {
1729
+ const platform = options.platform ?? process.platform;
1730
+ const isWindows = platform === "win32";
1731
+ const existsFn = options.exists ?? existsSync;
1732
+ const installed = /* @__PURE__ */ new Map();
1733
+ let lastStatus = "";
1734
+ let lastEffective = "";
1735
+ let lastCandidates = "";
1736
+ let failure = "";
1737
+ let busy = false;
1738
+ let again = false;
1739
+ const discover = options.discover ?? ((explicitPath) => probeBash(
1740
+ explicitPath,
1741
+ process.env.PATH ?? "",
1742
+ (name2) => process.env[name2],
1743
+ (path) => existsFn(path),
1744
+ // 只读子目录名(带版本号的来源:GitHub Desktop 的 app-*、旧 GitHub 的 PortableGit_*、VS 的年份/版本);
1745
+ // 目录不存在或没权限一律当空,探测就退化成纯存在性检查。
1746
+ (parent) => {
1747
+ try {
1748
+ return readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
1749
+ } catch {
1750
+ return [];
1751
+ }
1752
+ }
1753
+ ));
1754
+ const install = (agent, bashPath) => {
1755
+ const scoped = agent.ctx;
1756
+ const cleanups = [];
1757
+ const notes = [];
1758
+ try {
1759
+ const lift = scoped.tools?.restrict({ deny: ["pwsh"] });
1760
+ if (typeof lift === "function") cleanups.push(lift);
1761
+ } catch (error) {
1762
+ notes.push(`restrict(pwsh) \u8DF3\u8FC7\uFF1A${errorText(error)}`);
1763
+ }
1764
+ try {
1765
+ const deps = readToolDeps();
1766
+ if (deps === void 0) {
1767
+ notes.push("\u5BBF\u4E3B\u6CA1\u6709 subprocess \u670D\u52A1\uFF0C\u65E0\u6CD5\u6CE8\u518C bash");
1768
+ } else {
1769
+ const definition = createBashTool(deps, { bashPath, sep, isAbsolute });
1770
+ const unregister = scoped.tools?.register(definition);
1771
+ if (typeof unregister === "function") cleanups.push(unregister);
1772
+ }
1773
+ } catch (error) {
1774
+ notes.push(`\u6CE8\u518C bash \u5931\u8D25\uFF1A${errorText(error)}`);
1775
+ }
1776
+ try {
1777
+ const bashSection = scoped.systemPrompt?.section({
1778
+ name: "tool:bash",
1779
+ order: TOOL_BASH_SECTION_ORDER,
1780
+ text: BASH_SECTION_TEXT
1781
+ });
1782
+ if (typeof bashSection === "function") cleanups.push(bashSection);
1783
+ const dropPwsh = scoped.on?.("system-prompt/assemble", (async (_assembly, _context, next) => {
1784
+ const assembly = await next();
1785
+ return { ...assembly, sections: assembly.sections.filter((section) => section.name !== "tool:pwsh") };
1786
+ }));
1787
+ if (typeof dropPwsh === "function") cleanups.push(dropPwsh);
1788
+ } catch (error) {
1789
+ notes.push(`\u63D0\u793A\u8BCD\u6BB5\u5931\u8D25\uFF1A${errorText(error)}`);
1790
+ }
1791
+ return {
1792
+ path: bashPath,
1793
+ dispose: () => {
1794
+ for (const cleanup of [...cleanups].reverse()) {
1795
+ try {
1796
+ cleanup();
1797
+ } catch {
1798
+ }
1799
+ }
1800
+ },
1801
+ ...notes.length === 0 ? {} : { failure: notes.join("\uFF1B") }
1802
+ };
1803
+ };
1804
+ const reconcile = () => {
1805
+ if (busy) {
1806
+ again = true;
1807
+ return;
1808
+ }
1809
+ busy = true;
1810
+ void (async () => {
1811
+ const row = settings === void 0 ? void 0 : objectOf(settings.get(namespace));
1812
+ const mode = terminalModeFrom(row?.[TERMINAL_MODE_FIELD]);
1813
+ const explicit = textOf(row?.[TERMINAL_BASH_PATH_FIELD]);
1814
+ const agents = ctx.get("agents");
1815
+ const ops = [];
1816
+ const sectionOn = row?.["enabled"] !== false && sectionEnabledOf(TERMINAL_ENABLED_FIELD, row ?? {});
1817
+ if (!isWindows) {
1818
+ for (const entry of installed.values()) entry.dispose();
1819
+ installed.clear();
1820
+ failure = "";
1821
+ writeState(ops, {
1822
+ status: terminalStatusText({ platform, mode, candidates: [], excludedCount: 0, effective: "unsupported" }),
1823
+ effective: "unsupported",
1824
+ candidates: []
1825
+ });
1826
+ await flush(ops);
1827
+ return;
1828
+ }
1829
+ if (!sectionOn) {
1830
+ for (const entry of installed.values()) entry.dispose();
1831
+ installed.clear();
1832
+ failure = "";
1833
+ writeState(ops, { status: TERMINAL_OFF_TEXT, effective: "pwsh", candidates: [] });
1834
+ await flush(ops);
1835
+ return;
1836
+ }
1837
+ const found = discover(explicit);
1838
+ const candidates = candidatesToStored(found.candidates);
1839
+ const bashPath = activeBashPath(mode, explicit, candidates, found.explicit);
1840
+ const usable = bashPath !== "" && existsFn(bashPath);
1841
+ if (usable) {
1842
+ for (const agent of agents?.list() ?? []) {
1843
+ const current = installed.get(agent);
1844
+ if (current !== void 0 && current.path === bashPath) continue;
1845
+ if (current !== void 0) {
1846
+ current.dispose();
1847
+ installed.delete(agent);
1848
+ }
1849
+ const record = install(agent, bashPath);
1850
+ if (record !== void 0) installed.set(agent, record);
1851
+ }
1852
+ } else {
1853
+ for (const entry of installed.values()) entry.dispose();
1854
+ installed.clear();
1855
+ failure = "";
1856
+ }
1857
+ const firstFailure = [...installed.values()].find((entry) => entry.failure !== void 0)?.failure;
1858
+ writeState(ops, {
1859
+ status: terminalStatusText({
1860
+ platform,
1861
+ mode,
1862
+ candidates,
1863
+ excludedCount: found.excluded.length,
1864
+ ...found.explicit === void 0 ? {} : { explicit: found.explicit },
1865
+ effective: usable ? "bash" : "pwsh",
1866
+ ...usable ? { effectivePath: bashPath } : {},
1867
+ ...firstFailure === void 0 && failure === "" ? {} : { failure: firstFailure ?? failure }
1868
+ }),
1869
+ effective: usable ? "bash" : "pwsh",
1870
+ candidates
1871
+ });
1872
+ await flush(ops);
1873
+ })().catch((error) => {
1874
+ console.error("[composer-ux] \u7EC8\u7AEF\u7B56\u7565\u4E0B\u53D1\u5931\u8D25", error);
1875
+ }).finally(() => {
1876
+ busy = false;
1877
+ if (again) {
1878
+ again = false;
1879
+ reconcile();
1880
+ }
1881
+ });
1882
+ };
1883
+ const writeState = (ops, next) => {
1884
+ const candidatesJson = JSON.stringify(next.candidates);
1885
+ if (next.status !== lastStatus) {
1886
+ lastStatus = next.status;
1887
+ ops.push({ op: "set", path: [TERMINAL_STATUS_FIELD], value: next.status });
1888
+ }
1889
+ if (next.effective !== lastEffective) {
1890
+ lastEffective = next.effective;
1891
+ ops.push({ op: "set", path: [TERMINAL_EFFECTIVE_FIELD], value: next.effective });
1892
+ }
1893
+ if (candidatesJson !== lastCandidates) {
1894
+ lastCandidates = candidatesJson;
1895
+ ops.push({ op: "set", path: [TERMINAL_CANDIDATES_FIELD], value: next.candidates });
1896
+ }
1897
+ };
1898
+ const flush = async (ops) => {
1899
+ if (ops.length === 0 || settings === void 0) return;
1900
+ try {
1901
+ await settings.mutate(namespace, ops);
1902
+ } catch (error) {
1903
+ console.error("[composer-ux] \u7EC8\u7AEF\u72B6\u6001\u5199\u5165\u5931\u8D25", error);
1904
+ }
1905
+ };
1906
+ if (typeof ctx.on !== "function") {
1907
+ failure = "\u4E0A\u4E0B\u6587\u4E0D\u652F\u6301\u4E8B\u4EF6\uFF0C\u65E0\u6CD5\u5728\u4F1A\u8BDD\u521B\u5EFA\u65F6\u4E0B\u53D1";
1908
+ }
1909
+ ctx.on?.("agent/created", (() => {
1910
+ reconcile();
1911
+ }));
1912
+ ctx.on?.("agent/disposed", ((payload) => {
1913
+ if (payload?.agent !== void 0) installed.delete(payload.agent);
1914
+ }));
1915
+ if (settings !== void 0) ctx.on?.("settings/updated", (() => {
1916
+ reconcile();
1917
+ }));
1918
+ ctx.effect(() => ctx.webServer.register({
1919
+ kind: "exact",
1920
+ path: "/composer-ux/terminal",
1921
+ handler: async (req, res) => {
1922
+ const send = (code, payload) => {
1923
+ res.writeHead(code, { "content-type": "application/json; charset=utf-8" });
1924
+ res.end(JSON.stringify(payload));
1925
+ };
1926
+ const connection = ctx.get("connection");
1927
+ const rejection = connection?.requestRejection?.(req);
1928
+ if (rejection !== void 0) {
1929
+ res.statusCode = rejection;
1930
+ res.end();
1931
+ return;
1932
+ }
1933
+ const method = (req.method ?? "GET").toUpperCase();
1934
+ const row = settings === void 0 ? void 0 : objectOf(settings.get(namespace));
1935
+ const mode = terminalModeFrom(row?.[TERMINAL_MODE_FIELD]);
1936
+ const explicit = textOf(row?.[TERMINAL_BASH_PATH_FIELD]);
1937
+ if (method === "POST") {
1938
+ reconcile();
1939
+ const found2 = discover(explicit);
1940
+ send(200, {
1941
+ ok: true,
1942
+ candidates: candidatesToStored(found2.candidates),
1943
+ excluded: found2.excluded,
1944
+ ...found2.explicit === void 0 ? {} : { explicit: found2.explicit }
1945
+ });
1946
+ return;
1947
+ }
1948
+ const found = discover(explicit);
1949
+ send(200, {
1950
+ ok: true,
1951
+ platform,
1952
+ supported: isWindows,
1953
+ mode,
1954
+ bashPath: explicit,
1955
+ effective: lastEffective === "" ? "pwsh" : lastEffective,
1956
+ status: lastStatus,
1957
+ candidates: isWindows ? candidatesToStored(found.candidates) : [],
1958
+ excluded: found.excluded
1959
+ });
1960
+ }
1961
+ }), "composer-ux: terminal status route");
1962
+ reconcile();
1963
+ return;
1964
+ }
1965
+
1966
+ // src/restart.ts
1967
+ var RESTART_POLL_MS = 250;
1968
+ var RESTART_PORT_WAIT_MS = 3e4;
1969
+ var RESTART_PORT_SETTLE_MS = 300;
1970
+ var RESTART_EXIT_DELAY_MS = 500;
1971
+ var RESTART_REPLACEMENT_WAIT_MS = 2e4;
1972
+ var RESTART_PROBE_TIMEOUT_MS = 500;
1973
+ var RESTART_NO_PORT_DELAY_MS = 1500;
1974
+ var RESTART_HELPER_LINGER_MS = 3e3;
1975
+ var RESTART_STOP_FALLBACK_MS = 1e4;
1976
+ var RESTART_LOG_PREFIX = "composer-ux-restart-";
1977
+ function nodeExecutableOf(input) {
1978
+ const { argv0, execPath, exists } = input;
1979
+ if (argv0 !== void 0 && argv0 !== "" && isAbsolutePath(argv0) && exists(argv0)) return argv0;
1980
+ return execPath;
1981
+ }
1982
+ function isAbsolutePath(value) {
1983
+ return value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\");
1984
+ }
1985
+ function launchCommand(facts) {
1986
+ const { argv1, execArgv, rest, cwd, platform, resolve: resolve3, dirname: dirname3 } = facts;
1987
+ if (argv1 !== void 0 && /[\\/](?:bin\.(?:js|ts)|dsh)$/.test(argv1)) {
1988
+ const absolute = resolve3(argv1);
1989
+ return { file: facts.node, args: [...execArgv, absolute, ...rest], cwd: dirname3(absolute), viaShell: false };
1990
+ }
1991
+ return { file: "dsh", args: [...rest], cwd, viaShell: platform === "win32" };
1992
+ }
1993
+ function quotePowerShell(value) {
1994
+ return `'${value.replace(/'/g, "''")}'`;
1995
+ }
1996
+ function respawnCommand(launch, platform) {
1997
+ if (platform !== "win32") {
1998
+ return { file: launch.file, args: launch.args, viaShell: launch.viaShell, detached: true };
1999
+ }
2000
+ const file = launch.viaShell && !/\.(?:cmd|bat)$/iu.test(launch.file) ? `${launch.file}.cmd` : launch.file;
2001
+ return {
2002
+ file: "powershell.exe",
2003
+ args: [
2004
+ "-NoProfile",
2005
+ "-WindowStyle",
2006
+ "Hidden",
2007
+ "-Command",
2008
+ [`& ${quotePowerShell(file)}`, ...launch.args.map(quotePowerShell)].join(" ")
2009
+ ],
2010
+ viaShell: false,
2011
+ detached: false
2012
+ };
2013
+ }
2014
+ function servingPort(hostHeader) {
2015
+ if (hostHeader === void 0) return null;
2016
+ const match = /:(\d{1,5})$/u.exec(hostHeader);
2017
+ if (match === null) return null;
2018
+ const port = Number(match[1]);
2019
+ return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
2020
+ }
2021
+ function isLoopbackAddress(address) {
2022
+ return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
2023
+ }
2024
+ function trustedRestartRequest(request) {
2025
+ if (!isLoopbackAddress(request.remoteAddress)) return false;
2026
+ const headers = request.headers;
2027
+ if (headers.forwarded !== void 0 || headers["x-forwarded-for"] !== void 0 || headers["x-real-ip"] !== void 0) return false;
2028
+ const origin = firstHeader(headers.origin);
2029
+ const host = firstHeader(headers.host);
2030
+ if (origin === void 0 || host === void 0) return false;
2031
+ try {
2032
+ const parsed = new URL(origin);
2033
+ return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.host === host;
2034
+ } catch {
2035
+ return false;
2036
+ }
2037
+ }
2038
+ function firstHeader(value) {
2039
+ if (value === void 0) return void 0;
2040
+ return typeof value === "string" ? value : value[0];
2041
+ }
2042
+ var INSPECT_ARG_PREFIXES = ["--inspect", "--inspect-brk", "--inspect-port", "--inspect-wait"];
2043
+ function tokenHasInspectFlag(token) {
2044
+ for (const prefix of INSPECT_ARG_PREFIXES) {
2045
+ if (token === prefix || token.startsWith(`${prefix}=`)) return true;
2046
+ }
2047
+ if (token === "--debug-brk" || token.startsWith("--debug-brk=")) return true;
2048
+ if (token === "--debug" || token.startsWith("--debug=")) return true;
2049
+ return false;
2050
+ }
2051
+ function detectedDebugger(input) {
2052
+ const { inspectorUrl, execArgv, nodeOptions } = input;
2053
+ if (inspectorUrl !== void 0 && inspectorUrl !== "") return "inspector";
2054
+ if (execArgv.some(tokenHasInspectFlag)) return "inspector";
2055
+ const options = (nodeOptions ?? "").trim();
2056
+ if (options !== "" && options.split(/\s+/u).some(tokenHasInspectFlag)) return "inspector";
2057
+ return null;
2058
+ }
2059
+ function detectedSupervisor(input) {
2060
+ const set2 = (name2) => (input.env[name2] ?? "") !== "";
2061
+ if (!set2("INVOCATION_ID") && !set2("JOURNAL_STREAM")) return null;
2062
+ if (input.ppid === 1 || input.parentComm(input.ppid) === "systemd") return "systemd";
2063
+ return null;
2064
+ }
2065
+ function restartHelperSource(input) {
2066
+ const { spawned, cwd, logs, port } = input;
2067
+ return [
2068
+ "const { spawn } = require('node:child_process')",
2069
+ "const fs = require('node:fs')",
2070
+ "const net = require('node:net')",
2071
+ `const file = ${JSON.stringify(spawned.file)}`,
2072
+ `const args = ${JSON.stringify(spawned.args)}`,
2073
+ `const cwd = ${JSON.stringify(cwd)}`,
2074
+ `const viaShell = ${JSON.stringify(spawned.viaShell)}`,
2075
+ `const detached = ${JSON.stringify(spawned.detached)}`,
2076
+ `const logOut = ${JSON.stringify(logs.out)}`,
2077
+ `const logErr = ${JSON.stringify(logs.err)}`,
2078
+ `const port = ${JSON.stringify(port)}`,
2079
+ `const pollMs = ${String(RESTART_POLL_MS)}`,
2080
+ `const portWaitMs = ${String(RESTART_PORT_WAIT_MS)}`,
2081
+ `const settleMs = ${String(RESTART_PORT_SETTLE_MS)}`,
2082
+ `const probeMs = ${String(RESTART_PROBE_TIMEOUT_MS)}`,
2083
+ `const noPortDelayMs = ${String(RESTART_NO_PORT_DELAY_MS)}`,
2084
+ `const lingerMs = ${String(RESTART_HELPER_LINGER_MS)}`,
2085
+ `const replacementWaitMs = ${String(RESTART_REPLACEMENT_WAIT_MS)}`,
2086
+ "const sleep = (ms) => new Promise(r => setTimeout(r, ms))",
2087
+ // 失败必须留证据,而且这些行只能在"宿主已经退出"之后写 —— 所以由助手写。
2088
+ "const note = (line) => { try { fs.appendFileSync(logErr, '[dsh-composer-ux] ' + line + '\\n') } catch {} }",
2089
+ // "空闲" = 没人接受连接。用 connect 探而不是 bind:bind 一下自己就把端口占住了,
2090
+ // 而那正是替换进程马上要用的东西。
2091
+ "const listening = () => new Promise((resolve) => {",
2092
+ ' const probe = net.connect({ host: "127.0.0.1", port })',
2093
+ " const done = (value) => { probe.destroy(); resolve(value) }",
2094
+ ' probe.on("connect", () => done(true))',
2095
+ ' probe.on("error", () => done(false))',
2096
+ " setTimeout(() => done(false), probeMs)",
2097
+ "})",
2098
+ "const main = async () => {",
2099
+ " if (port) {",
2100
+ " const until = Date.now() + portWaitMs",
2101
+ " while (Date.now() < until && await listening()) await sleep(pollMs)",
2102
+ ' if (await listening()) note("port " + port + " was still in use after " + portWaitMs + "ms; starting anyway")',
2103
+ " await sleep(settleMs)",
2104
+ " } else {",
2105
+ " await sleep(noPortDelayMs)",
2106
+ " }",
2107
+ " let child",
2108
+ " try {",
2109
+ ' const out = fs.openSync(logOut, "a")',
2110
+ ' const err = fs.openSync(logErr, "a")',
2111
+ // windowsHide:助手自己是 detached 的,Windows 上它没有控制台,而一个没有控制台的
2112
+ // 父进程 spawn 控制台程序会**新建一个可见控制台** —— 就是那个关掉就把宿主带走的黑窗口。
2113
+ ' child = spawn(file, args, { cwd, detached, stdio: ["ignore", out, err], env: process.env, shell: viaShell, windowsHide: true })',
2114
+ // spawn 报"文件不存在/不可执行"是**异步**的,下面的 try/catch 只接同步抛出,
2115
+ // 所以少了这个监听,失败会和被修的那个 bug 一样安静。
2116
+ ' child.on("error", (error) => note("could not start the replacement: " + (error && error.message ? error.message : String(error))))',
2117
+ " child.unref()",
2118
+ " } catch (error) {",
2119
+ ' note("could not start the replacement: " + (error && error.message ? error.message : String(error)))',
2120
+ " return",
2121
+ " }",
2122
+ // 多活一会儿在 Windows 上是有意义的:spawn 完立刻退出的助手可能把还没 detach 完的
2123
+ // 替换进程一起带走。有端口可探的那条路本来就要多待,这条是给没有端口的情况补上同样的保证。
2124
+ " if (!port) { await sleep(lingerMs); return }",
2125
+ " const upBy = Date.now() + replacementWaitMs",
2126
+ " while (Date.now() < upBy && !(await listening())) await sleep(500)",
2127
+ ' if (!(await listening())) note("the replacement did not bind port " + port + " within " + replacementWaitMs + "ms \u2014 see the output log beside this one")',
2128
+ "}",
2129
+ "main()"
2130
+ ].join("\n");
2131
+ }
2132
+ function planRestart(io, port) {
2133
+ const node = nodeExecutableOf({ argv0: io.argv0, execPath: io.execPath, exists: io.exists });
2134
+ const launch = launchCommand({
2135
+ node,
2136
+ argv1: io.argv1,
2137
+ execArgv: io.execArgv,
2138
+ rest: io.rest,
2139
+ cwd: io.cwd,
2140
+ platform: io.platform,
2141
+ resolve: io.resolve,
2142
+ dirname: io.dirname
2143
+ });
2144
+ const respawn = respawnCommand(launch, io.platform);
2145
+ const logOut = io.join(io.tmpdir, `${RESTART_LOG_PREFIX}${io.stamp}.out.log`);
2146
+ const logErr = io.join(io.tmpdir, `${RESTART_LOG_PREFIX}${io.stamp}.err.log`);
2147
+ return {
2148
+ node,
2149
+ launch,
2150
+ respawn,
2151
+ helper: restartHelperSource({ spawned: respawn, cwd: launch.cwd, logs: { out: logOut, err: logErr }, port }),
2152
+ logOut,
2153
+ logErr,
2154
+ port
2155
+ };
2156
+ }
2157
+ function scheduleRestart(io, port) {
2158
+ const plan = planRestart(io, port);
2159
+ const helper = io.spawn(plan.node, ["-e", plan.helper], {
2160
+ detached: true,
2161
+ stdio: "ignore",
2162
+ windowsHide: true,
2163
+ env: io.env
2164
+ });
2165
+ helper.unref?.();
2166
+ void io.wait(RESTART_EXIT_DELAY_MS).then(() => {
2167
+ io.stop();
2168
+ });
2169
+ return {
2170
+ ok: true,
2171
+ pid: io.pid,
2172
+ helperPid: helper.pid,
2173
+ logOut: plan.logOut,
2174
+ logErr: plan.logErr,
2175
+ port: plan.port,
2176
+ command: [plan.respawn.file, ...plan.respawn.args].join(" ")
2177
+ };
2178
+ }
2179
+ function gracefulStop(io, signal = "SIGTERM") {
2180
+ try {
2181
+ io.emitSignal(signal);
2182
+ } catch {
2183
+ }
2184
+ io.timer(RESTART_STOP_FALLBACK_MS, () => {
2185
+ io.exit(0);
2186
+ });
2187
+ }
2188
+ function bootId(pid, now) {
2189
+ return `${String(pid)}-${String(now)}`;
2190
+ }
976
2191
 
977
2192
  // src/optimizer-prompt.ts
978
2193
  var RELAY_ROLE = [
@@ -1083,15 +2298,15 @@ function dshHome() {
1083
2298
  function quickStorePath() {
1084
2299
  return join(dshHome(), QUICK_STORE_FILE);
1085
2300
  }
1086
- function errorText(error) {
2301
+ function errorText2(error) {
1087
2302
  return error instanceof Error ? error.message : String(error);
1088
2303
  }
1089
2304
  function hasCode(error, code) {
1090
2305
  return typeof error === "object" && error !== null && error.code === code;
1091
2306
  }
1092
2307
  function delay(ms) {
1093
- return new Promise((resolve2) => {
1094
- setTimeout(resolve2, ms);
2308
+ return new Promise((resolve3) => {
2309
+ setTimeout(resolve3, ms);
1095
2310
  });
1096
2311
  }
1097
2312
  async function quarantine(file) {
@@ -1110,14 +2325,14 @@ async function readQuickBook(file = quickStorePath()) {
1110
2325
  raw = await readFile(file, "utf8");
1111
2326
  } catch (error) {
1112
2327
  if (hasCode(error, "ENOENT")) return { kind: "missing" };
1113
- return { kind: "broken", error: `\u8BFB\u53D6\u5931\u8D25\uFF1A${errorText(error)}` };
2328
+ return { kind: "broken", error: `\u8BFB\u53D6\u5931\u8D25\uFF1A${errorText2(error)}` };
1114
2329
  }
1115
2330
  if (raw.trim() === "") return { kind: "broken", error: "\u6587\u4EF6\u662F\u7A7A\u7684", quarantined: await quarantine(file) };
1116
2331
  let parsed;
1117
2332
  try {
1118
2333
  parsed = JSON.parse(raw);
1119
2334
  } catch (error) {
1120
- return { kind: "broken", error: `\u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${errorText(error)}`, quarantined: await quarantine(file) };
2335
+ return { kind: "broken", error: `\u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${errorText2(error)}`, quarantined: await quarantine(file) };
1121
2336
  }
1122
2337
  const book = sanitizeBook(parsed);
1123
2338
  if (book === void 0) {
@@ -1207,12 +2422,19 @@ async function ensureQuickBook(seed, file = quickStorePath()) {
1207
2422
  var name = "composer-ux";
1208
2423
  var PANEL_MIN = 560;
1209
2424
  var PANEL_MAX = 4e3;
1210
- function objectOf(value) {
2425
+ function objectOf2(value) {
1211
2426
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1212
2427
  }
1213
- function textOf(value) {
2428
+ function textOf2(value) {
1214
2429
  return typeof value === "string" ? value : "";
1215
2430
  }
2431
+ function firstHeaderValue(value) {
2432
+ if (value === void 0) return void 0;
2433
+ return typeof value === "string" ? value : value[0];
2434
+ }
2435
+ function bookLooksCustom(book) {
2436
+ return JSON.stringify(sanitizeBook(book)) !== JSON.stringify(defaultQuickBook());
2437
+ }
1216
2438
  function nameIsValid(name2) {
1217
2439
  if (name2 === "" || name2.length > HEADER_NAME_MAX) return false;
1218
2440
  try {
@@ -1232,7 +2454,7 @@ function valueIsValid(value) {
1232
2454
  }
1233
2455
  }
1234
2456
  function isOpencodeBaseUrl(value) {
1235
- const raw = textOf(value).trim();
2457
+ const raw = textOf2(value).trim();
1236
2458
  if (raw === "") return false;
1237
2459
  let host = "";
1238
2460
  try {
@@ -1245,28 +2467,28 @@ function isOpencodeBaseUrl(value) {
1245
2467
  function pickRoutes(providers, listed) {
1246
2468
  const keys = Object.keys(providers);
1247
2469
  if (listed.length === 0) {
1248
- return keys.filter((key) => key.startsWith(OPENCODE_ROUTE_PREFIX) || isOpencodeBaseUrl(objectOf(providers[key])?.baseURL));
2470
+ return keys.filter((key) => key.startsWith(OPENCODE_ROUTE_PREFIX) || isOpencodeBaseUrl(objectOf2(providers[key])?.baseURL));
1249
2471
  }
1250
2472
  return listed.filter((key) => Object.prototype.hasOwnProperty.call(providers, key));
1251
2473
  }
1252
- function errorText2(error) {
2474
+ function errorText3(error) {
1253
2475
  if (error instanceof Error && error.message !== "") return error.message;
1254
2476
  return String(error);
1255
2477
  }
1256
2478
  async function mirrorHeader(settings) {
1257
- const own = objectOf(settings.get(NAMESPACE));
2479
+ const own = objectOf2(settings.get(NAMESPACE));
1258
2480
  if (own === void 0) return;
1259
2481
  const enabled = own[ENABLED_FIELD] === true && own[HEADER_ENABLED_FIELD] === true;
1260
- const name2 = textOf(own[HEADER_NAME_FIELD]);
2482
+ const name2 = textOf2(own[HEADER_NAME_FIELD]);
1261
2483
  const nameOk = nameIsValid(name2);
1262
- const routesText = parseRouteList(textOf(own[HEADER_ROUTES_FIELD]));
1263
- let value = textOf(own[HEADER_VALUE_FIELD]);
2484
+ const routesText = parseRouteList(textOf2(own[HEADER_ROUTES_FIELD]));
2485
+ let value = textOf2(own[HEADER_VALUE_FIELD]);
1264
2486
  if (enabled && nameOk && value === "") {
1265
2487
  value = newSessionId();
1266
2488
  await settings.mutate(NAMESPACE, [{ op: "set", path: [HEADER_VALUE_FIELD], value }]);
1267
2489
  }
1268
- const llm = objectOf(settings.get(LLM_NAMESPACE));
1269
- const providers = llm === void 0 ? void 0 : objectOf(llm.providers);
2490
+ const llm = objectOf2(settings.get(LLM_NAMESPACE));
2491
+ const providers = llm === void 0 ? void 0 : objectOf2(llm.providers);
1270
2492
  const writes = [];
1271
2493
  const applied = [];
1272
2494
  let status = "";
@@ -1280,7 +2502,7 @@ async function mirrorHeader(settings) {
1280
2502
  status = `\u5934\u503C\u4E0D\u5408\u6CD5\uFF08\u9700 1\u2013${HEADER_VALUE_MAX} \u5B57\u7B26\u4E14\u5355\u884C\uFF09\uFF0C\u672A\u5199\u5165`;
1281
2503
  } else {
1282
2504
  for (const route of pickRoutes(providers, routesText)) {
1283
- const headers = objectOf(objectOf(providers[route])?.headers);
2505
+ const headers = objectOf2(objectOf2(providers[route])?.headers);
1284
2506
  if (headers?.[name2] !== value) {
1285
2507
  writes.push({ op: "set", path: ["providers", route, "headers", name2], value });
1286
2508
  }
@@ -1290,9 +2512,9 @@ async function mirrorHeader(settings) {
1290
2512
  }
1291
2513
  const stale = [];
1292
2514
  if (providers !== void 0) {
1293
- const previousName = textOf(own[HEADER_APPLIED_NAME_FIELD]);
1294
- const previousValue = textOf(own[HEADER_APPLIED_VALUE_FIELD]);
1295
- const ours = /* @__PURE__ */ new Set([textOf(own[HEADER_VALUE_FIELD]), previousValue]);
2515
+ const previousName = textOf2(own[HEADER_APPLIED_NAME_FIELD]);
2516
+ const previousValue = textOf2(own[HEADER_APPLIED_VALUE_FIELD]);
2517
+ const ours = /* @__PURE__ */ new Set([textOf2(own[HEADER_VALUE_FIELD]), previousValue]);
1296
2518
  ours.delete("");
1297
2519
  const targets = new Set(applied.map((route) => `${route}\0${name2}`));
1298
2520
  const candidates = /* @__PURE__ */ new Set([previousName, nameOk ? name2 : ""]);
@@ -1300,7 +2522,7 @@ async function mirrorHeader(settings) {
1300
2522
  for (const candidate of candidates) {
1301
2523
  for (const [route, profile] of Object.entries(providers)) {
1302
2524
  if (targets.has(`${route}\0${candidate}`)) continue;
1303
- const headers = objectOf(objectOf(profile)?.headers);
2525
+ const headers = objectOf2(objectOf2(profile)?.headers);
1304
2526
  const current = headers?.[candidate];
1305
2527
  if (typeof current !== "string" || !ours.has(current)) continue;
1306
2528
  stale.push({ op: "unset", path: ["providers", route, "headers", candidate] });
@@ -1314,19 +2536,19 @@ async function mirrorHeader(settings) {
1314
2536
  await settings.mutate(LLM_NAMESPACE, providerOps);
1315
2537
  } catch (error) {
1316
2538
  failed = true;
1317
- status = `\u5199\u5165\u5931\u8D25\uFF1A${errorText2(error)}`;
2539
+ status = `\u5199\u5165\u5931\u8D25\uFF1A${errorText3(error)}`;
1318
2540
  }
1319
2541
  }
1320
2542
  const nextAppliedName = !failed && applied.length > 0 ? name2 : "";
1321
2543
  const nextAppliedValue = !failed && applied.length > 0 ? value : "";
1322
2544
  const ownOps = [];
1323
- if (textOf(own[HEADER_APPLIED_NAME_FIELD]) !== nextAppliedName) {
2545
+ if (textOf2(own[HEADER_APPLIED_NAME_FIELD]) !== nextAppliedName) {
1324
2546
  ownOps.push({ op: "set", path: [HEADER_APPLIED_NAME_FIELD], value: nextAppliedName });
1325
2547
  }
1326
- if (textOf(own[HEADER_APPLIED_VALUE_FIELD]) !== nextAppliedValue) {
2548
+ if (textOf2(own[HEADER_APPLIED_VALUE_FIELD]) !== nextAppliedValue) {
1327
2549
  ownOps.push({ op: "set", path: [HEADER_APPLIED_VALUE_FIELD], value: nextAppliedValue });
1328
2550
  }
1329
- if (textOf(own[HEADER_STATUS_FIELD]) !== status) {
2551
+ if (textOf2(own[HEADER_STATUS_FIELD]) !== status) {
1330
2552
  ownOps.push({ op: "set", path: [HEADER_STATUS_FIELD], value: status });
1331
2553
  }
1332
2554
  if (ownOps.length > 0) await settings.mutate(NAMESPACE, ownOps);
@@ -1337,13 +2559,27 @@ function apply(ctx) {
1337
2559
  NAMESPACE,
1338
2560
  Schema.object({
1339
2561
  [ENABLED_FIELD]: Schema.boolean().default(DEFAULT_SETTINGS.enabled),
2562
+ // 五栏开关。**故意不给默认值、声明成可选**:迁移要用的信息就是"文档里有没有这个键"
2563
+ // —— 没有 ⇒ 从没碰过这一栏 ⇒ 关闭;有 ⇒ 用户碰过 ⇒ 保持他写下的值。
2564
+ // 给了静态默认值就再也分不出这两种情况了(净化的 sectionEnabledOf 靠它)。
2565
+ [KEYS_ENABLED_FIELD]: Schema.boolean().required(false),
2566
+ [MENU_ENABLED_FIELD]: Schema.boolean().required(false),
2567
+ [QUICK_ENABLED_FIELD]: Schema.boolean().required(false),
2568
+ [PANEL_ENABLED_FIELD]: Schema.boolean().required(false),
2569
+ [TERMINAL_ENABLED_FIELD]: Schema.boolean().required(false),
1340
2570
  [SEND_KEY_FIELD]: Schema.string().default(DEFAULT_SETTINGS.sendKey),
1341
2571
  [NEWLINE_KEY_FIELD]: Schema.string().default(DEFAULT_SETTINGS.newlineKey),
1342
2572
  ...Object.fromEntries(MENU_FIELDS.map((field) => [
1343
2573
  field,
1344
2574
  Schema.boolean().default(DEFAULT_SETTINGS[field])
1345
2575
  ])),
1346
- [MENU_NATIVE_FIELD]: Schema.boolean().default(DEFAULT_SETTINGS.menuNative),
2576
+ // 右键菜单模式(0.5.0 起三档)。**故意不给默认值、且声明成可选**:
2577
+ // 「文档里没有这个键」本身就是迁移要用的信息——净化据此按旧布尔 menuNative
2578
+ // 推断(见 menuModeFrom)。给了默认值就再也分不出「从没设置过」与「明确设成了它」。
2579
+ [MENU_MODE_FIELD]: Schema.string().required(false),
2580
+ // 旧的布尔字段:只作迁移线索,故同样不给默认值——旧文档里的 true / false
2581
+ // 都要保住原意(true = 浏览器菜单、false = 明确选过自定义菜单)。
2582
+ [MENU_NATIVE_FIELD]: Schema.boolean().required(false),
1347
2583
  [PANEL_SCROLL_FIELD]: Schema.boolean().default(DEFAULT_SETTINGS.panelScroll),
1348
2584
  [PANEL_RESIZE_FIELD]: Schema.boolean().default(DEFAULT_SETTINGS.panelResize),
1349
2585
  // schemastery 无 .optional():可选键用 .required(false)。
@@ -1365,7 +2601,19 @@ function apply(ctx) {
1365
2601
  prompt: Schema.string().default(""),
1366
2602
  always: Schema.boolean().default(false)
1367
2603
  })).default(DEFAULT_QUICK_PROMPTS.map((item) => ({ ...item }))),
1368
- [OPTIMIZER_TIER_FIELD]: Schema.string().default(DEFAULT_SETTINGS.optimizerTier)
2604
+ [OPTIMIZER_TIER_FIELD]: Schema.string().default(DEFAULT_SETTINGS.optimizerTier),
2605
+ // 「默认终端」(0.5.0 起):用户档位与可选路径。
2606
+ [TERMINAL_MODE_FIELD]: Schema.string().default(DEFAULT_SETTINGS.terminalMode),
2607
+ [TERMINAL_BASH_PATH_FIELD]: Schema.string().default(DEFAULT_SETTINGS.terminalBashPath),
2608
+ // 宿主半自持的三项:探测候选、状态行、当前生效 shell(见 HOST_OWNED_FIELDS)。
2609
+ [TERMINAL_CANDIDATES_FIELD]: Schema.array(Schema.object({
2610
+ path: Schema.string().default(""),
2611
+ label: Schema.string().default(""),
2612
+ kind: Schema.string().default("path"),
2613
+ explicit: Schema.boolean().default(false)
2614
+ })).default([]),
2615
+ [TERMINAL_STATUS_FIELD]: Schema.string().default(DEFAULT_SETTINGS.terminalStatus),
2616
+ [TERMINAL_EFFECTIVE_FIELD]: Schema.string().default(DEFAULT_SETTINGS.terminalEffective)
1369
2617
  })
1370
2618
  );
1371
2619
  const settings = settingsCtx.settings;
@@ -1413,13 +2661,13 @@ function apply(ctx) {
1413
2661
  return Buffer.concat(chunks).toString("utf8");
1414
2662
  };
1415
2663
  const resolveRoute = (payload) => {
1416
- const provider = textOf(payload.provider);
1417
- const model = textOf(payload.model);
2664
+ const provider = textOf2(payload.provider);
2665
+ const model = textOf2(payload.model);
1418
2666
  if (provider !== "" && model !== "") return { provider, model };
1419
2667
  try {
1420
2668
  const selector = optCtx.get("agentDefaultModel");
1421
2669
  const current = selector?.currentSelection?.();
1422
- return { provider: textOf(current?.provider), model: textOf(current?.model) };
2670
+ return { provider: textOf2(current?.provider), model: textOf2(current?.model) };
1423
2671
  } catch {
1424
2672
  return { provider: "", model: "" };
1425
2673
  }
@@ -1431,12 +2679,12 @@ function apply(ctx) {
1431
2679
  }
1432
2680
  let payload;
1433
2681
  try {
1434
- payload = objectOf(JSON.parse(await readBody(req))) ?? {};
2682
+ payload = objectOf2(JSON.parse(await readBody(req))) ?? {};
1435
2683
  } catch (error) {
1436
- sendJson(res, 400, { ok: false, error: `\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${errorText2(error)}` });
2684
+ sendJson(res, 400, { ok: false, error: `\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${errorText3(error)}` });
1437
2685
  return;
1438
2686
  }
1439
- const text = textOf(payload.text).trim();
2687
+ const text = textOf2(payload.text).trim();
1440
2688
  if (text === "") {
1441
2689
  sendJson(res, 400, { ok: false, error: "\u8F93\u5165\u6846\u662F\u7A7A\u7684\uFF0C\u6CA1\u6709\u53EF\u4F18\u5316\u7684\u5185\u5BB9" });
1442
2690
  return;
@@ -1445,7 +2693,7 @@ function apply(ctx) {
1445
2693
  sendJson(res, 400, { ok: false, error: `\u539F\u6587\u8FC7\u957F\uFF08\u4E0A\u9650 ${TEXT_MAX} \u5B57\u7B26\uFF09` });
1446
2694
  return;
1447
2695
  }
1448
- const tier = textOf(payload.tier) === "" ? DEFAULT_OPTIMIZER_TIER : textOf(payload.tier);
2696
+ const tier = textOf2(payload.tier) === "" ? DEFAULT_OPTIMIZER_TIER : textOf2(payload.tier);
1449
2697
  const route = resolveRoute(payload);
1450
2698
  if (route.provider === "" || route.model === "") {
1451
2699
  sendJson(res, 200, { ok: false, error: "\u62FF\u4E0D\u5230\u5F53\u524D\u7684\u6A21\u578B\u8DEF\u7531\uFF0C\u65E0\u6CD5\u4F18\u5316\uFF08\u8BF7\u5148\u5728\u8F93\u5165\u6846\u65C1\u7684\u6A21\u578B\u9009\u62E9\u5668\u91CC\u9009\u4E00\u4E2A\u6A21\u578B\uFF09" });
@@ -1476,15 +2724,15 @@ function apply(ctx) {
1476
2724
  out += String(chunk.text ?? "");
1477
2725
  if (out.length > OPTIMIZE_OUTPUT_MAX) break;
1478
2726
  } else if (chunk.type === "finish") {
1479
- const reason = objectOf(chunk.reason);
2727
+ const reason = objectOf2(chunk.reason);
1480
2728
  if (reason?.kind === "error" || reason?.kind === "aborted") {
1481
- const detail = objectOf(reason.failure);
1482
- failure = textOf(detail?.message) || (reason.kind === "aborted" ? "\u4F18\u5316\u88AB\u4E2D\u65AD" : "\u6A21\u578B\u8FD4\u56DE\u9519\u8BEF");
2729
+ const detail = objectOf2(reason.failure);
2730
+ failure = textOf2(detail?.message) || (reason.kind === "aborted" ? "\u4F18\u5316\u88AB\u4E2D\u65AD" : "\u6A21\u578B\u8FD4\u56DE\u9519\u8BEF");
1483
2731
  }
1484
2732
  }
1485
2733
  }
1486
2734
  } catch (error) {
1487
- failure = errorText2(error);
2735
+ failure = errorText3(error);
1488
2736
  } finally {
1489
2737
  clearTimeout(timer);
1490
2738
  }
@@ -1530,7 +2778,7 @@ function apply(ctx) {
1530
2778
  const legacyPrompts = () => {
1531
2779
  try {
1532
2780
  const settings = storeCtx.get("settings");
1533
- const row = objectOf(settings?.get(NAMESPACE));
2781
+ const row = objectOf2(settings?.get(NAMESPACE));
1534
2782
  const list = row?.[QUICK_PROMPTS_FIELD];
1535
2783
  return Array.isArray(list) ? list : void 0;
1536
2784
  } catch {
@@ -1560,9 +2808,9 @@ function apply(ctx) {
1560
2808
  }
1561
2809
  let payload;
1562
2810
  try {
1563
- payload = objectOf(JSON.parse(await readBody(req))) ?? {};
2811
+ payload = objectOf2(JSON.parse(await readBody(req))) ?? {};
1564
2812
  } catch (error) {
1565
- sendJson(res, 400, { ok: false, error: `\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${errorText2(error)}` });
2813
+ sendJson(res, 400, { ok: false, error: `\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${errorText3(error)}` });
1566
2814
  return;
1567
2815
  }
1568
2816
  const candidate = payload.book !== void 0 ? payload.book : payload;
@@ -1577,7 +2825,7 @@ function apply(ctx) {
1577
2825
  try {
1578
2826
  await writeQuickBook(next, file);
1579
2827
  } catch (error) {
1580
- sendJson(res, 500, { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${errorText2(error)}` });
2828
+ sendJson(res, 500, { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${errorText3(error)}` });
1581
2829
  return;
1582
2830
  }
1583
2831
  const verified = await readQuickBook(file);
@@ -1589,6 +2837,175 @@ function apply(ctx) {
1589
2837
  handler: handle
1590
2838
  }), "composer-ux: quick prompt store route");
1591
2839
  });
2840
+ ctx.inject(["settings", "webServer"], (termCtx) => {
2841
+ const readToolDeps = () => {
2842
+ const subprocess = termCtx.get("subprocess");
2843
+ if (subprocess === void 0) return void 0;
2844
+ const sandbox = termCtx.get("sandbox");
2845
+ const sandboxPolicy = termCtx.get("sandboxPolicy");
2846
+ const approval = termCtx.get("approval");
2847
+ const jobs = termCtx.get("jobs");
2848
+ const shellEnv = termCtx.get("shellEnv");
2849
+ return {
2850
+ subprocess,
2851
+ ...sandbox === void 0 ? {} : { sandbox },
2852
+ ...sandboxPolicy === void 0 ? {} : { sandboxPolicy },
2853
+ ...approval === void 0 ? {} : { approval },
2854
+ ...jobs === void 0 ? {} : { jobs },
2855
+ ...shellEnv === void 0 ? {} : { shellEnv }
2856
+ };
2857
+ };
2858
+ installTerminalPolicy(
2859
+ termCtx,
2860
+ NAMESPACE,
2861
+ termCtx.get("settings"),
2862
+ readToolDeps
2863
+ );
2864
+ });
2865
+ ctx.inject(["settings"], (migrateCtx) => {
2866
+ void (async () => {
2867
+ const service = migrateCtx.get("settings");
2868
+ if (service === void 0) return;
2869
+ const row = objectOf2(service.get(NAMESPACE));
2870
+ if (row === void 0 || row[QUICK_ENABLED_FIELD] !== void 0) return;
2871
+ try {
2872
+ const outcome = await readQuickBook(quickStorePath());
2873
+ if (outcome.kind !== "ok" || !bookLooksCustom(outcome.book)) return;
2874
+ await service.mutate(NAMESPACE, [
2875
+ { op: "set", path: [QUICK_ENABLED_FIELD], value: true }
2876
+ ]);
2877
+ } catch (error) {
2878
+ console.error("[composer-ux] quick section migration skipped", error);
2879
+ }
2880
+ })();
2881
+ });
2882
+ ctx.inject(["webServer"], (restartCtx) => {
2883
+ const BOOT_ID = bootId(process.pid, Date.now());
2884
+ const blockedBy = () => {
2885
+ const supervisor = detectedSupervisor({
2886
+ env: process.env,
2887
+ ppid: process.ppid,
2888
+ parentComm: (pid) => {
2889
+ try {
2890
+ return readFileSync(`/proc/${String(pid)}/comm`, "utf8").trim();
2891
+ } catch {
2892
+ return null;
2893
+ }
2894
+ }
2895
+ });
2896
+ if (supervisor !== null) return `supervised:${supervisor}`;
2897
+ if (detectedDebugger({
2898
+ inspectorUrl: inspector.url(),
2899
+ execArgv: process.execArgv,
2900
+ nodeOptions: process.env.NODE_OPTIONS
2901
+ }) !== null) return "debugger";
2902
+ return null;
2903
+ };
2904
+ const buildIo = () => ({
2905
+ platform: process.platform,
2906
+ pid: process.pid,
2907
+ argv0: process.argv0,
2908
+ execPath: process.execPath,
2909
+ argv1: process.argv[1],
2910
+ execArgv: process.execArgv,
2911
+ // argv[0] 是 node 自己、argv[1] 是入口;重放的是"入口 + 之后的参数"。
2912
+ rest: process.argv.slice(2),
2913
+ cwd: process.cwd(),
2914
+ env: process.env,
2915
+ tmpdir: tmpdir(),
2916
+ stamp: (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19),
2917
+ exists: (path) => existsSync2(path),
2918
+ resolve: (path) => resolve2(path),
2919
+ dirname: (path) => dirname2(path),
2920
+ join: (...parts) => join2(...parts),
2921
+ spawn: (command, args, options) => spawn(command, [...args], {
2922
+ detached: options.detached,
2923
+ stdio: options.stdio,
2924
+ windowsHide: options.windowsHide,
2925
+ env: options.env
2926
+ }),
2927
+ stop: () => {
2928
+ gracefulStop({
2929
+ emitSignal: (signal) => {
2930
+ process.emit(signal);
2931
+ },
2932
+ exit: (code) => {
2933
+ process.exit(code);
2934
+ },
2935
+ timer: (ms, run) => {
2936
+ setTimeout(run, ms);
2937
+ }
2938
+ });
2939
+ },
2940
+ wait: (ms) => new Promise((done) => {
2941
+ setTimeout(done, ms);
2942
+ })
2943
+ });
2944
+ let restarting = false;
2945
+ const planForDisplay = () => {
2946
+ const io = buildIo();
2947
+ const plan = planRestart(io, null);
2948
+ return {
2949
+ command: [plan.respawn.file, ...plan.respawn.args].join(" "),
2950
+ execPath: plan.node,
2951
+ logHint: join2(io.tmpdir, `${RESTART_LOG_PREFIX}*.err.log`)
2952
+ };
2953
+ };
2954
+ restartCtx.effect(() => restartCtx.webServer.register({
2955
+ kind: "exact",
2956
+ path: RESTART_API_PATH,
2957
+ handler: (req, res) => {
2958
+ const send = (code, payload) => {
2959
+ res.writeHead(code, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
2960
+ res.end(JSON.stringify(payload));
2961
+ };
2962
+ const connection = restartCtx.get("connection");
2963
+ const rejection = connection?.requestRejection?.(req);
2964
+ if (rejection !== void 0) {
2965
+ res.statusCode = rejection;
2966
+ res.end();
2967
+ return;
2968
+ }
2969
+ const agents = restartCtx.get("agents");
2970
+ const running = agents?.list?.().length ?? 0;
2971
+ const blocked = blockedBy();
2972
+ const method = (req.method ?? "GET").toUpperCase();
2973
+ if (method !== "GET" && method !== "POST") {
2974
+ res.writeHead(405, { allow: "GET, POST" });
2975
+ res.end();
2976
+ return;
2977
+ }
2978
+ if (method === "GET") {
2979
+ const plan = planForDisplay();
2980
+ send(200, { ok: true, ...plan, running, blocked, boot: BOOT_ID });
2981
+ return;
2982
+ }
2983
+ if (!trustedRestartRequest({
2984
+ remoteAddress: req.socket?.remoteAddress,
2985
+ headers: req.headers ?? {}
2986
+ })) {
2987
+ send(403, { ok: false, error: "restart is limited to same-origin loopback requests" });
2988
+ return;
2989
+ }
2990
+ if (blocked !== null) {
2991
+ send(403, { ok: false, error: blocked === "debugger" ? "self-restart is disabled while the host is under a debugger" : `restart belongs to the ${blocked.slice("supervised:".length)} supervisor on this host` });
2992
+ return;
2993
+ }
2994
+ if (restarting) {
2995
+ send(409, { ok: false, error: "restart already scheduled" });
2996
+ return;
2997
+ }
2998
+ restarting = true;
2999
+ try {
3000
+ const scheduled = scheduleRestart(buildIo(), servingPort(firstHeaderValue(req.headers?.host)));
3001
+ send(202, { ok: true, boot: BOOT_ID, running, ...scheduled });
3002
+ } catch (error) {
3003
+ restarting = false;
3004
+ send(500, { ok: false, error: error instanceof Error ? error.message : String(error) });
3005
+ }
3006
+ }
3007
+ }), "composer-ux: restart route");
3008
+ });
1592
3009
  }
1593
3010
  export {
1594
3011
  apply,