From 942bf154523f26ad97bcefb5eb54d5fd12ad4dc8 Mon Sep 17 00:00:00 2001 From: UABULAJIQL <1342434213@qq.com> Date: Mon, 22 Jun 2026 06:46:31 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Windows=20=E4=B8=8B=20?= =?UTF-8?q?CodeGraph=20MCP=20=E5=90=AF=E5=8A=A8=E5=91=BD=E4=BB=A4=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/windows-codegraph-launch.md | 5 +++ __tests__/codegraph.test.ts | 51 +++++++++++++++++++++++++- extensions/codegraph.ts | 45 ++++++++++++++++++++--- 3 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 .changeset/windows-codegraph-launch.md diff --git a/.changeset/windows-codegraph-launch.md b/.changeset/windows-codegraph-launch.md new file mode 100644 index 0000000..2c7400d --- /dev/null +++ b/.changeset/windows-codegraph-launch.md @@ -0,0 +1,5 @@ +--- +"@vndv/pi-codegraph": patch +--- + +Resolve the CodeGraph CLI through Windows-native command discovery before launching the MCP server on Windows, while preserving the existing direct `codegraph` spawn path on other platforms. diff --git a/__tests__/codegraph.test.ts b/__tests__/codegraph.test.ts index 9b010c7..fd3bd60 100644 --- a/__tests__/codegraph.test.ts +++ b/__tests__/codegraph.test.ts @@ -3,6 +3,7 @@ import { EventEmitter } from "node:events"; import { PassThrough } from "node:stream"; import os from "node:os"; import path from "node:path"; +import { fileURLToPath } from "node:url"; vi.mock("node:child_process", () => ({ spawn: vi.fn(() => createMockProcess()), @@ -60,12 +61,60 @@ describe("pi-codegraph extension", () => { await expect(callCodeGraphTool("codegraph_status", {})).resolves.toBe("called codegraph_status"); }); + it("uses the direct codegraph executable outside Windows", async () => { + vi.spyOn(process, "platform", "get").mockReturnValue("linux"); + const { spawn } = await import("node:child_process"); + const { withCodeGraphMcp } = await import("../extensions/codegraph.js"); + + await withCodeGraphMcp(process.cwd(), undefined, async () => "success"); + + expect(spawn).toHaveBeenCalledWith("codegraph", ["serve", "--mcp", "--path", process.cwd()], { + cwd: process.cwd(), + env: process.env, + stdio: ["pipe", "pipe", "pipe"], + }); + }); + + it("uses PowerShell command discovery for the CodeGraph executable on Windows", async () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const { spawn } = await import("node:child_process"); + const { withCodeGraphMcp } = await import("../extensions/codegraph.js"); + + await withCodeGraphMcp(process.cwd(), undefined, async () => "success"); + + const [command, args, options] = vi.mocked(spawn).mock.calls.at(-1)!; + const spawnArgs = args as string[]; + const script = spawnArgs[spawnArgs.indexOf("-Command") + 1]; + + expect(command).toBe("powershell.exe"); + expect(spawnArgs).toEqual(expect.arrayContaining([ + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-Command", + ])); + expect(script).toContain("Get-Command codegraph"); + expect(script).toContain("-CommandType Application"); + expect(script).toContain("Select-Object -First 1"); + expect(script).not.toContain("codegraph.cmd"); + expect(script).not.toMatch(/Users[\\/]cq/i); + expect(script).not.toMatch(/scoop/i); + expect(spawnArgs.at(-1)).toBe(process.cwd()); + expect(options).toEqual({ + cwd: process.cwd(), + env: process.env, + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, + }); + }); + it("validates projectPath before starting CodeGraph", async () => { const { resolveProjectCwd } = await import("../extensions/codegraph.js"); await expect(resolveProjectCwd("relative/project")).rejects.toThrow("absolute path"); await expect(resolveProjectCwd("/path/that/does/not/exist")).rejects.toThrow("does not exist"); - await expect(resolveProjectCwd(new URL(import.meta.url).pathname)).rejects.toThrow("directory"); + await expect(resolveProjectCwd(fileURLToPath(import.meta.url))).rejects.toThrow("directory"); }); it("preserves Unix paths on macOS/Linux", async () => { diff --git a/extensions/codegraph.ts b/extensions/codegraph.ts index c988082..f9be523 100644 --- a/extensions/codegraph.ts +++ b/extensions/codegraph.ts @@ -125,17 +125,52 @@ export const SessionTimeoutMs = 20_000; export const codegraphToolNames = ToolDefinitions.map((tool) => tool.name); +const WindowsCodeGraphLaunchScript = [ + "& {", + "param([string]$ProjectPath)", + "$ErrorActionPreference = 'Stop';", + "$cmd = Get-Command codegraph -CommandType Application -ErrorAction Stop | Select-Object -First 1;", + "if (-not $cmd) { throw 'codegraph command not found'; }", + "& $cmd.Source serve --mcp --path $ProjectPath;", + "if ($LASTEXITCODE -ne $null) { exit $LASTEXITCODE; }", + "}", +].join(" "); + +function spawnCodeGraphServer(cwd: string): ChildProcessWithoutNullStreams { + if (process.platform !== "win32") { + return spawn("codegraph", ["serve", "--mcp", "--path", cwd], { + cwd, + env: process.env, + stdio: ["pipe", "pipe", "pipe"], + }); + } + + // On Windows, Node's direct spawn can miss npm/Scoop command shims that the + // shell resolves correctly. Use PowerShell command discovery so global CLI + // installs are found without hardcoding .cmd, Scoop, npm, or user paths. + return spawn("powershell.exe", [ + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-Command", + WindowsCodeGraphLaunchScript, + cwd, + ], { + cwd, + env: process.env, + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, + }); +} + export async function withCodeGraphMcp( projectPath: string | undefined, signal: AbortSignal | undefined, fn: (request: JsonRpcRequest) => Promise, ): Promise { const cwd = await resolveProjectCwd(projectPath); - const child = spawn("codegraph", ["serve", "--mcp", "--path", cwd], { - cwd, - env: process.env, - stdio: ["pipe", "pipe", "pipe"], - }); + const child = spawnCodeGraphServer(cwd); const session = runJsonRpcSession(child, cwd, signal, fn); From 5e2d4b3315817393449c07df8891fde8efef99a6 Mon Sep 17 00:00:00 2001 From: Ivan Matveev Date: Mon, 22 Jun 2026 15:19:24 +0200 Subject: [PATCH 2/2] refactor: remove redundant $LASTEXITCODE -ne $null check in PowerShell script In PowerShell, $LASTEXITCODE is never $null (initialized to 0), so the if-guard was always true. Simplify to direct exit. --- extensions/codegraph.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/codegraph.ts b/extensions/codegraph.ts index f9be523..27b6c60 100644 --- a/extensions/codegraph.ts +++ b/extensions/codegraph.ts @@ -132,7 +132,7 @@ const WindowsCodeGraphLaunchScript = [ "$cmd = Get-Command codegraph -CommandType Application -ErrorAction Stop | Select-Object -First 1;", "if (-not $cmd) { throw 'codegraph command not found'; }", "& $cmd.Source serve --mcp --path $ProjectPath;", - "if ($LASTEXITCODE -ne $null) { exit $LASTEXITCODE; }", + "exit $LASTEXITCODE;", "}", ].join(" ");