mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 14:19:56 +00:00
Merge pull request #505 from kaitranntt/kai/feat/503-ccs-env-command
feat(env): add ccs env command for third-party tool integration
This commit is contained in:
@@ -131,6 +131,7 @@ bun run validate # Step 3: Final check (must pass)
|
||||
| `ccs copilot --help` | `src/commands/copilot-command.ts` → `handleHelp()` |
|
||||
| `ccs doctor --help` | `src/commands/doctor-command.ts` → `showHelp()` |
|
||||
| `ccs migrate --help` | `src/commands/migrate-command.ts` → `printMigrateHelp()` |
|
||||
| `ccs env --help` | `src/commands/env-command.ts` → `showHelp()` |
|
||||
| `ccs persist --help` | `src/commands/persist-command.ts` → `showHelp()` |
|
||||
| `ccs setup --help` | `src/commands/setup-command.ts` → `showHelp()` |
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@kaitranntt/ccs",
|
||||
|
||||
@@ -48,6 +48,7 @@ src/
|
||||
│ ├── config-command.ts # Config management commands
|
||||
│ ├── config-image-analysis-command.ts # Image analysis hook config (NEW v7.34)
|
||||
│ ├── doctor-command.ts # Health diagnostics
|
||||
│ ├── env-command.ts # Export shell env vars for third-party tools (v7.39)
|
||||
│ ├── help-command.ts # Help text generation
|
||||
│ ├── install-command.ts # Install/uninstall logic
|
||||
│ ├── shell-completion-command.ts
|
||||
@@ -465,10 +466,12 @@ export type { ProviderEditorProps } from './provider-editor';
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/ # Unit tests (6 core test files)
|
||||
├── unit/ # Unit tests (7 core test files)
|
||||
│ ├── data-aggregator.test.ts
|
||||
│ ├── cliproxy/
|
||||
│ │ └── remote-proxy-client.test.ts
|
||||
│ ├── commands/
|
||||
│ │ └── env-command.test.ts
|
||||
│ ├── jsonl-parser.test.ts
|
||||
│ ├── model-pricing.test.ts
|
||||
│ ├── unified-config.test.ts
|
||||
@@ -487,12 +490,12 @@ tests/
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Total Tests | 1407 |
|
||||
| Passing | 1407 |
|
||||
| Total Tests | 1440 |
|
||||
| Passing | 1440 |
|
||||
| Skipped | 6 |
|
||||
| Failed | 0 |
|
||||
| Coverage Threshold | 90% |
|
||||
| Test Files | 40+ |
|
||||
| Test Files | 41 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -115,6 +115,13 @@ CCS provides:
|
||||
- Entrypoint with privilege dropping and usage help
|
||||
- Environment variable configuration support
|
||||
|
||||
### FR-011: Third-Party Tool Integration
|
||||
- Export shell-evaluable env vars via `ccs env` command
|
||||
- Support OpenAI, Anthropic, raw output formats
|
||||
- Auto-detect shell (bash/zsh, fish, PowerShell) from $SHELL
|
||||
- Security: single-quoted output, key sanitization, shell-specific escaping
|
||||
- Cross-platform compatibility (macOS, Linux, Windows)
|
||||
|
||||
---
|
||||
|
||||
## Non-Functional Requirements
|
||||
@@ -192,7 +199,7 @@ CCS provides:
|
||||
| Startup time | < 100ms | Achieved |
|
||||
| Dashboard load | < 2s | Achieved |
|
||||
| Error rate | < 1% | Achieved |
|
||||
| Test coverage | > 90% | 90% (1407 tests, 6 skipped) |
|
||||
| Test coverage | > 90% | 90% (1440 tests, 6 skipped) |
|
||||
| File size compliance | 100% < 200 lines | 95% |
|
||||
|
||||
---
|
||||
@@ -261,6 +268,16 @@ CCS provides:
|
||||
- [x] Quota 429 rate limit handling improvements
|
||||
- [x] WebSocket maxPayload limit (DoS prevention)
|
||||
|
||||
### v7.39 Release (Complete)
|
||||
- [x] `ccs env` command for third-party tool integration (OpenCode, Cursor, Continue)
|
||||
- [x] Multi-format output: openai, anthropic, raw
|
||||
- [x] Multi-shell support: bash/zsh, fish, PowerShell (auto-detected)
|
||||
- [x] CLIProxy profile support (gemini, codex, agy, qwen)
|
||||
- [x] Settings profile support (glm, kimi, custom API)
|
||||
- [x] Security: single-quoted output, key sanitization, shell-specific escaping
|
||||
- [x] Shell completion updated (bash, zsh, fish, PowerShell)
|
||||
- [x] 34 unit tests for env command
|
||||
|
||||
### v8.0 Release (Planned - Q1 2026)
|
||||
- [ ] Multiple CLIProxyAPI instances (load balancing, failover)
|
||||
- [ ] Native git worktree support
|
||||
|
||||
@@ -26,13 +26,14 @@ All major modularization work is complete. The codebase evolved from monolithic
|
||||
| 12 | Hybrid Quota Management | `quota-manager.ts`, `quota-fetcher.ts` (v7.14) |
|
||||
| 13 | Docker Support | `docker/` directory with Dockerfile, Compose, entrypoint |
|
||||
| 14 | Image Analysis Hook | Vision proxying via CLIProxy transformers (v7.34) |
|
||||
| 15 | Third-Party Tool Integration | `ccs env` command with multi-format export (v7.39) |
|
||||
|
||||
**Metrics Achieved**:
|
||||
- Files >500 lines: 12 -> 5 (-58%)
|
||||
- UI files >200 lines: 28 -> 8 (-71%)
|
||||
- Barrel exports: 5 -> 39 (+680%)
|
||||
- Test coverage: 0% -> 90%
|
||||
- Total tests: 1407 (6 skipped)
|
||||
- Total tests: 1440 (6 skipped)
|
||||
|
||||
---
|
||||
|
||||
@@ -170,6 +171,7 @@ worktrees:
|
||||
| Hybrid Quota Management | COMPLETE | v7.14 |
|
||||
| Docker Support (PR #345) | COMPLETE | v7.23 |
|
||||
| Image Analysis Hook | COMPLETE | v7.34 |
|
||||
| Third-Party Tool Integration | COMPLETE | v7.39 |
|
||||
| Critical Bug Fixes (#158, #155, #124) | PLANNED | Q1 2026 |
|
||||
| Multiple CLIProxyAPI Instances | PLANNED | Q1 2026 |
|
||||
| Git Worktree Support | PLANNED | Q2 2026 |
|
||||
|
||||
@@ -116,9 +116,10 @@ CCS v7.34 adds Image Analysis Hook for vision model proxying through CLIProxy wi
|
||||
| commands/ | | auth/ | | config/ |
|
||||
|------------------| |------------------| |------------------|
|
||||
| doctor-command | | account-switcher | | unified-config- |
|
||||
| help-command | | profile-detector | | loader |
|
||||
| install-command | | commands/ | | migration-manager|
|
||||
| sync-command | +------------------+ +------------------+
|
||||
| env-command | | profile-detector | | loader |
|
||||
| help-command | | commands/ | | migration-manager|
|
||||
| install-command | +------------------+ +------------------+
|
||||
| sync-command |
|
||||
| update-command |
|
||||
+------------------+
|
||||
| | |
|
||||
|
||||
@@ -18,7 +18,7 @@ _ccs_completion() {
|
||||
|
||||
# Top-level completion (first argument)
|
||||
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
||||
local commands="auth api cliproxy doctor sync update"
|
||||
local commands="auth api cliproxy doctor env sync update"
|
||||
local flags="--help --version --shell-completion -h -v -sc"
|
||||
local cliproxy_profiles="gemini codex agy qwen"
|
||||
local profiles=""
|
||||
@@ -151,6 +151,33 @@ _ccs_completion() {
|
||||
esac
|
||||
fi
|
||||
|
||||
# env subcommands
|
||||
if [[ ${COMP_WORDS[1]} == "env" ]]; then
|
||||
case "${prev}" in
|
||||
env)
|
||||
# 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 iflow kiro ghcp claude"
|
||||
if [[ -f ~/.ccs/config.json ]]; then
|
||||
env_opts="$env_opts $(jq -r '.profiles | keys[]' ~/.ccs/config.json 2>/dev/null || true)"
|
||||
fi
|
||||
COMPREPLY=( $(compgen -W "${env_opts}" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
--format)
|
||||
COMPREPLY=( $(compgen -W "openai anthropic raw" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
--shell)
|
||||
COMPREPLY=( $(compgen -W "auto bash zsh fish powershell" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=( $(compgen -W "--format --shell --help -h" -- ${cur}) )
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Flags for doctor command
|
||||
if [[ ${COMP_WORDS[1]} == "doctor" ]]; then
|
||||
COMPREPLY=( $(compgen -W "--help -h" -- ${cur}) )
|
||||
|
||||
+26
-17
@@ -121,33 +121,34 @@ complete -c ccs -s v -l version -d 'Show version information'
|
||||
complete -c ccs -s sc -l shell-completion -d 'Install shell completion'
|
||||
|
||||
# Commands - grouped with [cmd] prefix for visual distinction
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'auth' -d '[cmd] Manage multiple Claude accounts'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'api' -d '[cmd] Manage API profiles (create/remove)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'cliproxy' -d '[cmd] Manage CLIProxy variants and binary'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'doctor' -d '[cmd] Run health check and diagnostics'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'sync' -d '[cmd] Sync delegation commands and skills'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'update' -d '[cmd] Update CCS to latest version'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'auth' -d '[cmd] Manage multiple Claude accounts'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'api' -d '[cmd] Manage API profiles (create/remove)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'cliproxy' -d '[cmd] Manage CLIProxy variants and binary'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'doctor' -d '[cmd] Run health check and diagnostics'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'env' -d '[cmd] Export env vars for third-party tools'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'sync' -d '[cmd] Sync delegation commands and skills'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'update' -d '[cmd] Update CCS to latest version'
|
||||
|
||||
# CLIProxy profiles - grouped with [proxy] prefix for OAuth providers
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'gemini' -d '[proxy] Google Gemini (OAuth)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'codex' -d '[proxy] OpenAI Codex (OAuth)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'agy' -d '[proxy] Antigravity (OAuth)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'qwen' -d '[proxy] Qwen Code (OAuth)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'gemini' -d '[proxy] Google Gemini (OAuth)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'codex' -d '[proxy] OpenAI Codex (OAuth)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'agy' -d '[proxy] Antigravity (OAuth)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'qwen' -d '[proxy] Qwen Code (OAuth)'
|
||||
|
||||
# Model profiles - grouped with [model] prefix for visual distinction
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'default' -d '[model] Default Claude Sonnet 4.5'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'glm' -d '[model] GLM-4.6 (cost-optimized)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'glmt' -d '[model] GLM-4.6 with thinking mode'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a 'kimi' -d '[model] Kimi for Coding (long-context)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'default' -d '[model] Default Claude Sonnet 4.5'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'glm' -d '[model] GLM-4.6 (cost-optimized)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'glmt' -d '[model] GLM-4.6 with thinking mode'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a 'kimi' -d '[model] Kimi for Coding (long-context)'
|
||||
|
||||
# Custom model profiles - dynamic with [model] prefix
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a '(__fish_ccs_get_custom_settings_profiles)' -d '[model] Settings-based profile'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a '(__fish_ccs_get_custom_settings_profiles)' -d '[model] Settings-based profile'
|
||||
|
||||
# CLIProxy variants - dynamic with [variant] prefix
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a '(__fish_ccs_get_cliproxy_variants)' -d '[variant] CLIProxy variant'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a '(__fish_ccs_get_cliproxy_variants)' -d '[variant] CLIProxy variant'
|
||||
|
||||
# Account profiles - dynamic with [account] prefix
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor sync update gemini codex agy qwen' -a '(__fish_ccs_get_account_profiles)' -d '[account] Account-based profile'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth api cliproxy doctor env sync update gemini codex agy qwen' -a '(__fish_ccs_get_account_profiles)' -d '[account] Account-based profile'
|
||||
|
||||
# shell-completion subflags
|
||||
complete -c ccs -n '__fish_seen_argument -l shell-completion; or __fish_seen_argument -s sc' -l bash -d 'Install for bash'
|
||||
@@ -171,6 +172,14 @@ complete -c ccs -n '__fish_seen_subcommand_from update' -s h -l help -d 'Show he
|
||||
# doctor command flags
|
||||
complete -c ccs -n '__fish_seen_subcommand_from doctor' -s h -l help -d 'Show help for doctor command'
|
||||
|
||||
# env command completions
|
||||
complete -c ccs -n '__fish_seen_subcommand_from env; and not __fish_seen_argument -l format -l shell' -a 'gemini codex agy qwen iflow kiro ghcp claude' -d '[proxy] CLIProxy profile'
|
||||
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 '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'
|
||||
|
||||
# ============================================================================
|
||||
# auth subcommands
|
||||
# ============================================================================
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
Register-ArgumentCompleter -CommandName ccs -ScriptBlock {
|
||||
param($commandName, $wordToComplete, $commandAst, $fakeBoundParameters)
|
||||
|
||||
$commands = @('auth', 'api', 'cliproxy', 'doctor', 'sync', 'update', '--help', '--version', '--shell-completion', '-h', '-v', '-sc')
|
||||
$cliproxyProfiles = @('gemini', 'codex', 'agy', 'qwen')
|
||||
$commands = @('auth', 'api', 'cliproxy', 'doctor', 'env', 'sync', 'update', '--help', '--version', '--shell-completion', '-h', '-v', '-sc')
|
||||
$cliproxyProfiles = @('gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude')
|
||||
$authCommands = @('create', 'list', 'show', 'remove', 'default', '--help', '-h')
|
||||
$apiCommands = @('create', 'list', 'remove', '--help', '-h')
|
||||
$cliproxyCommands = @('create', 'list', 'remove', '--install', '--latest', '--help', '-h')
|
||||
@@ -21,6 +21,9 @@ Register-ArgumentCompleter -CommandName ccs -ScriptBlock {
|
||||
$cliproxyCreateFlags = @('--provider', '--model', '--force', '--yes', '-y')
|
||||
$providerFlags = @('--auth', '--config', '--logout', '--headless', '--help', '-h')
|
||||
$updateFlags = @('--force', '--beta', '--dev', '--help', '-h')
|
||||
$envFlags = @('--format', '--shell', '--help', '-h')
|
||||
$envFormats = @('openai', 'anthropic', 'raw')
|
||||
$envShells = @('auto', 'bash', 'zsh', 'fish', 'powershell')
|
||||
$shellCompletionFlags = @('--bash', '--zsh', '--fish', '--powershell')
|
||||
$listFlags = @('--verbose', '--json')
|
||||
$removeFlags = @('--yes', '-y')
|
||||
@@ -130,6 +133,55 @@ Register-ArgumentCompleter -CommandName ccs -ScriptBlock {
|
||||
return
|
||||
}
|
||||
|
||||
# env command completion
|
||||
if ($words[1] -eq 'env') {
|
||||
if ($position -eq 3) {
|
||||
$options = $cliproxyProfiles + (Get-CcsProfiles -Type settings) + $envFlags
|
||||
$options | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
||||
[System.Management.Automation.CompletionResult]::new(
|
||||
$_,
|
||||
$_,
|
||||
'ParameterValue',
|
||||
$_
|
||||
)
|
||||
}
|
||||
} elseif ($position -ge 4) {
|
||||
switch ($words[$position - 2]) {
|
||||
'--format' {
|
||||
$envFormats | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
||||
[System.Management.Automation.CompletionResult]::new(
|
||||
$_,
|
||||
$_,
|
||||
'ParameterValue',
|
||||
$_
|
||||
)
|
||||
}
|
||||
}
|
||||
'--shell' {
|
||||
$envShells | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
||||
[System.Management.Automation.CompletionResult]::new(
|
||||
$_,
|
||||
$_,
|
||||
'ParameterValue',
|
||||
$_
|
||||
)
|
||||
}
|
||||
}
|
||||
default {
|
||||
$envFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
||||
[System.Management.Automation.CompletionResult]::new(
|
||||
$_,
|
||||
$_,
|
||||
'ParameterValue',
|
||||
$_
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
# auth subcommand completion
|
||||
if ($words[1] -eq 'auth') {
|
||||
if ($position -eq 3) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# sudo cp scripts/completion/ccs.zsh /usr/local/share/zsh/site-functions/_ccs
|
||||
|
||||
# Set up completion styles for better formatting and colors
|
||||
zstyle ':completion:*:*:ccs:*:commands' list-colors '=(#b)(auth|api|cliproxy|doctor|sync|update)([[:space:]]#--[[:space:]]#*)==0\;34=2\;37'
|
||||
zstyle ':completion:*:*:ccs:*:commands' list-colors '=(#b)(auth|api|cliproxy|doctor|env|sync|update)([[:space:]]#--[[:space:]]#*)==0\;34=2\;37'
|
||||
zstyle ':completion:*:*:ccs:*:proxy-profiles' list-colors '=(#b)(gemini|codex|agy|qwen)([[:space:]]#--[[:space:]]#*)==0\;35=2\;37'
|
||||
zstyle ':completion:*:*:ccs:*:model-profiles' list-colors '=(#b)(default|glm|glmt|kimi|[^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;32=2\;37'
|
||||
zstyle ':completion:*:*:ccs:*:account-profiles' list-colors '=(#b)([^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;33=2\;37'
|
||||
@@ -34,6 +34,7 @@ _ccs() {
|
||||
'api:Manage API profiles (create/remove)'
|
||||
'cliproxy:Manage CLIProxy variants and binary'
|
||||
'doctor:Run health check and diagnostics'
|
||||
'env:Export env vars for third-party tools'
|
||||
'sync:Sync delegation commands and skills'
|
||||
'update:Update CCS to latest version'
|
||||
)
|
||||
@@ -44,6 +45,10 @@ _ccs() {
|
||||
'codex:OpenAI Codex (OAuth)'
|
||||
'agy:Antigravity (OAuth)'
|
||||
'qwen:Qwen Code (OAuth)'
|
||||
'iflow:iFlow (OAuth)'
|
||||
'kiro:Kiro (OAuth)'
|
||||
'ghcp:GitHub Copilot (OAuth)'
|
||||
'claude:Claude Direct (OAuth)'
|
||||
)
|
||||
|
||||
# Define known settings profiles with descriptions
|
||||
@@ -124,6 +129,13 @@ _ccs() {
|
||||
_arguments \
|
||||
'(- *)'{-h,--help}'[Show help for doctor command]'
|
||||
;;
|
||||
env)
|
||||
_arguments \
|
||||
'--format[Output format]:format:(openai anthropic raw)' \
|
||||
'--shell[Shell syntax]:shell:(auto bash zsh fish powershell)' \
|
||||
'(- *)'{-h,--help}'[Show help]' \
|
||||
'1:profile:($proxy_profiles ${(k)settings_profiles_described})'
|
||||
;;
|
||||
gemini|codex|agy|qwen)
|
||||
_arguments \
|
||||
'--auth[Authenticate only]' \
|
||||
|
||||
@@ -450,6 +450,13 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: env command (export env vars for third-party tools)
|
||||
if (firstArg === 'env') {
|
||||
const { handleEnvCommand } = await import('./commands/env-command');
|
||||
await handleEnvCommand(args.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: setup command (first-time wizard)
|
||||
if (firstArg === 'setup' || firstArg === '--setup') {
|
||||
const { handleSetupCommand } = await import('./commands/setup-command');
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* Env Command Handler
|
||||
*
|
||||
* Export environment variables for third-party tool integration.
|
||||
* Outputs shell-evaluable exports for OpenCode, Cursor, Continue, etc.
|
||||
*/
|
||||
|
||||
import { initUI, header, dim, color, subheader, fail, warn } from '../utils/ui';
|
||||
import { CLIProxyProvider } from '../cliproxy/types';
|
||||
import { CLIPROXY_PROFILES, loadSettingsFromFile } from '../auth/profile-detector';
|
||||
import { getEffectiveEnvVars } from '../cliproxy/config/env-builder';
|
||||
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';
|
||||
|
||||
const VALID_FORMATS: OutputFormat[] = ['openai', 'anthropic', 'raw'];
|
||||
const VALID_SHELLS: ShellType[] = ['bash', 'fish', 'powershell'];
|
||||
const VALID_SHELL_INPUTS = ['auto', 'bash', 'zsh', 'fish', 'powershell'] as const;
|
||||
const VALID_ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||
|
||||
/** Auto-detect shell from environment */
|
||||
export function detectShell(flag?: string): ShellType {
|
||||
if (flag && flag !== 'auto' && VALID_SHELLS.includes(flag as ShellType)) {
|
||||
return flag as ShellType;
|
||||
}
|
||||
const shell = process.env['SHELL'] || '';
|
||||
if (shell.includes('fish')) return 'fish';
|
||||
if (shell.includes('pwsh') || process.platform === 'win32') return 'powershell';
|
||||
return 'bash';
|
||||
}
|
||||
|
||||
/** Format a single env var export for the target shell (single-quoted to prevent injection) */
|
||||
export function formatExportLine(shell: ShellType, key: string, value: string): string {
|
||||
switch (shell) {
|
||||
case 'fish':
|
||||
// Fish: single quotes prevent expansion; escape embedded single quotes with '\''
|
||||
return `set -gx ${key} '${value.replace(/'/g, "'\\''")}'`;
|
||||
case 'powershell':
|
||||
// PowerShell: single quotes prevent expansion; escape embedded single quotes with ''
|
||||
return `$env:${key} = '${value.replace(/'/g, "''")}'`;
|
||||
default:
|
||||
// Bash/zsh: single quotes prevent all expansion; handle embedded single quotes
|
||||
return `export ${key}='${value.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Map Anthropic env vars to OpenAI-compatible format.
|
||||
* OPENAI_MODEL is included so tools that need it (e.g. OpenCode local provider)
|
||||
* can discover the model without additional configuration. */
|
||||
export function transformToOpenAI(envVars: Record<string, string>): Record<string, string> {
|
||||
const baseUrl = envVars['ANTHROPIC_BASE_URL'] || '';
|
||||
const apiKey = envVars['ANTHROPIC_AUTH_TOKEN'] || '';
|
||||
const model = envVars['ANTHROPIC_MODEL'] || '';
|
||||
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;
|
||||
}
|
||||
|
||||
/** Parse --key=value or --key value style args */
|
||||
export function parseFlag(args: string[], flag: string): string | undefined {
|
||||
// --flag=value style
|
||||
const eqMatch = args.find((a) => a.startsWith(`--${flag}=`));
|
||||
if (eqMatch) return eqMatch.split('=').slice(1).join('=');
|
||||
// --flag value style
|
||||
const idx = args.indexOf(`--${flag}`);
|
||||
if (idx >= 0 && idx + 1 < args.length && !args[idx + 1].startsWith('-')) {
|
||||
return args[idx + 1];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Find the first positional argument, skipping flags and their values */
|
||||
export function findProfile(args: string[], flagsWithValues: string[]): string | undefined {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg.startsWith('-')) {
|
||||
// Skip flag values: --flag=value (single token) or --flag value (two tokens)
|
||||
const flagName = arg.replace(/^--/, '').split('=')[0];
|
||||
if (!arg.includes('=') && flagsWithValues.includes(flagName) && i + 1 < args.length) {
|
||||
i++; // skip next arg (the value)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Check if a profile is a CLIProxy profile */
|
||||
function isCLIProxyProfile(name: string): boolean {
|
||||
return (CLIPROXY_PROFILES as readonly string[]).includes(name);
|
||||
}
|
||||
|
||||
/** Resolve env vars for settings-based profiles (glm, kimi, custom API profiles) */
|
||||
function resolveSettingsProfile(profileName: string): Record<string, string> | null {
|
||||
if (!isUnifiedMode()) return null;
|
||||
|
||||
const config = loadUnifiedConfig();
|
||||
if (!config) return null;
|
||||
|
||||
// Check unified config profiles section
|
||||
const profileConfig = config.profiles?.[profileName];
|
||||
if (!profileConfig) return null;
|
||||
|
||||
if (profileConfig.type !== 'api') {
|
||||
console.error(
|
||||
fail(
|
||||
`Profile '${profileName}' is type '${profileConfig.type}', not a settings-based API profile.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (profileConfig.settings) {
|
||||
const settingsPath = expandPath(profileConfig.settings);
|
||||
return loadSettingsFromFile(settingsPath);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/** Show help for env command */
|
||||
function showHelp(): void {
|
||||
console.log('');
|
||||
console.log(header('ccs env'));
|
||||
console.log('');
|
||||
console.log(' Export environment variables for third-party tool integration.');
|
||||
console.log('');
|
||||
|
||||
console.log(subheader('Usage:'));
|
||||
console.log(` ${color('ccs env', 'command')} <profile> [options]`);
|
||||
console.log('');
|
||||
|
||||
console.log(subheader('Options:'));
|
||||
console.log(
|
||||
` ${color('--format', 'command')} <fmt> Output format: openai, anthropic, raw ${dim('(default: anthropic)')}`
|
||||
);
|
||||
console.log(
|
||||
` ${color('--shell', 'command')} <sh> Shell syntax: auto, bash/zsh, fish, powershell ${dim('(default: auto)')}`
|
||||
);
|
||||
console.log(` ${color('--help, -h', 'command')} Show this help message`);
|
||||
console.log('');
|
||||
|
||||
console.log(subheader('Formats:'));
|
||||
console.log(
|
||||
` ${color('openai', 'command')} OPENAI_API_KEY, OPENAI_BASE_URL, LOCAL_ENDPOINT`
|
||||
);
|
||||
console.log(
|
||||
` ${color('anthropic', 'command')} ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_MODEL`
|
||||
);
|
||||
console.log(` ${color('raw', 'command')} All effective env vars as-is`);
|
||||
console.log('');
|
||||
|
||||
console.log(subheader('Examples:'));
|
||||
console.log(
|
||||
` $ ${color('eval $(ccs env gemini --format openai)', 'command')} ${dim('# For OpenCode/Cursor')}`
|
||||
);
|
||||
console.log(
|
||||
` $ ${color('ccs env codex --format anthropic', 'command')} ${dim('# Anthropic vars')}`
|
||||
);
|
||||
console.log(
|
||||
` $ ${color('ccs env glm --format raw', 'command')} ${dim('# All vars from settings')}`
|
||||
);
|
||||
console.log(
|
||||
` $ ${color('ccs env agy --format openai --shell fish', 'command')} ${dim('# Fish shell syntax')}`
|
||||
);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle env command
|
||||
* @param args - Command line arguments (after 'env')
|
||||
*/
|
||||
export async function handleEnvCommand(args: string[]): Promise<void> {
|
||||
await initUI();
|
||||
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse profile (first positional argument, skipping flag values)
|
||||
const flagsWithValues = ['format', 'shell'];
|
||||
const profile = findProfile(args, flagsWithValues);
|
||||
if (!profile) {
|
||||
console.error(fail('Usage: ccs env <profile> [--format openai|anthropic|raw]'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse flags
|
||||
const formatStr = parseFlag(args, 'format') || 'anthropic';
|
||||
if (!VALID_FORMATS.includes(formatStr as OutputFormat)) {
|
||||
console.error(fail(`Invalid format: ${formatStr}. Use: ${VALID_FORMATS.join(', ')}`));
|
||||
process.exit(1);
|
||||
}
|
||||
const format = formatStr as OutputFormat;
|
||||
|
||||
const shellStr = parseFlag(args, 'shell') || 'auto';
|
||||
if (!VALID_SHELL_INPUTS.includes(shellStr as (typeof VALID_SHELL_INPUTS)[number])) {
|
||||
console.error(fail(`Invalid shell: ${shellStr}. Use: ${VALID_SHELL_INPUTS.join(', ')}`));
|
||||
process.exit(1);
|
||||
}
|
||||
// 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> = {};
|
||||
|
||||
if (isCLIProxyProfile(profile)) {
|
||||
// CLIProxy profile (gemini, codex, agy, etc.)
|
||||
const provider = profile as CLIProxyProvider;
|
||||
const resolved = getEffectiveEnvVars(provider, CLIPROXY_DEFAULT_PORT);
|
||||
// Convert NodeJS.ProcessEnv to Record<string, string>
|
||||
for (const [k, v] of Object.entries(resolved)) {
|
||||
if (v !== undefined) envVars[k] = v;
|
||||
}
|
||||
} else {
|
||||
// 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(', ')));
|
||||
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;
|
||||
}
|
||||
|
||||
if (Object.keys(envVars).length === 0) {
|
||||
console.error(warn(`No env vars resolved for profile '${profile}'.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Transform to requested format
|
||||
let output: Record<string, string>;
|
||||
switch (format) {
|
||||
case 'openai':
|
||||
output = transformToOpenAI(envVars);
|
||||
break;
|
||||
case 'anthropic': {
|
||||
// Filter to only Anthropic-relevant vars
|
||||
output = {};
|
||||
for (const [k, v] of Object.entries(envVars)) {
|
||||
if (k.startsWith('ANTHROPIC_')) {
|
||||
output[k] = v;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'raw':
|
||||
output = envVars;
|
||||
break;
|
||||
}
|
||||
|
||||
// Guard: format transformation may filter out all vars
|
||||
if (Object.keys(output).filter((k) => output[k]).length === 0) {
|
||||
console.error(
|
||||
warn(`No ${format}-format vars found for profile '${profile}'. Try --format raw`)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Output shell-formatted exports to stdout
|
||||
for (const [key, value] of Object.entries(output)) {
|
||||
if (!VALID_ENV_KEY.test(key)) {
|
||||
console.error(dim(` Skipping invalid key: ${key}`));
|
||||
continue;
|
||||
}
|
||||
if (value) {
|
||||
console.log(formatExportLine(shell, key, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,6 +257,15 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
['ccs update --beta', 'Install from dev channel (unstable)'],
|
||||
]);
|
||||
|
||||
// Environment export
|
||||
printSubSection('Environment Export', [
|
||||
['ccs env <profile>', 'Export env vars for third-party tools'],
|
||||
['ccs env <profile> --format openai', 'OpenAI-compatible vars (OpenCode/Cursor)'],
|
||||
['ccs env <profile> --format anthropic', 'Anthropic vars (default)'],
|
||||
['ccs env <profile> --format raw', 'All effective env vars'],
|
||||
['ccs env <profile> --shell fish', 'Fish shell syntax'],
|
||||
]);
|
||||
|
||||
// Flags
|
||||
printSubSection('Flags', [
|
||||
['-h, --help', 'Show this help message'],
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Unit tests for env-command.ts
|
||||
*
|
||||
* Tests pure utility functions: detectShell, formatExportLine, transformToOpenAI
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'bun:test';
|
||||
import {
|
||||
detectShell,
|
||||
formatExportLine,
|
||||
transformToOpenAI,
|
||||
parseFlag,
|
||||
findProfile,
|
||||
} from '../../../src/commands/env-command';
|
||||
|
||||
describe('env-command', () => {
|
||||
describe('detectShell', () => {
|
||||
const originalShell = process.env['SHELL'];
|
||||
|
||||
afterEach(() => {
|
||||
if (originalShell !== undefined) {
|
||||
process.env['SHELL'] = originalShell;
|
||||
} else {
|
||||
delete process.env['SHELL'];
|
||||
}
|
||||
});
|
||||
|
||||
it('returns explicit bash flag', () => {
|
||||
expect(detectShell('bash')).toBe('bash');
|
||||
});
|
||||
|
||||
it('returns explicit fish flag', () => {
|
||||
expect(detectShell('fish')).toBe('fish');
|
||||
});
|
||||
|
||||
it('returns explicit powershell flag', () => {
|
||||
expect(detectShell('powershell')).toBe('powershell');
|
||||
});
|
||||
|
||||
it('auto-detects bash from SHELL=/bin/zsh', () => {
|
||||
process.env['SHELL'] = '/bin/zsh';
|
||||
expect(detectShell('auto')).toBe('bash');
|
||||
});
|
||||
|
||||
it('auto-detects bash from SHELL=/bin/bash', () => {
|
||||
process.env['SHELL'] = '/bin/bash';
|
||||
expect(detectShell()).toBe('bash');
|
||||
});
|
||||
|
||||
it('auto-detects fish from SHELL=/usr/bin/fish', () => {
|
||||
process.env['SHELL'] = '/usr/bin/fish';
|
||||
expect(detectShell('auto')).toBe('fish');
|
||||
});
|
||||
|
||||
it('defaults to bash when SHELL is empty', () => {
|
||||
process.env['SHELL'] = '';
|
||||
expect(detectShell()).toBe('bash');
|
||||
});
|
||||
|
||||
it('ignores invalid flag and auto-detects', () => {
|
||||
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', () => {
|
||||
it('formats bash export', () => {
|
||||
expect(formatExportLine('bash', 'API_KEY', 'sk-123')).toBe("export API_KEY='sk-123'");
|
||||
});
|
||||
|
||||
it('formats fish export', () => {
|
||||
expect(formatExportLine('fish', 'API_KEY', 'sk-123')).toBe("set -gx API_KEY 'sk-123'");
|
||||
});
|
||||
|
||||
it('formats powershell export', () => {
|
||||
expect(formatExportLine('powershell', 'API_KEY', 'sk-123')).toBe(
|
||||
"$env:API_KEY = 'sk-123'"
|
||||
);
|
||||
});
|
||||
|
||||
it('escapes single quotes in values', () => {
|
||||
expect(formatExportLine('bash', 'VAL', "it's here")).toBe(
|
||||
"export VAL='it'\\''s here'"
|
||||
);
|
||||
});
|
||||
|
||||
it('handles empty values', () => {
|
||||
expect(formatExportLine('bash', 'EMPTY', '')).toBe("export EMPTY=''");
|
||||
});
|
||||
|
||||
it('handles URLs with special characters', () => {
|
||||
const url = 'http://127.0.0.1:8317/api/provider/gemini';
|
||||
expect(formatExportLine('bash', 'BASE_URL', url)).toBe(`export BASE_URL='${url}'`);
|
||||
});
|
||||
|
||||
it('prevents shell injection with $() in values', () => {
|
||||
expect(formatExportLine('bash', 'TOKEN', 'safe$(whoami)')).toBe(
|
||||
"export TOKEN='safe$(whoami)'"
|
||||
);
|
||||
});
|
||||
|
||||
it('prevents backtick injection in values', () => {
|
||||
expect(formatExportLine('bash', 'TOKEN', 'safe`whoami`')).toBe(
|
||||
"export TOKEN='safe`whoami`'"
|
||||
);
|
||||
});
|
||||
|
||||
it('escapes single quotes in fish values', () => {
|
||||
expect(formatExportLine('fish', 'VAL', "it's here")).toBe("set -gx VAL 'it'\\''s here'");
|
||||
});
|
||||
|
||||
it('escapes single quotes in powershell values', () => {
|
||||
expect(formatExportLine('powershell', 'VAL', "it's here")).toBe(
|
||||
"$env:VAL = 'it''s here'"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformToOpenAI', () => {
|
||||
it('maps Anthropic vars to OpenAI format', () => {
|
||||
const result = transformToOpenAI({
|
||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/gemini',
|
||||
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
|
||||
ANTHROPIC_MODEL: 'gemini-claude-sonnet-4-5',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
OPENAI_API_KEY: 'ccs-internal-managed',
|
||||
OPENAI_BASE_URL: 'http://127.0.0.1:8317/api/provider/gemini',
|
||||
LOCAL_ENDPOINT: 'http://127.0.0.1:8317/api/provider/gemini',
|
||||
OPENAI_MODEL: 'gemini-claude-sonnet-4-5',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles missing source vars gracefully', () => {
|
||||
const result = transformToOpenAI({});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('only extracts relevant vars', () => {
|
||||
const result = transformToOpenAI({
|
||||
ANTHROPIC_BASE_URL: 'http://localhost:8317',
|
||||
ANTHROPIC_AUTH_TOKEN: 'key',
|
||||
ANTHROPIC_MAX_TOKENS: '8096',
|
||||
DISABLE_TELEMETRY: '1',
|
||||
});
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
it('omits OPENAI_MODEL when ANTHROPIC_MODEL absent', () => {
|
||||
const result = transformToOpenAI({
|
||||
ANTHROPIC_BASE_URL: 'http://localhost:8317',
|
||||
ANTHROPIC_AUTH_TOKEN: 'key',
|
||||
});
|
||||
|
||||
expect(result['OPENAI_MODEL']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseFlag', () => {
|
||||
it('parses --flag=value style', () => {
|
||||
expect(parseFlag(['--format=openai'], 'format')).toBe('openai');
|
||||
});
|
||||
|
||||
it('parses --flag value style', () => {
|
||||
expect(parseFlag(['--format', 'openai'], 'format')).toBe('openai');
|
||||
});
|
||||
|
||||
it('handles values containing =', () => {
|
||||
expect(parseFlag(['--format=key=val=ue'], 'format')).toBe('key=val=ue');
|
||||
});
|
||||
|
||||
it('returns undefined for missing flag', () => {
|
||||
expect(parseFlag(['--shell', 'bash'], 'format')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not consume next flag as value', () => {
|
||||
expect(parseFlag(['--format', '--shell'], 'format')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findProfile', () => {
|
||||
it('finds profile as first positional arg', () => {
|
||||
expect(findProfile(['gemini'], ['format', 'shell'])).toBe('gemini');
|
||||
});
|
||||
|
||||
it('skips flags before profile', () => {
|
||||
expect(findProfile(['--format', 'openai', 'gemini'], ['format', 'shell'])).toBe('gemini');
|
||||
});
|
||||
|
||||
it('skips --flag=value style flags', () => {
|
||||
expect(findProfile(['--format=openai', 'gemini'], ['format', 'shell'])).toBe('gemini');
|
||||
});
|
||||
|
||||
it('handles profile before flags', () => {
|
||||
expect(findProfile(['gemini', '--format', 'openai'], ['format', 'shell'])).toBe('gemini');
|
||||
});
|
||||
|
||||
it('returns undefined when no positional args', () => {
|
||||
expect(findProfile(['--format', 'openai', '--shell', 'fish'], ['format', 'shell'])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('skips multiple flag-value pairs', () => {
|
||||
expect(findProfile(['--format', 'openai', '--shell', 'fish', 'codex'], ['format', 'shell'])).toBe('codex');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "ui",
|
||||
|
||||
Reference in New Issue
Block a user