From 89b64d9dd5411e40092d4417b8d77ce911092828 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 08:37:43 +0200 Subject: [PATCH 1/4] fix(coding-agent): avoid Windows self-update native locks closes #4157 --- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/src/main.ts | 7 +- .../coding-agent/src/package-manager-cli.ts | 24 ++++++ .../src/utils/windows-self-update.ts | 84 +++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 packages/coding-agent/src/utils/windows-self-update.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 969d9d32..822b83ec 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). + ## [0.75.1] - 2026-05-18 ### Fixed diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 72e502be..9f1af697 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -15,7 +15,7 @@ import { processFileArguments } from "./cli/file-processor.js"; import { buildInitialMessage } from "./cli/initial-message.js"; import { listModels } from "./cli/list-models.js"; import { selectSession } from "./cli/session-picker.js"; -import { ENV_SESSION_DIR, expandTildePath, getAgentDir, VERSION } from "./config.js"; +import { ENV_SESSION_DIR, expandTildePath, getAgentDir, getPackageDir, VERSION } from "./config.js"; import { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from "./core/agent-session-runtime.js"; import { type AgentSessionRuntimeDiagnostic, @@ -46,6 +46,7 @@ import { ExtensionSelectorComponent } from "./modes/interactive/components/exten import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js"; import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js"; import { isLocalPath } from "./utils/paths.js"; +import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.js"; /** * Read all content from piped stdin. @@ -428,6 +429,10 @@ export async function main(args: string[], options?: MainOptions) { process.env.PI_SKIP_VERSION_CHECK = "1"; } + if (process.platform === "win32") { + cleanupWindowsSelfUpdateQuarantine(getPackageDir()); + } + if (await handlePackageCommand(args)) { return; } diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index f421af32..a3ef1e21 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -3,7 +3,9 @@ import { spawn } from "child_process"; import { selectConfig } from "./cli/config-selector.js"; import { APP_NAME, + detectInstallMethod, getAgentDir, + getPackageDir, getSelfUpdateCommand, getSelfUpdateUnavailableInstruction, PACKAGE_NAME, @@ -14,6 +16,10 @@ import { DefaultPackageManager } from "./core/package-manager.js"; import { SettingsManager } from "./core/settings-manager.js"; import { resolveSpawnCommand } from "./utils/child-process.js"; import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.js"; +import { + cleanupWindowsSelfUpdateQuarantine, + quarantineWindowsNativeDependencies, +} from "./utils/windows-self-update.js"; export type PackageCommand = "install" | "remove" | "update" | "list"; @@ -336,6 +342,16 @@ async function runSelfUpdate(command: SelfUpdateCommand): Promise { } } +function prepareWindowsNpmSelfUpdate(): void { + if (process.platform !== "win32") { + return; + } + + const packageDir = getPackageDir(); + cleanupWindowsSelfUpdateQuarantine(packageDir); + quarantineWindowsNativeDependencies(packageDir); +} + export async function handleConfigCommand(args: string[]): Promise { if (args[0] !== "config") { return false; @@ -489,6 +505,13 @@ export async function handlePackageCommand(args: string[]): Promise { if (!selfUpdatePlan.shouldRun) { return true; } + const installMethod = detectInstallMethod(); + if (process.platform === "win32" && installMethod !== "npm") { + console.error(chalk.red(`${APP_NAME} self-update on Windows is only supported for npm installs.`)); + console.error(chalk.dim(`Detected install method: ${installMethod}. Update ${APP_NAME} manually.`)); + process.exitCode = 1; + return true; + } const selfUpdateCommand = getSelfUpdateCommand( PACKAGE_NAME, selfUpdateNpmCommand, @@ -500,6 +523,7 @@ export async function handlePackageCommand(args: string[]): Promise { return true; } try { + prepareWindowsNpmSelfUpdate(); await runSelfUpdate(selfUpdateCommand); } catch (error: unknown) { const message = error instanceof Error ? error.message : "Unknown package command error"; diff --git a/packages/coding-agent/src/utils/windows-self-update.ts b/packages/coding-agent/src/utils/windows-self-update.ts new file mode 100644 index 00000000..a988529e --- /dev/null +++ b/packages/coding-agent/src/utils/windows-self-update.ts @@ -0,0 +1,84 @@ +import { randomUUID } from "node:crypto"; +import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync } from "node:fs"; +import { basename, dirname, join, relative, resolve, toNamespacedPath } from "node:path"; +import { getCwdRelativePath } from "./paths.js"; + +const QUARANTINE_DIR_NAME = ".pi-native-quarantine"; + +function normalizePath(path: string): string { + return toNamespacedPath(resolve(path)); +} + +function getQuarantineRoot(packageDir: string): string | undefined { + let current = resolve(packageDir); + while (true) { + if (basename(current).toLowerCase() === "node_modules") { + return join(current, QUARANTINE_DIR_NAME); + } + const parent = dirname(current); + if (parent === current) { + return undefined; + } + current = parent; + } +} + +function getLoadedSharedObjectsInPackageDir(packageDir: string): string[] { + const sharedObjects = (process.report.getReport() as { sharedObjects?: unknown }).sharedObjects; + if (!Array.isArray(sharedObjects)) { + return []; + } + + const root = normalizePath(packageDir).toLowerCase(); + const seen = new Set(); + const loadedFiles: string[] = []; + for (const value of sharedObjects) { + if (typeof value !== "string") { + continue; + } + const filePath = normalizePath(value); + const comparisonPath = filePath.toLowerCase(); + if (getCwdRelativePath(comparisonPath, root) === undefined || seen.has(comparisonPath)) { + continue; + } + seen.add(comparisonPath); + loadedFiles.push(filePath); + } + return loadedFiles; +} + +export function cleanupWindowsSelfUpdateQuarantine(packageDir: string): void { + const quarantineRoot = getQuarantineRoot(packageDir); + if (!quarantineRoot) { + return; + } + try { + rmSync(quarantineRoot, { recursive: true, force: true }); + } catch { + // A previous pi process may still be exiting and holding a native addon. + } +} + +export function quarantineWindowsNativeDependencies(packageDir: string): void { + const resolvedPackageDir = normalizePath(packageDir); + const quarantineRoot = getQuarantineRoot(resolvedPackageDir); + if (!quarantineRoot) { + return; + } + + const loadedFiles = getLoadedSharedObjectsInPackageDir(resolvedPackageDir); + if (loadedFiles.length === 0) { + return; + } + + const quarantineRunDir = join(quarantineRoot, `${Date.now()}-${process.pid}-${randomUUID()}`); + for (const loadedFile of loadedFiles) { + if (!existsSync(loadedFile)) { + continue; + } + const quarantinePath = join(quarantineRunDir, relative(resolvedPackageDir, loadedFile)); + mkdirSync(dirname(quarantinePath), { recursive: true }); + renameSync(loadedFile, quarantinePath); + copyFileSync(quarantinePath, loadedFile); + } +} From 96cad24a07ba41d51935b2400ed0303f1edec288 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 08:56:24 +0200 Subject: [PATCH 2/4] fix(coding-agent): detect pnpm v11 self-update installs closes #4647 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/config.ts | 56 ++++++++++++++++------- packages/coding-agent/test/config.test.ts | 49 ++++++++++++++++++++ 3 files changed, 89 insertions(+), 17 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 822b83ec..e772b3b0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). +- Fixed `pi update --self` detection for pnpm v11 global installs whose package path resolves through the pnpm store ([#4647](https://github.com/earendil-works/pi/issues/4647)). ## [0.75.1] - 2026-05-18 diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index 901bd296..ad306b4c 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -215,16 +215,18 @@ function getGlobalPackageRoots(method: InstallMethod, _packageName: string, npmC } } -function normalizeExistingPathForComparison(path: string): string | undefined { +function normalizeExistingPathForComparison(path: string, resolveSymlinks: boolean): string | undefined { const resolvedPath = resolve(path); if (!existsSync(resolvedPath)) { return undefined; } - let normalizedPath: string; - try { - normalizedPath = realpathSync(resolvedPath); - } catch { - return undefined; + let normalizedPath = resolvedPath; + if (resolveSymlinks) { + try { + normalizedPath = realpathSync(resolvedPath); + } catch { + return undefined; + } } if (process.platform === "win32") { normalizedPath = normalizedPath.toLowerCase(); @@ -232,6 +234,29 @@ function normalizeExistingPathForComparison(path: string): string | undefined { return normalizedPath; } +function getPathComparisonCandidates(path: string): string[] { + return Array.from( + new Set( + [normalizeExistingPathForComparison(path, false), normalizeExistingPathForComparison(path, true)].filter( + (candidate): candidate is string => !!candidate, + ), + ), + ); +} + +function getEntrypointPackageDir(): string | undefined { + const entrypoint = process.argv[1]; + if (!entrypoint) return undefined; + let dir = dirname(entrypoint); + while (dir !== dirname(dir)) { + if (existsSync(join(dir, "package.json"))) { + return dir; + } + dir = dirname(dir); + } + return undefined; +} + function isSelfUpdatePathWritable(): boolean { const packageDir = getPackageDir(); try { @@ -244,17 +269,14 @@ function isSelfUpdatePathWritable(): boolean { } function isManagedByGlobalPackageManager(method: InstallMethod, packageName: string, npmCommand?: string[]): boolean { - const packageDir = normalizeExistingPathForComparison(getPackageDir()); - return ( - !!packageDir && - getGlobalPackageRoots(method, packageName, npmCommand).some((root) => { - const normalizedRoot = normalizeExistingPathForComparison(root); - return ( - !!normalizedRoot && - packageDir.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`) - ); - }) - ); + const packageDirs = [getPackageDir(), getEntrypointPackageDir()].filter((dir): dir is string => !!dir); + const packageDirCandidates = packageDirs.flatMap((dir) => getPathComparisonCandidates(dir)); + return getGlobalPackageRoots(method, packageName, npmCommand).some((root) => { + return getPathComparisonCandidates(root).some((normalizedRoot) => { + const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`; + return packageDirCandidates.some((packageDir) => packageDir.startsWith(rootPrefix)); + }); + }); } export function getSelfUpdateCommand( diff --git a/packages/coding-agent/test/config.test.ts b/packages/coding-agent/test/config.test.ts index 6516beb5..71c4b48b 100644 --- a/packages/coding-agent/test/config.test.ts +++ b/packages/coding-agent/test/config.test.ts @@ -12,6 +12,7 @@ import { const execPathDescriptor = Object.getOwnPropertyDescriptor(process, "execPath"); const originalPath = process.env.PATH; const originalPiPackageDir = process.env.PI_PACKAGE_DIR; +const originalArgv1 = process.argv[1]; let tempDir: string | undefined; function setExecPath(value: string): void { @@ -35,6 +36,11 @@ afterEach(() => { } else { process.env.PI_PACKAGE_DIR = originalPiPackageDir; } + if (originalArgv1 === undefined) { + process.argv.splice(1, 1); + } else { + process.argv[1] = originalArgv1; + } if (tempDir) { chmodSync(tempDir, 0o700); rmSync(tempDir, { recursive: true, force: true }); @@ -275,6 +281,49 @@ describe("detectInstallMethod", () => { }); }); + test("self-updates pnpm v11 global installs resolved through the store", () => { + const temp = mkdtempSync(join(tmpdir(), "pi-pnpm11-")); + const binDir = join(temp, "bin"); + const root = join(temp, "Library", "pnpm", "global", "v11"); + const packageName = "@earendil-works/pi-coding-agent"; + const globalPackageDir = join(root, "11e9a", "node_modules", "@earendil-works", "pi-coding-agent"); + const storePackageDir = join( + temp, + "Library", + "pnpm", + "store", + "v11", + "links", + "@earendil-works", + "pi-coding-agent", + "0.75.0", + "hash", + "node_modules", + "@earendil-works", + "pi-coding-agent", + ); + mkdirSync(globalPackageDir, { recursive: true }); + mkdirSync(storePackageDir, { recursive: true }); + mkdirSync(binDir, { recursive: true }); + writeFileSync(join(globalPackageDir, "package.json"), "{}"); + writeFileSync(join(binDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm"), createFakePnpmScript(root)); + chmodSync(join(binDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm"), 0o755); + tempDir = temp; + process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`; + process.env.PI_PACKAGE_DIR = storePackageDir; + process.argv[1] = join(globalPackageDir, "dist", "cli.js"); + setExecPath(join(storePackageDir, "dist", "cli.js")); + + const command = getSelfUpdateCommand(packageName); + + expect(detectInstallMethod()).toBe("pnpm"); + expect(command).toEqual({ + command: "pnpm", + args: ["install", "-g", packageName], + display: `pnpm install -g ${packageName}`, + }); + }); + test("self-updates renamed yarn global installs by removing the old package first", () => { createYarnGlobalInstall(); From c65177370b6b97669844797c7ddc80dd042d702b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 09:13:12 +0200 Subject: [PATCH 3/4] fix(coding-agent): allow Windows pnpm self-update closes #4157 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/package-manager-cli.ts | 10 +++++++--- packages/coding-agent/src/utils/child-process.ts | 14 ++++++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e772b3b0..0440adcf 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,7 @@ - Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). - Fixed `pi update --self` detection for pnpm v11 global installs whose package path resolves through the pnpm store ([#4647](https://github.com/earendil-works/pi/issues/4647)). +- Fixed Windows pnpm self-updates to resolve pnpm command shims and run through pnpm instead of requiring manual updates ([#4157](https://github.com/earendil-works/pi/issues/4157)). ## [0.75.1] - 2026-05-18 diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index a3ef1e21..d1a51e3b 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -506,8 +506,10 @@ export async function handlePackageCommand(args: string[]): Promise { return true; } const installMethod = detectInstallMethod(); - if (process.platform === "win32" && installMethod !== "npm") { - console.error(chalk.red(`${APP_NAME} self-update on Windows is only supported for npm installs.`)); + if (process.platform === "win32" && installMethod !== "npm" && installMethod !== "pnpm") { + console.error( + chalk.red(`${APP_NAME} self-update on Windows is only supported for npm and pnpm installs.`), + ); console.error(chalk.dim(`Detected install method: ${installMethod}. Update ${APP_NAME} manually.`)); process.exitCode = 1; return true; @@ -523,7 +525,9 @@ export async function handlePackageCommand(args: string[]): Promise { return true; } try { - prepareWindowsNpmSelfUpdate(); + if (installMethod === "npm") { + prepareWindowsNpmSelfUpdate(); + } await runSelfUpdate(selfUpdateCommand); } catch (error: unknown) { const message = error instanceof Error ? error.message : "Unknown package command error"; diff --git a/packages/coding-agent/src/utils/child-process.ts b/packages/coding-agent/src/utils/child-process.ts index 73fdf5af..57ebd3ae 100644 --- a/packages/coding-agent/src/utils/child-process.ts +++ b/packages/coding-agent/src/utils/child-process.ts @@ -3,9 +3,9 @@ import { existsSync, readFileSync } from "node:fs"; import { dirname, extname, join, resolve, sep } from "node:path"; const EXIT_STDIO_GRACE_MS = 100; -const WINDOWS_COMMAND_EXTENSIONS = ["", ".exe", ".cmd", ".bat"]; +const WINDOWS_COMMAND_EXTENSIONS = [".exe", ".cmd", ".bat", ""]; const WINDOWS_COMMAND_SHIM_RE = /\.(?:cmd|bat)$/i; -const NODE_SHIM_SCRIPT_RE = /(?:%~dp0|%dp0%|%basedir%)[^"'\r\n<>|&]*?\.(?:cjs|mjs|js)/i; +const NODE_SHIM_SCRIPT_RE = /(?:%~dp0|%dp0%|%basedir%)[^"'\r\n<>|&]*?\.(?:cjs|mjs|js)/gi; export interface ResolvedSpawnCommand { command: string; @@ -45,10 +45,12 @@ function expandShimPath(path: string, shimPath: string): string { } function findNodeShimScript(shimPath: string): string | undefined { - const match = readFileSync(shimPath, "utf-8").match(NODE_SHIM_SCRIPT_RE); - if (!match) return undefined; - const scriptPath = expandShimPath(match[0], shimPath); - return existsSync(scriptPath) ? scriptPath : undefined; + const matches = [...readFileSync(shimPath, "utf-8").matchAll(NODE_SHIM_SCRIPT_RE)]; + for (const match of matches.reverse()) { + const scriptPath = expandShimPath(match[0], shimPath); + if (existsSync(scriptPath)) return scriptPath; + } + return undefined; } export function resolveSpawnCommand( From a294cdafd9731de96b08b30ba42f3cefea9bb650 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 18 May 2026 09:49:24 +0200 Subject: [PATCH 4/4] fix(coding-agent): unblock Windows external editor closes #4612 --- packages/coding-agent/CHANGELOG.md | 1 + .../components/extension-editor.ts | 21 +++++++++++++------ .../src/modes/interactive/interactive-mode.ts | 20 ++++++++++++------ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0440adcf..57932dc7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed Windows external editor handoff so vim/nvim can receive input after opening from the TUI ([#4612](https://github.com/earendil-works/pi/issues/4612)). - Fixed Windows npm self-updates to move loaded native dependency packages out of the active install before reinstalling pi ([#4157](https://github.com/earendil-works/pi/issues/4157)). - Fixed `pi update --self` detection for pnpm v11 global installs whose package path resolves through the pnpm store ([#4647](https://github.com/earendil-works/pi/issues/4647)). - Fixed Windows pnpm self-updates to resolve pnpm command shims and run through pnpm instead of requiring manual updates ([#4157](https://github.com/earendil-works/pi/issues/4157)). diff --git a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts index e1134ce9..b33ad9f5 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts @@ -3,7 +3,7 @@ * Supports Ctrl+G for external editor. */ -import { spawnSync } from "node:child_process"; +import { spawn } from "node:child_process"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; @@ -110,7 +110,7 @@ export class ExtensionEditorComponent extends Container implements Focusable { this.editor.handleInput(keyData); } - private openExternalEditor(): void { + private async openExternalEditor(): Promise { const editorCmd = process.env.VISUAL || process.env.EDITOR; if (!editorCmd) { return; @@ -124,12 +124,21 @@ export class ExtensionEditorComponent extends Container implements Focusable { this.tui.stop(); const [editor, ...editorArgs] = editorCmd.split(" "); - const result = spawnSync(editor, [...editorArgs, tmpFile], { - stdio: "inherit", - shell: process.platform === "win32", + process.stdout.write(`Launching external editor: ${editorCmd}\nPi will resume when the editor exits.\n`); + + // Do not use spawnSync here. On Windows, synchronous child_process calls can keep + // Node/libuv's console input read active after tui.stop() pauses stdin, racing + // vim/nvim for the console input buffer until Ctrl+C cancels the pending read. + const status = await new Promise((resolve) => { + const child = spawn(editor, [...editorArgs, tmpFile], { + stdio: "inherit", + shell: process.platform === "win32", + }); + child.on("error", () => resolve(null)); + child.on("close", (code) => resolve(code)); }); - if (result.status === 0) { + if (status === 0) { const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, ""); this.editor.setText(newContent); } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index a493bf6f..b3df9eb4 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3497,7 +3497,7 @@ export class InteractiveMode { this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`); } - private openExternalEditor(): void { + private async openExternalEditor(): Promise { // Determine editor (respect $VISUAL, then $EDITOR) const editorCmd = process.env.VISUAL || process.env.EDITOR; if (!editorCmd) { @@ -3518,14 +3518,22 @@ export class InteractiveMode { // Split by space to support editor arguments (e.g., "code --wait") const [editor, ...editorArgs] = editorCmd.split(" "); - // Spawn editor synchronously with inherited stdio for interactive editing - const result = spawnSync(editor, [...editorArgs, tmpFile], { - stdio: "inherit", - shell: process.platform === "win32", + process.stdout.write(`Launching external editor: ${editorCmd}\nPi will resume when the editor exits.\n`); + + // Do not use spawnSync here. On Windows, synchronous child_process calls can keep + // Node/libuv's console input read active after ui.stop() pauses stdin, racing + // vim/nvim for the console input buffer until Ctrl+C cancels the pending read. + const status = await new Promise((resolve) => { + const child = spawn(editor, [...editorArgs, tmpFile], { + stdio: "inherit", + shell: process.platform === "win32", + }); + child.on("error", () => resolve(null)); + child.on("close", (code) => resolve(code)); }); // On successful exit (status 0), replace editor content - if (result.status === 0) { + if (status === 0) { const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, ""); this.editor.setText(newContent); }