msdevflow 0.7.6 → 0.7.8
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/README.md +19 -0
- package/lib/bootstrap.js +470 -96
- package/package.json +1 -1
- package/skill/msd/README.md +2 -0
package/README.md
CHANGED
|
@@ -58,6 +58,25 @@ Claude Code + Codex -> 两套受管副本
|
|
|
58
58
|
|
|
59
59
|
每个物理目标都包含完整套件,薄路由器不复制核心业务规则。已有 entry 只有在 manifest 精确匹配自身名称时才允许更新;由旧版包管理的 `msdevflow` 目录会在新套件全部验证后安全迁移,来源不明的目录、文件或符号链接会阻断安装。
|
|
60
60
|
|
|
61
|
+
### WSL 与 Windows Agent
|
|
62
|
+
|
|
63
|
+
setup 以 **Agent 可执行文件实际所在环境** 决定安装侧,而不是只看命令从哪个 shell 发起:
|
|
64
|
+
|
|
65
|
+
- Agent 只安装在 Windows:可以从 WSL 调用 Windows `npx.cmd`,setup 会标记 `Runtime mode: windows-bridge-from-wsl`,并把 Skill、Python venv 和 GitCode CLI 安装在 Windows 用户环境。
|
|
66
|
+
- Agent 只安装在 WSL:必须使用 WSL 内的 Linux Node.js/npm;若误用了 Windows Node,setup 会在写入任何 Python、npm 或 Skill 文件前停止。
|
|
67
|
+
- 同名 Agent 同时存在于 Windows 与 WSL:setup 不猜目标;请从目标环境的终端重新运行。
|
|
68
|
+
|
|
69
|
+
准备 WSL 原生 Agent 时先确认以下命令全部解析为 Linux 路径,而不是 `/mnt/<drive>/...` 或 `.cmd`:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
type -a node npm npx opencode
|
|
73
|
+
npx msdevflow@latest setup
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
示例使用 OpenCode;使用 Claude Code 或 Codex 时把 `opencode` 替换为实际 Agent 命令。
|
|
77
|
+
|
|
78
|
+
Windows bridge 模式应从 WSL 中解析到 Windows Agent;也可直接在 Windows Terminal/PowerShell 中运行 setup。此前失败后可直接重跑,setup 会安全复用或修复带受管 marker 的 runtime,不需要手动删除。
|
|
79
|
+
|
|
61
80
|
自定义目录:
|
|
62
81
|
|
|
63
82
|
```bash
|
package/lib/bootstrap.js
CHANGED
|
@@ -764,11 +764,13 @@ function outputLines(output) {
|
|
|
764
764
|
return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
765
765
|
}
|
|
766
766
|
|
|
767
|
-
|
|
768
|
-
|
|
767
|
+
function executableLookup(name, platform) {
|
|
768
|
+
return platform === "win32"
|
|
769
769
|
? { command: "where.exe", args: [name] }
|
|
770
770
|
: { command: "sh", args: ["-c", "command -v -- \"$1\"", "sh", name] };
|
|
771
|
-
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function executableFromLookup(result, platform) {
|
|
772
774
|
if (result.status !== 0) {
|
|
773
775
|
return "";
|
|
774
776
|
}
|
|
@@ -779,10 +781,237 @@ export function resolveExecutable(name, run, platform) {
|
|
|
779
781
|
return candidates[0] || "";
|
|
780
782
|
}
|
|
781
783
|
|
|
784
|
+
export function resolveExecutable(name, run, platform) {
|
|
785
|
+
return executableFromLookup(
|
|
786
|
+
runOptional(run, executableLookup(name, platform)),
|
|
787
|
+
platform,
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function resolveExecutableAsync(name, run, platform) {
|
|
792
|
+
return executableFromLookup(
|
|
793
|
+
await runOptionalAsync(run, executableLookup(name, platform)),
|
|
794
|
+
platform,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
782
798
|
export function detectClients(run, platform) {
|
|
783
799
|
return SUPPORTED_CLIENTS.filter((client) => Boolean(resolveExecutable(client, run, platform)));
|
|
784
800
|
}
|
|
785
801
|
|
|
802
|
+
function wslDistroFromUnc(value) {
|
|
803
|
+
return typeof value === "string"
|
|
804
|
+
? value.match(/^\\\\wsl(?:\.localhost|\$)\\([^\\]+)/i)?.[1] || ""
|
|
805
|
+
: "";
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function windowsNodeFromWsl(environment, platform) {
|
|
809
|
+
// Windows Node keeps process.platform=win32 when a WSL shell launches npx.cmd.
|
|
810
|
+
if (platform !== "win32") {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
const distro = environment.WSL_DISTRO_NAME
|
|
814
|
+
|| wslDistroFromUnc(environment.INIT_CWD)
|
|
815
|
+
|| wslDistroFromUnc(environment.PWD);
|
|
816
|
+
if (!distro && !environment.WSL_INTEROP) {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
return { distro: distro || "" };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function detectClientExecutables(run, platform) {
|
|
823
|
+
const entries = await Promise.all(SUPPORTED_CLIENTS.map(async (client) => [
|
|
824
|
+
client,
|
|
825
|
+
await resolveExecutableAsync(client, run, platform),
|
|
826
|
+
]));
|
|
827
|
+
return Object.fromEntries(entries.filter(([, executable]) => executable));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function wslClientLookup(distro) {
|
|
831
|
+
const args = [];
|
|
832
|
+
if (distro) {
|
|
833
|
+
args.push("--distribution", distro);
|
|
834
|
+
}
|
|
835
|
+
const script = [
|
|
836
|
+
"for name do",
|
|
837
|
+
" old_ifs=$IFS",
|
|
838
|
+
" IFS=: ",
|
|
839
|
+
" for dir in $PATH; do",
|
|
840
|
+
" IFS=$old_ifs",
|
|
841
|
+
" [ -n \"$dir\" ] || dir=.",
|
|
842
|
+
" for candidate in \"$dir/$name\" \"$dir/$name.cmd\" \"$dir/$name.exe\" \"$dir/$name.bat\"; do",
|
|
843
|
+
" [ -f \"$candidate\" ] || continue",
|
|
844
|
+
" case \"$candidate\" in",
|
|
845
|
+
" /mnt/[a-zA-Z]/*)",
|
|
846
|
+
" [ -f \"${candidate}.cmd\" ] && candidate=\"${candidate}.cmd\"",
|
|
847
|
+
" windows=$(wslpath -w -- \"$candidate\" 2>/dev/null) || continue",
|
|
848
|
+
" printf '%s\\twindows\\t%s\\n' \"$name\" \"$windows\"",
|
|
849
|
+
" ;;",
|
|
850
|
+
" *)",
|
|
851
|
+
" [ -x \"$candidate\" ] || continue",
|
|
852
|
+
" native=$(readlink -f -- \"$candidate\" 2>/dev/null || printf '%s' \"$candidate\")",
|
|
853
|
+
" case \"$native\" in",
|
|
854
|
+
" /mnt/[a-zA-Z]/*)",
|
|
855
|
+
" [ -f \"${native}.cmd\" ] && native=\"${native}.cmd\"",
|
|
856
|
+
" windows=$(wslpath -w -- \"$native\" 2>/dev/null) || continue",
|
|
857
|
+
" printf '%s\\twindows\\t%s\\n' \"$name\" \"$windows\"",
|
|
858
|
+
" ;;",
|
|
859
|
+
" *) printf '%s\\twsl\\t%s\\n' \"$name\" \"$native\" ;;",
|
|
860
|
+
" esac",
|
|
861
|
+
" ;;",
|
|
862
|
+
" esac",
|
|
863
|
+
" break",
|
|
864
|
+
" done",
|
|
865
|
+
" IFS=: ",
|
|
866
|
+
" done",
|
|
867
|
+
" IFS=$old_ifs",
|
|
868
|
+
"done",
|
|
869
|
+
].join("\n");
|
|
870
|
+
args.push("--exec", "sh", "-lc", script, "sh", ...SUPPORTED_CLIENTS);
|
|
871
|
+
return { command: "wsl.exe", args };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function detectWslClientExecutables(run, distro) {
|
|
875
|
+
const result = await runOptionalAsync(run, wslClientLookup(distro));
|
|
876
|
+
if (result.status !== 0) {
|
|
877
|
+
return { wsl: {}, windows: {} };
|
|
878
|
+
}
|
|
879
|
+
const clients = { wsl: {}, windows: {} };
|
|
880
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
881
|
+
const [name, side, executable] = line.split("\t", 3);
|
|
882
|
+
if (
|
|
883
|
+
SUPPORTED_CLIENTS.includes(name)
|
|
884
|
+
&& (side === "wsl" || side === "windows")
|
|
885
|
+
&& executable
|
|
886
|
+
&& !clients[side][name]
|
|
887
|
+
) {
|
|
888
|
+
clients[side][name] = executable;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return clients;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function inspectMixedWslClients(run, context) {
|
|
895
|
+
const [windowsPath, wslLookup] = await Promise.all([
|
|
896
|
+
detectClientExecutables(run, "win32"),
|
|
897
|
+
detectWslClientExecutables(run, context.distro),
|
|
898
|
+
]);
|
|
899
|
+
return {
|
|
900
|
+
...context,
|
|
901
|
+
windows: { ...wslLookup.windows, ...windowsPath },
|
|
902
|
+
wsl: wslLookup.wsl,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function clientLocations(inspection, clients) {
|
|
907
|
+
return clients.map((client) => ({
|
|
908
|
+
client,
|
|
909
|
+
windows: inspection.windows[client] || "",
|
|
910
|
+
wsl: inspection.wsl[client] || "",
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function mixedClientError(inspection, clients, reason) {
|
|
915
|
+
const locations = clientLocations(inspection, clients);
|
|
916
|
+
const details = locations.map(({ client, windows, wsl }) => [
|
|
917
|
+
`${client}:`,
|
|
918
|
+
windows ? `Windows=${windows}` : "Windows=not-found",
|
|
919
|
+
wsl ? `WSL=${wsl}` : "WSL=not-found",
|
|
920
|
+
].join(" ")).join("; ");
|
|
921
|
+
const guidance = `Use Node.js >=18 and npm installed inside WSL, verify "type -a node npm npx ${clients.join(" ")}" resolves to Linux paths, then rerun "npx msdevflow@latest setup" from WSL. To configure a Windows agent instead, run setup from a Windows terminal. No changes were applied.`;
|
|
922
|
+
return new BootstrapError(`${reason} ${details}. ${guidance}`, 2);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function resolveMixedWslTargets(inspection, clients) {
|
|
926
|
+
const locations = clientLocations(inspection, clients);
|
|
927
|
+
const ambiguous = locations.filter(({ windows, wsl }) => windows && wsl);
|
|
928
|
+
if (ambiguous.length) {
|
|
929
|
+
throw mixedClientError(
|
|
930
|
+
inspection,
|
|
931
|
+
ambiguous.map(({ client }) => client),
|
|
932
|
+
"The selected agent exists in both Windows and WSL, so setup cannot safely choose which environment owns it.",
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
const wslOnly = locations.filter(({ windows, wsl }) => !windows && wsl);
|
|
936
|
+
if (wslOnly.length) {
|
|
937
|
+
throw mixedClientError(
|
|
938
|
+
inspection,
|
|
939
|
+
wslOnly.map(({ client }) => client),
|
|
940
|
+
"The selected agent exists only inside WSL, but msdevflow is running with Windows Node.js.",
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
const missing = locations.filter(({ windows, wsl }) => !windows && !wsl);
|
|
944
|
+
if (missing.length) {
|
|
945
|
+
throw mixedClientError(
|
|
946
|
+
inspection,
|
|
947
|
+
missing.map(({ client }) => client),
|
|
948
|
+
"The selected agent was not found in either Windows or WSL.",
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
mode: "windows-bridge-from-wsl",
|
|
953
|
+
agentExecutables: Object.fromEntries(
|
|
954
|
+
locations.map(({ client, windows }) => [client, windows]),
|
|
955
|
+
),
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
async function resolveSetupContext(
|
|
960
|
+
options,
|
|
961
|
+
environment,
|
|
962
|
+
platform,
|
|
963
|
+
run,
|
|
964
|
+
runLong,
|
|
965
|
+
selectClients,
|
|
966
|
+
progress,
|
|
967
|
+
) {
|
|
968
|
+
const context = windowsNodeFromWsl(environment, platform);
|
|
969
|
+
if (!context) {
|
|
970
|
+
return {
|
|
971
|
+
targetSelection: await resolveClientTargets(
|
|
972
|
+
options,
|
|
973
|
+
run,
|
|
974
|
+
platform,
|
|
975
|
+
selectClients,
|
|
976
|
+
),
|
|
977
|
+
runtime: {
|
|
978
|
+
mode: platform === "win32" ? "windows-native" : "posix-native",
|
|
979
|
+
agentExecutables: {},
|
|
980
|
+
},
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const inspection = await progress.run(
|
|
985
|
+
"Inspecting Windows and WSL agent installations",
|
|
986
|
+
() => inspectMixedWslClients(runLong, context),
|
|
987
|
+
);
|
|
988
|
+
let targetSelection;
|
|
989
|
+
if (options.targetsSpecified) {
|
|
990
|
+
targetSelection = {
|
|
991
|
+
clients: options.targets,
|
|
992
|
+
source: "explicit",
|
|
993
|
+
detected: [],
|
|
994
|
+
};
|
|
995
|
+
} else if (options.skillsDir) {
|
|
996
|
+
targetSelection = {
|
|
997
|
+
clients: ["claude"],
|
|
998
|
+
source: "legacy-skills-dir",
|
|
999
|
+
detected: [],
|
|
1000
|
+
};
|
|
1001
|
+
} else {
|
|
1002
|
+
const detected = SUPPORTED_CLIENTS.filter(
|
|
1003
|
+
(client) => inspection.windows[client] || inspection.wsl[client],
|
|
1004
|
+
);
|
|
1005
|
+
targetSelection = detected.length
|
|
1006
|
+
? { clients: detected, source: "detected", detected }
|
|
1007
|
+
: { clients: await selectClients(), source: "selected", detected: [] };
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
targetSelection,
|
|
1011
|
+
runtime: resolveMixedWslTargets(inspection, targetSelection.clients),
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
786
1015
|
async function selectClientTargets(input, output, promptOutput = output) {
|
|
787
1016
|
if (!input.isTTY || !output.isTTY) {
|
|
788
1017
|
throw new BootstrapError(
|
|
@@ -849,60 +1078,95 @@ function hasPythonShebang(file) {
|
|
|
849
1078
|
}
|
|
850
1079
|
}
|
|
851
1080
|
|
|
852
|
-
function
|
|
853
|
-
|
|
1081
|
+
function pythonCandidates(platform) {
|
|
1082
|
+
return platform === "win32"
|
|
854
1083
|
? [{ command: "py", prefix: ["-3"] }, { command: "python", prefix: [] }, { command: "python3", prefix: [] }]
|
|
855
1084
|
: [{ command: "python3", prefix: [] }, { command: "python", prefix: [] }];
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function parsePythonScriptDirectory(result) {
|
|
1088
|
+
if (result.status !== 0) {
|
|
1089
|
+
return "";
|
|
1090
|
+
}
|
|
1091
|
+
try {
|
|
1092
|
+
const directory = JSON.parse(result.stdout.trim());
|
|
1093
|
+
return typeof directory === "string" ? directory : "";
|
|
1094
|
+
} catch {
|
|
1095
|
+
return "";
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function pythonScriptDirectories(run, platform) {
|
|
1100
|
+
const probe = "import json,sysconfig; print(json.dumps(sysconfig.get_path('scripts')))";
|
|
1101
|
+
return pythonCandidates(platform)
|
|
1102
|
+
.map((candidate) => parsePythonScriptDirectory(runOptional(run, {
|
|
1103
|
+
command: candidate.command,
|
|
1104
|
+
args: [...candidate.prefix, "-c", probe],
|
|
1105
|
+
})))
|
|
1106
|
+
.filter(Boolean);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
async function pythonScriptDirectoriesAsync(run, platform) {
|
|
856
1110
|
const probe = "import json,sysconfig; print(json.dumps(sysconfig.get_path('scripts')))";
|
|
857
1111
|
const directories = [];
|
|
858
|
-
for (const candidate of
|
|
859
|
-
const
|
|
1112
|
+
for (const candidate of pythonCandidates(platform)) {
|
|
1113
|
+
const directory = parsePythonScriptDirectory(await runOptionalAsync(run, {
|
|
860
1114
|
command: candidate.command,
|
|
861
1115
|
args: [...candidate.prefix, "-c", probe],
|
|
862
|
-
});
|
|
863
|
-
if (
|
|
864
|
-
|
|
865
|
-
}
|
|
866
|
-
try {
|
|
867
|
-
const directory = JSON.parse(result.stdout.trim());
|
|
868
|
-
if (typeof directory === "string" && directory) {
|
|
869
|
-
directories.push(directory);
|
|
870
|
-
}
|
|
871
|
-
} catch {
|
|
872
|
-
continue;
|
|
1116
|
+
}));
|
|
1117
|
+
if (directory) {
|
|
1118
|
+
directories.push(directory);
|
|
873
1119
|
}
|
|
874
1120
|
}
|
|
875
1121
|
return directories;
|
|
876
1122
|
}
|
|
877
1123
|
|
|
878
|
-
function
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1124
|
+
function isKnownPythonGitcodePath(executable) {
|
|
1125
|
+
return /[\\/]Python[^\\/]*[\\/]Scripts[\\/]gitcode(?:\.exe)?$/i.test(executable)
|
|
1126
|
+
|| /[\\/]pipx[\\/]/i.test(executable)
|
|
1127
|
+
|| hasPythonShebang(executable);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function executableMatchesPythonScripts(executable, directories, platform) {
|
|
884
1131
|
const directory = normalizedPath(executableDirectory(executable, platform), platform);
|
|
885
|
-
return
|
|
886
|
-
.some((candidate) => normalizedPath(candidate, platform) === directory);
|
|
1132
|
+
return directories.some((candidate) => normalizedPath(candidate, platform) === directory);
|
|
887
1133
|
}
|
|
888
1134
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1135
|
+
function isPythonGitcode(executable, run, platform) {
|
|
1136
|
+
return isKnownPythonGitcodePath(executable)
|
|
1137
|
+
|| executableMatchesPythonScripts(
|
|
1138
|
+
executable,
|
|
1139
|
+
pythonScriptDirectories(run, platform),
|
|
1140
|
+
platform,
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function isPythonGitcodeAsync(executable, run, platform) {
|
|
1145
|
+
return isKnownPythonGitcodePath(executable)
|
|
1146
|
+
|| executableMatchesPythonScripts(
|
|
1147
|
+
executable,
|
|
1148
|
+
await pythonScriptDirectoriesAsync(run, platform),
|
|
1149
|
+
platform,
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function absentGitcodeDiagnosis() {
|
|
1154
|
+
return {
|
|
1155
|
+
classification: "absent",
|
|
1156
|
+
existingExecutable: null,
|
|
1157
|
+
workflowCommand: "gitcode",
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function pythonGitcodeDiagnosis(executable) {
|
|
1162
|
+
return {
|
|
1163
|
+
classification: "python",
|
|
1164
|
+
existingExecutable: executable,
|
|
1165
|
+
workflowCommand: "gitcode-npm",
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function npmGitcodeDiagnosis(executable, doctor) {
|
|
906
1170
|
if (doctor.status === 0) {
|
|
907
1171
|
try {
|
|
908
1172
|
const metadata = JSON.parse(doctor.stdout);
|
|
@@ -920,23 +1184,74 @@ export function diagnoseGitcode(run, platform) {
|
|
|
920
1184
|
throw new BootstrapError(`Existing gitcode has unknown ownership: ${executable}. Refusing to overwrite it.`, 3);
|
|
921
1185
|
}
|
|
922
1186
|
|
|
923
|
-
function
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
1187
|
+
export function diagnoseGitcode(run, platform) {
|
|
1188
|
+
const executable = resolveExecutable("gitcode", run, platform);
|
|
1189
|
+
if (!executable) {
|
|
1190
|
+
return absentGitcodeDiagnosis();
|
|
1191
|
+
}
|
|
1192
|
+
if (isPythonGitcode(executable, run, platform)) {
|
|
1193
|
+
return pythonGitcodeDiagnosis(executable);
|
|
1194
|
+
}
|
|
1195
|
+
return npmGitcodeDiagnosis(
|
|
1196
|
+
executable,
|
|
1197
|
+
runOptional(run, executableCommand(executable, ["doctor", "install", "--json"], platform)),
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
async function diagnoseGitcodeAsync(run, platform) {
|
|
1202
|
+
const executable = await resolveExecutableAsync("gitcode", run, platform);
|
|
1203
|
+
if (!executable) {
|
|
1204
|
+
return absentGitcodeDiagnosis();
|
|
1205
|
+
}
|
|
1206
|
+
if (await isPythonGitcodeAsync(executable, run, platform)) {
|
|
1207
|
+
return pythonGitcodeDiagnosis(executable);
|
|
1208
|
+
}
|
|
1209
|
+
return npmGitcodeDiagnosis(
|
|
1210
|
+
executable,
|
|
1211
|
+
await runOptionalAsync(
|
|
1212
|
+
run,
|
|
1213
|
+
executableCommand(executable, ["doctor", "install", "--json"], platform),
|
|
1214
|
+
),
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function parsePythonProbe(result) {
|
|
1219
|
+
if (result.status !== 0) {
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
try {
|
|
1223
|
+
const data = JSON.parse(result.stdout.trim());
|
|
1224
|
+
if (data.version[0] > 3 || (data.version[0] === 3 && data.version[1] >= 10)) {
|
|
1225
|
+
return data;
|
|
1226
|
+
}
|
|
1227
|
+
} catch {
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function pythonProbe(candidate) {
|
|
927
1234
|
const probe = "import json,sys; print(json.dumps({'executable':sys.executable,'version':list(sys.version_info[:3])}))";
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1235
|
+
return { command: candidate.command, args: [...candidate.prefix, "-c", probe] };
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function detectPython(run, platform) {
|
|
1239
|
+
for (const candidate of pythonCandidates(platform)) {
|
|
1240
|
+
const data = parsePythonProbe(runOptional(run, pythonProbe(candidate)));
|
|
1241
|
+
if (data) {
|
|
1242
|
+
return data;
|
|
932
1243
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1244
|
+
}
|
|
1245
|
+
throw new BootstrapError("Python >=3.10 is required for workflow scripts.", 2);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
async function detectPythonAsync(run, platform) {
|
|
1249
|
+
for (const candidate of pythonCandidates(platform)) {
|
|
1250
|
+
const python = parsePythonProbe(
|
|
1251
|
+
await runOptionalAsync(run, pythonProbe(candidate)),
|
|
1252
|
+
);
|
|
1253
|
+
if (python) {
|
|
1254
|
+
return python;
|
|
940
1255
|
}
|
|
941
1256
|
}
|
|
942
1257
|
throw new BootstrapError("Python >=3.10 is required for workflow scripts.", 2);
|
|
@@ -1004,7 +1319,7 @@ export function pythonRuntimeDetails(
|
|
|
1004
1319
|
return pythonRuntimeAt(directory, python, platform);
|
|
1005
1320
|
}
|
|
1006
1321
|
|
|
1007
|
-
function validatePythonRuntime(runtime, run) {
|
|
1322
|
+
async function validatePythonRuntime(runtime, run) {
|
|
1008
1323
|
let marker;
|
|
1009
1324
|
try {
|
|
1010
1325
|
const metadata = lstatSync(runtime.directory);
|
|
@@ -1043,10 +1358,10 @@ function validatePythonRuntime(runtime, run) {
|
|
|
1043
1358
|
closeSync(descriptor);
|
|
1044
1359
|
}
|
|
1045
1360
|
}
|
|
1046
|
-
if (
|
|
1361
|
+
if ((await runOptionalAsync(run, {
|
|
1047
1362
|
command: runtime.executable,
|
|
1048
1363
|
args: ["-m", "pip", "--version"],
|
|
1049
|
-
}).status !== 0) {
|
|
1364
|
+
})).status !== 0) {
|
|
1050
1365
|
return "repair";
|
|
1051
1366
|
}
|
|
1052
1367
|
return "current";
|
|
@@ -1069,8 +1384,8 @@ function writePythonRuntimeMarker(runtime) {
|
|
|
1069
1384
|
writeFileSync(runtime.marker, PYTHON_RUNTIME_MARKER_CONTENT, { encoding: "utf8", flag: "wx" });
|
|
1070
1385
|
}
|
|
1071
1386
|
|
|
1072
|
-
async function ensurePythonRuntime(runtime, status, runLong
|
|
1073
|
-
if (validatePythonRuntime(runtime,
|
|
1387
|
+
async function ensurePythonRuntime(runtime, status, runLong) {
|
|
1388
|
+
if ((await validatePythonRuntime(runtime, runLong)) !== status) {
|
|
1074
1389
|
throw new BootstrapError(
|
|
1075
1390
|
`Managed Python runtime changed after confirmation: ${runtime.directory}`,
|
|
1076
1391
|
3,
|
|
@@ -1155,11 +1470,15 @@ export function gitcodeInstallDetails(
|
|
|
1155
1470
|
npmCommand: ["npm", "install", "-g", GITCODE_PACKAGE, "--prefix", installPrefix, `--registry=${REGISTRY}`],
|
|
1156
1471
|
};
|
|
1157
1472
|
}
|
|
1473
|
+
const installPrefix = npmPrefix || "<npm-global-prefix>";
|
|
1158
1474
|
return {
|
|
1159
1475
|
mode: classification === "npm-official" ? "upgrade-npm" : "standard",
|
|
1160
1476
|
workflowCommand: "gitcode",
|
|
1161
|
-
target:
|
|
1162
|
-
cliTarget:
|
|
1477
|
+
target: installPrefix,
|
|
1478
|
+
cliTarget: pathApi.join(
|
|
1479
|
+
installPrefix,
|
|
1480
|
+
...(platform === "win32" ? ["gitcode.cmd"] : ["bin", "gitcode"]),
|
|
1481
|
+
),
|
|
1163
1482
|
wrapperDir: null,
|
|
1164
1483
|
wrapper: null,
|
|
1165
1484
|
npmCommand: ["npm", "install", "-g", GITCODE_PACKAGE, `--registry=${REGISTRY}`],
|
|
@@ -1255,12 +1574,20 @@ async function installGitcode(plan, runLong, run, platform) {
|
|
|
1255
1574
|
};
|
|
1256
1575
|
}
|
|
1257
1576
|
const executable = resolveExecutable("gitcode", run, platform);
|
|
1258
|
-
if (
|
|
1259
|
-
|
|
1577
|
+
if (executable) {
|
|
1578
|
+
return {
|
|
1579
|
+
command: plan.gitcodeInstall.workflowCommand,
|
|
1580
|
+
executable,
|
|
1581
|
+
};
|
|
1260
1582
|
}
|
|
1583
|
+
// The parent process keeps its original PATH after npm creates a global shim.
|
|
1584
|
+
verifyFile(
|
|
1585
|
+
plan.gitcodeInstall.cliTarget,
|
|
1586
|
+
`GitCode npm CLI was installed but its executable was not found on PATH or at ${plan.gitcodeInstall.cliTarget}.`,
|
|
1587
|
+
);
|
|
1261
1588
|
return {
|
|
1262
1589
|
command: plan.gitcodeInstall.workflowCommand,
|
|
1263
|
-
executable,
|
|
1590
|
+
executable: plan.gitcodeInstall.cliTarget,
|
|
1264
1591
|
};
|
|
1265
1592
|
}
|
|
1266
1593
|
|
|
@@ -1286,6 +1613,10 @@ function safeAuthStatus(output) {
|
|
|
1286
1613
|
function printPlan(plan, write) {
|
|
1287
1614
|
write("Setup plan (no changes made yet):");
|
|
1288
1615
|
write(` Client targets: ${plan.clientTargets.clients.join(", ")} (${plan.clientTargets.source})`);
|
|
1616
|
+
write(` Runtime mode: ${plan.clientTargets.runtimeMode}`);
|
|
1617
|
+
for (const [client, executable] of Object.entries(plan.clientTargets.agentExecutables)) {
|
|
1618
|
+
write(` Agent executable (${client}): ${executable}`);
|
|
1619
|
+
}
|
|
1289
1620
|
write(` Bundled skill suite: ${plan.skillInstalls[0].details.source}`);
|
|
1290
1621
|
for (const { layout, details } of plan.skillInstalls) {
|
|
1291
1622
|
write(` Skill root: ${details.target}`);
|
|
@@ -1422,6 +1753,7 @@ async function authenticateGitcode(
|
|
|
1422
1753
|
function textResult(result, write) {
|
|
1423
1754
|
write("Setup completed:");
|
|
1424
1755
|
write(` Client targets: ${result.clients.join(", ")}`);
|
|
1756
|
+
write(` Runtime mode: ${result.runtimeMode}`);
|
|
1425
1757
|
for (const skill of result.skills) {
|
|
1426
1758
|
write(` Skill suite: ${skill.status} at ${skill.target} (${skill.clients.join(", ")})`);
|
|
1427
1759
|
write(` Entries: ${skill.entries.map((entry) => entry.name).join(", ")}`);
|
|
@@ -1456,16 +1788,20 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1456
1788
|
writeProgress,
|
|
1457
1789
|
dependencies.timers,
|
|
1458
1790
|
);
|
|
1459
|
-
const
|
|
1791
|
+
const setupContext = await resolveSetupContext(
|
|
1460
1792
|
options,
|
|
1461
|
-
|
|
1793
|
+
environment,
|
|
1462
1794
|
platform,
|
|
1795
|
+
run,
|
|
1796
|
+
runLong,
|
|
1463
1797
|
dependencies.selectClients || (() => selectClientTargets(
|
|
1464
1798
|
process.stdin,
|
|
1465
1799
|
process.stdout,
|
|
1466
1800
|
options.json ? process.stderr : process.stdout,
|
|
1467
1801
|
)),
|
|
1802
|
+
progress,
|
|
1468
1803
|
);
|
|
1804
|
+
const targetSelection = setupContext.targetSelection;
|
|
1469
1805
|
const layouts = resolveLayouts(
|
|
1470
1806
|
options,
|
|
1471
1807
|
targetSelection.clients,
|
|
@@ -1482,6 +1818,8 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1482
1818
|
}));
|
|
1483
1819
|
const clientTargets = {
|
|
1484
1820
|
...targetSelection,
|
|
1821
|
+
runtimeMode: setupContext.runtime.mode,
|
|
1822
|
+
agentExecutables: setupContext.runtime.agentExecutables,
|
|
1485
1823
|
warnings: clientTargetWarnings(targetSelection.clients, layouts),
|
|
1486
1824
|
};
|
|
1487
1825
|
|
|
@@ -1499,7 +1837,42 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1499
1837
|
platform,
|
|
1500
1838
|
)),
|
|
1501
1839
|
);
|
|
1502
|
-
const diagnosis =
|
|
1840
|
+
const { diagnosis, python, pythonRuntime } = await progress.run(
|
|
1841
|
+
"Inspecting local GitCode and Python environment",
|
|
1842
|
+
async () => {
|
|
1843
|
+
const [diagnosisResult, pythonResult] = await Promise.all([
|
|
1844
|
+
diagnoseGitcodeAsync(runLong, platform),
|
|
1845
|
+
detectPythonAsync(runLong, platform),
|
|
1846
|
+
]);
|
|
1847
|
+
const configuredPythonRuntime = pythonRuntimeDetails(
|
|
1848
|
+
pythonResult,
|
|
1849
|
+
environment,
|
|
1850
|
+
platform,
|
|
1851
|
+
);
|
|
1852
|
+
const runtime = dependencies.pythonRuntimeDir
|
|
1853
|
+
? pythonRuntimeAt(dependencies.pythonRuntimeDir, pythonResult, platform)
|
|
1854
|
+
: configuredPythonRuntime;
|
|
1855
|
+
const [venvCapability, runtimeStatus] = await Promise.all([
|
|
1856
|
+
runOptionalAsync(runLong, {
|
|
1857
|
+
command: pythonResult.executable,
|
|
1858
|
+
args: ["-m", "venv", "--help"],
|
|
1859
|
+
}),
|
|
1860
|
+
validatePythonRuntime(runtime, runLong),
|
|
1861
|
+
]);
|
|
1862
|
+
if (venvCapability.status !== 0) {
|
|
1863
|
+
throw new BootstrapError(
|
|
1864
|
+
`Python venv is unavailable for ${pythonResult.executable}. Install the venv component for this Python (for example python3-venv on Debian/Ubuntu) and rerun setup.`,
|
|
1865
|
+
2,
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
runtime.status = runtimeStatus;
|
|
1869
|
+
return {
|
|
1870
|
+
diagnosis: diagnosisResult,
|
|
1871
|
+
python: pythonResult,
|
|
1872
|
+
pythonRuntime: runtime,
|
|
1873
|
+
};
|
|
1874
|
+
},
|
|
1875
|
+
);
|
|
1503
1876
|
diagnosis.officialLatest = latestResult.status === 0 ? latestResult.stdout.trim() || null : null;
|
|
1504
1877
|
const gitcodeInstall = gitcodeInstallDetails(diagnosis.classification, environment, platform, npmPrefix);
|
|
1505
1878
|
if (gitcodeInstall.wrapper) {
|
|
@@ -1511,22 +1884,6 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1511
1884
|
platform,
|
|
1512
1885
|
);
|
|
1513
1886
|
|
|
1514
|
-
const python = detectPython(run, platform);
|
|
1515
|
-
const venvCapability = runOptional(run, {
|
|
1516
|
-
command: python.executable,
|
|
1517
|
-
args: ["-m", "venv", "--help"],
|
|
1518
|
-
});
|
|
1519
|
-
if (venvCapability.status !== 0) {
|
|
1520
|
-
throw new BootstrapError(
|
|
1521
|
-
`Python venv is unavailable for ${python.executable}. Install the venv component for this Python (for example python3-venv on Debian/Ubuntu) and rerun setup.`,
|
|
1522
|
-
2,
|
|
1523
|
-
);
|
|
1524
|
-
}
|
|
1525
|
-
const configuredPythonRuntime = pythonRuntimeDetails(python, environment, platform);
|
|
1526
|
-
const pythonRuntime = dependencies.pythonRuntimeDir
|
|
1527
|
-
? pythonRuntimeAt(dependencies.pythonRuntimeDir, python, platform)
|
|
1528
|
-
: configuredPythonRuntime;
|
|
1529
|
-
pythonRuntime.status = validatePythonRuntime(pythonRuntime, run);
|
|
1530
1887
|
const venvInvocation = {
|
|
1531
1888
|
command: python.executable,
|
|
1532
1889
|
args: ["-m", "venv", pythonRuntime.directory],
|
|
@@ -1554,14 +1911,29 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1554
1911
|
if (!options.yes && !(await (dependencies.confirm || confirmPlan)(process.stdin, process.stdout))) {
|
|
1555
1912
|
throw new BootstrapError("Setup cancelled; no changes were applied.", 2);
|
|
1556
1913
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1914
|
+
await progress.run("Revalidating environment ownership", async () => {
|
|
1915
|
+
const runtimeContext = windowsNodeFromWsl(environment, platform);
|
|
1916
|
+
if (clientTargets.runtimeMode === "windows-bridge-from-wsl") {
|
|
1917
|
+
const currentRuntime = resolveMixedWslTargets(
|
|
1918
|
+
await inspectMixedWslClients(runLong, runtimeContext),
|
|
1919
|
+
clientTargets.clients,
|
|
1920
|
+
);
|
|
1921
|
+
if (JSON.stringify(currentRuntime) !== JSON.stringify(setupContext.runtime)) {
|
|
1922
|
+
throw new BootstrapError(
|
|
1923
|
+
"Agent installation locations changed after confirmation; refusing to install.",
|
|
1924
|
+
3,
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
const currentDiagnosis = await diagnoseGitcodeAsync(runLong, platform);
|
|
1929
|
+
if (currentDiagnosis.classification !== diagnosis.classification
|
|
1930
|
+
|| currentDiagnosis.existingExecutable !== diagnosis.existingExecutable) {
|
|
1931
|
+
throw new BootstrapError("GitCode command ownership changed after confirmation; refusing to install.", 3);
|
|
1932
|
+
}
|
|
1933
|
+
if (gitcodeInstall.wrapper) {
|
|
1934
|
+
validateWrapper(gitcodeInstall, platform);
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1565
1937
|
|
|
1566
1938
|
await progress.run(
|
|
1567
1939
|
pythonRuntime.status === "absent"
|
|
@@ -1569,7 +1941,7 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1569
1941
|
: pythonRuntime.status === "repair"
|
|
1570
1942
|
? "Repairing the managed Python runtime"
|
|
1571
1943
|
: "Validating the managed Python runtime",
|
|
1572
|
-
() => ensurePythonRuntime(pythonRuntime, pythonRuntime.status, runLong
|
|
1944
|
+
() => ensurePythonRuntime(pythonRuntime, pythonRuntime.status, runLong),
|
|
1573
1945
|
);
|
|
1574
1946
|
await progress.run("Installing reviewed Python dependencies", () => runLong(pipInvocation));
|
|
1575
1947
|
await progress.run(
|
|
@@ -1613,6 +1985,8 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1613
1985
|
git: gitVersion,
|
|
1614
1986
|
clients: clientTargets.clients,
|
|
1615
1987
|
targetSource: clientTargets.source,
|
|
1988
|
+
runtimeMode: clientTargets.runtimeMode,
|
|
1989
|
+
agentExecutables: clientTargets.agentExecutables,
|
|
1616
1990
|
warnings: clientTargets.warnings,
|
|
1617
1991
|
skills,
|
|
1618
1992
|
gitcode,
|
package/package.json
CHANGED
package/skill/msd/README.md
CHANGED
|
@@ -273,6 +273,8 @@ Claude Code + Codex -> 两套受管副本
|
|
|
273
273
|
|
|
274
274
|
每个物理目标都包含一个 `msd` 核心和十个 `msd-<action>` 路由器;业务规则仍只有一份。
|
|
275
275
|
|
|
276
|
+
WSL 中的安装侧按 Agent 可执行文件实际所在环境决定:Agent 只在 Windows 时允许通过 Windows Node bridge 安装到 Windows 用户环境;Agent 只在 WSL 时必须使用 WSL 内 Linux Node/npm;同名 Agent 同时存在于两侧时停止而不猜测。WSL 原生模式先用 `type -a node npm npx opencode`(以实际选中的 Agent 命令替换 `opencode`) 确认所选命令解析为 Linux 路径,而不是 `/mnt/<drive>/...` 或 `.cmd`。此前失败后可直接按正确模式重跑,受管 runtime 会被复用或修复,无需手动删除。
|
|
277
|
+
|
|
276
278
|
显式选择客户端、只读预检或自定义目录:
|
|
277
279
|
|
|
278
280
|
```bash
|