diff --git a/CHANGELOG.md b/CHANGELOG.md index c00be0b7..25f64fee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ ### Bug Fixes -* **codex:** Pass `ccsx resume` through to native Codex instead of treating `resume` as a CCS profile. +* **codex:** Pass native `ccsx` Codex subcommands such as `exec`, `apply`, `mcp`, `plugin`, `completion`, and `resume` plus upstream aliases such as `e` and `a` through before CCS profile detection while keeping CCS-owned commands reserved. ## [7.79.1](https://github.com/kaitranntt/ccs/compare/v7.79.0...v7.79.1) (2026-05-14) diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index 275c559f..417a5ec6 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -1,6 +1,6 @@ # CCS Codebase Summary -Last Updated: 2026-04-26 +Last Updated: 2026-05-18 Comprehensive overview of the modularized CCS codebase structure following the Phase 9 modularization effort (Settings, Analytics, Auth Monitor splits + Test Infrastructure), v7.1 Remote CLIProxy feature, v7.2 Kiro + GitHub Copilot (ghcp) OAuth providers, v7.14 Hybrid Quota Management, v7.34 Image Analysis Hook, account-context validation hardening, Official Claude Channels runtime support, native Codex runtime target support, native Codex/Droid usage collectors, and models.dev-backed model pricing metadata. @@ -273,7 +273,7 @@ src/ ### Native Codex Runtime Target - Dedicated runtime entrypoints: `ccs-codex` and `ccsx` resolve through `src/bin/codex-runtime.ts`, while `ccsxp` resolves through `src/bin/ccsxp-runtime.ts`; all three set `CCS_INTERNAL_ENTRY_TARGET=codex` before delegating to `src/targets/target-resolver.ts`. -- Native Codex passthrough: `ccsx --help`, `ccsx --version`, and `ccsx resume ...` short-circuit before CCS profile detection, so Codex diagnostics and session resume behavior stay aligned with the upstream Codex CLI. CCS-owned `ccsx auth` remains the managed Codex profile namespace. +- Native Codex passthrough: `ccsx --help`, `ccsx --version`, and known upstream Codex subcommands such as `ccsx exec ...`, `ccsx apply ...`, `ccsx mcp ...`, `ccsx plugin ...`, `ccsx completion ...`, and `ccsx resume ...` short-circuit before CCS profile detection; upstream aliases such as `ccsx e ...` and `ccsx a ...` are included. CCS-owned `ccsx auth`, `ccsx doctor`, and `ccsx update` remain reserved for CCS. - Provider shortcut behavior: `ccsxp` strips user-supplied `--target` overrides and prepends `--config model_provider="cliproxy"` so it behaves like native Codex plus the CLIProxy provider recipe. The stricter CCS-managed bridge remains available explicitly through `ccs codex --target codex`. It pins `CODEX_HOME` to native `~/.codex` by default so inherited launcher state does not send history/config writes to a nonstandard Codex root; `CCSXP_CODEX_HOME` is the explicit override. On launch, CCS repairs the native `[model_providers.cliproxy]` stanza in `config.toml`, preserves a valid custom `base_url`, reads that provider's configured `env_key` (default `CLIPROXY_API_KEY`), and injects the effective CLIProxy auth token into that key for the child Codex process. - Implicit Codex launches such as `ccs --target codex` and `ccsxp` use native Codex default mode even when the CCS default profile is a Claude account. Explicit unsupported profiles such as `ccs work --target codex` still fail fast with native-vs-pool guidance. - `argv[0]` alias mapping still exists in `src/targets/target-resolver.ts` for same-binary/custom alias scenarios, but the built-in npm bins above do not depend on that map at runtime. diff --git a/docs/system-architecture/target-adapters.md b/docs/system-architecture/target-adapters.md index 929a6ffb..6d785128 100644 --- a/docs/system-architecture/target-adapters.md +++ b/docs/system-architecture/target-adapters.md @@ -1,6 +1,6 @@ # Target Adapters -Last Updated: 2026-03-28 +Last Updated: 2026-05-18 Detailed documentation of the target adapter pattern and implementations. @@ -515,7 +515,8 @@ ccs-codex ccsx → dist/bin/codex-runtime.js → CCS_INTERNAL_ENTRY_TARGET=codex -→ passes native Codex diagnostics and `resume` through before CCS profile detection +→ passes native Codex diagnostics plus known upstream Codex subcommands and aliases through before CCS profile detection +→ reserves CCS-owned `auth`, `doctor`, and `update` ccsxp → dist/bin/ccsxp-runtime.js diff --git a/src/dispatcher/__tests__/cli-argument-parser-bootstrap.test.ts b/src/dispatcher/__tests__/cli-argument-parser-bootstrap.test.ts index 7372c1f9..b07f23ab 100644 --- a/src/dispatcher/__tests__/cli-argument-parser-bootstrap.test.ts +++ b/src/dispatcher/__tests__/cli-argument-parser-bootstrap.test.ts @@ -9,7 +9,37 @@ import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:te import * as os from 'os'; import * as fs from 'fs'; import * as path from 'path'; -import { bootstrapAndParseEarlyCli } from '../cli-argument-parser'; +import { + bootstrapAndParseEarlyCli, + CODEX_NATIVE_PASSTHROUGH_SUBCOMMANDS, + getNativeCodexPassthroughArgs, +} from '../cli-argument-parser'; +import { setGlobalConfigDir } from '../../utils/config-manager'; + +const EXPECTED_NATIVE_CODEX_SUBCOMMANDS = [ + 'a', + 'app', + 'app-server', + 'apply', + 'cloud', + 'completion', + 'debug', + 'e', + 'exec', + 'exec-server', + 'features', + 'fork', + 'help', + 'login', + 'logout', + 'mcp', + 'mcp-server', + 'plugin', + 'remote-control', + 'resume', + 'review', + 'sandbox', +]; // ========== Helpers ========== @@ -53,6 +83,7 @@ describe('bootstrapAndParseEarlyCli', () => { configurable: true, }); delete process.env['CI']; + setGlobalConfigDir(undefined); exitSpy.mockRestore(); }); @@ -150,3 +181,30 @@ describe('bootstrapAndParseEarlyCli', () => { expect(result.browserLaunchOverride).toBeUndefined(); }); }); + +describe('native Codex passthrough parsing', () => { + it('keeps the native Codex passthrough subcommand list explicit', () => { + expect([...CODEX_NATIVE_PASSTHROUGH_SUBCOMMANDS].sort()).toEqual( + EXPECTED_NATIVE_CODEX_SUBCOMMANDS + ); + + for (const subcommand of EXPECTED_NATIVE_CODEX_SUBCOMMANDS) { + expect(getNativeCodexPassthroughArgs(['--target', 'codex', subcommand, '--help'])).toEqual([ + subcommand, + '--help', + ]); + expect( + getNativeCodexPassthroughArgs(['--target', 'codex', 'codex', subcommand, '--help']) + ).toEqual([subcommand, '--help']); + } + }); + + it('keeps CCS-owned codex runtime commands out of native passthrough', () => { + for (const subcommand of ['auth', 'doctor', 'update']) { + expect(getNativeCodexPassthroughArgs(['--target', 'codex', subcommand, '--help'])).toBeNull(); + expect( + getNativeCodexPassthroughArgs(['--target', 'codex', 'codex', subcommand, '--help']) + ).toBeNull(); + } + }); +}); diff --git a/src/dispatcher/cli-argument-parser.ts b/src/dispatcher/cli-argument-parser.ts index dcb8c72c..c8407960 100644 --- a/src/dispatcher/cli-argument-parser.ts +++ b/src/dispatcher/cli-argument-parser.ts @@ -175,7 +175,30 @@ export const CODEX_RUNTIME_REASONING_LEVELS = new Set([ ]); export const CODEX_NATIVE_PASSTHROUGH_FLAGS = new Set(['--help', '-h', '--version', '-v']); -export const CODEX_NATIVE_PASSTHROUGH_SUBCOMMANDS = new Set(['resume']); +export const CODEX_NATIVE_PASSTHROUGH_SUBCOMMANDS = new Set([ + 'a', + 'app', + 'app-server', + 'apply', + 'cloud', + 'completion', + 'debug', + 'e', + 'exec', + 'exec-server', + 'features', + 'fork', + 'help', + 'login', + 'logout', + 'mcp', + 'mcp-server', + 'plugin', + 'remote-control', + 'resume', + 'review', + 'sandbox', +]); export const NATIVE_CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'] as const; diff --git a/tests/unit/targets/codex-runtime-integration.test.ts b/tests/unit/targets/codex-runtime-integration.test.ts index 92f24a44..51d4f226 100644 --- a/tests/unit/targets/codex-runtime-integration.test.ts +++ b/tests/unit/targets/codex-runtime-integration.test.ts @@ -498,10 +498,32 @@ process.exit(0); }); } - it('passes ccsx resume straight through to the native Codex binary', () => { + for (const subcommand of ['exec', 'e', 'apply', 'a', 'mcp', 'plugin', 'completion', 'resume']) { + it(`passes ccsx ${subcommand} straight through to the native Codex binary`, () => { + if (process.platform === 'win32') return; + + const result = runCodexAlias([subcommand, '--help'], { + ...process.env, + CI: '1', + NO_COLOR: '1', + CCS_HOME: tmpHome, + CODEX_HOME: path.join(tmpHome, '.codex'), + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_HELP: `Native ${subcommand} help`, + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain(`Native ${subcommand} help`); + expect(result.stderr).not.toContain(`Profile not found: ${subcommand}`); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([[subcommand, '--help']]); + }); + } + + it('passes nested ccsx codex subcommands through to the native Codex binary', () => { if (process.platform === 'win32') return; - const result = runCodexAlias(['resume', '--help'], { + const result = runCodexAlias(['codex', 'exec', '--help'], { ...process.env, CI: '1', NO_COLOR: '1', @@ -509,13 +531,13 @@ process.exit(0); CODEX_HOME: path.join(tmpHome, '.codex'), CCS_CODEX_PATH: fakeCodexPath, CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, - CCS_TEST_CODEX_HELP: 'Resume a previous interactive session', + CCS_TEST_CODEX_HELP: 'Native exec help', }); expect(result.status).toBe(0); - expect(result.stdout).toContain('Resume a previous interactive session'); - expect(result.stderr).not.toContain('Profile not found: resume'); - expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['resume', '--help']]); + expect(result.stdout).toContain('Native exec help'); + expect(result.stderr).not.toContain('Profile not found: codex'); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['exec', '--help']]); }); it('strips nested Codex session env from passthrough launches while keeping CODEX_HOME', () => {