From 3f5ecd4d6963a25ce909cd7e17fcca610d9dc978 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 11 Feb 2026 07:09:58 +0700 Subject: [PATCH] fix(env): address P1-P3 review items from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - P1: Fix bash completion $cliproxy_profiles scoping — inline profiles in env block since variable is only defined at COMP_CWORD=1 scope - P2: Detect account-based profiles and show specific error message instead of generic "not found" - P2: Show `ccs migrate` hint when unified mode is disabled and settings profile resolution fails - P2: transformToOpenAI omits empty entries at transform time instead of relying on output filter (removes fragile coupling) - P3: Add zsh and auto to --shell completions across all 4 shells; map --shell zsh to bash in command handler since syntax is identical - P3: Auto-detect PowerShell from SHELL containing pwsh on non-Windows - Tests: 33 pass (+1 pwsh detection test, updated transform assertions) --- scripts/completion/ccs.bash | 6 ++-- scripts/completion/ccs.fish | 2 +- scripts/completion/ccs.ps1 | 2 +- scripts/completion/ccs.zsh | 2 +- src/commands/env-command.ts | 38 +++++++++++++++++++------ tests/unit/commands/env-command.test.ts | 13 +++++---- 6 files changed, 43 insertions(+), 20 deletions(-) diff --git a/scripts/completion/ccs.bash b/scripts/completion/ccs.bash index a9b1d392..815f27a1 100644 --- a/scripts/completion/ccs.bash +++ b/scripts/completion/ccs.bash @@ -155,8 +155,8 @@ _ccs_completion() { if [[ ${COMP_WORDS[1]} == "env" ]]; then case "${prev}" in env) - # Complete with profile names and flags - local env_opts="--format --shell --help -h $cliproxy_profiles" + # Complete with profile names and flags (inline profiles since $cliproxy_profiles is out of scope) + local env_opts="--format --shell --help -h gemini codex agy qwen" if [[ -f ~/.ccs/config.json ]]; then env_opts="$env_opts $(jq -r '.profiles | keys[]' ~/.ccs/config.json 2>/dev/null || true)" fi @@ -168,7 +168,7 @@ _ccs_completion() { return 0 ;; --shell) - COMPREPLY=( $(compgen -W "bash fish powershell" -- ${cur}) ) + COMPREPLY=( $(compgen -W "auto bash zsh fish powershell" -- ${cur}) ) return 0 ;; *) diff --git a/scripts/completion/ccs.fish b/scripts/completion/ccs.fish index 9593e736..5f81c5d0 100644 --- a/scripts/completion/ccs.fish +++ b/scripts/completion/ccs.fish @@ -176,7 +176,7 @@ complete -c ccs -n '__fish_seen_subcommand_from doctor' -s h -l help -d 'Show he complete -c ccs -n '__fish_seen_subcommand_from env' -l format -d 'Output format' complete -c ccs -n '__fish_seen_subcommand_from env; and __fish_seen_argument -l format' -a 'openai anthropic raw' -d 'Format' complete -c ccs -n '__fish_seen_subcommand_from env' -l shell -d 'Shell syntax' -complete -c ccs -n '__fish_seen_subcommand_from env; and __fish_seen_argument -l shell' -a 'bash fish powershell' -d 'Shell' +complete -c ccs -n '__fish_seen_subcommand_from env; and __fish_seen_argument -l shell' -a 'auto bash zsh fish powershell' -d 'Shell' complete -c ccs -n '__fish_seen_subcommand_from env' -s h -l help -d 'Show help for env command' # ============================================================================ diff --git a/scripts/completion/ccs.ps1 b/scripts/completion/ccs.ps1 index 9f5ac508..daad20d3 100644 --- a/scripts/completion/ccs.ps1 +++ b/scripts/completion/ccs.ps1 @@ -23,7 +23,7 @@ Register-ArgumentCompleter -CommandName ccs -ScriptBlock { $updateFlags = @('--force', '--beta', '--dev', '--help', '-h') $envFlags = @('--format', '--shell', '--help', '-h') $envFormats = @('openai', 'anthropic', 'raw') - $envShells = @('bash', 'fish', 'powershell') + $envShells = @('auto', 'bash', 'zsh', 'fish', 'powershell') $shellCompletionFlags = @('--bash', '--zsh', '--fish', '--powershell') $listFlags = @('--verbose', '--json') $removeFlags = @('--yes', '-y') diff --git a/scripts/completion/ccs.zsh b/scripts/completion/ccs.zsh index 225bf2b4..3ae11d32 100644 --- a/scripts/completion/ccs.zsh +++ b/scripts/completion/ccs.zsh @@ -128,7 +128,7 @@ _ccs() { env) _arguments \ '--format[Output format]:format:(openai anthropic raw)' \ - '--shell[Shell syntax]:shell:(bash fish powershell)' \ + '--shell[Shell syntax]:shell:(auto bash zsh fish powershell)' \ '(- *)'{-h,--help}'[Show help]' \ '1:profile:($proxy_profiles ${(k)settings_profiles_described})' ;; diff --git a/src/commands/env-command.ts b/src/commands/env-command.ts index 92bf626f..843b8c01 100644 --- a/src/commands/env-command.ts +++ b/src/commands/env-command.ts @@ -13,6 +13,7 @@ import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager'; import { isUnifiedMode, loadUnifiedConfig } from '../config/unified-config-loader'; import { expandPath } from '../utils/helpers'; import { getCcsDir } from '../utils/config-manager'; +import { ProfileRegistry } from '../auth/profile-registry'; type ShellType = 'bash' | 'fish' | 'powershell'; type OutputFormat = 'openai' | 'anthropic' | 'raw'; @@ -28,7 +29,7 @@ export function detectShell(flag?: string): ShellType { } const shell = process.env['SHELL'] || ''; if (shell.includes('fish')) return 'fish'; - if (process.platform === 'win32') return 'powershell'; + if (shell.includes('pwsh') || process.platform === 'win32') return 'powershell'; return 'bash'; } @@ -54,11 +55,12 @@ export function transformToOpenAI(envVars: Record): Record = { - OPENAI_API_KEY: apiKey, - OPENAI_BASE_URL: baseUrl, - LOCAL_ENDPOINT: baseUrl, - }; + const result: Record = {}; + if (apiKey) result['OPENAI_API_KEY'] = apiKey; + if (baseUrl) { + result['OPENAI_BASE_URL'] = baseUrl; + result['LOCAL_ENDPOINT'] = baseUrl; + } if (model) result['OPENAI_MODEL'] = model; return result; } @@ -195,7 +197,8 @@ export async function handleEnvCommand(args: string[]): Promise { const format = formatStr as OutputFormat; const shellStr = parseFlag(args, 'shell') || 'auto'; - const shell = detectShell(shellStr); + // zsh uses the same syntax as bash + const shell = detectShell(shellStr === 'zsh' ? 'bash' : shellStr); // Resolve env vars based on profile type let envVars: Record = {}; @@ -212,9 +215,28 @@ export async function handleEnvCommand(args: string[]): Promise { // Settings-based profile (glm, kimi, custom API) const resolved = resolveSettingsProfile(profile); if (!resolved) { + // Check if it's an account-based profile + const registry = new ProfileRegistry(); + const allProfiles = registry.getAllProfiles(); + if (allProfiles[profile]) { + console.error( + fail( + `'${profile}' is an account-based profile. ` + + '`ccs env` only supports CLIProxy and settings profiles.' + ) + ); + process.exit(1); + } + console.error(fail(`Profile '${profile}' not found.`)); console.error(dim(' Available CLIProxy profiles: ' + CLIPROXY_PROFILES.join(', '))); - console.error(dim(` Check ${getCcsDir()}/config.yaml for custom profiles.`)); + if (!isUnifiedMode()) { + console.error( + dim(' Settings profiles require unified config. Run `ccs migrate` to upgrade.') + ); + } else { + console.error(dim(` Check ${getCcsDir()}/config.yaml for custom profiles.`)); + } process.exit(1); } envVars = resolved; diff --git a/tests/unit/commands/env-command.test.ts b/tests/unit/commands/env-command.test.ts index d1c0aeca..d70a140c 100644 --- a/tests/unit/commands/env-command.test.ts +++ b/tests/unit/commands/env-command.test.ts @@ -60,6 +60,11 @@ describe('env-command', () => { process.env['SHELL'] = '/bin/bash'; expect(detectShell('invalid')).toBe('bash'); }); + + it('auto-detects powershell from SHELL containing pwsh', () => { + process.env['SHELL'] = '/usr/local/bin/pwsh'; + expect(detectShell('auto')).toBe('powershell'); + }); }); describe('formatExportLine', () => { @@ -128,11 +133,7 @@ describe('env-command', () => { it('handles missing source vars gracefully', () => { const result = transformToOpenAI({}); - expect(result).toEqual({ - OPENAI_API_KEY: '', - OPENAI_BASE_URL: '', - LOCAL_ENDPOINT: '', - }); + expect(result).toEqual({}); }); it('only extracts relevant vars', () => { @@ -143,7 +144,7 @@ describe('env-command', () => { DISABLE_TELEMETRY: '1', }); - // Should only have 3 keys (no OPENAI_MODEL when ANTHROPIC_MODEL absent) + // OPENAI_API_KEY + OPENAI_BASE_URL + LOCAL_ENDPOINT (no OPENAI_MODEL when ANTHROPIC_MODEL absent) expect(Object.keys(result)).toHaveLength(3); expect(result['ANTHROPIC_MAX_TOKENS']).toBeUndefined(); });