@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/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
- // Color formatting (TTY-aware)
8
- const useColors = process.stderr.isTTY && !process.env.NO_COLOR;
9
- const colors = useColors ? {
10
- red: '\x1b[0;31m',
11
- yellow: '\x1b[1;33m',
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
- // Error formatting
19
- function showError(message) {
20
- console.error('');
21
- console.error(colors.red + colors.bold + '╔═════════════════════════════════════════════╗' + colors.reset);
22
- console.error(colors.red + colors.bold + '║ ERROR ║' + colors.reset);
23
- console.error(colors.red + colors.bold + '╚═════════════════════════════════════════════╝' + colors.reset);
24
- console.error('');
25
- console.error(colors.red + message + colors.reset);
26
- console.error('');
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
- showError,
62
- expandPath,
63
- validateProfileName,
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.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 [[ -t 2 ]] && [[ -z "${NO_COLOR:-}" ]]; then
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
- # Priority 1: CCS_CLAUDE_PATH environment variable (if user wants custom path)
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
- CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
76
-
77
- # Installation function for commands and skills
78
- install_commands_and_skills() {
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
- # Uninstallation function for commands and skills
185
- uninstall_commands_and_skills() {
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
- echo "CCS (Claude Code Switch) version $CCS_VERSION"
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
- shift # Remove the help argument
287
- CLAUDE_CLI=$(detect_claude_cli)
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
- install_commands_and_skills
298
- exit $?
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
- uninstall_commands_and_skills
304
- exit $?
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
- # Validate JSON syntax
357
- if ! jq -e . "$CONFIG_FILE" &>/dev/null; then
358
- msg_error "Invalid JSON in $CONFIG_FILE
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
- exit 1
363
- fi
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
- exit 1
373
- fi
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