@kaitranntt/ccs 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +325 -0
- package/README.md +133 -122
- package/README.vi.md +147 -94
- package/VERSION +1 -1
- package/bin/auth-commands.js +10 -1
- package/bin/ccs.js +44 -34
- package/bin/config-manager.js +36 -7
- package/bin/doctor.js +365 -0
- package/bin/error-manager.js +159 -0
- package/bin/profile-registry.js +3 -4
- package/bin/recovery-manager.js +135 -0
- package/lib/ccs +853 -306
- package/lib/ccs.ps1 +794 -127
- package/package.json +1 -1
- package/scripts/postinstall.js +111 -12
package/lib/ccs
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
# Version (updated by scripts/bump-version.sh)
|
|
5
|
-
CCS_VERSION="3.0.
|
|
5
|
+
CCS_VERSION="3.0.2"
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
7
|
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
|
|
8
|
+
readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
|
|
9
|
+
readonly INSTANCES_DIR="$HOME/.ccs/instances"
|
|
8
10
|
|
|
9
11
|
# --- Color/Format Functions ---
|
|
10
12
|
setup_colors() {
|
|
@@ -13,12 +15,13 @@ setup_colors() {
|
|
|
13
15
|
([[ -t 1 || -t 2 ]] && [[ -z "${NO_COLOR:-}" ]]) || \
|
|
14
16
|
([[ -n "${TERM:-}" && "${TERM}" != "dumb" ]] && [[ -z "${NO_COLOR:-}" ]]); then
|
|
15
17
|
RED='\033[0;31m'
|
|
18
|
+
GREEN='\033[0;32m'
|
|
16
19
|
YELLOW='\033[1;33m'
|
|
17
20
|
CYAN='\033[0;36m'
|
|
18
21
|
BOLD='\033[1m'
|
|
19
22
|
RESET='\033[0m'
|
|
20
23
|
else
|
|
21
|
-
RED='' YELLOW='' CYAN='' BOLD='' RESET=''
|
|
24
|
+
RED='' GREEN='' YELLOW='' CYAN='' BOLD='' RESET=''
|
|
22
25
|
fi
|
|
23
26
|
}
|
|
24
27
|
|
|
@@ -37,53 +40,40 @@ show_help() {
|
|
|
37
40
|
echo ""
|
|
38
41
|
echo -e "${CYAN}Usage:${RESET}"
|
|
39
42
|
echo -e " ${YELLOW}ccs${RESET} [profile] [claude-args...]"
|
|
43
|
+
echo -e " ${YELLOW}ccs auth${RESET} <command> [options]"
|
|
40
44
|
echo -e " ${YELLOW}ccs${RESET} [flags]"
|
|
41
45
|
echo ""
|
|
42
46
|
echo -e "${CYAN}Description:${RESET}"
|
|
43
|
-
echo -e " Switch between Claude
|
|
44
|
-
echo -e "
|
|
45
|
-
echo ""
|
|
46
|
-
echo -e "${CYAN}Profile Switching:${RESET}"
|
|
47
|
-
echo -e " ${YELLOW}ccs${RESET} Use default profile"
|
|
48
|
-
echo -e " ${YELLOW}ccs glm${RESET} Switch to GLM profile"
|
|
49
|
-
echo -e " ${YELLOW}ccs kimi${RESET} Switch to Kimi profile"
|
|
50
|
-
echo -e " ${YELLOW}ccs glm${RESET} \"debug this code\" Switch to GLM and run command"
|
|
51
|
-
echo -e " ${YELLOW}ccs kimi${RESET} \"write tests\" Switch to Kimi and run command"
|
|
52
|
-
echo -e " ${YELLOW}ccs glm${RESET} --verbose Switch to GLM with Claude flags"
|
|
53
|
-
echo -e " ${YELLOW}ccs kimi${RESET} --verbose Switch to Kimi with Claude flags"
|
|
47
|
+
echo -e " Switch between multiple Claude accounts (work, personal, team) and"
|
|
48
|
+
echo -e " alternative models (GLM, Kimi) instantly. Concurrent sessions with"
|
|
49
|
+
echo -e " auto-recovery. Zero downtime."
|
|
54
50
|
echo ""
|
|
55
|
-
echo -e "${CYAN}
|
|
56
|
-
echo -e " ${YELLOW}
|
|
57
|
-
echo -e " ${YELLOW}
|
|
58
|
-
|
|
59
|
-
echo -e "${
|
|
60
|
-
echo -e " Config File: ~/.ccs/config.json"
|
|
61
|
-
echo -e " Settings: ~/.ccs/*.settings.json"
|
|
62
|
-
echo -e " Environment: CCS_CONFIG (override config path)"
|
|
51
|
+
echo -e "${CYAN}Model Switching:${RESET}"
|
|
52
|
+
echo -e " ${YELLOW}ccs${RESET} Use default Claude account"
|
|
53
|
+
echo -e " ${YELLOW}ccs glm${RESET} Switch to GLM 4.6 model"
|
|
54
|
+
echo -e " ${YELLOW}ccs kimi${RESET} Switch to Kimi for Coding"
|
|
55
|
+
echo -e " ${YELLOW}ccs glm${RESET} \"debug this code\" Use GLM and run command"
|
|
63
56
|
echo ""
|
|
64
|
-
echo -e "${CYAN}
|
|
65
|
-
echo -e "
|
|
66
|
-
echo -e " ${YELLOW}ccs${RESET}
|
|
57
|
+
echo -e "${CYAN}Account Management:${RESET}"
|
|
58
|
+
echo -e " ${YELLOW}ccs auth --help${RESET} Manage multiple Claude accounts"
|
|
59
|
+
echo -e " ${YELLOW}ccs work${RESET} Switch to work account"
|
|
60
|
+
echo -e " ${YELLOW}ccs personal${RESET} Switch to personal account"
|
|
67
61
|
echo ""
|
|
68
|
-
echo -e "
|
|
69
|
-
echo -e " ${YELLOW}ccs
|
|
62
|
+
echo -e "${CYAN}Diagnostics:${RESET}"
|
|
63
|
+
echo -e " ${YELLOW}ccs doctor${RESET} Run health check and diagnostics"
|
|
70
64
|
echo ""
|
|
71
|
-
echo -e "
|
|
72
|
-
echo -e " ${YELLOW}
|
|
73
|
-
echo ""
|
|
74
|
-
echo -e " # Use with verbose output"
|
|
75
|
-
echo -e " ${YELLOW}ccs glm${RESET} --verbose \"Debug error\""
|
|
76
|
-
echo -e " ${YELLOW}ccs kimi${RESET} --verbose \"Review code\""
|
|
65
|
+
echo -e "${CYAN}Flags:${RESET}"
|
|
66
|
+
echo -e " ${YELLOW}-h, --help${RESET} Show this help message"
|
|
67
|
+
echo -e " ${YELLOW}-v, --version${RESET} Show version and installation info"
|
|
77
68
|
echo ""
|
|
78
|
-
|
|
79
|
-
echo -e "
|
|
80
|
-
echo -e "
|
|
81
|
-
echo -e "
|
|
69
|
+
echo -e "${CYAN}Configuration:${RESET}"
|
|
70
|
+
echo -e " Config: ~/.ccs/config.json"
|
|
71
|
+
echo -e " Profiles: ~/.ccs/profiles.json"
|
|
72
|
+
echo -e " Settings: ~/.ccs/*.settings.json"
|
|
82
73
|
echo ""
|
|
83
74
|
echo -e "${CYAN}Documentation:${RESET}"
|
|
84
75
|
echo -e " GitHub: ${CYAN}https://github.com/kaitranntt/ccs${RESET}"
|
|
85
76
|
echo -e " Docs: https://github.com/kaitranntt/ccs/blob/main/README.md"
|
|
86
|
-
echo -e " Issues: https://github.com/kaitranntt/ccs/issues"
|
|
87
77
|
echo ""
|
|
88
78
|
echo -e "${CYAN}License:${RESET} MIT"
|
|
89
79
|
}
|
|
@@ -96,6 +86,210 @@ command -v jq &>/dev/null || {
|
|
|
96
86
|
exit 1
|
|
97
87
|
}
|
|
98
88
|
|
|
89
|
+
# --- Auto-Recovery Functions ---
|
|
90
|
+
|
|
91
|
+
ensure_ccs_directory() {
|
|
92
|
+
[[ -d "$HOME/.ccs" ]] && return 0
|
|
93
|
+
|
|
94
|
+
mkdir -p "$HOME/.ccs" 2>/dev/null || {
|
|
95
|
+
msg_error "Cannot create ~/.ccs/ directory. Check permissions."
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
echo "[i] Auto-recovery: Created ~/.ccs/ directory"
|
|
100
|
+
return 0
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ensure_config_json() {
|
|
104
|
+
local config_file="$HOME/.ccs/config.json"
|
|
105
|
+
|
|
106
|
+
# Check if exists and valid
|
|
107
|
+
if [[ -f "$config_file" ]]; then
|
|
108
|
+
jq empty "$config_file" 2>/dev/null && return 0
|
|
109
|
+
|
|
110
|
+
# Corrupted - backup and recreate
|
|
111
|
+
local backup_file="${config_file}.backup.$(date +%s)"
|
|
112
|
+
mv "$config_file" "$backup_file" 2>/dev/null
|
|
113
|
+
echo "[i] Auto-recovery: Backed up corrupted config.json"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Create default config
|
|
117
|
+
cat > "$config_file" <<'EOF'
|
|
118
|
+
{
|
|
119
|
+
"profiles": {
|
|
120
|
+
"glm": "~/.ccs/glm.settings.json",
|
|
121
|
+
"kimi": "~/.ccs/kimi.settings.json",
|
|
122
|
+
"default": "~/.claude/settings.json"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
EOF
|
|
126
|
+
|
|
127
|
+
echo "[i] Auto-recovery: Created ~/.ccs/config.json"
|
|
128
|
+
return 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ensure_claude_settings() {
|
|
132
|
+
local claude_dir="$HOME/.claude"
|
|
133
|
+
local settings_file="$claude_dir/settings.json"
|
|
134
|
+
|
|
135
|
+
# Create ~/.claude/ if missing
|
|
136
|
+
if [[ ! -d "$claude_dir" ]]; then
|
|
137
|
+
mkdir -p "$claude_dir" 2>/dev/null || return 1
|
|
138
|
+
echo "[i] Auto-recovery: Created ~/.claude/ directory"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# Create settings.json if missing
|
|
142
|
+
if [[ ! -f "$settings_file" ]]; then
|
|
143
|
+
echo '{}' > "$settings_file" 2>/dev/null || return 1
|
|
144
|
+
echo "[i] Auto-recovery: Created ~/.claude/settings.json"
|
|
145
|
+
echo "[i] Next step: Run 'claude /login' to authenticate"
|
|
146
|
+
return 0
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Run auto-recovery
|
|
153
|
+
auto_recover() {
|
|
154
|
+
ensure_ccs_directory || return 1
|
|
155
|
+
ensure_config_json || return 1
|
|
156
|
+
ensure_claude_settings || return 1
|
|
157
|
+
return 0
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# --- Doctor Command ---
|
|
161
|
+
|
|
162
|
+
doctor_check() {
|
|
163
|
+
local check_name="$1"
|
|
164
|
+
local status="$2" # success, warning, error
|
|
165
|
+
local message="${3:-}"
|
|
166
|
+
|
|
167
|
+
case "$status" in
|
|
168
|
+
success)
|
|
169
|
+
echo -e "${GREEN}[OK]${RESET} $check_name"
|
|
170
|
+
;;
|
|
171
|
+
warning)
|
|
172
|
+
echo -e "${YELLOW}[!]${RESET} $check_name${message:+: $message}"
|
|
173
|
+
;;
|
|
174
|
+
error)
|
|
175
|
+
echo -e "${RED}[X]${RESET} $check_name: $message"
|
|
176
|
+
;;
|
|
177
|
+
esac
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
doctor_run() {
|
|
181
|
+
echo -e "${CYAN}Running CCS Health Check...${RESET}"
|
|
182
|
+
echo ""
|
|
183
|
+
|
|
184
|
+
local has_errors=false
|
|
185
|
+
|
|
186
|
+
# Check Claude CLI
|
|
187
|
+
if command -v "$(detect_claude_cli)" &>/dev/null; then
|
|
188
|
+
doctor_check "Claude CLI" "success"
|
|
189
|
+
else
|
|
190
|
+
doctor_check "Claude CLI" "error" "Not found in PATH"
|
|
191
|
+
has_errors=true
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# Check ~/.ccs/
|
|
195
|
+
if [[ -d "$HOME/.ccs" ]]; then
|
|
196
|
+
doctor_check "CCS Directory" "success"
|
|
197
|
+
else
|
|
198
|
+
doctor_check "CCS Directory" "error" "~/.ccs/ not found"
|
|
199
|
+
has_errors=true
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# Check config.json
|
|
203
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
204
|
+
if jq empty "$CONFIG_FILE" 2>/dev/null; then
|
|
205
|
+
doctor_check "config.json" "success"
|
|
206
|
+
else
|
|
207
|
+
doctor_check "config.json" "error" "Invalid JSON"
|
|
208
|
+
has_errors=true
|
|
209
|
+
fi
|
|
210
|
+
else
|
|
211
|
+
doctor_check "config.json" "error" "Not found"
|
|
212
|
+
has_errors=true
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# Check glm.settings.json
|
|
216
|
+
local glm_file="$HOME/.ccs/glm.settings.json"
|
|
217
|
+
if [[ -f "$glm_file" ]]; then
|
|
218
|
+
if jq empty "$glm_file" 2>/dev/null; then
|
|
219
|
+
doctor_check "glm.settings.json" "success"
|
|
220
|
+
else
|
|
221
|
+
doctor_check "glm.settings.json" "error" "Invalid JSON"
|
|
222
|
+
has_errors=true
|
|
223
|
+
fi
|
|
224
|
+
else
|
|
225
|
+
doctor_check "glm.settings.json" "error" "Not found"
|
|
226
|
+
has_errors=true
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# Check kimi.settings.json
|
|
230
|
+
local kimi_file="$HOME/.ccs/kimi.settings.json"
|
|
231
|
+
if [[ -f "$kimi_file" ]]; then
|
|
232
|
+
if jq empty "$kimi_file" 2>/dev/null; then
|
|
233
|
+
doctor_check "kimi.settings.json" "success"
|
|
234
|
+
else
|
|
235
|
+
doctor_check "kimi.settings.json" "error" "Invalid JSON"
|
|
236
|
+
has_errors=true
|
|
237
|
+
fi
|
|
238
|
+
else
|
|
239
|
+
doctor_check "kimi.settings.json" "error" "Not found"
|
|
240
|
+
has_errors=true
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
# Check ~/.claude/settings.json
|
|
244
|
+
if [[ -f "$HOME/.claude/settings.json" ]]; then
|
|
245
|
+
if jq empty "$HOME/.claude/settings.json" 2>/dev/null; then
|
|
246
|
+
doctor_check "Claude Settings" "success"
|
|
247
|
+
else
|
|
248
|
+
doctor_check "Claude Settings" "warning" "Invalid JSON"
|
|
249
|
+
fi
|
|
250
|
+
else
|
|
251
|
+
doctor_check "Claude Settings" "warning" "Not found - run 'claude /login'"
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
# Check profiles
|
|
255
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
256
|
+
local profile_count=$(jq -r '.profiles | length' "$CONFIG_FILE" 2>/dev/null || echo "0")
|
|
257
|
+
doctor_check "Profiles" "success" "($profile_count configured)"
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
# Check instances
|
|
261
|
+
if [[ -d "$INSTANCES_DIR" ]]; then
|
|
262
|
+
local instance_count=$(find "$INSTANCES_DIR" -maxdepth 1 -type d 2>/dev/null | wc -l)
|
|
263
|
+
instance_count=$((instance_count - 1)) # Exclude parent dir
|
|
264
|
+
doctor_check "Instances" "success" "($instance_count account profiles)"
|
|
265
|
+
else
|
|
266
|
+
doctor_check "Instances" "success" "(no account profiles)"
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# Check permissions
|
|
270
|
+
local test_file="$HOME/.ccs/.permission-test"
|
|
271
|
+
if echo "test" > "$test_file" 2>/dev/null; then
|
|
272
|
+
rm -f "$test_file" 2>/dev/null
|
|
273
|
+
doctor_check "Permissions" "success"
|
|
274
|
+
else
|
|
275
|
+
doctor_check "Permissions" "error" "Cannot write to ~/.ccs/"
|
|
276
|
+
has_errors=true
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
# Summary
|
|
280
|
+
echo ""
|
|
281
|
+
echo -e "${CYAN}═══════════════════════════════════════════${RESET}"
|
|
282
|
+
if $has_errors; then
|
|
283
|
+
echo -e "${RED}Status: Installation has errors${RESET}"
|
|
284
|
+
echo "Run: npm install -g @kaitranntt/ccs --force"
|
|
285
|
+
else
|
|
286
|
+
echo -e "${GREEN}✓ All checks passed!${RESET}"
|
|
287
|
+
fi
|
|
288
|
+
echo ""
|
|
289
|
+
|
|
290
|
+
$has_errors && exit 1 || exit 0
|
|
291
|
+
}
|
|
292
|
+
|
|
99
293
|
# --- Claude CLI Detection Logic ---
|
|
100
294
|
|
|
101
295
|
detect_claude_cli() {
|
|
@@ -124,219 +318,600 @@ Solutions:
|
|
|
124
318
|
Restart your terminal after installation."
|
|
125
319
|
}
|
|
126
320
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
local
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
321
|
+
show_version() {
|
|
322
|
+
echo -e "${BOLD}CCS (Claude Code Switch) v${CCS_VERSION}${RESET}"
|
|
323
|
+
echo ""
|
|
324
|
+
echo -e "${CYAN}Installation:${RESET}"
|
|
325
|
+
|
|
326
|
+
# Simple location - just show what 'command -v' returns
|
|
327
|
+
local location=$(command -v ccs 2>/dev/null || echo "(not installed)")
|
|
328
|
+
echo -e " ${CYAN}Location:${RESET} ${location}"
|
|
329
|
+
|
|
330
|
+
# Simple config display
|
|
331
|
+
local config="${CCS_CONFIG:-$HOME/.ccs/config.json}"
|
|
332
|
+
echo -e " ${CYAN}Config:${RESET} ${config}"
|
|
333
|
+
echo ""
|
|
334
|
+
|
|
335
|
+
echo -e "${CYAN}Documentation:${RESET} https://github.com/kaitranntt/ccs"
|
|
336
|
+
echo -e "${CYAN}License:${RESET} MIT"
|
|
337
|
+
echo ""
|
|
338
|
+
echo -e "${YELLOW}Run 'ccs --help' for usage information${RESET}"
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# --- Profile Registry Functions (Phase 4) ---
|
|
342
|
+
|
|
343
|
+
# Initialize empty registry if missing
|
|
344
|
+
init_profiles_json() {
|
|
345
|
+
[[ -f "$PROFILES_JSON" ]] && return 0
|
|
346
|
+
|
|
347
|
+
local init_data='{
|
|
348
|
+
"version": "2.0.0",
|
|
349
|
+
"profiles": {},
|
|
350
|
+
"default": null
|
|
351
|
+
}'
|
|
352
|
+
|
|
353
|
+
echo "$init_data" > "$PROFILES_JSON"
|
|
354
|
+
chmod 0600 "$PROFILES_JSON"
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# Read entire profiles.json
|
|
358
|
+
read_profiles_json() {
|
|
359
|
+
init_profiles_json
|
|
360
|
+
cat "$PROFILES_JSON"
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# Write entire profiles.json (atomic)
|
|
364
|
+
write_profiles_json() {
|
|
365
|
+
local content="$1"
|
|
366
|
+
local temp_file="$PROFILES_JSON.tmp"
|
|
367
|
+
|
|
368
|
+
echo "$content" > "$temp_file" || {
|
|
369
|
+
msg_error "Failed to write profiles registry"
|
|
370
|
+
return 1
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
chmod 0600 "$temp_file"
|
|
374
|
+
mv "$temp_file" "$PROFILES_JSON" || {
|
|
375
|
+
rm -f "$temp_file"
|
|
376
|
+
msg_error "Failed to update profiles registry"
|
|
377
|
+
return 1
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
# Check if profile exists
|
|
382
|
+
profile_exists() {
|
|
383
|
+
local profile_name="$1"
|
|
384
|
+
init_profiles_json
|
|
385
|
+
|
|
386
|
+
local exists=$(jq -r ".profiles.\"$profile_name\" // empty" "$PROFILES_JSON")
|
|
387
|
+
[[ -n "$exists" ]]
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# Create new profile
|
|
391
|
+
register_profile() {
|
|
392
|
+
local profile_name="$1"
|
|
393
|
+
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
394
|
+
|
|
395
|
+
init_profiles_json
|
|
396
|
+
|
|
397
|
+
# Check if exists
|
|
398
|
+
profile_exists "$profile_name" && {
|
|
399
|
+
msg_error "Profile already exists: $profile_name"
|
|
400
|
+
return 1
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
# Read current data
|
|
404
|
+
local data=$(read_profiles_json)
|
|
405
|
+
|
|
406
|
+
# Add new profile
|
|
407
|
+
data=$(echo "$data" | jq \
|
|
408
|
+
--arg name "$profile_name" \
|
|
409
|
+
--arg timestamp "$timestamp" \
|
|
410
|
+
'.profiles[$name] = {
|
|
411
|
+
"type": "account",
|
|
412
|
+
"created": $timestamp,
|
|
413
|
+
"last_used": null
|
|
414
|
+
}')
|
|
415
|
+
|
|
416
|
+
# Note: No longer auto-set as default
|
|
417
|
+
# Users must explicitly run: ccs auth default <profile>
|
|
418
|
+
# Default always stays on implicit 'default' profile (uses ~/.claude/)
|
|
419
|
+
|
|
420
|
+
write_profiles_json "$data"
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
# Delete profile
|
|
424
|
+
unregister_profile() {
|
|
425
|
+
local profile_name="$1"
|
|
426
|
+
|
|
427
|
+
init_profiles_json
|
|
428
|
+
|
|
429
|
+
profile_exists "$profile_name" || return 0 # Idempotent
|
|
430
|
+
|
|
431
|
+
local data=$(read_profiles_json)
|
|
432
|
+
|
|
433
|
+
# Remove profile
|
|
434
|
+
data=$(echo "$data" | jq --arg name "$profile_name" 'del(.profiles[$name])')
|
|
435
|
+
|
|
436
|
+
# Update default if it was the deleted profile
|
|
437
|
+
local current_default=$(echo "$data" | jq -r '.default // empty')
|
|
438
|
+
if [[ "$current_default" == "$profile_name" ]]; then
|
|
439
|
+
# Set to first remaining profile or null
|
|
440
|
+
local first_profile=$(echo "$data" | jq -r '.profiles | keys[0] // empty')
|
|
441
|
+
data=$(echo "$data" | jq --arg first "$first_profile" '
|
|
442
|
+
.default = if $first != "" then $first else null end
|
|
443
|
+
')
|
|
444
|
+
fi
|
|
445
|
+
|
|
446
|
+
write_profiles_json "$data"
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
# Update last_used timestamp
|
|
450
|
+
touch_profile() {
|
|
451
|
+
local profile_name="$1"
|
|
452
|
+
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
453
|
+
|
|
454
|
+
profile_exists "$profile_name" || return 0 # Silent fail if not exists
|
|
455
|
+
|
|
456
|
+
local data=$(read_profiles_json)
|
|
457
|
+
|
|
458
|
+
data=$(echo "$data" | jq \
|
|
459
|
+
--arg name "$profile_name" \
|
|
460
|
+
--arg timestamp "$timestamp" \
|
|
461
|
+
'.profiles[$name].last_used = $timestamp')
|
|
462
|
+
|
|
463
|
+
write_profiles_json "$data"
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
# Get default profile
|
|
467
|
+
get_default_profile() {
|
|
468
|
+
init_profiles_json
|
|
469
|
+
jq -r '.default // empty' "$PROFILES_JSON"
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# Set default profile
|
|
473
|
+
set_default_profile() {
|
|
474
|
+
local profile_name="$1"
|
|
475
|
+
|
|
476
|
+
profile_exists "$profile_name" || {
|
|
477
|
+
msg_error "Profile not found: $profile_name"
|
|
478
|
+
return 1
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
local data=$(read_profiles_json)
|
|
482
|
+
data=$(echo "$data" | jq --arg name "$profile_name" '.default = $name')
|
|
483
|
+
|
|
484
|
+
write_profiles_json "$data"
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
# --- Instance Management Functions (Phase 2) ---
|
|
488
|
+
|
|
489
|
+
# Sanitize profile name for filesystem
|
|
490
|
+
sanitize_profile_name() {
|
|
491
|
+
local name="$1"
|
|
492
|
+
# Replace unsafe chars with dash, lowercase
|
|
493
|
+
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g'
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
# Initialize new instance directory
|
|
497
|
+
initialize_instance() {
|
|
498
|
+
local instance_path="$1"
|
|
499
|
+
|
|
500
|
+
# Create base directory
|
|
501
|
+
mkdir -m 0700 -p "$instance_path"
|
|
502
|
+
|
|
503
|
+
# Create subdirectories
|
|
504
|
+
local subdirs=(session-env todos logs file-history shell-snapshots debug .anthropic commands skills)
|
|
505
|
+
for dir in "${subdirs[@]}"; do
|
|
506
|
+
mkdir -m 0700 -p "$instance_path/$dir"
|
|
507
|
+
done
|
|
508
|
+
|
|
509
|
+
# Copy global configs (optional)
|
|
510
|
+
copy_global_configs "$instance_path"
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
# Validate instance structure (auto-repair)
|
|
514
|
+
validate_instance() {
|
|
515
|
+
local instance_path="$1"
|
|
516
|
+
local required_dirs=(session-env todos logs file-history shell-snapshots debug .anthropic)
|
|
517
|
+
|
|
518
|
+
for dir in "${required_dirs[@]}"; do
|
|
519
|
+
if [[ ! -d "$instance_path/$dir" ]]; then
|
|
520
|
+
mkdir -m 0700 -p "$instance_path/$dir"
|
|
142
521
|
fi
|
|
143
522
|
done
|
|
523
|
+
}
|
|
144
524
|
|
|
145
|
-
|
|
525
|
+
# Copy global Claude configs to instance
|
|
526
|
+
copy_global_configs() {
|
|
527
|
+
local instance_path="$1"
|
|
528
|
+
local global_claude="$HOME/.claude"
|
|
146
529
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
echo "│"
|
|
530
|
+
# Copy settings.json
|
|
531
|
+
[[ -f "$global_claude/settings.json" ]] && \
|
|
532
|
+
cp "$global_claude/settings.json" "$instance_path/settings.json" 2>/dev/null || true
|
|
151
533
|
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
msg_error "Source directory not found.
|
|
534
|
+
# Copy commands/
|
|
535
|
+
[[ -d "$global_claude/commands" ]] && \
|
|
536
|
+
cp -r "$global_claude/commands" "$instance_path/" 2>/dev/null || true
|
|
156
537
|
|
|
157
|
-
|
|
158
|
-
- $
|
|
159
|
-
|
|
538
|
+
# Copy skills/
|
|
539
|
+
[[ -d "$global_claude/skills" ]] && \
|
|
540
|
+
cp -r "$global_claude/skills" "$instance_path/" 2>/dev/null || true
|
|
541
|
+
}
|
|
160
542
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
543
|
+
# Ensure instance exists (lazy initialization)
|
|
544
|
+
ensure_instance() {
|
|
545
|
+
local profile_name="$1"
|
|
546
|
+
local safe_name=$(sanitize_profile_name "$profile_name")
|
|
547
|
+
local instance_path="$INSTANCES_DIR/$safe_name"
|
|
548
|
+
|
|
549
|
+
# Create if missing
|
|
550
|
+
if [[ ! -d "$instance_path" ]]; then
|
|
551
|
+
initialize_instance "$instance_path"
|
|
165
552
|
fi
|
|
166
553
|
|
|
167
|
-
#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
installed_count=$((installed_count + 1))
|
|
189
|
-
else
|
|
190
|
-
echo "| | [X] Failed to install command: $cmd_name.md"
|
|
191
|
-
fi
|
|
192
|
-
fi
|
|
193
|
-
fi
|
|
194
|
-
done
|
|
195
|
-
else
|
|
196
|
-
echo "| [i] No commands directory found"
|
|
554
|
+
# Validate structure
|
|
555
|
+
validate_instance "$instance_path"
|
|
556
|
+
|
|
557
|
+
echo "$instance_path"
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
# --- Profile Detection Logic (Phase 1) ---
|
|
561
|
+
|
|
562
|
+
# List available profiles for error messages
|
|
563
|
+
list_available_profiles() {
|
|
564
|
+
local lines=()
|
|
565
|
+
|
|
566
|
+
# Settings-based profiles
|
|
567
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
568
|
+
local settings_profiles=$(jq -r '.profiles | keys[]' "$CONFIG_FILE" 2>/dev/null || true)
|
|
569
|
+
if [[ -n "$settings_profiles" ]]; then
|
|
570
|
+
lines+=("Settings-based profiles (GLM, Kimi, etc.):")
|
|
571
|
+
while IFS= read -r name; do
|
|
572
|
+
lines+=(" - $name")
|
|
573
|
+
done <<< "$settings_profiles"
|
|
574
|
+
fi
|
|
197
575
|
fi
|
|
198
576
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
local
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
echo "| | [X] Failed to install skill: $skill_name"
|
|
218
|
-
fi
|
|
219
|
-
fi
|
|
220
|
-
fi
|
|
221
|
-
done
|
|
577
|
+
# Account-based profiles
|
|
578
|
+
if [[ -f "$PROFILES_JSON" ]]; then
|
|
579
|
+
local account_profiles=$(jq -r '.profiles | keys[]' "$PROFILES_JSON" 2>/dev/null || true)
|
|
580
|
+
local default_profile=$(jq -r '.default // empty' "$PROFILES_JSON" 2>/dev/null || true)
|
|
581
|
+
|
|
582
|
+
if [[ -n "$account_profiles" ]]; then
|
|
583
|
+
lines+=("Account-based profiles:")
|
|
584
|
+
while IFS= read -r name; do
|
|
585
|
+
local is_default=""
|
|
586
|
+
[[ "$name" == "$default_profile" ]] && is_default=" [DEFAULT]"
|
|
587
|
+
lines+=(" - $name$is_default")
|
|
588
|
+
done <<< "$account_profiles"
|
|
589
|
+
fi
|
|
590
|
+
fi
|
|
591
|
+
|
|
592
|
+
if [[ ${#lines[@]} -eq 0 ]]; then
|
|
593
|
+
echo " (no profiles configured)"
|
|
594
|
+
echo " Run \"ccs auth create <profile>\" to create your first account profile."
|
|
222
595
|
else
|
|
223
|
-
|
|
596
|
+
printf '%s\n' "${lines[@]}"
|
|
224
597
|
fi
|
|
598
|
+
}
|
|
225
599
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
echo " Removed: 0 items (nothing was installed)"
|
|
600
|
+
# Detect profile type and return info
|
|
601
|
+
# Sets global variables: PROFILE_TYPE, PROFILE_PATH/INSTANCE_PATH
|
|
602
|
+
detect_profile_type() {
|
|
603
|
+
local profile_name="$1"
|
|
604
|
+
|
|
605
|
+
# Special case: 'default' resolves to default profile
|
|
606
|
+
if [[ "$profile_name" == "default" ]]; then
|
|
607
|
+
# Check account-based default first
|
|
608
|
+
if [[ -f "$PROFILES_JSON" ]]; then
|
|
609
|
+
local default_account=$(jq -r '.default // empty' "$PROFILES_JSON" 2>/dev/null || true)
|
|
610
|
+
if [[ -n "$default_account" ]] && profile_exists "$default_account"; then
|
|
611
|
+
PROFILE_TYPE="account"
|
|
612
|
+
PROFILE_NAME="$default_account"
|
|
613
|
+
return 0
|
|
614
|
+
fi
|
|
615
|
+
fi
|
|
616
|
+
|
|
617
|
+
# Check settings-based default
|
|
618
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
619
|
+
local default_settings=$(jq -r '.profiles.default // empty' "$CONFIG_FILE" 2>/dev/null || true)
|
|
620
|
+
if [[ -n "$default_settings" ]]; then
|
|
621
|
+
PROFILE_TYPE="settings"
|
|
622
|
+
PROFILE_PATH="$default_settings"
|
|
623
|
+
PROFILE_NAME="default"
|
|
624
|
+
return 0
|
|
625
|
+
fi
|
|
626
|
+
fi
|
|
627
|
+
|
|
628
|
+
# No default configured, use Claude's defaults
|
|
629
|
+
PROFILE_TYPE="default"
|
|
630
|
+
PROFILE_NAME="default"
|
|
258
631
|
return 0
|
|
259
632
|
fi
|
|
260
633
|
|
|
261
|
-
#
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
removed_count=$((removed_count + 1))
|
|
271
|
-
else
|
|
272
|
-
echo "| | [X] Failed to remove command: $cmd_name.md"
|
|
273
|
-
fi
|
|
274
|
-
else
|
|
275
|
-
echo "| | [i] CCS command not found"
|
|
276
|
-
not_found_count=$((not_found_count + 1))
|
|
277
|
-
fi
|
|
278
|
-
done
|
|
279
|
-
else
|
|
280
|
-
echo "│ [i] Commands directory not found"
|
|
281
|
-
not_found_count=$((not_found_count + 1))
|
|
634
|
+
# Priority 1: Check settings-based profiles (backward compatibility)
|
|
635
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
636
|
+
local settings_path=$(jq -r ".profiles.\"$profile_name\" // empty" "$CONFIG_FILE" 2>/dev/null || true)
|
|
637
|
+
if [[ -n "$settings_path" ]]; then
|
|
638
|
+
PROFILE_TYPE="settings"
|
|
639
|
+
PROFILE_PATH="$settings_path"
|
|
640
|
+
PROFILE_NAME="$profile_name"
|
|
641
|
+
return 0
|
|
642
|
+
fi
|
|
282
643
|
fi
|
|
283
644
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
echo "| Removing skills..."
|
|
290
|
-
for skill_dir in "$skills_dir"/ccs-delegation; do
|
|
291
|
-
if [[ -d "$skill_dir" ]]; then
|
|
292
|
-
local skill_name=$(basename "$skill_dir")
|
|
293
|
-
if rm -rf "$skill_dir"; then
|
|
294
|
-
echo "| | [OK] Removed skill: $skill_name"
|
|
295
|
-
removed_count=$((removed_count + 1))
|
|
296
|
-
else
|
|
297
|
-
echo "| | [X] Failed to remove skill: $skill_name"
|
|
298
|
-
fi
|
|
299
|
-
else
|
|
300
|
-
echo "| | [i] CCS skill not found"
|
|
301
|
-
not_found_count=$((not_found_count + 1))
|
|
302
|
-
fi
|
|
303
|
-
done
|
|
304
|
-
else
|
|
305
|
-
echo "│ [i] Skills directory not found"
|
|
306
|
-
not_found_count=$((not_found_count + 1))
|
|
645
|
+
# Priority 2: Check account-based profiles
|
|
646
|
+
if [[ -f "$PROFILES_JSON" ]] && profile_exists "$profile_name"; then
|
|
647
|
+
PROFILE_TYPE="account"
|
|
648
|
+
PROFILE_NAME="$profile_name"
|
|
649
|
+
return 0
|
|
307
650
|
fi
|
|
308
651
|
|
|
309
|
-
|
|
652
|
+
# Not found
|
|
653
|
+
PROFILE_TYPE="error"
|
|
654
|
+
return 1
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
# --- Auth Commands (Phase 3) ---
|
|
658
|
+
|
|
659
|
+
auth_help() {
|
|
660
|
+
echo -e "${BOLD}CCS Account Management${RESET}"
|
|
661
|
+
echo ""
|
|
662
|
+
echo -e "${CYAN}Usage:${RESET}"
|
|
663
|
+
echo -e " ${YELLOW}ccs auth${RESET} <command> [options]"
|
|
310
664
|
echo ""
|
|
311
|
-
echo
|
|
312
|
-
echo "
|
|
313
|
-
echo "
|
|
665
|
+
echo -e "${CYAN}Commands:${RESET}"
|
|
666
|
+
echo -e " ${YELLOW}create <profile>${RESET} Create new profile and login"
|
|
667
|
+
echo -e " ${YELLOW}list${RESET} List all saved profiles"
|
|
668
|
+
echo -e " ${YELLOW}show <profile>${RESET} Show profile details"
|
|
669
|
+
echo -e " ${YELLOW}remove <profile>${RESET} Remove saved profile"
|
|
670
|
+
echo -e " ${YELLOW}default <profile>${RESET} Set default profile"
|
|
671
|
+
echo ""
|
|
672
|
+
echo -e "${CYAN}Examples:${RESET}"
|
|
673
|
+
echo -e " ${YELLOW}ccs auth create work${RESET} # Create & login to work profile"
|
|
674
|
+
echo -e " ${YELLOW}ccs auth default work${RESET} # Set work as default"
|
|
675
|
+
echo -e " ${YELLOW}ccs auth list${RESET} # List all profiles"
|
|
676
|
+
echo -e " ${YELLOW}ccs work \"review code\"${RESET} # Use work profile"
|
|
677
|
+
echo -e " ${YELLOW}ccs \"review code\"${RESET} # Use default profile"
|
|
314
678
|
echo ""
|
|
315
|
-
echo
|
|
316
|
-
echo "
|
|
317
|
-
|
|
318
|
-
|
|
679
|
+
echo -e "${CYAN}Note:${RESET}"
|
|
680
|
+
echo -e " By default, ${YELLOW}ccs${RESET} uses Claude CLI defaults from ~/.claude/"
|
|
681
|
+
echo -e " Use ${YELLOW}ccs auth default <profile>${RESET} to change the default profile."
|
|
682
|
+
echo ""
|
|
683
|
+
}
|
|
319
684
|
|
|
320
|
-
|
|
321
|
-
|
|
685
|
+
auth_create() {
|
|
686
|
+
local profile_name=""
|
|
687
|
+
local force=false
|
|
688
|
+
|
|
689
|
+
# Parse arguments
|
|
690
|
+
while [[ $# -gt 0 ]]; do
|
|
691
|
+
case "$1" in
|
|
692
|
+
--force) force=true ;;
|
|
693
|
+
-*) msg_error "Unknown option: $1"; return 1 ;;
|
|
694
|
+
*) profile_name="$1" ;;
|
|
695
|
+
esac
|
|
696
|
+
shift
|
|
697
|
+
done
|
|
698
|
+
|
|
699
|
+
# Validate profile name
|
|
700
|
+
[[ -z "$profile_name" ]] && {
|
|
701
|
+
msg_error "Profile name is required"
|
|
702
|
+
echo ""
|
|
703
|
+
echo "Usage: ${YELLOW}ccs auth create <profile> [--force]${RESET}"
|
|
704
|
+
return 1
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
# Check if exists
|
|
708
|
+
if ! $force && profile_exists "$profile_name"; then
|
|
709
|
+
msg_error "Profile already exists: $profile_name"
|
|
710
|
+
echo "Use ${YELLOW}--force${RESET} to overwrite"
|
|
711
|
+
return 1
|
|
712
|
+
fi
|
|
713
|
+
|
|
714
|
+
# Create instance
|
|
715
|
+
echo "[i] Creating profile: $profile_name"
|
|
716
|
+
local instance_path=$(ensure_instance "$profile_name")
|
|
717
|
+
echo "[i] Instance directory: $instance_path"
|
|
322
718
|
echo ""
|
|
323
|
-
echo -e "${CYAN}Installation:${RESET}"
|
|
324
719
|
|
|
325
|
-
#
|
|
326
|
-
|
|
327
|
-
echo -e " ${CYAN}Location:${RESET} ${location}"
|
|
720
|
+
# Register profile
|
|
721
|
+
register_profile "$profile_name"
|
|
328
722
|
|
|
329
|
-
#
|
|
330
|
-
|
|
331
|
-
echo -e "
|
|
723
|
+
# Launch Claude for login
|
|
724
|
+
echo -e "${YELLOW}[i] Starting Claude in isolated instance...${RESET}"
|
|
725
|
+
echo -e "${YELLOW}[i] You will be prompted to login with your account.${RESET}"
|
|
332
726
|
echo ""
|
|
333
727
|
|
|
334
|
-
|
|
335
|
-
|
|
728
|
+
CLAUDE_CONFIG_DIR="$instance_path" $(detect_claude_cli) || {
|
|
729
|
+
msg_error "Login failed or cancelled"
|
|
730
|
+
echo "To retry: ${YELLOW}ccs auth create $profile_name --force${RESET}"
|
|
731
|
+
return 1
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
echo ""
|
|
735
|
+
echo -e "${GREEN}[OK] Profile created successfully${RESET}"
|
|
736
|
+
echo ""
|
|
737
|
+
echo " Profile: $profile_name"
|
|
738
|
+
echo " Instance: $instance_path"
|
|
739
|
+
echo ""
|
|
740
|
+
echo "Usage:"
|
|
741
|
+
echo " ${YELLOW}ccs $profile_name \"your prompt here\"${RESET} # Use this specific profile"
|
|
742
|
+
echo ""
|
|
743
|
+
echo "To set as default (so you can use just \"ccs\"):"
|
|
744
|
+
echo " ${YELLOW}ccs auth default $profile_name${RESET}"
|
|
745
|
+
echo ""
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
auth_list() {
|
|
749
|
+
local verbose=false
|
|
750
|
+
[[ "${1:-}" == "--verbose" ]] && verbose=true
|
|
751
|
+
|
|
752
|
+
# Read profiles.json
|
|
753
|
+
[[ ! -f "$PROFILES_JSON" ]] && {
|
|
754
|
+
echo -e "${YELLOW}No account profiles found${RESET}"
|
|
755
|
+
echo ""
|
|
756
|
+
echo "To create your first profile:"
|
|
757
|
+
echo " ${YELLOW}ccs auth create <profile>${RESET}"
|
|
758
|
+
return 0
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
local profiles=$(jq -r '.profiles | keys[]' "$PROFILES_JSON" 2>/dev/null || true)
|
|
762
|
+
local default_profile=$(jq -r '.default // empty' "$PROFILES_JSON" 2>/dev/null || true)
|
|
763
|
+
|
|
764
|
+
[[ -z "$profiles" ]] && {
|
|
765
|
+
echo -e "${YELLOW}No account profiles found${RESET}"
|
|
766
|
+
return 0
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
echo -e "${BOLD}Saved Account Profiles:${RESET}"
|
|
770
|
+
echo ""
|
|
771
|
+
|
|
772
|
+
# Display profiles
|
|
773
|
+
while IFS= read -r profile; do
|
|
774
|
+
local is_default=false
|
|
775
|
+
[[ "$profile" == "$default_profile" ]] && is_default=true
|
|
776
|
+
|
|
777
|
+
if $is_default; then
|
|
778
|
+
echo -e "${GREEN}[*] ${CYAN}$profile${GREEN} (default)${RESET}"
|
|
779
|
+
else
|
|
780
|
+
echo -e "[ ] ${CYAN}$profile${RESET}"
|
|
781
|
+
fi
|
|
782
|
+
|
|
783
|
+
local type=$(jq -r ".profiles.\"$profile\".type // \"account\"" "$PROFILES_JSON" 2>/dev/null || true)
|
|
784
|
+
echo " Type: $type"
|
|
785
|
+
|
|
786
|
+
if $verbose; then
|
|
787
|
+
local created=$(jq -r ".profiles.\"$profile\".created" "$PROFILES_JSON" 2>/dev/null || true)
|
|
788
|
+
local last_used=$(jq -r ".profiles.\"$profile\".last_used // \"Never\"" "$PROFILES_JSON" 2>/dev/null || true)
|
|
789
|
+
echo " Created: $created"
|
|
790
|
+
echo " Last used: $last_used"
|
|
791
|
+
fi
|
|
792
|
+
|
|
793
|
+
echo ""
|
|
794
|
+
done <<< "$profiles"
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
auth_show() {
|
|
798
|
+
local profile_name="${1:-}"
|
|
799
|
+
|
|
800
|
+
[[ -z "$profile_name" ]] && {
|
|
801
|
+
msg_error "Profile name is required"
|
|
802
|
+
echo "Usage: ${YELLOW}ccs auth show <profile>${RESET}"
|
|
803
|
+
return 1
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
# Check if exists
|
|
807
|
+
profile_exists "$profile_name" || {
|
|
808
|
+
msg_error "Profile not found: $profile_name"
|
|
809
|
+
return 1
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
local default_profile=$(jq -r '.default // empty' "$PROFILES_JSON" 2>/dev/null || true)
|
|
813
|
+
local is_default=false
|
|
814
|
+
[[ "$profile_name" == "$default_profile" ]] && is_default=true
|
|
815
|
+
|
|
816
|
+
echo -e "${BOLD}Profile: $profile_name${RESET}"
|
|
817
|
+
echo ""
|
|
818
|
+
|
|
819
|
+
local type=$(jq -r ".profiles.\"$profile_name\".type // \"account\"" "$PROFILES_JSON" 2>/dev/null || true)
|
|
820
|
+
local created=$(jq -r ".profiles.\"$profile_name\".created" "$PROFILES_JSON" 2>/dev/null || true)
|
|
821
|
+
local last_used=$(jq -r ".profiles.\"$profile_name\".last_used // \"Never\"" "$PROFILES_JSON" 2>/dev/null || true)
|
|
822
|
+
local instance_path="$INSTANCES_DIR/$(sanitize_profile_name "$profile_name")"
|
|
823
|
+
|
|
824
|
+
echo " Type: $type"
|
|
825
|
+
echo " Default: $($is_default && echo "Yes" || echo "No")"
|
|
826
|
+
echo " Instance: $instance_path"
|
|
827
|
+
echo " Created: $created"
|
|
828
|
+
echo " Last used: $last_used"
|
|
829
|
+
echo ""
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
auth_remove() {
|
|
833
|
+
local profile_name=""
|
|
834
|
+
local force=false
|
|
835
|
+
|
|
836
|
+
while [[ $# -gt 0 ]]; do
|
|
837
|
+
case "$1" in
|
|
838
|
+
--force) force=true ;;
|
|
839
|
+
*) profile_name="$1" ;;
|
|
840
|
+
esac
|
|
841
|
+
shift
|
|
842
|
+
done
|
|
843
|
+
|
|
844
|
+
[[ -z "$profile_name" ]] && {
|
|
845
|
+
msg_error "Profile name is required"
|
|
846
|
+
echo "Usage: ${YELLOW}ccs auth remove <profile> --force${RESET}"
|
|
847
|
+
return 1
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
profile_exists "$profile_name" || {
|
|
851
|
+
msg_error "Profile not found: $profile_name"
|
|
852
|
+
return 1
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
$force || {
|
|
856
|
+
msg_error "Removal requires --force flag for safety"
|
|
857
|
+
echo "Run: ${YELLOW}ccs auth remove $profile_name --force${RESET}"
|
|
858
|
+
return 1
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
# Delete instance directory
|
|
862
|
+
local instance_path="$INSTANCES_DIR/$(sanitize_profile_name "$profile_name")"
|
|
863
|
+
rm -rf "$instance_path"
|
|
864
|
+
|
|
865
|
+
# Remove from registry
|
|
866
|
+
unregister_profile "$profile_name"
|
|
867
|
+
|
|
868
|
+
echo -e "${GREEN}[OK] Profile removed successfully${RESET}"
|
|
869
|
+
echo " Profile: $profile_name"
|
|
870
|
+
echo ""
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
auth_default() {
|
|
874
|
+
local profile_name="${1:-}"
|
|
875
|
+
|
|
876
|
+
[[ -z "$profile_name" ]] && {
|
|
877
|
+
msg_error "Profile name is required"
|
|
878
|
+
echo "Usage: ${YELLOW}ccs auth default <profile>${RESET}"
|
|
879
|
+
return 1
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
profile_exists "$profile_name" || {
|
|
883
|
+
msg_error "Profile not found: $profile_name"
|
|
884
|
+
return 1
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
set_default_profile "$profile_name"
|
|
888
|
+
|
|
889
|
+
echo -e "${GREEN}[OK] Default profile set${RESET}"
|
|
890
|
+
echo " Profile: $profile_name"
|
|
891
|
+
echo ""
|
|
892
|
+
echo "Now you can use:"
|
|
893
|
+
echo " ${YELLOW}ccs \"your prompt\"${RESET} # Uses $profile_name profile"
|
|
336
894
|
echo ""
|
|
337
|
-
echo -e "${YELLOW}Run 'ccs --help' for usage information${RESET}"
|
|
338
895
|
}
|
|
339
896
|
|
|
897
|
+
handle_auth_commands() {
|
|
898
|
+
shift # Remove 'auth'
|
|
899
|
+
|
|
900
|
+
local subcommand="${1:-}"
|
|
901
|
+
shift || true
|
|
902
|
+
|
|
903
|
+
case "$subcommand" in
|
|
904
|
+
create) auth_create "$@" ;;
|
|
905
|
+
list) auth_list "$@" ;;
|
|
906
|
+
show) auth_show "$@" ;;
|
|
907
|
+
remove) auth_remove "$@" ;;
|
|
908
|
+
default) auth_default "$@" ;;
|
|
909
|
+
*) auth_help ;;
|
|
910
|
+
esac
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
# --- Main Execution Logic ---
|
|
914
|
+
|
|
340
915
|
# Special case: version command (check BEFORE profile detection)
|
|
341
916
|
if [[ $# -gt 0 ]] && [[ "${1}" == "version" || "${1}" == "--version" || "${1}" == "-v" ]]; then
|
|
342
917
|
show_version
|
|
@@ -345,37 +920,28 @@ fi
|
|
|
345
920
|
|
|
346
921
|
# Special case: help command (check BEFORE profile detection)
|
|
347
922
|
if [[ $# -gt 0 ]] && [[ "${1}" == "--help" || "${1}" == "-h" || "${1}" == "help" ]]; then
|
|
348
|
-
setup_colors
|
|
349
923
|
show_help
|
|
350
924
|
exit 0
|
|
351
925
|
fi
|
|
352
926
|
|
|
353
|
-
# Special case:
|
|
354
|
-
if [[ $# -gt 0 ]] && [[ "${1}" == "
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
echo ""
|
|
358
|
-
echo "The --install flag is currently under development."
|
|
359
|
-
echo ".claude/ integration testing is not complete."
|
|
360
|
-
echo ""
|
|
361
|
-
echo "For updates: https://github.com/kaitranntt/ccs/issues"
|
|
362
|
-
echo ""
|
|
363
|
-
exit 0
|
|
927
|
+
# Special case: auth commands
|
|
928
|
+
if [[ $# -gt 0 ]] && [[ "${1}" == "auth" ]]; then
|
|
929
|
+
handle_auth_commands "$@"
|
|
930
|
+
exit $?
|
|
364
931
|
fi
|
|
365
932
|
|
|
366
|
-
# Special case:
|
|
367
|
-
if [[ $# -gt 0 ]] && [[ "${1}" == "--
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
echo ""
|
|
371
|
-
echo "The --uninstall flag is currently under development."
|
|
372
|
-
echo ".claude/ integration testing is not complete."
|
|
373
|
-
echo ""
|
|
374
|
-
echo "For updates: https://github.com/kaitranntt/ccs/issues"
|
|
375
|
-
echo ""
|
|
376
|
-
exit 0
|
|
933
|
+
# Special case: doctor command
|
|
934
|
+
if [[ $# -gt 0 ]] && [[ "${1}" == "doctor" || "${1}" == "--doctor" ]]; then
|
|
935
|
+
doctor_run
|
|
936
|
+
exit $?
|
|
377
937
|
fi
|
|
378
938
|
|
|
939
|
+
# Run auto-recovery before main logic
|
|
940
|
+
auto_recover || {
|
|
941
|
+
msg_error "Auto-recovery failed. Check permissions."
|
|
942
|
+
exit 1
|
|
943
|
+
}
|
|
944
|
+
|
|
379
945
|
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
|
|
380
946
|
if [[ $# -eq 0 ]] || [[ "${1}" =~ ^- ]]; then
|
|
381
947
|
# No args or first arg is a flag → use default profile
|
|
@@ -385,29 +951,6 @@ else
|
|
|
385
951
|
PROFILE="${1}"
|
|
386
952
|
fi
|
|
387
953
|
|
|
388
|
-
# Check config exists
|
|
389
|
-
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
390
|
-
msg_error "Config file not found: $CONFIG_FILE
|
|
391
|
-
|
|
392
|
-
Solutions:
|
|
393
|
-
1. Reinstall CCS:
|
|
394
|
-
curl -fsSL ccs.kaitran.ca/install | bash
|
|
395
|
-
|
|
396
|
-
2. Or create config manually:
|
|
397
|
-
mkdir -p ~/.ccs
|
|
398
|
-
cat > ~/.ccs/config.json << 'EOF'
|
|
399
|
-
{
|
|
400
|
-
\"profiles\": {
|
|
401
|
-
\"glm\": \"~/.ccs/glm.settings.json\",
|
|
402
|
-
\"kimi\": \"~/.ccs/kimi.settings.json\",
|
|
403
|
-
\"default\": \"~/.claude/settings.json\"
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
EOF"
|
|
407
|
-
exit 1
|
|
408
|
-
fi
|
|
409
|
-
|
|
410
|
-
|
|
411
954
|
# Validate profile name (alphanumeric, dash, underscore only)
|
|
412
955
|
if [[ "$PROFILE" =~ [^a-zA-Z0-9_-] ]]; then
|
|
413
956
|
msg_error "Invalid profile name: $PROFILE
|
|
@@ -416,44 +959,12 @@ Use only alphanumeric characters, dash, or underscore."
|
|
|
416
959
|
exit 1
|
|
417
960
|
fi
|
|
418
961
|
|
|
419
|
-
#
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if [[ -z "$SETTINGS_PATH" ]]; then
|
|
423
|
-
# Could be: invalid JSON, no profiles object, or profile not found
|
|
424
|
-
# Show helpful error based on what we can detect
|
|
425
|
-
if ! jq -e . "$CONFIG_FILE" &>/dev/null; then
|
|
426
|
-
msg_error "Invalid JSON in $CONFIG_FILE
|
|
427
|
-
|
|
428
|
-
Fix the JSON syntax or reinstall:
|
|
429
|
-
curl -fsSL ccs.kaitran.ca/install | bash"
|
|
430
|
-
elif ! jq -e '.profiles' "$CONFIG_FILE" &>/dev/null; then
|
|
431
|
-
msg_error "Config must have 'profiles' object
|
|
432
|
-
|
|
433
|
-
See config/config.example.json for correct format
|
|
434
|
-
Or reinstall:
|
|
435
|
-
curl -fsSL ccs.kaitran.ca/install | bash"
|
|
436
|
-
else
|
|
437
|
-
AVAILABLE_PROFILES=$(jq -r '.profiles | keys[]' "$CONFIG_FILE" 2>/dev/null | sed 's/^/ - /')
|
|
438
|
-
msg_error "Profile '$PROFILE' not found in $CONFIG_FILE
|
|
962
|
+
# Detect profile type
|
|
963
|
+
if ! detect_profile_type "$PROFILE"; then
|
|
964
|
+
msg_error "Profile '$PROFILE' not found
|
|
439
965
|
|
|
440
966
|
Available profiles:
|
|
441
|
-
$
|
|
442
|
-
fi
|
|
443
|
-
exit 1
|
|
444
|
-
fi
|
|
445
|
-
|
|
446
|
-
# Expand ~ in path
|
|
447
|
-
SETTINGS_PATH="${SETTINGS_PATH/#\~/$HOME}"
|
|
448
|
-
|
|
449
|
-
# Validate settings file exists
|
|
450
|
-
if [[ ! -f "$SETTINGS_PATH" ]]; then
|
|
451
|
-
msg_error "Settings file not found: $SETTINGS_PATH
|
|
452
|
-
|
|
453
|
-
Solutions:
|
|
454
|
-
1. Create the settings file for profile '$PROFILE'
|
|
455
|
-
2. Update the path in $CONFIG_FILE
|
|
456
|
-
3. Or reinstall: curl -fsSL ccs.kaitran.ca/install | bash"
|
|
967
|
+
$(list_available_profiles)"
|
|
457
968
|
exit 1
|
|
458
969
|
fi
|
|
459
970
|
|
|
@@ -465,9 +976,45 @@ fi
|
|
|
465
976
|
# Detect Claude CLI executable
|
|
466
977
|
CLAUDE_CLI=$(detect_claude_cli)
|
|
467
978
|
|
|
468
|
-
# Execute
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
979
|
+
# Execute based on profile type (Phase 5)
|
|
980
|
+
case "$PROFILE_TYPE" in
|
|
981
|
+
account)
|
|
982
|
+
# Account-based profile: use CLAUDE_CONFIG_DIR
|
|
983
|
+
INSTANCE_PATH=$(ensure_instance "$PROFILE_NAME")
|
|
984
|
+
touch_profile "$PROFILE_NAME" # Update last_used
|
|
985
|
+
|
|
986
|
+
# Execute Claude with isolated config
|
|
987
|
+
CLAUDE_CONFIG_DIR="$INSTANCE_PATH" exec "$CLAUDE_CLI" "$@" || {
|
|
988
|
+
show_claude_not_found_error
|
|
989
|
+
exit 1
|
|
990
|
+
}
|
|
991
|
+
;;
|
|
992
|
+
|
|
993
|
+
settings)
|
|
994
|
+
# Settings-based profile: use --settings flag
|
|
995
|
+
SETTINGS_PATH="${PROFILE_PATH/#\~/$HOME}"
|
|
996
|
+
|
|
997
|
+
[[ ! -f "$SETTINGS_PATH" ]] && {
|
|
998
|
+
msg_error "Settings file not found: $SETTINGS_PATH"
|
|
999
|
+
exit 1
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
exec "$CLAUDE_CLI" --settings "$SETTINGS_PATH" "$@" || {
|
|
1003
|
+
show_claude_not_found_error
|
|
1004
|
+
exit 1
|
|
1005
|
+
}
|
|
1006
|
+
;;
|
|
1007
|
+
|
|
1008
|
+
default)
|
|
1009
|
+
# Default: no special handling
|
|
1010
|
+
exec "$CLAUDE_CLI" "$@" || {
|
|
1011
|
+
show_claude_not_found_error
|
|
1012
|
+
exit 1
|
|
1013
|
+
}
|
|
1014
|
+
;;
|
|
1015
|
+
|
|
1016
|
+
*)
|
|
1017
|
+
msg_error "Unknown profile type: $PROFILE_TYPE"
|
|
1018
|
+
exit 1
|
|
1019
|
+
;;
|
|
1020
|
+
esac
|