diff --git a/README.md b/README.md index 828919e2..905f8e36 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/docs/code-standards.md b/docs/code-standards.md index 8d6182ba..60e41441 100644 --- a/docs/code-standards.md +++ b/docs/code-standards.md @@ -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 ↓ diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index 17ae9fca..37049183 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -246,7 +246,7 @@ The targets module provides an extensible interface for dispatching profiles to 2. **Target Resolution** - Priority order: - `--target ` 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:** diff --git a/docs/system-architecture/index.md b/docs/system-architecture/index.md index 7de060eb..1fba949e 100644 --- a/docs/system-architecture/index.md +++ b/docs/system-architecture/index.md @@ -86,11 +86,12 @@ Spawn Target Process - Spawns: `droid -m custom:ccs- ` - 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/ ``` diff --git a/docs/system-architecture/target-adapters.md b/docs/system-architecture/target-adapters.md index c1944c6b..23a29a97 100644 --- a/docs/system-architecture/target-adapters.md +++ b/docs/system-architecture/target-adapters.md @@ -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 ``` --- diff --git a/package.json b/package.json index c353c70a..c0dd9ea1 100644 --- a/package.json +++ b/package.json @@ -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/", diff --git a/scripts/add-shebang.js b/scripts/add-shebang.js index 96055446..e58074e8 100755 --- a/scripts/add-shebang.js +++ b/scripts/add-shebang.js @@ -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(); \ No newline at end of file +addShebang(); diff --git a/src/bin/droid-runtime.ts b/src/bin/droid-runtime.ts new file mode 100644 index 00000000..de641d2a --- /dev/null +++ b/src/bin/droid-runtime.ts @@ -0,0 +1,2 @@ +process.env.CCS_INTERNAL_ENTRY_TARGET = 'droid'; +require('../ccs'); diff --git a/src/commands/api-command/create-command.ts b/src/commands/api-command/create-command.ts index 0bcce273..dbd798a6 100644 --- a/src/commands/api-command/create-command.ts +++ b/src/commands/api-command/create-command.ts @@ -366,12 +366,18 @@ export async function handleApiCreateCommand(args: string[]): Promise { ` ${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 { ` ${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 { ` ${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(''); diff --git a/src/commands/cliproxy/variant-subcommand.ts b/src/commands/cliproxy/variant-subcommand.ts index b3a00056..44d69d2b 100644 --- a/src/commands/cliproxy/variant-subcommand.ts +++ b/src/commands/cliproxy/variant-subcommand.ts @@ -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(''); diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index acb6adbe..0d8c0e26 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -355,22 +355,24 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); // Aliases printSubSection('Aliases', [ - ['ccsd [args]', 'Shorthand for: ccs --target droid'], + ['ccs-droid [args]', 'Explicit Droid runtime alias'], + ['ccsd [args]', 'Legacy shortcut for: ccs-droid [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', ], [ diff --git a/src/targets/target-resolver.ts b/src/targets/target-resolver.ts index b7a811f4..a7a5999d 100644 --- a/src/targets/target-resolver.ts +++ b/src/targets/target-resolver.ts @@ -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 = { +const BUILTIN_ARGV0_TARGET_MAP: Record = { + '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> = { + droid: 'CCS_DROID_ALIASES', +}; +const RESERVED_BIN_NAMES = new Set(['ccs', ...Object.keys(BUILTIN_ARGV0_TARGET_MAP)]); -function buildArgv0TargetMap(): Record { - const map: Record = { ...ARGV0_TARGET_MAP }; - const envAliases = process.env['CCS_DROID_ALIASES']; - if (!envAliases) { - return map; +function addAliasToMap(map: Record, 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, + target: TargetType, + rawAliases: string +): void { + for (const rawAlias of rawAliases.split(',')) { + addAliasToMap(map, rawAlias, target); + } +} + +function parseGenericTargetAliasConfig(map: Record, 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 { + const map: Record = { ...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'; } diff --git a/tests/npm/cross-platform.test.js b/tests/npm/cross-platform.test.js index f99c56a8..ed2c68d8 100644 --- a/tests/npm/cross-platform.test.js +++ b/tests/npm/cross-platform.test.js @@ -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'); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/targets/ccsd-alias-integration.test.ts b/tests/unit/targets/ccsd-alias-integration.test.ts index ebc4fd69..1c5c8f74 100644 --- a/tests/unit/targets/ccsd-alias-integration.test.ts +++ b/tests/unit/targets/ccsd-alias-integration.test.ts @@ -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'); }); }); diff --git a/tests/unit/targets/target-resolver.test.ts b/tests/unit/targets/target-resolver.test.ts index 9841b9ce..6e60a177 100644 --- a/tests/unit/targets/target-resolver.test.ts +++ b/tests/unit/targets/target-resolver.test.ts @@ -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', () => { diff --git a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx index d2e7f4b8..bc6c8f5f 100644 --- a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx +++ b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx @@ -90,15 +90,11 @@ export function ProviderInfoTab({ diff --git a/ui/src/components/profiles/editor/info-section.tsx b/ui/src/components/profiles/editor/info-section.tsx index 38ee6916..6c6122c0 100644 --- a/ui/src/components/profiles/editor/info-section.tsx +++ b/ui/src/components/profiles/editor/info-section.tsx @@ -90,16 +90,10 @@ export function InfoSection({ profileName, target, data }: InfoSectionProps) {
- {isDroidTarget - ? `ccsd ${profileName} "prompt"` - : `ccs ${profileName} --target droid "prompt"`} + {`ccs-droid ${profileName} "prompt"`} @@ -109,14 +103,14 @@ export function InfoSection({ profileName, target, data }: InfoSectionProps) {
- ccs {profileName} --target claude "prompt" + ccs {profileName} --target {isDroidTarget ? 'claude' : 'droid'} "prompt" diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index 09a63560..ca256fe2 100644 --- a/ui/src/components/profiles/profile-create-dialog.tsx +++ b/ui/src/components/profiles/profile-create-dialog.tsx @@ -524,11 +524,25 @@ export function ProfileCreateDialog({

- Run with{' '} - - {targetValue === 'droid' ? 'ccsd' : 'ccs'} - {' '} - by default. You can still override each run with{' '} + {targetValue === 'droid' ? ( + <> + {t('profileEditor.targetHintPreferredAlias')}{' '} + ccs-droid. + + ) : ( + <> + {t('profileEditor.targetHintClaudeDefault')}{' '} + ccs. + + )} + {targetValue === 'droid' ? ( + <> + {' '} + {t('profileEditor.targetHintLegacyAlias')}{' '} + ccsd. + + ) : null}{' '} + {t('profileEditor.targetHintOverride')}{' '} --target.

diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index de2bdd5f..099156d6 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -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: 'カスタム', diff --git a/ui/src/lib/support-updates-catalog.ts b/ui/src/lib/support-updates-catalog.ts index aa9cee80..7a7097c1 100644 --- a/ui/src/lib/support-updates-catalog.ts +++ b/ui/src/lib/support-updates-catalog.ts @@ -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',