From 7045a00365aa3c16767a839e26bb237e4be554eb Mon Sep 17 00:00:00 2001 From: agentHits Date: Thu, 3 Sep 2026 22:43:49 +0300 Subject: [PATCH 1/3] fix(cli): heal deleted cwd at launch and avoid stream init in TTY guard --- bin/ocx.mjs | 7 +++++++ src/cli/index.ts | 11 +++++++++++ src/cli/star-prompt.ts | 3 ++- src/update/notify.ts | 7 ++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/bin/ocx.mjs b/bin/ocx.mjs index 5357985bca..65f5cba22c 100755 --- a/bin/ocx.mjs +++ b/bin/ocx.mjs @@ -33,6 +33,13 @@ import { } from "../src/update/codex-cli-update-launch-policy.mjs"; const PKG = "@bitkyc08/opencodex"; +try { + process.cwd(); +} catch { + try { + process.chdir(homedir()); + } catch {} +} const require = createRequire(import.meta.url); const here = dirname(fileURLToPath(import.meta.url)); const cliPath = join(here, "..", "src", "cli", "index.ts"); diff --git a/src/cli/index.ts b/src/cli/index.ts index 8259596f34..fec11e30e5 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,5 +1,16 @@ #!/usr/bin/env bun import { spawn } from "node:child_process"; +import { homedir } from "node:os"; + +try { + process.cwd(); +} catch { + try { + process.chdir(homedir()); + } catch { + /* best-effort */ + } +} import { currentExternalCodexModelProvider, restoreNativeCodex, restoreNativeCodexAsync, shouldInjectApiAuthHeader } from "../codex/inject"; import { stripGrokConfig } from "../grok/inject"; import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../update/stop-contract.mjs"; diff --git a/src/cli/star-prompt.ts b/src/cli/star-prompt.ts index f4f16f022d..adc138cbbf 100644 --- a/src/cli/star-prompt.ts +++ b/src/cli/star-prompt.ts @@ -1,5 +1,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; +import { isatty } from "node:tty"; import { spawnSync } from "node:child_process"; import { getConfigDir } from "../config"; import { recordOwnedConfigPath } from "../lib/config-ownership"; @@ -167,7 +168,7 @@ function printAgentDeferral(): void { */ export async function maybeShowStarPrompt(): Promise { try { - if (process.env.OCX_SERVICE || !process.stdin.isTTY || !process.stdout.isTTY) return; + if (process.env.OCX_SERVICE || !isatty(0) || !isatty(1)) return; const dir = getConfigDir(); const marker = join(dir, MARKER); if (existsSync(marker)) return; diff --git a/src/update/notify.ts b/src/update/notify.ts index 28dbfcb634..e30c2ad2c1 100644 --- a/src/update/notify.ts +++ b/src/update/notify.ts @@ -1,6 +1,7 @@ import { spawn } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { isatty } from "node:tty"; import { createInterface } from "node:readline/promises"; import { atomicWriteFile, getConfigDir } from "../config"; import { hasStarPromptRun } from "../cli/star-prompt"; @@ -123,7 +124,11 @@ export function isSourceBuildVersion(v: string): boolean { /** The interactive/TTY + install-method gate shared with the star prompt. */ function interactiveGuardOk(): boolean { - return !(process.env.OCX_SERVICE || !process.stdin.isTTY || !process.stdout.isTTY); + try { + return !(process.env.OCX_SERVICE || !isatty(0) || !isatty(1)); + } catch { + return false; + } } /** From ed62a1bfb31652316ce7c80fd13dbf99610f725a Mon Sep 17 00:00:00 2001 From: agentHits Date: Thu, 3 Sep 2026 22:47:22 +0300 Subject: [PATCH 2/3] test(update): add regression coverage for interactiveGuardOk and fix empty catch --- bin/ocx.mjs | 4 +++- src/update/notify.ts | 3 ++- tests/update-notify.test.ts | 13 +++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/bin/ocx.mjs b/bin/ocx.mjs index 65f5cba22c..08eb51262d 100755 --- a/bin/ocx.mjs +++ b/bin/ocx.mjs @@ -38,7 +38,9 @@ try { } catch { try { process.chdir(homedir()); - } catch {} + } catch { + /* best-effort */ + } } const require = createRequire(import.meta.url); const here = dirname(fileURLToPath(import.meta.url)); diff --git a/src/update/notify.ts b/src/update/notify.ts index e30c2ad2c1..5764af3992 100644 --- a/src/update/notify.ts +++ b/src/update/notify.ts @@ -123,10 +123,11 @@ export function isSourceBuildVersion(v: string): boolean { } /** The interactive/TTY + install-method gate shared with the star prompt. */ -function interactiveGuardOk(): boolean { +export function interactiveGuardOk(): boolean { try { return !(process.env.OCX_SERVICE || !isatty(0) || !isatty(1)); } catch { + /* best-effort */ return false; } } diff --git a/tests/update-notify.test.ts b/tests/update-notify.test.ts index e401dbc25a..a004d4cff4 100644 --- a/tests/update-notify.test.ts +++ b/tests/update-notify.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { getUpgradeVersionForPopup, + interactiveGuardOk, isNewer, isSourceBuildVersion, readVersionCache, @@ -135,6 +136,18 @@ describe("cli wiring", () => { expect(promptIndex).toBeLessThan(serverIndex); }); + test("interactiveGuardOk safely evaluates without throwing when cwd is unlinked", () => { + const origCwd = process.cwd(); + const tempDir = mkdtempSync(join(tmpdir(), "ocx-unlinked-cwd-")); + process.chdir(tempDir); + removeTreeWithRetry(tempDir); + try { + expect(typeof interactiveGuardOk()).toBe("boolean"); + } finally { + try { process.chdir(origCwd); } catch { /* best-effort */ } + } + }); + test("hidden __refresh-version subcommand is wired", async () => { const dispatch = await readText("src/cli/dispatch.ts"); expect(dispatch).toContain("\"__refresh-version\": async"); From 8fb7ae55349c5e5a98c5f67ee8f2d965bb300ef4 Mon Sep 17 00:00:00 2001 From: agentHits Date: Thu, 3 Sep 2026 22:50:03 +0300 Subject: [PATCH 3/3] chore(cli): align star-prompt isatty guard and clarify cwd heal comment --- src/cli/index.ts | 2 ++ src/cli/star-prompt.ts | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/cli/index.ts b/src/cli/index.ts index fec11e30e5..06478ba3a3 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -2,6 +2,8 @@ import { spawn } from "node:child_process"; import { homedir } from "node:os"; +// Best-effort recovery for runtime execution and spawned children if launched +// from an unlinked/deleted working directory (runs after hoisted ESM module imports). try { process.cwd(); } catch { diff --git a/src/cli/star-prompt.ts b/src/cli/star-prompt.ts index adc138cbbf..b304c9a010 100644 --- a/src/cli/star-prompt.ts +++ b/src/cli/star-prompt.ts @@ -168,7 +168,13 @@ function printAgentDeferral(): void { */ export async function maybeShowStarPrompt(): Promise { try { - if (process.env.OCX_SERVICE || !isatty(0) || !isatty(1)) return; + let isTty = false; + try { + isTty = isatty(0) && isatty(1); + } catch { + /* best-effort */ + } + if (process.env.OCX_SERVICE || !isTty) return; const dir = getConfigDir(); const marker = join(dir, MARKER); if (existsSync(marker)) return;