mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 20:17:03 +00:00
feat(profiles): expose codex runtime across surfaces
- update CLI, API, route, and dashboard surfaces to recognize the Codex runtime target - normalize persisted codex targets back to claude so runtime-only behavior stays truthful - add regression coverage for help text, route parsing, and profile storage normalization Refs #773
This commit is contained in:
1 parent
8f60820f33
commit
f9c1238483
19 files changed
+332
-26
No files matched your search
@@ -8,6 +8,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { Config, Settings } from '../../types';
|
||||
import type { TargetType } from '../../targets/target-adapter';
|
||||
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
|
||||
import { getCcsDir, getConfigPath, loadConfigSafe } from '../../utils/config-manager';
|
||||
import { ensureProfileHooksOrThrow } from '../../utils/websearch/profile-hook-injector';
|
||||
import { isSensitiveKey } from '../../utils/sensitive-keys';
|
||||
@@ -29,7 +30,7 @@ const SETTINGS_FILE_SUFFIX = '.settings.json';
|
||||
const REDACTED_TOKEN_SENTINEL = '__CCS_REDACTED__';
|
||||
|
||||
function parseTargetValue(value: unknown): TargetType | null {
|
||||
if (value === 'claude' || value === 'droid') {
|
||||
if (isPersistedTargetType(value)) {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
@@ -359,7 +360,7 @@ export function importApiProfileBundle(
|
||||
if (input.profile.target !== undefined && bundleTarget === null) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid bundle profile target. Expected: claude or droid.',
|
||||
error: `Invalid bundle profile target. Expected: ${getPersistedTargetChoices()}.`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,13 @@ import { loadConfigSafe } from '../../utils/config-manager';
|
||||
import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader';
|
||||
import { expandPath } from '../../utils/helpers';
|
||||
import type { TargetType } from '../../targets/target-adapter';
|
||||
import { isPersistedTargetType } from '../../targets/target-metadata';
|
||||
import type { Settings } from '../../types/config';
|
||||
import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types';
|
||||
import { resolveCliproxyBridgeMetadata } from './cliproxy-profile-bridge';
|
||||
|
||||
const VALID_TARGETS: ReadonlySet<TargetType> = new Set<TargetType>(['claude', 'droid']);
|
||||
|
||||
function sanitizeTarget(target: unknown): TargetType {
|
||||
if (typeof target === 'string' && VALID_TARGETS.has(target as TargetType)) {
|
||||
if (isPersistedTargetType(target)) {
|
||||
return target as TargetType;
|
||||
}
|
||||
return 'claude';
|
||||
|
||||
@@ -292,7 +292,7 @@ export function getOfficialChannelsStateScopeMessage(): string {
|
||||
}
|
||||
|
||||
export function getOfficialChannelsSupportMessage(): string {
|
||||
return 'Works only for native Claude default/account sessions. It does not apply to API, OAuth, or Droid targets such as `ccs glm`, `ccs gemini`, `ccs codex`, or `ccs --target droid`.';
|
||||
return 'Works only for native Claude default/account sessions. It does not apply to API, OAuth, or non-Claude targets such as `ccs glm`, `ccs gemini`, `ccs codex`, `ccs --target droid`, or `ccs --target codex`.';
|
||||
}
|
||||
|
||||
export function getOfficialChannelsAccountStatusCaveat(): string {
|
||||
|
||||
@@ -417,6 +417,17 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
|
||||
` ${color(`ccs ${result.name} --target droid "your prompt"`, 'command')} ${dim('# target flag alternative')}`
|
||||
);
|
||||
}
|
||||
if (cliproxyProvider === 'codex') {
|
||||
console.log(
|
||||
` ${color(`ccs ${result.name} --target codex "your prompt"`, 'command')} ${dim('# native Codex runtime')}`
|
||||
);
|
||||
console.log(
|
||||
` ${color(`ccs-codex ${result.name} "your prompt"`, 'command')} ${dim('# explicit Codex alias')}`
|
||||
);
|
||||
console.log(
|
||||
` ${color(`ccsx ${result.name} "your prompt"`, 'command')} ${dim('# short alias')}`
|
||||
);
|
||||
}
|
||||
console.log('');
|
||||
console.log(dim('Manage provider accounts, keys, and models in: ccs cliproxy'));
|
||||
return;
|
||||
|
||||
@@ -107,6 +107,10 @@ export async function showApiCommandHelp(writeLine: HelpWriter = console.log): P
|
||||
writeLine(
|
||||
` ${color('ccs api create gemini-droid --cliproxy-provider gemini --target droid', 'command')}`
|
||||
);
|
||||
writeLine(` ${color('ccs api create codex-api --cliproxy-provider codex', 'command')}`);
|
||||
writeLine(
|
||||
` ${color('ccs codex-api --target codex', 'command')} ${dim('# runtime-only native Codex launch')}`
|
||||
);
|
||||
writeLine('');
|
||||
writeLine(` ${dim('# Create with name')}`);
|
||||
writeLine(` ${color('ccs api create myapi', 'command')}`);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ModelMapping } from '../../api/services';
|
||||
import type { TargetType } from '../../targets/target-adapter';
|
||||
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
|
||||
import {
|
||||
applyExtendedContextSuffix,
|
||||
hasExtendedContextSuffix,
|
||||
@@ -108,7 +109,7 @@ export function extractPositionalArgs(args: string[]): string[] {
|
||||
|
||||
function parseTargetValue(value: string): TargetType | null {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === 'claude' || normalized === 'droid') {
|
||||
if (isPersistedTargetType(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return null;
|
||||
@@ -132,7 +133,7 @@ export function parseOptionalTargetFlag(
|
||||
if (!target) {
|
||||
return {
|
||||
remainingArgs: extracted.remainingArgs,
|
||||
errors: [`Invalid --target value "${extracted.value}". Use: claude or droid`],
|
||||
errors: [`Invalid --target value "${extracted.value}". Use: ${getPersistedTargetChoices()}`],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -251,7 +252,9 @@ export function parseApiCommandArgs(
|
||||
(value) => {
|
||||
const target = parseTargetValue(value);
|
||||
if (!target) {
|
||||
result.errors.push(`Invalid --target value "${value}". Use: claude or droid`);
|
||||
result.errors.push(
|
||||
`Invalid --target value "${value}". Use: ${getPersistedTargetChoices()}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
result.target = target;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { CLIProxyProfileName, CLIPROXY_PROFILES } from '../../auth/profile-detec
|
||||
import { supportsModelConfig, getProviderCatalog, ModelEntry } from '../../cliproxy/model-catalog';
|
||||
import { CLIProxyProvider, CLIProxyBackend } from '../../cliproxy/types';
|
||||
import type { TargetType } from '../../targets/target-adapter';
|
||||
import { getPersistedTargetChoices, isPersistedTargetType } from '../../targets/target-metadata';
|
||||
import { isUnifiedMode } from '../../config/unified-config-loader';
|
||||
import { initUI, header, color, ok, fail, warn, info, infoBox, dim } from '../../utils/ui';
|
||||
import { InteractivePrompt } from '../../utils/prompt';
|
||||
@@ -42,7 +43,7 @@ interface CliproxyProfileArgs {
|
||||
|
||||
function parseTargetValue(rawValue: string): TargetType | null {
|
||||
const normalized = rawValue.trim().toLowerCase();
|
||||
if (normalized === 'claude' || normalized === 'droid') {
|
||||
if (isPersistedTargetType(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return null;
|
||||
@@ -73,7 +74,9 @@ export function parseProfileArgs(args: string[]): CliproxyProfileArgs {
|
||||
i += 1;
|
||||
const parsedTarget = parseTargetValue(rawValue);
|
||||
if (!parsedTarget) {
|
||||
result.errors.push(`Invalid --target value "${rawValue}". Use: claude or droid`);
|
||||
result.errors.push(
|
||||
`Invalid --target value "${rawValue}". Use: ${getPersistedTargetChoices()}`
|
||||
);
|
||||
} else {
|
||||
result.target = parsedTarget;
|
||||
}
|
||||
@@ -82,7 +85,9 @@ export function parseProfileArgs(args: string[]): CliproxyProfileArgs {
|
||||
const rawValue = arg.slice('--target='.length);
|
||||
const parsedTarget = parseTargetValue(rawValue);
|
||||
if (!parsedTarget) {
|
||||
result.errors.push(`Invalid --target value "${rawValue}". Use: claude or droid`);
|
||||
result.errors.push(
|
||||
`Invalid --target value "${rawValue}". Use: ${getPersistedTargetChoices()}`
|
||||
);
|
||||
} else {
|
||||
result.target = parsedTarget;
|
||||
}
|
||||
|
||||
@@ -391,7 +391,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
'Flags',
|
||||
[
|
||||
['--config-dir <path>', 'Use custom CCS config directory'],
|
||||
['--target <cli>', 'Target CLI: claude (default), droid'],
|
||||
['--target <cli>', 'Target CLI: claude (default), droid, codex (runtime-only)'],
|
||||
['-h, --help', 'Show this help message'],
|
||||
['-v, --version', 'Show version and installation info'],
|
||||
['-sc, --shell-completion', 'Install shell auto-completion'],
|
||||
@@ -405,6 +405,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
[
|
||||
['ccs-droid <profile> [args]', 'Explicit Droid runtime alias'],
|
||||
['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]'],
|
||||
],
|
||||
writeLine
|
||||
);
|
||||
@@ -416,6 +418,15 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
['ccs glm --target droid', 'Run GLM profile on Droid CLI'],
|
||||
['ccs-droid glm', 'Same as above (explicit alias)'],
|
||||
['ccsd glm', 'Legacy shortcut for ccs-droid'],
|
||||
['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'],
|
||||
[
|
||||
'ccs api create codex-api --cliproxy-provider codex',
|
||||
'Create a routed API bridge that can also run on Codex',
|
||||
],
|
||||
['ccs codex-api --target codex', 'Run a Codex bridge profile on native Codex CLI'],
|
||||
['ccs-droid codex', 'Run built-in CLIProxy Codex profile on Droid'],
|
||||
['ccs-droid agy', 'Run built-in CLIProxy Antigravity profile on Droid'],
|
||||
[
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
validateApiName,
|
||||
} from '../../api/services';
|
||||
import { normalizeDroidProvider } from '../../targets/droid-provider';
|
||||
import { getPersistedTargetChoices } from '../../targets/target-metadata';
|
||||
import { isCLIProxyProvider } from '../../cliproxy/provider-capabilities';
|
||||
import { isAnthropicDirectProfile, updateSettingsFile, parseTarget } from './route-helpers';
|
||||
|
||||
@@ -100,7 +101,7 @@ router.post('/cliproxy-bridge', (req: Request, res: Response): void => {
|
||||
|
||||
const target = parseTarget(shape.payload.target);
|
||||
if (shape.payload.target !== undefined && target === null) {
|
||||
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
|
||||
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,7 +156,7 @@ router.post('/', (req: Request, res: Response): void => {
|
||||
|
||||
const parsedTarget = parseTarget(target);
|
||||
if (target !== undefined && parsedTarget === null) {
|
||||
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
|
||||
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
|
||||
return;
|
||||
}
|
||||
if (providerHint !== undefined && parsedProvider === null) {
|
||||
@@ -265,7 +266,7 @@ router.post('/orphans/register', (req: Request, res: Response): void => {
|
||||
const force = payload.force === true;
|
||||
|
||||
if (payload.target !== undefined && target === null) {
|
||||
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
|
||||
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -306,7 +307,7 @@ router.post('/:name/copy', (req: Request, res: Response): void => {
|
||||
return;
|
||||
}
|
||||
if (shape.payload.target !== undefined && target === null) {
|
||||
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
|
||||
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -357,7 +358,7 @@ router.post('/import', (req: Request, res: Response): void => {
|
||||
|
||||
const target = parseTarget(shape.payload.target);
|
||||
if (shape.payload.target !== undefined && target === null) {
|
||||
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
|
||||
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -368,7 +369,9 @@ router.post('/import', (req: Request, res: Response): void => {
|
||||
}
|
||||
const bundleTarget = (bundle as { profile?: { target?: unknown } }).profile?.target;
|
||||
if (bundleTarget !== undefined && parseTarget(bundleTarget) === null) {
|
||||
res.status(400).json({ error: 'Invalid bundle profile target. Expected: claude or droid' });
|
||||
res.status(400).json({
|
||||
error: `Invalid bundle profile target. Expected: ${getPersistedTargetChoices()}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -418,7 +421,7 @@ router.put('/:name', (req: Request, res: Response): void => {
|
||||
|
||||
const parsedTarget = parseTarget(target);
|
||||
if (target !== undefined && parsedTarget === null) {
|
||||
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
|
||||
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
|
||||
return;
|
||||
}
|
||||
if (providerHint !== undefined && parsedProvider === null) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import type { CLIProxyProvider } from '../../cliproxy/types';
|
||||
import type { Config, Settings } from '../../types/config';
|
||||
import type { TargetType } from '../../targets/target-adapter';
|
||||
import { isPersistedTargetType } from '../../targets/target-metadata';
|
||||
import { ValidationError } from '../../errors/error-types';
|
||||
|
||||
/** Model mapping for API profiles */
|
||||
@@ -438,7 +439,7 @@ export function validateFilePath(filePath: string): {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate a target param (claude/droid). Returns null if invalid/absent.
|
||||
* Parse and validate a persisted target param. Returns null if invalid/absent.
|
||||
* Shared by profile-routes and variant-routes.
|
||||
*/
|
||||
export function parseTarget(rawTarget: unknown): TargetType | null {
|
||||
@@ -451,7 +452,7 @@ export function parseTarget(rawTarget: unknown): TargetType | null {
|
||||
}
|
||||
|
||||
const normalized = rawTarget.trim().toLowerCase();
|
||||
if (normalized === 'claude' || normalized === 'droid') {
|
||||
if (isPersistedTargetType(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { isReservedName, RESERVED_PROFILE_NAMES } from '../../config/reserved-names';
|
||||
import type { CLIProxyProvider } from '../../cliproxy/types';
|
||||
import { getPersistedTargetChoices } from '../../targets/target-metadata';
|
||||
import { parseTarget } from './route-helpers';
|
||||
import {
|
||||
createVariant,
|
||||
@@ -55,7 +56,7 @@ router.post('/', (req: Request, res: Response): void => {
|
||||
const parsedTarget = parseTarget(req.body.target);
|
||||
|
||||
if (req.body.target !== undefined && parsedTarget === null) {
|
||||
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
|
||||
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,7 +179,7 @@ router.put('/:name', (req: Request, res: Response): void => {
|
||||
const parsedTarget = parseTarget(req.body.target);
|
||||
|
||||
if (req.body.target !== undefined && parsedTarget === null) {
|
||||
res.status(400).json({ error: 'Invalid target. Expected: claude or droid' });
|
||||
res.status(400).json({ error: `Invalid target. Expected: ${getPersistedTargetChoices()}` });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user