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
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
;;
*)
+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; 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'
# ============================================================================
+1 -1
View File
@@ -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')
+1 -1
View File
@@ -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})'
;;
+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 { 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<string, string>): Record<strin
const baseUrl = envVars['ANTHROPIC_BASE_URL'] || '';
const apiKey = envVars['ANTHROPIC_AUTH_TOKEN'] || '';
const model = envVars['ANTHROPIC_MODEL'] || '';
const result: Record<string, string> = {
OPENAI_API_KEY: apiKey,
OPENAI_BASE_URL: baseUrl,
LOCAL_ENDPOINT: baseUrl,
};
const result: Record<string, string> = {};
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<void> {
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<string, string> = {};
@@ -212,9 +215,28 @@ export async function handleEnvCommand(args: string[]): Promise<void> {
// 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;
+7 -6
View File
@@ -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();
});