Skip to content

Commit 2fd1b55

Browse files
author
moc
committed
fix(win): field-report round — extensionless shim, releases layout, pwsh first
From a full field debugging report on a real Windows deployment (anonymized): - executablePath probed the empty PATHEXT extension first for bare names, resolving mcode to the extensionless POSIX shim shipped beside mcode.cmd — spawn then fails with ENOENT. Bare names now match PATHEXT variants only; commands that already carry an extension (pwsh.exe) still probe directly. - the candidate pool for the node entry now includes the staged-installer layout releases/<version>/node_modules/@minimax-ai/code/cli.js — the same entry .mcode-launcher.cmd targets. A stale flat node_modules (0.2.x) beside a current releases/<v> now loses the version comparison instead of winning by proximity. - the last-resort PowerShell hop prefers pwsh.exe (PS7): PS 5.1 binds flag-shaped argv as its own named parameters under -File and does not forward piped stdin through the nested invocation (field-verified pair)
1 parent 6dd3f62 commit 2fd1b55

3 files changed

Lines changed: 73 additions & 10 deletions

File tree

‎plugins/hetaoBackend/mcode-dynamic-workflows/checks/win-launcher.check.mjs‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,40 @@ test('posix resolution is unchanged',async()=>{
7777
assert.equal(r.command,join(f.shim,'mcode'));assert.deepEqual(r.args,[]);
7878
}finally{await f.cleanup();}
7979
});
80+
81+
test('field layout: extensionless mcode + mcode.cmd + ps1 + stale sibling + releases/0.4.12 -> newest via node, never the bash script',async()=>{
82+
const f=await layout();try{
83+
const root=join(f.official,'.minimax-code');
84+
await install(f.shim,{}); // provides fake powershell.exe + pwsh? (powershell only)
85+
await writeFile(join(f.shim,'pwsh.exe'),'fake'); // pwsh preferred on the last-resort path
86+
await mkdir(root,{recursive:true});
87+
await writeFile(join(root,'mcode'),'#!/bin/sh'); // extensionless bash script — must never be spawned
88+
await install(root,{version:'0.2.7',npmLayout:true,withCmd:true,withPs1:true});
89+
const rel=join(root,'releases','0.4.12','node_modules','@minimax-ai','code');
90+
await mkdir(rel,{recursive:true});
91+
await writeFile(join(rel,'cli.js'),'#!/usr/bin/env node');
92+
await writeFile(join(rel,'package.json'),pkg('0.4.12'));
93+
const env={PATH:`${root};${f.shim}`,PATHEXT:'.COM;.EXE;.BAT;.CMD',SystemRoot:'C:\\Windows'};
94+
const r=await resolveMcode('mcode',{env,home:f.official,platform:'win32'});
95+
assert.equal(r.command,process.execPath,'extensionless bash script must not be selected');
96+
assert.equal(JSON.parse(await readFile(join(r.args[0],'..','package.json'),'utf8')).version,'0.4.12','releases layout wins over stale sibling');
97+
}finally{await f.cleanup();}
98+
});
99+
test('ps1 renamed, sibling 0.2.7, releases/0.4.12 present -> releases entry (not the sibling)',async()=>{
100+
const f=await layout();try{
101+
const root=join(f.official,'.minimax-code');
102+
await install(root,{version:'0.2.7',npmLayout:true,withPs1:false});
103+
const rel=join(root,'releases','0.4.12','node_modules','@minimax-ai','code');
104+
await mkdir(rel,{recursive:true});
105+
await writeFile(join(rel,'cli.js'),'x');await writeFile(join(rel,'package.json'),pkg('0.4.12'));
106+
const r=await resolveMcode('mcode',{env:winEnv(root),home:f.official,platform:'win32'});
107+
assert.equal(JSON.parse(await readFile(join(r.args[0],'..','package.json'),'utf8')).version,'0.4.12');
108+
}finally{await f.cleanup();}
109+
});
110+
test('last-resort PS hop prefers pwsh (PS7) over powershell (PS5.1 -File is broken in the field)',async()=>{
111+
const f=await layout();try{
112+
await install(f.shim,{});await writeFile(join(f.shim,'pwsh.exe'),'fake');
113+
const r=await resolveMcode('mcode',{env:winEnv(f.shim),home:f.official,platform:'win32'});
114+
assert.match(r.command,/pwsh\.exe$/i);
115+
}finally{await f.cleanup();}
116+
});

‎plugins/hetaoBackend/mcode-dynamic-workflows/dist/main.mjs‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13939,7 +13939,7 @@ import { constants as constants2 } from "node:fs";
1393913939
import { resolve as resolve2, relative, isAbsolute, sep } from "node:path";
1394013940

1394113941
// src/mcode-location.mjs
13942-
import { access, stat, readFile } from "node:fs/promises";
13942+
import { access, stat, readFile, readdir } from "node:fs/promises";
1394313943
import { constants } from "node:fs";
1394413944
import { delimiter, dirname, join as join2, resolve } from "node:path";
1394513945
import { homedir } from "node:os";
@@ -13958,7 +13958,8 @@ async function executablePath(command, env = process.env, platform = process.pla
1395813958
if (typeof command !== "string" || !command) return null;
1395913959
const direct = /[\\/]/.test(command);
1396013960
const dirs = direct ? [""] : (env.PATH ?? env.Path ?? "").split(platform === "win32" ? ";" : delimiter).filter(Boolean);
13961-
const extensions = platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] : [""];
13961+
const dotted = /\.[a-z0-9]+$/i.test(command);
13962+
const extensions = platform === "win32" ? [...dotted ? [""] : [], ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean)] : [""];
1396213963
for (const dir of dirs) for (const ext of extensions) {
1396313964
const file = direct ? resolve(command + ext) : resolve(join2(dir, command + ext));
1396413965
if (await fileExists(file, true, platform)) return file;
@@ -13974,11 +13975,17 @@ async function resolveMcode(command = "mcode", { env = process.env, home = homed
1397413975
}
1397513976
if (path) {
1397613977
if (platform === "win32" && /\.(cmd|bat)$/i.test(path)) {
13978+
const root = officialRoot(home, env);
1397713979
const candidates = [
1397813980
join2(dirname(path), "node_modules", "@minimax-ai", "code", "cli.js"),
13979-
join2(officialRoot(home, env), "lib", "node_modules", "@minimax-ai", "code", "cli.js"),
13980-
join2(officialRoot(home, env), "node_modules", "@minimax-ai", "code", "cli.js")
13981+
join2(root, "lib", "node_modules", "@minimax-ai", "code", "cli.js"),
13982+
join2(root, "node_modules", "@minimax-ai", "code", "cli.js")
1398113983
];
13984+
try {
13985+
for (const entry of await readdir(join2(root, "releases"), { withFileTypes: true }))
13986+
if (entry.isDirectory()) candidates.push(join2(root, "releases", entry.name, "node_modules", "@minimax-ai", "code", "cli.js"));
13987+
} catch {
13988+
}
1398213989
const versionOf = async (entry) => {
1398313990
try {
1398413991
const pkg = JSON.parse(await readFile(join2(entry, "..", "package.json"), "utf8"));
@@ -14000,7 +14007,7 @@ async function resolveMcode(command = "mcode", { env = process.env, home = homed
1400014007
if (best) return { command: process.execPath, args: [best], source };
1400114008
const launcher = join2(dirname(path), "mcode.ps1");
1400214009
if (await fileExists(launcher)) {
14003-
const powershell = await executablePath("powershell.exe", env, platform) ?? await executablePath("pwsh.exe", env, platform);
14010+
const powershell = await executablePath("pwsh.exe", env, platform) ?? await executablePath("powershell.exe", env, platform);
1400414011
if (!powershell) throw new Error("\u53D1\u73B0 MCode PowerShell \u542F\u52A8\u5668\uFF0C\u4F46\u627E\u4E0D\u5230 PowerShell\u3002");
1400514012
return { command: powershell, args: ["-NoProfile", "-File", launcher], source };
1400614013
}

‎plugins/hetaoBackend/mcode-dynamic-workflows/src/mcode-location.mjs‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { access, stat, readFile } from 'node:fs/promises';
1+
import { access, stat, readFile, readdir } from 'node:fs/promises';
22
import { constants } from 'node:fs';
33
import { delimiter, dirname, join, resolve } from 'node:path';
44
import { homedir } from 'node:os';
@@ -13,7 +13,15 @@ export async function executablePath(command, env = process.env, platform = proc
1313
if (typeof command !== 'string' || !command) return null;
1414
const direct = /[\\/]/.test(command);
1515
const dirs = direct ? [''] : (env.PATH ?? env.Path ?? '').split(platform === 'win32' ? ';' : delimiter).filter(Boolean);
16-
const extensions = platform === 'win32' ? ['', ...(env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';')] : [''];
16+
// win32: an extensionless probe is only valid when the command itself already
17+
// carries a PATHEXT extension (pwsh.exe). For bare names (mcode) install roots
18+
// ship an extensionless POSIX shim beside mcode.cmd; matching '' first resolves
19+
// to a script Windows cannot spawn (ENOENT), so bare names match PATHEXT
20+
// variants only. POSIX keeps the bare name as the only form.
21+
const dotted = /\.[a-z0-9]+$/i.test(command);
22+
const extensions = platform === 'win32'
23+
? [...(dotted ? [''] : []), ...(env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)]
24+
: [''];
1725
for (const dir of dirs) for (const ext of extensions) {
1826
const file = direct ? resolve(command + ext) : resolve(join(dir, command + ext));
1927
if (await fileExists(file, true, platform)) return file;
@@ -36,11 +44,20 @@ export async function resolveMcode(command = 'mcode', { env = process.env, home
3644
// installs coexist (PATH shim with an old sibling, newer official root),
3745
// resolve the NEWEST cli.js across layouts instead of whatever sits next to
3846
// the resolved shim — a stale 0.2.x entry lacks current exec flags.
47+
const root = officialRoot(home, env);
3948
const candidates = [
4049
join(dirname(path), 'node_modules', '@minimax-ai', 'code', 'cli.js'),
41-
join(officialRoot(home, env), 'lib', 'node_modules', '@minimax-ai', 'code', 'cli.js'),
42-
join(officialRoot(home, env), 'node_modules', '@minimax-ai', 'code', 'cli.js'),
50+
join(root, 'lib', 'node_modules', '@minimax-ai', 'code', 'cli.js'),
51+
join(root, 'node_modules', '@minimax-ai', 'code', 'cli.js'),
4352
];
53+
// The staged-installer layout keeps the current CLI under
54+
// releases/<version>/node_modules — the same entry .mcode-launcher.cmd
55+
// targets. Older roots can leave a stale flat node_modules behind, so these
56+
// compete on version like every other candidate.
57+
try {
58+
for (const entry of await readdir(join(root, 'releases'), { withFileTypes: true }))
59+
if (entry.isDirectory()) candidates.push(join(root, 'releases', entry.name, 'node_modules', '@minimax-ai', 'code', 'cli.js'));
60+
} catch { /* no releases directory */ }
4461
const versionOf = async entry => { try {
4562
const pkg = JSON.parse(await readFile(join(entry, '..', 'package.json'), 'utf8'));
4663
return String(pkg.version ?? '0.0.0').split('.').map(n => Number.parseInt(n, 10) || 0);
@@ -55,7 +72,9 @@ export async function resolveMcode(command = 'mcode', { env = process.env, home
5572
if (best) return { command: process.execPath, args: [best], source };
5673
const launcher = join(dirname(path), 'mcode.ps1');
5774
if (await fileExists(launcher)) {
58-
const powershell = await executablePath('powershell.exe', env, platform) ?? await executablePath('pwsh.exe', env, platform);
75+
// pwsh (PS7) first: PS 5.1 binds flag-shaped argv as its own named parameters
76+
// under -File and does not forward piped stdin through the nested invocation.
77+
const powershell = await executablePath('pwsh.exe', env, platform) ?? await executablePath('powershell.exe', env, platform);
5978
if (!powershell) throw new Error('发现 MCode PowerShell 启动器,但找不到 PowerShell。');
6079
return { command: powershell, args: ['-NoProfile', '-File', launcher], source };
6180
}

0 commit comments

Comments
 (0)