msdevflow 0.7.7 → 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 +259 -8
- 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
|
@@ -799,6 +799,219 @@ export function detectClients(run, platform) {
|
|
|
799
799
|
return SUPPORTED_CLIENTS.filter((client) => Boolean(resolveExecutable(client, run, platform)));
|
|
800
800
|
}
|
|
801
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
|
+
|
|
802
1015
|
async function selectClientTargets(input, output, promptOutput = output) {
|
|
803
1016
|
if (!input.isTTY || !output.isTTY) {
|
|
804
1017
|
throw new BootstrapError(
|
|
@@ -1257,11 +1470,15 @@ export function gitcodeInstallDetails(
|
|
|
1257
1470
|
npmCommand: ["npm", "install", "-g", GITCODE_PACKAGE, "--prefix", installPrefix, `--registry=${REGISTRY}`],
|
|
1258
1471
|
};
|
|
1259
1472
|
}
|
|
1473
|
+
const installPrefix = npmPrefix || "<npm-global-prefix>";
|
|
1260
1474
|
return {
|
|
1261
1475
|
mode: classification === "npm-official" ? "upgrade-npm" : "standard",
|
|
1262
1476
|
workflowCommand: "gitcode",
|
|
1263
|
-
target:
|
|
1264
|
-
cliTarget:
|
|
1477
|
+
target: installPrefix,
|
|
1478
|
+
cliTarget: pathApi.join(
|
|
1479
|
+
installPrefix,
|
|
1480
|
+
...(platform === "win32" ? ["gitcode.cmd"] : ["bin", "gitcode"]),
|
|
1481
|
+
),
|
|
1265
1482
|
wrapperDir: null,
|
|
1266
1483
|
wrapper: null,
|
|
1267
1484
|
npmCommand: ["npm", "install", "-g", GITCODE_PACKAGE, `--registry=${REGISTRY}`],
|
|
@@ -1357,12 +1574,20 @@ async function installGitcode(plan, runLong, run, platform) {
|
|
|
1357
1574
|
};
|
|
1358
1575
|
}
|
|
1359
1576
|
const executable = resolveExecutable("gitcode", run, platform);
|
|
1360
|
-
if (
|
|
1361
|
-
|
|
1577
|
+
if (executable) {
|
|
1578
|
+
return {
|
|
1579
|
+
command: plan.gitcodeInstall.workflowCommand,
|
|
1580
|
+
executable,
|
|
1581
|
+
};
|
|
1362
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
|
+
);
|
|
1363
1588
|
return {
|
|
1364
1589
|
command: plan.gitcodeInstall.workflowCommand,
|
|
1365
|
-
executable,
|
|
1590
|
+
executable: plan.gitcodeInstall.cliTarget,
|
|
1366
1591
|
};
|
|
1367
1592
|
}
|
|
1368
1593
|
|
|
@@ -1388,6 +1613,10 @@ function safeAuthStatus(output) {
|
|
|
1388
1613
|
function printPlan(plan, write) {
|
|
1389
1614
|
write("Setup plan (no changes made yet):");
|
|
1390
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
|
+
}
|
|
1391
1620
|
write(` Bundled skill suite: ${plan.skillInstalls[0].details.source}`);
|
|
1392
1621
|
for (const { layout, details } of plan.skillInstalls) {
|
|
1393
1622
|
write(` Skill root: ${details.target}`);
|
|
@@ -1524,6 +1753,7 @@ async function authenticateGitcode(
|
|
|
1524
1753
|
function textResult(result, write) {
|
|
1525
1754
|
write("Setup completed:");
|
|
1526
1755
|
write(` Client targets: ${result.clients.join(", ")}`);
|
|
1756
|
+
write(` Runtime mode: ${result.runtimeMode}`);
|
|
1527
1757
|
for (const skill of result.skills) {
|
|
1528
1758
|
write(` Skill suite: ${skill.status} at ${skill.target} (${skill.clients.join(", ")})`);
|
|
1529
1759
|
write(` Entries: ${skill.entries.map((entry) => entry.name).join(", ")}`);
|
|
@@ -1558,16 +1788,20 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1558
1788
|
writeProgress,
|
|
1559
1789
|
dependencies.timers,
|
|
1560
1790
|
);
|
|
1561
|
-
const
|
|
1791
|
+
const setupContext = await resolveSetupContext(
|
|
1562
1792
|
options,
|
|
1563
|
-
|
|
1793
|
+
environment,
|
|
1564
1794
|
platform,
|
|
1795
|
+
run,
|
|
1796
|
+
runLong,
|
|
1565
1797
|
dependencies.selectClients || (() => selectClientTargets(
|
|
1566
1798
|
process.stdin,
|
|
1567
1799
|
process.stdout,
|
|
1568
1800
|
options.json ? process.stderr : process.stdout,
|
|
1569
1801
|
)),
|
|
1802
|
+
progress,
|
|
1570
1803
|
);
|
|
1804
|
+
const targetSelection = setupContext.targetSelection;
|
|
1571
1805
|
const layouts = resolveLayouts(
|
|
1572
1806
|
options,
|
|
1573
1807
|
targetSelection.clients,
|
|
@@ -1584,6 +1818,8 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1584
1818
|
}));
|
|
1585
1819
|
const clientTargets = {
|
|
1586
1820
|
...targetSelection,
|
|
1821
|
+
runtimeMode: setupContext.runtime.mode,
|
|
1822
|
+
agentExecutables: setupContext.runtime.agentExecutables,
|
|
1587
1823
|
warnings: clientTargetWarnings(targetSelection.clients, layouts),
|
|
1588
1824
|
};
|
|
1589
1825
|
|
|
@@ -1675,7 +1911,20 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1675
1911
|
if (!options.yes && !(await (dependencies.confirm || confirmPlan)(process.stdin, process.stdout))) {
|
|
1676
1912
|
throw new BootstrapError("Setup cancelled; no changes were applied.", 2);
|
|
1677
1913
|
}
|
|
1678
|
-
await progress.run("Revalidating
|
|
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
|
+
}
|
|
1679
1928
|
const currentDiagnosis = await diagnoseGitcodeAsync(runLong, platform);
|
|
1680
1929
|
if (currentDiagnosis.classification !== diagnosis.classification
|
|
1681
1930
|
|| currentDiagnosis.existingExecutable !== diagnosis.existingExecutable) {
|
|
@@ -1736,6 +1985,8 @@ export async function runSetup(options, dependencies = {}) {
|
|
|
1736
1985
|
git: gitVersion,
|
|
1737
1986
|
clients: clientTargets.clients,
|
|
1738
1987
|
targetSource: clientTargets.source,
|
|
1988
|
+
runtimeMode: clientTargets.runtimeMode,
|
|
1989
|
+
agentExecutables: clientTargets.agentExecutables,
|
|
1739
1990
|
warnings: clientTargets.warnings,
|
|
1740
1991
|
skills,
|
|
1741
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
|