diff --git a/bin/ocx.mjs b/bin/ocx.mjs index 5357985bca9..08eb51262da 100755 --- a/bin/ocx.mjs +++ b/bin/ocx.mjs @@ -33,6 +33,15 @@ import { } from "../src/update/codex-cli-update-launch-policy.mjs"; const PKG = "@bitkyc08/opencodex"; +try { + process.cwd(); +} catch { + try { + process.chdir(homedir()); + } catch { + /* best-effort */ + } +} 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 8259596f34b..06478ba3a3a 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,5 +1,18 @@ #!/usr/bin/env bun 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 { + 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 f4f16f022d1..b304c9a0105 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,13 @@ function printAgentDeferral(): void { */ export async function maybeShowStarPrompt(): Promise { try { - if (process.env.OCX_SERVICE || !process.stdin.isTTY || !process.stdout.isTTY) 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; diff --git a/src/update/notify.ts b/src/update/notify.ts index 28dbfcb634f..5764af3992c 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"; @@ -122,8 +123,13 @@ 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); +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 e401dbc25a6..a004d4cff44 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");