@kaitranntt/ccs 2.4.4 → 2.4.6
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.md +21 -40
- package/README.vi.md +13 -32
- package/VERSION +1 -1
- package/bin/ccs.js +122 -131
- package/bin/claude-detector.js +5 -34
- package/bin/config-manager.js +8 -69
- package/bin/helpers.js +40 -33
- package/lib/ccs +131 -80
- package/lib/ccs.ps1 +219 -364
- package/package.json +1 -1
package/bin/helpers.js
CHANGED
|
@@ -4,26 +4,44 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
cyan: '\x1b[0;36m',
|
|
13
|
-
green: '\x1b[0;32m',
|
|
14
|
-
bold: '\x1b[1m',
|
|
15
|
-
reset: '\x1b[0m'
|
|
16
|
-
} : { red: '', yellow: '', cyan: '', green: '', bold: '', reset: '' };
|
|
7
|
+
// TTY-aware color detection (matches lib/ccs bash logic)
|
|
8
|
+
function getColors() {
|
|
9
|
+
const forcedColors = process.env.FORCE_COLOR;
|
|
10
|
+
const noColor = process.env.NO_COLOR;
|
|
11
|
+
const isTTY = process.stdout.isTTY === true; // Must be explicitly true
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
const useColors = forcedColors || (isTTY && !noColor);
|
|
14
|
+
|
|
15
|
+
if (useColors) {
|
|
16
|
+
return {
|
|
17
|
+
red: '\x1b[0;31m',
|
|
18
|
+
yellow: '\x1b[1;33m',
|
|
19
|
+
cyan: '\x1b[0;36m',
|
|
20
|
+
green: '\x1b[0;32m',
|
|
21
|
+
bold: '\x1b[1m',
|
|
22
|
+
reset: '\x1b[0m'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { red: '', yellow: '', cyan: '', green: '', bold: '', reset: '' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
// Colors object (dynamic)
|
|
31
|
+
const colors = getColors();
|
|
32
|
+
|
|
33
|
+
// Helper: Apply color to text (returns plain text if colors disabled)
|
|
34
|
+
function colored(text, colorName = 'reset') {
|
|
35
|
+
const currentColors = getColors();
|
|
36
|
+
const color = currentColors[colorName] || '';
|
|
37
|
+
return color ? `${color}${text}${currentColors.reset}` : text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Simple error formatting
|
|
41
|
+
function error(message) {
|
|
42
|
+
console.error(`ERROR: ${message}`);
|
|
43
|
+
console.error('Try: npm install -g @kaitranntt/ccs --force');
|
|
44
|
+
process.exit(1);
|
|
27
45
|
}
|
|
28
46
|
|
|
29
47
|
// Path expansion (~ and env vars)
|
|
@@ -45,21 +63,10 @@ function expandPath(pathStr) {
|
|
|
45
63
|
return path.normalize(pathStr);
|
|
46
64
|
}
|
|
47
65
|
|
|
48
|
-
// Validate profile name (alphanumeric, dash, underscore only)
|
|
49
|
-
function validateProfileName(profile) {
|
|
50
|
-
return /^[a-zA-Z0-9_-]+$/.test(profile);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Validate path safety (prevent injection)
|
|
54
|
-
function isPathSafe(pathStr) {
|
|
55
|
-
// Allow: alphanumeric, path separators, space, dash, underscore, dot, colon, tilde
|
|
56
|
-
return !/[;|&<>`$*?\[\]'"()]/.test(pathStr);
|
|
57
|
-
}
|
|
58
66
|
|
|
59
67
|
module.exports = {
|
|
60
68
|
colors,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
isPathSafe
|
|
69
|
+
colored,
|
|
70
|
+
error,
|
|
71
|
+
expandPath
|
|
65
72
|
};
|
package/lib/ccs
CHANGED
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
# Version (updated by scripts/bump-version.sh)
|
|
5
|
-
CCS_VERSION="2.4.
|
|
5
|
+
CCS_VERSION="2.4.6"
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
|
|
7
8
|
|
|
8
9
|
# --- Color/Format Functions ---
|
|
9
10
|
setup_colors() {
|
|
10
|
-
if
|
|
11
|
+
# Enable colors if: FORCE_COLOR set OR (TTY detected AND NO_COLOR not set) OR (TERM supports colors AND NO_COLOR not set)
|
|
12
|
+
if [[ -n "${FORCE_COLOR:-}" ]] || \
|
|
13
|
+
([[ -t 1 || -t 2 ]] && [[ -z "${NO_COLOR:-}" ]]) || \
|
|
14
|
+
([[ -n "${TERM:-}" && "${TERM}" != "dumb" ]] && [[ -z "${NO_COLOR:-}" ]]); then
|
|
11
15
|
RED='\033[0;31m'
|
|
12
16
|
YELLOW='\033[1;33m'
|
|
17
|
+
CYAN='\033[0;36m'
|
|
13
18
|
BOLD='\033[1m'
|
|
14
19
|
RESET='\033[0m'
|
|
15
20
|
else
|
|
16
|
-
RED='' YELLOW='' BOLD='' RESET=''
|
|
21
|
+
RED='' YELLOW='' CYAN='' BOLD='' RESET=''
|
|
17
22
|
fi
|
|
18
23
|
}
|
|
19
24
|
|
|
@@ -27,27 +32,67 @@ msg_error() {
|
|
|
27
32
|
echo "" >&2
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
show_help() {
|
|
36
|
+
echo -e "${BOLD}CCS (Claude Code Switch) - Instant profile switching for Claude CLI${RESET}"
|
|
37
|
+
echo ""
|
|
38
|
+
echo -e "${CYAN}Usage:${RESET}"
|
|
39
|
+
echo -e " ${YELLOW}ccs${RESET} [profile] [claude-args...]"
|
|
40
|
+
echo -e " ${YELLOW}ccs${RESET} [flags]"
|
|
41
|
+
echo ""
|
|
42
|
+
echo -e "${CYAN}Description:${RESET}"
|
|
43
|
+
echo -e " Switch between Claude models instantly. Stop hitting rate limits."
|
|
44
|
+
echo -e " Maps profile names to Claude settings files via ~/.ccs/config.json"
|
|
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 glm${RESET} \"debug this code\" Switch to GLM and run command"
|
|
50
|
+
echo -e " ${YELLOW}ccs glm${RESET} --verbose Switch to GLM with Claude flags"
|
|
51
|
+
echo ""
|
|
52
|
+
echo -e "${CYAN}Flags:${RESET}"
|
|
53
|
+
echo -e " ${YELLOW}-h, --help${RESET} Show this help message"
|
|
54
|
+
echo -e " ${YELLOW}-v, --version${RESET} Show version and installation info"
|
|
55
|
+
echo ""
|
|
56
|
+
echo -e "${CYAN}Configuration:${RESET}"
|
|
57
|
+
echo -e " Config File: ~/.ccs/config.json"
|
|
58
|
+
echo -e " Settings: ~/.ccs/*.settings.json"
|
|
59
|
+
echo -e " Environment: CCS_CONFIG (override config path)"
|
|
60
|
+
echo ""
|
|
61
|
+
echo -e "${CYAN}Examples:${RESET}"
|
|
62
|
+
echo -e " # Use default Claude subscription"
|
|
63
|
+
echo -e " ${YELLOW}ccs${RESET} \"Review this architecture\""
|
|
64
|
+
echo ""
|
|
65
|
+
echo -e " # Switch to GLM for cost-effective tasks"
|
|
66
|
+
echo -e " ${YELLOW}ccs glm${RESET} \"Write unit tests\""
|
|
67
|
+
echo ""
|
|
68
|
+
echo -e " # Use GLM with verbose output"
|
|
69
|
+
echo -e " ${YELLOW}ccs glm${RESET} --verbose \"Debug error\""
|
|
70
|
+
echo ""
|
|
71
|
+
echo -e "${YELLOW}Uninstall:${RESET}"
|
|
72
|
+
echo -e " macOS/Linux: curl -fsSL ccs.kaitran.ca/uninstall | bash"
|
|
73
|
+
echo -e " Windows: irm ccs.kaitran.ca/uninstall | iex"
|
|
74
|
+
echo -e " npm: npm uninstall -g @kaitranntt/ccs"
|
|
75
|
+
echo ""
|
|
76
|
+
echo -e "${CYAN}Documentation:${RESET}"
|
|
77
|
+
echo -e " GitHub: ${CYAN}https://github.com/kaitranntt/ccs${RESET}"
|
|
78
|
+
echo -e " Docs: https://github.com/kaitranntt/ccs/blob/main/README.md"
|
|
79
|
+
echo -e " Issues: https://github.com/kaitranntt/ccs/issues"
|
|
80
|
+
echo ""
|
|
81
|
+
echo -e "${CYAN}License:${RESET} MIT"
|
|
82
|
+
}
|
|
83
|
+
|
|
30
84
|
setup_colors
|
|
31
85
|
|
|
86
|
+
# Check dependencies early
|
|
87
|
+
command -v jq &>/dev/null || {
|
|
88
|
+
msg_error "jq required but not installed. Install: brew install jq (macOS) or apt install jq (Ubuntu)"
|
|
89
|
+
exit 1
|
|
90
|
+
}
|
|
91
|
+
|
|
32
92
|
# --- Claude CLI Detection Logic ---
|
|
33
93
|
|
|
34
94
|
detect_claude_cli() {
|
|
35
|
-
|
|
36
|
-
if [[ -n "${CCS_CLAUDE_PATH:-}" ]]; then
|
|
37
|
-
# Basic validation: file exists
|
|
38
|
-
if [[ -f "$CCS_CLAUDE_PATH" ]]; then
|
|
39
|
-
echo "$CCS_CLAUDE_PATH"
|
|
40
|
-
return 0
|
|
41
|
-
fi
|
|
42
|
-
# Invalid CCS_CLAUDE_PATH - show warning and fall back to PATH
|
|
43
|
-
echo "[!] Warning: CCS_CLAUDE_PATH is set but file not found: $CCS_CLAUDE_PATH" >&2
|
|
44
|
-
echo " Falling back to system PATH lookup..." >&2
|
|
45
|
-
fi
|
|
46
|
-
|
|
47
|
-
# Priority 2: Use 'claude' from PATH (trust the system)
|
|
48
|
-
# This is the standard case - if user installed Claude CLI, it's in their PATH
|
|
49
|
-
echo "claude"
|
|
50
|
-
return 0
|
|
95
|
+
echo "${CCS_CLAUDE_PATH:-claude}"
|
|
51
96
|
}
|
|
52
97
|
|
|
53
98
|
show_claude_not_found_error() {
|
|
@@ -72,10 +117,10 @@ Solutions:
|
|
|
72
117
|
Restart your terminal after installation."
|
|
73
118
|
}
|
|
74
119
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
#
|
|
78
|
-
|
|
120
|
+
# WIP: .claude/ integration testing incomplete
|
|
121
|
+
# Feature disabled until testing complete
|
|
122
|
+
# install_commands_and_skills() {
|
|
123
|
+
: <<'COMMENTED_OUT'
|
|
79
124
|
# Try both possible locations for .claude directory
|
|
80
125
|
local source_dir=""
|
|
81
126
|
local possible_dirs=(
|
|
@@ -179,10 +224,13 @@ Solution:
|
|
|
179
224
|
echo ""
|
|
180
225
|
echo "You can now use the /ccs command in Claude CLI for task delegation."
|
|
181
226
|
echo "Example: /ccs glm /plan 'add user authentication'"
|
|
182
|
-
|
|
227
|
+
COMMENTED_OUT
|
|
228
|
+
# }
|
|
183
229
|
|
|
184
|
-
#
|
|
185
|
-
|
|
230
|
+
# WIP: .claude/ integration testing incomplete
|
|
231
|
+
# Feature disabled until testing complete
|
|
232
|
+
# uninstall_commands_and_skills() {
|
|
233
|
+
: <<'COMMENTED_OUT'
|
|
186
234
|
local target_dir="$HOME/.claude"
|
|
187
235
|
local removed_count=0
|
|
188
236
|
local not_found_count=0
|
|
@@ -259,49 +307,66 @@ uninstall_commands_and_skills() {
|
|
|
259
307
|
echo ""
|
|
260
308
|
echo "The /ccs command is no longer available in Claude CLI."
|
|
261
309
|
echo "To reinstall: ccs --install"
|
|
310
|
+
COMMENTED_OUT
|
|
311
|
+
# }
|
|
312
|
+
|
|
313
|
+
show_version() {
|
|
314
|
+
echo -e "${BOLD}CCS (Claude Code Switch) v${CCS_VERSION}${RESET}"
|
|
315
|
+
echo ""
|
|
316
|
+
echo -e "${CYAN}Installation:${RESET}"
|
|
317
|
+
|
|
318
|
+
# Simple location - just show what 'command -v' returns
|
|
319
|
+
local location=$(command -v ccs 2>/dev/null || echo "(not installed)")
|
|
320
|
+
echo -e " ${CYAN}Location:${RESET} ${location}"
|
|
321
|
+
|
|
322
|
+
# Simple config display
|
|
323
|
+
local config="${CCS_CONFIG:-$HOME/.ccs/config.json}"
|
|
324
|
+
echo -e " ${CYAN}Config:${RESET} ${config}"
|
|
325
|
+
echo ""
|
|
326
|
+
|
|
327
|
+
echo -e "${CYAN}Documentation:${RESET} https://github.com/kaitranntt/ccs"
|
|
328
|
+
echo -e "${CYAN}License:${RESET} MIT"
|
|
329
|
+
echo ""
|
|
330
|
+
echo -e "${YELLOW}Run 'ccs --help' for usage information${RESET}"
|
|
262
331
|
}
|
|
263
332
|
|
|
264
333
|
# Special case: version command (check BEFORE profile detection)
|
|
265
334
|
if [[ $# -gt 0 ]] && [[ "${1}" == "version" || "${1}" == "--version" || "${1}" == "-v" ]]; then
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
# Show install location if we can determine it
|
|
269
|
-
INSTALL_LOCATION=$(command -v ccs 2>/dev/null || echo "unknown")
|
|
270
|
-
if [[ "$INSTALL_LOCATION" != "unknown" ]]; then
|
|
271
|
-
# Resolve symlink to actual file
|
|
272
|
-
if [[ -L "$INSTALL_LOCATION" ]]; then
|
|
273
|
-
ACTUAL_LOCATION=$(readlink "$INSTALL_LOCATION" 2>/dev/null || echo "$INSTALL_LOCATION")
|
|
274
|
-
echo "Installed at: $INSTALL_LOCATION -> $ACTUAL_LOCATION"
|
|
275
|
-
else
|
|
276
|
-
echo "Installed at: $INSTALL_LOCATION"
|
|
277
|
-
fi
|
|
278
|
-
fi
|
|
279
|
-
|
|
280
|
-
echo "https://github.com/kaitranntt/ccs"
|
|
335
|
+
show_version
|
|
281
336
|
exit 0
|
|
282
337
|
fi
|
|
283
338
|
|
|
284
339
|
# Special case: help command (check BEFORE profile detection)
|
|
285
340
|
if [[ $# -gt 0 ]] && [[ "${1}" == "--help" || "${1}" == "-h" || "${1}" == "help" ]]; then
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if ! exec "$CLAUDE_CLI" --help "$@"; then
|
|
290
|
-
show_claude_not_found_error
|
|
291
|
-
exit 1
|
|
292
|
-
fi
|
|
341
|
+
setup_colors
|
|
342
|
+
show_help
|
|
343
|
+
exit 0
|
|
293
344
|
fi
|
|
294
345
|
|
|
295
346
|
# Special case: install command (check BEFORE profile detection)
|
|
296
347
|
if [[ $# -gt 0 ]] && [[ "${1}" == "--install" ]]; then
|
|
297
|
-
|
|
298
|
-
|
|
348
|
+
echo ""
|
|
349
|
+
echo "Feature not available"
|
|
350
|
+
echo ""
|
|
351
|
+
echo "The --install flag is currently under development."
|
|
352
|
+
echo ".claude/ integration testing is not complete."
|
|
353
|
+
echo ""
|
|
354
|
+
echo "For updates: https://github.com/kaitranntt/ccs/issues"
|
|
355
|
+
echo ""
|
|
356
|
+
exit 0
|
|
299
357
|
fi
|
|
300
358
|
|
|
301
359
|
# Special case: uninstall command (check BEFORE profile detection)
|
|
302
360
|
if [[ $# -gt 0 ]] && [[ "${1}" == "--uninstall" ]]; then
|
|
303
|
-
|
|
304
|
-
|
|
361
|
+
echo ""
|
|
362
|
+
echo "Feature not available"
|
|
363
|
+
echo ""
|
|
364
|
+
echo "The --uninstall flag is currently under development."
|
|
365
|
+
echo ".claude/ integration testing is not complete."
|
|
366
|
+
echo ""
|
|
367
|
+
echo "For updates: https://github.com/kaitranntt/ccs/issues"
|
|
368
|
+
echo ""
|
|
369
|
+
exit 0
|
|
305
370
|
fi
|
|
306
371
|
|
|
307
372
|
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
|
|
@@ -334,16 +399,6 @@ EOF"
|
|
|
334
399
|
exit 1
|
|
335
400
|
fi
|
|
336
401
|
|
|
337
|
-
# Check jq installed
|
|
338
|
-
if ! command -v jq &> /dev/null; then
|
|
339
|
-
msg_error "jq is required but not installed
|
|
340
|
-
|
|
341
|
-
Install jq:
|
|
342
|
-
macOS: brew install jq
|
|
343
|
-
Ubuntu: sudo apt install jq
|
|
344
|
-
Fedora: sudo dnf install jq"
|
|
345
|
-
exit 1
|
|
346
|
-
fi
|
|
347
402
|
|
|
348
403
|
# Validate profile name (alphanumeric, dash, underscore only)
|
|
349
404
|
if [[ "$PROFILE" =~ [^a-zA-Z0-9_-] ]]; then
|
|
@@ -353,34 +408,30 @@ Use only alphanumeric characters, dash, or underscore."
|
|
|
353
408
|
exit 1
|
|
354
409
|
fi
|
|
355
410
|
|
|
356
|
-
#
|
|
357
|
-
|
|
358
|
-
|
|
411
|
+
# Single check gets profile path, validates JSON + structure in one step
|
|
412
|
+
SETTINGS_PATH=$(jq -r --arg profile "$PROFILE" '.profiles[$profile] // empty' "$CONFIG_FILE" 2>/dev/null)
|
|
413
|
+
|
|
414
|
+
if [[ -z "$SETTINGS_PATH" ]]; then
|
|
415
|
+
# Could be: invalid JSON, no profiles object, or profile not found
|
|
416
|
+
# Show helpful error based on what we can detect
|
|
417
|
+
if ! jq -e . "$CONFIG_FILE" &>/dev/null; then
|
|
418
|
+
msg_error "Invalid JSON in $CONFIG_FILE
|
|
359
419
|
|
|
360
420
|
Fix the JSON syntax or reinstall:
|
|
361
421
|
curl -fsSL ccs.kaitran.ca/install | bash"
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
# Validate config has profiles object
|
|
366
|
-
if ! jq -e '.profiles' "$CONFIG_FILE" &>/dev/null; then
|
|
367
|
-
msg_error "Config must have 'profiles' object
|
|
422
|
+
elif ! jq -e '.profiles' "$CONFIG_FILE" &>/dev/null; then
|
|
423
|
+
msg_error "Config must have 'profiles' object
|
|
368
424
|
|
|
369
425
|
See config/config.example.json for correct format
|
|
370
426
|
Or reinstall:
|
|
371
427
|
curl -fsSL ccs.kaitran.ca/install | bash"
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
# Get settings path for profile (using --arg to prevent injection)
|
|
376
|
-
SETTINGS_PATH=$(jq -r --arg profile "$PROFILE" '.profiles[$profile] // empty' "$CONFIG_FILE")
|
|
377
|
-
|
|
378
|
-
if [[ -z "$SETTINGS_PATH" ]]; then
|
|
379
|
-
AVAILABLE_PROFILES=$(jq -r '.profiles | keys[]' "$CONFIG_FILE" 2>/dev/null | sed 's/^/ - /')
|
|
380
|
-
msg_error "Profile '$PROFILE' not found in $CONFIG_FILE
|
|
428
|
+
else
|
|
429
|
+
AVAILABLE_PROFILES=$(jq -r '.profiles | keys[]' "$CONFIG_FILE" 2>/dev/null | sed 's/^/ - /')
|
|
430
|
+
msg_error "Profile '$PROFILE' not found in $CONFIG_FILE
|
|
381
431
|
|
|
382
432
|
Available profiles:
|
|
383
433
|
$AVAILABLE_PROFILES"
|
|
434
|
+
fi
|
|
384
435
|
exit 1
|
|
385
436
|
fi
|
|
386
437
|
|