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/CHANGELOG.md +124 -1
- package/README.md +125 -16
- package/lib/client.js +1624 -582
- package/lib/client.js.map +4 -4
- package/lib/index.js +1473 -56
- package/lib/index.js.map +4 -4
- package/package.json +60 -55
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,
|
|
423
|
-
resolvers[type] =
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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((
|
|
1094
|
-
setTimeout(
|
|
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${
|
|
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${
|
|
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
|
|
2425
|
+
function objectOf2(value) {
|
|
1211
2426
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1212
2427
|
}
|
|
1213
|
-
function
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
2482
|
+
const name2 = textOf2(own[HEADER_NAME_FIELD]);
|
|
1261
2483
|
const nameOk = nameIsValid(name2);
|
|
1262
|
-
const routesText = parseRouteList(
|
|
1263
|
-
let value =
|
|
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 =
|
|
1269
|
-
const providers = llm === void 0 ? void 0 :
|
|
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 =
|
|
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 =
|
|
1294
|
-
const previousValue =
|
|
1295
|
-
const ours = /* @__PURE__ */ new Set([
|
|
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 =
|
|
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${
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
1417
|
-
const 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:
|
|
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 =
|
|
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${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2727
|
+
const reason = objectOf2(chunk.reason);
|
|
1480
2728
|
if (reason?.kind === "error" || reason?.kind === "aborted") {
|
|
1481
|
-
const detail =
|
|
1482
|
-
failure =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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${
|
|
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${
|
|
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,
|