Merge pull request #774 from kaitranntt/kai/feat/772-runtime-alias-codex-prep

feat: add explicit droid runtime alias surface
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-03-23 15:08:58 -04:00
commit 8415d896a5
20 files changed
+397 -125

No files matched your search

+20 -11
View File
@@ -194,16 +194,25 @@ ccs api export glm --out ./glm.ccs-profile.json # Export for cross-device trans
ccs api import ./glm.ccs-profile.json # Import exported profile bundle
```
### Droid Alias (`argv[0]` pattern)
### Runtime Aliases (built-in bins / `argv[0]` pattern)
By default, invoking CCS as `ccsd` auto-selects the Droid target:
Built-in Droid runtime aliases are installed with the package:
```bash
ln -s "$(command -v ccs)" /usr/local/bin/ccsd
ccsd glm
ccs-droid glm # explicit alias
ccsd glm # legacy shortcut
```
Need additional alias names? Set `CCS_DROID_ALIASES` as a comma-separated list (for example: `CCS_DROID_ALIASES=ccs-droid,mydroid`).
Need additional alias names? First create the matching symlink or another launcher that
preserves the invoked basename, then map that name with `CCS_TARGET_ALIASES` (preferred) or legacy
`CCS_DROID_ALIASES`:
```bash
ln -s "$(command -v ccs)" /usr/local/bin/mydroid
CCS_TARGET_ALIASES='droid=mydroid'
# Legacy fallback still supported:
CCS_DROID_ALIASES='mydroid'
```
For Factory BYOK compatibility, CCS also stores a per-profile Droid provider hint
(`CCS_DROID_PROVIDER`) using one of:
@@ -217,9 +226,9 @@ which Droid treats as queued prompt text.
CCS supports structural Droid command passthrough after profile selection:
```bash
ccsd codex exec --skip-permissions-unsafe "fix failing tests"
ccsd codex --skip-permissions-unsafe "fix failing tests" # auto-routed to: droid exec ...
ccsd codex -m custom:gpt-5.3-codex "fix failing tests" # short exec flags auto-routed too
ccs-droid codex exec --skip-permissions-unsafe "fix failing tests"
ccs-droid codex --skip-permissions-unsafe "fix failing tests" # auto-routed to: droid exec ...
ccs-droid codex -m custom:gpt-5.3-codex "fix failing tests" # short exec flags auto-routed too
```
If you pass exec-only flags without a prompt (for example `--skip-permissions-unsafe`),
@@ -245,10 +254,10 @@ ccs cliproxy create mycodex --provider codex --target droid
Built-in CLIProxy providers also work with Droid alias/target override:
```bash
ccsd codex
ccsd agy
ccs-droid codex
ccs-droid agy
ccs codex --target droid
ccsd codex exec --auto high "triage this bug report"
ccs-droid codex exec --auto high "triage this bug report"
```
Dashboard parity:
+2 -1
View File
@@ -200,7 +200,8 @@ Resolves which adapter to use via `resolveTargetType()`:
↓
2. Profile config: profileConfig.target field
↓
3. argv[0] detection (busybox pattern):
3. argv[0] detection (runtime alias pattern):
- ccs-droid → droid
- ccsd → droid
- ccs → default
↓
+1 -1
View File
@@ -246,7 +246,7 @@ The targets module provides an extensible interface for dispatching profiles to
2. **Target Resolution** - Priority order:
- `--target <cli>` flag (CLI argument)
- Per-profile `target` field (from config.yaml)
- `argv[0]` detection (busybox pattern: `ccsd` → droid)
- `argv[0]` detection (runtime alias pattern: `ccs-droid` / `ccsd` → droid)
- Default: `claude`
3. **Implementations:**
+5 -4
View File
@@ -86,11 +86,12 @@ Spawn Target Process
- Spawns: `droid -m custom:ccs-<profile> <args>`
- Model config includes baseUrl, apiKey, provider
**Binary alias pattern (busybox-style):**
**Runtime alias pattern (built-in bins / argv[0]-style):**
```
ccs → Target: claude (default)
ccsd → Target: droid (auto-selected via argv[0])
ccs → Target: claude (default)
ccs-droid → Target: droid (explicit alias)
ccsd → Target: droid (legacy shortcut)
```
For details on the adapter architecture, see [Target Adapters](./target-adapters.md).
@@ -392,7 +393,7 @@ See [Provider Flows](./provider-flows.md) → Authentication Flow section.
|
+---> Creates symlink: ccs --> dist/ccs.js
|
+---> Binary alias: ccsd → ccs (auto-selects droid target)
+---> Runtime aliases: ccs-droid / ccsd → ccs (auto-select droid target)
|
+---> First run creates: ~/.ccs/
```
+21 -11
View File
@@ -79,8 +79,9 @@ CCS resolves which adapter to use via priority-ordered checks:
glm:
target: droid
3. argv[0] detection (busybox pattern) — binary name mapping
└─ ccsd (symlink/batch file) → droid
3. argv[0] detection (runtime alias pattern) — binary name mapping
└─ ccs-droid (explicit alias) → droid
└─ ccsd (legacy shortcut) → droid
└─ ccs (regular command) → default
4. Fallback: 'claude' — lowest priority
@@ -351,22 +352,31 @@ ccs --target droid glm
(credentials loaded from ~/.factory/settings.json)
```
### Binary Alias Pattern
### Runtime Alias Pattern
```bash
# Create alias/symlink to auto-select droid target
# Built-in alias: ccsd
ln -s /path/to/ccs /path/to/ccsd
# Built-in package bin aliases
ccs-droid glm
→ Target: droid (forced by runtime alias)
→ droid -m custom:ccs-glm "args..."
# Usage
# Legacy shortcut still works
ccsd glm
→ Target: droid (detected from argv[0])
→ Target: droid (forced by runtime alias)
→ droid -m custom:ccs-glm "args..."
```
On Windows, `ccsd.cmd`, `ccsd.bat`, `ccsd.ps1`, and `ccsd.exe` wrappers are also recognized.
On Windows, `ccs-droid.cmd`, `ccsd.cmd`, `ccsd.bat`, `ccsd.ps1`, and `ccsd.exe` wrappers are also recognized.
Additional alias names can be configured at runtime via `CCS_DROID_ALIASES` (comma-separated). Example: `CCS_DROID_ALIASES=ccs-droid,mydroid`.
Additional alias names can be configured at runtime after you create a matching
symlink or another launcher that preserves the invoked basename. Use `CCS_TARGET_ALIASES` (preferred,
`target=alias1,alias2;...`) or legacy `CCS_DROID_ALIASES` (comma-separated).
Example:
```bash
ln -s /path/to/ccs /path/to/mydroid
CCS_TARGET_ALIASES=droid=mydroid
```
---
@@ -612,7 +622,7 @@ ccs --target claude help
ccs --target droid help
# Test argv[0] detection
ccsd help
ccs-droid help
```
---
+2 -1
View File
@@ -27,7 +27,8 @@
"types": "dist/ccs.d.ts",
"bin": {
"ccs": "dist/ccs.js",
"ccsd": "dist/ccs.js"
"ccs-droid": "dist/bin/droid-runtime.js",
"ccsd": "dist/bin/droid-runtime.js"
},
"files": [
"dist/",
+33 -20
View File
@@ -4,36 +4,49 @@
const fs = require('fs');
const path = require('path');
function getExecutablePaths() {
const packageJsonPath = path.join(__dirname, '../package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const binEntries = packageJson.bin || {};
const uniqueRelativePaths = [...new Set(Object.values(binEntries))];
return uniqueRelativePaths.map((relativePath) => path.join(__dirname, '..', relativePath));
}
/**
* Add shebang to dist/ccs.js and make executable
* Add shebangs to all published bin entrypoints and make them executable.
* Run after: tsc
*/
function addShebang() {
const ccsPath = path.join(__dirname, '../dist/ccs.js');
if (!fs.existsSync(ccsPath)) {
console.error('[X] dist/ccs.js not found. Run tsc first.');
const executablePaths = getExecutablePaths();
const missingPaths = executablePaths.filter((executablePath) => !fs.existsSync(executablePath));
if (missingPaths.length > 0) {
console.error(`[X] Missing built executable(s): ${missingPaths.join(', ')}. Run tsc first.`);
process.exit(1);
}
let content = fs.readFileSync(ccsPath, 'utf8');
for (const executablePath of executablePaths) {
let content = fs.readFileSync(executablePath, 'utf8');
// Add shebang if missing
if (!content.startsWith('#!/usr/bin/env node')) {
content = '#!/usr/bin/env node\n' + content;
fs.writeFileSync(ccsPath, content);
console.log('[OK] Shebang added to dist/ccs.js');
}
if (!content.startsWith('#!/usr/bin/env node')) {
content = '#!/usr/bin/env node\n' + content;
fs.writeFileSync(executablePath, content);
console.log(`[OK] Shebang added to ${path.relative(path.join(__dirname, '..'), executablePath)}`);
}
// Make executable (Unix-like systems)
if (process.platform !== 'win32') {
try {
fs.chmodSync(ccsPath, 0o755);
console.log('[OK] dist/ccs.js is now executable');
} catch (err) {
console.warn('[!] Could not chmod dist/ccs.js:', err.message);
if (process.platform !== 'win32') {
try {
fs.chmodSync(executablePath, 0o755);
console.log(
`[OK] ${path.relative(path.join(__dirname, '..'), executablePath)} is now executable`
);
} catch (err) {
console.warn(
`[!] Could not chmod ${path.relative(path.join(__dirname, '..'), executablePath)}: ${err.message}`
);
}
}
}
}
addShebang();
addShebang();
+2
View File
@@ -0,0 +1,2 @@
process.env.CCS_INTERNAL_ENTRY_TARGET = 'droid';
require('../ccs');
+14 -4
View File
@@ -366,12 +366,18 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
` ${color(`ccs ${result.name} "your prompt"`, 'command')} ${dim('# uses droid by default')}`
);
console.log(
` ${color(`ccsd ${result.name} "your prompt"`, 'command')} ${dim('# explicit droid alias')}`
` ${color(`ccs-droid ${result.name} "your prompt"`, 'command')} ${dim('# explicit droid alias')}`
);
console.log(
` ${color(`ccsd ${result.name} "your prompt"`, 'command')} ${dim('# legacy shortcut')}`
);
} else {
console.log(` ${color(`ccs ${result.name} "your prompt"`, 'command')}`);
console.log(
` ${color(`ccs ${result.name} --target droid "your prompt"`, 'command')} ${dim('# optional target override')}`
` ${color(`ccs-droid ${result.name} "your prompt"`, 'command')} ${dim('# explicit one-off droid alias')}`
);
console.log(
` ${color(`ccs ${result.name} --target droid "your prompt"`, 'command')} ${dim('# target flag alternative')}`
);
}
console.log('');
@@ -447,8 +453,9 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses droid by default')}`
);
console.log(
` ${color(`ccsd ${name} "your prompt"`, 'command')} ${dim('# explicit droid alias')}`
` ${color(`ccs-droid ${name} "your prompt"`, 'command')} ${dim('# explicit droid alias')}`
);
console.log(` ${color(`ccsd ${name} "your prompt"`, 'command')} ${dim('# legacy shortcut')}`);
console.log(
` ${color(`ccs ${name} --target claude "your prompt"`, 'command')} ${dim('# override to Claude')}`
);
@@ -457,7 +464,10 @@ export async function handleApiCreateCommand(args: string[]): Promise<void> {
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses claude by default')}`
);
console.log(
` ${color(`ccs ${name} --target droid "your prompt"`, 'command')} ${dim('# run on droid for this call')}`
` ${color(`ccs-droid ${name} "your prompt"`, 'command')} ${dim('# explicit one-off droid alias')}`
);
console.log(
` ${color(`ccs ${name} --target droid "your prompt"`, 'command')} ${dim('# target flag alternative')}`
);
}
console.log('');
+22 -6
View File
@@ -290,7 +290,10 @@ export async function handleCreate(
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses droid by default')}`
);
console.log(
` ${color(`ccsd ${name} "your prompt"`, 'command')} ${dim('# explicit droid alias')}`
` ${color(`ccs-droid ${name} "your prompt"`, 'command')} ${dim('# explicit droid alias')}`
);
console.log(
` ${color(`ccsd ${name} "your prompt"`, 'command')} ${dim('# legacy shortcut')}`
);
console.log(
` ${color(`ccs ${name} --target claude "your prompt"`, 'command')} ${dim('# override to Claude')}`
@@ -300,7 +303,10 @@ export async function handleCreate(
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses claude by default')}`
);
console.log(
` ${color(`ccs ${name} --target droid "your prompt"`, 'command')} ${dim('# run on droid for this call')}`
` ${color(`ccs-droid ${name} "your prompt"`, 'command')} ${dim('# explicit one-off droid alias')}`
);
console.log(
` ${color(`ccs ${name} --target droid "your prompt"`, 'command')} ${dim('# target flag alternative')}`
);
}
console.log('');
@@ -452,8 +458,9 @@ export async function handleCreate(
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses droid by default')}`
);
console.log(
` ${color(`ccsd ${name} "your prompt"`, 'command')} ${dim('# explicit droid alias')}`
` ${color(`ccs-droid ${name} "your prompt"`, 'command')} ${dim('# explicit droid alias')}`
);
console.log(` ${color(`ccsd ${name} "your prompt"`, 'command')} ${dim('# legacy shortcut')}`);
console.log(
` ${color(`ccs ${name} --target claude "your prompt"`, 'command')} ${dim('# override to Claude')}`
);
@@ -462,7 +469,10 @@ export async function handleCreate(
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses claude by default')}`
);
console.log(
` ${color(`ccs ${name} --target droid "your prompt"`, 'command')} ${dim('# run on droid for this call')}`
` ${color(`ccs-droid ${name} "your prompt"`, 'command')} ${dim('# explicit one-off droid alias')}`
);
console.log(
` ${color(`ccs ${name} --target droid "your prompt"`, 'command')} ${dim('# target flag alternative')}`
);
}
console.log('');
@@ -697,7 +707,10 @@ export async function handleEdit(
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses droid by default')}`
);
console.log(
` ${color(`ccsd ${name} "your prompt"`, 'command')} ${dim('# explicit droid alias')}`
` ${color(`ccs-droid ${name} "your prompt"`, 'command')} ${dim('# explicit droid alias')}`
);
console.log(
` ${color(`ccsd ${name} "your prompt"`, 'command')} ${dim('# legacy shortcut')}`
);
console.log(
` ${color(`ccs ${name} --target claude "your prompt"`, 'command')} ${dim('# override to Claude')}`
@@ -707,7 +720,10 @@ export async function handleEdit(
` ${color(`ccs ${name} "your prompt"`, 'command')} ${dim('# uses claude by default')}`
);
console.log(
` ${color(`ccs ${name} --target droid "your prompt"`, 'command')} ${dim('# run on droid for this call')}`
` ${color(`ccs-droid ${name} "your prompt"`, 'command')} ${dim('# explicit one-off droid alias')}`
);
console.log(
` ${color(`ccs ${name} --target droid "your prompt"`, 'command')} ${dim('# target flag alternative')}`
);
}
console.log('');
+9 -7
View File
@@ -355,22 +355,24 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
// Aliases
printSubSection('Aliases', [
['ccsd <profile> [args]', 'Shorthand for: ccs <profile> --target droid'],
['ccs-droid <profile> [args]', 'Explicit Droid runtime alias'],
['ccsd <profile> [args]', 'Legacy shortcut for: ccs-droid <profile> [args]'],
]);
// Multi-target examples
printSubSection('Multi-Target', [
['ccs glm --target droid', 'Run GLM profile on Droid CLI'],
['ccsd glm', 'Same as above (alias)'],
['ccsd codex', 'Run built-in CLIProxy Codex profile on Droid'],
['ccsd agy', 'Run built-in CLIProxy Antigravity profile on Droid'],
['ccs-droid glm', 'Same as above (explicit alias)'],
['ccsd glm', 'Legacy shortcut for ccs-droid'],
['ccs-droid codex', 'Run built-in CLIProxy Codex profile on Droid'],
['ccs-droid agy', 'Run built-in CLIProxy Antigravity profile on Droid'],
[
'ccsd codex exec --skip-permissions-unsafe "fix failing tests"',
'ccs-droid codex exec --skip-permissions-unsafe "fix failing tests"',
'Pass through Droid exec mode',
],
['ccsd codex -m custom:gpt-5.3-codex "fix failing tests"', 'Auto-routes short exec flags'],
['ccs-droid codex -m custom:gpt-5.3-codex "fix failing tests"', 'Auto-routes short exec flags'],
[
'ccsd codex --skip-permissions-unsafe "fix failing tests"',
'ccs-droid codex --skip-permissions-unsafe "fix failing tests"',
'Auto-routes to Droid exec when exec-only flags are detected',
],
[
+88 -19
View File
@@ -3,8 +3,8 @@
*
* Resolves which CLI target to use based on:
* 1. --target flag (highest priority)
* 2. Per-profile config
* 3. argv[0] detection (busybox/symlink pattern)
* 2. Runtime alias entrypoint / argv[0] detection
* 3. Per-profile config
* 4. Default: 'claude'
*/
@@ -12,32 +12,97 @@ import * as path from 'path';
import { TargetType } from './target-adapter';
/**
* Map of binary names to target types (busybox pattern).
* When CCS is invoked as `ccsd`, it auto-selects the droid target.
* Built-in argv[0] aliases for explicit runtime entrypoints.
* `ccs-droid` is the transparent alias; `ccsd` remains as a legacy shortcut.
*/
const ARGV0_TARGET_MAP: Record<string, TargetType> = {
const BUILTIN_ARGV0_TARGET_MAP: Record<string, TargetType> = {
'ccs-droid': 'droid',
ccsd: 'droid',
};
const ALIAS_NAME_REGEX = /^[a-z0-9._-]+$/;
const INTERNAL_ENTRY_TARGET_ENV_VAR = 'CCS_INTERNAL_ENTRY_TARGET';
const GENERIC_TARGET_ALIAS_ENV_VAR = 'CCS_TARGET_ALIASES';
const LEGACY_TARGET_ALIAS_ENV_VARS: Partial<Record<TargetType, string>> = {
droid: 'CCS_DROID_ALIASES',
};
const RESERVED_BIN_NAMES = new Set<string>(['ccs', ...Object.keys(BUILTIN_ARGV0_TARGET_MAP)]);
function buildArgv0TargetMap(): Record<string, TargetType> {
const map: Record<string, TargetType> = { ...ARGV0_TARGET_MAP };
const envAliases = process.env['CCS_DROID_ALIASES'];
if (!envAliases) {
return map;
function addAliasToMap(map: Record<string, TargetType>, alias: string, target: TargetType): void {
const normalizedAlias = alias.trim().toLowerCase();
if (
!normalizedAlias ||
!ALIAS_NAME_REGEX.test(normalizedAlias) ||
RESERVED_BIN_NAMES.has(normalizedAlias)
) {
return;
}
for (const rawAlias of envAliases.split(',')) {
const alias = rawAlias.trim().toLowerCase();
if (!alias || !ALIAS_NAME_REGEX.test(alias)) {
map[normalizedAlias] = target;
}
function addAliasListToMap(
map: Record<string, TargetType>,
target: TargetType,
rawAliases: string
): void {
for (const rawAlias of rawAliases.split(',')) {
addAliasToMap(map, rawAlias, target);
}
}
function parseGenericTargetAliasConfig(map: Record<string, TargetType>, rawConfig: string): void {
for (const rawEntry of rawConfig.split(';')) {
const entry = rawEntry.trim();
if (!entry) {
continue;
}
map[alias] = 'droid';
const separatorIndex = entry.indexOf('=');
if (separatorIndex <= 0 || separatorIndex === entry.length - 1) {
continue;
}
const rawTarget = entry.slice(0, separatorIndex).trim().toLowerCase();
const rawAliases = entry.slice(separatorIndex + 1).trim();
if (!rawAliases || !isValidTarget(rawTarget)) {
continue;
}
addAliasListToMap(map, rawTarget, rawAliases);
}
}
function buildArgv0TargetMap(): Record<string, TargetType> {
const map: Record<string, TargetType> = { ...BUILTIN_ARGV0_TARGET_MAP };
const genericAliasConfig = process.env[GENERIC_TARGET_ALIAS_ENV_VAR];
if (genericAliasConfig) {
parseGenericTargetAliasConfig(map, genericAliasConfig);
}
for (const [target, envVar] of Object.entries(LEGACY_TARGET_ALIAS_ENV_VARS) as Array<
[TargetType, string]
>) {
const rawAliases = process.env[envVar];
if (!rawAliases) {
continue;
}
addAliasListToMap(map, target, rawAliases);
}
return map;
}
function resolveEntrypointTarget(): TargetType | null {
const rawTarget = process.env[INTERNAL_ENTRY_TARGET_ENV_VAR];
if (!rawTarget) {
return null;
}
const normalizedTarget = rawTarget.trim().toLowerCase();
return isValidTarget(normalizedTarget) ? normalizedTarget : null;
}
/**
* Valid target types for --target flag validation.
*/
@@ -123,13 +188,12 @@ export function resolveTargetType(
return parsed.targetOverride;
}
// 2. Check per-profile config
if (profileConfig?.target !== undefined) {
return isValidTarget(profileConfig.target) ? profileConfig.target : 'claude';
// 2. Check runtime alias entrypoint / argv[0]
const entrypointTarget = resolveEntrypointTarget();
if (entrypointTarget) {
return entrypointTarget;
}
// 3. Check argv[0] (busybox pattern)
// Strip common wrapper extensions for Windows shims/wrappers
const rawBin = path.basename(process.argv[1] || process.argv0 || '');
const binName = rawBin.replace(/\.(cmd|bat|ps1|exe)$/i, '').toLowerCase();
const argv0TargetMap = buildArgv0TargetMap();
@@ -138,6 +202,11 @@ export function resolveTargetType(
return argv0Target;
}
// 3. Check per-profile config
if (profileConfig?.target !== undefined) {
return isValidTarget(profileConfig.target) ? profileConfig.target : 'claude';
}
// 4. Default
return 'claude';
}
+17 -1
View File
@@ -129,7 +129,23 @@ describe('cross-platform', () => {
assert(packageJson.bin, 'package.json should have bin field');
assert(packageJson.bin.ccs, 'bin field should specify ccs command');
assert(packageJson.bin['ccs-droid'], 'bin field should specify ccs-droid command');
assert(packageJson.bin.ccsd, 'bin field should specify ccsd command');
assert.notStrictEqual(
packageJson.bin['ccs-droid'],
packageJson.bin.ccs,
'ccs-droid should use a dedicated runtime entrypoint'
);
assert.strictEqual(
packageJson.bin['ccs-droid'],
packageJson.bin.ccsd,
'legacy ccsd alias should share the dedicated droid runtime entrypoint'
);
assert(
fs.existsSync(path.join(__dirname, '..', '..', packageJson.bin['ccs-droid'])),
'dedicated droid runtime entrypoint should exist'
);
assert(packageJson.scripts, 'package.json should have scripts field');
});
});
});
});
@@ -62,11 +62,22 @@ describe('ccsd alias integration', () => {
expect(path.basename(argvPath)).toBe('ccsd');
});
it('should preserve ccs-droid symlink basename in argv[1] under node', () => {
if (process.platform === 'win32') {
return;
}
const argvPath = probeArgvPath('ccs-droid');
expect(path.basename(argvPath)).toBe('ccs-droid');
});
it('should preserve extension-style alias basenames for wrapper compatibility', () => {
const cmdArgvPath = probeArgvPathDirect('ccsd.cmd');
const ps1ArgvPath = probeArgvPathDirect('ccsd.ps1');
const explicitCmdArgvPath = probeArgvPathDirect('ccs-droid.cmd');
expect(path.basename(cmdArgvPath)).toBe('ccsd.cmd');
expect(path.basename(ps1ArgvPath)).toBe('ccsd.ps1');
expect(path.basename(explicitCmdArgvPath)).toBe('ccs-droid.cmd');
});
});
+92 -2
View File
@@ -7,6 +7,8 @@ import { resolveTargetType, stripTargetFlag } from '../../../src/targets/target-
describe('resolveTargetType', () => {
const originalArgv = process.argv;
const originalDroidAliases = process.env.CCS_DROID_ALIASES;
const originalTargetAliases = process.env.CCS_TARGET_ALIASES;
const originalInternalEntryTarget = process.env.CCS_INTERNAL_ENTRY_TARGET;
afterEach(() => {
process.argv = originalArgv;
@@ -15,6 +17,18 @@ describe('resolveTargetType', () => {
} else {
process.env.CCS_DROID_ALIASES = originalDroidAliases;
}
if (originalTargetAliases === undefined) {
delete process.env.CCS_TARGET_ALIASES;
} else {
process.env.CCS_TARGET_ALIASES = originalTargetAliases;
}
if (originalInternalEntryTarget === undefined) {
delete process.env.CCS_INTERNAL_ENTRY_TARGET;
} else {
process.env.CCS_INTERNAL_ENTRY_TARGET = originalInternalEntryTarget;
}
});
it('should return claude as default', () => {
@@ -52,18 +66,52 @@ describe('resolveTargetType', () => {
expect(resolveTargetType([])).toBe('droid');
});
it('should detect built-in ccs-droid argv[0] alias', () => {
process.argv = ['node', 'ccs-droid'];
expect(resolveTargetType([])).toBe('droid');
});
it('should detect custom target aliases from CCS_TARGET_ALIASES', () => {
process.env.CCS_TARGET_ALIASES = 'droid=droidx,my-droid';
process.argv = ['node', 'my-droid'];
expect(resolveTargetType([])).toBe('droid');
});
it('should ignore unsupported targets in CCS_TARGET_ALIASES', () => {
process.env.CCS_TARGET_ALIASES = 'codex=ccsx;droid=ccs-droid-custom';
process.argv = ['node', 'ccsx'];
expect(resolveTargetType([])).toBe('claude');
});
it('should detect custom argv[0] aliases from CCS_DROID_ALIASES', () => {
process.env.CCS_DROID_ALIASES = 'droidx,my-droid';
process.argv = ['node', 'my-droid'];
expect(resolveTargetType([])).toBe('droid');
});
it('should merge CCS_TARGET_ALIASES and CCS_DROID_ALIASES', () => {
process.env.CCS_TARGET_ALIASES = 'droid=team-droid';
process.env.CCS_DROID_ALIASES = 'legacy-droid';
process.argv = ['node', 'team-droid'];
expect(resolveTargetType([])).toBe('droid');
process.argv = ['node', 'legacy-droid'];
expect(resolveTargetType([])).toBe('droid');
});
it('should ignore invalid custom alias entries', () => {
process.env.CCS_DROID_ALIASES = 'valid_alias,../bad,';
process.argv = ['node', '../bad'];
expect(resolveTargetType([])).toBe('claude');
});
it('should detect internal entry target for dedicated package bin entrypoints', () => {
process.env.CCS_INTERNAL_ENTRY_TARGET = 'droid';
process.argv = ['node', 'ccs'];
expect(resolveTargetType([])).toBe('droid');
});
it('should normalize argv[0] and custom aliases case-insensitively', () => {
process.env.CCS_DROID_ALIASES = 'DroidCaps';
process.argv = ['node', 'DROIDCAPS'];
@@ -75,6 +123,11 @@ describe('resolveTargetType', () => {
expect(resolveTargetType([])).toBe('droid');
});
it('should strip .cmd extension on built-in explicit alias', () => {
process.argv = ['node', 'ccs-droid.cmd'];
expect(resolveTargetType([])).toBe('droid');
});
it('should strip .bat extension on Windows argv[0]', () => {
process.argv = ['node', 'ccsd.bat'];
expect(resolveTargetType([])).toBe('droid');
@@ -95,14 +148,51 @@ describe('resolveTargetType', () => {
expect(resolveTargetType([])).toBe('droid');
});
it('should handle full path argv[0] for ccs-droid', () => {
process.argv = ['node', '/usr/local/bin/ccs-droid'];
expect(resolveTargetType([])).toBe('droid');
});
it('should prioritize --target over argv[0]', () => {
process.argv = ['node', 'ccsd'];
expect(resolveTargetType(['--target', 'claude'])).toBe('claude');
});
it('should prioritize profile config over argv[0]', () => {
it('should prioritize --target over internal entry target', () => {
process.env.CCS_INTERNAL_ENTRY_TARGET = 'droid';
process.argv = ['node', 'ccs'];
expect(resolveTargetType(['--target', 'claude'])).toBe('claude');
});
it('should prioritize runtime alias over profile config', () => {
process.argv = ['node', 'ccsd'];
expect(resolveTargetType([], { target: 'claude' })).toBe('claude');
expect(resolveTargetType([], { target: 'claude' })).toBe('droid');
});
it('should prioritize internal entry target over profile config', () => {
process.env.CCS_INTERNAL_ENTRY_TARGET = 'droid';
process.argv = ['node', 'ccs'];
expect(resolveTargetType([], { target: 'claude' })).toBe('droid');
});
it('should keep reserved command names authoritative', () => {
process.env.CCS_TARGET_ALIASES = 'claude=ccs,ccs-droid,ccsd;droid=mydroid';
process.env.CCS_DROID_ALIASES = 'ccs,ccs-droid,ccsd,legacy-droid';
process.argv = ['node', 'ccs'];
expect(resolveTargetType([])).toBe('claude');
process.argv = ['node', 'ccs-droid'];
expect(resolveTargetType([])).toBe('droid');
process.argv = ['node', 'ccsd'];
expect(resolveTargetType([])).toBe('droid');
process.argv = ['node', 'mydroid'];
expect(resolveTargetType([])).toBe('droid');
process.argv = ['node', 'legacy-droid'];
expect(resolveTargetType([])).toBe('droid');
});
it('should throw for invalid --target value', () => {
@@ -90,15 +90,11 @@ export function ProviderInfoTab({
<UsageCommand label="Run with prompt" command={`ccs ${provider} "your prompt"`} />
<UsageCommand
label={isDroidTarget ? 'Droid alias (explicit)' : 'Run on Droid'}
command={
isDroidTarget
? `ccsd ${provider} "your prompt"`
: `ccs ${provider} --target droid "your prompt"`
}
command={`ccs-droid ${provider} "your prompt"`}
/>
<UsageCommand
label={isDroidTarget ? 'Override to Claude' : 'Override target'}
command={`ccs ${provider} --target claude "your prompt"`}
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`} />
@@ -90,16 +90,10 @@ export function InfoSection({ profileName, target, data }: InfoSectionProps) {
</Label>
<div className="mt-1 flex gap-2">
<code className="flex-1 px-2 py-1.5 bg-muted rounded text-xs font-mono truncate">
{isDroidTarget
? `ccsd ${profileName} "prompt"`
: `ccs ${profileName} --target droid "prompt"`}
{`ccs-droid ${profileName} "prompt"`}
</code>
<CopyButton
value={
isDroidTarget
? `ccsd ${profileName} "prompt"`
: `ccs ${profileName} --target droid "prompt"`
}
value={`ccs-droid ${profileName} "prompt"`}
size="icon"
className="h-6 w-6"
/>
@@ -109,14 +103,14 @@ export function InfoSection({ profileName, target, data }: InfoSectionProps) {
<Label className="text-xs text-muted-foreground">
{isDroidTarget
? t('profileEditor.overrideToClaude')
: t('profileEditor.overrideToClaudeExplicit')}
: t('profileEditor.runOnDroidWithFlag')}
</Label>
<div className="mt-1 flex gap-2">
<code className="flex-1 px-2 py-1.5 bg-muted rounded text-xs font-mono truncate">
ccs {profileName} --target claude "prompt"
ccs {profileName} --target {isDroidTarget ? 'claude' : 'droid'} "prompt"
</code>
<CopyButton
value={`ccs ${profileName} --target claude "prompt"`}
value={`ccs ${profileName} --target ${isDroidTarget ? 'claude' : 'droid'} "prompt"`}
size="icon"
className="h-6 w-6"
/>
@@ -524,11 +524,25 @@ export function ProfileCreateDialog({
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Run with{' '}
<code className="bg-muted px-1 rounded text-[10px]">
{targetValue === 'droid' ? 'ccsd' : 'ccs'}
</code>{' '}
by default. You can still override each run with{' '}
{targetValue === 'droid' ? (
<>
{t('profileEditor.targetHintPreferredAlias')}{' '}
<code className="bg-muted px-1 rounded text-[10px]">ccs-droid</code>.
</>
) : (
<>
{t('profileEditor.targetHintClaudeDefault')}{' '}
<code className="bg-muted px-1 rounded text-[10px]">ccs</code>.
</>
)}
{targetValue === 'droid' ? (
<>
{' '}
{t('profileEditor.targetHintLegacyAlias')}{' '}
<code className="bg-muted px-1 rounded text-[10px]">ccsd</code>.
</>
) : null}{' '}
{t('profileEditor.targetHintOverride')}{' '}
<code className="bg-muted px-1 rounded text-[10px]">--target</code>.
</p>
</div>
+23 -6
View File
@@ -619,9 +619,14 @@ const resources = {
quickUsage: 'Quick Usage',
runWithProfile: 'Run with profile',
runOnDroid: 'Run on Droid',
runOnDroidWithFlag: 'Run on Droid (--target)',
droidAliasExplicit: 'Droid alias (explicit)',
overrideToClaude: 'Override to Claude',
overrideToClaudeExplicit: 'Override to Claude (explicit)',
targetHintPreferredAlias: 'Preferred explicit alias:',
targetHintClaudeDefault: 'Default command:',
targetHintLegacyAlias: 'Legacy shortcut still works:',
targetHintOverride: 'You can still override each run with',
setAsDefault: 'Set as default',
provider: 'Provider',
custom: 'Custom',
@@ -1451,10 +1456,8 @@ const resources = {
openrouterModelsBadge: '{{modelCountLabel}} 个 OpenRouter 模型',
runtimeProviderBadge: '运行时提供商设置',
runtimeProviderTitle: '打开 AI Providers',
runtimeProviderDescription:
'先管理共享的提供商密钥和运行时连接器,再创建独立的 API 配置。',
runtimeProviderFeatureConnectors:
'Gemini、Codex、Claude、Vertex 以及 OpenAI 兼容连接器',
runtimeProviderDescription: '先管理共享的提供商密钥和运行时连接器,再创建独立的 API 配置。',
runtimeProviderFeatureConnectors: 'Gemini、Codex、Claude、Vertex 以及 OpenAI 兼容连接器',
runtimeProviderFeatureSecrets: '共享密钥与单个 API 配置分离存放',
runtimeProviderFooter: '当多个配置需要共享同一提供商设置时最适合使用。',
createOpenRouterProfile: '创建 OpenRouter 配置',
@@ -1792,9 +1795,14 @@ const resources = {
quickUsage: '快速使用',
runWithProfile: '使用配置运行',
runOnDroid: '在 Droid 上运行',
runOnDroidWithFlag: '通过 --target 在 Droid 上运行',
droidAliasExplicit: 'Droid 别名(显式)',
overrideToClaude: '切换为 Claude',
overrideToClaudeExplicit: '切换为 Claude(显式)',
targetHintPreferredAlias: '推荐显式别名:',
targetHintClaudeDefault: '默认命令:',
targetHintLegacyAlias: '旧快捷方式仍可用:',
targetHintOverride: '你仍可在每次运行时用',
setAsDefault: '设为默认',
provider: '提供商',
custom: '自定义',
@@ -2988,9 +2996,14 @@ const resources = {
quickUsage: 'Sử dụng nhanh',
runWithProfile: 'Chạy với hồ sơ',
runOnDroid: 'Chạy trên Droid',
runOnDroidWithFlag: 'Chạy trên Droid (--target)',
droidAliasExplicit: 'Bí danh Droid (rõ ràng)',
overrideToClaude: 'Ghi đè lên Claude',
overrideToClaudeExplicit: 'Ghi đè lên Claude (rõ ràng)',
targetHintPreferredAlias: 'Bí danh rõ ràng nên dùng:',
targetHintClaudeDefault: 'Lệnh mặc định:',
targetHintLegacyAlias: 'Lối tắt cũ vẫn dùng được:',
targetHintOverride: 'Bạn vẫn có thể ghi đè mỗi lần chạy bằng',
setAsDefault: 'Đặt làm mặc định',
provider: 'Nhà cung cấp',
custom: 'Tùy chỉnh',
@@ -3837,8 +3850,7 @@ const resources = {
runtimeProviderTitle: 'AI Providers を開く',
runtimeProviderDescription:
'個別の API プロファイルを作成する前に、共有プロバイダーキーとランタイムコネクタを管理します。',
runtimeProviderFeatureConnectors:
'Gemini、Codex、Claude、Vertex、OpenAI 互換コネクタ',
runtimeProviderFeatureConnectors: 'Gemini、Codex、Claude、Vertex、OpenAI 互換コネクタ',
runtimeProviderFeatureSecrets:
'共有シークレットを個別の API プロファイルから切り離して管理',
runtimeProviderFooter:
@@ -4209,9 +4221,14 @@ const resources = {
quickUsage: 'クイック実行',
runWithProfile: 'このプロファイルで実行',
runOnDroid: 'Droid で実行',
runOnDroidWithFlag: '--target で Droid 実行',
droidAliasExplicit: 'Droid エイリアス(明示)',
overrideToClaude: 'Claude に切り替え',
overrideToClaudeExplicit: 'Claude に切り替え(明示)',
targetHintPreferredAlias: '推奨の明示エイリアス:',
targetHintClaudeDefault: 'デフォルトコマンド:',
targetHintLegacyAlias: '従来ショートカットも利用可能:',
targetHintOverride: '実行ごとに次でも上書きできます',
setAsDefault: 'デフォルトに設定',
provider: 'プロバイダー',
custom: 'カスタム',
+8 -8
View File
@@ -68,7 +68,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
highlights: [
'Set default target to Droid when creating or editing API Profiles.',
'Set default target to Droid for CLIProxy variants, including Codex and Antigravity flows.',
'Use ccsd alias or --target droid for one-off target overrides.',
'Use ccs-droid as the explicit alias, with ccsd kept as the legacy shortcut.',
],
actions: [
{
@@ -88,11 +88,11 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
path: '/cliproxy',
},
{
id: 'copy-ccsd-command',
label: 'Run once with Droid alias',
description: 'Use ccsd to force Droid target with your current profile.',
id: 'copy-ccs-droid-command',
label: 'Run once with explicit Droid alias',
description: 'Use ccs-droid to force the Droid target with your current profile.',
type: 'command',
command: 'ccsd glm',
command: 'ccs-droid glm',
},
{
id: 'copy-target-override',
@@ -107,7 +107,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
{ label: 'CLIProxy', path: '/cliproxy' },
],
commands: [
'ccsd glm',
'ccs-droid glm',
'ccs codex --target droid "your prompt"',
'ccs cliproxy create mycodex --provider codex --target droid',
],
@@ -182,8 +182,8 @@ export const CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [
{ label: 'API Profiles', path: '/providers' },
{ label: 'CLIProxy', path: '/cliproxy' },
],
commands: ['ccsd glm', 'ccs km --target droid', 'ccs codex --target droid'],
notes: 'Use ccsd alias for automatic Droid target selection.',
commands: ['ccs-droid glm', 'ccs km --target droid', 'ccs codex --target droid'],
notes: 'Use ccs-droid as the explicit runtime alias. Legacy ccsd still works.',
},
{
id: 'codex-cliproxy',