From d32b6f04765afeec9e9ca858b37df4c9d4435ef5 Mon Sep 17 00:00:00 2001 From: Kevin Codex Date: Thu, 25 Jun 2026 06:52:58 +0800 Subject: [PATCH] fix(update): stop false "development build" block on npm installs with NODE_ENV=development (#1781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `/update` and `openclaude update` reported "Auto-update is unavailable for a development build." even when OpenClaude was correctly installed from npm, as long as the launching shell had `NODE_ENV=development` exported. Root cause: `getCurrentInstallationType()` checked `NODE_ENV === 'development'` as its first branch, before any path-based detection. A user's shell env var then downgraded a real npm install to 'development', which routed `resolveUpdateStrategy()` to `{ action: 'blocked', reason: 'development' }`. Two-part fix: 1. doctorDiagnostic.ts — move the `NODE_ENV === 'development'` check to a fallback position after all real-install path markers (bundled mode, local npm, npm-global paths, /npm/, /nvm/, `npm config get prefix`). Path detection runs first; NODE_ENV only classifies as 'development' when no install path matches (i.e. an actual source-tree `bun run dev` run). 2. bin/openclaude — the heap-sizing relaunch previously used `fileURLToPath(import.meta.url)`, which resolves symlinks. After relaunch, `process.argv[1]` pointed at the real file target (repo path for `npm install -g .`, package path inside node_modules for real installs), defeating path-based detection. Preserve `process.argv[1]` (the original invocation path, e.g. /usr/local/bin/openclaude or nvm bin symlink) so npm-global path markers can match correctly. Verified: `bun run typecheck` passes; `openclaude doctor` now reports npm-global (not development) with NODE_ENV=development set on a real npm global install. Co-authored-by: OpenClaude --- bin/openclaude | 12 +++++++++++- src/utils/doctorDiagnostic.ts | 13 +++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/bin/openclaude b/bin/openclaude index a66e13659..9f3f66e7f 100755 --- a/bin/openclaude +++ b/bin/openclaude @@ -75,9 +75,19 @@ function relaunchWithLongSessionHeapIfNeeded() { arg => !arg.startsWith('--max-memory=') && arg !== '--max-memory', ) + // Preserve the original argv[1] (which may be a symlink like + // /usr/local/bin/openclaude) instead of resolving it via import.meta.url. + // Resolving symlinks here defeats install-type detection downstream: a real + // npm global install (symlink → node_modules/@gitlawb/openclaude/bin) would + // resolve to the package's real path inside node_modules, which is fine, but + // a `npm install -g .` dev symlink resolves back to the repo and looks like + // a source-tree dev run. Using argv[1] keeps the invocation path stable so + // doctorDiagnostic's npm-global path markers can match correctly. + const launcherPath = process.argv[1] || fileURLToPath(import.meta.url) + const result = spawnSync(process.execPath, [ ...execArgv, - fileURLToPath(import.meta.url), + launcherPath, ...childArgs, ], { stdio: 'inherit', diff --git a/src/utils/doctorDiagnostic.ts b/src/utils/doctorDiagnostic.ts index 34aabeeb8..122f6ad4c 100644 --- a/src/utils/doctorDiagnostic.ts +++ b/src/utils/doctorDiagnostic.ts @@ -99,10 +99,6 @@ function getNormalizedPaths(): [invokedPath: string, execPath: string] { } export async function getCurrentInstallationType(): Promise { - if (process.env.NODE_ENV === 'development') { - return 'development' - } - const [invokedPath] = getNormalizedPaths() // Check if running in bundled mode first @@ -158,6 +154,15 @@ export async function getCurrentInstallationType(): Promise { return 'npm-global' } + // Development build: running from a source tree (e.g. `bun run dev`) with + // NODE_ENV=development. Checked AFTER all real-install path markers so that + // a user shell exporting NODE_ENV=development can't downgrade a real npm + // install to 'development' (which would block /update). A source-tree run + // matches none of the path markers above, so it lands here. + if (process.env.NODE_ENV === 'development') { + return 'development' + } + // If we can't determine, return unknown return 'unknown' }