Skip to content

fix(dynamic-workflow): resolve newest cli.js directly on Windows, avoid the PowerShell -File hop - #52

Open
modacker wants to merge 5 commits into
MiniMax-AI:mainfrom
modacker:community/win-launcher
Open

modacker wants to merge 5 commits into
MiniMax-AI:mainfrom
modacker:community/win-launcher

Conversation

@modacker

@modacker modacker commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

What changes

Fixes Windows mcode launcher resolution in mcode-location.mjs — two failure modes observed on a real Windows deployment (details anonymized; a user's debugging session was shared with us verbatim):

  1. PowerShell 5.1 breaks the -File hop. A resolved mcode.cmd with a sibling mcode.ps1 is invoked as powershell -NoProfile -File mcode.ps1 exec --input - --cwd .... Under -File, PowerShell binds flag-shaped tokens (-input, --cwd, --timeout …) as its own named parameters, so the exec argv never reaches the CLI — parameter-binding errors on every invocation. (The user's attempted --% stop-parsing cannot help: it affects interactive command lines, not -File argument passing.)
  2. The no-ps1 fallback accepts a stale sibling install. With the .ps1 renamed away (the user's workaround), resolution falls back to dirname(mcode.cmd)/node_modules/@minimax-ai/code/cli.js — the npm-global layout, whichever version sits there. On a machine with an old install (0.2.x, missing current exec flags) providing the PATH shim while a newer official install exists, resolution silently picked the 0.2.x entry and every exec call died on an unknown flag. The user eventually junction-linked the new cli.js into the expected path — a workaround this PR makes unnecessary.

The fix

  • On win32, prefer a directly spawnable node entry over the .ps1 hop: collect candidate cli.js locations (shim sibling node_modules, official lib/node_modules, official root node_modules) and pick the newest by package version — mixed installs resolve to the freshest CLI regardless of which install owns the PATH shim.
  • The powershell -File hop remains only as a documented last resort when no node entry exists in any layout.
  • POSIX resolution is untouched (guarded by a regression test).

Test evidence

  • New checks/win-launcher.check.mjs (7 tests): the mixed-install matrix is reproduced from POSIX by faking the win32 on-disk layout and passing platform:'win32' + controlled PATH/PATHEXT (resolution is pure filesystem probing): ps1-present-with-old-sibling + newer official → direct node entry of the newest; ps1 renamed + stale sibling → newest official, never the stale one; newest-wins regardless of origin; single npm layout; ps1 last resort preserved (with -NoProfile,-File,<launcher> argv pinned); no-entry error message preserved; POSIX unchanged.
  • TDD order: the matrix was written against the expected behavior first and verified red (4/7) on current main before the fix → 7/7 after.
  • Full plugin suite 89/89, packaged MCP smoke 1/1, byte-reproducible rebuild. Real-Windows behavior is exercised by this repository's windows CI job on the PR.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@modacker

Copy link
Copy Markdown
Contributor Author

Substantially expanded after receiving the reporter's full field debugging notes (Windows 11 / PowerShell 5.1 + pwsh 7.6.6 / Node 26 / mcode 0.4.12; identities sanitized). Their four findings, mapped to this PR:

  1. Empty-extension priority conflictexecutablePath probed the '' PATHEXT entry first, resolving mcode to the extensionless POSIX shell shim shipped beside mcode.cmd in the install root; spawn then fails with ENOENT. → Bare names now match PATHEXT variants only; commands that already carry an extension (pwsh.exe) still probe directly.
  2. PS 5.1 launcher hop broken (their A/B testing: PS7 pwsh -File works; PS 5.1 fails two ways — flag-shaped argv bound as cmdlet named parameters, and piped stdin not forwarded through the nested invocation), plus the plugin's PS priority was inverted (powershell.exe before pwsh.exe). → The main path no longer hops through PowerShell at all (direct node entry); the last-resort hop now prefers pwsh.exe.
  3. Stale fallback cli.js — the real current CLI lives in the staged-installer layout releases/<version>/node_modules/@minimax-ai/code/cli.js (what .mcode-launcher.cmd targets), while the flat node_modules beside the shim held a 0.2.x entry lacking current exec flags (unknown option '--prompt-mode'). → releases/*/node_modules/... now competes in the version-pick pool; proximity no longer beats version.
  4. The cleanest existing path was never used — equivalent outcome now achieved without any intermediate: direct node + the newest cli.js.

New regressions reproduce the reporter's exact on-disk layout from POSIX (extensionless shim + mcode.cmd + ps1 + stale flat node_modules + releases/0.4.12): red 3/3 on the previous head, green after. Suite now 10/10 launcher tests, 92/92 plugin-wide, packaged smoke 1/1, byte-reproducible rebuild.

The reporter also supplied maintainer-runnable repro commands for the PS 5.1/PS7 pair and the direct mcode.cmd path, plus failing run IDs — kept out of this comment for privacy, available on request via the reporter.

@hetaoBackend hetaoBackend left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes for exact current head 67980640498827fd2f7feec688127eb79e2ca4f4.

Two Windows-launcher correctness/evidence blockers remain:

  1. The resolver does not follow the authoritative active release. src/mcode-location.mjs:53-70 enumerates every releases/* directory and chooses the numerically largest parsed package version. It never resolves a launcher/current pointer, and the parseInt comparison does not implement SemVer or define stable-versus-prerelease/tie behavior. A retained beta/newer directory or rollback can therefore select a different CLI from the release the installed launcher would run. checks/win-launcher.check.mjs:103-112 currently locks in highest-numeric-directory behavior rather than testing the active release contract. Resolve the authoritative active target, or document and implement a complete version/channel/tie policy with rollback tests.

  2. The Windows-specific test is not run on Windows. checks/win-launcher.check.mjs:5-9 says real Windows behavior is covered by repository CI, but .github/workflows/dynamic-workflow.yml:37-48 runs only checks/process-tree.check.mjs in the Windows job. The launcher suite currently runs under POSIX while merely injecting platform: win32, so it does not exercise Windows path, PATHEXT, filesystem, or spawn semantics. Run win-launcher.check.mjs in the Windows job and preferably add an end-to-end fake cli.js spawn with spaces and flag-shaped argv.

I confirmed the good parts: .cmd and PATHEXT probing, npm-prefix layout, separated argv with shell:false, PowerShell fallback only when no Node entry is found, source/dist sync, and the existing exact-head CI are sound. [code]smith is skipped and was not used as evidence.

moc added 4 commits September 19, 2026 23:09
…ile hop

On win32, a resolved mcode.cmd currently prefers the sibling mcode.ps1 via
powershell -NoProfile -File, then falls back to dirname(cmd)/node_modules.
Two field failure modes on real installs (anonymized deployment report):

- PowerShell 5.1 binds flag-shaped tokens (-input, --cwd, ...) as its own
  named parameters under -File, breaking the exec argv entirely
- with the .ps1 renamed away, the fallback accepts whatever cli.js sits
  next to the PATH shim; when an old install (0.2.x) provides that shim
  while a newer official install exists, resolution silently picks the
  stale entry, which lacks current exec flags

Resolution now prefers a directly spawnable node entry, picking the
NEWEST cli.js across the candidate layouts (shim sibling, official
lib/node_modules, official root node_modules) by package version; the
powershell -File hop remains only as a documented last resort when no
node entry exists anywhere. POSIX resolution is unchanged.

checks/win-launcher.check.mjs covers the mixed-install matrix by faking
the win32 layout + PATH/PATHEXT from POSIX (resolution is pure filesystem
probing); real-Windows behavior is exercised by the repository's windows
CI job.
…wsh 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)
The field-layout test probed mcode.CMD (uppercase PATHEXT) against a
lowercase mcode.cmd fixture — Windows filesystems are case-insensitive so
the behavior is fine in production, but the Linux CI runner simulating
win32 is case-sensitive and resolution returned null. PATHEXT in the test
now matches the fixture files; assertion gains an explicit non-null guard.
…ck policy

- when .mcode-launcher.cmd sits beside the resolved shim, the cli.js it
  references is returned verbatim before any version arithmetic — whatever
  the installed launcher runs is what we run, so rollbacks and channel
  switches are followed for free (%~dp0 expanded, backslashes normalized)
- without a launcher pointer, candidates rank by full SemVer with an
  explicit channel/tie policy documented in-source: stable globally
  outranks prerelease (the fallback never auto-upgrades into a prerelease
  channel), numeric vs alphanumeric prerelease identifiers, build metadata
  ignored, ties broken by directory name
- end-to-end spawn fidelity test: a fake cli.js echoes its argv as JSON
  and the test asserts every token (spaced --cwd, flag-shaped tokens,
  trailing dash) survives a real node subprocess — real on whichever host
  runs it
- 15 launcher tests; the three new policy tests fail against the previous
  resolver (discriminative), all prior behavior tests unchanged
@modacker
modacker force-pushed the community/win-launcher branch from 6798064 to 4c9915e Compare September 19, 2026 15:21
@modacker

Copy link
Copy Markdown
Contributor Author

Both blockers addressed at exact head 4c9915e (rebased after #49's merge):

  1. The resolver now follows the authoritative active release. When .mcode-launcher.cmd sits beside the resolved shim, the cli.js it references is returned verbatim before any version arithmetic — whatever the installed launcher runs is what we run, so rollbacks and channel switches are followed for free (%~dp0 expanded, backslashes normalized; the pointer form was verified against the field deployment's actual layout). With no launcher pointer, candidates rank by full SemVer with an explicit channel/tie policy documented in-source: stable globally outranks prerelease (the fallback never auto-upgrades into a prerelease channel — a deliberate, documented deviation from strict core-version precedence), numeric-vs-alphanumeric prerelease identifiers per SemVer, build metadata ignored, ties broken by directory name with the staged layout preferred over flat siblings. Regressions: launcher-pointing-at-older-stable-while-a-newer-beta-exists → follows the pointer; pointer switched to the beta → follows it; no pointer → stable wins; beta.10 > beta.2; tie-break case. The three new policy tests fail against the previous resolver; prior behavior tests are unchanged.
  2. Windows execution + spawn fidelity. The launcher suite now includes an end-to-end spawn test: a fake cli.js echoes its argv as JSON and the test asserts every token (spaced --cwd, flag-shaped tokens, trailing dash) survives a real node subprocess — it is genuinely executed on whichever host runs it. For the Windows job itself, the one-line patch adding checks/win-launcher.check.mjs alongside process-tree.check.mjs in .github/workflows/dynamic-workflow.yml applies clean (git apply verified) — patch/windows-job.patch:
--- a/.github/workflows/dynamic-workflow.yml
+++ b/.github/workflows/dynamic-workflow.yml
@@ -45,4 +45,4 @@
         with:
           node-version: 22
       - run: npm ci --ignore-scripts --registry=https://registry.npmjs.org
-      - run: node --test checks/process-tree.check.mjs
+      - run: node --test checks/process-tree.check.mjs checks/win-launcher.check.mjs

We cannot push workflow files (OAuth token lacks workflow scope) — feel free to apply it yourself, or grant the scope and we'll commit it.

Validation at this head: plugin suite 117/117, packaged MCP smoke 1/1, byte-reproducible rebuild, and the full 345-test repository gate on a local Ubuntu box (345/345).

…ite in Windows CI

Real-Windows evidence (fork preview, windows-latest, Node 22): running
checks/win-launcher.check.mjs on a real runner surfaced a resolver bug the
POSIX+win32-injected suite could never catch — fs.promises.mkdir(recursive)
returns a \\?\-namespaced path on Windows, every join below keeps the
prefix, and node's main resolution (realpathSync on argv[1]) dies with
EISDIR lstat 'C:' (nodejs/node#62446, fixed by #65378 first in v24.21.0 /
v26.8.0 — this plugin supports Node >=22, so we self-defend). loadableEntry()
strips the namespace marker (drive + UNC forms) at all three node-entry
return points; POSIX strings pass through unchanged. dist regenerated in
lockstep. The Windows job now runs the launcher suite next to
process-tree.check.mjs, closing the second review blocker.
@modacker

Copy link
Copy Markdown
Contributor Author

Windows CI wiring + a real-Windows resolver fix, at exact head e3d0166 (rebased on 4c9915e, no other changes):

1. Blocker 2 closed — the Windows job now runs the launcher suite. windows-process-lifecycle gained one line: node --test checks/win-launcher.check.mjs next to process-tree.check.mjs (workflow-scope token now available, so the edit ships here instead of as a patch for you to apply).

2. Running the suite on a real runner surfaced a bug the POSIX+win32-injected suite structurally cannot catch — fixed in the same head. On windows-latest (Node 22) the end-to-end argv-fidelity case died inside node's own main resolution: EISDIR: illegal operation on a directory, lstat 'C:'. Root cause: fs.promises.mkdir(recursive) returns a \\?\-namespaced path on Windows; every join() below keeps the prefix, and when the resolver hands that entry to node <entry>, fs.realpathSync probes the namespaced drive root, the fs binding drops the trailing separator, and lstat receives the bare drive. Upstream: nodejs/node#62446, fixed by #65378 — first shipped in v24.21.0 / v26.8.0. This plugin supports Node >=22, so the resolver now self-defends: loadableEntry() strips the namespace marker (drive and UNC forms) at all three node-entry return points; POSIX strings pass through unchanged; argv semantics untouched (executor concatenates the same tokens). dist/main.mjs regenerated in lockstep; npm run build + git diff --exit-code -- dist clean.

Evidence trail (fork-side preview mirrors, windows-latest / Node 22): diagnosis run 35491670398 (14/15, EISDIR), ENTRY-instrumented run 35492809510 (captured the namespaced entry verbatim), fixed run 3549300404516/16 pass, 0 fail (one new unit test for loadableEntry included). Local: full npm test 118/118, test:package green.

The Windows check on this PR should now reproduce the green directly.

@hetaoBackend hetaoBackend left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved exact head e3d016661df26d523ab9fde67d0daf34a6262bb1. The resolver now follows the authoritative active release from .mcode-launcher.cmd before fallback scanning, and its no-pointer fallback implements an explicit stable/prerelease SemVer and tie policy with rollback/channel tests. The Windows job now runs the launcher suite on a real runner; namespace-path normalization, PATHEXT/extensionless-shim selection, direct trusted cli.js execution, argv fidelity, PowerShell last-resort behavior, and source/dist consistency are covered. Exact-head Ubuntu, Windows, package, and CodeQL checks are green. [code]smith is skipped and was not used as evidence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants