@kaitranntt/ccs 2.4.0

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/lib/ccs ADDED
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Version (updated by scripts/bump-version.sh)
5
+ CCS_VERSION="2.4.0"
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+
8
+ # --- Color/Format Functions ---
9
+ setup_colors() {
10
+ if [[ -t 2 ]] && [[ -z "${NO_COLOR:-}" ]]; then
11
+ RED='\033[0;31m'
12
+ YELLOW='\033[1;33m'
13
+ BOLD='\033[1m'
14
+ RESET='\033[0m'
15
+ else
16
+ RED='' YELLOW='' BOLD='' RESET=''
17
+ fi
18
+ }
19
+
20
+ msg_error() {
21
+ echo "" >&2
22
+ echo -e "${RED}${BOLD}╔═════════════════════════════════════════════╗${RESET}" >&2
23
+ echo -e "${RED}${BOLD}║ ERROR ║${RESET}" >&2
24
+ echo -e "${RED}${BOLD}╚═════════════════════════════════════════════╝${RESET}" >&2
25
+ echo "" >&2
26
+ echo -e "${RED}$1${RESET}" >&2
27
+ echo "" >&2
28
+ }
29
+
30
+ setup_colors
31
+
32
+ # --- Claude CLI Detection Logic ---
33
+
34
+ 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
51
+ }
52
+
53
+ show_claude_not_found_error() {
54
+ msg_error "Claude CLI not found in PATH
55
+
56
+ CCS requires Claude CLI to be installed and available in your PATH.
57
+
58
+ Solutions:
59
+ 1. Install Claude CLI:
60
+ https://docs.claude.com/en/docs/claude-code/installation
61
+
62
+ 2. Verify installation:
63
+ command -v claude
64
+
65
+ 3. If installed but not in PATH, add it:
66
+ # Find Claude installation
67
+ which claude
68
+
69
+ # Or set custom path
70
+ export CCS_CLAUDE_PATH='/path/to/claude'
71
+
72
+ Restart your terminal after installation."
73
+ }
74
+
75
+ CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
76
+
77
+ # Installation function for commands and skills
78
+ install_commands_and_skills() {
79
+ # Try both possible locations for .claude directory
80
+ local source_dir=""
81
+ local possible_dirs=(
82
+ "$SCRIPT_DIR/.claude" # Development: tools/ccs/.claude
83
+ "$HOME/.ccs/.claude" # Installed: ~/.ccs/.claude
84
+ )
85
+
86
+ for dir in "${possible_dirs[@]}"; do
87
+ if [[ -d "$dir" ]]; then
88
+ source_dir="$dir"
89
+ break
90
+ fi
91
+ done
92
+
93
+ local target_dir="$HOME/.claude"
94
+
95
+ echo "┌─ Installing CCS Commands & Skills"
96
+ echo "│ Source: $source_dir"
97
+ echo "│ Target: $target_dir"
98
+ echo "│"
99
+
100
+ # Check if source directory exists
101
+ if [[ ! -d "$source_dir" ]]; then
102
+ echo "|"
103
+ msg_error "Source directory not found.
104
+
105
+ Checked locations:
106
+ - $SCRIPT_DIR/.claude (development)
107
+ - $HOME/.ccs/.claude (installed)
108
+
109
+ Solution:
110
+ 1. If developing: Ensure you're in the CCS repository
111
+ 2. If installed: Reinstall CCS with: curl -fsSL ccs.kaitran.ca/install | bash"
112
+ return 1
113
+ fi
114
+
115
+ # Create target directories if they don't exist
116
+ mkdir -p "$target_dir/commands"
117
+ mkdir -p "$target_dir/skills"
118
+
119
+ local installed_count=0
120
+ local skipped_count=0
121
+
122
+ # Install commands
123
+ if [[ -d "$source_dir/commands" ]]; then
124
+ echo "│ Installing commands..."
125
+ for cmd_file in "$source_dir/commands"/*.md; do
126
+ if [[ -f "$cmd_file" ]]; then
127
+ local cmd_name=$(basename "$cmd_file" .md)
128
+ local target_file="$target_dir/commands/$cmd_name.md"
129
+
130
+ if [[ -f "$target_file" ]]; then
131
+ echo "| | [i] Skipping existing command: $cmd_name.md"
132
+ skipped_count=$((skipped_count + 1))
133
+ else
134
+ if cp "$cmd_file" "$target_file"; then
135
+ echo "| | [OK] Installed command: $cmd_name.md"
136
+ installed_count=$((installed_count + 1))
137
+ else
138
+ echo "| | [X] Failed to install command: $cmd_name.md"
139
+ fi
140
+ fi
141
+ fi
142
+ done
143
+ else
144
+ echo "| [i] No commands directory found"
145
+ fi
146
+
147
+ echo "|"
148
+
149
+ # Install skills
150
+ if [[ -d "$source_dir/skills" ]]; then
151
+ echo "| Installing skills..."
152
+ for skill_dir in "$source_dir/skills"/*; do
153
+ if [[ -d "$skill_dir" ]]; then
154
+ local skill_name=$(basename "$skill_dir")
155
+ local target_skill_dir="$target_dir/skills/$skill_name"
156
+
157
+ if [[ -d "$target_skill_dir" ]]; then
158
+ echo "| | [i] Skipping existing skill: $skill_name"
159
+ skipped_count=$((skipped_count + 1))
160
+ else
161
+ if cp -r "$skill_dir" "$target_skill_dir"; then
162
+ echo "| | [OK] Installed skill: $skill_name"
163
+ installed_count=$((installed_count + 1))
164
+ else
165
+ echo "| | [X] Failed to install skill: $skill_name"
166
+ fi
167
+ fi
168
+ fi
169
+ done
170
+ else
171
+ echo "| [i] No skills directory found"
172
+ fi
173
+
174
+ echo "└─"
175
+ echo ""
176
+ echo "[OK] Installation complete!"
177
+ echo " Installed: $installed_count items"
178
+ echo " Skipped: $skipped_count items (already exist)"
179
+ echo ""
180
+ echo "You can now use the /ccs command in Claude CLI for task delegation."
181
+ echo "Example: /ccs glm /plan 'add user authentication'"
182
+ }
183
+
184
+ # Uninstallation function for commands and skills
185
+ uninstall_commands_and_skills() {
186
+ local target_dir="$HOME/.claude"
187
+ local removed_count=0
188
+ local not_found_count=0
189
+
190
+ echo "┌─ Uninstalling CCS Commands & Skills"
191
+ echo "│ Target: $target_dir"
192
+ echo "│"
193
+
194
+ # Check if target directory exists
195
+ if [[ ! -d "$target_dir" ]]; then
196
+ echo "|"
197
+ echo "│ [i] Claude directory not found: $target_dir"
198
+ echo "│ Nothing to uninstall."
199
+ echo "└─"
200
+ echo ""
201
+ echo "[OK] Uninstall complete!"
202
+ echo " Removed: 0 items (nothing was installed)"
203
+ return 0
204
+ fi
205
+
206
+ # Remove commands
207
+ local commands_dir="$target_dir/commands"
208
+ if [[ -d "$commands_dir" ]]; then
209
+ echo "│ Removing commands..."
210
+ for cmd_file in "$commands_dir"/ccs.md; do
211
+ if [[ -f "$cmd_file" ]]; then
212
+ local cmd_name=$(basename "$cmd_file" .md)
213
+ if rm "$cmd_file"; then
214
+ echo "| | [OK] Removed command: $cmd_name.md"
215
+ removed_count=$((removed_count + 1))
216
+ else
217
+ echo "| | [X] Failed to remove command: $cmd_name.md"
218
+ fi
219
+ else
220
+ echo "| | [i] CCS command not found"
221
+ not_found_count=$((not_found_count + 1))
222
+ fi
223
+ done
224
+ else
225
+ echo "│ [i] Commands directory not found"
226
+ not_found_count=$((not_found_count + 1))
227
+ fi
228
+
229
+ echo "|"
230
+
231
+ # Remove skills
232
+ local skills_dir="$target_dir/skills"
233
+ if [[ -d "$skills_dir" ]]; then
234
+ echo "| Removing skills..."
235
+ for skill_dir in "$skills_dir"/ccs-delegation; do
236
+ if [[ -d "$skill_dir" ]]; then
237
+ local skill_name=$(basename "$skill_dir")
238
+ if rm -rf "$skill_dir"; then
239
+ echo "| | [OK] Removed skill: $skill_name"
240
+ removed_count=$((removed_count + 1))
241
+ else
242
+ echo "| | [X] Failed to remove skill: $skill_name"
243
+ fi
244
+ else
245
+ echo "| | [i] CCS skill not found"
246
+ not_found_count=$((not_found_count + 1))
247
+ fi
248
+ done
249
+ else
250
+ echo "│ [i] Skills directory not found"
251
+ not_found_count=$((not_found_count + 1))
252
+ fi
253
+
254
+ echo "└─"
255
+ echo ""
256
+ echo "[OK] Uninstall complete!"
257
+ echo " Removed: $removed_count items"
258
+ echo " Not found: $not_found_count items (already removed)"
259
+ echo ""
260
+ echo "The /ccs command is no longer available in Claude CLI."
261
+ echo "To reinstall: ccs --install"
262
+ }
263
+
264
+ # Special case: version command (check BEFORE profile detection)
265
+ 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"
281
+ exit 0
282
+ fi
283
+
284
+ # Special case: help command (check BEFORE profile detection)
285
+ 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
293
+ fi
294
+
295
+ # Special case: install command (check BEFORE profile detection)
296
+ if [[ $# -gt 0 ]] && [[ "${1}" == "--install" ]]; then
297
+ install_commands_and_skills
298
+ exit $?
299
+ fi
300
+
301
+ # Special case: uninstall command (check BEFORE profile detection)
302
+ if [[ $# -gt 0 ]] && [[ "${1}" == "--uninstall" ]]; then
303
+ uninstall_commands_and_skills
304
+ exit $?
305
+ fi
306
+
307
+ # Smart profile detection: if first arg starts with '-', it's a flag not a profile
308
+ if [[ $# -eq 0 ]] || [[ "${1}" =~ ^- ]]; then
309
+ # No args or first arg is a flag → use default profile
310
+ PROFILE="default"
311
+ else
312
+ # First arg doesn't start with '-' → treat as profile name
313
+ PROFILE="${1}"
314
+ fi
315
+
316
+ # Check config exists
317
+ if [[ ! -f "$CONFIG_FILE" ]]; then
318
+ msg_error "Config file not found: $CONFIG_FILE
319
+
320
+ Solutions:
321
+ 1. Reinstall CCS:
322
+ curl -fsSL ccs.kaitran.ca/install | bash
323
+
324
+ 2. Or create config manually:
325
+ mkdir -p ~/.ccs
326
+ cat > ~/.ccs/config.json << 'EOF'
327
+ {
328
+ \"profiles\": {
329
+ \"glm\": \"~/.ccs/glm.settings.json\",
330
+ \"default\": \"~/.claude/settings.json\"
331
+ }
332
+ }
333
+ EOF"
334
+ exit 1
335
+ fi
336
+
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
+
348
+ # Validate profile name (alphanumeric, dash, underscore only)
349
+ if [[ "$PROFILE" =~ [^a-zA-Z0-9_-] ]]; then
350
+ msg_error "Invalid profile name: $PROFILE
351
+
352
+ Use only alphanumeric characters, dash, or underscore."
353
+ exit 1
354
+ fi
355
+
356
+ # Validate JSON syntax
357
+ if ! jq -e . "$CONFIG_FILE" &>/dev/null; then
358
+ msg_error "Invalid JSON in $CONFIG_FILE
359
+
360
+ Fix the JSON syntax or reinstall:
361
+ 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
368
+
369
+ See config/config.example.json for correct format
370
+ Or reinstall:
371
+ 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
381
+
382
+ Available profiles:
383
+ $AVAILABLE_PROFILES"
384
+ exit 1
385
+ fi
386
+
387
+ # Expand ~ in path
388
+ SETTINGS_PATH="${SETTINGS_PATH/#\~/$HOME}"
389
+
390
+ # Validate settings file exists
391
+ if [[ ! -f "$SETTINGS_PATH" ]]; then
392
+ msg_error "Settings file not found: $SETTINGS_PATH
393
+
394
+ Solutions:
395
+ 1. Create the settings file for profile '$PROFILE'
396
+ 2. Update the path in $CONFIG_FILE
397
+ 3. Or reinstall: curl -fsSL ccs.kaitran.ca/install | bash"
398
+ exit 1
399
+ fi
400
+
401
+ # Shift profile arg only if first arg was NOT a flag
402
+ if [[ $# -gt 0 ]] && [[ ! "${1}" =~ ^- ]]; then
403
+ shift
404
+ fi
405
+
406
+ # Detect Claude CLI executable
407
+ CLAUDE_CLI=$(detect_claude_cli)
408
+
409
+ # Execute Claude with the profile settings
410
+ # If claude is not found, exec will fail and show an error
411
+ if ! exec "$CLAUDE_CLI" --settings "$SETTINGS_PATH" "$@"; then
412
+ show_claude_not_found_error
413
+ exit 1
414
+ fi