mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 22:14:22 +00:00
Merge pull request #853 from kaitranntt/kai/feat/ccsxp-codex-shortcut
feat(codex): add ccsxp shortcut and native cliproxy guidance
This commit is contained in:
29 files changed
+754
-98
No files matched your search
@@ -197,13 +197,19 @@ ccs api import ./glm.ccs-profile.json # Import exported profile bundle
|
||||
|
||||
### Runtime Aliases (built-in bins / `argv[0]` pattern)
|
||||
|
||||
Built-in Droid runtime aliases are installed with the package:
|
||||
Built-in Droid and native Codex runtime aliases are installed with the package:
|
||||
|
||||
```bash
|
||||
ccs-droid glm # explicit alias
|
||||
ccsd glm # legacy shortcut
|
||||
ccs-codex # explicit Codex alias
|
||||
ccsx # short Codex alias
|
||||
ccs-codex # explicit native Codex alias
|
||||
ccsx # short native Codex alias
|
||||
```
|
||||
|
||||
CCS also ships an opinionated Codex provider shortcut:
|
||||
|
||||
```bash
|
||||
ccsxp # same as: ccs codex --target codex
|
||||
```
|
||||
|
||||
Need additional alias names? First create the matching symlink or another launcher that
|
||||
@@ -255,8 +261,9 @@ Supported in v1:
|
||||
```bash
|
||||
ccs --target codex # native Codex default session
|
||||
ccs-codex # explicit Codex alias
|
||||
ccsx # short alias
|
||||
ccs codex --target codex # built-in CLIProxy Codex on native Codex
|
||||
ccsx # short native Codex alias
|
||||
ccsxp # built-in CCS Codex provider on native Codex
|
||||
ccs codex --target codex # explicit equivalent of ccsxp
|
||||
ccs api create codex-api --cliproxy-provider codex
|
||||
ccs codex-api --target codex # Codex bridge profile on native Codex
|
||||
```
|
||||
@@ -287,6 +294,42 @@ with `Use default`. CCS also keeps warning that transient runtime overrides such
|
||||
`codex -c key=value` and `CCS_CODEX_API_KEY` can change the effective runtime without persisting
|
||||
back into the user config file.
|
||||
|
||||
#### CLIProxy-backed native Codex
|
||||
|
||||
There are two supported ways to use CLIProxy with native Codex:
|
||||
|
||||
1. `ccsxp` or `ccs codex --target codex`
|
||||
Uses the built-in CCS Codex provider route on native Codex. This path relies on transient
|
||||
CCS-managed `-c` overrides and does **not** require changing your saved `model_provider`.
|
||||
|
||||
2. Plain `codex` or a personal alias like `cxp`
|
||||
Save CLIProxy as the native default provider in `~/.codex/config.toml` (or
|
||||
`$CODEX_HOME/config.toml`), then export `CLIPROXY_API_KEY` in your shell.
|
||||
|
||||
```toml
|
||||
model_provider = "cliproxy"
|
||||
|
||||
[model_providers.cliproxy]
|
||||
base_url = "http://127.0.0.1:8317/api/provider/codex"
|
||||
env_key = "CLIPROXY_API_KEY"
|
||||
wire_api = "responses"
|
||||
```
|
||||
|
||||
The top-level `model_provider = "cliproxy"` line is required. Defining only
|
||||
`[model_providers.cliproxy]` is not enough for plain `codex` to pick it by default.
|
||||
|
||||
```bash
|
||||
export CLIPROXY_API_KEY="ccs-internal-managed"
|
||||
ccsxp "your prompt" # CCS shortcut for the built-in provider route
|
||||
codex "your prompt" # native Codex using your saved cliproxy default
|
||||
```
|
||||
|
||||
Dashboard parity: `ccs config` -> `Compatible` -> `Codex CLI`
|
||||
|
||||
- `Overview`: explains `ccsx` vs `ccsxp`
|
||||
- `Top-level settings`: set **Default provider** to `cliproxy`
|
||||
- `Model providers`: save `cliproxy` with `env_key = "CLIPROXY_API_KEY"`
|
||||
|
||||
### Per-Profile Target Defaults
|
||||
|
||||
You can pin a default target (`claude` or `droid`) per profile:
|
||||
|
||||
@@ -198,16 +198,21 @@ Resolves which adapter to use via `resolveTargetType()`:
|
||||
```
|
||||
1. --target <name> flag (highest priority)
|
||||
↓
|
||||
2. argv[0] detection (runtime alias pattern):
|
||||
2. explicit runtime entrypoint (`CCS_INTERNAL_ENTRY_TARGET`):
|
||||
- ccs-droid / ccsd → droid
|
||||
- ccs-codex / ccsx → codex
|
||||
- ccsxp → codex (provider shortcut)
|
||||
↓
|
||||
3. argv[0] detection (runtime alias pattern / custom alias map):
|
||||
- ccs-droid → droid
|
||||
- ccsd → droid
|
||||
- ccs-codex → codex
|
||||
- ccsx → codex
|
||||
- ccs → default
|
||||
↓
|
||||
3. Profile config: profileConfig.target field
|
||||
4. Profile config: profileConfig.target field
|
||||
↓
|
||||
4. Fallback: 'claude' (lowest priority)
|
||||
5. Fallback: 'claude' (lowest priority)
|
||||
```
|
||||
|
||||
### Registration Pattern
|
||||
|
||||
@@ -36,8 +36,9 @@ The main CLI is organized into domain-specific modules with barrel exports.
|
||||
src/
|
||||
├── ccs.ts # Main entry point & profile execution flow
|
||||
├── bin/ # Dedicated runtime entrypoints
|
||||
│ ├── droid-runtime.ts # argv[0] shim for ccs-droid / ccsd
|
||||
│ └── codex-runtime.ts # argv[0] shim for ccs-codex / ccsx
|
||||
│ ├── droid-runtime.ts # Forces droid target for ccs-droid / ccsd package bins
|
||||
│ ├── codex-runtime.ts # Forces codex target for ccs-codex / ccsx package bins
|
||||
│ └── ccsxp-runtime.ts # Forces built-in codex profile + codex target for ccsxp
|
||||
├── types/ # TypeScript type definitions
|
||||
│ ├── index.ts # Barrel export (aggregates all types)
|
||||
│ ├── cli.ts # CLI types (ParsedArgs, ExitCode)
|
||||
@@ -69,7 +70,7 @@ src/
|
||||
│ ├── index.ts # Barrel export
|
||||
│ ├── target-adapter.ts # TargetAdapter interface contract
|
||||
│ ├── target-registry.ts # Registry for runtime adapter lookup
|
||||
│ ├── target-resolver.ts # Resolution logic (flag > argv[0] > config)
|
||||
│ ├── target-resolver.ts # Resolution logic (flag > runtime entrypoint / argv[0] > config)
|
||||
│ ├── target-metadata.ts # Runtime vs persisted target metadata and alias lists
|
||||
│ ├── target-runtime-compatibility.ts # Guardrails for target/profile combinations
|
||||
│ ├── claude-adapter.ts # Claude Code CLI implementation
|
||||
@@ -248,7 +249,9 @@ src/
|
||||
|
||||
### Native Codex Runtime Target
|
||||
|
||||
- Runtime aliases: `ccs-codex` and `ccsx` resolve through `src/bin/codex-runtime.ts` and `src/targets/target-resolver.ts`.
|
||||
- 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`.
|
||||
- Provider shortcut behavior: `ccsxp` also strips user-supplied `--target` overrides and rewrites argv to `ccs codex --target codex ...`, so it always lands on the built-in Codex-via-CLIProxy route.
|
||||
- `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.
|
||||
- Metadata boundary: `src/targets/target-metadata.ts` keeps Codex runtime-only in v1, so persisted default targets remain `claude | droid`.
|
||||
- Compatibility guardrails: `src/targets/target-runtime-compatibility.ts` centralizes which profile types can execute on Codex.
|
||||
- Adapter behavior: `src/targets/codex-adapter.ts` and `src/targets/codex-detector.ts` launch native Codex without rewriting `~/.codex/config.toml`; CCS-backed routes use transient `codex -c key=value` overrides and env-key injection.
|
||||
@@ -279,7 +282,8 @@ The targets module provides an extensible interface for dispatching profiles to
|
||||
|
||||
2. **Target Resolution** - Priority order:
|
||||
- `--target <cli>` flag (CLI argument)
|
||||
- `argv[0]` detection (runtime alias pattern: `ccs-droid` / `ccsd` → droid)
|
||||
- Explicit runtime entrypoint via `CCS_INTERNAL_ENTRY_TARGET` (used by `src/bin/droid-runtime.ts`, `src/bin/codex-runtime.ts`, and `src/bin/ccsxp-runtime.ts`)
|
||||
- `argv[0]` detection for custom/same-binary runtime aliases
|
||||
- Per-profile `target` field (from config.yaml)
|
||||
- Default: `claude`
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ CCS v7.45 introduces the Target Adapter pattern, enabling seamless integration w
|
||||
Profile Resolution (CLIProxy, Settings/API, Account-based)
|
||||
|
|
||||
v
|
||||
Target Resolution (--target flag > argv[0] > config > default)
|
||||
Target Resolution (--target flag > runtime entrypoint / argv[0] > config > default)
|
||||
|
|
||||
v
|
||||
Get Target Adapter (Claude, Droid, or Codex)
|
||||
@@ -91,7 +91,7 @@ Spawn Target Process
|
||||
- Preserves native `~/.codex/config.toml` ownership
|
||||
- Dashboard page reads/writes only the user config layer with explicit runtime-vs-provider warnings
|
||||
|
||||
**Runtime alias pattern (built-in bins / argv[0]-style):**
|
||||
**Runtime entrypoints (built-in bins) and argv[0]-style aliases:**
|
||||
|
||||
```
|
||||
ccs → Target: claude (default)
|
||||
@@ -99,6 +99,7 @@ ccs-droid → Target: droid (explicit alias)
|
||||
ccsd → Target: droid (legacy shortcut)
|
||||
ccs-codex → Target: codex (explicit alias)
|
||||
ccsx → Target: codex (short alias)
|
||||
ccsxp → Target: codex (provider shortcut; rewrites argv to `ccs codex --target codex`)
|
||||
```
|
||||
|
||||
For details on the adapter architecture, see [Target Adapters](./target-adapters.md).
|
||||
|
||||
@@ -85,20 +85,25 @@ CCS resolves which adapter to use via priority-ordered checks:
|
||||
└─ ccs --target droid glm
|
||||
└─ ccs --target codex
|
||||
|
||||
2. argv[0] detection (runtime alias pattern) — binary name mapping
|
||||
2. Explicit runtime entrypoint (`CCS_INTERNAL_ENTRY_TARGET`) — dedicated bin shims
|
||||
└─ ccs-droid / ccsd → droid
|
||||
└─ ccs-codex / ccsx → codex
|
||||
└─ ccsxp → codex, then rewrites argv to `ccs codex --target codex ...`
|
||||
|
||||
3. argv[0] detection (runtime alias pattern) — binary name mapping for same-binary/custom aliases
|
||||
└─ ccs-droid (explicit alias) → droid
|
||||
└─ ccsd (legacy shortcut) → droid
|
||||
└─ ccs-codex (explicit alias) → codex
|
||||
└─ ccsx (short alias) → codex
|
||||
└─ ccs (regular command) → default
|
||||
|
||||
3. Per-profile config (from ~/.ccs/config.yaml or settings.json)
|
||||
4. Per-profile config (from ~/.ccs/config.yaml or settings.json)
|
||||
└─ persisted targets are currently only `claude` and `droid`
|
||||
└─ profiles:
|
||||
glm:
|
||||
target: droid
|
||||
|
||||
4. Fallback: 'claude' — lowest priority
|
||||
5. Fallback: 'claude' — lowest priority
|
||||
```
|
||||
|
||||
### Implementation
|
||||
@@ -117,19 +122,25 @@ export function resolveTargetType(
|
||||
return parsed.targetOverride;
|
||||
}
|
||||
|
||||
// 2. Check argv[0] (binary name)
|
||||
// 2. Check explicit runtime entrypoint shim
|
||||
const entrypointTarget = resolveEntrypointTarget();
|
||||
if (entrypointTarget) {
|
||||
return entrypointTarget;
|
||||
}
|
||||
|
||||
// 3. Check argv[0] (binary name / custom alias map)
|
||||
const binName = path.basename(process.argv[1] || process.argv0 || '').replace(/\.(cmd|bat|ps1|exe)$/i, '');
|
||||
if (ARGV0_TARGET_MAP[binName]) {
|
||||
return ARGV0_TARGET_MAP[binName];
|
||||
}
|
||||
|
||||
// 3. Check profile config
|
||||
// 4. Check profile config
|
||||
if (profileConfig?.target) {
|
||||
// Persisted targets intentionally exclude runtime-only codex.
|
||||
return profileConfig.target;
|
||||
}
|
||||
|
||||
// 4. Default to claude
|
||||
// 5. Default to claude
|
||||
return 'claude';
|
||||
}
|
||||
```
|
||||
@@ -488,20 +499,27 @@ longer read-mostly:
|
||||
This keeps the dashboard honest about Codex's merged configuration model while still giving users
|
||||
one place to inspect and manage the user-owned layer safely.
|
||||
|
||||
### Runtime Alias Pattern
|
||||
### Runtime Entrypoints and argv[0] Fallback
|
||||
|
||||
```bash
|
||||
# Built-in package bin aliases
|
||||
# Built-in package bin entrypoints
|
||||
ccs-codex
|
||||
→ Target: codex (forced by runtime alias)
|
||||
→ dist/bin/codex-runtime.js
|
||||
→ CCS_INTERNAL_ENTRY_TARGET=codex
|
||||
|
||||
ccsx codex
|
||||
→ Target: codex (forced by runtime alias)
|
||||
→ codex ...args
|
||||
ccsx
|
||||
→ dist/bin/codex-runtime.js
|
||||
→ CCS_INTERNAL_ENTRY_TARGET=codex
|
||||
|
||||
ccsxp
|
||||
→ dist/bin/ccsxp-runtime.js
|
||||
→ CCS_INTERNAL_ENTRY_TARGET=codex
|
||||
→ injects built-in codex profile shortcut
|
||||
```
|
||||
|
||||
Runtime aliases can also be extended with `CCS_TARGET_ALIASES` or legacy
|
||||
`CCS_CODEX_ALIASES` after creating a matching launcher:
|
||||
If a user launches CCS through a custom shim instead of the built-in package bins, target
|
||||
resolution falls back to `argv[0]` aliases from `CCS_TARGET_ALIASES` or legacy
|
||||
`CCS_CODEX_ALIASES`:
|
||||
|
||||
```bash
|
||||
ln -s /path/to/ccs /path/to/mycodex
|
||||
@@ -757,6 +775,7 @@ ccs --target droid help
|
||||
# Test Codex adapter (if installed)
|
||||
ccs --target codex
|
||||
ccs-codex
|
||||
ccsxp
|
||||
|
||||
# Test argv[0] detection
|
||||
ccs-droid help
|
||||
|
||||
+2
-1
@@ -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/",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const { stripTargetFlag } = require('../targets/target-resolver');
|
||||
const { fail } = require('../utils/ui');
|
||||
|
||||
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 = (() => {
|
||||
try {
|
||||
return stripTargetFlag(process.argv.slice(2));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(fail(message));
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
||||
process.argv.splice(2, process.argv.length - 2, 'codex', '--target', 'codex', ...forwardedArgs);
|
||||
require('../ccs');
|
||||
+22
-6
@@ -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: '',
|
||||
|
||||
@@ -177,9 +177,7 @@ function showHelp(): void {
|
||||
console.log(
|
||||
` $ ${color('ccs config channels --clear-token discord', 'command')} ${dim('# Clear one token')}`
|
||||
);
|
||||
console.log(
|
||||
` ${dim('Official Channels only work on native Claude default/account sessions, not on ccs glm or other API/OAuth/Droid targets.')}`
|
||||
);
|
||||
console.log(` ${dim(getOfficialChannelsSupportMessage())}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { extractOption, hasAnyFlag, scanCommandArgs } from './arg-extractor';
|
||||
import { getOfficialChannelsSupportMessage } from '../channels/official-channels-runtime';
|
||||
|
||||
const CONFIG_COMMAND_FLAGS = ['--help', '-h', '--port', '-p', '--host', '-H', '--dev'] as const;
|
||||
|
||||
@@ -92,8 +93,7 @@ export function showConfigCommandHelp(): void {
|
||||
console.log(' --set-token <s> Save channel token (telegram=<t> or discord=<t>)');
|
||||
console.log(' --clear-token Remove all saved channel tokens');
|
||||
console.log(' --clear-token <c> Remove one saved channel token');
|
||||
console.log(' Works only for native Claude default/account sessions');
|
||||
console.log(' Not for ccs glm, other API/OAuth profiles, or Droid targets');
|
||||
console.log(` ${getOfficialChannelsSupportMessage()}`);
|
||||
console.log('');
|
||||
console.log(' auth Manage dashboard authentication');
|
||||
console.log(' auth setup Configure username and password');
|
||||
|
||||
@@ -4,6 +4,7 @@ import { initUI, box, color, dim, sectionHeader, subheader } from '../utils/ui';
|
||||
import { isUnifiedMode } from '../config/unified-config-loader';
|
||||
import { getCcsDir, getCcsDirSource } from '../utils/config-manager';
|
||||
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
|
||||
import { getOfficialChannelsSupportMessage } from '../channels/official-channels-runtime';
|
||||
|
||||
type HelpWriter = (line: string) => void;
|
||||
|
||||
@@ -407,6 +408,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
['ccsd <profile> [args]', 'Legacy shortcut for: ccs-droid <profile> [args]'],
|
||||
['ccs-codex <profile> [args]', 'Explicit Codex runtime alias'],
|
||||
['ccsx <profile> [args]', 'Short alias for: ccs-codex <profile> [args]'],
|
||||
['ccsxp [args]', 'Shortcut for: ccs codex --target codex [args]'],
|
||||
],
|
||||
writeLine
|
||||
);
|
||||
@@ -421,7 +423,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
['ccs --target codex', 'Open a native Codex session with your existing ~/.codex setup'],
|
||||
['ccs-codex', 'Same as above (explicit Codex alias)'],
|
||||
['ccsx', 'Short alias for ccs-codex'],
|
||||
['ccs codex --target codex', 'Run built-in CLIProxy Codex on native Codex CLI'],
|
||||
['ccsxp "your prompt"', 'Run built-in CLIProxy Codex on native Codex CLI'],
|
||||
['ccs codex --target codex', 'Explicit form of ccsxp'],
|
||||
[
|
||||
'ccs api create codex-api --cliproxy-provider codex',
|
||||
'Create a routed API bridge that can also run on Codex',
|
||||
@@ -450,6 +453,20 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
writeLine
|
||||
);
|
||||
|
||||
printSubSection(
|
||||
'Codex + CLIProxy',
|
||||
[
|
||||
['ccsxp "your prompt"', 'Use the built-in CCS Codex provider shortcut on native Codex'],
|
||||
['ccs config', 'Open Compatible -> Codex CLI for native Codex setup and diagnostics'],
|
||||
[
|
||||
'Default provider',
|
||||
'Set to cliproxy if plain codex or a personal cxp alias should use CLIProxy',
|
||||
],
|
||||
['Model provider auth', 'Save cliproxy with env_key = "CLIPROXY_API_KEY"'],
|
||||
],
|
||||
writeLine
|
||||
);
|
||||
|
||||
// Configuration
|
||||
printConfigSection(
|
||||
'Configuration',
|
||||
@@ -592,8 +609,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
['ccs config channels --clear-token [channel]', 'Remove one or all saved channel tokens'],
|
||||
['', ''],
|
||||
['', 'Fastest path: turn on the channel, save the token if needed, then run ccs.'],
|
||||
['Note:', 'Runtime-only. Applies to native Claude default/account sessions only.'],
|
||||
['', 'Not supported for ccs glm, other API/OAuth profiles, or Droid targets.'],
|
||||
['Note:', getOfficialChannelsSupportMessage()],
|
||||
['', 'Telegram/Discord tokens live in ~/.claude/channels/<channel>/.env.'],
|
||||
['', 'Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work for that launch.'],
|
||||
['', 'iMessage is macOS-only and requires local OS permissions instead of a bot token.'],
|
||||
|
||||
@@ -65,7 +65,7 @@ export function evaluateTargetRuntimeCompatibility(
|
||||
if (input.cliproxyProvider !== 'codex') {
|
||||
return unsupported(
|
||||
`Codex CLI only supports CLIProxy provider "codex". This profile routes to "${input.cliproxyProvider || 'unknown'}".`,
|
||||
'Use: ccs codex --target codex, ccs-codex codex, or stay on Claude/Droid for other providers.'
|
||||
'Use: ccsxp, ccs codex --target codex, or stay on Claude/Droid for other providers.'
|
||||
);
|
||||
}
|
||||
return { supported: true };
|
||||
|
||||
@@ -72,6 +72,7 @@ const SANDBOX_MODE_VALUES = new Set(['read-only', 'workspace-write', 'danger-ful
|
||||
const WEB_SEARCH_VALUES = new Set(['cached', 'live', 'disabled']);
|
||||
const PERSONALITY_VALUES = new Set(['none', 'friendly', 'pragmatic']);
|
||||
const PROJECT_TRUST_LEVEL_VALUES = new Set(['trusted', 'untrusted']);
|
||||
const BUILT_IN_CODEX_MODEL_PROVIDERS = new Set(['openai', 'oss']);
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
@@ -572,6 +573,10 @@ export function summarizeCodexModelProviders(value: unknown): CodexModelProvider
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
}
|
||||
|
||||
function isBuiltInCodexModelProvider(name: string | null): boolean {
|
||||
return name !== null && BUILT_IN_CODEX_MODEL_PROVIDERS.has(name);
|
||||
}
|
||||
|
||||
export function summarizeCodexFeatureFlags(value: unknown): {
|
||||
all: CodexFeatureFlagDiagnostics[];
|
||||
enabled: CodexFeatureFlagDiagnostics[];
|
||||
@@ -702,6 +707,7 @@ export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiag
|
||||
? Object.keys(config).sort((left, right) => left.localeCompare(right))
|
||||
: [];
|
||||
const activeProfile = asString(config?.profile);
|
||||
const activeModelProvider = asString(config?.model_provider);
|
||||
const profileNames = Object.keys(asObject(config?.profiles) ?? {}).sort((left, right) =>
|
||||
left.localeCompare(right)
|
||||
);
|
||||
@@ -726,6 +732,21 @@ export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiag
|
||||
if (activeProfile && !profileNames.includes(activeProfile)) {
|
||||
warnings.push(`Active profile "${activeProfile}" is selected but missing from [profiles].`);
|
||||
}
|
||||
if (activeModelProvider) {
|
||||
const activeProvider = modelProviders.find((provider) => provider.name === activeModelProvider);
|
||||
if (!activeProvider && !isBuiltInCodexModelProvider(activeModelProvider)) {
|
||||
warnings.push(
|
||||
`Active model provider "${activeModelProvider}" is selected but missing from [model_providers].`
|
||||
);
|
||||
} else if (activeProvider) {
|
||||
if (!activeProvider.baseUrl) {
|
||||
warnings.push(`Active model provider "${activeProvider.name}" is missing base_url.`);
|
||||
}
|
||||
if (!activeProvider.envKey && !activeProvider.requiresOpenaiAuth) {
|
||||
warnings.push(`Active model provider "${activeProvider.name}" is missing env_key.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (modelProviders.some((provider) => provider.usesExperimentalBearerToken)) {
|
||||
warnings.push(
|
||||
'One or more model_providers entries use experimental_bearer_token; prefer env_key-backed auth.'
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -81,7 +81,8 @@ describe('help command parity', () => {
|
||||
'Fastest path: turn on the channel, save the token if needed, then run ccs.'
|
||||
)
|
||||
).toBe(true);
|
||||
expect(rendered.includes('Not supported for ccs glm')).toBe(true);
|
||||
expect(rendered.includes('ccs codex')).toBe(true);
|
||||
expect(rendered.includes('ccs --target codex')).toBe(true);
|
||||
expect(
|
||||
rendered.includes('Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work')
|
||||
).toBe(true);
|
||||
@@ -106,8 +107,12 @@ describe('help command parity', () => {
|
||||
const rendered = stripAnsi(lines.join('\n'));
|
||||
expect(rendered.includes('ccs-codex <profile> [args]')).toBe(true);
|
||||
expect(rendered.includes('ccsx <profile> [args]')).toBe(true);
|
||||
expect(rendered.includes('ccsxp [args]')).toBe(true);
|
||||
expect(rendered.includes('ccs --target codex')).toBe(true);
|
||||
expect(rendered.includes('ccsxp "your prompt"')).toBe(true);
|
||||
expect(rendered.includes('ccs codex-api --target codex')).toBe(true);
|
||||
expect(rendered.includes('Compatible -> Codex CLI')).toBe(true);
|
||||
expect(rendered.includes('CLIPROXY_API_KEY')).toBe(true);
|
||||
expect(rendered.includes('codex (runtime-only)')).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 <key=value>\\n -p, --profile <CONFIG_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,64 @@ 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 <CONFIG_PROFILE>\\n',
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain('codex-cli 9.9.9-test');
|
||||
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]);
|
||||
});
|
||||
|
||||
it('fails with a clean CLI error when ccsxp receives a malformed --target flag', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const result = runCcsxpAlias(['--target'], {
|
||||
...process.env,
|
||||
CI: '1',
|
||||
NO_COLOR: '1',
|
||||
CCS_HOME: tmpHome,
|
||||
CCS_CODEX_PATH: fakeCodexPath,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain('[X] --target requires a value (claude, droid, codex)');
|
||||
expect(result.stderr).not.toContain('at parseTargetFlags');
|
||||
});
|
||||
|
||||
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 <CONFIG_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');
|
||||
|
||||
@@ -218,6 +218,48 @@ model = "gpt-5.4"
|
||||
expect(diagnostics.supportMatrix.some((entry) => entry.id === 'default')).toBe(true);
|
||||
});
|
||||
|
||||
it('warns when the active model provider is selected without base_url or env_key', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(codexHome, 'config.toml'),
|
||||
`model_provider = "cliproxy"
|
||||
|
||||
[model_providers.cliproxy]
|
||||
wire_api = "responses"
|
||||
`
|
||||
);
|
||||
|
||||
const diagnostics = await getCodexDashboardDiagnostics();
|
||||
|
||||
expect(diagnostics.warnings.some((warning) => warning.includes('missing base_url'))).toBe(true);
|
||||
expect(diagnostics.warnings.some((warning) => warning.includes('missing env_key'))).toBe(true);
|
||||
});
|
||||
|
||||
it('does not warn for built-in openai provider without a custom model_providers entry', async () => {
|
||||
fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model_provider = "openai"\n');
|
||||
|
||||
const diagnostics = await getCodexDashboardDiagnostics();
|
||||
|
||||
expect(diagnostics.warnings.some((warning) => warning.includes('missing from [model_providers]'))).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('allows custom providers that use requires_openai_auth without env_key warnings', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(codexHome, 'config.toml'),
|
||||
`model_provider = "corp-openai"
|
||||
|
||||
[model_providers.corp-openai]
|
||||
base_url = "https://example.test/v1"
|
||||
requires_openai_auth = true
|
||||
`
|
||||
);
|
||||
|
||||
const diagnostics = await getCodexDashboardDiagnostics();
|
||||
|
||||
expect(diagnostics.warnings.some((warning) => warning.includes('missing env_key'))).toBe(false);
|
||||
});
|
||||
|
||||
it('summarizes granular approval policies without flattening them to null', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(codexHome, 'config.toml'),
|
||||
|
||||
@@ -297,6 +297,7 @@ export function ProviderEditor({
|
||||
defaultTarget={defaultTarget}
|
||||
data={data}
|
||||
authStatus={authStatus}
|
||||
supportsModelConfig={Boolean(catalog)}
|
||||
/>
|
||||
</TabsContent>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ interface ProviderInfoTabProps {
|
||||
defaultTarget?: CliTarget;
|
||||
data?: SettingsResponse;
|
||||
authStatus: AuthStatus;
|
||||
supportsModelConfig?: boolean;
|
||||
}
|
||||
|
||||
export function ProviderInfoTab({
|
||||
@@ -25,10 +26,16 @@ export function ProviderInfoTab({
|
||||
defaultTarget,
|
||||
data,
|
||||
authStatus,
|
||||
supportsModelConfig = false,
|
||||
}: ProviderInfoTabProps) {
|
||||
const resolvedTarget = defaultTarget || 'claude';
|
||||
const isDroidTarget = resolvedTarget === 'droid';
|
||||
const isCodexProvider = provider === 'codex';
|
||||
const managementPrefix =
|
||||
resolvedTarget === 'claude' ? `ccs ${provider}` : `ccs ${provider} --target claude`;
|
||||
const changeModelCommand = `${managementPrefix} --config`;
|
||||
const addAccountCommand = `${managementPrefix} --auth --add`;
|
||||
const listAccountsCommand = `${managementPrefix} --accounts`;
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
@@ -91,18 +98,14 @@ export function ProviderInfoTab({
|
||||
<UsageCommand label="Run with prompt" command={`ccs ${provider} "your prompt"`} />
|
||||
{isCodexProvider && (
|
||||
<>
|
||||
<UsageCommand
|
||||
label="Run on native Codex (shortcut)"
|
||||
command={`ccsxp "your prompt"`}
|
||||
/>
|
||||
<UsageCommand
|
||||
label="Run on native Codex (--target)"
|
||||
command={`ccs ${provider} --target codex "your prompt"`}
|
||||
/>
|
||||
<UsageCommand
|
||||
label="Codex alias (explicit)"
|
||||
command={`ccs-codex ${provider} "your prompt"`}
|
||||
/>
|
||||
<UsageCommand
|
||||
label="Codex alias (short)"
|
||||
command={`ccsx ${provider} "your prompt"`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<UsageCommand
|
||||
@@ -113,9 +116,30 @@ export function ProviderInfoTab({
|
||||
label={isDroidTarget ? 'Override to Claude' : 'Run on Droid (--target)'}
|
||||
command={`ccs ${provider} --target ${isDroidTarget ? 'claude' : 'droid'} "your prompt"`}
|
||||
/>
|
||||
<UsageCommand label="Change model" command={`ccs ${provider} --config`} />
|
||||
<UsageCommand label="Add account" command={`ccs ${provider} --add`} />
|
||||
<UsageCommand label="List accounts" command={`ccs ${provider} --accounts`} />
|
||||
{supportsModelConfig && (
|
||||
<UsageCommand
|
||||
label={
|
||||
resolvedTarget === 'claude' ? 'Change model' : 'Change model (Claude target)'
|
||||
}
|
||||
command={changeModelCommand}
|
||||
/>
|
||||
)}
|
||||
<UsageCommand
|
||||
label={resolvedTarget === 'claude' ? 'Add account' : 'Add account (Claude target)'}
|
||||
command={addAccountCommand}
|
||||
/>
|
||||
<UsageCommand
|
||||
label={
|
||||
resolvedTarget === 'claude' ? 'List accounts' : 'List accounts (Claude target)'
|
||||
}
|
||||
command={listAccountsCommand}
|
||||
/>
|
||||
{resolvedTarget !== 'claude' && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Account and model-management flags stay on Claude target. Codex and Droid runtime
|
||||
launches reject those CLIProxy management commands.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
CompatibleCliProviderDocLink,
|
||||
CodexDashboardDiagnostics,
|
||||
} from '@/hooks/use-codex-types';
|
||||
import { CLIPROXY_NATIVE_CODEX_RECIPE } from '@/lib/codex-config';
|
||||
|
||||
const DEFAULT_CODEX_DOC_LINKS = [
|
||||
{
|
||||
@@ -98,6 +99,36 @@ export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) {
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="space-y-4 pr-1">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
CCS bridge recipe
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Use <code>ccsxp</code> if you want the built-in CCS Codex provider shortcut on native
|
||||
Codex. Use the saved recipe below if you want plain <code>codex</code> or a personal
|
||||
alias like <code>cxp</code> to default to CLIProxy.
|
||||
</p>
|
||||
<pre className="overflow-x-auto rounded-md border bg-muted/20 p-3 text-xs text-foreground">
|
||||
{CLIPROXY_NATIVE_CODEX_RECIPE}
|
||||
</pre>
|
||||
<div className="space-y-1">
|
||||
<p>
|
||||
1. Save the <code>cliproxy</code> provider in your user config.
|
||||
</p>
|
||||
<p>
|
||||
2. Set top-level <code>model_provider</code> to <code>cliproxy</code>.
|
||||
</p>
|
||||
<p>
|
||||
3. Export <code>CLIPROXY_API_KEY</code> in your shell before launching native Codex.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
|
||||
@@ -33,6 +33,16 @@ const EMPTY_MODEL_PROVIDER_DRAFT: CodexModelProviderEntry = {
|
||||
supportsWebsockets: false,
|
||||
};
|
||||
|
||||
const CLIPROXY_CODEX_PROVIDER_PRESET: CodexModelProviderEntry = {
|
||||
name: 'cliproxy',
|
||||
displayName: 'CLIProxy Codex',
|
||||
baseUrl: 'http://127.0.0.1:8317/api/provider/codex',
|
||||
envKey: 'CLIPROXY_API_KEY',
|
||||
wireApi: 'responses',
|
||||
requiresOpenaiAuth: false,
|
||||
supportsWebsockets: false,
|
||||
};
|
||||
|
||||
interface ModelProviderEditorProps {
|
||||
initialDraft: CodexModelProviderEntry;
|
||||
isNew: boolean;
|
||||
@@ -56,6 +66,23 @@ function ModelProviderEditor({
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNew && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 rounded-md border border-dashed px-3 py-2 text-xs text-muted-foreground">
|
||||
<p>
|
||||
Quick start: apply the CLIProxy Codex preset here, then set{' '}
|
||||
<strong>Default provider</strong> to <code>cliproxy</code> in Top-level settings.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setDraft(CLIPROXY_CODEX_PROVIDER_PRESET)}
|
||||
disabled={disabled}
|
||||
>
|
||||
Use CLIProxy Codex preset
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Input
|
||||
value={draft.name}
|
||||
@@ -146,6 +173,12 @@ function ModelProviderEditor({
|
||||
Save provider
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
If you want plain native <code>codex</code> to default to CLIProxy, save a provider named{' '}
|
||||
<code>cliproxy</code> with <code>CLIPROXY_API_KEY</code> here, then pick{' '}
|
||||
<code>cliproxy</code> in the <strong>Default provider</strong> control above.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { CodexDashboardDiagnostics } from '@/hooks/use-codex-types';
|
||||
import { CLIPROXY_NATIVE_CODEX_RECIPE } from '@/lib/codex-config';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function formatTimestamp(value: number | null | undefined): string {
|
||||
@@ -58,6 +59,10 @@ interface CodexOverviewTabProps {
|
||||
}
|
||||
|
||||
export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
const inspectProfileCommand = diagnostics.config.activeProfile
|
||||
? `codex --profile ${diagnostics.config.activeProfile}`
|
||||
: 'codex';
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="space-y-4 pr-1">
|
||||
@@ -70,6 +75,21 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>Codex is a first-class runtime target in CCS, but it stays runtime-only in v1.</p>
|
||||
<p>
|
||||
<code>ccs-codex</code> and <code>ccsx</code> launch native Codex against your saved
|
||||
native config, while <code>ccsxp</code> is the opinionated shortcut for{' '}
|
||||
<code>ccs codex --target codex</code>.
|
||||
</p>
|
||||
<p>
|
||||
Plain <code>codex</code> or a personal alias like <code>cxp</code> needs{' '}
|
||||
<code>model_provider = "cliproxy"</code> plus a matching{' '}
|
||||
<code>[model_providers.cliproxy]</code> entry if you want CLIProxy as the saved native
|
||||
default.
|
||||
</p>
|
||||
<p>
|
||||
Built-in <code>openai</code> and <code>oss</code> providers are also valid native
|
||||
defaults and do not need a custom <code>[model_providers]</code> stanza.
|
||||
</p>
|
||||
<p>
|
||||
Saved default targets for API profiles and variants still remain on Claude or Droid.
|
||||
</p>
|
||||
@@ -103,7 +123,8 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
mono
|
||||
/>
|
||||
<DetailRow label="Version" value={diagnostics.binary.version || 'Unknown'} mono />
|
||||
<DetailRow label="Alias commands" value="ccs-codex, ccsx" mono />
|
||||
<DetailRow label="Native aliases" value="ccs-codex, ccsx" mono />
|
||||
<DetailRow label="CCS provider shortcut" value="ccsxp" mono />
|
||||
<div className="flex items-center justify-between rounded-md border px-3 py-2">
|
||||
<span className="text-sm text-muted-foreground">--config override support</span>
|
||||
<Badge variant={diagnostics.binary.supportsConfigOverrides ? 'default' : 'secondary'}>
|
||||
@@ -113,6 +134,40 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Route className="h-4 w-4" />
|
||||
CLIProxy-backed native Codex
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<p>
|
||||
There are two supported paths. Use <code>ccsxp</code> if you want the built-in CCS
|
||||
Codex provider shortcut. Use the saved recipe below if you want plain{' '}
|
||||
<code>codex</code> or a personal alias like <code>cxp</code> to default to CLIProxy.
|
||||
</p>
|
||||
<div className="rounded-md border bg-muted/20 p-3">
|
||||
<p className="font-medium text-foreground">Saved native Codex recipe</p>
|
||||
<pre className="mt-2 overflow-x-auto rounded-md bg-background p-3 text-xs text-foreground">
|
||||
{CLIPROXY_NATIVE_CODEX_RECIPE}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p>
|
||||
1. Save a provider named <code>cliproxy</code> with the base URL and env key above.
|
||||
</p>
|
||||
<p>
|
||||
2. In <strong>Top-level settings</strong>, set <strong>Default provider</strong> to{' '}
|
||||
<code>cliproxy</code>.
|
||||
</p>
|
||||
<p>
|
||||
3. Export <code>CLIPROXY_API_KEY</code> in your shell before launching native Codex.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
@@ -213,24 +268,28 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<QuickCommands
|
||||
snippets={[
|
||||
{
|
||||
label: 'Native Codex',
|
||||
command: 'ccs-codex',
|
||||
description: 'Launch the native Codex runtime alias.',
|
||||
},
|
||||
{
|
||||
label: 'Short alias',
|
||||
label: 'Native short alias',
|
||||
command: 'ccsx',
|
||||
description: 'Launch the short Codex runtime alias.',
|
||||
description: 'Launch the short native Codex runtime alias.',
|
||||
},
|
||||
{
|
||||
label: 'Target override',
|
||||
label: 'CCS Codex shortcut',
|
||||
command: 'ccsxp "your prompt"',
|
||||
description: 'Run the built-in CCS Codex provider on native Codex.',
|
||||
},
|
||||
{
|
||||
label: 'Explicit provider route',
|
||||
command: 'ccs codex --target codex "your prompt"',
|
||||
description: 'Run a CCS profile on the native Codex target.',
|
||||
description: 'Use the explicit built-in Codex provider route.',
|
||||
},
|
||||
{
|
||||
label: 'Workspace trust',
|
||||
command: `codex --profile ${diagnostics.config.activeProfile || 'default'}`,
|
||||
description: 'Inspect the active profile directly in native Codex.',
|
||||
label: diagnostics.config.activeProfile
|
||||
? 'Inspect active profile'
|
||||
: 'Open native Codex',
|
||||
command: inspectProfileCommand,
|
||||
description: diagnostics.config.activeProfile
|
||||
? 'Inspect the active named profile directly in native Codex.'
|
||||
: 'Open native Codex without forcing a named profile overlay.',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -246,17 +305,16 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<div className="rounded-md border p-3 text-sm">
|
||||
<p className="font-medium">Native Codex runtime</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Use <code>ccs-codex</code>, <code>ccsx</code>, or <code>--target codex</code>. CCS
|
||||
launches the local Codex CLI and depends on native Codex capabilities such as{' '}
|
||||
<code>--config</code> overrides.
|
||||
Use <code>ccs-codex</code>, <code>ccsx</code>, or <code>--target codex</code> when
|
||||
you want the local Codex CLI to honor your saved native user config.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-md border p-3 text-sm">
|
||||
<p className="font-medium">Codex provider / bridge</p>
|
||||
<p className="font-medium">CCS Codex provider / bridge</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
CCS can route provider credentials transiently through CLIProxy. That is not the
|
||||
same as editing local <code>config.toml</code>, and some routed values may never
|
||||
persist here.
|
||||
Use <code>ccsxp</code> or <code>ccs codex --target codex</code> when you want the
|
||||
built-in CCS Codex provider on native Codex. That path uses transient CCS-managed
|
||||
overrides and is separate from the saved <code>cliproxy</code> recipe above.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -49,6 +49,13 @@ export interface CodexFeatureCatalogEntry {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const CLIPROXY_NATIVE_CODEX_RECIPE = `model_provider = "cliproxy"
|
||||
|
||||
[model_providers.cliproxy]
|
||||
base_url = "http://127.0.0.1:8317/api/provider/codex"
|
||||
env_key = "CLIPROXY_API_KEY"
|
||||
wire_api = "responses"`;
|
||||
|
||||
export const KNOWN_CODEX_FEATURES: CodexFeatureCatalogEntry[] = [
|
||||
{
|
||||
name: 'multi_agent',
|
||||
|
||||
+4
-4
@@ -950,7 +950,7 @@ const resources = {
|
||||
actionRequired: 'Action Required',
|
||||
done: 'Done',
|
||||
all: 'All',
|
||||
inboxTitle: 'Updates Inbox',
|
||||
inboxTitle: 'Updates Center',
|
||||
inboxSubtitle: 'Focus on actions, then mark updates done or dismissed.',
|
||||
needsAction: 'Needs Action',
|
||||
doneCount: 'Done',
|
||||
@@ -2118,7 +2118,7 @@ const resources = {
|
||||
actionRequired: '待处理',
|
||||
done: '已完成',
|
||||
all: '全部',
|
||||
inboxTitle: '更新收件箱',
|
||||
inboxTitle: '更新中心',
|
||||
inboxSubtitle: '先处理操作,再将更新标记为完成或忽略。',
|
||||
needsAction: '需处理',
|
||||
doneCount: '已完成',
|
||||
@@ -3344,7 +3344,7 @@ const resources = {
|
||||
actionRequired: 'Hành động bắt buộc',
|
||||
done: 'Xong',
|
||||
all: 'Tất cả',
|
||||
inboxTitle: 'Hộp thư cập nhật',
|
||||
inboxTitle: 'Trung tâm cập nhật',
|
||||
inboxSubtitle:
|
||||
'Tập trung vào các hành động, sau đó đánh dấu các cập nhật đã hoàn tất hoặc bị loại bỏ.',
|
||||
needsAction: 'Cần hành động',
|
||||
@@ -4580,7 +4580,7 @@ const resources = {
|
||||
actionRequired: '対応が必要',
|
||||
done: '完了',
|
||||
all: 'すべて',
|
||||
inboxTitle: '更新インボックス',
|
||||
inboxTitle: '更新センター',
|
||||
inboxSubtitle: '対応を優先し、その後で完了または非表示にします。',
|
||||
needsAction: '要対応',
|
||||
doneCount: '完了',
|
||||
|
||||
@@ -59,7 +59,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
|
||||
id: 'codex-target-runtime-support',
|
||||
title: 'Native Codex runtime support is live',
|
||||
summary:
|
||||
'Codex now participates as a first-class runtime target through ccs-codex, ccsx, or --target codex.',
|
||||
'Codex now participates as a first-class runtime target through ccs-codex, ccsx, ccsxp, or --target codex.',
|
||||
primaryAction:
|
||||
'Use Codex as a runtime target for native Codex sessions and Codex-routed CLIProxy flows.',
|
||||
publishedAt: '2026-03-28',
|
||||
@@ -68,6 +68,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
|
||||
entryIds: ['codex-target', 'codex-cliproxy'],
|
||||
highlights: [
|
||||
'Use ccs-codex or ccsx for native Codex runs.',
|
||||
'Use ccsxp for the built-in CCS Codex provider shortcut on native Codex.',
|
||||
'Built-in Codex and Codex bridge profiles can run on native Codex with --target codex.',
|
||||
'Saved default targets for API profiles and variants remain claude or droid.',
|
||||
],
|
||||
@@ -82,7 +83,14 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
|
||||
{
|
||||
id: 'copy-codex-provider-command',
|
||||
label: 'Run built-in Codex on Codex',
|
||||
description: 'Use the built-in Codex provider with native Codex runtime.',
|
||||
description: 'Use the built-in Codex provider shortcut on native Codex.',
|
||||
type: 'command',
|
||||
command: 'ccsxp "your prompt"',
|
||||
},
|
||||
{
|
||||
id: 'copy-codex-provider-command-explicit',
|
||||
label: 'Run built-in Codex on Codex (explicit)',
|
||||
description: 'Use the explicit built-in Codex provider route on native Codex.',
|
||||
type: 'command',
|
||||
command: 'ccs codex --target codex "your prompt"',
|
||||
},
|
||||
@@ -95,7 +103,12 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
|
||||
},
|
||||
],
|
||||
routes: [{ label: 'Codex CLI', path: '/codex' }],
|
||||
commands: ['ccs-codex', 'ccsx', 'ccs codex --target codex "your prompt"'],
|
||||
commands: [
|
||||
'ccs-codex',
|
||||
'ccsx',
|
||||
'ccsxp "your prompt"',
|
||||
'ccs codex --target codex "your prompt"',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'droid-target-support',
|
||||
@@ -156,9 +169,9 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
|
||||
},
|
||||
{
|
||||
id: 'updates-center-launch',
|
||||
title: 'Updates inbox is available for rollout tasks',
|
||||
title: 'Updates Center is available for rollout tasks',
|
||||
summary:
|
||||
'A focused updates inbox exists for setup tasks and rollout guidance when you need it.',
|
||||
'A focused Updates Center exists for setup tasks and rollout guidance when you need it.',
|
||||
primaryAction:
|
||||
'Use this page only when needed for rollout tasks, then return to your normal workflow.',
|
||||
publishedAt: '2026-02-25',
|
||||
@@ -173,7 +186,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
|
||||
actions: [
|
||||
{
|
||||
id: 'open-updates-page',
|
||||
label: 'Open updates inbox when needed',
|
||||
label: 'Open Updates Center when needed',
|
||||
description: 'Review rollout tasks only when you want guided setup changes.',
|
||||
type: 'route',
|
||||
path: '/updates',
|
||||
@@ -186,7 +199,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
|
||||
command: 'ccs config',
|
||||
},
|
||||
],
|
||||
routes: [{ label: 'Updates Inbox', path: '/updates' }],
|
||||
routes: [{ label: 'Updates Center', path: '/updates' }],
|
||||
commands: ['ccs config'],
|
||||
},
|
||||
];
|
||||
@@ -250,7 +263,7 @@ export const CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [
|
||||
name: 'Codex via CLIProxy',
|
||||
scope: 'cliproxy',
|
||||
status: 'stable',
|
||||
summary: 'OAuth-backed provider with configurable variant model and target.',
|
||||
summary: 'OAuth-backed provider with configurable variant model and a native Codex shortcut.',
|
||||
pillars: {
|
||||
baseUrl: 'Managed by CLIProxy backend',
|
||||
auth: 'OAuth account via CLIProxy auth flow',
|
||||
@@ -261,11 +274,14 @@ export const CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [
|
||||
{ label: 'Control Panel', path: '/cliproxy/control-panel' },
|
||||
],
|
||||
commands: [
|
||||
'ccs codex',
|
||||
'ccsxp "your prompt"',
|
||||
'ccs codex --target codex',
|
||||
'ccs codex',
|
||||
'ccs cliproxy create mycodex --provider codex',
|
||||
'ccs api create codex-api --cliproxy-provider codex',
|
||||
],
|
||||
notes:
|
||||
'Use ccsxp when you want the built-in Codex provider on native Codex without retyping --target codex.',
|
||||
},
|
||||
{
|
||||
id: 'gemini-cliproxy',
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render, screen } from '@tests/setup/test-utils';
|
||||
import { ProviderInfoTab } from '@/components/cliproxy/provider-editor/provider-info-tab';
|
||||
|
||||
const authenticatedStatus = {
|
||||
provider: 'codex',
|
||||
displayName: 'Codex',
|
||||
authenticated: true,
|
||||
lastAuth: null,
|
||||
tokenFiles: 1,
|
||||
accounts: [],
|
||||
};
|
||||
|
||||
describe('ProviderInfoTab', () => {
|
||||
it('routes management commands through Claude target for codex-target variants', () => {
|
||||
render(
|
||||
<ProviderInfoTab
|
||||
provider="codex"
|
||||
displayName="Codex"
|
||||
defaultTarget="codex"
|
||||
authStatus={authenticatedStatus}
|
||||
supportsModelConfig
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('ccs codex --target claude --config')).toBeInTheDocument();
|
||||
expect(screen.getByText('ccs codex --target claude --auth --add')).toBeInTheDocument();
|
||||
expect(screen.getByText('ccs codex --target claude --accounts')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Codex and Droid runtime launches reject those CLIProxy management commands\./
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides unsupported model-config commands when the provider has no catalog support', () => {
|
||||
render(
|
||||
<ProviderInfoTab
|
||||
provider="custom-provider"
|
||||
displayName="Custom Provider"
|
||||
defaultTarget="claude"
|
||||
authStatus={{
|
||||
...authenticatedStatus,
|
||||
provider: 'custom-provider',
|
||||
displayName: 'Custom Provider',
|
||||
}}
|
||||
supportsModelConfig={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Change model')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('ccs custom-provider --auth --add')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { render, screen } from '@tests/setup/test-utils';
|
||||
import { CodexOverviewTab } from '@/components/compatible-cli/codex-overview-tab';
|
||||
import type { CodexDashboardDiagnostics } from '@/hooks/use-codex-types';
|
||||
|
||||
function buildDiagnostics(activeProfile: string | null): CodexDashboardDiagnostics {
|
||||
return {
|
||||
binary: {
|
||||
installed: true,
|
||||
path: '/tmp/codex',
|
||||
installDir: '/tmp',
|
||||
source: 'PATH',
|
||||
version: 'codex-cli 0.118.0-alpha.3',
|
||||
overridePath: null,
|
||||
supportsConfigOverrides: true,
|
||||
},
|
||||
file: {
|
||||
label: 'Codex user config',
|
||||
path: '$CODEX_HOME/config.toml',
|
||||
resolvedPath: '/tmp/.codex/config.toml',
|
||||
exists: true,
|
||||
isSymlink: false,
|
||||
isRegularFile: true,
|
||||
sizeBytes: 64,
|
||||
mtimeMs: 100,
|
||||
parseError: null,
|
||||
readError: null,
|
||||
},
|
||||
workspacePath: '/tmp/workspace',
|
||||
config: {
|
||||
model: 'gpt-5.4',
|
||||
modelReasoningEffort: null,
|
||||
modelProvider: 'openai',
|
||||
activeProfile,
|
||||
approvalPolicy: null,
|
||||
sandboxMode: null,
|
||||
webSearch: null,
|
||||
toolOutputTokenLimit: null,
|
||||
personality: null,
|
||||
topLevelKeys: ['model_provider'],
|
||||
profileCount: activeProfile ? 1 : 0,
|
||||
profileNames: activeProfile ? [activeProfile] : [],
|
||||
modelProviderCount: 0,
|
||||
modelProviders: [],
|
||||
featureCount: 0,
|
||||
enabledFeatures: [],
|
||||
disabledFeatures: [],
|
||||
trustedProjectCount: 0,
|
||||
untrustedProjectCount: 0,
|
||||
projectTrust: [],
|
||||
mcpServerCount: 0,
|
||||
mcpServers: [],
|
||||
},
|
||||
supportMatrix: [],
|
||||
warnings: [],
|
||||
docsReference: {
|
||||
providerValues: ['openai', 'oss', 'custom model_providers'],
|
||||
settingsHierarchy: [],
|
||||
notes: [],
|
||||
links: [],
|
||||
providerDocs: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('CodexOverviewTab', () => {
|
||||
it('uses a bare codex command when no active profile is set', () => {
|
||||
render(<CodexOverviewTab diagnostics={buildDiagnostics(null)} />);
|
||||
|
||||
expect(screen.getByText('Open native Codex')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('codex').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('codex --profile default')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
(_, node) =>
|
||||
node?.textContent?.includes(
|
||||
'Built-in openai and oss providers are also valid native defaults'
|
||||
) ?? false
|
||||
).length
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('uses the active named profile when one is selected', () => {
|
||||
render(<CodexOverviewTab diagnostics={buildDiagnostics('work')} />);
|
||||
|
||||
expect(screen.getByText('Inspect active profile')).toBeInTheDocument();
|
||||
expect(screen.getByText('codex --profile work')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -22,4 +22,18 @@ describe('support-updates catalog codex routing', () => {
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry?.routes).toEqual([{ label: 'Codex CLI', path: '/codex' }]);
|
||||
});
|
||||
|
||||
it('uses Updates Center naming consistently for the rollout notice', () => {
|
||||
const notice = SUPPORT_NOTICES.find((entry) => entry.id === 'updates-center-launch');
|
||||
|
||||
expect(notice).toBeDefined();
|
||||
expect(notice?.title).toContain('Updates Center');
|
||||
expect(notice?.actions).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'open-updates-page',
|
||||
label: 'Open Updates Center when needed',
|
||||
})
|
||||
);
|
||||
expect(notice?.routes).toContainEqual({ label: 'Updates Center', path: '/updates' });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user