diff --git a/extensions/lobster/src/lobster-tool.test.ts b/extensions/lobster/src/lobster-tool.test.ts index 618e9e64dd..50a7dbf935 100644 --- a/extensions/lobster/src/lobster-tool.test.ts +++ b/extensions/lobster/src/lobster-tool.test.ts @@ -304,6 +304,38 @@ describe("lobster plugin tool", () => { expect(options).not.toHaveProperty("shell"); }); + it("runs Windows cmd shims with rooted dp0 tokens through Node", async () => { + setProcessPlatform("win32"); + const shimScriptPath = path.join(tempDir, "shim-dist", "lobster-cli.cjs"); + const shimPath = path.join(tempDir, "shim", "lobster.cmd"); + await fs.mkdir(path.dirname(shimScriptPath), { recursive: true }); + await fs.mkdir(path.dirname(shimPath), { recursive: true }); + await fs.writeFile(shimScriptPath, "module.exports = {};\n", "utf8"); + await fs.writeFile( + shimPath, + `@echo off\r\n"%dp0%\\..\\shim-dist\\lobster-cli.cjs" %*\r\n`, + "utf8", + ); + spawnState.queue.push({ + stdout: JSON.stringify({ + ok: true, + status: "ok", + output: [{ hello: "rooted" }], + requiresApproval: null, + }), + }); + + const tool = createLobsterTool(fakeApi({ pluginConfig: { lobsterPath: shimPath } })); + await tool.execute("call-win-rooted-shim", { + action: "run", + pipeline: "noop", + }); + + const [command, argv] = spawnState.spawn.mock.calls[0] ?? []; + expect(command).toBe(process.execPath); + expect(argv).toEqual([shimScriptPath, "run", "--mode", "tool", "noop"]); + }); + it("ignores node.exe shim entries and resolves the actual lobster script", async () => { setProcessPlatform("win32"); const shimDir = path.join(tempDir, "shim-with-node"); diff --git a/extensions/lobster/src/windows-spawn.ts b/extensions/lobster/src/windows-spawn.ts index f13d788490..a5c4c2bc9f 100644 --- a/extensions/lobster/src/windows-spawn.ts +++ b/extensions/lobster/src/windows-spawn.ts @@ -121,13 +121,30 @@ function resolveLobsterScriptFromCmdShim(wrapperPath: string): string | null { try { const content = fs.readFileSync(wrapperPath, "utf8"); const candidates: string[] = []; - const matches = content.matchAll(/"%~?dp0%\\([^"\r\n]+)"/gi); - for (const match of matches) { + const extractRelativeFromToken = (token: string): string | null => { + const match = token.match(/%~?dp0%\s*[\\/]*(.*)$/i); + if (!match) { + return null; + } const relative = match[1]; + if (!relative) { + return null; + } + return relative; + }; + + const matches = content.matchAll(/"([^"\r\n]*)"/g); + for (const match of matches) { + const token = match[1] ?? ""; + const relative = extractRelativeFromToken(token); if (!relative) { continue; } - const normalizedRelative = relative.replace(/[\\/]+/g, path.sep); + + const normalizedRelative = relative + .trim() + .replace(/[\\/]+/g, path.sep) + .replace(/^[\\/]+/, ""); const candidate = path.resolve(path.dirname(wrapperPath), normalizedRelative); if (isFilePath(candidate)) { candidates.push(candidate);