From bef7635665143b2ee61a52966880525dc3253d40 Mon Sep 17 00:00:00 2001 From: bash0816 Date: Mon, 20 Jul 2026 16:58:23 +0900 Subject: [PATCH] fix: codex update targets correct npm prefix on multi-prefix systems Adds explicit --prefix flag to npm install -g in installTarget(), derived from packageDir. Fixes update on machines with multiple npm global prefixes where the wrong prefix was silently updated instead of the running installation. Includes fixture-based regression tests (prefixA/prefixB isolation via fake npm binary). Version bump and npm publish deferred to next upstream release per project policy. Co-Authored-By: Claude Sonnet 5 --- packages/codex-termux/lib/check-updates.js | 5 +- .../codex-termux/lib/check-updates.test.js | 118 ++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 packages/codex-termux/lib/check-updates.test.js diff --git a/packages/codex-termux/lib/check-updates.js b/packages/codex-termux/lib/check-updates.js index d623fd6..09325e8 100644 --- a/packages/codex-termux/lib/check-updates.js +++ b/packages/codex-termux/lib/check-updates.js @@ -9,6 +9,7 @@ import https from 'https'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageDir = resolve(__dirname, '..'); +const prefix = resolve(packageDir, '..', '..', '..', '..'); const pkg = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')); const localManifest = JSON.parse( @@ -127,13 +128,13 @@ function shouldNotify(cache, latestVersion) { function installTarget(targetVersion) { const spec = `${pkg.name}@${targetVersion}`; - const command = `npm install -g ${spec}`; + const command = `npm install -g --prefix ${prefix} ${spec}`; if (dryRun) { console.log(command); return 0; } console.error(`Updating to audited version ${targetVersion}`); - const result = spawnSync('npm', ['install', '-g', spec], { + const result = spawnSync('npm', ['install', '-g', '--prefix', prefix, spec], { stdio: 'inherit', env: process.env, }); diff --git a/packages/codex-termux/lib/check-updates.test.js b/packages/codex-termux/lib/check-updates.test.js new file mode 100644 index 0000000..81e32ae --- /dev/null +++ b/packages/codex-termux/lib/check-updates.test.js @@ -0,0 +1,118 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { mkdtempSync, mkdirSync, rmSync, cpSync, writeFileSync, readFileSync, chmodSync } from 'fs'; +import { spawnSync } from 'child_process'; +import path, { resolve, join } from 'path'; +import os from 'os'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageDir = resolve(__dirname, '..'); +const repoRoot = resolve(packageDir, '..', '..', '..'); + +const manifestPath = join(packageDir, 'config', 'codex-termux-release-manifest.json'); +const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); +const latestAuditedVersion = manifest.latest_audited_version; +const testVersion = '0.144.4'; // Older version for testing + +test('installTarget with prefixA uses correct --prefix', async (t) => { + const tmpdir = mkdtempSync(join(os.tmpdir(), 'codex-prefix-test-')); + try { + const prefixA = join(tmpdir, 'prefixA'); + const prefixANodeModules = join(prefixA, 'lib', 'node_modules', '@bash0816', 'codex-termux'); + const fakeNpmDir = join(tmpdir, 'fake-bin'); + const recordFileA = join(tmpdir, 'npm-args-a.json'); + + // Create directory structure for prefixA + cpSync(packageDir, prefixANodeModules, { recursive: true }); + + // Update package.json version to old version + const pkgJsonPath = join(prefixANodeModules, 'package.json'); + const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); + pkg.version = testVersion; + writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n'); + + // Create fake npm binary + mkdirSync(fakeNpmDir, { recursive: true }); + const fakeNpmPath = join(fakeNpmDir, 'npm'); + writeFileSync( + fakeNpmPath, + `#!/usr/bin/env node +const fs = require('fs'); +fs.writeFileSync(process.env.FAKE_NPM_RECORD_FILE, JSON.stringify(process.argv.slice(2))); +process.exit(0); +` + ); + chmodSync(fakeNpmPath, 0o755); + + // Run check-updates.js from prefixA + const result = spawnSync('node', [join(prefixANodeModules, 'lib', 'check-updates.js'), 'update'], { + env: { + ...process.env, + PATH: `${fakeNpmDir}:${process.env.PATH}`, + CODEX_TERMUX_SKIP_UPDATE_CHECK: '1', + FAKE_NPM_RECORD_FILE: recordFileA, + }, + stdio: 'pipe', + }); + + // Check recorded npm arguments + const recordedArgs = JSON.parse(readFileSync(recordFileA, 'utf8')); + const expectedArgs = ['install', '-g', '--prefix', prefixA, `@bash0816/codex-termux@${latestAuditedVersion}`]; + assert.deepStrictEqual(recordedArgs, expectedArgs, `Expected ${JSON.stringify(expectedArgs)} but got ${JSON.stringify(recordedArgs)}`); + } finally { + rmSync(tmpdir, { recursive: true, force: true }); + } +}); + +test('installTarget with prefixB uses correct --prefix', async (t) => { + const tmpdir = mkdtempSync(join(os.tmpdir(), 'codex-prefix-test-')); + try { + const prefixB = join(tmpdir, 'prefixB'); + const prefixBNodeModules = join(prefixB, 'lib', 'node_modules', '@bash0816', 'codex-termux'); + const fakeNpmDir = join(tmpdir, 'fake-bin'); + const recordFileB = join(tmpdir, 'npm-args-b.json'); + + // Create directory structure for prefixB + cpSync(packageDir, prefixBNodeModules, { recursive: true }); + + // Update package.json version to old version + const pkgJsonPath = join(prefixBNodeModules, 'package.json'); + const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); + pkg.version = testVersion; + writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n'); + + // Create fake npm binary + mkdirSync(fakeNpmDir, { recursive: true }); + const fakeNpmPath = join(fakeNpmDir, 'npm'); + writeFileSync( + fakeNpmPath, + `#!/usr/bin/env node +const fs = require('fs'); +fs.writeFileSync(process.env.FAKE_NPM_RECORD_FILE, JSON.stringify(process.argv.slice(2))); +process.exit(0); +` + ); + chmodSync(fakeNpmPath, 0o755); + + // Run check-updates.js from prefixB + const result = spawnSync('node', [join(prefixBNodeModules, 'lib', 'check-updates.js'), 'update'], { + env: { + ...process.env, + PATH: `${fakeNpmDir}:${process.env.PATH}`, + CODEX_TERMUX_SKIP_UPDATE_CHECK: '1', + FAKE_NPM_RECORD_FILE: recordFileB, + }, + stdio: 'pipe', + }); + + // Check recorded npm arguments + const recordedArgs = JSON.parse(readFileSync(recordFileB, 'utf8')); + const expectedArgs = ['install', '-g', '--prefix', prefixB, `@bash0816/codex-termux@${latestAuditedVersion}`]; + assert.deepStrictEqual(recordedArgs, expectedArgs, `Expected ${JSON.stringify(expectedArgs)} but got ${JSON.stringify(recordedArgs)}`); + } finally { + rmSync(tmpdir, { recursive: true, force: true }); + } +});