@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/LICENSE +21 -0
- package/README.md +287 -0
- package/README.vi.md +257 -0
- package/VERSION +1 -0
- package/bin/ccs.js +196 -0
- package/bin/claude-detector.js +156 -0
- package/bin/config-manager.js +129 -0
- package/bin/helpers.js +65 -0
- package/config/base-glm.settings.json +10 -0
- package/config/config.example.json +6 -0
- package/lib/ccs +414 -0
- package/lib/ccs.ps1 +493 -0
- package/package.json +52 -0
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
|