mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 22:21:20 +00:00
feat(profile,shell-completion,prompt): add profile commands and improve input handling (#34)
* fix(cliproxy): use double-dash flags for cliproxyapi auth (#24) CLIProxyAPI updated from single-dash to double-dash CLI flags. Changed -login to --login, -codex-login to --codex-login, -antigravity-login to --antigravity-login, -config to --config, and -no-browser to --no-browser. * chore(release): 5.1.1-beta.1 [skip ci] ## [5.1.1-beta.1](https://github.com/kaitranntt/ccs/compare/v5.1.0...v5.1.1-beta.1) (2025-12-01) ### Bug Fixes * **cliproxy:** use double-dash flags for cliproxyapi auth ([#24](https://github.com/kaitranntt/ccs/issues/24)) ([4c81f28](https://github.com/kaitranntt/ccs/commit/4c81f28f0b67ef92cf74d0f5c13a5943ff0a7f00)) * chore(release): 5.1.1-beta.2 [skip ci] ## [5.1.1-beta.2](https://github.com/kaitranntt/ccs/compare/v5.1.1-beta.1...v5.1.1-beta.2) (2025-12-01) ### Bug Fixes * **cliproxy:** use double-dash flags for cliproxyapi auth ([9489884](https://github.com/kaitranntt/ccs/commit/94898848ea4533dcfc142e1b6c9bf939ba655537)) * fix(glmt): handle 401 errors and headers-already-sent exception (#30) - add res.headersSent check before writing error headers - write error as SSE event when headers already sent (streaming mode) - add parseUpstreamError() for user-friendly error messages - return proper HTTP status codes (401, 403, 429, 502, 504) - clear auth error: check ANTHROPIC_AUTH_TOKEN in config Fixes #26 * chore(release): 5.1.1-beta.3 [skip ci] ## [5.1.1-beta.3](https://github.com/kaitranntt/ccs/compare/v5.1.1-beta.2...v5.1.1-beta.3) (2025-12-01) ### Bug Fixes * **glmt:** handle 401 errors and headers-already-sent exception ([#30](https://github.com/kaitranntt/ccs/issues/30)) ([c953382](https://github.com/kaitranntt/ccs/commit/c95338232a37981b95b785b47185ce18d6d94b7a)), closes [#26](https://github.com/kaitranntt/ccs/issues/26) * chore(release): enable success comments and released labels in semantic-release * feat(cliproxy): add qwen code oauth provider support (#31) Add 'qwen' as new CLIProxy OAuth provider with --qwen-login flag. Support qwen3-coder model with base config at config/base-qwen.settings.json. Register qwen OAuth endpoints and token prefix handling. Update help documentation across all CLI entry points. Implements issue #29 * chore(release): 5.2.0-beta.1 [skip ci] # [5.2.0-beta.1](https://github.com/kaitranntt/ccs/compare/v5.1.1-beta.3...v5.2.0-beta.1) (2025-12-01) ### Features * **cliproxy:** add qwen code oauth provider support ([#31](https://github.com/kaitranntt/ccs/issues/31)) ([a3f1e52](https://github.com/kaitranntt/ccs/commit/a3f1e52ac68600ba0806d67aacceb6477ffa3543)), closes [#29](https://github.com/kaitranntt/ccs/issues/29) * feat(cliproxy): auto-update cliproxyapi to latest release on startup - add github api integration to fetch latest release version - check for updates on every startup (cached for 1 hour) - auto-download newer versions when available - rename CLIPROXY_VERSION to CLIPROXY_FALLBACK_VERSION (safety net) - add version tracking (.version file) and caching (.version-cache.json) - non-blocking: startup continues if update check fails * chore(release): 5.2.0-beta.2 [skip ci] # [5.2.0-beta.2](https://github.com/kaitranntt/ccs/compare/v5.2.0-beta.1...v5.2.0-beta.2) (2025-12-01) ### Features * **cliproxy:** auto-update cliproxyapi to latest release on startup ([8873ccd](https://github.com/kaitranntt/ccs/commit/8873ccd981679e8acff8965accdc22215c6e4aa2)) * chore(release): 5.3.0-beta.1 [skip ci] # [5.3.0-beta.1](https://github.com/kaitranntt/ccs/compare/v5.2.0...v5.3.0-beta.1) (2025-12-01) ### Bug Fixes * **cliproxy:** use double-dash flags for cliproxyapi auth ([#24](https://github.com/kaitranntt/ccs/issues/24)) ([4c81f28](https://github.com/kaitranntt/ccs/commit/4c81f28f0b67ef92cf74d0f5c13a5943ff0a7f00)) * **glmt:** handle 401 errors and headers-already-sent exception ([#30](https://github.com/kaitranntt/ccs/issues/30)) ([c953382](https://github.com/kaitranntt/ccs/commit/c95338232a37981b95b785b47185ce18d6d94b7a)), closes [#26](https://github.com/kaitranntt/ccs/issues/26) ### Features * **cliproxy:** add qwen code oauth provider support ([#31](https://github.com/kaitranntt/ccs/issues/31)) ([a3f1e52](https://github.com/kaitranntt/ccs/commit/a3f1e52ac68600ba0806d67aacceb6477ffa3543)), closes [#29](https://github.com/kaitranntt/ccs/issues/29) * **cliproxy:** auto-update cliproxyapi to latest release on startup ([8873ccd](https://github.com/kaitranntt/ccs/commit/8873ccd981679e8acff8965accdc22215c6e4aa2)) * fix(cliproxy): improve qwen oauth error handling - Updated OAuth port documentation with correct flow types per provider - Removed Qwen from port cleanup logic (Device Code Flow has no callback) - Fixed Qwen error messages to show Device Code Flow troubleshooting tips Fixes #29 * chore(release): 5.3.0-beta.2 [skip ci] # [5.3.0-beta.2](https://github.com/kaitranntt/ccs/compare/v5.3.0-beta.1...v5.3.0-beta.2) (2025-12-01) ### Bug Fixes * **cliproxy:** improve qwen oauth error handling ([7e0b0fe](https://github.com/kaitranntt/ccs/commit/7e0b0feca8ce2ed5d505c5bf6c84e54c6df8839e)), closes [#29](https://github.com/kaitranntt/ccs/issues/29) * feat(profile): add profile command with configuration display * feat(shell-completion): add --force flag and fix zsh profile coloring * fix(prompt): improve password input handling with raw mode and buffer support * chore(release): 5.3.0-beta.3 [skip ci] # [5.3.0-beta.3](https://github.com/kaitranntt/ccs/compare/v5.3.0-beta.2...v5.3.0-beta.3) (2025-12-01) ### Bug Fixes * **prompt:** improve password input handling with raw mode and buffer support ([bc56d2e](https://github.com/kaitranntt/ccs/commit/bc56d2e135532b2ae443144dd42217b26bcba951)) ### Features * **profile:** add profile command with configuration display ([53d7a15](https://github.com/kaitranntt/ccs/commit/53d7a15c047e760723e051dc0f7be3c0dd42d087)) * **shell-completion:** add --force flag and fix zsh profile coloring ([7faed1d](https://github.com/kaitranntt/ccs/commit/7faed1d84ba29ba02bf687bae5b3458617512e67)) * chore(release): 5.3.0-beta.4 [skip ci] # [5.3.0-beta.4](https://github.com/kaitranntt/ccs/compare/v5.3.0-beta.3...v5.3.0-beta.4) (2025-12-01) ### Bug Fixes * **cliproxy:** improve qwen oauth error handling ([#33](https://github.com/kaitranntt/ccs/issues/33)) ([1c3374f](https://github.com/kaitranntt/ccs/commit/1c3374f6a7e4440e299d49b58808c6454b4547c2)), closes [#29](https://github.com/kaitranntt/ccs/issues/29) --------- Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
This commit is contained in:
co-authored by
semantic-release-bot
parent
7f3f1fc34a
commit
7ec8cc8369
@@ -1,3 +1,23 @@
|
||||
# [5.3.0-beta.4](https://github.com/kaitranntt/ccs/compare/v5.3.0-beta.3...v5.3.0-beta.4) (2025-12-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cliproxy:** improve qwen oauth error handling ([#33](https://github.com/kaitranntt/ccs/issues/33)) ([1c3374f](https://github.com/kaitranntt/ccs/commit/1c3374f6a7e4440e299d49b58808c6454b4547c2)), closes [#29](https://github.com/kaitranntt/ccs/issues/29)
|
||||
|
||||
# [5.3.0-beta.3](https://github.com/kaitranntt/ccs/compare/v5.3.0-beta.2...v5.3.0-beta.3) (2025-12-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **prompt:** improve password input handling with raw mode and buffer support ([bc56d2e](https://github.com/kaitranntt/ccs/commit/bc56d2e135532b2ae443144dd42217b26bcba951))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **profile:** add profile command with configuration display ([53d7a15](https://github.com/kaitranntt/ccs/commit/53d7a15c047e760723e051dc0f7be3c0dd42d087))
|
||||
* **shell-completion:** add --force flag and fix zsh profile coloring ([7faed1d](https://github.com/kaitranntt/ccs/commit/7faed1d84ba29ba02bf687bae5b3458617512e67))
|
||||
|
||||
## [5.2.1](https://github.com/kaitranntt/ccs/compare/v5.2.0...v5.2.1) (2025-12-01)
|
||||
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or (
|
||||
# IMPORTANT: Update this version when releasing new versions!
|
||||
# This hardcoded version is used for standalone installations (irm | iex)
|
||||
# For git installations, VERSION file is read if available
|
||||
$CcsVersion = "5.2.1"
|
||||
$CcsVersion = "5.3.0-beta.4"
|
||||
|
||||
# Try to read VERSION file for git installations
|
||||
if ($ScriptDir) {
|
||||
|
||||
@@ -84,7 +84,7 @@ fi
|
||||
# IMPORTANT: Update this version when releasing new versions!
|
||||
# This hardcoded version is used for standalone installations (curl | bash)
|
||||
# For git installations, VERSION file is read if available
|
||||
CCS_VERSION="5.2.1"
|
||||
CCS_VERSION="5.3.0-beta.4"
|
||||
|
||||
# Try to read VERSION file for git installations
|
||||
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kaitranntt/ccs",
|
||||
"version": "5.2.1",
|
||||
"version": "5.3.0-beta.4",
|
||||
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -18,7 +18,7 @@ _ccs_completion() {
|
||||
|
||||
# Top-level completion (first argument)
|
||||
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
||||
local commands="auth doctor sync update"
|
||||
local commands="auth profile doctor sync update"
|
||||
local flags="--help --version --shell-completion -h -v -sc"
|
||||
local profiles=""
|
||||
|
||||
@@ -45,6 +45,36 @@ _ccs_completion() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# profile subcommands
|
||||
if [[ ${prev} == "profile" ]]; then
|
||||
local profile_commands="create list remove --help -h"
|
||||
COMPREPLY=( $(compgen -W "${profile_commands}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Completion for profile subcommands
|
||||
if [[ ${COMP_WORDS[1]} == "profile" ]]; 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
|
||||
|
||||
+43
-10
@@ -78,22 +78,23 @@ 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 doctor sync update' -a 'auth' -d '[cmd] Manage multiple Claude accounts'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'doctor' -d '[cmd] Run health check and diagnostics'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'sync' -d '[cmd] Sync delegation commands and skills'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'update' -d '[cmd] Update CCS to latest version'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth profile doctor sync update' -a 'auth' -d '[cmd] Manage multiple Claude accounts'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth profile doctor sync update' -a 'profile' -d '[cmd] Manage API profiles (create/remove)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth profile doctor sync update' -a 'doctor' -d '[cmd] Run health check and diagnostics'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth profile doctor sync update' -a 'sync' -d '[cmd] Sync delegation commands and skills'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth profile doctor sync update' -a 'update' -d '[cmd] Update CCS to latest version'
|
||||
|
||||
# Model profiles - grouped with [model] prefix for visual distinction
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'default' -d '[model] Default Claude Sonnet 4.5'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'glm' -d '[model] GLM-4.6 (cost-optimized)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'glmt' -d '[model] GLM-4.6 with thinking mode'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a 'kimi' -d '[model] Kimi for Coding (long-context)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth profile doctor sync update' -a 'default' -d '[model] Default Claude Sonnet 4.5'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth profile doctor sync update' -a 'glm' -d '[model] GLM-4.6 (cost-optimized)'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth profile doctor sync update' -a 'glmt' -d '[model] GLM-4.6 with thinking mode'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth profile doctor sync update' -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 doctor sync update' -a '(__fish_ccs_get_custom_settings_profiles)' -d '[model] Settings-based profile'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth profile doctor sync update' -a '(__fish_ccs_get_custom_settings_profiles)' -d '[model] Settings-based profile'
|
||||
|
||||
# Account profiles - dynamic with [account] prefix
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth doctor sync update' -a '(__fish_ccs_get_account_profiles)' -d '[account] Account-based profile'
|
||||
complete -c ccs -n 'not __fish_seen_subcommand_from auth profile doctor sync update' -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'
|
||||
@@ -132,3 +133,35 @@ complete -c ccs -n '__fish_ccs_using_auth_subcommand default' -a '(__fish_ccs_ge
|
||||
|
||||
# doctor command flags
|
||||
complete -c ccs -n '__fish_seen_subcommand_from doctor' -s h -l help -d 'Show help for doctor command'
|
||||
|
||||
# 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
|
||||
|
||||
# profile subcommands
|
||||
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'
|
||||
|
||||
# profile command flags
|
||||
complete -c ccs -n '__fish_ccs_using_profile' -s h -l help -d 'Show help for profile commands'
|
||||
|
||||
# profile create flags
|
||||
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'
|
||||
|
||||
# profile remove - profile names and flags
|
||||
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'
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
Register-ArgumentCompleter -CommandName ccs -ScriptBlock {
|
||||
param($commandName, $wordToComplete, $commandAst, $fakeBoundParameters)
|
||||
|
||||
$commands = @('auth', 'doctor', 'sync', 'update', '--help', '--version', '--shell-completion', '-h', '-v', '-sc')
|
||||
$commands = @('auth', 'profile', 'doctor', 'sync', 'update', '--help', '--version', '--shell-completion', '-h', '-v', '-sc')
|
||||
$authCommands = @('create', 'list', 'show', 'remove', 'default', '--help', '-h')
|
||||
$profileCommands = @('create', 'list', 'remove', '--help', '-h')
|
||||
$profileCreateFlags = @('--base-url', '--api-key', '--model', '--force', '--yes', '-y')
|
||||
$shellCompletionFlags = @('--bash', '--zsh', '--fish', '--powershell')
|
||||
$listFlags = @('--verbose', '--json')
|
||||
$removeFlags = @('--yes', '-y')
|
||||
@@ -170,4 +172,58 @@ Register-ArgumentCompleter -CommandName ccs -ScriptBlock {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# profile subcommand completion
|
||||
if ($words[1] -eq 'profile') {
|
||||
if ($position -eq 3) {
|
||||
# profile subcommands
|
||||
$profileCommands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
||||
[System.Management.Automation.CompletionResult]::new(
|
||||
$_,
|
||||
$_,
|
||||
'ParameterValue',
|
||||
$_
|
||||
)
|
||||
}
|
||||
} elseif ($position -eq 4) {
|
||||
# Profile names or flags for profile subcommands
|
||||
switch ($words[2]) {
|
||||
'remove' {
|
||||
$options = (Get-CcsProfiles -Type settings) + $removeFlags
|
||||
$options | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
||||
[System.Management.Automation.CompletionResult]::new(
|
||||
$_,
|
||||
$_,
|
||||
'ParameterValue',
|
||||
$_
|
||||
)
|
||||
}
|
||||
}
|
||||
'create' {
|
||||
$profileCreateFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
||||
[System.Management.Automation.CompletionResult]::new(
|
||||
$_,
|
||||
$_,
|
||||
'ParameterValue',
|
||||
$_
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($position -eq 5) {
|
||||
# Flags after profile name
|
||||
switch ($words[2]) {
|
||||
'remove' {
|
||||
$removeFlags | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
||||
[System.Management.Automation.CompletionResult]::new(
|
||||
$_,
|
||||
$_,
|
||||
'ParameterValue',
|
||||
$_
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# Color codes: 0;34=blue, 0;32=green, 0;33=yellow, 2;37=dim white
|
||||
# Pattern format: =(#b)(group1)(group2)==color_for_group1=color_for_group2
|
||||
# The leading '=' means no color for whole match, then each '=' assigns to each group
|
||||
zstyle ':completion:*:*:ccs:*:commands' list-colors '=(#b)(auth|doctor|sync|update)([[:space:]]#--[[:space:]]#*)==0\;34=2\;37'
|
||||
zstyle ':completion:*:*:ccs:*:commands' list-colors '=(#b)(auth|profile|doctor|sync|update)([[:space:]]#--[[:space:]]#*)==0\;34=2\;37'
|
||||
zstyle ':completion:*:*:ccs:*:model-profiles' list-colors '=(#b)(default|glm|glmt|kimi|[^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;32=2\;37'
|
||||
zstyle ':completion:*:*:ccs:*:account-profiles' list-colors '=(#b)([^[:space:]]##)([[:space:]]#--[[:space:]]#*)==0\;33=2\;37'
|
||||
zstyle ':completion:*:*:ccs:*' group-name ''
|
||||
@@ -33,6 +33,7 @@ _ccs() {
|
||||
# Define top-level commands (padded for alignment)
|
||||
commands=(
|
||||
'auth:Manage multiple Claude accounts'
|
||||
'profile:Manage API profiles (create/remove)'
|
||||
'doctor:Run health check and diagnostics'
|
||||
'sync:Sync delegation commands and skills'
|
||||
'update:Update CCS to latest version'
|
||||
@@ -90,6 +91,9 @@ _ccs() {
|
||||
auth)
|
||||
_ccs_auth
|
||||
;;
|
||||
profile)
|
||||
_ccs_profile
|
||||
;;
|
||||
doctor)
|
||||
_arguments \
|
||||
'(- *)'{-h,--help}'[Show help for doctor command]'
|
||||
@@ -110,6 +114,58 @@ _ccs() {
|
||||
esac
|
||||
}
|
||||
|
||||
_ccs_profile() {
|
||||
local curcontext="$curcontext" state line
|
||||
typeset -A opt_args
|
||||
|
||||
local -a profile_commands settings_profiles
|
||||
|
||||
# Define profile subcommands
|
||||
profile_commands=(
|
||||
'create:Create new API profile (interactive)'
|
||||
'list:List all profiles'
|
||||
'remove:Remove a profile'
|
||||
)
|
||||
|
||||
# Load settings profiles for remove command
|
||||
if [[ -f ~/.ccs/config.json ]]; then
|
||||
settings_profiles=(${(f)"$(jq -r '.profiles | keys[]' ~/.ccs/config.json 2>/dev/null)"})
|
||||
fi
|
||||
|
||||
_arguments -C \
|
||||
'(- *)'{-h,--help}'[Show help for profile commands]' \
|
||||
'1: :->subcommand' \
|
||||
'*:: :->subargs'
|
||||
|
||||
case $state in
|
||||
subcommand)
|
||||
_describe -t profile-commands 'profile commands' profile_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)
|
||||
# No arguments
|
||||
;;
|
||||
remove|delete|rm)
|
||||
_arguments \
|
||||
'1:profile:($settings_profiles)' \
|
||||
{--yes,-y}'[Skip confirmation]'
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_ccs_auth() {
|
||||
local curcontext="$curcontext" state line
|
||||
typeset -A opt_args
|
||||
|
||||
@@ -253,6 +253,13 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: profile command
|
||||
if (firstArg === 'profile') {
|
||||
const { handleProfileCommand } = await import('./commands/profile-command');
|
||||
await handleProfileCommand(args.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: headless delegation (-p flag)
|
||||
if (args.includes('-p') || args.includes('--prompt')) {
|
||||
const { DelegationHandler } = await import('./delegation/delegation-handler');
|
||||
|
||||
@@ -13,15 +13,18 @@ export function handleHelpCommand(): void {
|
||||
console.log('');
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SECTION 1: API KEY MODELS
|
||||
// SECTION 1: API KEY MODELS + PROFILE MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
console.log(colored('═══ API Key Models ═══', 'cyanBold'));
|
||||
console.log(' Configure API keys in ~/.ccs/*.settings.json');
|
||||
console.log(colored('═══ API Key Profiles ═══', 'cyanBold'));
|
||||
console.log(' Configure in ~/.ccs/*.settings.json');
|
||||
console.log('');
|
||||
console.log(` ${colored('ccs', 'yellow')} Use default Claude account`);
|
||||
console.log(` ${colored('ccs glm', 'yellow')} GLM 4.6 (API key required)`);
|
||||
console.log(` ${colored('ccs glmt', 'yellow')} GLM with thinking mode`);
|
||||
console.log(` ${colored('ccs kimi', 'yellow')} Kimi for Coding (API key)`);
|
||||
console.log(` ${colored('ccs profile create', 'yellow')} Create custom API profile`);
|
||||
console.log(` ${colored('ccs profile remove', 'yellow')} Remove a profile`);
|
||||
console.log(` ${colored('ccs profile list', 'yellow')} List all profiles`);
|
||||
console.log('');
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* Profile Command Handler
|
||||
*
|
||||
* Manages CCS profiles for custom API providers.
|
||||
* Commands: create, list
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { colored } from '../utils/helpers';
|
||||
import { InteractivePrompt } from '../utils/prompt';
|
||||
import { getCcsDir, getConfigPath, loadConfig } from '../utils/config-manager';
|
||||
|
||||
interface ProfileCommandArgs {
|
||||
name?: string;
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
force?: boolean;
|
||||
yes?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments for profile commands
|
||||
*/
|
||||
function parseArgs(args: string[]): ProfileCommandArgs {
|
||||
const result: ProfileCommandArgs = {};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === '--base-url' && args[i + 1]) {
|
||||
result.baseUrl = args[++i];
|
||||
} else if (arg === '--api-key' && args[i + 1]) {
|
||||
result.apiKey = args[++i];
|
||||
} else if (arg === '--model' && args[i + 1]) {
|
||||
result.model = args[++i];
|
||||
} else if (arg === '--force') {
|
||||
result.force = true;
|
||||
} else if (arg === '--yes' || arg === '-y') {
|
||||
result.yes = true;
|
||||
} else if (!arg.startsWith('-') && !result.name) {
|
||||
result.name = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate profile name
|
||||
*/
|
||||
function validateProfileName(name: string): string | null {
|
||||
if (!name) {
|
||||
return 'Profile name is required';
|
||||
}
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9._-]*$/.test(name)) {
|
||||
return 'Profile name must start with letter, contain only letters, numbers, dot, dash, underscore';
|
||||
}
|
||||
if (name.length > 32) {
|
||||
return 'Profile name must be 32 characters or less';
|
||||
}
|
||||
// Reserved names
|
||||
const reserved = ['default', 'auth', 'profile', 'doctor', 'sync', 'update', 'help', 'version'];
|
||||
if (reserved.includes(name.toLowerCase())) {
|
||||
return `'${name}' is a reserved name`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate URL format
|
||||
*/
|
||||
function validateUrl(url: string): string | null {
|
||||
if (!url) {
|
||||
return 'Base URL is required';
|
||||
}
|
||||
try {
|
||||
new URL(url);
|
||||
return null;
|
||||
} catch {
|
||||
return 'Invalid URL format (must include protocol, e.g., https://)';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if profile already exists in config.json
|
||||
*/
|
||||
function profileExists(name: string): boolean {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
return name in config.profiles;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create settings.json file for profile
|
||||
*/
|
||||
function createSettingsFile(name: string, baseUrl: string, apiKey: string, model: string): string {
|
||||
const ccsDir = getCcsDir();
|
||||
const settingsPath = path.join(ccsDir, `${name}.settings.json`);
|
||||
|
||||
const settings = {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: baseUrl,
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
ANTHROPIC_MODEL: model,
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
||||
return settingsPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update config.json with new profile
|
||||
*/
|
||||
function updateConfig(name: string, _settingsPath: string): void {
|
||||
const configPath = getConfigPath();
|
||||
const ccsDir = getCcsDir();
|
||||
|
||||
// Read existing config or create new
|
||||
let config: { profiles: Record<string, string>; cliproxy?: Record<string, unknown> };
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} catch {
|
||||
config = { profiles: {} };
|
||||
}
|
||||
|
||||
// Use relative path with ~ for portability
|
||||
const relativePath = `~/.ccs/${name}.settings.json`;
|
||||
config.profiles[name] = relativePath;
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(ccsDir)) {
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write config atomically (write to temp, then rename)
|
||||
const tempPath = configPath + '.tmp';
|
||||
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tempPath, configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 'ccs profile create' command
|
||||
*/
|
||||
async function handleCreate(args: string[]): Promise<void> {
|
||||
const parsedArgs = parseArgs(args);
|
||||
|
||||
console.log(colored('Create API Profile', 'bold'));
|
||||
console.log('');
|
||||
|
||||
// Step 1: Profile name
|
||||
let name = parsedArgs.name;
|
||||
if (!name) {
|
||||
name = await InteractivePrompt.input('Profile name', {
|
||||
validate: validateProfileName,
|
||||
});
|
||||
} else {
|
||||
const error = validateProfileName(name);
|
||||
if (error) {
|
||||
console.error(`[X] ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if exists
|
||||
if (profileExists(name) && !parsedArgs.force) {
|
||||
console.error(`[X] Profile '${name}' already exists`);
|
||||
console.log(` Use ${colored('--force', 'yellow')} to overwrite`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 2: Base URL
|
||||
let baseUrl = parsedArgs.baseUrl;
|
||||
if (!baseUrl) {
|
||||
baseUrl = await InteractivePrompt.input('API Base URL (e.g., https://api.example.com)', {
|
||||
validate: validateUrl,
|
||||
});
|
||||
} else {
|
||||
const error = validateUrl(baseUrl);
|
||||
if (error) {
|
||||
console.error(`[X] ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: API Key
|
||||
let apiKey = parsedArgs.apiKey;
|
||||
if (!apiKey) {
|
||||
apiKey = await InteractivePrompt.password('API Key');
|
||||
if (!apiKey) {
|
||||
console.error('[X] API key is required');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Model (optional)
|
||||
const defaultModel = 'claude-sonnet-4-5-20250929';
|
||||
let model = parsedArgs.model;
|
||||
if (!model && !parsedArgs.yes) {
|
||||
model = await InteractivePrompt.input('Default model', {
|
||||
default: defaultModel,
|
||||
});
|
||||
}
|
||||
model = model || defaultModel;
|
||||
|
||||
// Create files
|
||||
console.log('');
|
||||
console.log('[i] Creating profile...');
|
||||
|
||||
try {
|
||||
const settingsPath = createSettingsFile(name, baseUrl, apiKey, model);
|
||||
updateConfig(name, settingsPath);
|
||||
|
||||
console.log(colored('[OK] Profile created successfully', 'green'));
|
||||
console.log('');
|
||||
console.log(` Profile: ${name}`);
|
||||
console.log(` Settings: ~/.ccs/${name}.settings.json`);
|
||||
console.log(` Base URL: ${baseUrl}`);
|
||||
console.log(` Model: ${model}`);
|
||||
console.log('');
|
||||
console.log(colored('Usage:', 'cyan'));
|
||||
console.log(` ${colored(`ccs ${name} "your prompt"`, 'yellow')}`);
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error(`[X] Failed to create profile: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 'ccs profile list' command
|
||||
*/
|
||||
async function handleList(): Promise<void> {
|
||||
console.log(colored('CCS Profiles', 'bold'));
|
||||
console.log('');
|
||||
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const profiles = Object.keys(config.profiles);
|
||||
|
||||
if (profiles.length === 0) {
|
||||
console.log(colored('No profiles configured', 'yellow'));
|
||||
console.log('');
|
||||
console.log('To create a profile:');
|
||||
console.log(` ${colored('ccs profile create', 'yellow')}`);
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(colored('Settings-based Profiles:', 'cyan'));
|
||||
// Calculate max name length for alignment
|
||||
const maxNameLen = Math.max(...profiles.map((n) => n.length));
|
||||
profiles.forEach((name) => {
|
||||
const settingsPath = config.profiles[name];
|
||||
const paddedName = name.padEnd(maxNameLen);
|
||||
console.log(` ${colored(paddedName, 'yellow')} ${settingsPath}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// Also show CLIProxy profiles if any
|
||||
if (config.cliproxy && Object.keys(config.cliproxy).length > 0) {
|
||||
console.log(colored('CLIProxy Variants:', 'cyan'));
|
||||
Object.entries(config.cliproxy).forEach(([name, variant]) => {
|
||||
const v = variant as { provider: string; settings: string };
|
||||
console.log(` ${colored(name, 'yellow')} → ${v.provider} (${v.settings})`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`Total: ${profiles.length} profile(s)`);
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
console.error(`[X] Failed to list profiles: ${(error as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle 'ccs profile remove' command
|
||||
*/
|
||||
async function handleRemove(args: string[]): Promise<void> {
|
||||
const parsedArgs = parseArgs(args);
|
||||
|
||||
// Load config first to get available profiles
|
||||
let config: { profiles: Record<string, string>; cliproxy?: Record<string, unknown> };
|
||||
try {
|
||||
config = loadConfig();
|
||||
} catch {
|
||||
console.error('[X] Failed to load config');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const profiles = Object.keys(config.profiles);
|
||||
if (profiles.length === 0) {
|
||||
console.log(colored('No profiles to remove', 'yellow'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Interactive profile selection if not provided
|
||||
let name = parsedArgs.name;
|
||||
if (!name) {
|
||||
console.log(colored('Remove Profile', 'bold'));
|
||||
console.log('');
|
||||
console.log('Available profiles:');
|
||||
profiles.forEach((p, i) => console.log(` ${i + 1}. ${p}`));
|
||||
console.log('');
|
||||
|
||||
name = await InteractivePrompt.input('Profile name to remove', {
|
||||
validate: (val) => {
|
||||
if (!val) return 'Profile name is required';
|
||||
if (!profiles.includes(val)) return `Profile '${val}' not found`;
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!(name in config.profiles)) {
|
||||
console.error(`[X] Profile '${name}' not found`);
|
||||
console.log('');
|
||||
console.log('Available profiles:');
|
||||
profiles.forEach((p) => console.log(` - ${p}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const settingsPath = config.profiles[name];
|
||||
const expandedPath = path.join(getCcsDir(), `${name}.settings.json`);
|
||||
|
||||
// Confirm deletion
|
||||
console.log('');
|
||||
console.log(`Profile '${colored(name, 'yellow')}' will be removed.`);
|
||||
console.log(` Settings: ${settingsPath}`);
|
||||
console.log('');
|
||||
|
||||
const confirmed =
|
||||
parsedArgs.yes || (await InteractivePrompt.confirm('Delete this profile?', { default: false }));
|
||||
|
||||
if (!confirmed) {
|
||||
console.log('[i] Cancelled');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Remove from config.json
|
||||
delete config.profiles[name];
|
||||
const configPath = getConfigPath();
|
||||
const tempPath = configPath + '.tmp';
|
||||
fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tempPath, configPath);
|
||||
|
||||
// Remove settings file if it exists
|
||||
if (fs.existsSync(expandedPath)) {
|
||||
fs.unlinkSync(expandedPath);
|
||||
}
|
||||
|
||||
console.log(colored('[OK] Profile removed', 'green'));
|
||||
console.log(` Profile: ${name}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show help for profile commands
|
||||
*/
|
||||
function showHelp(): void {
|
||||
console.log(colored('CCS Profile Management', 'bold'));
|
||||
console.log('');
|
||||
console.log(colored('Usage:', 'cyan'));
|
||||
console.log(` ${colored('ccs profile', 'yellow')} <command> [options]`);
|
||||
console.log('');
|
||||
console.log(colored('Commands:', 'cyan'));
|
||||
console.log(` ${colored('create [name]', 'yellow')} Create new API profile (interactive)`);
|
||||
console.log(` ${colored('list', 'yellow')} List all profiles`);
|
||||
console.log(` ${colored('remove <name>', 'yellow')} Remove a profile`);
|
||||
console.log('');
|
||||
console.log(colored('Options:', 'cyan'));
|
||||
console.log(` ${colored('--base-url <url>', 'yellow')} API base URL (create)`);
|
||||
console.log(` ${colored('--api-key <key>', 'yellow')} API key (create)`);
|
||||
console.log(` ${colored('--model <model>', 'yellow')} Default model (create)`);
|
||||
console.log(` ${colored('--force', 'yellow')} Overwrite existing (create)`);
|
||||
console.log(` ${colored('--yes, -y', 'yellow')} Skip confirmation prompts`);
|
||||
console.log('');
|
||||
console.log(colored('Examples:', 'cyan'));
|
||||
console.log(` ${colored('ccs profile create', 'yellow')} # Interactive wizard`);
|
||||
console.log(` ${colored('ccs profile create myapi', 'yellow')} # Create with name`);
|
||||
console.log(` ${colored('ccs profile remove myapi', 'yellow')} # Remove profile`);
|
||||
console.log(` ${colored('ccs profile list', 'yellow')} # Show all profiles`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Main profile command router
|
||||
*/
|
||||
export async function handleProfileCommand(args: string[]): Promise<void> {
|
||||
const command = args[0];
|
||||
|
||||
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
||||
showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'create':
|
||||
await handleCreate(args.slice(1));
|
||||
break;
|
||||
case 'list':
|
||||
await handleList();
|
||||
break;
|
||||
case 'remove':
|
||||
case 'delete':
|
||||
case 'rm':
|
||||
await handleRemove(args.slice(1));
|
||||
break;
|
||||
default:
|
||||
console.error(`[X] Unknown command: ${command}`);
|
||||
console.log('');
|
||||
console.log('Run for help:');
|
||||
console.log(` ${colored('ccs profile --help', 'yellow')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export async function handleShellCompletionCommand(args: string[]): Promise<void
|
||||
|
||||
// Parse flags
|
||||
let targetShell: string | null = null;
|
||||
const force = args.includes('--force') || args.includes('-f');
|
||||
if (args.includes('--bash')) targetShell = 'bash';
|
||||
else if (args.includes('--zsh')) targetShell = 'zsh';
|
||||
else if (args.includes('--fish')) targetShell = 'fish';
|
||||
@@ -24,10 +25,13 @@ export async function handleShellCompletionCommand(args: string[]): Promise<void
|
||||
|
||||
try {
|
||||
const installer = new ShellCompletionInstaller();
|
||||
const result = installer.install(targetShell as 'bash' | 'zsh' | 'fish' | 'powershell' | null);
|
||||
const result = installer.install(targetShell as 'bash' | 'zsh' | 'fish' | 'powershell' | null, {
|
||||
force,
|
||||
});
|
||||
|
||||
if (result.alreadyInstalled) {
|
||||
if (result.alreadyInstalled && !force) {
|
||||
console.log(colored('[OK] Shell completion already installed', 'green'));
|
||||
console.log(` Use ${colored('--force', 'yellow')} to reinstall`);
|
||||
console.log('');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ interface InputOptions {
|
||||
validate?: (value: string) => string | null;
|
||||
}
|
||||
|
||||
interface PasswordOptions {
|
||||
mask?: string; // Character to show (default: '*')
|
||||
}
|
||||
|
||||
export class InteractivePrompt {
|
||||
/**
|
||||
* Ask for confirmation
|
||||
@@ -127,4 +131,78 @@ export class InteractivePrompt {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get password/secret input (masked)
|
||||
*/
|
||||
static async password(message: string, options: PasswordOptions = {}): Promise<string> {
|
||||
const { mask = '*' } = options;
|
||||
|
||||
// Non-TTY: error (passwords require interactive input)
|
||||
if (!process.stdin.isTTY) {
|
||||
throw new Error('Password input requires interactive TTY');
|
||||
}
|
||||
|
||||
// Set raw mode BEFORE writing prompt to prevent any echo
|
||||
if (process.stdin.setRawMode) {
|
||||
process.stdin.setRawMode(true);
|
||||
}
|
||||
|
||||
const promptText = `${message}: `;
|
||||
process.stderr.write(promptText);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let input = '';
|
||||
|
||||
const cleanup = (): void => {
|
||||
if (process.stdin.setRawMode) {
|
||||
process.stdin.setRawMode(false);
|
||||
}
|
||||
process.stdin.removeListener('data', onData);
|
||||
};
|
||||
|
||||
const onData = (data: Buffer): void => {
|
||||
// Convert buffer to string and process each character
|
||||
const str = data.toString('utf8');
|
||||
|
||||
for (const char of str) {
|
||||
const charCode = char.charCodeAt(0);
|
||||
|
||||
// Enter key (CR or LF)
|
||||
if (charCode === 13 || charCode === 10) {
|
||||
cleanup();
|
||||
process.stderr.write('\n');
|
||||
resolve(input);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+C
|
||||
if (charCode === 3) {
|
||||
cleanup();
|
||||
process.stderr.write('\n');
|
||||
process.exit(130);
|
||||
}
|
||||
|
||||
// Backspace (127 or 8)
|
||||
if (charCode === 127 || charCode === 8) {
|
||||
if (input.length > 0) {
|
||||
input = input.slice(0, -1);
|
||||
// Move cursor back, overwrite with space, move back again
|
||||
process.stderr.write('\b \b');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular printable character (ignore control chars and newlines in paste)
|
||||
if (charCode >= 32) {
|
||||
input += char;
|
||||
process.stderr.write(mask);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
process.stdin.on('data', onData);
|
||||
process.stdin.resume();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export class ShellCompletionInstaller {
|
||||
/**
|
||||
* Install bash completion
|
||||
*/
|
||||
private installBash(): InstallResult {
|
||||
private installBash(force = false): InstallResult {
|
||||
const rcFile = path.join(this.homeDir, '.bashrc');
|
||||
const completionPath = path.join(this.completionDir, 'ccs.bash');
|
||||
|
||||
@@ -108,7 +108,15 @@ export class ShellCompletionInstaller {
|
||||
if (fs.existsSync(rcFile)) {
|
||||
const content = fs.readFileSync(rcFile, 'utf8');
|
||||
if (content.includes(marker)) {
|
||||
return { success: true, alreadyInstalled: true };
|
||||
if (!force) {
|
||||
return { success: true, alreadyInstalled: true };
|
||||
}
|
||||
// Force: completion files already updated by ensureCompletionFiles()
|
||||
return {
|
||||
success: true,
|
||||
message: `Updated completion files in ${this.completionDir}`,
|
||||
reload: 'source ~/.bashrc',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +133,7 @@ export class ShellCompletionInstaller {
|
||||
/**
|
||||
* Install zsh completion
|
||||
*/
|
||||
private installZsh(): InstallResult {
|
||||
private installZsh(force = false): InstallResult {
|
||||
const rcFile = path.join(this.homeDir, '.zshrc');
|
||||
const completionPath = path.join(this.completionDir, 'ccs.zsh');
|
||||
const zshCompDir = path.join(this.homeDir, '.zsh', 'completion');
|
||||
@@ -137,7 +145,7 @@ export class ShellCompletionInstaller {
|
||||
// Create zsh completion directory (with file conflict checking)
|
||||
this.ensureDirectory(zshCompDir);
|
||||
|
||||
// Copy to zsh completion directory
|
||||
// Copy to zsh completion directory (always update for force)
|
||||
const destFile = path.join(zshCompDir, '_ccs');
|
||||
fs.copyFileSync(completionPath, destFile);
|
||||
|
||||
@@ -149,7 +157,15 @@ export class ShellCompletionInstaller {
|
||||
if (fs.existsSync(rcFile)) {
|
||||
const content = fs.readFileSync(rcFile, 'utf8');
|
||||
if (content.includes(marker)) {
|
||||
return { success: true, alreadyInstalled: true };
|
||||
if (!force) {
|
||||
return { success: true, alreadyInstalled: true };
|
||||
}
|
||||
// Force: files already updated above
|
||||
return {
|
||||
success: true,
|
||||
message: `Updated ${destFile}`,
|
||||
reload: 'source ~/.zshrc',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +182,7 @@ export class ShellCompletionInstaller {
|
||||
/**
|
||||
* Install fish completion
|
||||
*/
|
||||
private installFish(): InstallResult {
|
||||
private installFish(force = false): InstallResult {
|
||||
const completionPath = path.join(this.completionDir, 'ccs.fish');
|
||||
const fishCompDir = path.join(this.homeDir, '.config', 'fish', 'completions');
|
||||
|
||||
@@ -179,6 +195,12 @@ export class ShellCompletionInstaller {
|
||||
|
||||
// Copy to fish completion directory (fish auto-loads from here)
|
||||
const destFile = path.join(fishCompDir, 'ccs.fish');
|
||||
|
||||
// Check if already installed
|
||||
if (fs.existsSync(destFile) && !force) {
|
||||
return { success: true, alreadyInstalled: true };
|
||||
}
|
||||
|
||||
fs.copyFileSync(completionPath, destFile);
|
||||
|
||||
return {
|
||||
@@ -191,7 +213,7 @@ export class ShellCompletionInstaller {
|
||||
/**
|
||||
* Install PowerShell completion
|
||||
*/
|
||||
private installPowerShell(): InstallResult {
|
||||
private installPowerShell(force = false): InstallResult {
|
||||
const profilePath =
|
||||
process.env.PROFILE ||
|
||||
path.join(this.homeDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
|
||||
@@ -213,7 +235,15 @@ export class ShellCompletionInstaller {
|
||||
if (fs.existsSync(profilePath)) {
|
||||
const content = fs.readFileSync(profilePath, 'utf8');
|
||||
if (content.includes(marker)) {
|
||||
return { success: true, alreadyInstalled: true };
|
||||
if (!force) {
|
||||
return { success: true, alreadyInstalled: true };
|
||||
}
|
||||
// Force: completion files already updated by ensureCompletionFiles()
|
||||
return {
|
||||
success: true,
|
||||
message: `Updated completion files in ${this.completionDir}`,
|
||||
reload: '. $PROFILE',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,8 +260,9 @@ export class ShellCompletionInstaller {
|
||||
/**
|
||||
* Install for detected or specified shell
|
||||
*/
|
||||
install(shell: ShellType = null): InstallResult {
|
||||
install(shell: ShellType = null, options: { force?: boolean } = {}): InstallResult {
|
||||
const targetShell = shell || this.detectShell();
|
||||
const { force = false } = options;
|
||||
|
||||
if (!targetShell) {
|
||||
throw new Error(
|
||||
@@ -239,19 +270,19 @@ export class ShellCompletionInstaller {
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure completion files exist
|
||||
// Ensure completion files exist (always copy latest)
|
||||
this.ensureCompletionFiles();
|
||||
|
||||
// Install for target shell
|
||||
switch (targetShell) {
|
||||
case 'bash':
|
||||
return this.installBash();
|
||||
return this.installBash(force);
|
||||
case 'zsh':
|
||||
return this.installZsh();
|
||||
return this.installZsh(force);
|
||||
case 'fish':
|
||||
return this.installFish();
|
||||
return this.installFish(force);
|
||||
case 'powershell':
|
||||
return this.installPowerShell();
|
||||
return this.installPowerShell(force);
|
||||
default:
|
||||
throw new Error(`Unsupported shell: ${targetShell}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user