@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.ps1 ADDED
@@ -0,0 +1,493 @@
1
+ # CCS - Claude Code Switch (Windows PowerShell)
2
+ # Cross-platform Claude CLI profile switcher
3
+ # https://github.com/kaitranntt/ccs
4
+
5
+ param(
6
+ [Parameter(Position=0)]
7
+ [string]$ProfileOrFlag = "default",
8
+
9
+ [Parameter(ValueFromRemainingArguments=$true)]
10
+ [string[]]$RemainingArgs
11
+ )
12
+
13
+ $ErrorActionPreference = "Stop"
14
+
15
+ # --- Color/Format Functions ---
16
+ function Write-ErrorMsg {
17
+ param([string]$Message)
18
+ Write-Host ""
19
+ Write-Host "╔═════════════════════════════════════════════╗" -ForegroundColor Red
20
+ Write-Host "║ ERROR ║" -ForegroundColor Red
21
+ Write-Host "╚═════════════════════════════════════════════╝" -ForegroundColor Red
22
+ Write-Host ""
23
+ Write-Host $Message -ForegroundColor Red
24
+ Write-Host ""
25
+ }
26
+
27
+ # --- Claude CLI Detection Logic ---
28
+
29
+ function Find-ClaudeCli {
30
+ [OutputType([string])]
31
+ param()
32
+
33
+ # Priority 1: CCS_CLAUDE_PATH environment variable (if user wants custom path)
34
+ $CcsClaudePath = $env:CCS_CLAUDE_PATH
35
+ if ($CcsClaudePath) {
36
+ # Basic validation: file exists
37
+ if (Test-Path $CcsClaudePath -PathType Leaf) {
38
+ return $CcsClaudePath
39
+ }
40
+ # Invalid CCS_CLAUDE_PATH - show warning and fall back to PATH
41
+ Write-Host "[!] Warning: CCS_CLAUDE_PATH is set but file not found: $CcsClaudePath" -ForegroundColor Yellow
42
+ Write-Host " Falling back to system PATH lookup..." -ForegroundColor Yellow
43
+ }
44
+
45
+ # Priority 2: Use 'claude' from PATH (trust the system)
46
+ # This is the standard case - if user installed Claude CLI, it's in their PATH
47
+ return "claude"
48
+ }
49
+
50
+ function Show-ClaudeNotFoundError {
51
+ Write-ErrorMsg @"
52
+ Claude CLI not found in PATH
53
+
54
+ CCS requires Claude CLI to be installed and available in your PATH.
55
+
56
+ Solutions:
57
+ 1. Install Claude CLI:
58
+ https://docs.claude.com/en/docs/claude-code/installation
59
+
60
+ 2. Verify installation:
61
+ Get-Command claude
62
+
63
+ 3. If installed but not in PATH, add it:
64
+ # Find Claude installation
65
+ where.exe claude
66
+
67
+ # Or set custom path
68
+ `$env:CCS_CLAUDE_PATH = 'C:\path\to\claude.exe'
69
+
70
+ Restart your terminal after installation.
71
+ "@
72
+ }
73
+
74
+ # Version (updated by scripts/bump-version.sh)
75
+ $CcsVersion = "2.4.0"
76
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
77
+
78
+ # Installation function for commands and skills
79
+ function Install-CommandsAndSkills {
80
+ # Try both possible locations for .claude directory
81
+ $SourceDir = $null
82
+ $PossibleDirs = @(
83
+ (Join-Path $ScriptDir ".claude"), # Development: tools/ccs/.claude
84
+ (Join-Path $env:USERPROFILE ".ccs\.claude") # Installed: ~/.ccs/.claude
85
+ )
86
+
87
+ foreach ($Dir in $PossibleDirs) {
88
+ if (Test-Path $Dir) {
89
+ $SourceDir = $Dir
90
+ break
91
+ }
92
+ }
93
+
94
+ $HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
95
+ $TargetDir = Join-Path $HomeDir ".claude"
96
+
97
+ Write-Host "[Installing CCS Commands and Skills]" -ForegroundColor Cyan
98
+ Write-Host "│ Source: $SourceDir"
99
+ Write-Host "│ Target: $TargetDir"
100
+ Write-Host "│"
101
+
102
+ # Check if source directory exists
103
+ if (-not $SourceDir) {
104
+ Write-Host "│"
105
+ $DevelopmentPath = Join-Path $ScriptDir ".claude"
106
+ $InstalledPath = Join-Path $env:USERPROFILE ".ccs\.claude"
107
+ Write-ErrorMsg @"
108
+ Source directory not found.
109
+
110
+ Checked locations:
111
+ - $DevelopmentPath (development)
112
+ - $InstalledPath (installed)
113
+
114
+ Solution:
115
+ 1. If developing: Ensure you're in the CCS repository
116
+ 2. If installed: Reinstall CCS with: irm ccs.kaitran.ca/install | iex
117
+ "@
118
+ exit 1
119
+ }
120
+
121
+ # Create target directories if they don't exist
122
+ $CommandsDir = Join-Path $TargetDir "commands"
123
+ $SkillsDir = Join-Path $TargetDir "skills"
124
+
125
+ if (-not (Test-Path $CommandsDir)) {
126
+ New-Item -ItemType Directory -Path $CommandsDir -Force | Out-Null
127
+ }
128
+ if (-not (Test-Path $SkillsDir)) {
129
+ New-Item -ItemType Directory -Path $SkillsDir -Force | Out-Null
130
+ }
131
+
132
+ $InstalledCount = 0
133
+ $SkippedCount = 0
134
+
135
+ # Install commands
136
+ $SourceCommandsDir = Join-Path $SourceDir "commands"
137
+ if (Test-Path $SourceCommandsDir) {
138
+ Write-Host "│ Installing commands..." -ForegroundColor Yellow
139
+ Get-ChildItem $SourceCommandsDir -Filter "*.md" | ForEach-Object {
140
+ $CmdName = $_.BaseName
141
+ $TargetFile = Join-Path $CommandsDir "$CmdName.md"
142
+
143
+ if (Test-Path $TargetFile) {
144
+ Write-Host "│ | [i] Skipping existing command: $CmdName.md" -ForegroundColor Yellow
145
+ $SkippedCount++
146
+ } else {
147
+ try {
148
+ Copy-Item $_.FullName $TargetFile -ErrorAction Stop
149
+ Write-Host "│ | [OK] Installed command: $CmdName.md" -ForegroundColor Green
150
+ $InstalledCount++
151
+ } catch {
152
+ Write-Host "│ | [!] Failed to install command: $CmdName.md" -ForegroundColor Red
153
+ Write-Host "│ Error: $($_.Exception.Message)" -ForegroundColor Red
154
+ }
155
+ }
156
+ }
157
+ } else {
158
+ Write-Host "│ [i] No commands directory found" -ForegroundColor Gray
159
+ }
160
+
161
+ Write-Host "│"
162
+
163
+ # Install skills
164
+ $SourceSkillsDir = Join-Path $SourceDir "skills"
165
+ if (Test-Path $SourceSkillsDir) {
166
+ Write-Host "│ Installing skills..." -ForegroundColor Yellow
167
+ Get-ChildItem $SourceSkillsDir -Directory | ForEach-Object {
168
+ $SkillName = $_.Name
169
+ $TargetSkillDir = Join-Path $SkillsDir $SkillName
170
+
171
+ if (Test-Path $TargetSkillDir) {
172
+ Write-Host "│ | [i] Skipping existing skill: $SkillName" -ForegroundColor Yellow
173
+ $SkippedCount++
174
+ } else {
175
+ try {
176
+ Copy-Item $_.FullName $TargetSkillDir -Recurse -ErrorAction Stop
177
+ Write-Host "│ | [OK] Installed skill: $SkillName" -ForegroundColor Green
178
+ $InstalledCount++
179
+ } catch {
180
+ Write-Host "│ | [!] Failed to install skill: $SkillName" -ForegroundColor Red
181
+ Write-Host "│ Error: $($_.Exception.Message)" -ForegroundColor Red
182
+ }
183
+ }
184
+ }
185
+ } else {
186
+ Write-Host "│ [i] No skills directory found" -ForegroundColor Gray
187
+ }
188
+
189
+ Write-Host "[DONE]"
190
+ Write-Host ""
191
+ Write-Host "[OK] Installation complete!" -ForegroundColor Green
192
+ Write-Host " Installed: $InstalledCount items"
193
+ Write-Host " Skipped: $SkippedCount items (already exist)"
194
+ Write-Host ""
195
+ Write-Host "You can now use the /ccs command in Claude CLI for task delegation." -ForegroundColor Cyan
196
+ Write-Host "Example: /ccs glm /plan 'add user authentication'" -ForegroundColor Cyan
197
+ }
198
+
199
+ # Uninstallation function for commands and skills
200
+ function Uninstall-CommandsAndSkills {
201
+ $HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
202
+ $TargetDir = Join-Path $HomeDir ".claude"
203
+ $RemovedCount = 0
204
+ $NotFoundCount = 0
205
+
206
+ Write-Host "[Uninstalling CCS Commands and Skills]" -ForegroundColor Cyan
207
+ Write-Host "│ Target: $TargetDir"
208
+ Write-Host "│"
209
+
210
+ # Check if target directory exists
211
+ if (-not (Test-Path $TargetDir)) {
212
+ Write-Host "│"
213
+ Write-Host "│ [i] Claude directory not found: $TargetDir" -ForegroundColor Gray
214
+ Write-Host "│ Nothing to uninstall."
215
+ Write-Host "[DONE]"
216
+ Write-Host ""
217
+ Write-Host "[OK] Uninstall complete!" -ForegroundColor Green
218
+ Write-Host " Removed: 0 items (nothing was installed)"
219
+ return
220
+ }
221
+
222
+ # Remove commands
223
+ $CommandsDir = Join-Path $TargetDir "commands"
224
+ if (Test-Path $CommandsDir) {
225
+ Write-Host "│ Removing commands..." -ForegroundColor Yellow
226
+ $CmdFile = Join-Path $CommandsDir "ccs.md"
227
+ if (Test-Path $CmdFile) {
228
+ try {
229
+ Remove-Item $CmdFile -Force -ErrorAction Stop
230
+ Write-Host "│ | [OK] Removed command: ccs.md" -ForegroundColor Green
231
+ $RemovedCount++
232
+ } catch {
233
+ Write-Host "│ | [!] Failed to remove command: ccs.md" -ForegroundColor Red
234
+ Write-Host "│ Error: $($_.Exception.Message)" -ForegroundColor Red
235
+ }
236
+ } else {
237
+ Write-Host "│ | [i] CCS command not found" -ForegroundColor Gray
238
+ $NotFoundCount++
239
+ }
240
+ } else {
241
+ Write-Host "│ [i] Commands directory not found" -ForegroundColor Gray
242
+ $NotFoundCount++
243
+ }
244
+
245
+ Write-Host "│"
246
+
247
+ # Remove skills
248
+ $SkillsDir = Join-Path $TargetDir "skills"
249
+ if (Test-Path $SkillsDir) {
250
+ Write-Host "│ Removing skills..." -ForegroundColor Yellow
251
+ $SkillDir = Join-Path $SkillsDir "ccs-delegation"
252
+ if (Test-Path $SkillDir) {
253
+ try {
254
+ Remove-Item $SkillDir -Recurse -Force -ErrorAction Stop
255
+ Write-Host "│ | [OK] Removed skill: ccs-delegation" -ForegroundColor Green
256
+ $RemovedCount++
257
+ } catch {
258
+ Write-Host "│ | [!] Failed to remove skill: ccs-delegation" -ForegroundColor Red
259
+ Write-Host "│ Error: $($_.Exception.Message)" -ForegroundColor Red
260
+ }
261
+ } else {
262
+ Write-Host "│ | [i] CCS skill not found" -ForegroundColor Gray
263
+ $NotFoundCount++
264
+ }
265
+ } else {
266
+ Write-Host "│ [i] Skills directory not found" -ForegroundColor Gray
267
+ $NotFoundCount++
268
+ }
269
+
270
+ Write-Host "[DONE]"
271
+ Write-Host ""
272
+ Write-Host "[OK] Uninstall complete!" -ForegroundColor Green
273
+ Write-Host " Removed: $RemovedCount items"
274
+ Write-Host " Not found: $NotFoundCount items (already removed)"
275
+ Write-Host ""
276
+ Write-Host "The /ccs command is no longer available in Claude CLI." -ForegroundColor Cyan
277
+ Write-Host "To reinstall: ccs --install" -ForegroundColor Cyan
278
+ }
279
+
280
+ # Special case: version command (check BEFORE profile detection)
281
+ # Check both $ProfileOrFlag and first element of $RemainingArgs
282
+ $FirstArg = if ($ProfileOrFlag -ne "default") { $ProfileOrFlag } elseif ($RemainingArgs.Count -gt 0) { $RemainingArgs[0] } else { $null }
283
+ if ($FirstArg -eq "version" -or $FirstArg -eq "--version" -or $FirstArg -eq "-v") {
284
+ Write-Host "CCS (Claude Code Switch) version $CcsVersion"
285
+
286
+ # Show install location
287
+ $InstallLocation = (Get-Command ccs -ErrorAction SilentlyContinue).Source
288
+ if ($InstallLocation) {
289
+ Write-Host "Installed at: $InstallLocation"
290
+ }
291
+
292
+ Write-Host "https://github.com/kaitranntt/ccs"
293
+ exit 0
294
+ }
295
+
296
+ # Special case: help command (check BEFORE profile detection)
297
+ if ($FirstArg -eq "--help" -or $FirstArg -eq "-h" -or $FirstArg -eq "help") {
298
+ $ClaudeCli = Find-ClaudeCli
299
+
300
+ try {
301
+ if ($RemainingArgs) {
302
+ & $ClaudeCli --help @RemainingArgs
303
+ } else {
304
+ & $ClaudeCli --help
305
+ }
306
+ exit $LASTEXITCODE
307
+ } catch {
308
+ Show-ClaudeNotFoundError
309
+ exit 1
310
+ }
311
+ }
312
+
313
+ # Special case: install command (check BEFORE profile detection)
314
+ if ($FirstArg -eq "--install") {
315
+ Install-CommandsAndSkills
316
+ exit $LASTEXITCODE
317
+ }
318
+
319
+ # Special case: uninstall command (check BEFORE profile detection)
320
+ if ($FirstArg -eq "--uninstall") {
321
+ Uninstall-CommandsAndSkills
322
+ exit $LASTEXITCODE
323
+ }
324
+
325
+ # Smart profile detection: if first arg starts with '-', it's a flag not a profile
326
+ if ($ProfileOrFlag -match '^-') {
327
+ # First arg is a flag → use default profile, keep all args
328
+ $Profile = "default"
329
+ # Prepend $ProfileOrFlag to $RemainingArgs (it's actually a flag, not a profile)
330
+ if ($RemainingArgs) {
331
+ $RemainingArgs = @($ProfileOrFlag) + $RemainingArgs
332
+ } else {
333
+ $RemainingArgs = @($ProfileOrFlag)
334
+ }
335
+ } else {
336
+ # First arg is a profile name
337
+ $Profile = $ProfileOrFlag
338
+ # $RemainingArgs already contains correct args (PowerShell handles this)
339
+ }
340
+
341
+ # Special case: "default" profile just runs claude directly (no profile switching)
342
+ if ($Profile -eq "default") {
343
+ try {
344
+ if ($RemainingArgs) {
345
+ & claude @RemainingArgs
346
+ } else {
347
+ & claude
348
+ }
349
+ exit $LASTEXITCODE
350
+ } catch {
351
+ Write-Host "Error: Failed to execute claude" -ForegroundColor Red
352
+ Write-Host $_.Exception.Message
353
+ exit 1
354
+ }
355
+ }
356
+
357
+ # Config file location (supports environment variable override)
358
+ $ConfigFile = if ($env:CCS_CONFIG) {
359
+ $env:CCS_CONFIG
360
+ } else {
361
+ "$env:USERPROFILE\.ccs\config.json"
362
+ }
363
+
364
+ # Check config exists
365
+ if (-not (Test-Path $ConfigFile)) {
366
+ Write-ErrorMsg @"
367
+ Config file not found: $ConfigFile
368
+
369
+ Solutions:
370
+ 1. Reinstall CCS:
371
+ irm ccs.kaitran.ca/install | iex
372
+
373
+ 2. Or create config manually:
374
+ New-Item -ItemType Directory -Force -Path '$env:USERPROFILE\.ccs'
375
+ Set-Content -Path '$env:USERPROFILE\.ccs\config.json' -Value '{
376
+ "profiles": {
377
+ "glm": "~/.ccs/glm.settings.json",
378
+ "default": "~/.claude/settings.json"
379
+ }
380
+ }'
381
+ "@
382
+ exit 1
383
+ }
384
+
385
+ # Validate profile name (alphanumeric, dash, underscore only)
386
+ if ($Profile -notmatch '^[a-zA-Z0-9_-]+$') {
387
+ Write-ErrorMsg @"
388
+ Invalid profile name: $Profile
389
+
390
+ Use only alphanumeric characters, dash, or underscore.
391
+ "@
392
+ exit 1
393
+ }
394
+
395
+ # Read and parse JSON config
396
+ try {
397
+ $ConfigContent = Get-Content $ConfigFile -Raw -ErrorAction Stop
398
+ $Config = $ConfigContent | ConvertFrom-Json -ErrorAction Stop
399
+ } catch {
400
+ Write-ErrorMsg @"
401
+ Invalid JSON in $ConfigFile
402
+
403
+ Fix the JSON syntax or reinstall:
404
+ irm ccs.kaitran.ca/install | iex
405
+ "@
406
+ exit 1
407
+ }
408
+
409
+ # Validate config has profiles object
410
+ if (-not $Config.profiles) {
411
+ Write-ErrorMsg @"
412
+ Config must have 'profiles' object
413
+
414
+ See .ccs.example.json for correct format
415
+ Or reinstall:
416
+ irm ccs.kaitran.ca/install | iex
417
+ "@
418
+ exit 1
419
+ }
420
+
421
+ # Get settings path for profile
422
+ $SettingsPath = $Config.profiles.$Profile
423
+
424
+ if (-not $SettingsPath) {
425
+ $AvailableProfiles = ($Config.profiles.PSObject.Properties.Name | ForEach-Object { " - $_" }) -join "`n"
426
+ Write-ErrorMsg @"
427
+ Profile '$Profile' not found in $ConfigFile
428
+
429
+ Available profiles:
430
+ $AvailableProfiles
431
+ "@
432
+ exit 1
433
+ }
434
+
435
+ # Path expansion and normalization
436
+ # 1. Handle Unix-style tilde expansion (~/path -> %USERPROFILE%\path)
437
+ if ($SettingsPath -match '^~[/\\]') {
438
+ $SettingsPath = $SettingsPath -replace '^~', $env:USERPROFILE
439
+ }
440
+
441
+ # 2. Expand Windows environment variables (%USERPROFILE%, etc.)
442
+ $SettingsPath = [System.Environment]::ExpandEnvironmentVariables($SettingsPath)
443
+
444
+ # 3. Convert forward slashes to backslashes (Unix path compatibility)
445
+ $SettingsPath = $SettingsPath -replace '/', '\'
446
+
447
+ # Validate settings file exists
448
+ if (-not (Test-Path $SettingsPath)) {
449
+ Write-ErrorMsg @"
450
+ Settings file not found: $SettingsPath
451
+
452
+ Solutions:
453
+ 1. Create the settings file for profile '$Profile'
454
+ 2. Update the path in $ConfigFile
455
+ 3. Or reinstall: irm ccs.kaitran.ca/install | iex
456
+ "@
457
+ exit 1
458
+ }
459
+
460
+ # Validate settings file is valid JSON (basic check)
461
+ try {
462
+ $SettingsContent = Get-Content $SettingsPath -Raw -ErrorAction Stop
463
+ $Settings = $SettingsContent | ConvertFrom-Json -ErrorAction Stop
464
+ } catch {
465
+ Write-ErrorMsg @"
466
+ Invalid JSON in $SettingsPath
467
+
468
+ Details: $_
469
+
470
+ Solutions:
471
+ 1. Validate JSON at https://jsonlint.com
472
+ 2. Or reset to template:
473
+ Set-Content -Path '$SettingsPath' -Value '{`"env`":{}}`'
474
+ 3. Or reinstall: irm ccs.kaitran.ca/install | iex
475
+ "@
476
+ exit 1
477
+ }
478
+
479
+ # Detect Claude CLI executable
480
+ $ClaudeCli = Find-ClaudeCli
481
+
482
+ # Execute Claude with the profile settings
483
+ try {
484
+ if ($RemainingArgs) {
485
+ & $ClaudeCli --settings $SettingsPath @RemainingArgs
486
+ } else {
487
+ & $ClaudeCli --settings $SettingsPath
488
+ }
489
+ exit $LASTEXITCODE
490
+ } catch {
491
+ Show-ClaudeNotFoundError
492
+ exit 1
493
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@kaitranntt/ccs",
3
+ "version": "2.4.0",
4
+ "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
5
+ "keywords": [
6
+ "cli",
7
+ "claude",
8
+ "glm",
9
+ "ai",
10
+ "profile",
11
+ "switch"
12
+ ],
13
+ "homepage": "https://github.com/kaitranntt/ccs",
14
+ "bugs": {
15
+ "url": "https://github.com/kaitranntt/ccs/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/kaitranntt/ccs.git"
20
+ },
21
+ "license": "MIT",
22
+ "author": {
23
+ "name": "Tam Nhu Tran (Kai)",
24
+ "email": "kaitranntt@users.noreply.github.com"
25
+ },
26
+ "bin": {
27
+ "ccs": "bin/ccs.js"
28
+ },
29
+ "files": [
30
+ "bin/",
31
+ "lib/",
32
+ "config/",
33
+ "VERSION",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "engines": {
38
+ "node": ">=14.0.0"
39
+ },
40
+ "os": [
41
+ "darwin",
42
+ "linux",
43
+ "win32"
44
+ ],
45
+ "preferGlobal": true,
46
+ "scripts": {
47
+ "test": "bash tests/edge-cases.sh",
48
+ "prepublishOnly": "node scripts/sync-version.js",
49
+ "prepack": "node scripts/sync-version.js",
50
+ "prepare": "node scripts/check-executables.js"
51
+ }
52
+ }