diff --git a/docs/codex-auth.md b/docs/codex-auth.md index 020a609b..3858ef2c 100644 --- a/docs/codex-auth.md +++ b/docs/codex-auth.md @@ -28,6 +28,9 @@ codex # Terminal B: eval "$(ccsx auth use personal)" codex + +# Or launch a named profile directly through ccsx +ccsx work ``` ## Two-terminal example @@ -49,6 +52,7 @@ codex # runs with CODEX_HOME=~/.ccs/codex-instances/pers | Command | Description | |---------|-------------| | `ccsx auth create ` | Create profile dir + auto-login | +| `ccsx ` | Launch a named Codex auth profile | | `ccsx auth login ` | (Re-)authenticate an existing profile | | `ccsx auth switch ` | Set the persistent default profile for future `ccsx` launches | | `ccsx auth use ` | Emit shell exports for this shell only (use with `eval`) | @@ -60,6 +64,7 @@ codex # runs with CODEX_HOME=~/.ccs/codex-instances/pers | Method | Scope | How | |--------|-------|-----| +| `ccsx ` | One launch | Resolves `` from the Codex profile registry | | `ccsx auth switch ` | Future `ccsx` launches | Writes to `~/.ccs/codex-profiles.yaml` | | `eval "$(ccsx auth use )"` | Current shell only | Sets `CODEX_HOME` + `CCS_CODEX_PROFILE` in your shell | diff --git a/src/bin/codex-runtime-router.ts b/src/bin/codex-runtime-router.ts index e04c8ef2..287ac491 100644 --- a/src/bin/codex-runtime-router.ts +++ b/src/bin/codex-runtime-router.ts @@ -35,6 +35,31 @@ function errorMessage(err: unknown): string { return String(err); } +function maybeSelectPositionalCodexProfile(argv: string[]): void { + const candidate = (argv[2] ?? '').trim(); + if (!candidate || candidate.startsWith('-') || candidate === 'auth') { + return; + } + + const { getNativeCodexPassthroughArgs } = require('../dispatcher/cli-argument-parser') as { + getNativeCodexPassthroughArgs: (args: string[]) => string[] | null; + }; + if (getNativeCodexPassthroughArgs(argv.slice(2)) !== null) { + return; + } + + const { CodexProfileRegistry } = require('../codex-auth/codex-profile-registry') as { + CodexProfileRegistry: new () => { hasProfile: (name: string) => boolean }; + }; + const registry = new CodexProfileRegistry(); + if (!registry.hasProfile(candidate)) { + return; + } + + process.env.CCS_CODEX_PROFILE = candidate; + argv[2] = 'default'; +} + /** * Main entry-point for the ccsx / codex-runtime binary. * @@ -54,11 +79,13 @@ export async function main(argv: string[]): Promise { // ── non-auth branch: profile resolution ───────────────────────────────── - // F1: respect explicit CODEX_HOME unless CCS_CODEX_PROFILE asks for a managed profile. - const explicit = (process.env.CODEX_HOME ?? '').trim(); - const profileOverride = (process.env.CCS_CODEX_PROFILE ?? '').trim(); - if (!explicit || profileOverride) { - try { + try { + maybeSelectPositionalCodexProfile(argv); + + // F1: respect explicit CODEX_HOME unless CCS_CODEX_PROFILE asks for a managed profile. + const explicit = (process.env.CODEX_HOME ?? '').trim(); + const profileOverride = (process.env.CCS_CODEX_PROFILE ?? '').trim(); + if (!explicit || profileOverride) { const { resolveActiveProfile } = require('../codex-auth/resolve-active-profile') as { resolveActiveProfile: ( env: NodeJS.ProcessEnv @@ -85,15 +112,15 @@ export async function main(argv: string[]): Promise { ); } } - } catch (resolverErr) { - const msg = errorMessage(resolverErr); - if (isCodexAuthProfileResolutionError(resolverErr)) { - process.stderr.write(`[X] codex-auth: ${msg}\n`); - return 1; - } - process.stderr.write(`[X] codex-auth: profile resolution failed (${msg})\n`); + } + } catch (resolverErr) { + const msg = errorMessage(resolverErr); + if (isCodexAuthProfileResolutionError(resolverErr)) { + process.stderr.write(`[X] codex-auth: ${msg}\n`); return 1; } + process.stderr.write(`[X] codex-auth: profile resolution failed (${msg})\n`); + return 1; } // ── delegate to CCS ───────────────────────────────────────────────────── diff --git a/tests/unit/bin/codex-runtime-router.test.ts b/tests/unit/bin/codex-runtime-router.test.ts index 2562e969..615d6fa0 100644 --- a/tests/unit/bin/codex-runtime-router.test.ts +++ b/tests/unit/bin/codex-runtime-router.test.ts @@ -146,6 +146,28 @@ describe('codex-runtime router — non-auth profile resolution', () => { expect(process.env.CODEX_HOME).toBe(profileDir); }); + it('resolves a first positional ccsx arg from the Codex auth registry before CCS profiles', async () => { + const profileDir = makeProfileDir('ck'); + writeRegistry({ + version: '1.0', + default: null, + profiles: { ck: { type: 'codex', created: '2026-01-01T00:00:00.000Z', last_used: null } }, + }); + + require.cache[ccsPath] = { exports: {} } as NodeJS.Module; + flushRouterCache(); + require.cache[ccsPath] = { exports: {} } as NodeJS.Module; + + const argv = ['node', 'codex-runtime', 'ck', 'fix failing tests']; + const { main } = require(routerPath) as { main: (argv: string[]) => Promise }; + const code = await main(argv); + + expect(code).toBe(-1); + expect(process.env.CCS_CODEX_PROFILE).toBe('ck'); + expect(process.env.CODEX_HOME).toBe(profileDir); + expect(argv).toEqual(['node', 'codex-runtime', 'default', 'fix failing tests']); + }); + it('leaves CODEX_HOME unset when no registry exists and no env profile set', async () => { // No registry file, no CCS_CODEX_PROFILE require.cache[ccsPath] = { exports: {} } as NodeJS.Module; diff --git a/tests/unit/targets/codex-runtime-integration.test.ts b/tests/unit/targets/codex-runtime-integration.test.ts index ce428ca3..a14dfed4 100644 --- a/tests/unit/targets/codex-runtime-integration.test.ts +++ b/tests/unit/targets/codex-runtime-integration.test.ts @@ -1095,6 +1095,62 @@ supports_websockets = false expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([]); }); + it('launches a ccsx auth profile even when a Claude account has the same name', () => { + if (process.platform === 'win32') return; + + const codexProfileDir = path.join(ccsDir, 'codex-instances', 'ck'); + fs.mkdirSync(codexProfileDir, { recursive: true }); + fs.writeFileSync( + path.join(ccsDir, 'config.yaml'), + [ + 'version: 2', + 'accounts:', + ' ck:', + ' created: "2026-01-01"', + ' last_used: "2026-01-01"', + ].join('\n') + ); + fs.writeFileSync( + path.join(ccsDir, 'codex-profiles.yaml'), + [ + 'version: "1.0"', + 'default: null', + 'profiles:', + ' ck:', + ' type: codex', + ' created: "2026-01-01T00:00:00.000Z"', + ' last_used: null', + ].join('\n') + ); + + const result = runCodexAlias(['ck', 'fix failing tests'], { + ...process.env, + CI: '1', + NO_COLOR: '1', + HOME: tmpHome, + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath, + }); + + expect(result.status).toBe(0); + expect(result.stderr).not.toContain('Codex CLI does not support Claude account-based profiles.'); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['fix failing tests']]); + expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([ + { + CODEX_HOME: codexProfileDir, + CODEX_CI: undefined, + CODEX_MANAGED_BY_BUN: undefined, + CODEX_THREAD_ID: undefined, + ANTHROPIC_BASE_URL: undefined, + CCS_BROWSER_USER_DATA_DIR: undefined, + CCS_BROWSER_PROFILE_DIR: undefined, + CCS_BROWSER_DEVTOOLS_WS_URL: undefined, + }, + ]); + }); + it('rejects conflicting native provider config overrides for ccsxp', () => { if (process.platform === 'win32') return;