From 160be319d0b1982c05dc2984673a81aa6ec245b5 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Mon, 23 Mar 2026 13:44:32 -0400 Subject: [PATCH 1/2] feat: add explicit droid runtime alias surface --- README.md | 29 +++++--- docs/code-standards.md | 3 +- docs/codebase-summary.md | 2 +- docs/system-architecture/index.md | 9 ++- docs/system-architecture/target-adapters.md | 25 +++++-- package.json | 1 + src/commands/api-command/create-command.ts | 18 ++++- src/commands/cliproxy/variant-subcommand.ts | 28 +++++-- src/commands/help-command.ts | 16 ++-- src/targets/target-resolver.ts | 73 ++++++++++++++++--- .../targets/ccsd-alias-integration.test.ts | 11 +++ tests/unit/targets/target-resolver.test.ts | 34 +++++++++ .../provider-editor/provider-info-tab.tsx | 6 +- .../profiles/editor/info-section.tsx | 10 +-- .../profiles/profile-create-dialog.tsx | 24 ++++-- ui/src/lib/i18n.ts | 9 +-- ui/src/lib/support-updates-catalog.ts | 16 ++-- 17 files changed, 229 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 828919e2..62012ab1 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 (`argv[0]` pattern) -By default, invoking CCS as `ccsd` auto-selects the Droid target: +Built-in Droid runtime aliases: ```bash +ln -s "$(command -v ccs)" /usr/local/bin/ccs-droid 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? Use `CCS_TARGET_ALIASES` (preferred) or keep using +legacy `CCS_DROID_ALIASES`: + +```bash +CCS_TARGET_ALIASES='droid=mydroid,team-droid' +# Legacy fallback still supported: +CCS_DROID_ALIASES='mydroid,team-droid' +``` 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..6c4e1647 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 (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..5d81005e 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,30 @@ 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 +# Create aliases/symlinks to auto-select droid target +ln -s /path/to/ccs /path/to/ccs-droid ln -s /path/to/ccs /path/to/ccsd # Usage +ccs-droid glm +→ Target: droid (detected from argv[0]) +→ droid -m custom:ccs-glm "args..." + +# Legacy shortcut still works ccsd glm → Target: droid (detected from argv[0]) → 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 via `CCS_TARGET_ALIASES` +(preferred, `target=alias1,alias2;...`) or legacy `CCS_DROID_ALIASES` +(comma-separated). Example: +`CCS_TARGET_ALIASES=droid=mydroid,team-droid` --- @@ -612,7 +621,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..ef687955 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "types": "dist/ccs.d.ts", "bin": { "ccs": "dist/ccs.js", + "ccs-droid": "dist/ccs.js", "ccsd": "dist/ccs.js" }, "files": [ 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..eced5f8a 100644 --- a/src/targets/target-resolver.ts +++ b/src/targets/target-resolver.ts @@ -12,27 +12,76 @@ 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 GENERIC_TARGET_ALIAS_ENV_VAR = 'CCS_TARGET_ALIASES'; +const LEGACY_TARGET_ALIAS_ENV_VARS: Partial> = { + droid: 'CCS_DROID_ALIASES', +}; -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)) { + 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; 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..7628ed63 100644 --- a/tests/unit/targets/target-resolver.test.ts +++ b/tests/unit/targets/target-resolver.test.ts @@ -7,6 +7,7 @@ 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; afterEach(() => { process.argv = originalArgv; @@ -15,6 +16,12 @@ 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; + } }); it('should return claude as default', () => { @@ -52,6 +59,23 @@ 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']; @@ -75,6 +99,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,6 +124,11 @@ 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'); 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..8cb6835d 100644 --- a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx +++ b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx @@ -90,11 +90,7 @@ export function ProviderInfoTab({
- {isDroidTarget - ? `ccsd ${profileName} "prompt"` - : `ccs ${profileName} --target droid "prompt"`} + {`ccs-droid ${profileName} "prompt"`} diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index 09a63560..fe32c183 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' ? ( + <> + Preferred explicit alias:{' '} + ccs-droid. + + ) : ( + <> + Run with ccs by + default. + + )} + {targetValue === 'droid' ? ( + <> + {' '} + Legacy ccsd still + works for Droid. + + ) : null}{' '} + You can still override each run with{' '} --target.

diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index de2bdd5f..237be3e4 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -1451,10 +1451,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 配置', @@ -3837,8 +3835,7 @@ const resources = { runtimeProviderTitle: 'AI Providers を開く', runtimeProviderDescription: '個別の API プロファイルを作成する前に、共有プロバイダーキーとランタイムコネクタを管理します。', - runtimeProviderFeatureConnectors: - 'Gemini、Codex、Claude、Vertex、OpenAI 互換コネクタ', + runtimeProviderFeatureConnectors: 'Gemini、Codex、Claude、Vertex、OpenAI 互換コネクタ', runtimeProviderFeatureSecrets: '共有シークレットを個別の API プロファイルから切り離して管理', runtimeProviderFooter: 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', From 1f667b5953297189dfe8e88112088b11641ba676 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Mon, 23 Mar 2026 14:15:30 -0400 Subject: [PATCH 2/2] fix: harden droid runtime alias resolution --- README.md | 16 ++--- docs/system-architecture/index.md | 2 +- docs/system-architecture/target-adapters.md | 23 +++---- package.json | 4 +- scripts/add-shebang.js | 53 +++++++++------- src/bin/droid-runtime.ts | 2 + src/targets/target-resolver.ts | 36 ++++++++--- tests/npm/cross-platform.test.js | 18 +++++- tests/unit/targets/target-resolver.test.ts | 60 ++++++++++++++++++- .../provider-editor/provider-info-tab.tsx | 4 +- .../profiles/editor/info-section.tsx | 6 +- .../profiles/profile-create-dialog.tsx | 12 ++-- ui/src/lib/i18n.ts | 20 +++++++ 13 files changed, 192 insertions(+), 64 deletions(-) create mode 100644 src/bin/droid-runtime.ts diff --git a/README.md b/README.md index 62012ab1..905f8e36 100644 --- a/README.md +++ b/README.md @@ -194,24 +194,24 @@ 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 ``` -### Runtime Aliases (`argv[0]` pattern) +### Runtime Aliases (built-in bins / `argv[0]` pattern) -Built-in Droid runtime aliases: +Built-in Droid runtime aliases are installed with the package: ```bash -ln -s "$(command -v ccs)" /usr/local/bin/ccs-droid -ln -s "$(command -v ccs)" /usr/local/bin/ccsd ccs-droid glm # explicit alias ccsd glm # legacy shortcut ``` -Need additional alias names? Use `CCS_TARGET_ALIASES` (preferred) or keep using -legacy `CCS_DROID_ALIASES`: +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 -CCS_TARGET_ALIASES='droid=mydroid,team-droid' +ln -s "$(command -v ccs)" /usr/local/bin/mydroid +CCS_TARGET_ALIASES='droid=mydroid' # Legacy fallback still supported: -CCS_DROID_ALIASES='mydroid,team-droid' +CCS_DROID_ALIASES='mydroid' ``` For Factory BYOK compatibility, CCS also stores a per-profile Droid provider hint diff --git a/docs/system-architecture/index.md b/docs/system-architecture/index.md index 6c4e1647..1fba949e 100644 --- a/docs/system-architecture/index.md +++ b/docs/system-architecture/index.md @@ -86,7 +86,7 @@ Spawn Target Process - Spawns: `droid -m custom:ccs- ` - Model config includes baseUrl, apiKey, provider -**Runtime alias pattern (argv[0]-style):** +**Runtime alias pattern (built-in bins / argv[0]-style):** ``` ccs → Target: claude (default) diff --git a/docs/system-architecture/target-adapters.md b/docs/system-architecture/target-adapters.md index 5d81005e..23a29a97 100644 --- a/docs/system-architecture/target-adapters.md +++ b/docs/system-architecture/target-adapters.md @@ -355,27 +355,28 @@ ccs --target droid glm ### Runtime Alias Pattern ```bash -# Create aliases/symlinks to auto-select droid target -ln -s /path/to/ccs /path/to/ccs-droid -ln -s /path/to/ccs /path/to/ccsd - -# Usage +# Built-in package bin aliases ccs-droid glm -→ Target: droid (detected from argv[0]) +→ Target: droid (forced by runtime alias) → droid -m custom:ccs-glm "args..." # 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, `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_TARGET_ALIASES` -(preferred, `target=alias1,alias2;...`) or legacy `CCS_DROID_ALIASES` -(comma-separated). Example: -`CCS_TARGET_ALIASES=droid=mydroid,team-droid` +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 +``` --- diff --git a/package.json b/package.json index ef687955..c0dd9ea1 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "types": "dist/ccs.d.ts", "bin": { "ccs": "dist/ccs.js", - "ccs-droid": "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/targets/target-resolver.ts b/src/targets/target-resolver.ts index eced5f8a..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' */ @@ -20,14 +20,20 @@ const BUILTIN_ARGV0_TARGET_MAP: Record = { 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 addAliasToMap(map: Record, alias: string, target: TargetType): void { const normalizedAlias = alias.trim().toLowerCase(); - if (!normalizedAlias || !ALIAS_NAME_REGEX.test(normalizedAlias)) { + if ( + !normalizedAlias || + !ALIAS_NAME_REGEX.test(normalizedAlias) || + RESERVED_BIN_NAMES.has(normalizedAlias) + ) { return; } @@ -87,6 +93,16 @@ function buildArgv0TargetMap(): Record { 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. */ @@ -172,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(); @@ -187,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/target-resolver.test.ts b/tests/unit/targets/target-resolver.test.ts index 7628ed63..6e60a177 100644 --- a/tests/unit/targets/target-resolver.test.ts +++ b/tests/unit/targets/target-resolver.test.ts @@ -8,6 +8,7 @@ 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; @@ -22,6 +23,12 @@ describe('resolveTargetType', () => { } 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', () => { @@ -82,12 +89,29 @@ describe('resolveTargetType', () => { 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']; @@ -134,9 +158,41 @@ describe('resolveTargetType', () => { 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 8cb6835d..bc6c8f5f 100644 --- a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx +++ b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx @@ -93,8 +93,8 @@ export function ProviderInfoTab({ command={`ccs-droid ${provider} "your prompt"`} /> diff --git a/ui/src/components/profiles/editor/info-section.tsx b/ui/src/components/profiles/editor/info-section.tsx index 535b4b4e..6c6122c0 100644 --- a/ui/src/components/profiles/editor/info-section.tsx +++ b/ui/src/components/profiles/editor/info-section.tsx @@ -103,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 fe32c183..ca256fe2 100644 --- a/ui/src/components/profiles/profile-create-dialog.tsx +++ b/ui/src/components/profiles/profile-create-dialog.tsx @@ -526,23 +526,23 @@ export function ProfileCreateDialog({

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

diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index 237be3e4..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', @@ -1790,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: '自定义', @@ -2986,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', @@ -4206,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: 'カスタム',