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