diff --git a/package.json b/package.json index 5f9fcd31..b47e3bcc 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "ccs-droid": "dist/bin/droid-runtime.js", "ccsd": "dist/bin/droid-runtime.js", "ccs-codex": "dist/bin/codex-runtime.js", - "ccsx": "dist/bin/codex-runtime.js" + "ccsx": "dist/bin/codex-runtime.js", + "ccsxp": "dist/bin/ccsxp-runtime.js" }, "files": [ "dist/", diff --git a/src/bin/ccsxp-runtime.ts b/src/bin/ccsxp-runtime.ts new file mode 100644 index 00000000..6758e655 --- /dev/null +++ b/src/bin/ccsxp-runtime.ts @@ -0,0 +1,9 @@ +const { stripTargetFlag } = require('../targets/target-resolver'); + +process.env.CCS_INTERNAL_ENTRY_TARGET = 'codex'; + +// ccsxp is an opinionated shortcut for the built-in Codex-on-Codex route. +// Strip user-supplied target overrides before forcing the shortcut target. +const forwardedArgs = stripTargetFlag(process.argv.slice(2)); +process.argv.splice(2, process.argv.length - 2, 'codex', '--target', 'codex', ...forwardedArgs); +require('../ccs'); diff --git a/src/ccs.ts b/src/ccs.ts index 9ae26160..ca2625fb 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -241,14 +241,26 @@ function resolveNativeClaudeLaunchArgs( } function shouldPassthroughNativeCodexFlagCommand(args: string[]): boolean { + return getNativeCodexPassthroughArgs(args) !== null; +} + +function getNativeCodexPassthroughArgs(args: string[]): string[] | null { const targetArgs = stripTargetFlag(args); - if (targetArgs.length === 0) { - return false; + if (resolveTargetType(args) !== 'codex' || targetArgs.length === 0) { + return null; } - return ( - resolveTargetType(args) === 'codex' && CODEX_NATIVE_PASSTHROUGH_FLAGS.has(targetArgs[0] || '') - ); + const firstArg = targetArgs[0] || ''; + if (CODEX_NATIVE_PASSTHROUGH_FLAGS.has(firstArg)) { + return targetArgs; + } + + const secondArg = targetArgs[1] || ''; + if (firstArg === 'codex' && CODEX_NATIVE_PASSTHROUGH_FLAGS.has(secondArg)) { + return targetArgs.slice(1); + } + + return null; } function execNativeCodexFlagCommand(args: string[]): void { @@ -265,7 +277,11 @@ function execNativeCodexFlagCommand(args: string[]): void { process.exit(1); } - const targetArgs = stripTargetFlag(args); + const targetArgs = getNativeCodexPassthroughArgs(args); + if (!targetArgs) { + console.error(fail('Native Codex passthrough args could not be resolved.')); + process.exit(1); + } const creds: TargetCredentials = { profile: 'default', baseUrl: '', diff --git a/tests/npm/cross-platform.test.js b/tests/npm/cross-platform.test.js index 1382ed88..f3416fef 100644 --- a/tests/npm/cross-platform.test.js +++ b/tests/npm/cross-platform.test.js @@ -8,7 +8,7 @@ try { expandPath = require('../../dist/utils/helpers').expandPath; } catch (e) { // If helpers module doesn't exist or doesn't export expandPath, create a mock - expandPath = function(p) { + expandPath = function (p) { if (!p || typeof p !== 'string') return p; if (p.startsWith('~/')) { return path.join(os.homedir(), p.slice(2)); @@ -76,7 +76,10 @@ describe('cross-platform', () => { describe('platform-specific behavior', () => { it('detects platform correctly', () => { const platform = os.platform(); - assert(['darwin', 'linux', 'win32'].includes(platform), 'Should be running on supported platform'); + assert( + ['darwin', 'linux', 'win32'].includes(platform), + 'Should be running on supported platform' + ); }); it('handles path separators correctly', () => { @@ -133,6 +136,7 @@ describe('cross-platform', () => { assert(packageJson.bin.ccsd, 'bin field should specify ccsd command'); assert(packageJson.bin['ccs-codex'], 'bin field should specify ccs-codex command'); assert(packageJson.bin.ccsx, 'bin field should specify ccsx command'); + assert(packageJson.bin.ccsxp, 'bin field should specify ccsxp command'); assert.notStrictEqual( packageJson.bin['ccs-droid'], packageJson.bin.ccs, @@ -153,6 +157,11 @@ describe('cross-platform', () => { packageJson.bin.ccsx, 'ccsx should share the dedicated codex runtime entrypoint' ); + assert.notStrictEqual( + packageJson.bin.ccsxp, + packageJson.bin.ccsx, + 'ccsxp should use a dedicated shortcut entrypoint' + ); assert( fs.existsSync(path.join(__dirname, '..', '..', packageJson.bin['ccs-droid'])), 'dedicated droid runtime entrypoint should exist' @@ -161,6 +170,10 @@ describe('cross-platform', () => { fs.existsSync(path.join(__dirname, '..', '..', packageJson.bin['ccs-codex'])), 'dedicated codex runtime entrypoint should exist' ); + assert( + fs.existsSync(path.join(__dirname, '..', '..', packageJson.bin.ccsxp)), + 'dedicated ccsxp shortcut entrypoint should exist' + ); assert(packageJson.scripts, 'package.json should have scripts field'); }); }); diff --git a/tests/unit/bin/ccsxp-runtime.test.ts b/tests/unit/bin/ccsxp-runtime.test.ts new file mode 100644 index 00000000..ee4d0f40 --- /dev/null +++ b/tests/unit/bin/ccsxp-runtime.test.ts @@ -0,0 +1,55 @@ +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; + +const wrapperPath = require.resolve('../../../src/bin/ccsxp-runtime.ts'); +const ccsPath = require.resolve('../../../src/ccs.ts'); + +describe('ccsxp runtime wrapper', () => { + const originalArgv = process.argv; + const originalEntryTarget = process.env.CCS_INTERNAL_ENTRY_TARGET; + + beforeEach(() => { + delete require.cache[wrapperPath]; + delete require.cache[ccsPath]; + }); + + afterEach(() => { + process.argv = originalArgv; + + if (originalEntryTarget === undefined) { + delete process.env.CCS_INTERNAL_ENTRY_TARGET; + } else { + process.env.CCS_INTERNAL_ENTRY_TARGET = originalEntryTarget; + } + + delete require.cache[wrapperPath]; + delete require.cache[ccsPath]; + }); + + it('prepends the built-in codex profile and target before loading CCS', () => { + process.argv = ['node', wrapperPath, 'fix failing tests']; + require.cache[ccsPath] = { exports: {} } as NodeJS.Module; + + require(wrapperPath); + + expect(process.env.CCS_INTERNAL_ENTRY_TARGET).toBe('codex'); + expect(process.argv.slice(2)).toEqual(['codex', '--target', 'codex', 'fix failing tests']); + }); + + it('keeps flag-only invocations routed through the built-in codex profile shortcut', () => { + process.argv = ['node', wrapperPath, '--version']; + require.cache[ccsPath] = { exports: {} } as NodeJS.Module; + + require(wrapperPath); + + expect(process.argv.slice(2)).toEqual(['codex', '--target', 'codex', '--version']); + }); + + it('strips user-supplied target overrides before forcing the codex shortcut target', () => { + process.argv = ['node', wrapperPath, '--target', 'claude', '--version']; + require.cache[ccsPath] = { exports: {} } as NodeJS.Module; + + require(wrapperPath); + + expect(process.argv.slice(2)).toEqual(['codex', '--target', 'codex', '--version']); + }); +}); diff --git a/tests/unit/targets/codex-runtime-integration.test.ts b/tests/unit/targets/codex-runtime-integration.test.ts index 161348fd..3c186354 100644 --- a/tests/unit/targets/codex-runtime-integration.test.ts +++ b/tests/unit/targets/codex-runtime-integration.test.ts @@ -40,6 +40,21 @@ function runCodexAlias(args: string[], env: NodeJS.ProcessEnv): RunResult { }; } +function runCcsxpAlias(args: string[], env: NodeJS.ProcessEnv): RunResult { + const ccsxpEntry = path.join(process.cwd(), 'src', 'bin', 'ccsxp-runtime.ts'); + const result = spawnSync(process.execPath, [ccsxpEntry, ...args], { + encoding: 'utf8', + env, + timeout: 20000, + }); + + return { + status: result.status, + stdout: result.stdout || '', + stderr: result.stderr || '', + }; +} + function readLoggedCodexCalls(logPath: string): string[][] { if (!fs.existsSync(logPath)) { return []; @@ -94,8 +109,9 @@ describe('codex runtime integration', () => { `#!/usr/bin/env node const fs = require('fs'); const out = process.env.CCS_TEST_CODEX_ARGS_OUT; +const cliArgs = process.argv.slice(2); if (out) { - fs.appendFileSync(out, JSON.stringify(process.argv.slice(2)) + '\\n'); + fs.appendFileSync(out, JSON.stringify(cliArgs) + '\\n'); } const envOut = process.env.CCS_TEST_CODEX_ENV_OUT; if (envOut) { @@ -110,11 +126,11 @@ if (envOut) { }) + '\\n' ); } -if (process.argv[2] === '--version' || process.argv[2] === '-v') { +if (cliArgs.includes('--version') || cliArgs.includes('-v')) { process.stdout.write(process.env.CCS_TEST_CODEX_VERSION || 'codex-cli 0.118.0-alpha.3'); process.exit(0); } -if (process.argv[2] === '--help' || process.argv[2] === '-h') { +if (cliArgs.includes('--help') || cliArgs.includes('-h')) { process.stdout.write( process.env.CCS_TEST_CODEX_HELP || ' -c, --config \\n -p, --profile \\n' @@ -244,10 +260,10 @@ process.exit(0); ]); }); - it('fails fast when native Codex reasoning overrides need unsupported --config support', () => { + it('keeps ccsxp pinned to native Codex even when a user passes another --target override', () => { if (process.platform === 'win32') return; - const result = runCcs(['default', '--target', 'codex', '--effort', 'high', 'fix failing tests'], { + const result = runCcsxpAlias(['--target', 'claude', '--version'], { ...process.env, CI: '1', NO_COLOR: '1', @@ -255,9 +271,48 @@ process.exit(0); CCS_CODEX_PATH: fakeCodexPath, CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test', - CCS_TEST_CODEX_HELP: ' -p, --profile \\n', }); + expect(result.status).toBe(0); + expect(result.stdout).toContain('codex-cli 9.9.9-test'); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]); + }); + + it('passes ccs codex --target codex --version through to the native Codex binary', () => { + if (process.platform === 'win32') return; + + const result = runCcs(['codex', '--target', 'codex', '--version'], { + ...process.env, + CI: '1', + NO_COLOR: '1', + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test', + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('codex-cli 9.9.9-test'); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]); + }); + + it('fails fast when native Codex reasoning overrides need unsupported --config support', () => { + if (process.platform === 'win32') return; + + const result = runCcs( + ['default', '--target', 'codex', '--effort', 'high', 'fix failing tests'], + { + ...process.env, + CI: '1', + NO_COLOR: '1', + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test', + CCS_TEST_CODEX_HELP: ' -p, --profile \\n', + } + ); + expect(result.status).toBe(1); expect(result.stderr).toContain('Codex CLI (codex-cli 9.9.9-test)'); expect(result.stderr).toContain('does not advertise --config overrides');