mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 10:19:37 +00:00
feat(cli): refresh help and shell completion UX
- add a shared command catalog for root help and completion routing - replace shell-local completion logic with a hidden __complete backend - add topic help and parity tests for router, help, and completion coverage
This commit is contained in:
+56
-132
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
Tab completion for CCS commands, subcommands, profiles, and flags.
|
Tab completion for CCS commands, subcommands, profiles, and flags.
|
||||||
|
|
||||||
|
The completion scripts are thin adapters over the hidden `ccs __complete` backend so
|
||||||
|
all supported shells stay aligned with the same command graph.
|
||||||
|
|
||||||
**Supported Shells:** Bash, Zsh, Fish, PowerShell
|
**Supported Shells:** Bash, Zsh, Fish, PowerShell
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Complete profile names (both settings-based and account-based)
|
- Complete profile names (both settings-based and account-based)
|
||||||
- Complete `ccs auth` subcommands (create, list, show, remove, default)
|
- Complete root commands, help topics, provider shortcuts, and command flags
|
||||||
- Complete flags (`--help`, `--version`, `--json`, `--verbose`, `--yes`)
|
- Complete `ccs auth` and `ccs api` lifecycle subcommands
|
||||||
- Complete profile names for auth subcommands
|
|
||||||
- Context-aware: suggests relevant options based on current command
|
- Context-aware: suggests relevant options based on current command
|
||||||
|
|
||||||
## Quick Install (Recommended)
|
## Quick Install (Recommended)
|
||||||
@@ -32,6 +34,12 @@ ccs --shell-completion --fish # Force fish
|
|||||||
ccs --shell-completion --powershell # Force PowerShell
|
ccs --shell-completion --powershell # Force PowerShell
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Help and verification:**
|
||||||
|
```bash
|
||||||
|
ccs help completion
|
||||||
|
ccs --shell-completion --force
|
||||||
|
```
|
||||||
|
|
||||||
## Manual Installation
|
## Manual Installation
|
||||||
|
|
||||||
Completion files are installed to `~/.ccs/completions/` during `npm install`.
|
Completion files are installed to `~/.ccs/completions/` during `npm install`.
|
||||||
@@ -116,188 +124,104 @@ sudo cp scripts/completion/ccs.fish /usr/share/fish/vendor_completions.d/
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ccs <TAB>
|
$ ccs <TAB>
|
||||||
auth doctor glm kimi work personal --help --version
|
auth api cliproxy config doctor docker help
|
||||||
|
|
||||||
$ ccs auth <TAB>
|
$ ccs help <TAB>
|
||||||
create list show remove default --help
|
profiles providers completion targets
|
||||||
```
|
```
|
||||||
|
|
||||||
### Profile Completion
|
### Context Completion
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ccs auth show <TAB>
|
$ ccs auth show <TAB>
|
||||||
work personal team --json
|
work personal team --json
|
||||||
|
|
||||||
$ ccs auth remove <TAB>
|
$ ccs api <TAB>
|
||||||
work personal team --yes -y
|
create discover copy export import remove
|
||||||
```
|
```
|
||||||
|
|
||||||
### Flag Completion
|
### Backend Contract
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ccs auth list <TAB>
|
$ ccs __complete --shell bash --current do
|
||||||
--verbose --json
|
doctor
|
||||||
|
docker
|
||||||
$ ccs auth show work <TAB>
|
|
||||||
--json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Completion Behavior
|
Shell adapters now call the shared CCS completion backend instead of maintaining their own
|
||||||
|
copy of the command graph. That means:
|
||||||
### Top-level (after `ccs`)
|
- top-level commands, help topics, and provider shortcuts come from CCS itself
|
||||||
- Built-in commands: `auth`, `doctor`
|
- dynamic profiles and CLIProxy variants resolve through the real config loaders
|
||||||
- Flags: `--help`, `--version`, `-h`, `-v`
|
- bash, zsh, fish, and PowerShell stay aligned with the same completion logic
|
||||||
- Settings-based profiles: from `~/.ccs/config.json`
|
|
||||||
- Account-based profiles: from `~/.ccs/profiles.json`
|
|
||||||
|
|
||||||
### After `ccs auth`
|
|
||||||
- Subcommands: `create`, `list`, `show`, `remove`, `default`
|
|
||||||
- Flags: `--help`, `-h`
|
|
||||||
|
|
||||||
### After `ccs auth <subcommand>`
|
|
||||||
- **create**: No completion (user enters new profile name)
|
|
||||||
- Flags: `--force`
|
|
||||||
- **list**: No profile completion
|
|
||||||
- Flags: `--verbose`, `--json`
|
|
||||||
- **show**: Account profiles only
|
|
||||||
- Flags: `--json`
|
|
||||||
- **remove**: Account profiles only
|
|
||||||
- Flags: `--yes`, `-y`
|
|
||||||
- **default**: Account profiles only
|
|
||||||
|
|
||||||
### After `ccs <profile>`
|
|
||||||
- No completion (Claude CLI arguments are free-form)
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Bash: Completion not working
|
### Bash
|
||||||
|
|
||||||
1. Check if bash-completion is installed:
|
1. Check if completion is loaded:
|
||||||
```bash
|
|
||||||
# macOS
|
|
||||||
brew install bash-completion
|
|
||||||
|
|
||||||
# Ubuntu/Debian
|
|
||||||
sudo apt install bash-completion
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Verify jq is installed (required for profile completion):
|
|
||||||
```bash
|
|
||||||
command -v jq
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Check if completion is loaded:
|
|
||||||
```bash
|
```bash
|
||||||
complete -p ccs
|
complete -p ccs
|
||||||
```
|
```
|
||||||
|
2. Verify the backend directly:
|
||||||
Should output:
|
```bash
|
||||||
```
|
ccs __complete --shell bash --current "" -- help
|
||||||
complete -F _ccs_completion ccs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Zsh: Completion not working
|
### Zsh
|
||||||
|
|
||||||
1. Verify completion system is enabled in `~/.zshrc`:
|
1. Verify completion system is enabled:
|
||||||
```zsh
|
```zsh
|
||||||
autoload -Uz compinit && compinit
|
autoload -Uz compinit && compinit
|
||||||
```
|
```
|
||||||
|
2. Rebuild the cache if needed:
|
||||||
2. Check if completion is loaded:
|
|
||||||
```zsh
|
|
||||||
which _ccs
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Rebuild completion cache:
|
|
||||||
```zsh
|
```zsh
|
||||||
rm ~/.zcompdump && compinit
|
rm ~/.zcompdump && compinit
|
||||||
```
|
```
|
||||||
|
3. Verify the backend directly:
|
||||||
### PowerShell: Completion not working
|
```zsh
|
||||||
|
ccs __complete --shell zsh --current "" -- help
|
||||||
1. Check PowerShell version (5.1+ required):
|
|
||||||
```powershell
|
|
||||||
$PSVersionTable.PSVersion
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Verify profile is loaded:
|
### PowerShell
|
||||||
|
|
||||||
|
1. Check that the profile exists:
|
||||||
```powershell
|
```powershell
|
||||||
Test-Path $PROFILE
|
Test-Path $PROFILE
|
||||||
```
|
```
|
||||||
|
2. Verify the backend directly:
|
||||||
3. Check if completion is registered:
|
|
||||||
```powershell
|
```powershell
|
||||||
(Get-ArgumentCompleter).CommandName | Select-String ccs
|
ccs __complete --shell powershell --current "" -- help
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fish: Completion not working
|
### Fish
|
||||||
|
|
||||||
1. Check Fish version (3.0+ required):
|
1. Verify completion file location:
|
||||||
```fish
|
|
||||||
fish --version
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Verify completion file is in the right location:
|
|
||||||
```fish
|
```fish
|
||||||
ls ~/.config/fish/completions/ccs.fish
|
ls ~/.config/fish/completions/ccs.fish
|
||||||
```
|
```
|
||||||
|
2. Test completion manually:
|
||||||
3. Verify jq is installed (required for profile completion):
|
|
||||||
```fish
|
|
||||||
which jq
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Test completion manually:
|
|
||||||
```fish
|
```fish
|
||||||
complete -C'ccs '
|
complete -C'ccs '
|
||||||
```
|
```
|
||||||
|
3. Verify the backend directly:
|
||||||
5. If needed, rebuild completions:
|
|
||||||
```fish
|
```fish
|
||||||
fish_update_completions
|
ccs __complete --shell fish --current "" -- help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Technical Details
|
## Technical Details
|
||||||
|
|
||||||
### Bash Implementation
|
- Bash uses `complete -F`
|
||||||
- Uses `complete -F` for programmable completion
|
- Zsh uses a custom `_ccs` completion function
|
||||||
- Compatible with bash 3.2+ (macOS default)
|
- Fish uses `complete -a` with backend command substitution
|
||||||
- Reads profiles dynamically using `jq`
|
- PowerShell uses `Register-ArgumentCompleter`
|
||||||
- Context-aware based on `COMP_CWORD` and `COMP_WORDS`
|
- All four shells now delegate suggestion logic to `ccs __complete`
|
||||||
|
|
||||||
### Zsh Implementation
|
|
||||||
- Uses `_arguments` and `_describe` for rich completion
|
|
||||||
- Compatible with zsh 5.0+
|
|
||||||
- Supports completion descriptions
|
|
||||||
- Context-aware using `$state` and `$words`
|
|
||||||
|
|
||||||
### PowerShell Implementation
|
|
||||||
- Uses `Register-ArgumentCompleter`
|
|
||||||
- Compatible with PowerShell 5.1+
|
|
||||||
- Reads profiles dynamically using `ConvertFrom-Json`
|
|
||||||
- Provides `CompletionResult` objects
|
|
||||||
|
|
||||||
### Fish Implementation
|
|
||||||
- Uses declarative `complete` command
|
|
||||||
- Compatible with Fish 3.0+
|
|
||||||
- Automatic loading from `~/.config/fish/completions/`
|
|
||||||
- Helper functions for dynamic profile loading
|
|
||||||
- Context-aware using `__fish_seen_subcommand_from`
|
|
||||||
- No manual sourcing required
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- **jq**: Required for reading profiles from JSON files
|
|
||||||
- Install: `brew install jq` (macOS) or `apt install jq` (Ubuntu)
|
|
||||||
- Already required by CCS core functionality
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
When adding new commands or flags:
|
When adding or changing command surfaces:
|
||||||
1. Update all four completion scripts (bash, zsh, fish, PowerShell)
|
1. Update the shared TypeScript command/completion catalog
|
||||||
2. Test on each shell
|
2. Run `bun run validate`
|
||||||
3. Update this README with new completion examples
|
3. Smoke-check at least one installed shell adapter plus the backend directly
|
||||||
4. Maintain cross-shell parity
|
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
|
|||||||
+28
-196
@@ -1,210 +1,42 @@
|
|||||||
# Bash completion for CCS (Claude Code Switch)
|
# Bash completion for CCS (Claude Code Switch)
|
||||||
# Compatible with bash 3.2+
|
# Compatible with bash 3.2+
|
||||||
#
|
|
||||||
# Installation:
|
|
||||||
# Add to ~/.bashrc or ~/.bash_profile:
|
|
||||||
# source /path/to/ccs/scripts/completion/ccs.bash
|
|
||||||
#
|
|
||||||
# Or install system-wide (requires sudo):
|
|
||||||
# sudo cp scripts/completion/ccs.bash /etc/bash_completion.d/ccs
|
|
||||||
|
|
||||||
_ccs_completion() {
|
_ccs_completion() {
|
||||||
local cur prev words cword
|
local cur
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
|
|
||||||
# Get current word and previous word
|
|
||||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
||||||
|
|
||||||
# Top-level completion (first argument)
|
local tokens_before_current=()
|
||||||
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
if [[ ${COMP_CWORD} -gt 1 ]]; then
|
||||||
local commands="auth api cliproxy config doctor env persist sync update"
|
tokens_before_current=("${COMP_WORDS[@]:1:COMP_CWORD-1}")
|
||||||
local flags="--help --version --shell-completion -h -v -sc"
|
|
||||||
local cliproxy_profiles="gemini codex agy qwen"
|
|
||||||
local profiles=""
|
|
||||||
|
|
||||||
# Add profiles from config.json (settings-based profiles)
|
|
||||||
if [[ -f ~/.ccs/config.json ]]; then
|
|
||||||
profiles="$profiles $(jq -r '.profiles | keys[]' ~/.ccs/config.json 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add profiles from profiles.json (account-based profiles)
|
|
||||||
if [[ -f ~/.ccs/profiles.json ]]; then
|
|
||||||
profiles="$profiles $(jq -r '.profiles | keys[]' ~/.ccs/profiles.json 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add cliproxy variants from config.json
|
|
||||||
if [[ -f ~/.ccs/config.json ]]; then
|
|
||||||
profiles="$profiles $(jq -r '.cliproxy | keys[]' ~/.ccs/config.json 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Combine all options
|
|
||||||
local opts="$commands $flags $cliproxy_profiles $profiles"
|
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# CLIProxy provider flags (gemini, codex, agy, qwen)
|
while IFS= read -r line; do
|
||||||
if [[ ${COMP_WORDS[1]} =~ ^(gemini|codex|agy|qwen)$ ]]; then
|
[[ -n "${line}" ]] && COMPREPLY+=("${line}")
|
||||||
local provider_flags="--auth --config --logout --headless --help -h"
|
done < <(__ccs_completion_run "${cur}" "${tokens_before_current[@]}")
|
||||||
COMPREPLY=( $(compgen -W "${provider_flags}" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# auth subcommands
|
|
||||||
if [[ ${prev} == "auth" ]]; then
|
|
||||||
local auth_commands="create list show remove default --help -h"
|
|
||||||
COMPREPLY=( $(compgen -W "${auth_commands}" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# api subcommands
|
|
||||||
if [[ ${prev} == "api" ]]; then
|
|
||||||
local api_commands="create list remove --help -h"
|
|
||||||
COMPREPLY=( $(compgen -W "${api_commands}" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# cliproxy subcommands
|
|
||||||
if [[ ${prev} == "cliproxy" ]]; then
|
|
||||||
local cliproxy_commands="create list remove --install --latest --help -h"
|
|
||||||
COMPREPLY=( $(compgen -W "${cliproxy_commands}" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Completion for cliproxy subcommands
|
|
||||||
if [[ ${COMP_WORDS[1]} == "cliproxy" ]]; then
|
|
||||||
case "${prev}" in
|
|
||||||
remove|delete|rm)
|
|
||||||
# Complete with cliproxy variant names
|
|
||||||
if [[ -f ~/.ccs/config.json ]]; then
|
|
||||||
local variants=$(jq -r '.cliproxy | keys[]' ~/.ccs/config.json 2>/dev/null || true)
|
|
||||||
COMPREPLY=( $(compgen -W "${variants}" -- ${cur}) )
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
create)
|
|
||||||
# Complete with create flags
|
|
||||||
COMPREPLY=( $(compgen -W "--provider --model --force --yes -y" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
--provider)
|
|
||||||
# Complete with provider names
|
|
||||||
COMPREPLY=( $(compgen -W "gemini codex agy qwen" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
list|ls)
|
|
||||||
# No flags for list
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
--install)
|
|
||||||
# User enters version number
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Completion for api subcommands
|
|
||||||
if [[ ${COMP_WORDS[1]} == "api" ]]; then
|
|
||||||
case "${prev}" in
|
|
||||||
remove|delete|rm)
|
|
||||||
# Complete with settings profile names
|
|
||||||
if [[ -f ~/.ccs/config.json ]]; then
|
|
||||||
local profiles=$(jq -r '.profiles | keys[]' ~/.ccs/config.json 2>/dev/null || true)
|
|
||||||
COMPREPLY=( $(compgen -W "${profiles}" -- ${cur}) )
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
create)
|
|
||||||
# Complete with create flags
|
|
||||||
COMPREPLY=( $(compgen -W "--base-url --api-key --model --force --yes -y" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
list)
|
|
||||||
# No flags for list
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Completion for auth subcommands that need profile names
|
|
||||||
if [[ ${COMP_WORDS[1]} == "auth" ]]; then
|
|
||||||
case "${prev}" in
|
|
||||||
show|remove|default)
|
|
||||||
# Complete with account profile names only
|
|
||||||
if [[ -f ~/.ccs/profiles.json ]]; then
|
|
||||||
local profiles=$(jq -r '.profiles | keys[]' ~/.ccs/profiles.json 2>/dev/null || true)
|
|
||||||
COMPREPLY=( $(compgen -W "${profiles}" -- ${cur}) )
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
create)
|
|
||||||
# Complete with create flags
|
|
||||||
COMPREPLY=( $(compgen -W "--force" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
list)
|
|
||||||
# Complete with list flags
|
|
||||||
COMPREPLY=( $(compgen -W "--verbose --json" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
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 --ide --help -h gemini codex agy qwen iflow kiro ghcp claude default"
|
|
||||||
if [[ -f ~/.ccs/config.json ]]; then
|
|
||||||
env_opts="$env_opts $(jq -r '.profiles | keys[]' ~/.ccs/config.json 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
if [[ -f ~/.ccs/profiles.json ]]; then
|
|
||||||
env_opts="$env_opts $(jq -r '.profiles | keys[]' ~/.ccs/profiles.json 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
COMPREPLY=( $(compgen -W "${env_opts}" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
--format)
|
|
||||||
COMPREPLY=( $(compgen -W "openai anthropic raw claude-extension" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
--shell)
|
|
||||||
COMPREPLY=( $(compgen -W "auto bash zsh fish powershell" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
--ide)
|
|
||||||
COMPREPLY=( $(compgen -W "vscode cursor windsurf" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
COMPREPLY=( $(compgen -W "--format --shell --ide --help -h" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Flags for doctor command
|
|
||||||
if [[ ${COMP_WORDS[1]} == "doctor" ]]; then
|
|
||||||
COMPREPLY=( $(compgen -W "--help -h" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Flags for update command
|
|
||||||
if [[ ${COMP_WORDS[1]} == "update" ]]; then
|
|
||||||
COMPREPLY=( $(compgen -W "--force --beta --dev --help -h" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Flags for shell-completion command
|
|
||||||
if [[ ${prev} == "--shell-completion" || ${prev} == "-sc" ]]; then
|
|
||||||
COMPREPLY=( $(compgen -W "--bash --zsh --fish --powershell" -- ${cur}) )
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Register completion function
|
__ccs_completion_run() {
|
||||||
|
local current="$1"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
local script_dir repo_root repo_cli
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
repo_root="$(cd "${script_dir}/../.." && pwd)"
|
||||||
|
repo_cli="${repo_root}/dist/ccs.js"
|
||||||
|
if [[ ! -f "${repo_cli}" ]]; then
|
||||||
|
repo_cli="${repo_root}/bin/ccs.js"
|
||||||
|
fi
|
||||||
|
if [[ -f "${repo_cli}" ]]; then
|
||||||
|
node "${repo_cli}" __complete --shell bash --current "${current}" -- "$@" 2>/dev/null
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v ccs >/dev/null 2>&1; then
|
||||||
|
ccs __complete --shell bash --current "${current}" -- "$@" 2>/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
complete -F _ccs_completion ccs
|
complete -F _ccs_completion ccs
|
||||||
|
|||||||
+21
-250
@@ -1,256 +1,27 @@
|
|||||||
# Fish completion for CCS (Claude Code Switch)
|
# Fish completion for CCS (Claude Code Switch)
|
||||||
# Compatible with fish 3.0+
|
|
||||||
#
|
|
||||||
# Features:
|
|
||||||
# - Categorized completions with [cmd], [proxy], [model], and [account] prefixes
|
|
||||||
# - Dynamic profile loading from config.json and profiles.json
|
|
||||||
# - Context-aware subcommand completion
|
|
||||||
#
|
|
||||||
# Installation:
|
|
||||||
# Copy to ~/.config/fish/completions/:
|
|
||||||
# mkdir -p ~/.config/fish/completions
|
|
||||||
# cp scripts/completion/ccs.fish ~/.config/fish/completions/
|
|
||||||
#
|
|
||||||
# Fish will automatically load completions from this directory.
|
|
||||||
# No need to source or reload - completions are loaded on demand.
|
|
||||||
|
|
||||||
# Helper function to get settings profiles
|
function __fish_ccs_complete
|
||||||
function __fish_ccs_get_settings_profiles
|
set -l tokens_before_current (commandline -opc)
|
||||||
set -l config_path ~/.ccs/config.json
|
if test (count $tokens_before_current) -gt 0
|
||||||
|
set -e tokens_before_current[1]
|
||||||
|
end
|
||||||
|
|
||||||
if test -f $config_path
|
set -l current (commandline -ct)
|
||||||
jq -r '.profiles | keys[]' $config_path 2>/dev/null
|
|
||||||
|
set -l script_file (status filename)
|
||||||
|
set -l repo_root (realpath (dirname $script_file)/../.. 2>/dev/null)
|
||||||
|
set -l repo_cli "$repo_root/dist/ccs.js"
|
||||||
|
if not test -f "$repo_cli"
|
||||||
|
set repo_cli "$repo_root/bin/ccs.js"
|
||||||
|
end
|
||||||
|
if test -f "$repo_cli"
|
||||||
|
node "$repo_cli" __complete --shell fish --current "$current" -- $tokens_before_current 2>/dev/null
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if command -sq ccs
|
||||||
|
ccs __complete --shell fish --current "$current" -- $tokens_before_current 2>/dev/null
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper function to get custom/unknown settings profiles
|
complete -c ccs -f -a "(__fish_ccs_complete)"
|
||||||
function __fish_ccs_get_custom_settings_profiles
|
|
||||||
set -l config_path ~/.ccs/config.json
|
|
||||||
set -l known_profiles default glm kimi
|
|
||||||
|
|
||||||
if test -f $config_path
|
|
||||||
set -l all_profiles (jq -r '.profiles | keys[]' $config_path 2>/dev/null)
|
|
||||||
|
|
||||||
for profile in $all_profiles
|
|
||||||
if not contains $profile $known_profiles
|
|
||||||
echo $profile
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to get cliproxy variants
|
|
||||||
function __fish_ccs_get_cliproxy_variants
|
|
||||||
set -l config_path ~/.ccs/config.json
|
|
||||||
|
|
||||||
if test -f $config_path
|
|
||||||
jq -r '.cliproxy | keys[]' $config_path 2>/dev/null
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to get profiles with all types
|
|
||||||
function __fish_ccs_get_profiles
|
|
||||||
__fish_ccs_get_settings_profiles
|
|
||||||
__fish_ccs_get_account_profiles
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to get account profiles only
|
|
||||||
function __fish_ccs_get_account_profiles
|
|
||||||
set -l profiles_path ~/.ccs/profiles.json
|
|
||||||
|
|
||||||
if test -f $profiles_path
|
|
||||||
jq -r '.profiles | keys[]' $profiles_path 2>/dev/null
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to check if we're in auth context
|
|
||||||
function __fish_ccs_using_auth
|
|
||||||
__fish_seen_subcommand_from auth
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to check specific auth subcommand
|
|
||||||
function __fish_ccs_using_auth_subcommand
|
|
||||||
set -l subcommand $argv[1]
|
|
||||||
__fish_ccs_using_auth; and __fish_seen_subcommand_from $subcommand
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to check if we're in api context
|
|
||||||
function __fish_ccs_using_api
|
|
||||||
__fish_seen_subcommand_from api
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to check specific api subcommand
|
|
||||||
function __fish_ccs_using_api_subcommand
|
|
||||||
set -l subcommand $argv[1]
|
|
||||||
__fish_ccs_using_api; and __fish_seen_subcommand_from $subcommand
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to check if we're in cliproxy context
|
|
||||||
function __fish_ccs_using_cliproxy
|
|
||||||
__fish_seen_subcommand_from cliproxy
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to check specific cliproxy subcommand
|
|
||||||
function __fish_ccs_using_cliproxy_subcommand
|
|
||||||
set -l subcommand $argv[1]
|
|
||||||
__fish_ccs_using_cliproxy; and __fish_seen_subcommand_from $subcommand
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to check if we're in profile context
|
|
||||||
function __fish_ccs_using_profile
|
|
||||||
__fish_seen_subcommand_from profile
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to check specific profile subcommand
|
|
||||||
function __fish_ccs_using_profile_subcommand
|
|
||||||
set -l subcommand $argv[1]
|
|
||||||
__fish_ccs_using_profile; and __fish_seen_subcommand_from $subcommand
|
|
||||||
end
|
|
||||||
|
|
||||||
# Helper function to check if we're using a CLIProxy provider
|
|
||||||
function __fish_ccs_using_provider
|
|
||||||
__fish_seen_subcommand_from gemini codex agy qwen
|
|
||||||
end
|
|
||||||
|
|
||||||
# Disable file completion for ccs
|
|
||||||
complete -c ccs -f
|
|
||||||
|
|
||||||
# Top-level flags
|
|
||||||
complete -c ccs -s h -l help -d 'Show help message'
|
|
||||||
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 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 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 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 '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 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 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 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'
|
|
||||||
complete -c ccs -n '__fish_seen_argument -l shell-completion; or __fish_seen_argument -s sc' -l zsh -d 'Install for zsh'
|
|
||||||
complete -c ccs -n '__fish_seen_argument -l shell-completion; or __fish_seen_argument -s sc' -l fish -d 'Install for fish'
|
|
||||||
complete -c ccs -n '__fish_seen_argument -l shell-completion; or __fish_seen_argument -s sc' -l powershell -d 'Install for PowerShell'
|
|
||||||
|
|
||||||
# CLIProxy provider flags (gemini, codex, agy, qwen)
|
|
||||||
complete -c ccs -n '__fish_ccs_using_provider' -l auth -d 'Authenticate only'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_provider' -l config -d 'Change model configuration'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_provider' -l logout -d 'Clear authentication'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_provider' -l headless -d 'Headless auth (for SSH)'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_provider' -s h -l help -d 'Show help'
|
|
||||||
|
|
||||||
# update command flags
|
|
||||||
complete -c ccs -n '__fish_seen_subcommand_from update' -l force -d 'Force reinstall current version'
|
|
||||||
complete -c ccs -n '__fish_seen_subcommand_from update' -l beta -d 'Install from dev channel'
|
|
||||||
complete -c ccs -n '__fish_seen_subcommand_from update' -l dev -d 'Install from dev channel'
|
|
||||||
complete -c ccs -n '__fish_seen_subcommand_from update' -s h -l help -d 'Show help'
|
|
||||||
|
|
||||||
# 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
|
|
||||||
# ============================================================================
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth; and not __fish_seen_subcommand_from create list show remove default' -a 'create' -d 'Create new profile and login'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth; and not __fish_seen_subcommand_from create list show remove default' -a 'list' -d 'List all saved profiles'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth; and not __fish_seen_subcommand_from create list show remove default' -a 'show' -d 'Show profile details'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth; and not __fish_seen_subcommand_from create list show remove default' -a 'remove' -d 'Remove saved profile'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth; and not __fish_seen_subcommand_from create list show remove default' -a 'default' -d 'Set default profile'
|
|
||||||
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth' -s h -l help -d 'Show help for auth commands'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth_subcommand create' -l force -d 'Allow overwriting existing profile'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth_subcommand list' -l verbose -d 'Show additional details'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth_subcommand list' -l json -d 'Output in JSON format'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth_subcommand show' -a '(__fish_ccs_get_account_profiles)' -d 'Account profile'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth_subcommand show' -l json -d 'Output in JSON format'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth_subcommand remove' -a '(__fish_ccs_get_account_profiles)' -d 'Account profile'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth_subcommand remove' -l yes -d 'Skip confirmation prompts'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth_subcommand remove' -s y -d 'Skip confirmation prompts'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_auth_subcommand default' -a '(__fish_ccs_get_account_profiles)' -d 'Account profile'
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# api subcommands
|
|
||||||
# ============================================================================
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api; and not __fish_seen_subcommand_from create list remove' -a 'create' -d 'Create new API profile'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api; and not __fish_seen_subcommand_from create list remove' -a 'list' -d 'List all API profiles'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api; and not __fish_seen_subcommand_from create list remove' -a 'remove' -d 'Remove an API profile'
|
|
||||||
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api' -s h -l help -d 'Show help for api commands'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api_subcommand create' -l base-url -d 'API base URL'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api_subcommand create' -l api-key -d 'API key'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api_subcommand create' -l model -d 'Default model'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api_subcommand create' -l force -d 'Overwrite existing profile'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api_subcommand create' -l yes -d 'Skip prompts'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api_subcommand create' -s y -d 'Skip prompts'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api_subcommand remove' -a '(__fish_ccs_get_settings_profiles)' -d 'Settings profile'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api_subcommand remove' -l yes -d 'Skip confirmation'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_api_subcommand remove' -s y -d 'Skip confirmation'
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# cliproxy subcommands
|
|
||||||
# ============================================================================
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy; and not __fish_seen_subcommand_from create list remove' -a 'create' -d 'Create new CLIProxy variant'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy; and not __fish_seen_subcommand_from create list remove' -a 'list' -d 'List all CLIProxy variants'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy; and not __fish_seen_subcommand_from create list remove' -a 'remove' -d 'Remove a CLIProxy variant'
|
|
||||||
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy' -s h -l help -d 'Show help for cliproxy commands'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy' -l install -d 'Install specific version'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy' -l latest -d 'Install latest version'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy_subcommand create' -l provider -d 'Provider name'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy_subcommand create; and __fish_seen_argument -l provider' -a 'gemini codex agy qwen' -d 'Provider'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy_subcommand create' -l model -d 'Model name'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy_subcommand create' -l force -d 'Overwrite existing variant'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy_subcommand create' -l yes -d 'Skip prompts'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy_subcommand create' -s y -d 'Skip prompts'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy_subcommand remove' -a '(__fish_ccs_get_cliproxy_variants)' -d 'CLIProxy variant'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy_subcommand remove' -l yes -d 'Skip confirmation'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_cliproxy_subcommand remove' -s y -d 'Skip confirmation'
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# profile subcommands (legacy - redirects to api)
|
|
||||||
# ============================================================================
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile; and not __fish_seen_subcommand_from create list remove' -a 'create' -d 'Create new API profile'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile; and not __fish_seen_subcommand_from create list remove' -a 'list' -d 'List all profiles'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile; and not __fish_seen_subcommand_from create list remove' -a 'remove' -d 'Remove a profile'
|
|
||||||
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile' -s h -l help -d 'Show help for profile commands'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile_subcommand create' -l base-url -d 'API base URL'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile_subcommand create' -l api-key -d 'API key'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile_subcommand create' -l model -d 'Default model'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile_subcommand create' -l force -d 'Overwrite existing profile'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile_subcommand create' -l yes -d 'Skip prompts'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile_subcommand create' -s y -d 'Skip prompts'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile_subcommand remove' -a '(__fish_ccs_get_settings_profiles)' -d 'Settings profile'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile_subcommand remove' -l yes -d 'Skip confirmation'
|
|
||||||
complete -c ccs -n '__fish_ccs_using_profile_subcommand remove' -s y -d 'Skip confirmation'
|
|
||||||
|
|||||||
+42
-432
@@ -1,446 +1,56 @@
|
|||||||
# PowerShell completion for CCS (Claude Code Switch)
|
# PowerShell completion for CCS (Claude Code Switch)
|
||||||
# Compatible with PowerShell 5.1+
|
|
||||||
#
|
function Invoke-CcsCompletionBackend {
|
||||||
# Installation:
|
param(
|
||||||
# Add to your PowerShell profile ($PROFILE):
|
[string]$CurrentWord,
|
||||||
# . /path/to/ccs/scripts/completion/ccs.ps1
|
[string[]]$TokensBeforeCurrent
|
||||||
#
|
)
|
||||||
# Or install for current user:
|
|
||||||
# Copy-Item scripts/completion/ccs.ps1 ~\Documents\PowerShell\Scripts\
|
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||||
# Add to profile: . ~\Documents\PowerShell\Scripts\ccs.ps1
|
$repoCli = Join-Path $repoRoot 'dist\ccs.js'
|
||||||
|
if (-not (Test-Path $repoCli)) {
|
||||||
|
$repoCli = Join-Path $repoRoot 'bin\ccs.js'
|
||||||
|
}
|
||||||
|
if (Test-Path $repoCli) {
|
||||||
|
& node $repoCli __complete --shell powershell --current $CurrentWord -- @TokensBeforeCurrent 2>$null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Get-Command ccs -ErrorAction SilentlyContinue) {
|
||||||
|
& ccs __complete --shell powershell --current $CurrentWord -- @TokensBeforeCurrent 2>$null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Register-ArgumentCompleter -CommandName ccs -ScriptBlock {
|
Register-ArgumentCompleter -CommandName ccs -ScriptBlock {
|
||||||
param($commandName, $wordToComplete, $commandAst, $fakeBoundParameters)
|
param($commandName, $wordToComplete, $commandAst, $fakeBoundParameters)
|
||||||
|
|
||||||
$commands = @('auth', 'api', 'cliproxy', 'doctor', 'env', 'sync', 'update', '--help', '--version', '--shell-completion', '-h', '-v', '-sc')
|
$commandElements = @($commandAst.CommandElements | ForEach-Object { $_.Extent.Text })
|
||||||
$cliproxyProfiles = @('gemini', 'codex', 'agy', 'qwen', 'iflow', 'kiro', 'ghcp', 'claude')
|
$tokensBeforeCurrent = @()
|
||||||
$authCommands = @('create', 'list', 'show', 'remove', 'default', '--help', '-h')
|
if ($commandElements.Count -gt 1) {
|
||||||
$apiCommands = @('create', 'list', 'remove', '--help', '-h')
|
$tokensBeforeCurrent = $commandElements[1..($commandElements.Count - 1)]
|
||||||
$cliproxyCommands = @('create', 'list', 'remove', '--install', '--latest', '--help', '-h')
|
if ($tokensBeforeCurrent.Count -gt 0 -and $tokensBeforeCurrent[-1] -eq $wordToComplete) {
|
||||||
$apiCreateFlags = @('--base-url', '--api-key', '--model', '--force', '--yes', '-y')
|
$tokensBeforeCurrent = if ($tokensBeforeCurrent.Count -gt 1) {
|
||||||
$cliproxyCreateFlags = @('--provider', '--model', '--force', '--yes', '-y')
|
$tokensBeforeCurrent[0..($tokensBeforeCurrent.Count - 2)]
|
||||||
$providerFlags = @('--auth', '--config', '--logout', '--headless', '--help', '-h')
|
} else {
|
||||||
$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')
|
|
||||||
$showFlags = @('--json')
|
|
||||||
$providers = @('gemini', 'codex', 'agy', 'qwen')
|
|
||||||
|
|
||||||
# Get current position in command
|
|
||||||
$words = $commandAst.ToString() -split '\s+' | Where-Object { $_ -ne '' }
|
|
||||||
$position = $words.Count
|
|
||||||
|
|
||||||
# Helper function to get profiles
|
|
||||||
function Get-CcsProfiles {
|
|
||||||
param([string]$Type = 'all')
|
|
||||||
|
|
||||||
$profiles = @()
|
|
||||||
|
|
||||||
# Settings-based profiles
|
|
||||||
if ($Type -in @('all', 'settings')) {
|
|
||||||
$configPath = "$env:USERPROFILE\.ccs\config.json"
|
|
||||||
if (Test-Path $configPath) {
|
|
||||||
try {
|
|
||||||
$config = Get-Content $configPath -Raw | ConvertFrom-Json
|
|
||||||
$profiles += $config.profiles.PSObject.Properties.Name
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Account-based profiles
|
|
||||||
if ($Type -in @('all', 'account')) {
|
|
||||||
$profilesPath = "$env:USERPROFILE\.ccs\profiles.json"
|
|
||||||
if (Test-Path $profilesPath) {
|
|
||||||
try {
|
|
||||||
$data = Get-Content $profilesPath -Raw | ConvertFrom-Json
|
|
||||||
$profiles += $data.profiles.PSObject.Properties.Name
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# CLIProxy variants
|
|
||||||
if ($Type -in @('all', 'cliproxy')) {
|
|
||||||
$configPath = "$env:USERPROFILE\.ccs\config.json"
|
|
||||||
if (Test-Path $configPath) {
|
|
||||||
try {
|
|
||||||
$config = Get-Content $configPath -Raw | ConvertFrom-Json
|
|
||||||
if ($config.cliproxy) {
|
|
||||||
$profiles += $config.cliproxy.PSObject.Properties.Name
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $profiles | Sort-Object -Unique
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Top-level completion
|
foreach ($line in Invoke-CcsCompletionBackend -CurrentWord $wordToComplete -TokensBeforeCurrent $tokensBeforeCurrent) {
|
||||||
if ($position -eq 2) {
|
if ([string]::IsNullOrWhiteSpace($line)) {
|
||||||
$allOptions = $commands + $cliproxyProfiles + (Get-CcsProfiles)
|
continue
|
||||||
$allOptions | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# shell-completion flag completion
|
$parts = $line -split "`t", 2
|
||||||
if ($words[1] -eq '--shell-completion' -or $words[1] -eq '-sc') {
|
$value = $parts[0]
|
||||||
if ($position -eq 3) {
|
$description = if ($parts.Count -gt 1) { $parts[1] } else { $parts[0] }
|
||||||
$shellCompletionFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# CLIProxy provider flags (gemini, codex, agy, qwen)
|
[System.Management.Automation.CompletionResult]::new(
|
||||||
if ($words[1] -in $cliproxyProfiles) {
|
$value,
|
||||||
$providerFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
$value,
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
'ParameterValue',
|
||||||
$_,
|
$description
|
||||||
$_,
|
)
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# update command completion
|
|
||||||
if ($words[1] -eq 'update') {
|
|
||||||
$updateFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
$authCommands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} elseif ($position -eq 4) {
|
|
||||||
switch ($words[2]) {
|
|
||||||
'show' {
|
|
||||||
$options = (Get-CcsProfiles -Type account) + $showFlags
|
|
||||||
$options | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'remove' {
|
|
||||||
$options = (Get-CcsProfiles -Type account) + $removeFlags
|
|
||||||
$options | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'default' {
|
|
||||||
Get-CcsProfiles -Type account | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'list' {
|
|
||||||
$listFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'create' {
|
|
||||||
@('--force') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif ($position -eq 5) {
|
|
||||||
switch ($words[2]) {
|
|
||||||
'show' {
|
|
||||||
$showFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'remove' {
|
|
||||||
$removeFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# api subcommand completion
|
|
||||||
if ($words[1] -eq 'api') {
|
|
||||||
if ($position -eq 3) {
|
|
||||||
$apiCommands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} elseif ($position -eq 4) {
|
|
||||||
switch ($words[2]) {
|
|
||||||
'remove' {
|
|
||||||
$options = (Get-CcsProfiles -Type settings) + $removeFlags
|
|
||||||
$options | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'create' {
|
|
||||||
$apiCreateFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif ($position -eq 5) {
|
|
||||||
switch ($words[2]) {
|
|
||||||
'remove' {
|
|
||||||
$removeFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# cliproxy subcommand completion
|
|
||||||
if ($words[1] -eq 'cliproxy') {
|
|
||||||
if ($position -eq 3) {
|
|
||||||
$cliproxyCommands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} elseif ($position -eq 4) {
|
|
||||||
switch ($words[2]) {
|
|
||||||
'remove' {
|
|
||||||
$options = (Get-CcsProfiles -Type cliproxy) + $removeFlags
|
|
||||||
$options | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'create' {
|
|
||||||
$cliproxyCreateFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif ($position -eq 5) {
|
|
||||||
switch ($words[2]) {
|
|
||||||
'remove' {
|
|
||||||
$removeFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'create' {
|
|
||||||
# After --provider, complete with provider names
|
|
||||||
if ($words[3] -eq '--provider') {
|
|
||||||
$providers | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# profile subcommand completion (legacy)
|
|
||||||
if ($words[1] -eq 'profile') {
|
|
||||||
if ($position -eq 3) {
|
|
||||||
@('create', 'list', 'remove', '--help', '-h') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} elseif ($position -eq 4) {
|
|
||||||
switch ($words[2]) {
|
|
||||||
'remove' {
|
|
||||||
$options = (Get-CcsProfiles -Type settings) + $removeFlags
|
|
||||||
$options | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'create' {
|
|
||||||
$apiCreateFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif ($position -eq 5) {
|
|
||||||
switch ($words[2]) {
|
|
||||||
'remove' {
|
|
||||||
$removeFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$_,
|
|
||||||
$_,
|
|
||||||
'ParameterValue',
|
|
||||||
$_
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-304
@@ -1,319 +1,41 @@
|
|||||||
#compdef ccs
|
#compdef ccs
|
||||||
|
|
||||||
# Zsh completion for CCS (Claude Code Switch)
|
# Zsh completion for CCS (Claude Code Switch)
|
||||||
# Compatible with zsh 5.0+
|
|
||||||
#
|
|
||||||
# Installation:
|
|
||||||
# Add to ~/.zshrc:
|
|
||||||
# fpath=(~/.zsh/completion $fpath)
|
|
||||||
# autoload -Uz compinit && compinit
|
|
||||||
# source /path/to/ccs/scripts/completion/ccs.zsh
|
|
||||||
#
|
|
||||||
# Or install system-wide:
|
|
||||||
# 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|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|kimi|[^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;32=2\;37'
|
|
||||||
zstyle ':completion:*:*:ccs:*:account-profiles' list-colors '=(#b)([^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;33=2\;37'
|
|
||||||
zstyle ':completion:*:*:ccs:*' group-name ''
|
|
||||||
zstyle ':completion:*:*:ccs:*:descriptions' format $'\n%B%F{yellow}── %d ──%f%b'
|
|
||||||
zstyle ':completion:*:*:ccs:*' list-separator ' -- '
|
|
||||||
zstyle ':completion:*:*:ccs:*' list-rows-first true
|
|
||||||
zstyle ':completion:*:*:ccs:*' menu select
|
|
||||||
|
|
||||||
_ccs() {
|
_ccs() {
|
||||||
local -a commands proxy_profiles settings_profiles_described account_profiles_described cliproxy_variants_described
|
local current
|
||||||
local curcontext="$curcontext" state line
|
current="${words[CURRENT]}"
|
||||||
typeset -A opt_args
|
|
||||||
|
|
||||||
# Define top-level commands
|
local -a tokens_before_current
|
||||||
commands=(
|
if (( CURRENT > 2 )); then
|
||||||
'auth:Manage multiple Claude accounts'
|
tokens_before_current=("${words[@]:2:$((CURRENT-2))}")
|
||||||
'api:Manage API profiles (create/remove)'
|
else
|
||||||
'cliproxy:Manage CLIProxy variants and binary'
|
tokens_before_current=()
|
||||||
'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'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Define CLIProxy hardcoded profiles (OAuth providers)
|
|
||||||
proxy_profiles=(
|
|
||||||
'gemini:Google Gemini (OAuth)'
|
|
||||||
'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
|
|
||||||
local -A profile_descriptions
|
|
||||||
profile_descriptions=(
|
|
||||||
'default' 'Default Claude Sonnet 4.5'
|
|
||||||
'glm' 'GLM-4.6 (cost-optimized)'
|
|
||||||
'kimi' 'Kimi for Coding (long-context)'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Load settings-based profiles from config.json
|
|
||||||
if [[ -f ~/.ccs/config.json ]]; then
|
|
||||||
local -a raw_settings_profiles
|
|
||||||
raw_settings_profiles=(${(f)"$(jq -r '.profiles | keys[]' ~/.ccs/config.json 2>/dev/null)"})
|
|
||||||
|
|
||||||
for profile in $raw_settings_profiles; do
|
|
||||||
local desc="${profile_descriptions[$profile]:-Settings-based profile}"
|
|
||||||
settings_profiles_described+=("${profile}:${desc}")
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Load account-based profiles from profiles.json
|
local -a suggestions
|
||||||
if [[ -f ~/.ccs/profiles.json ]]; then
|
suggestions=("${(@f)$(__ccs_completion_run "${current}" "${tokens_before_current[@]}")}")
|
||||||
local -a raw_account_profiles
|
compadd -- "${suggestions[@]}"
|
||||||
raw_account_profiles=(${(f)"$(jq -r '.profiles | keys[]' ~/.ccs/profiles.json 2>/dev/null)"})
|
|
||||||
|
|
||||||
for profile in $raw_account_profiles; do
|
|
||||||
account_profiles_described+=("${profile}:Account-based profile")
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Load cliproxy variants from config.json
|
|
||||||
if [[ -f ~/.ccs/config.json ]]; then
|
|
||||||
local -a raw_cliproxy_variants
|
|
||||||
raw_cliproxy_variants=(${(f)"$(jq -r '.cliproxy | keys[]' ~/.ccs/config.json 2>/dev/null)"})
|
|
||||||
|
|
||||||
for variant in $raw_cliproxy_variants; do
|
|
||||||
cliproxy_variants_described+=("${variant}:CLIProxy variant")
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
_arguments -C \
|
|
||||||
'(- *)'{-h,--help}'[Show help message]' \
|
|
||||||
'(- *)'{-v,--version}'[Show version information]' \
|
|
||||||
'(- *)'{-sc,--shell-completion}'[Install shell completion]' \
|
|
||||||
'1: :->command' \
|
|
||||||
'*:: :->args'
|
|
||||||
|
|
||||||
case $state in
|
|
||||||
command)
|
|
||||||
_describe -t commands 'commands' commands
|
|
||||||
_describe -t proxy-profiles 'CLIProxy profiles' proxy_profiles
|
|
||||||
_describe -t model-profiles 'model profiles' settings_profiles_described
|
|
||||||
_describe -t account-profiles 'account profiles' account_profiles_described
|
|
||||||
_describe -t cliproxy-variants 'CLIProxy variants' cliproxy_variants_described
|
|
||||||
;;
|
|
||||||
|
|
||||||
args)
|
|
||||||
case $words[1] in
|
|
||||||
auth)
|
|
||||||
_ccs_auth
|
|
||||||
;;
|
|
||||||
api)
|
|
||||||
_ccs_api
|
|
||||||
;;
|
|
||||||
cliproxy)
|
|
||||||
_ccs_cliproxy
|
|
||||||
;;
|
|
||||||
update)
|
|
||||||
_arguments \
|
|
||||||
'--force[Force reinstall current version]' \
|
|
||||||
'--beta[Install from dev channel]' \
|
|
||||||
'--dev[Install from dev channel]' \
|
|
||||||
'(- *)'{-h,--help}'[Show help]'
|
|
||||||
;;
|
|
||||||
doctor)
|
|
||||||
_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]' \
|
|
||||||
'--config[Change model configuration]' \
|
|
||||||
'--logout[Clear authentication]' \
|
|
||||||
'--headless[Headless auth (for SSH)]' \
|
|
||||||
'(- *)'{-h,--help}'[Show help]'
|
|
||||||
;;
|
|
||||||
--shell-completion|-sc)
|
|
||||||
_arguments \
|
|
||||||
'--bash[Install for bash]' \
|
|
||||||
'--zsh[Install for zsh]' \
|
|
||||||
'--fish[Install for fish]' \
|
|
||||||
'--powershell[Install for PowerShell]'
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
_message 'Claude CLI arguments'
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ccs_api() {
|
__ccs_completion_run() {
|
||||||
local curcontext="$curcontext" state line
|
local current="$1"
|
||||||
typeset -A opt_args
|
shift || true
|
||||||
|
|
||||||
local -a api_commands settings_profiles
|
local script_path script_dir repo_root repo_cli
|
||||||
|
script_path="${(%):-%N}"
|
||||||
api_commands=(
|
script_dir="${script_path:A:h}"
|
||||||
'create:Create new API profile (interactive)'
|
repo_root="${script_dir:h:h}"
|
||||||
'list:List all API profiles'
|
repo_cli="${repo_root}/dist/ccs.js"
|
||||||
'remove:Remove an API profile'
|
if [[ ! -f "${repo_cli}" ]]; then
|
||||||
)
|
repo_cli="${repo_root}/bin/ccs.js"
|
||||||
|
fi
|
||||||
if [[ -f ~/.ccs/config.json ]]; then
|
if [[ -f "${repo_cli}" ]]; then
|
||||||
settings_profiles=(${(f)"$(jq -r '.profiles | keys[]' ~/.ccs/config.json 2>/dev/null)"})
|
node "${repo_cli}" __complete --shell zsh --current "${current}" -- "$@" 2>/dev/null
|
||||||
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_arguments -C \
|
if (( $+commands[ccs] )); then
|
||||||
'(- *)'{-h,--help}'[Show help for api commands]' \
|
ccs __complete --shell zsh --current "${current}" -- "$@" 2>/dev/null
|
||||||
'1: :->subcommand' \
|
|
||||||
'*:: :->subargs'
|
|
||||||
|
|
||||||
case $state in
|
|
||||||
subcommand)
|
|
||||||
_describe -t api-commands 'api commands' api_commands
|
|
||||||
;;
|
|
||||||
|
|
||||||
subargs)
|
|
||||||
case $words[1] in
|
|
||||||
create)
|
|
||||||
_arguments \
|
|
||||||
'1:profile name:' \
|
|
||||||
'--base-url[API base URL]:url:' \
|
|
||||||
'--api-key[API key]:key:' \
|
|
||||||
'--model[Default model]:model:' \
|
|
||||||
'--force[Overwrite existing profile]' \
|
|
||||||
{--yes,-y}'[Skip prompts]'
|
|
||||||
;;
|
|
||||||
list)
|
|
||||||
;;
|
|
||||||
remove|delete|rm)
|
|
||||||
_arguments \
|
|
||||||
'1:profile:($settings_profiles)' \
|
|
||||||
{--yes,-y}'[Skip confirmation]'
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
_ccs_cliproxy() {
|
|
||||||
local curcontext="$curcontext" state line
|
|
||||||
typeset -A opt_args
|
|
||||||
|
|
||||||
local -a cliproxy_commands cliproxy_variants providers
|
|
||||||
|
|
||||||
cliproxy_commands=(
|
|
||||||
'create:Create new CLIProxy variant profile'
|
|
||||||
'list:List all CLIProxy variant profiles'
|
|
||||||
'remove:Remove a CLIProxy variant profile'
|
|
||||||
)
|
|
||||||
|
|
||||||
providers=(gemini codex agy qwen)
|
|
||||||
|
|
||||||
if [[ -f ~/.ccs/config.json ]]; then
|
|
||||||
cliproxy_variants=(${(f)"$(jq -r '.cliproxy | keys[]' ~/.ccs/config.json 2>/dev/null)"})
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_arguments -C \
|
|
||||||
'(- *)'{-h,--help}'[Show help for cliproxy commands]' \
|
|
||||||
'--install[Install specific version]:version:' \
|
|
||||||
'--latest[Install latest version]' \
|
|
||||||
'1: :->subcommand' \
|
|
||||||
'*:: :->subargs'
|
|
||||||
|
|
||||||
case $state in
|
|
||||||
subcommand)
|
|
||||||
_describe -t cliproxy-commands 'cliproxy commands' cliproxy_commands
|
|
||||||
;;
|
|
||||||
|
|
||||||
subargs)
|
|
||||||
case $words[1] in
|
|
||||||
create)
|
|
||||||
_arguments \
|
|
||||||
'1:variant name:' \
|
|
||||||
'--provider[Provider name]:provider:($providers)' \
|
|
||||||
'--model[Model name]:model:' \
|
|
||||||
'--force[Overwrite existing variant]' \
|
|
||||||
{--yes,-y}'[Skip prompts]'
|
|
||||||
;;
|
|
||||||
list|ls)
|
|
||||||
;;
|
|
||||||
remove|delete|rm)
|
|
||||||
_arguments \
|
|
||||||
'1:variant:($cliproxy_variants)' \
|
|
||||||
{--yes,-y}'[Skip confirmation]'
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ccs_auth() {
|
|
||||||
local curcontext="$curcontext" state line
|
|
||||||
typeset -A opt_args
|
|
||||||
|
|
||||||
local -a auth_commands account_profiles
|
|
||||||
|
|
||||||
auth_commands=(
|
|
||||||
'create:Create new profile and login'
|
|
||||||
'list:List all saved profiles'
|
|
||||||
'show:Show profile details'
|
|
||||||
'remove:Remove saved profile'
|
|
||||||
'default:Set default profile'
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ -f ~/.ccs/profiles.json ]]; then
|
|
||||||
account_profiles=(${(f)"$(jq -r '.profiles | keys[]' ~/.ccs/profiles.json 2>/dev/null)"})
|
|
||||||
fi
|
|
||||||
|
|
||||||
_arguments -C \
|
|
||||||
'(- *)'{-h,--help}'[Show help for auth commands]' \
|
|
||||||
'1: :->subcommand' \
|
|
||||||
'*:: :->subargs'
|
|
||||||
|
|
||||||
case $state in
|
|
||||||
subcommand)
|
|
||||||
_describe -t auth-commands 'auth commands' auth_commands
|
|
||||||
;;
|
|
||||||
|
|
||||||
subargs)
|
|
||||||
case $words[1] in
|
|
||||||
create)
|
|
||||||
_message 'new profile name'
|
|
||||||
_arguments '--force[Allow overwriting existing profile]'
|
|
||||||
;;
|
|
||||||
list)
|
|
||||||
_arguments \
|
|
||||||
'--verbose[Show additional details]' \
|
|
||||||
'--json[Output in JSON format]'
|
|
||||||
;;
|
|
||||||
show)
|
|
||||||
_arguments \
|
|
||||||
'1:profile:($account_profiles)' \
|
|
||||||
'--json[Output in JSON format]'
|
|
||||||
;;
|
|
||||||
remove)
|
|
||||||
_arguments \
|
|
||||||
'1:profile:($account_profiles)' \
|
|
||||||
{--yes,-y}'[Skip confirmation prompts]'
|
|
||||||
;;
|
|
||||||
default)
|
|
||||||
_arguments '1:profile:($account_profiles)'
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
_ccs "$@"
|
|
||||||
|
|||||||
+10
-4
@@ -318,10 +318,11 @@ async function main(): Promise<void> {
|
|||||||
registerTarget(new CodexAdapter());
|
registerTarget(new CodexAdapter());
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
const isCompletionCommand = args[0] === '__complete';
|
||||||
|
|
||||||
// Initialize UI colors early to ensure consistent colored output
|
// Initialize UI colors early to ensure consistent colored output
|
||||||
// Must happen before any status messages (ok, info, fail, etc.)
|
// Must happen before any status messages (ok, info, fail, etc.)
|
||||||
if (process.stdout.isTTY && !process.env['CI']) {
|
if (!isCompletionCommand && process.stdout.isTTY && !process.env['CI']) {
|
||||||
const { initUI } = await import('./utils/ui');
|
const { initUI } = await import('./utils/ui');
|
||||||
await initUI();
|
await initUI();
|
||||||
}
|
}
|
||||||
@@ -361,7 +362,7 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
// Security warning: cloud sync paths expose OAuth tokens
|
// Security warning: cloud sync paths expose OAuth tokens
|
||||||
const cloudService = detectCloudSyncPath(configDirValue);
|
const cloudService = detectCloudSyncPath(configDirValue);
|
||||||
if (cloudService) {
|
if (!isCompletionCommand && cloudService) {
|
||||||
console.error(warn(`CCS directory is under ${cloudService}.`));
|
console.error(warn(`CCS directory is under ${cloudService}.`));
|
||||||
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
|
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
|
||||||
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
|
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
|
||||||
@@ -372,7 +373,7 @@ async function main(): Promise<void> {
|
|||||||
} else if (process.env.CCS_DIR) {
|
} else if (process.env.CCS_DIR) {
|
||||||
// Also warn for CCS_DIR env var pointing to cloud sync
|
// Also warn for CCS_DIR env var pointing to cloud sync
|
||||||
const cloudService = detectCloudSyncPath(process.env.CCS_DIR);
|
const cloudService = detectCloudSyncPath(process.env.CCS_DIR);
|
||||||
if (cloudService) {
|
if (!isCompletionCommand && cloudService) {
|
||||||
console.error(warn(`CCS directory is under ${cloudService}.`));
|
console.error(warn(`CCS directory is under ${cloudService}.`));
|
||||||
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
|
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
|
||||||
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
|
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
|
||||||
@@ -380,13 +381,18 @@ async function main(): Promise<void> {
|
|||||||
} else if (process.env.CCS_HOME) {
|
} else if (process.env.CCS_HOME) {
|
||||||
// Also warn for CCS_HOME env var pointing to cloud sync
|
// Also warn for CCS_HOME env var pointing to cloud sync
|
||||||
const cloudService = detectCloudSyncPath(process.env.CCS_HOME);
|
const cloudService = detectCloudSyncPath(process.env.CCS_HOME);
|
||||||
if (cloudService) {
|
if (!isCompletionCommand && cloudService) {
|
||||||
console.error(warn(`CCS directory is under ${cloudService}.`));
|
console.error(warn(`CCS directory is under ${cloudService}.`));
|
||||||
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
|
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
|
||||||
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
|
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCompletionCommand) {
|
||||||
|
await tryHandleRootCommand(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldPassthroughNativeCodexFlagCommand(args)) {
|
if (shouldPassthroughNativeCodexFlagCommand(args)) {
|
||||||
execNativeCodexFlagCommand(args);
|
execNativeCodexFlagCommand(args);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
import { COPILOT_SUBCOMMANDS } from '../copilot/constants';
|
||||||
|
import { CLIPROXY_PROVIDER_IDS } from '../cliproxy/provider-capabilities';
|
||||||
|
import { CURSOR_SUBCOMMANDS } from './cursor-command';
|
||||||
|
|
||||||
|
export type HelpTopicName = 'profiles' | 'providers' | 'completion' | 'targets';
|
||||||
|
|
||||||
|
export interface HelpTopicEntry {
|
||||||
|
name: HelpTopicName;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RootCommandEntry {
|
||||||
|
name: string;
|
||||||
|
summary: string;
|
||||||
|
group: 'start' | 'manage' | 'runtime' | 'operations';
|
||||||
|
visibility: 'public' | 'hidden';
|
||||||
|
aliases?: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShortcutEntry {
|
||||||
|
name: string;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ROOT_HELP_TOPICS: readonly HelpTopicEntry[] = [
|
||||||
|
{ name: 'profiles', summary: 'Account profiles, API profiles, and CLIProxy variants' },
|
||||||
|
{ name: 'providers', summary: 'Built-in OAuth providers and runtime shortcuts' },
|
||||||
|
{ name: 'completion', summary: 'Shell completion install, refresh, and testing' },
|
||||||
|
{ name: 'targets', summary: 'Claude, Droid, and Codex target routing' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const ROOT_COMMAND_CATALOG: readonly RootCommandEntry[] = [
|
||||||
|
{
|
||||||
|
name: 'help',
|
||||||
|
summary: 'Show root help or help for a topic/command',
|
||||||
|
group: 'start',
|
||||||
|
aliases: ['--help', '-h'],
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'version',
|
||||||
|
summary: 'Show version and install details',
|
||||||
|
group: 'start',
|
||||||
|
aliases: ['--version', '-v'],
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'setup',
|
||||||
|
summary: 'Run the first-time setup wizard',
|
||||||
|
group: 'start',
|
||||||
|
aliases: ['--setup'],
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'config',
|
||||||
|
summary: 'Open the dashboard and config subcommands',
|
||||||
|
group: 'start',
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'doctor',
|
||||||
|
summary: 'Run health checks and diagnostics',
|
||||||
|
group: 'start',
|
||||||
|
aliases: ['--doctor'],
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'auth',
|
||||||
|
summary: 'Manage concurrent Claude accounts',
|
||||||
|
group: 'manage',
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'api',
|
||||||
|
summary: 'Create, discover, copy, export, and import API profiles',
|
||||||
|
group: 'manage',
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cliproxy',
|
||||||
|
summary: 'Manage CLIProxy variants, quota, and local lifecycle',
|
||||||
|
group: 'manage',
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'env',
|
||||||
|
summary: 'Export profile env for third-party tools',
|
||||||
|
group: 'manage',
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'persist',
|
||||||
|
summary: 'Write profile setup to ~/.claude/settings.json',
|
||||||
|
group: 'manage',
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{ name: 'tokens', summary: 'Manage CLIProxy auth tokens', group: 'manage', visibility: 'public' },
|
||||||
|
{
|
||||||
|
name: 'migrate',
|
||||||
|
summary: 'Move legacy JSON config to unified YAML config',
|
||||||
|
group: 'manage',
|
||||||
|
aliases: ['--migrate'],
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cursor',
|
||||||
|
summary: 'Run or manage the Cursor bridge',
|
||||||
|
group: 'runtime',
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'copilot',
|
||||||
|
summary: 'Run or manage the GitHub Copilot bridge',
|
||||||
|
group: 'runtime',
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'docker',
|
||||||
|
summary: 'Deploy or operate the bundled Docker stack',
|
||||||
|
group: 'operations',
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sync',
|
||||||
|
summary: 'Sync delegation commands and skills',
|
||||||
|
group: 'operations',
|
||||||
|
aliases: ['--sync'],
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'update',
|
||||||
|
summary: 'Update CCS to the latest version',
|
||||||
|
group: 'operations',
|
||||||
|
aliases: ['--update'],
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cleanup',
|
||||||
|
summary: 'Remove old CLIProxy logs',
|
||||||
|
group: 'operations',
|
||||||
|
aliases: ['--cleanup'],
|
||||||
|
visibility: 'public',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '--shell-completion',
|
||||||
|
summary: 'Install shell completion',
|
||||||
|
group: 'operations',
|
||||||
|
aliases: ['-sc'],
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '--install',
|
||||||
|
summary: 'Post-install bootstrap hook',
|
||||||
|
group: 'operations',
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '--uninstall',
|
||||||
|
summary: 'Post-uninstall cleanup hook',
|
||||||
|
group: 'operations',
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '__complete',
|
||||||
|
summary: 'Hidden shell completion backend',
|
||||||
|
group: 'operations',
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const BUILTIN_PROVIDER_SHORTCUTS: readonly ShortcutEntry[] = CLIPROXY_PROVIDER_IDS.map(
|
||||||
|
(name) => ({
|
||||||
|
name,
|
||||||
|
summary:
|
||||||
|
{
|
||||||
|
gemini: 'Google Gemini via CLIProxy OAuth',
|
||||||
|
codex: 'OpenAI Codex via CLIProxy OAuth',
|
||||||
|
agy: 'Antigravity via CLIProxy OAuth',
|
||||||
|
qwen: 'Qwen Code via CLIProxy OAuth',
|
||||||
|
iflow: 'iFlow via CLIProxy OAuth',
|
||||||
|
kiro: 'Kiro via CLIProxy OAuth',
|
||||||
|
ghcp: 'GitHub Copilot via CLIProxy OAuth',
|
||||||
|
claude: 'Claude via CLIProxy OAuth',
|
||||||
|
kimi: 'Kimi via CLIProxy OAuth',
|
||||||
|
}[name] || 'CLIProxy OAuth provider',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ROOT_PROFILE_EXAMPLES: readonly ShortcutEntry[] = [
|
||||||
|
{ name: 'ccs auth create work', summary: 'Create a concurrent Claude account profile' },
|
||||||
|
{ name: 'ccs api create --preset glm', summary: 'Create a GLM-backed API profile' },
|
||||||
|
{
|
||||||
|
name: 'ccs api create --preset anthropic --1m',
|
||||||
|
summary: 'Create a Claude API profile with explicit [1m]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ccs env <profile> --format claude-extension --ide vscode',
|
||||||
|
summary: 'Export IDE extension settings',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const ROOT_COMPATIBLE_ALIAS_EXAMPLES: readonly ShortcutEntry[] = [
|
||||||
|
{ name: '--target claude|droid|codex', summary: 'Route a profile to the target runtime' },
|
||||||
|
{ name: 'ccs-droid / ccsd', summary: 'Explicit Droid runtime aliases' },
|
||||||
|
{ name: 'ccs-codex / ccsx / ccsxp', summary: 'Explicit Codex runtime aliases' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const ROOT_COMMAND_FLAGS = [
|
||||||
|
'--help',
|
||||||
|
'-h',
|
||||||
|
'--version',
|
||||||
|
'-v',
|
||||||
|
'--shell-completion',
|
||||||
|
'-sc',
|
||||||
|
] as const;
|
||||||
|
export const AUTH_SUBCOMMANDS = [
|
||||||
|
'create',
|
||||||
|
'list',
|
||||||
|
'show',
|
||||||
|
'remove',
|
||||||
|
'default',
|
||||||
|
'reset-default',
|
||||||
|
] as const;
|
||||||
|
export const API_SUBCOMMANDS = [
|
||||||
|
'create',
|
||||||
|
'list',
|
||||||
|
'discover',
|
||||||
|
'copy',
|
||||||
|
'export',
|
||||||
|
'import',
|
||||||
|
'remove',
|
||||||
|
] as const;
|
||||||
|
export const CLIPROXY_SUBCOMMANDS = [
|
||||||
|
'create',
|
||||||
|
'edit',
|
||||||
|
'list',
|
||||||
|
'remove',
|
||||||
|
'catalog',
|
||||||
|
'sync',
|
||||||
|
'quota',
|
||||||
|
'start',
|
||||||
|
'restart',
|
||||||
|
'status',
|
||||||
|
'stop',
|
||||||
|
'doctor',
|
||||||
|
'default',
|
||||||
|
'pause',
|
||||||
|
'resume',
|
||||||
|
] as const;
|
||||||
|
export const CONFIG_SUBCOMMANDS = ['auth', 'channels', 'image-analysis', 'thinking'] as const;
|
||||||
|
export const DOCKER_SUBCOMMANDS = ['up', 'down', 'status', 'update', 'logs', 'config'] as const;
|
||||||
|
export const TOKENS_FLAGS = [
|
||||||
|
'--show',
|
||||||
|
'--api-key',
|
||||||
|
'--secret',
|
||||||
|
'--regenerate-secret',
|
||||||
|
'--variant',
|
||||||
|
'--reset',
|
||||||
|
'--help',
|
||||||
|
'-h',
|
||||||
|
] as const;
|
||||||
|
export const MIGRATE_FLAGS = ['--dry-run', '--rollback', '--list-backups', '--help'] as const;
|
||||||
|
export const CLEANUP_FLAGS = [
|
||||||
|
'--errors',
|
||||||
|
'--days=',
|
||||||
|
'--dry-run',
|
||||||
|
'--force',
|
||||||
|
'--help',
|
||||||
|
'-h',
|
||||||
|
] as const;
|
||||||
|
export const PROVIDER_FLAGS = [
|
||||||
|
'--auth',
|
||||||
|
'--add',
|
||||||
|
'--paste-callback',
|
||||||
|
'--accounts',
|
||||||
|
'--use',
|
||||||
|
'--config',
|
||||||
|
'--thinking',
|
||||||
|
'--effort',
|
||||||
|
'--1m',
|
||||||
|
'--no-1m',
|
||||||
|
'--logout',
|
||||||
|
'--headless',
|
||||||
|
'--port-forward',
|
||||||
|
'--help',
|
||||||
|
'-h',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const COMMAND_FLAG_SUGGESTIONS: Readonly<Record<string, readonly string[]>> = {
|
||||||
|
'--shell-completion': ['--bash', '--zsh', '--fish', '--powershell', '--force', '-f'],
|
||||||
|
auth: ['--help', '-h'],
|
||||||
|
api: ['--help', '-h'],
|
||||||
|
cleanup: CLEANUP_FLAGS,
|
||||||
|
config: ['--help', '-h', '--port', '-p', '--host', '-H', '--dev'],
|
||||||
|
cursor: ['--help', '-h'],
|
||||||
|
doctor: ['--fix', '-f', '--help', '-h'],
|
||||||
|
docker: ['--help', '-h', '--host'],
|
||||||
|
env: ['--format', '--shell', '--ide', '--help', '-h'],
|
||||||
|
migrate: MIGRATE_FLAGS,
|
||||||
|
tokens: TOKENS_FLAGS,
|
||||||
|
update: ['--force', '--beta', '--dev', '--help', '-h'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CURSOR_COMPLETION_SUBCOMMANDS = [...CURSOR_SUBCOMMANDS] as const;
|
||||||
|
export const COPILOT_COMPLETION_SUBCOMMANDS = [...COPILOT_SUBCOMMANDS, 'help'] as const;
|
||||||
|
|
||||||
|
export function getPublicRootCommands(): readonly RootCommandEntry[] {
|
||||||
|
return ROOT_COMMAND_CATALOG.filter((entry) => entry.visibility === 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllRootCommandTokens(): string[] {
|
||||||
|
return uniqueStrings(
|
||||||
|
ROOT_COMMAND_CATALOG.flatMap((entry) => [entry.name, ...(entry.aliases || [])]).concat(
|
||||||
|
'copilot'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPublicRootCommandTokens(): string[] {
|
||||||
|
return uniqueStrings(
|
||||||
|
ROOT_COMMAND_CATALOG.filter((entry) => entry.visibility === 'public').flatMap((entry) => [
|
||||||
|
entry.name,
|
||||||
|
...(entry.aliases || []),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uniqueStrings(values: readonly string[]): string[] {
|
||||||
|
return [...new Set(values.filter(Boolean))].sort((left, right) => left.localeCompare(right));
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import ProfileDetector from '../auth/profile-detector';
|
||||||
|
import {
|
||||||
|
API_SUBCOMMANDS,
|
||||||
|
AUTH_SUBCOMMANDS,
|
||||||
|
BUILTIN_PROVIDER_SHORTCUTS,
|
||||||
|
CLEANUP_FLAGS,
|
||||||
|
COMMAND_FLAG_SUGGESTIONS,
|
||||||
|
CONFIG_SUBCOMMANDS,
|
||||||
|
COPILOT_COMPLETION_SUBCOMMANDS,
|
||||||
|
DOCKER_SUBCOMMANDS,
|
||||||
|
ROOT_COMMAND_FLAGS,
|
||||||
|
ROOT_HELP_TOPICS,
|
||||||
|
TOKENS_FLAGS,
|
||||||
|
PROVIDER_FLAGS,
|
||||||
|
uniqueStrings,
|
||||||
|
getPublicRootCommandTokens,
|
||||||
|
CLIPROXY_SUBCOMMANDS,
|
||||||
|
MIGRATE_FLAGS,
|
||||||
|
CURSOR_COMPLETION_SUBCOMMANDS,
|
||||||
|
} from './command-catalog';
|
||||||
|
|
||||||
|
export interface CompletionSuggestion {
|
||||||
|
value: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompletionShell = 'bash' | 'zsh' | 'fish' | 'powershell';
|
||||||
|
|
||||||
|
interface CompletionRequest {
|
||||||
|
current: string;
|
||||||
|
shell: CompletionShell;
|
||||||
|
tokensBeforeCurrent: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function suggestion(value: string, description?: string): CompletionSuggestion {
|
||||||
|
return { value, description };
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterSuggestions(
|
||||||
|
suggestions: readonly CompletionSuggestion[],
|
||||||
|
current: string
|
||||||
|
): CompletionSuggestion[] {
|
||||||
|
const needle = current.trim().toLowerCase();
|
||||||
|
const deduped = [...new Map(suggestions.map((entry) => [entry.value, entry])).values()];
|
||||||
|
if (!needle) return deduped.sort((left, right) => left.value.localeCompare(right.value));
|
||||||
|
return deduped
|
||||||
|
.filter((entry) => entry.value.toLowerCase().startsWith(needle))
|
||||||
|
.sort((left, right) => left.value.localeCompare(right.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDynamicProfileSuggestions(): CompletionSuggestion[] {
|
||||||
|
const detector = new ProfileDetector();
|
||||||
|
const profiles = detector.getAllProfiles();
|
||||||
|
|
||||||
|
return uniqueStrings([
|
||||||
|
...profiles.settings,
|
||||||
|
...profiles.accounts,
|
||||||
|
...profiles.cliproxyVariants,
|
||||||
|
]).map((value) => suggestion(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTopLevelSuggestions(): CompletionSuggestion[] {
|
||||||
|
return [
|
||||||
|
...getPublicRootCommandTokens().map((value) => suggestion(value)),
|
||||||
|
...ROOT_COMMAND_FLAGS.map((value) => suggestion(value)),
|
||||||
|
...BUILTIN_PROVIDER_SHORTCUTS.map((entry) => suggestion(entry.name, entry.summary)),
|
||||||
|
...getDynamicProfileSuggestions(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProfileDetector(): ProfileDetector {
|
||||||
|
return new ProfileDetector();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProfileNames(type: 'settings' | 'accounts' | 'cliproxyVariants'): string[] {
|
||||||
|
return getProfileDetector().getAllProfiles()[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeSubcommands(
|
||||||
|
values: readonly string[],
|
||||||
|
flags: readonly string[] = ['--help', '-h']
|
||||||
|
): CompletionSuggestion[] {
|
||||||
|
return uniqueStrings([...values, ...flags]).map((value) => suggestion(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSuggestion[] {
|
||||||
|
const [command, subcommand] = tokensBeforeCurrent;
|
||||||
|
const lastToken = tokensBeforeCurrent[tokensBeforeCurrent.length - 1];
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case undefined:
|
||||||
|
return getTopLevelSuggestions();
|
||||||
|
case 'help':
|
||||||
|
return completeSubcommands(ROOT_HELP_TOPICS.map((topic) => topic.name));
|
||||||
|
case 'auth':
|
||||||
|
if (!subcommand) return completeSubcommands(AUTH_SUBCOMMANDS);
|
||||||
|
if (subcommand === 'show')
|
||||||
|
return completeSubcommands(getProfileNames('accounts'), ['--json']);
|
||||||
|
if (subcommand === 'remove')
|
||||||
|
return completeSubcommands(getProfileNames('accounts'), ['--yes', '-y']);
|
||||||
|
if (subcommand === 'default') return completeSubcommands(getProfileNames('accounts'));
|
||||||
|
if (subcommand === 'create') {
|
||||||
|
return completeSubcommands(
|
||||||
|
[],
|
||||||
|
['--force', '--share-context', '--context-group', '--deeper-continuity', '--bare']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (subcommand === 'list') return completeSubcommands([], ['--verbose', '--json']);
|
||||||
|
return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS.auth);
|
||||||
|
case 'api':
|
||||||
|
if (!subcommand) return completeSubcommands(API_SUBCOMMANDS);
|
||||||
|
if (subcommand === 'create') {
|
||||||
|
return completeSubcommands(
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
'--preset',
|
||||||
|
'--cliproxy-provider',
|
||||||
|
'--base-url',
|
||||||
|
'--api-key',
|
||||||
|
'--model',
|
||||||
|
'--1m',
|
||||||
|
'--no-1m',
|
||||||
|
'--target',
|
||||||
|
'--force',
|
||||||
|
'--yes',
|
||||||
|
'-y',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (subcommand === 'discover')
|
||||||
|
return completeSubcommands([], ['--register', '--json', '--force']);
|
||||||
|
if (subcommand === 'copy')
|
||||||
|
return completeSubcommands(getProfileNames('settings'), [
|
||||||
|
'--target',
|
||||||
|
'--force',
|
||||||
|
'--yes',
|
||||||
|
'-y',
|
||||||
|
]);
|
||||||
|
if (subcommand === 'export')
|
||||||
|
return completeSubcommands(getProfileNames('settings'), ['--out', '--include-secrets']);
|
||||||
|
if (subcommand === 'import')
|
||||||
|
return completeSubcommands([], ['--name', '--yes', '-y', '--force']);
|
||||||
|
if (subcommand === 'remove')
|
||||||
|
return completeSubcommands(getProfileNames('settings'), ['--yes', '-y']);
|
||||||
|
return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS.api);
|
||||||
|
case 'cliproxy':
|
||||||
|
if (!subcommand)
|
||||||
|
return completeSubcommands(CLIPROXY_SUBCOMMANDS, [
|
||||||
|
'--install',
|
||||||
|
'--latest',
|
||||||
|
'--update',
|
||||||
|
'--backend',
|
||||||
|
'--verbose',
|
||||||
|
'-v',
|
||||||
|
'--help',
|
||||||
|
'-h',
|
||||||
|
]);
|
||||||
|
if (['remove', 'edit'].includes(subcommand)) {
|
||||||
|
return completeSubcommands(getProfileNames('cliproxyVariants'), ['--yes', '-y']);
|
||||||
|
}
|
||||||
|
if (subcommand === 'create' && lastToken === '--provider') {
|
||||||
|
return BUILTIN_PROVIDER_SHORTCUTS.map((entry) => suggestion(entry.name, entry.summary));
|
||||||
|
}
|
||||||
|
return completeSubcommands(
|
||||||
|
[],
|
||||||
|
['--provider', '--model', '--target', '--backend', '--force', '--yes', '-y', '--help', '-h']
|
||||||
|
);
|
||||||
|
case 'config':
|
||||||
|
if (!subcommand || subcommand.startsWith('-')) {
|
||||||
|
return completeSubcommands(CONFIG_SUBCOMMANDS, COMMAND_FLAG_SUGGESTIONS.config);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
case 'docker':
|
||||||
|
if (!subcommand || subcommand.startsWith('-')) {
|
||||||
|
return completeSubcommands(DOCKER_SUBCOMMANDS, COMMAND_FLAG_SUGGESTIONS.docker);
|
||||||
|
}
|
||||||
|
if (subcommand === 'up')
|
||||||
|
return completeSubcommands([], ['--port', '--proxy-port', '--host', '--help', '-h']);
|
||||||
|
if (subcommand === 'logs')
|
||||||
|
return completeSubcommands([], ['--follow', '--service', '--host', '--help', '-h']);
|
||||||
|
return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS.docker);
|
||||||
|
case 'cursor':
|
||||||
|
return completeSubcommands(CURSOR_COMPLETION_SUBCOMMANDS);
|
||||||
|
case 'copilot':
|
||||||
|
return completeSubcommands(COPILOT_COMPLETION_SUBCOMMANDS);
|
||||||
|
case 'env':
|
||||||
|
if (lastToken === '--format')
|
||||||
|
return completeSubcommands(['openai', 'anthropic', 'raw', 'claude-extension']);
|
||||||
|
if (lastToken === '--shell')
|
||||||
|
return completeSubcommands(['auto', 'bash', 'zsh', 'fish', 'powershell']);
|
||||||
|
if (lastToken === '--ide') return completeSubcommands(['vscode', 'cursor', 'windsurf']);
|
||||||
|
return completeSubcommands(
|
||||||
|
uniqueStrings([
|
||||||
|
...getProfileNames('settings'),
|
||||||
|
...getProfileNames('accounts'),
|
||||||
|
...BUILTIN_PROVIDER_SHORTCUTS.map((entry) => entry.name),
|
||||||
|
]),
|
||||||
|
COMMAND_FLAG_SUGGESTIONS.env
|
||||||
|
);
|
||||||
|
case 'tokens':
|
||||||
|
if (lastToken === '--variant')
|
||||||
|
return completeSubcommands(getProfileNames('cliproxyVariants'));
|
||||||
|
return completeSubcommands([], TOKENS_FLAGS);
|
||||||
|
case 'migrate':
|
||||||
|
return completeSubcommands([], MIGRATE_FLAGS);
|
||||||
|
case 'cleanup':
|
||||||
|
return completeSubcommands([], CLEANUP_FLAGS);
|
||||||
|
case '--shell-completion':
|
||||||
|
case '-sc':
|
||||||
|
return completeSubcommands(
|
||||||
|
[],
|
||||||
|
['--bash', '--zsh', '--fish', '--powershell', '--force', '-f']
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
if (BUILTIN_PROVIDER_SHORTCUTS.some((entry) => entry.name === command)) {
|
||||||
|
if (command === 'agy')
|
||||||
|
return completeSubcommands([], [...PROVIDER_FLAGS, '--accept-agr-risk']);
|
||||||
|
if (command === 'kiro') {
|
||||||
|
return completeSubcommands(
|
||||||
|
[],
|
||||||
|
[...PROVIDER_FLAGS, '--kiro-auth-method', '--import', '--incognito']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return completeSubcommands([], PROVIDER_FLAGS);
|
||||||
|
}
|
||||||
|
return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS[command] || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCompletionSuggestions(request: CompletionRequest): CompletionSuggestion[] {
|
||||||
|
return filterSuggestions(getSuggestionsForCommand(request.tokensBeforeCurrent), request.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCompletionArgs(args: string[]): CompletionRequest {
|
||||||
|
const shellIndex = args.indexOf('--shell');
|
||||||
|
const currentIndex = args.indexOf('--current');
|
||||||
|
const separatorIndex = args.indexOf('--');
|
||||||
|
const shell = (shellIndex !== -1 ? args[shellIndex + 1] : 'bash') as CompletionShell;
|
||||||
|
const current = currentIndex !== -1 ? args[currentIndex + 1] || '' : '';
|
||||||
|
const tokensBeforeCurrent = separatorIndex === -1 ? [] : args.slice(separatorIndex + 1);
|
||||||
|
return { shell, current, tokensBeforeCurrent };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatForShell(
|
||||||
|
shell: CompletionShell,
|
||||||
|
suggestions: readonly CompletionSuggestion[]
|
||||||
|
): string[] {
|
||||||
|
if (shell === 'fish' || shell === 'powershell') {
|
||||||
|
return suggestions.map((entry) =>
|
||||||
|
entry.description ? `${entry.value}\t${entry.description}` : entry.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return suggestions.map((entry) => entry.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleCompletionCommand(args: string[]): Promise<void> {
|
||||||
|
const request = parseCompletionArgs(args);
|
||||||
|
const suggestions = getCompletionSuggestions(request);
|
||||||
|
process.stdout.write(formatForShell(request.shell, suggestions).join('\n'));
|
||||||
|
}
|
||||||
+196
-662
@@ -1,695 +1,229 @@
|
|||||||
import * as fs from 'fs';
|
import packageJson from '../../package.json';
|
||||||
import * as path from 'path';
|
import { color, dim, header, initUI, subheader } from '../utils/ui';
|
||||||
import { initUI, box, color, dim, sectionHeader, subheader } from '../utils/ui';
|
import {
|
||||||
import { isUnifiedMode } from '../config/unified-config-loader';
|
BUILTIN_PROVIDER_SHORTCUTS,
|
||||||
import { getCcsDirDisplay } from '../utils/config-manager';
|
ROOT_COMMAND_CATALOG,
|
||||||
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
|
ROOT_COMPATIBLE_ALIAS_EXAMPLES,
|
||||||
import { getOfficialChannelsSupportMessage } from '../channels/official-channels-runtime';
|
ROOT_HELP_TOPICS,
|
||||||
|
ROOT_PROFILE_EXAMPLES,
|
||||||
|
getPublicRootCommands,
|
||||||
|
type HelpTopicName,
|
||||||
|
type RootCommandEntry,
|
||||||
|
} from './command-catalog';
|
||||||
|
|
||||||
type HelpWriter = (line: string) => void;
|
type HelpWriter = (line: string) => void;
|
||||||
|
|
||||||
// Get version from package.json (same as version-command.ts)
|
function getTopicSummary(name: HelpTopicName): string {
|
||||||
const VERSION = JSON.parse(
|
return ROOT_HELP_TOPICS.find((topic) => topic.name === name)?.summary || '';
|
||||||
fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf8')
|
}
|
||||||
).version;
|
|
||||||
|
|
||||||
/**
|
function writeCommandTable(
|
||||||
* Print a major section with ═══ borders (only for 3 main sections)
|
|
||||||
* Format:
|
|
||||||
* ═══ TITLE ═══
|
|
||||||
* Subtitle line 1
|
|
||||||
* Subtitle line 2
|
|
||||||
*
|
|
||||||
* command Description
|
|
||||||
*/
|
|
||||||
function printMajorSection(
|
|
||||||
title: string,
|
title: string,
|
||||||
subtitles: string[],
|
entries: readonly { name: string; summary: string }[],
|
||||||
items: [string, string][],
|
writeLine: HelpWriter
|
||||||
writeLine: HelpWriter = console.log
|
|
||||||
): void {
|
): void {
|
||||||
// Section header with ═══ borders
|
writeLine(subheader(title));
|
||||||
writeLine(sectionHeader(title));
|
const maxWidth = Math.max(...entries.map((entry) => entry.name.length));
|
||||||
|
for (const entry of entries) {
|
||||||
// Subtitles on separate lines (dim)
|
writeLine(` ${color(entry.name.padEnd(maxWidth + 2), 'command')} ${entry.summary}`);
|
||||||
for (const subtitle of subtitles) {
|
|
||||||
writeLine(` ${dim(subtitle)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty line before items
|
|
||||||
writeLine('');
|
|
||||||
|
|
||||||
// Calculate max command length for alignment
|
|
||||||
const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length));
|
|
||||||
|
|
||||||
for (const [cmd, desc] of items) {
|
|
||||||
const paddedCmd = cmd.padEnd(maxCmdLen + 2);
|
|
||||||
writeLine(` ${color(paddedCmd, 'command')} ${desc}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra spacing after section
|
|
||||||
writeLine('');
|
writeLine('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function writeGroupedCommands(group: RootCommandEntry['group'], writeLine: HelpWriter): void {
|
||||||
* Print a sub-section with colored title
|
const entries = getPublicRootCommands()
|
||||||
* Format:
|
.filter((entry) => entry.group === group)
|
||||||
* Title (context):
|
.map((entry) => ({ name: entry.name, summary: entry.summary }));
|
||||||
* command Description
|
|
||||||
*/
|
|
||||||
function printSubSection(
|
|
||||||
title: string,
|
|
||||||
items: [string, string][],
|
|
||||||
writeLine: HelpWriter = console.log
|
|
||||||
): void {
|
|
||||||
// Sub-section header (colored, no borders)
|
|
||||||
writeLine(subheader(`${title}:`));
|
|
||||||
|
|
||||||
// Calculate max command length for alignment
|
const titles: Record<RootCommandEntry['group'], string> = {
|
||||||
const maxCmdLen = Math.max(...items.map(([cmd]) => cmd.length));
|
start: 'Start Here',
|
||||||
|
manage: 'Profile Management',
|
||||||
|
operations: 'Operations',
|
||||||
|
runtime: 'Compatible Runtimes',
|
||||||
|
};
|
||||||
|
|
||||||
for (const [cmd, desc] of items) {
|
writeCommandTable(titles[group], entries, writeLine);
|
||||||
const paddedCmd = cmd.padEnd(maxCmdLen + 2);
|
}
|
||||||
writeLine(` ${color(paddedCmd, 'command')} ${desc}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spacing after section
|
async function showProfilesHelp(writeLine: HelpWriter): Promise<void> {
|
||||||
|
await initUI();
|
||||||
|
writeLine(header('CCS Profiles Help'));
|
||||||
|
writeLine('');
|
||||||
|
writeCommandTable(
|
||||||
|
'Profile Types',
|
||||||
|
[
|
||||||
|
{ name: 'ccs auth create <name>', summary: 'Concurrent Claude account profile' },
|
||||||
|
{ name: 'ccs api create', summary: 'API-backed settings profile' },
|
||||||
|
{ name: 'ccs cliproxy create <name>', summary: 'Named CLIProxy variant profile' },
|
||||||
|
{ name: 'ccs env <profile>', summary: 'Export an existing profile for other tools' },
|
||||||
|
],
|
||||||
|
writeLine
|
||||||
|
);
|
||||||
|
writeCommandTable('Examples', ROOT_PROFILE_EXAMPLES, writeLine);
|
||||||
|
writeLine(` ${dim('Deep help: ccs auth --help | ccs api --help | ccs cliproxy --help')}`);
|
||||||
writeLine('');
|
writeLine('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async function showProvidersHelp(writeLine: HelpWriter): Promise<void> {
|
||||||
* Print a config/paths section
|
await initUI();
|
||||||
* Format:
|
writeLine(header('CCS Providers Help'));
|
||||||
* Title:
|
writeLine('');
|
||||||
* Label: path
|
writeCommandTable('Built-in OAuth Providers', BUILTIN_PROVIDER_SHORTCUTS, writeLine);
|
||||||
*/
|
writeCommandTable(
|
||||||
function printConfigSection(
|
'Common Setup Paths',
|
||||||
title: string,
|
[
|
||||||
items: [string, string][],
|
{
|
||||||
writeLine: HelpWriter = console.log
|
name: 'ccs <provider> --auth',
|
||||||
): void {
|
summary: 'Authenticate a provider account without launching',
|
||||||
writeLine(subheader(`${title}:`));
|
},
|
||||||
|
{ name: 'ccs api create --preset <id>', summary: 'Create an API-backed provider profile' },
|
||||||
// Calculate max label length for alignment
|
{ name: 'ccs config', summary: 'Use the dashboard for provider and model setup' },
|
||||||
const maxLabelLen = Math.max(...items.map(([label]) => label.length));
|
],
|
||||||
|
writeLine
|
||||||
for (const [label, path] of items) {
|
);
|
||||||
const paddedLabel = label.padEnd(maxLabelLen);
|
writeLine(` ${dim('Deep help: ccs cliproxy --help | ccs api --help')}`);
|
||||||
writeLine(` ${paddedLabel} ${color(path, 'path')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeLine('');
|
writeLine('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async function showTargetsHelp(writeLine: HelpWriter): Promise<void> {
|
||||||
* Display comprehensive help information for CCS (Claude Code Switch)
|
await initUI();
|
||||||
*/
|
writeLine(header('CCS Targets Help'));
|
||||||
|
writeLine('');
|
||||||
|
writeCommandTable('Target Routing', ROOT_COMPATIBLE_ALIAS_EXAMPLES, writeLine);
|
||||||
|
writeCommandTable(
|
||||||
|
'Examples',
|
||||||
|
[
|
||||||
|
{ name: 'ccs glm --target droid', summary: 'Run a profile on Droid instead of Claude' },
|
||||||
|
{
|
||||||
|
name: 'ccs --target codex',
|
||||||
|
summary: 'Open a native Codex session with your current setup',
|
||||||
|
},
|
||||||
|
{ name: 'ccs codex-api --target codex', summary: 'Run a routed API bridge on native Codex' },
|
||||||
|
],
|
||||||
|
writeLine
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleHelpCommand(writeLine: HelpWriter = console.log): Promise<void> {
|
export async function handleHelpCommand(writeLine: HelpWriter = console.log): Promise<void> {
|
||||||
// Initialize UI (if not already)
|
|
||||||
await initUI();
|
await initUI();
|
||||||
|
|
||||||
// Hero box with ASCII art logo and config hint
|
writeLine(header(`CCS CLI v${packageJson.version}`));
|
||||||
// Each letter: C=╔═╗/║ /╚═╝, C=╔═╗/║ /╚═╝, S=╔═╗/╚═╗/╚═╝
|
writeLine('');
|
||||||
const logo = `
|
writeLine(' Claude profile switching, provider routing, and compatible runtime bridges.');
|
||||||
╔═╗ ╔═╗ ╔═╗
|
|
||||||
║ ║ ╚═╗ v${VERSION}
|
|
||||||
╚═╝ ╚═╝ ╚═╝
|
|
||||||
|
|
||||||
Claude Code Profile & Model Switcher
|
|
||||||
|
|
||||||
Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
|
||||||
|
|
||||||
writeLine(
|
|
||||||
box(logo, {
|
|
||||||
padding: 1,
|
|
||||||
borderStyle: 'round',
|
|
||||||
titleAlignment: 'center',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
writeLine('');
|
writeLine('');
|
||||||
|
|
||||||
// Resolve display path for dynamic sections
|
writeLine(subheader('Usage'));
|
||||||
const dirDisplay = getCcsDirDisplay();
|
writeLine(` ${color('ccs <profile> [claude-args...]', 'command')}`);
|
||||||
|
writeLine(` ${color('ccs <command> [options]', 'command')}`);
|
||||||
// Usage section
|
writeLine(` ${color('ccs help <topic>', 'command')}`);
|
||||||
writeLine(subheader('Usage:'));
|
|
||||||
writeLine(` ${color('ccs', 'command')} [profile] [claude-args...]`);
|
|
||||||
writeLine(` ${color('ccs', 'command')} [flags]`);
|
|
||||||
writeLine('');
|
writeLine('');
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
writeGroupedCommands('start', writeLine);
|
||||||
// MAJOR SECTION 1: API Key Profiles
|
writeGroupedCommands('manage', writeLine);
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
writeGroupedCommands('runtime', writeLine);
|
||||||
printMajorSection(
|
writeGroupedCommands('operations', writeLine);
|
||||||
'API Key Profiles',
|
|
||||||
[`Configure in ${dirDisplay}/*.settings.json`],
|
writeCommandTable(
|
||||||
|
'OAuth Provider Shortcuts',
|
||||||
|
BUILTIN_PROVIDER_SHORTCUTS.map((entry) => ({
|
||||||
|
name: `ccs ${entry.name}`,
|
||||||
|
summary: entry.summary,
|
||||||
|
})),
|
||||||
|
writeLine
|
||||||
|
);
|
||||||
|
|
||||||
|
writeCommandTable('Examples', ROOT_PROFILE_EXAMPLES, writeLine);
|
||||||
|
writeCommandTable('Targets and Aliases', ROOT_COMPATIBLE_ALIAS_EXAMPLES, writeLine);
|
||||||
|
writeCommandTable(
|
||||||
|
'More Help',
|
||||||
[
|
[
|
||||||
['ccs', 'Use default Claude account'],
|
{ name: 'ccs help profiles', summary: getTopicSummary('profiles') },
|
||||||
['ccs glm', 'GLM 5 (API key required)'],
|
{ name: 'ccs help providers', summary: getTopicSummary('providers') },
|
||||||
['ccs km', 'Kimi for Coding (API key)'],
|
{ name: 'ccs help completion', summary: getTopicSummary('completion') },
|
||||||
[
|
{ name: 'ccs help targets', summary: getTopicSummary('targets') },
|
||||||
'ccs api create --preset alibaba-coding-plan',
|
{ name: 'ccs api --help', summary: 'Deep help for API profile lifecycle commands' },
|
||||||
'Alibaba Coding Plan (Anthropic-compatible API key)',
|
{ name: 'ccs cliproxy --help', summary: 'Deep help for variants, quota, and lifecycle' },
|
||||||
],
|
{ name: 'ccs docker --help', summary: 'Deep help for Docker deployment commands' },
|
||||||
['ccs ollama', 'Local Ollama (http://localhost:11434)'],
|
{ name: 'ccs cursor --help', summary: 'Deep help for Cursor runtime/admin commands' },
|
||||||
['ccs llamacpp', 'Local llama.cpp (http://127.0.0.1:8080)'],
|
{ name: 'ccs copilot --help', summary: 'Deep help for GitHub Copilot commands' },
|
||||||
['ccs ollama-cloud', 'Ollama Cloud (API key required)'],
|
|
||||||
['', ''], // Spacer
|
|
||||||
['ccs api create --preset anthropic', 'Anthropic direct API key (sk-ant-...)'],
|
|
||||||
[
|
|
||||||
'ccs api create --cliproxy-provider gemini',
|
|
||||||
'Create routed API profile from CLIProxy Gemini',
|
|
||||||
],
|
|
||||||
['ccs api create', 'Create custom API profile'],
|
|
||||||
['ccs api discover --register', 'Discover/register orphan settings files'],
|
|
||||||
['ccs api copy <src> <dest>', 'Duplicate API profile'],
|
|
||||||
['ccs api export <name>', 'Export profile bundle'],
|
|
||||||
['ccs api import <file>', 'Import profile bundle'],
|
|
||||||
['ccs api remove', 'Remove an API profile'],
|
|
||||||
['ccs api list', 'List all API profiles'],
|
|
||||||
],
|
],
|
||||||
writeLine
|
writeLine
|
||||||
);
|
);
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
writeLine(` ${dim('Flags: -h/--help, -v/--version, -sc/--shell-completion, --target <cli>')}`);
|
||||||
// MAJOR SECTION 2: Account Management
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
printMajorSection(
|
|
||||||
'Account Management',
|
|
||||||
['Run multiple Claude accounts concurrently'],
|
|
||||||
[
|
|
||||||
['ccs auth --help', 'Show account management commands'],
|
|
||||||
[
|
|
||||||
'ccs auth create <name>',
|
|
||||||
'Create account profile (supports --bare, shared groups, --deeper-continuity)',
|
|
||||||
],
|
|
||||||
['ccs config', 'Dashboard: Accounts table can edit context mode/group/continuity depth'],
|
|
||||||
[
|
|
||||||
'~/.ccs/config.yaml',
|
|
||||||
'Optional: continuity.inherit_from_account maps API/CLIProxy/copilot/default profiles to an account context',
|
|
||||||
],
|
|
||||||
['ccs auth list', 'List all account profiles'],
|
|
||||||
['ccs auth default <name>', 'Set default profile'],
|
|
||||||
['ccs auth reset-default', 'Restore original CCS default'],
|
|
||||||
['ccs cliproxy auth claude', 'Alternative: authenticate Claude account pool via CLIProxy'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// MAJOR SECTION 3: CLI Proxy (OAuth Providers)
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
printMajorSection(
|
|
||||||
'CLI Proxy Plus (OAuth Providers)',
|
|
||||||
[
|
|
||||||
'Zero-config OAuth authentication via CLIProxy Plus',
|
|
||||||
'First run: Browser opens for authentication, then model selection',
|
|
||||||
'Settings: ~/.ccs/{provider}.settings.json (created after auth)',
|
|
||||||
'Safety: do not reuse one Google account across "ccs gemini" and "ccs agy" (issue #509)',
|
|
||||||
'Antigravity requires multi-step responsibility confirmation (issue #509)',
|
|
||||||
'If you want to keep Google AI access, do not continue this shared-account setup',
|
|
||||||
'CCS is as-is and does not take responsibility for account bans/access loss',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
['ccs gemini', 'Google Gemini (gemini-2.5-pro or 3-pro)'],
|
|
||||||
['ccs codex', 'OpenAI Codex (supports -medium/-high/-xhigh model suffixes)'],
|
|
||||||
['ccs agy', 'Antigravity (Claude/Gemini models)'],
|
|
||||||
['ccs qwen', 'Qwen Code OAuth (CLIProxy)'],
|
|
||||||
['ccs kimi', 'Kimi (Moonshot AI K2/K2.5 models)'],
|
|
||||||
['ccs kiro', 'Kiro (AWS CodeWhisperer Claude models)'],
|
|
||||||
['ccs ghcp', 'GitHub Copilot (OAuth via CLIProxy Plus)'],
|
|
||||||
['', ''], // Spacer
|
|
||||||
['ccs <provider> --auth', 'Authenticate only'],
|
|
||||||
['ccs <provider> --auth --add', 'Add another account'],
|
|
||||||
[
|
|
||||||
'ccs <provider> --paste-callback',
|
|
||||||
'Show auth URL and prompt for callback paste (cross-browser)',
|
|
||||||
],
|
|
||||||
['ccs <provider> --accounts', 'List all accounts'],
|
|
||||||
['ccs <provider> --use <nickname-or-id>', 'Switch to account'],
|
|
||||||
['ccs <provider> --config', 'Change model (agy, gemini)'],
|
|
||||||
[
|
|
||||||
'ccs agy --accept-agr-risk',
|
|
||||||
'Bypass interactive Antigravity confirmation (you accept full responsibility)',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'ccs <provider> --thinking <value>',
|
|
||||||
'Set thinking budget (low/medium/high/xhigh/auto/off or number)',
|
|
||||||
],
|
|
||||||
['ccs codex --effort <level>', 'Set codex reasoning effort (minimal/low/medium/high/xhigh)'],
|
|
||||||
['ccs <provider> --1m', 'Request explicit 1M context when the selected model supports [1m]'],
|
|
||||||
['ccs <provider> --no-1m', 'Force standard context / clear [1m]'],
|
|
||||||
['ccs <provider> --logout', 'Clear authentication'],
|
|
||||||
['ccs <provider> --headless', 'Headless auth (for SSH)'],
|
|
||||||
['ccs <provider> --port-forward', 'Force port-forwarding mode (skip prompt)'],
|
|
||||||
['ccs kiro --auth --kiro-auth-method aws', 'Kiro via AWS Builder ID (device code)'],
|
|
||||||
['ccs kiro --auth --kiro-auth-method aws-authcode', 'Kiro via AWS auth code flow'],
|
|
||||||
['ccs kiro --auth --kiro-auth-method google', 'Kiro via Google OAuth'],
|
|
||||||
['ccs kiro --auth --kiro-auth-method github', 'Kiro via GitHub OAuth (Dashboard flow)'],
|
|
||||||
['ccs kiro --import', 'Import token from Kiro IDE'],
|
|
||||||
['ccs kiro --incognito', 'Use incognito browser (default: normal)'],
|
|
||||||
['ccs codex "explain code"', 'Use with prompt'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// MAJOR SECTION 4: GitHub Copilot Integration (copilot-api)
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
printMajorSection(
|
|
||||||
'GitHub Copilot Integration (copilot-api)',
|
|
||||||
[
|
|
||||||
'Use your GitHub Copilot subscription with Claude Code via copilot-api',
|
|
||||||
'Requires: npm install -g copilot-api',
|
|
||||||
'Note: For OAuth-based access, use ccs ghcp instead',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
['ccs copilot', 'Use Copilot via copilot-api daemon'],
|
|
||||||
['ccs copilot auth', 'Authenticate with GitHub'],
|
|
||||||
['ccs copilot status', 'Show integration status'],
|
|
||||||
['ccs copilot models', 'List available models'],
|
|
||||||
['ccs copilot usage', 'Show Copilot quota usage'],
|
|
||||||
['ccs copilot start', 'Start copilot-api daemon'],
|
|
||||||
['ccs copilot stop', 'Stop copilot-api daemon'],
|
|
||||||
['ccs copilot enable', 'Enable integration'],
|
|
||||||
['ccs copilot disable', 'Disable integration'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// MAJOR SECTION 5: Cursor IDE Integration
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
printMajorSection(
|
|
||||||
'Cursor IDE Integration',
|
|
||||||
[
|
|
||||||
'Use Cursor IDE with Claude Code via cursor proxy daemon',
|
|
||||||
'Auto-detects token from Cursor installation',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
['ccs cursor', 'Run Claude via Cursor local proxy'],
|
|
||||||
['ccs cursor auth', 'Import Cursor token'],
|
|
||||||
['ccs cursor auth --manual --token <t> --machine-id <id>', 'Manual token import'],
|
|
||||||
['ccs cursor status', 'Show connection status'],
|
|
||||||
['ccs cursor models', 'List available models'],
|
|
||||||
['ccs cursor start', 'Start proxy daemon'],
|
|
||||||
['ccs cursor stop', 'Stop proxy daemon'],
|
|
||||||
['ccs cursor enable', 'Enable cursor integration'],
|
|
||||||
['ccs cursor disable', 'Disable cursor integration'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
// SUB-SECTIONS (simpler styling)
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// Delegation
|
|
||||||
printSubSection(
|
|
||||||
'Delegation (inside Claude Code CLI)',
|
|
||||||
[
|
|
||||||
['/ccs "task"', 'Delegate task (auto-selects profile)'],
|
|
||||||
['/ccs --glm "task"', 'Force GLM-5 for simple tasks'],
|
|
||||||
['/ccs --kimi "task"', 'Force Kimi OAuth for long context'],
|
|
||||||
['/ccs --km "task"', 'Force Kimi API key for long context'],
|
|
||||||
['/ccs:continue "follow-up"', 'Continue last delegation session'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delegation CLI Flags (Claude Code passthrough)
|
|
||||||
printSubSection(
|
|
||||||
'Delegation Flags (Claude Code passthrough)',
|
|
||||||
[
|
|
||||||
['--max-turns <n>', 'Limit agentic turns (prevents loops)'],
|
|
||||||
['--fallback-model <model>', 'Auto-fallback on overload (sonnet)'],
|
|
||||||
['--agents <json>', 'Inject dynamic subagents'],
|
|
||||||
['--betas <features>', 'Enable experimental features'],
|
|
||||||
['--allowedTools <list>', 'Restrict available tools'],
|
|
||||||
['--disallowedTools <list>', 'Block specific tools'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// Diagnostics
|
|
||||||
printSubSection(
|
|
||||||
'Diagnostics',
|
|
||||||
[
|
|
||||||
['ccs setup', 'First-time setup wizard'],
|
|
||||||
['ccs doctor', 'Run health check and diagnostics'],
|
|
||||||
['ccs cleanup', 'Remove old CLIProxy logs'],
|
|
||||||
['ccs config', 'Open web dashboard (includes Claude IDE Extension setup page)'],
|
|
||||||
['ccs config auth setup', 'Configure dashboard login'],
|
|
||||||
['ccs config auth show', 'Show dashboard auth status'],
|
|
||||||
['ccs config channels', 'Show Official Channels status'],
|
|
||||||
[
|
|
||||||
'ccs config channels --set telegram,discord',
|
|
||||||
'Auto-add Telegram + Discord on supported native Claude runs',
|
|
||||||
],
|
|
||||||
['ccs config channels --set-token telegram=<token>', 'Save TELEGRAM_BOT_TOKEN'],
|
|
||||||
['ccs config image-analysis', 'Show image analysis settings'],
|
|
||||||
['ccs config image-analysis --enable', 'Enable image analysis'],
|
|
||||||
['ccs config thinking', 'Show thinking/reasoning settings'],
|
|
||||||
['ccs config thinking --mode auto', 'Set thinking mode'],
|
|
||||||
['ccs config thinking --clear-provider-override codex', 'Clear provider overrides'],
|
|
||||||
['ccs config --port 3000', 'Use specific port'],
|
|
||||||
['ccs config --host 0.0.0.0', 'Force all-interface binding for remote devices'],
|
|
||||||
['ccs persist <profile>', 'Write profile setup to ~/.claude/settings.json'],
|
|
||||||
['ccs persist --list-backups', 'List available settings.json backups'],
|
|
||||||
['ccs persist --restore', 'Restore settings.json from latest backup'],
|
|
||||||
['ccs sync', 'Sync delegation commands and skills'],
|
|
||||||
['ccs update', 'Update CCS to latest version'],
|
|
||||||
['ccs update --force', 'Force reinstall current version'],
|
|
||||||
['ccs update --beta', 'Install from dev channel (unstable)'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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> --format claude-extension --ide vscode',
|
|
||||||
'VS Code/Cursor Claude extension settings JSON',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'ccs env <profile> --format claude-extension --ide windsurf',
|
|
||||||
'Windsurf Claude extension settings JSON',
|
|
||||||
],
|
|
||||||
['ccs env <profile> --shell fish', 'Fish shell syntax'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// Flags
|
|
||||||
printSubSection(
|
|
||||||
'Flags',
|
|
||||||
[
|
|
||||||
['--config-dir <path>', 'Use custom CCS config directory'],
|
|
||||||
['--target <cli>', 'Target CLI: claude (default), droid, codex (runtime-only)'],
|
|
||||||
['-h, --help', 'Show this help message'],
|
|
||||||
['-v, --version', 'Show version and installation info'],
|
|
||||||
['-sc, --shell-completion', 'Install shell auto-completion'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// Aliases
|
|
||||||
printSubSection(
|
|
||||||
'Aliases',
|
|
||||||
[
|
|
||||||
['ccs-droid <profile> [args]', 'Explicit Droid runtime alias'],
|
|
||||||
['ccsd <profile> [args]', 'Legacy shortcut for: ccs-droid <profile> [args]'],
|
|
||||||
['ccs-codex <profile> [args]', 'Explicit Codex runtime alias'],
|
|
||||||
['ccsx <profile> [args]', 'Short alias for: ccs-codex <profile> [args]'],
|
|
||||||
['ccsxp [args]', 'Shortcut for: ccs codex --target codex [args]'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// Multi-target examples
|
|
||||||
printSubSection(
|
|
||||||
'Multi-Target',
|
|
||||||
[
|
|
||||||
['ccs glm --target droid', 'Run GLM profile on Droid CLI'],
|
|
||||||
['ccs-droid glm', 'Same as above (explicit alias)'],
|
|
||||||
['ccsd glm', 'Legacy shortcut for ccs-droid'],
|
|
||||||
['ccs --target codex', 'Open a native Codex session with your existing ~/.codex setup'],
|
|
||||||
['ccs-codex', 'Same as above (explicit Codex alias)'],
|
|
||||||
['ccsx', 'Short alias for ccs-codex'],
|
|
||||||
['ccsxp "your prompt"', 'Run built-in CLIProxy Codex on native Codex CLI'],
|
|
||||||
['ccs codex --target codex', 'Explicit form of ccsxp'],
|
|
||||||
[
|
|
||||||
'ccs api create codex-api --cliproxy-provider codex',
|
|
||||||
'Create a routed API bridge that can also run on Codex',
|
|
||||||
],
|
|
||||||
['ccs codex-api --target codex', 'Run a Codex bridge profile on native Codex CLI'],
|
|
||||||
['ccs-droid codex', 'Run built-in CLIProxy Codex profile on Droid'],
|
|
||||||
['ccs-droid agy', 'Run built-in CLIProxy Antigravity profile on Droid'],
|
|
||||||
[
|
|
||||||
'ccs-droid codex exec --skip-permissions-unsafe "fix failing tests"',
|
|
||||||
'Pass through Droid exec mode',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'ccs-droid codex -m custom:gpt-5.3-codex "fix failing tests"',
|
|
||||||
'Auto-routes short exec flags',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'ccs-droid codex --skip-permissions-unsafe "fix failing tests"',
|
|
||||||
'Auto-routes to Droid exec when exec-only flags are detected',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'ccs cliproxy create my-codex --provider codex --target droid',
|
|
||||||
'Create CLIProxy variant with Droid as default target',
|
|
||||||
],
|
|
||||||
['ccs glm', 'Run GLM profile on Claude Code (default)'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
printSubSection(
|
|
||||||
'Codex + CLIProxy',
|
|
||||||
[
|
|
||||||
['ccsxp "your prompt"', 'Use the built-in CCS Codex provider shortcut on native Codex'],
|
|
||||||
['ccs config', 'Open Compatible -> Codex CLI for native Codex setup and diagnostics'],
|
|
||||||
[
|
|
||||||
'Default provider',
|
|
||||||
'Set to cliproxy if plain codex or a personal cxp alias should use CLIProxy',
|
|
||||||
],
|
|
||||||
['Model provider auth', 'Save cliproxy with env_key = "CLIPROXY_API_KEY"'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
printConfigSection(
|
|
||||||
'Configuration',
|
|
||||||
[
|
|
||||||
['Config File:', isUnifiedMode() ? `${dirDisplay}/config.yaml` : `${dirDisplay}/config.json`],
|
|
||||||
['Profiles:', `${dirDisplay}/profiles.json`],
|
|
||||||
['Instances:', `${dirDisplay}/instances/`],
|
|
||||||
['Settings:', `${dirDisplay}/*.settings.json`],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// CLI Proxy management
|
|
||||||
printSubSection(
|
|
||||||
'CLI Proxy Plus Management',
|
|
||||||
[
|
|
||||||
['ccs cliproxy', 'Show CLIProxy Plus status and version'],
|
|
||||||
['ccs cliproxy --help', 'Full CLIProxy Plus management help'],
|
|
||||||
['ccs cliproxy doctor', 'Quota diagnostics (Antigravity)'],
|
|
||||||
['ccs cliproxy --install <ver>', 'Install specific version (e.g., 6.6.6)'],
|
|
||||||
['ccs cliproxy --latest', 'Update to latest version'],
|
|
||||||
['', ''], // Spacer
|
|
||||||
['ccs cliproxy pause <p> <a>', 'Pause account from rotation'],
|
|
||||||
['ccs cliproxy resume <p> <a>', 'Resume paused account'],
|
|
||||||
['ccs cliproxy status', 'Show CLIProxy process status'],
|
|
||||||
['ccs cliproxy quota', 'Show quota/tier/pause status for all providers'],
|
|
||||||
['ccs cliproxy quota --provider <name>', 'Show quota/tier/pause status for one provider'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
printSubSection(
|
|
||||||
'Docker Deployment',
|
|
||||||
[
|
|
||||||
['ccs docker --help', 'Manage the integrated CCS + CLIProxy Docker stack'],
|
|
||||||
['ccs docker up', 'Build and start the stack locally'],
|
|
||||||
['ccs docker up --host <ssh>', 'Stage assets to ~/.ccs/docker and deploy remotely'],
|
|
||||||
['ccs docker status', 'Show docker compose and supervisor status'],
|
|
||||||
['ccs docker logs --follow', 'Tail combined CCS + CLIProxy logs'],
|
|
||||||
['ccs docker update', 'Update CCS and CLIProxy inside the running container'],
|
|
||||||
['ccs docker config', 'Show bundled asset paths and deployment defaults'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// CLI Proxy configuration flags (new)
|
|
||||||
printSubSection(
|
|
||||||
'CLI Proxy Configuration',
|
|
||||||
[
|
|
||||||
['--proxy-host <host>', 'Remote proxy hostname/IP'],
|
|
||||||
['--proxy-port <port>', `Proxy port (default: ${CLIPROXY_DEFAULT_PORT})`],
|
|
||||||
['--proxy-protocol <proto>', 'Protocol: http or https (default: http)'],
|
|
||||||
['--proxy-auth-token <token>', 'Auth token for remote proxy'],
|
|
||||||
['--proxy-timeout <ms>', 'Connection timeout in ms (default: 2000)'],
|
|
||||||
['--local-proxy', 'Force local mode, ignore remote config'],
|
|
||||||
['--remote-only', 'Fail if remote unreachable (no fallback)'],
|
|
||||||
['--allow-self-signed', 'Allow self-signed certs (for dev proxies)'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// W3: Thinking Budget explanation
|
|
||||||
printSubSection(
|
|
||||||
'Extended Thinking / Reasoning',
|
|
||||||
[
|
|
||||||
['--thinking off', 'Disable extended thinking'],
|
|
||||||
['--thinking auto', 'Let model decide dynamically'],
|
|
||||||
['--thinking low', '1K tokens - Quick responses'],
|
|
||||||
['--thinking medium', '8K tokens - Standard analysis'],
|
|
||||||
['--thinking high', '24K tokens - Deep reasoning'],
|
|
||||||
['--thinking xhigh', '32K tokens - Maximum depth'],
|
|
||||||
['--thinking <number>', 'Custom token budget (512-100000)'],
|
|
||||||
['', ''],
|
|
||||||
['--effort <level>', 'Codex alias for reasoning effort (minimal/low/medium/high/xhigh)'],
|
|
||||||
['--effort xhigh', 'Pin Codex effort to xhigh for this run'],
|
|
||||||
['', ''],
|
|
||||||
['Droid exec:', 'Use native Droid flag: --reasoning-effort <level>'],
|
|
||||||
['', 'CCS auto-maps --thinking/--effort to --reasoning-effort in droid exec mode.'],
|
|
||||||
['', 'For interactive droid sessions, CCS applies reasoning via Droid BYOK model config.'],
|
|
||||||
['', 'When multiple reasoning flags are provided, the first flag wins.'],
|
|
||||||
['', ''],
|
|
||||||
['Note:', 'Extended thinking allocates compute for step-by-step reasoning'],
|
|
||||||
['', 'before responding.'],
|
|
||||||
['', 'Providers: agy/gemini use --thinking, codex uses --effort (or --thinking alias).'],
|
|
||||||
['', 'Codex model suffixes also pin effort: -medium / -high / -xhigh.'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// Extended Context (1M)
|
|
||||||
printSubSection(
|
|
||||||
'Extended Context (--1m)',
|
|
||||||
[
|
|
||||||
['--1m', 'Request 1M token context when the selected model supports [1m]'],
|
|
||||||
['--no-1m', 'Force standard context (Claude default stays plain)'],
|
|
||||||
['', ''],
|
|
||||||
['Auto behavior:', 'Gemini models: CCS auto-adds [1m] when supported'],
|
|
||||||
['', 'Claude models: plain by default, opt-in with --1m or saved [1m]'],
|
|
||||||
['', ''],
|
|
||||||
['Note:', 'CCS only controls the saved [1m] suffix.'],
|
|
||||||
['', 'Provider pricing and entitlement stay upstream.'],
|
|
||||||
[
|
|
||||||
'',
|
|
||||||
'Some accounts/providers can still return 429 extra-usage errors for long-context requests.',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// Image Analysis
|
|
||||||
printSubSection(
|
|
||||||
'Image Analysis (first-class local tool)',
|
|
||||||
[
|
|
||||||
['ccs config image-analysis', 'Show current settings'],
|
|
||||||
['ccs config image-analysis --enable', 'Enable for CLIProxy providers'],
|
|
||||||
['ccs config image-analysis --disable', 'Disable (use native Read)'],
|
|
||||||
['ccs config image-analysis --timeout 120', 'Set analysis timeout'],
|
|
||||||
['ccs config image-analysis --set-model <p> <m>', 'Set provider model'],
|
|
||||||
['', ''],
|
|
||||||
['Note:', 'When ready, third-party launches expose the local ImageAnalysis MCP tool'],
|
|
||||||
['', 'and route requests directly to the resolved CCS provider path.'],
|
|
||||||
['', 'If runtime/auth/proxy is unavailable, CCS falls back to native Read.'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
printSubSection(
|
|
||||||
'Official Channels (official Claude plugins)',
|
|
||||||
[
|
|
||||||
['ccs config', 'Dashboard -> Settings -> Channels (fastest path)'],
|
|
||||||
['ccs config channels', 'Show current status'],
|
|
||||||
[
|
|
||||||
'ccs config channels --set telegram,discord',
|
|
||||||
'Auto-add selected channels on native Claude default/account sessions',
|
|
||||||
],
|
|
||||||
['ccs config channels --set all', 'Enable Telegram, Discord, and iMessage'],
|
|
||||||
['ccs config channels --unattended', 'Also add --dangerously-skip-permissions'],
|
|
||||||
['ccs config channels --set-token telegram=<token>', 'Save TELEGRAM_BOT_TOKEN'],
|
|
||||||
['ccs config channels --set-token discord=<token>', 'Save DISCORD_BOT_TOKEN'],
|
|
||||||
['ccs config channels --clear-token [channel]', 'Remove one or all saved channel tokens'],
|
|
||||||
['', ''],
|
|
||||||
['', 'Fastest path: turn on the channel, save the token if needed, then run ccs.'],
|
|
||||||
['Note:', getOfficialChannelsSupportMessage()],
|
|
||||||
['', 'Telegram/Discord tokens live in ~/.claude/channels/<channel>/.env.'],
|
|
||||||
['', 'Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work for that launch.'],
|
|
||||||
['', 'iMessage is macOS-only and requires local OS permissions instead of a bot token.'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// CCS Environment Variables
|
|
||||||
printSubSection(
|
|
||||||
'Environment Variables',
|
|
||||||
[
|
|
||||||
['CCS_DIR', 'Override CCS config directory (default: ~/.ccs)'],
|
|
||||||
['CCS_HOME', 'Override home directory (legacy, appends .ccs)'],
|
|
||||||
['CCS_DEBUG', 'Enable debug logging'],
|
|
||||||
['CCS_THINKING', 'Override thinking level (flag > env > config)'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// CLI Proxy env vars
|
|
||||||
printSubSection(
|
|
||||||
'CLI Proxy Environment Variables',
|
|
||||||
[
|
|
||||||
['CCS_PROXY_HOST', 'Remote proxy hostname'],
|
|
||||||
['CCS_PROXY_PORT', 'Proxy port'],
|
|
||||||
['CCS_PROXY_PROTOCOL', 'Protocol (http/https)'],
|
|
||||||
['CCS_PROXY_AUTH_TOKEN', 'Auth token'],
|
|
||||||
['CCS_PROXY_TIMEOUT', 'Connection timeout in ms'],
|
|
||||||
['CCS_PROXY_FALLBACK_ENABLED', 'Enable local fallback (1/0)'],
|
|
||||||
['CCS_ALLOW_SELF_SIGNED', 'Allow self-signed certs (1/0)'],
|
|
||||||
],
|
|
||||||
writeLine
|
|
||||||
);
|
|
||||||
|
|
||||||
// CLI Proxy paths
|
|
||||||
writeLine(subheader('CLI Proxy:'));
|
|
||||||
writeLine(` Binary: ${color(`${dirDisplay}/cliproxy/bin/cli-proxy-api-plus`, 'path')}`);
|
|
||||||
writeLine(` Config: ${color(`${dirDisplay}/cliproxy/config.yaml`, 'path')}`);
|
|
||||||
writeLine(` Auth: ${color(`${dirDisplay}/cliproxy/auth/`, 'path')}`);
|
|
||||||
writeLine(` ${dim(`Port: ${CLIPROXY_DEFAULT_PORT} (default)`)}`);
|
|
||||||
writeLine('');
|
|
||||||
|
|
||||||
// Shared Data
|
|
||||||
writeLine(subheader('Shared Data:'));
|
|
||||||
writeLine(` Commands: ${color(`${dirDisplay}/shared/commands/`, 'path')}`);
|
|
||||||
writeLine(` Skills: ${color(`${dirDisplay}/shared/skills/`, 'path')}`);
|
|
||||||
writeLine(` Agents: ${color(`${dirDisplay}/shared/agents/`, 'path')}`);
|
|
||||||
writeLine(` ${dim('Note: Symlinked across all profiles')}`);
|
|
||||||
writeLine('');
|
|
||||||
|
|
||||||
// Examples (aligned with consistent spacing)
|
|
||||||
writeLine(subheader('Examples:'));
|
|
||||||
writeLine(` $ ${color('ccs', 'command')} ${dim('# Use default account')}`);
|
|
||||||
writeLine(
|
|
||||||
` $ ${color('ccs gemini', 'command')} ${dim('# OAuth (browser opens first time)')}`
|
|
||||||
);
|
|
||||||
writeLine(` $ ${color('ccs glm "implement API"', 'command')} ${dim('# API key model')}`);
|
|
||||||
writeLine(` $ ${color('ccs config', 'command')} ${dim('# Open web dashboard')}`);
|
|
||||||
writeLine('');
|
|
||||||
|
|
||||||
// Update examples
|
|
||||||
writeLine(subheader('Update:'));
|
|
||||||
writeLine(
|
|
||||||
` $ ${color('ccs update', 'command')} ${dim('# Update to latest stable')}`
|
|
||||||
);
|
|
||||||
writeLine(
|
|
||||||
` $ ${color('ccs update --force', 'command')} ${dim('# Force reinstall current')}`
|
|
||||||
);
|
|
||||||
writeLine(` $ ${color('ccs update --beta', 'command')} ${dim('# Install dev channel')}`);
|
|
||||||
writeLine('');
|
|
||||||
|
|
||||||
// Docs link
|
|
||||||
writeLine(` ${dim('Docs: https://github.com/kaitranntt/ccs')}`);
|
|
||||||
writeLine('');
|
|
||||||
|
|
||||||
// Uninstall
|
|
||||||
writeLine(subheader('Uninstall:'));
|
|
||||||
writeLine(` ${color('npm uninstall -g @kaitranntt/ccs', 'command')}`);
|
|
||||||
writeLine('');
|
|
||||||
|
|
||||||
// License
|
|
||||||
writeLine(dim('License: MIT'));
|
|
||||||
writeLine('');
|
writeLine('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listHelpTargets(): string {
|
||||||
|
return ROOT_HELP_TOPICS.map((topic) => topic.name).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleHelpRoute(
|
||||||
|
args: string[],
|
||||||
|
writeLine: HelpWriter = console.log
|
||||||
|
): Promise<void> {
|
||||||
|
const topic = args[0] as HelpTopicName | undefined;
|
||||||
|
if (!topic) {
|
||||||
|
await handleHelpCommand(writeLine);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topic === 'profiles') {
|
||||||
|
await showProfilesHelp(writeLine);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (topic === 'providers') {
|
||||||
|
await showProvidersHelp(writeLine);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (topic === 'targets') {
|
||||||
|
await showTargetsHelp(writeLine);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (topic === 'completion') {
|
||||||
|
const { showShellCompletionHelp } = await import('./shell-completion-command');
|
||||||
|
showShellCompletionHelp(writeLine);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandHandlers: Partial<Record<string, () => Promise<void>>> = {
|
||||||
|
api: async () => (await import('./api-command/help')).showApiCommandHelp(writeLine),
|
||||||
|
auth: async () => {
|
||||||
|
const authModule = await import('../auth/auth-commands');
|
||||||
|
const AuthCommands = authModule.default;
|
||||||
|
await new AuthCommands().showHelp();
|
||||||
|
},
|
||||||
|
cleanup: async () => (await import('./cleanup-command')).handleCleanupCommand(['--help']),
|
||||||
|
cliproxy: async () => (await import('./cliproxy/help-subcommand')).showHelp(),
|
||||||
|
copilot: async () =>
|
||||||
|
process.exit(await (await import('./copilot-command')).handleCopilotCommand(['--help'])),
|
||||||
|
cursor: async () =>
|
||||||
|
process.exit(await (await import('./cursor-command')).handleCursorCommand(['--help'])),
|
||||||
|
docker: async () => (await import('./docker/help-subcommand')).showHelp(),
|
||||||
|
migrate: async () => (await import('./migrate-command')).printMigrateHelp(),
|
||||||
|
setup: async () => (await import('./setup-command')).handleSetupCommand(['--help']),
|
||||||
|
tokens: async () =>
|
||||||
|
process.exit(await (await import('./tokens-command')).handleTokensCommand(['--help'])),
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = commandHandlers[topic];
|
||||||
|
if (handler) {
|
||||||
|
await handler();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await initUI();
|
||||||
|
writeLine(color(`Unknown help topic or command: ${topic}`, 'error'));
|
||||||
|
writeLine('');
|
||||||
|
writeLine(` ${dim(`Available help topics: ${listHelpTargets()}`)}`);
|
||||||
|
writeLine('');
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRootHelpVisibleCommands(): string[] {
|
||||||
|
return getPublicRootCommands().map((entry) => entry.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRootHelpCatalogEntries(): readonly RootCommandEntry[] {
|
||||||
|
return ROOT_COMMAND_CATALOG;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ async function printUpdateCommandHelp(): Promise<void> {
|
|||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
|
export const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
|
||||||
{
|
{
|
||||||
name: 'migrate',
|
name: 'migrate',
|
||||||
aliases: ['--migrate'],
|
aliases: ['--migrate'],
|
||||||
@@ -55,9 +55,9 @@ const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
|
|||||||
{
|
{
|
||||||
name: 'help',
|
name: 'help',
|
||||||
aliases: ['--help', '-h'],
|
aliases: ['--help', '-h'],
|
||||||
handle: async () => {
|
handle: async (args) => {
|
||||||
const { handleHelpCommand } = await import('./help-command');
|
const { handleHelpRoute } = await import('./help-command');
|
||||||
await handleHelpCommand();
|
await handleHelpRoute(args);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -82,6 +82,13 @@ const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
|
|||||||
await handleShellCompletionCommand(args);
|
await handleShellCompletionCommand(args);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '__complete',
|
||||||
|
handle: async (args) => {
|
||||||
|
const { handleCompletionCommand } = await import('./completion-backend');
|
||||||
|
await handleCompletionCommand(args);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'doctor',
|
name: 'doctor',
|
||||||
aliases: ['--doctor'],
|
aliases: ['--doctor'],
|
||||||
|
|||||||
@@ -28,6 +28,24 @@ interface ShellCompletionInstallerLike {
|
|||||||
install(shell: ShellTarget, options: { force: boolean }): ShellCompletionInstallResult;
|
install(shell: ShellTarget, options: { force: boolean }): ShellCompletionInstallResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function showShellCompletionHelp(writeLine: (line: string) => void = console.log): void {
|
||||||
|
writeLine(header('Shell Completion'));
|
||||||
|
writeLine('');
|
||||||
|
writeLine(color('Usage:', 'info'));
|
||||||
|
writeLine(' ccs --shell-completion # Auto-detect shell and install');
|
||||||
|
writeLine(' ccs --shell-completion --bash # Install for bash');
|
||||||
|
writeLine(' ccs --shell-completion --zsh # Install for zsh');
|
||||||
|
writeLine(' ccs --shell-completion --fish # Install for fish');
|
||||||
|
writeLine(' ccs --shell-completion --powershell # Install for PowerShell');
|
||||||
|
writeLine(' ccs --shell-completion --force # Reinstall/refresh the active shell setup');
|
||||||
|
writeLine('');
|
||||||
|
writeLine(color('Test:', 'info'));
|
||||||
|
writeLine(' ccs <TAB>');
|
||||||
|
writeLine(' ccs help <TAB>');
|
||||||
|
writeLine(' ccs auth <TAB>');
|
||||||
|
writeLine('');
|
||||||
|
}
|
||||||
|
|
||||||
export function parseShellCompletionArgs(args: string[]): ShellCompletionParsedArgs {
|
export function parseShellCompletionArgs(args: string[]): ShellCompletionParsedArgs {
|
||||||
let targetShell: ShellTarget = null;
|
let targetShell: ShellTarget = null;
|
||||||
const force = args.includes('--force') || args.includes('-f');
|
const force = args.includes('--force') || args.includes('-f');
|
||||||
@@ -77,12 +95,15 @@ export function createShellCompletionCommandContract(
|
|||||||
*/
|
*/
|
||||||
export async function handleShellCompletionCommand(args: string[]): Promise<void> {
|
export async function handleShellCompletionCommand(args: string[]): Promise<void> {
|
||||||
await initUI();
|
await initUI();
|
||||||
const { ShellCompletionInstaller } = await import('../utils/shell-completion');
|
if (args.includes('--help') || args.includes('-h')) {
|
||||||
|
showShellCompletionHelp();
|
||||||
console.log(header('Shell Completion Installer'));
|
return;
|
||||||
console.log('');
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const { ShellCompletionInstaller } = await import('../utils/shell-completion');
|
||||||
|
console.log(header('Shell Completion Installer'));
|
||||||
|
console.log('');
|
||||||
const installer = new ShellCompletionInstaller();
|
const installer = new ShellCompletionInstaller();
|
||||||
const contract = createShellCompletionCommandContract(installer);
|
const contract = createShellCompletionCommandContract(installer);
|
||||||
await runCommandWithContract(args, contract);
|
await runCommandWithContract(args, contract);
|
||||||
@@ -90,13 +111,7 @@ export async function handleShellCompletionCommand(args: string[]): Promise<void
|
|||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
console.error(fail(`Error: ${err.message}`));
|
console.error(fail(`Error: ${err.message}`));
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error(color('Usage:', 'warning'));
|
showShellCompletionHelp(console.error);
|
||||||
console.error(' ccs --shell-completion # Auto-detect shell');
|
|
||||||
console.error(' ccs --shell-completion --bash # Install for bash');
|
|
||||||
console.error(' ccs --shell-completion --zsh # Install for zsh');
|
|
||||||
console.error(' ccs --shell-completion --fish # Install for fish');
|
|
||||||
console.error(' ccs --shell-completion --powershell # Install for PowerShell');
|
|
||||||
console.error('');
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
|
import { ROOT_COMMAND_CATALOG, getAllRootCommandTokens } from '../../../src/commands/command-catalog';
|
||||||
|
import { ROOT_COMMAND_ROUTES } from '../../../src/commands/root-command-router';
|
||||||
|
|
||||||
|
describe('command catalog', () => {
|
||||||
|
test('covers every routed root command and alias', () => {
|
||||||
|
const catalogTokens = new Set(getAllRootCommandTokens());
|
||||||
|
|
||||||
|
for (const route of ROOT_COMMAND_ROUTES) {
|
||||||
|
expect(catalogTokens.has(route.name)).toBe(true);
|
||||||
|
for (const alias of route.aliases || []) {
|
||||||
|
expect(catalogTokens.has(alias)).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps hidden operational hooks out of the public help surface', () => {
|
||||||
|
const hiddenCommands = ROOT_COMMAND_CATALOG.filter((entry) => entry.visibility === 'hidden').map(
|
||||||
|
(entry) => entry.name
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(hiddenCommands).toContain('--install');
|
||||||
|
expect(hiddenCommands).toContain('--uninstall');
|
||||||
|
expect(hiddenCommands).toContain('__complete');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { getCompletionSuggestions } from '../../../src/commands/completion-backend';
|
||||||
|
|
||||||
|
const originalHome = process.env.HOME;
|
||||||
|
const originalUserProfile = process.env.USERPROFILE;
|
||||||
|
const originalCcsHome = process.env.CCS_HOME;
|
||||||
|
let tempHome = '';
|
||||||
|
|
||||||
|
function suggestionValues(
|
||||||
|
tokensBeforeCurrent: string[],
|
||||||
|
current = '',
|
||||||
|
shell: 'bash' | 'fish' | 'powershell' | 'zsh' = 'bash'
|
||||||
|
): string[] {
|
||||||
|
return getCompletionSuggestions({ tokensBeforeCurrent, current, shell }).map((entry) => entry.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-completion-'));
|
||||||
|
fs.mkdirSync(path.join(tempHome, '.ccs'), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempHome, '.ccs', 'config.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
profiles: {
|
||||||
|
localglm: '~/.ccs/localglm.settings.json',
|
||||||
|
},
|
||||||
|
cliproxy: {
|
||||||
|
'my-codex': {
|
||||||
|
provider: 'codex',
|
||||||
|
settings: '~/.ccs/my-codex.settings.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tempHome, '.ccs', 'profiles.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
profiles: {
|
||||||
|
work: { type: 'account', created: '2026-04-02T00:00:00.000Z' },
|
||||||
|
},
|
||||||
|
default: 'work',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
process.env.HOME = tempHome;
|
||||||
|
process.env.USERPROFILE = tempHome;
|
||||||
|
process.env.CCS_HOME = tempHome;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalHome === undefined) delete process.env.HOME;
|
||||||
|
else process.env.HOME = originalHome;
|
||||||
|
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
||||||
|
else process.env.USERPROFILE = originalUserProfile;
|
||||||
|
if (originalCcsHome === undefined) delete process.env.CCS_HOME;
|
||||||
|
else process.env.CCS_HOME = originalCcsHome;
|
||||||
|
|
||||||
|
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('completion backend', () => {
|
||||||
|
test('includes current root commands and dynamic profiles at the top level', () => {
|
||||||
|
const values = suggestionValues([]);
|
||||||
|
|
||||||
|
expect(values).toContain('config');
|
||||||
|
expect(values).toContain('docker');
|
||||||
|
expect(values).toContain('tokens');
|
||||||
|
expect(values).toContain('migrate');
|
||||||
|
expect(values).toContain('cursor');
|
||||||
|
expect(values).toContain('copilot');
|
||||||
|
expect(values).toContain('gemini');
|
||||||
|
expect(values).toContain('localglm');
|
||||||
|
expect(values).toContain('work');
|
||||||
|
expect(values).toContain('my-codex');
|
||||||
|
expect(values).not.toContain('__complete');
|
||||||
|
expect(values).not.toContain('--install');
|
||||||
|
expect(values).not.toContain('--uninstall');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('suggests help topics from the help command', () => {
|
||||||
|
const values = suggestionValues(['help']);
|
||||||
|
expect(values).toContain('profiles');
|
||||||
|
expect(values).toContain('providers');
|
||||||
|
expect(values).toContain('completion');
|
||||||
|
expect(values).toContain('targets');
|
||||||
|
expect(values).not.toContain('api');
|
||||||
|
expect(values).not.toContain('__complete');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('suggests lifecycle subcommands for api', () => {
|
||||||
|
const values = suggestionValues(['api']);
|
||||||
|
expect(values).toEqual(expect.arrayContaining(['create', 'discover', 'copy', 'export', 'import', 'remove']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('suggests account profiles for auth show', () => {
|
||||||
|
const values = suggestionValues(['auth', 'show']);
|
||||||
|
expect(values).toContain('work');
|
||||||
|
expect(values).toContain('--json');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('suggests cliproxy variants for variant-scoped commands', () => {
|
||||||
|
const values = suggestionValues(['cliproxy', 'edit']);
|
||||||
|
expect(values).toContain('my-codex');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('suggests env format values after the format flag', () => {
|
||||||
|
const values = suggestionValues(['env', '--format']);
|
||||||
|
expect(values).toEqual(expect.arrayContaining(['openai', 'anthropic', 'raw', 'claude-extension']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('includes live doctor and cliproxy flags from the shared catalog', () => {
|
||||||
|
expect(suggestionValues(['doctor'])).toEqual(expect.arrayContaining(['--fix', '-f']));
|
||||||
|
expect(suggestionValues(['cliproxy'])).toEqual(expect.arrayContaining(['remove', '--backend']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('filters suggestions by the current token prefix', () => {
|
||||||
|
const values = suggestionValues([], 'do');
|
||||||
|
expect(values).toEqual(expect.arrayContaining(['docker', 'doctor']));
|
||||||
|
expect(values).not.toContain('tokens');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,152 +1,77 @@
|
|||||||
import { describe, expect, test } from 'bun:test';
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
import { showApiCommandHelp } from '../../../src/commands/api-command/help';
|
import {
|
||||||
import { handleHelpCommand } from '../../../src/commands/help-command';
|
handleHelpCommand,
|
||||||
|
handleHelpRoute,
|
||||||
|
getRootHelpVisibleCommands,
|
||||||
|
} from '../../../src/commands/help-command';
|
||||||
|
|
||||||
function stripAnsi(input: string): string {
|
function stripAnsi(input: string): string {
|
||||||
return input.replace(/\u001b\[[0-9;]*m/g, '');
|
return input.replace(/\u001b\[[0-9;]*m/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renderLines(
|
||||||
|
render: (writeLine: (line: string) => void) => Promise<void>
|
||||||
|
): Promise<string> {
|
||||||
|
const lines: string[] = [];
|
||||||
|
await render((line) => lines.push(line));
|
||||||
|
return stripAnsi(lines.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
describe('help command parity', () => {
|
describe('help command parity', () => {
|
||||||
test('root help documents cliproxy provider filter under quota command', async () => {
|
test('root help stays within the compact line budget', async () => {
|
||||||
const lines: string[] = [];
|
const rendered = await renderLines((writeLine) => handleHelpCommand(writeLine));
|
||||||
await handleHelpCommand((line) => lines.push(line));
|
const visibleLines = rendered.split('\n').filter((line) => line.trim().length > 0);
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
expect(visibleLines.length).toBeLessThanOrEqual(90);
|
||||||
expect(rendered.includes('ccs cliproxy status [provider]')).toBe(false);
|
expect(rendered.includes('ccs help <topic>')).toBe(true);
|
||||||
expect(rendered.includes('ccs cliproxy status')).toBe(true);
|
expect(rendered.includes('ccs help completion')).toBe(true);
|
||||||
expect(rendered.includes('ccs cliproxy quota --provider <name>')).toBe(true);
|
|
||||||
expect(rendered.includes('ccs llamacpp')).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('root help documents llama.cpp as a local API profile', async () => {
|
test('root help covers every public root command once the catalog is updated', async () => {
|
||||||
const lines: string[] = [];
|
const rendered = await renderLines((writeLine) => handleHelpCommand(writeLine));
|
||||||
await handleHelpCommand((line) => lines.push(line));
|
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
for (const command of getRootHelpVisibleCommands()) {
|
||||||
expect(rendered.includes('ccs llamacpp')).toBe(true);
|
expect(rendered.includes(command)).toBe(true);
|
||||||
expect(rendered.includes('http://127.0.0.1:8080')).toBe(true);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('root help no longer markets glmt as a supported profile', async () => {
|
test('root help no longer markets deprecated glmt directly', async () => {
|
||||||
const lines: string[] = [];
|
const rendered = await renderLines((writeLine) => handleHelpCommand(writeLine));
|
||||||
await handleHelpCommand((line) => lines.push(line));
|
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
|
||||||
expect(rendered.includes('ccs glmt')).toBe(false);
|
expect(rendered.includes('ccs glmt')).toBe(false);
|
||||||
expect(rendered.includes('ccs glm')).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('root help documents Claude IDE extension setup surfaces', async () => {
|
test('providers topic lists built-in OAuth provider shortcuts', async () => {
|
||||||
const lines: string[] = [];
|
const rendered = await renderLines((writeLine) => handleHelpRoute(['providers'], writeLine));
|
||||||
await handleHelpCommand((line) => lines.push(line));
|
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
expect(rendered.includes('Built-in OAuth Providers')).toBe(true);
|
||||||
expect(rendered.includes('Claude IDE Extension setup page')).toBe(true);
|
expect(rendered.includes('ccs cliproxy --help')).toBe(true);
|
||||||
expect(rendered.includes('ccs env <profile> --format claude-extension --ide vscode')).toBe(
|
expect(rendered.includes('gemini')).toBe(true);
|
||||||
true
|
expect(rendered.includes('codex')).toBe(true);
|
||||||
);
|
expect(rendered.includes('ghcp')).toBe(true);
|
||||||
expect(rendered.includes('ccs env <profile> --format claude-extension --ide windsurf')).toBe(
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('root help documents dashboard host binding example', async () => {
|
test('completion topic documents install and verification paths', async () => {
|
||||||
const lines: string[] = [];
|
const rendered = await renderLines((writeLine) => handleHelpRoute(['completion'], writeLine));
|
||||||
await handleHelpCommand((line) => lines.push(line));
|
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
expect(rendered.includes('ccs --shell-completion')).toBe(true);
|
||||||
expect(rendered.includes('ccs config --host 0.0.0.0')).toBe(true);
|
expect(rendered.includes('ccs help <TAB>')).toBe(true);
|
||||||
expect(rendered.includes('Force all-interface binding for remote devices')).toBe(true);
|
expect(rendered.includes('--force')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('root help documents docker deployment commands', async () => {
|
test('api topic delegates to command-specific help', async () => {
|
||||||
const lines: string[] = [];
|
const rendered = await renderLines((writeLine) => handleHelpRoute(['api'], writeLine));
|
||||||
await handleHelpCommand((line) => lines.push(line));
|
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
expect(rendered.includes('CCS API Management')).toBe(true);
|
||||||
expect(rendered.includes('ccs docker --help')).toBe(true);
|
|
||||||
expect(rendered.includes('ccs docker up --host <ssh>')).toBe(true);
|
|
||||||
expect(rendered.includes('ccs docker update')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('root help documents official channels native-only scope and process-env tokens', async () => {
|
|
||||||
const lines: string[] = [];
|
|
||||||
await handleHelpCommand((line) => lines.push(line));
|
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
|
||||||
expect(rendered.includes('Dashboard -> Settings -> Channels (fastest path)')).toBe(true);
|
|
||||||
expect(
|
|
||||||
rendered.includes(
|
|
||||||
'Fastest path: turn on the channel, save the token if needed, then run ccs.'
|
|
||||||
)
|
|
||||||
).toBe(true);
|
|
||||||
expect(rendered.includes('ccs codex')).toBe(true);
|
|
||||||
expect(rendered.includes('ccs --target codex')).toBe(true);
|
|
||||||
expect(
|
|
||||||
rendered.includes('Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work')
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('root help documents bare cursor as the runtime entrypoint', async () => {
|
|
||||||
const lines: string[] = [];
|
|
||||||
await handleHelpCommand((line) => lines.push(line));
|
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
|
||||||
expect(rendered.includes('ccs cursor <cmd>')).toBe(false);
|
|
||||||
expect(rendered.includes('Run Claude via Cursor local proxy')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('root help explains Claude [1m] as an explicit CCS suffix with upstream limits', async () => {
|
|
||||||
const lines: string[] = [];
|
|
||||||
await handleHelpCommand((line) => lines.push(line));
|
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
|
||||||
expect(
|
|
||||||
rendered.includes('Claude models: plain by default, opt-in with --1m or saved [1m]')
|
|
||||||
).toBe(true);
|
|
||||||
expect(rendered.includes('CCS only controls the saved [1m] suffix.')).toBe(true);
|
|
||||||
expect(rendered.includes('return 429 extra-usage errors for long-context requests')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('root help documents native Codex runtime alias and runtime-only scope', async () => {
|
|
||||||
const lines: string[] = [];
|
|
||||||
await handleHelpCommand((line) => lines.push(line));
|
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
|
||||||
expect(rendered.includes('ccs-codex <profile> [args]')).toBe(true);
|
|
||||||
expect(rendered.includes('ccsx <profile> [args]')).toBe(true);
|
|
||||||
expect(rendered.includes('ccsxp [args]')).toBe(true);
|
|
||||||
expect(rendered.includes('ccs --target codex')).toBe(true);
|
|
||||||
expect(rendered.includes('ccsxp "your prompt"')).toBe(true);
|
|
||||||
expect(rendered.includes('ccs codex-api --target codex')).toBe(true);
|
|
||||||
expect(rendered.includes('Compatible -> Codex CLI')).toBe(true);
|
|
||||||
expect(rendered.includes('CLIPROXY_API_KEY')).toBe(true);
|
|
||||||
expect(rendered.includes('codex (runtime-only)')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('api help documents create-time Claude [1m] flags and entitlement warning', async () => {
|
|
||||||
const lines: string[] = [];
|
|
||||||
await showApiCommandHelp((line) => lines.push(line));
|
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
|
||||||
expect(rendered.includes('--1m / --no-1m')).toBe(true);
|
|
||||||
expect(rendered.includes('ccs api create --preset anthropic --1m')).toBe(true);
|
expect(rendered.includes('ccs api create --preset anthropic --1m')).toBe(true);
|
||||||
expect(rendered.includes('Plain Claude model IDs stay on standard context by default.')).toBe(
|
expect(rendered.includes('ccs api discover --register')).toBe(true);
|
||||||
true
|
|
||||||
);
|
|
||||||
expect(rendered.includes('some accounts can still return 429 for long-context requests')).toBe(
|
|
||||||
true
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('api help documents Codex bridge runtime launch separately from persisted targets', async () => {
|
test('unknown help target shows an actionable fallback', async () => {
|
||||||
const lines: string[] = [];
|
const rendered = await renderLines((writeLine) => handleHelpRoute(['unknown-topic'], writeLine));
|
||||||
await showApiCommandHelp((line) => lines.push(line));
|
|
||||||
|
|
||||||
const rendered = stripAnsi(lines.join('\n'));
|
expect(rendered.includes('Unknown help topic or command: unknown-topic')).toBe(true);
|
||||||
expect(rendered.includes('ccs api create codex-api --cliproxy-provider codex')).toBe(true);
|
expect(rendered.includes('Available help topics:')).toBe(true);
|
||||||
expect(rendered.includes('ccs codex-api --target codex')).toBe(true);
|
process.exitCode = 0;
|
||||||
expect(rendered.includes('Default target: claude or droid (create)')).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ beforeEach(() => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
mock.module('../../../src/commands/api-command', () => ({
|
mock.module('../../../src/commands/api-command/index', () => ({
|
||||||
handleApiCommand: async (args: string[]) => {
|
handleApiCommand: async (args: string[]) => {
|
||||||
calls.push(`api:${args.join(' ')}`);
|
calls.push(`api:${args.join(' ')}`);
|
||||||
},
|
},
|
||||||
@@ -42,6 +42,12 @@ beforeEach(() => {
|
|||||||
mock.module('../../../src/commands/tokens-command', () => ({
|
mock.module('../../../src/commands/tokens-command', () => ({
|
||||||
handleTokensCommand: async () => 37,
|
handleTokensCommand: async () => 37,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
mock.module('../../../src/commands/completion-backend', () => ({
|
||||||
|
handleCompletionCommand: async (args: string[]) => {
|
||||||
|
calls.push(`complete:${args.join(' ')}`);
|
||||||
|
},
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -120,4 +126,12 @@ describe('root-command-router', () => {
|
|||||||
|
|
||||||
await expect(tryHandleRootCommand(['tokens', 'list'])).rejects.toThrow('process.exit(37)');
|
await expect(tryHandleRootCommand(['tokens', 'list'])).rejects.toThrow('process.exit(37)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('routes hidden completion queries through the completion backend', async () => {
|
||||||
|
const tryHandleRootCommand = await loadTryHandleRootCommand();
|
||||||
|
|
||||||
|
await expect(tryHandleRootCommand(['__complete', '--shell', 'bash', '--current', 'do'])).resolves.toBe(true);
|
||||||
|
|
||||||
|
expect(calls).toEqual(['complete:--shell bash --current do']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user