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 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: npmPrefix || "<npm-global-prefix>",
1264
- cliTarget: null,
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 (!executable) {
1361
- throw new BootstrapError("GitCode npm CLI was installed but the gitcode executable is not available on PATH.");
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 targetSelection = await resolveClientTargets(
1791
+ const setupContext = await resolveSetupContext(
1562
1792
  options,
1563
- run,
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 command ownership", async () => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msdevflow",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
4
4
  "description": "Install the msdevflow GitCode skill and its runtime dependencies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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