fix(env): address P1-P3 review items from code review

- 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)
This commit is contained in:
Tam Nhu Tran
2026-02-11 07:09:58 +07:00
parent d5c03d1f2d
commit 3f5ecd4d69
6 changed files with 43 additions and 20 deletions
+3 -3
View File
@@ -155,8 +155,8 @@ _ccs_completion() {
if [[ ${COMP_WORDS[1]} == "env" ]]; then if [[ ${COMP_WORDS[1]} == "env" ]]; then
case "${prev}" in case "${prev}" in
env) env)
# Complete with profile names and flags # Complete with profile names and flags (inline profiles since $cliproxy_profiles is out of scope)
local env_opts="--format --shell --help -h $cliproxy_profiles" local env_opts="--format --shell --help -h gemini codex agy qwen"
if [[ -f ~/.ccs/config.json ]]; then if [[ -f ~/.ccs/config.json ]]; then
env_opts="$env_opts $(jq -r '.profiles | keys[]' ~/.ccs/config.json 2>/dev/null || true)" env_opts="$env_opts $(jq -r '.profiles | keys[]' ~/.ccs/config.json 2>/dev/null || true)"
fi fi
@@ -168,7 +168,7 @@ _ccs_completion() {
return 0 return 0
;; ;;
--shell) --shell)
COMPREPLY=( $(compgen -W "bash fish powershell" -- ${cur}) ) COMPREPLY=( $(compgen -W "auto bash zsh fish powershell" -- ${cur}) )
return 0 return 0
;; ;;
*) *)
+1 -1
View File
@@ -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' -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; 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' -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' complete -c ccs -n '__fish_seen_subcommand_from env' -s h -l help -d 'Show help for env command'
# ============================================================================ # ============================================================================
+1 -1
View File
@@ -23,7 +23,7 @@ Register-ArgumentCompleter -CommandName ccs -ScriptBlock {
$updateFlags = @('--force', '--beta', '--dev', '--help', '-h') $updateFlags = @('--force', '--beta', '--dev', '--help', '-h')
$envFlags = @('--format', '--shell', '--help', '-h') $envFlags = @('--format', '--shell', '--help', '-h')
$envFormats = @('openai', 'anthropic', 'raw') $envFormats = @('openai', 'anthropic', 'raw')
$envShells = @('bash', 'fish', 'powershell') $envShells = @('auto', 'bash', 'zsh', 'fish', 'powershell')
$shellCompletionFlags = @('--bash', '--zsh', '--fish', '--powershell') $shellCompletionFlags = @('--bash', '--zsh', '--fish', '--powershell')
$listFlags = @('--verbose', '--json') $listFlags = @('--verbose', '--json')
$removeFlags = @('--yes', '-y') $removeFlags = @('--yes', '-y')
+1 -1
View File
@@ -128,7 +128,7 @@ _ccs() {
env) env)
_arguments \ _arguments \
'--format[Output format]:format:(openai anthropic raw)' \ '--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]' \ '(- *)'{-h,--help}'[Show help]' \
'1:profile:($proxy_profiles ${(k)settings_profiles_described})' '1:profile:($proxy_profiles ${(k)settings_profiles_described})'
;; ;;
+30 -8
View File
@@ -13,6 +13,7 @@ import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
import { isUnifiedMode, loadUnifiedConfig } from '../config/unified-config-loader'; import { isUnifiedMode, loadUnifiedConfig } from '../config/unified-config-loader';
import { expandPath } from '../utils/helpers'; import { expandPath } from '../utils/helpers';
import { getCcsDir } from '../utils/config-manager'; import { getCcsDir } from '../utils/config-manager';
import { ProfileRegistry } from '../auth/profile-registry';
type ShellType = 'bash' | 'fish' | 'powershell'; type ShellType = 'bash' | 'fish' | 'powershell';
type OutputFormat = 'openai' | 'anthropic' | 'raw'; type OutputFormat = 'openai' | 'anthropic' | 'raw';
@@ -28,7 +29,7 @@ export function detectShell(flag?: string): ShellType {
} }
const shell = process.env['SHELL'] || ''; const shell = process.env['SHELL'] || '';
if (shell.includes('fish')) return 'fish'; if (shell.includes('fish')) return 'fish';
if (process.platform === 'win32') return 'powershell'; if (shell.includes('pwsh') || process.platform === 'win32') return 'powershell';
return 'bash'; return 'bash';
} }
@@ -54,11 +55,12 @@ export function transformToOpenAI(envVars: Record<string, string>): Record<strin
const baseUrl = envVars['ANTHROPIC_BASE_URL'] || ''; const baseUrl = envVars['ANTHROPIC_BASE_URL'] || '';
const apiKey = envVars['ANTHROPIC_AUTH_TOKEN'] || ''; const apiKey = envVars['ANTHROPIC_AUTH_TOKEN'] || '';
const model = envVars['ANTHROPIC_MODEL'] || ''; const model = envVars['ANTHROPIC_MODEL'] || '';
const result: Record<string, string> = { const result: Record<string, string> = {};
OPENAI_API_KEY: apiKey, if (apiKey) result['OPENAI_API_KEY'] = apiKey;
OPENAI_BASE_URL: baseUrl, if (baseUrl) {
LOCAL_ENDPOINT: baseUrl, result['OPENAI_BASE_URL'] = baseUrl;
}; result['LOCAL_ENDPOINT'] = baseUrl;
}
if (model) result['OPENAI_MODEL'] = model; if (model) result['OPENAI_MODEL'] = model;
return result; return result;
} }
@@ -195,7 +197,8 @@ export async function handleEnvCommand(args: string[]): Promise<void> {
const format = formatStr as OutputFormat; const format = formatStr as OutputFormat;
const shellStr = parseFlag(args, 'shell') || 'auto'; 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 // Resolve env vars based on profile type
let envVars: Record<string, string> = {}; let envVars: Record<string, string> = {};
@@ -212,9 +215,28 @@ export async function handleEnvCommand(args: string[]): Promise<void> {
// Settings-based profile (glm, kimi, custom API) // Settings-based profile (glm, kimi, custom API)
const resolved = resolveSettingsProfile(profile); const resolved = resolveSettingsProfile(profile);
if (!resolved) { 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(fail(`Profile '${profile}' not found.`));
console.error(dim(' Available CLIProxy profiles: ' + CLIPROXY_PROFILES.join(', '))); 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); process.exit(1);
} }
envVars = resolved; envVars = resolved;
+7 -6
View File
@@ -60,6 +60,11 @@ describe('env-command', () => {
process.env['SHELL'] = '/bin/bash'; process.env['SHELL'] = '/bin/bash';
expect(detectShell('invalid')).toBe('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', () => { describe('formatExportLine', () => {
@@ -128,11 +133,7 @@ describe('env-command', () => {
it('handles missing source vars gracefully', () => { it('handles missing source vars gracefully', () => {
const result = transformToOpenAI({}); const result = transformToOpenAI({});
expect(result).toEqual({ expect(result).toEqual({});
OPENAI_API_KEY: '',
OPENAI_BASE_URL: '',
LOCAL_ENDPOINT: '',
});
}); });
it('only extracts relevant vars', () => { it('only extracts relevant vars', () => {
@@ -143,7 +144,7 @@ describe('env-command', () => {
DISABLE_TELEMETRY: '1', 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(Object.keys(result)).toHaveLength(3);
expect(result['ANTHROPIC_MAX_TOKENS']).toBeUndefined(); expect(result['ANTHROPIC_MAX_TOKENS']).toBeUndefined();
}); });