@kaitranntt/ccs 2.5.1 → 3.0.1

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 CHANGED
@@ -11,6 +11,13 @@ param(
11
11
 
12
12
  $ErrorActionPreference = "Stop"
13
13
 
14
+ # Version (updated by scripts/bump-version.sh)
15
+ $CcsVersion = "3.0.1"
16
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
17
+ $ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
18
+ $ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json"
19
+ $InstancesDir = "$env:USERPROFILE\.ccs\instances"
20
+
14
21
  # --- Color/Format Functions ---
15
22
  function Write-ErrorMsg {
16
23
  param([string]$Message)
@@ -89,62 +96,59 @@ function Show-Help {
89
96
  Write-Host ""
90
97
  Write-ColorLine "Usage:" "Cyan"
91
98
  Write-ColorLine " ccs [profile] [claude-args...]" "Yellow"
99
+ Write-ColorLine " ccs auth <command> [options]" "Yellow"
92
100
  Write-ColorLine " ccs [flags]" "Yellow"
93
101
  Write-Host ""
94
102
  Write-ColorLine "Description:" "Cyan"
95
- Write-Host " Switch between Claude models instantly. Stop hitting rate limits."
96
- Write-Host " Maps profile names to Claude settings files via ~/.ccs/config.json"
97
- Write-Host ""
98
- Write-ColorLine "Profile Switching:" "Cyan"
99
- Write-ColorLine " ccs Use default profile" "Yellow"
100
- Write-ColorLine " ccs glm Switch to GLM profile" "Yellow"
101
- Write-ColorLine " ccs kimi Switch to Kimi profile" "Yellow"
102
- Write-ColorLine " ccs glm 'debug this code' Switch to GLM and run command" "Yellow"
103
- Write-ColorLine " ccs kimi 'write tests' Switch to Kimi and run command" "Yellow"
104
- Write-ColorLine " ccs glm --verbose Switch to GLM with Claude flags" "Yellow"
105
- Write-ColorLine " ccs kimi --verbose Switch to Kimi with Claude flags" "Yellow"
103
+ Write-Host " Switch between multiple Claude accounts (work, personal, team) and"
104
+ Write-Host " alternative models (GLM, Kimi) instantly. Concurrent sessions with"
105
+ Write-Host " auto-recovery. Zero downtime."
106
+ Write-Host ""
107
+ Write-ColorLine "Model Switching:" "Cyan"
108
+ Write-ColorLine " ccs Use default Claude account" "Yellow"
109
+ Write-ColorLine " ccs glm Switch to GLM 4.6 model" "Yellow"
110
+ Write-ColorLine " ccs kimi Switch to Kimi for Coding" "Yellow"
111
+ Write-ColorLine " ccs glm 'debug this code' Use GLM and run command" "Yellow"
112
+ Write-Host ""
113
+ Write-ColorLine "Account Management:" "Cyan"
114
+ Write-ColorLine " ccs auth create <profile> Create new account profile" "Yellow"
115
+ Write-ColorLine " ccs auth list List all profiles" "Yellow"
116
+ Write-ColorLine " ccs auth show <profile> Show profile details" "Yellow"
117
+ Write-ColorLine " ccs auth remove <profile> Remove profile (requires --force)" "Yellow"
118
+ Write-ColorLine " ccs auth default <profile> Set default profile" "Yellow"
119
+ Write-ColorLine " ccs work Switch to work account" "Yellow"
120
+ Write-ColorLine " ccs personal Switch to personal account" "Yellow"
121
+ Write-ColorLine " ccs work 'review code' Run command with work account" "Yellow"
122
+ Write-Host ""
123
+ Write-ColorLine "Diagnostics:" "Cyan"
124
+ Write-ColorLine " ccs doctor Run health check and diagnostics" "Yellow"
106
125
  Write-Host ""
107
126
  Write-ColorLine "Flags:" "Cyan"
108
127
  Write-ColorLine " -h, --help Show this help message" "Yellow"
109
128
  Write-ColorLine " -v, --version Show version and installation info" "Yellow"
110
- Write-Host ""
129
+ Write-Host ""
111
130
  Write-ColorLine "Configuration:" "Cyan"
112
- Write-Host " Config File: ~/.ccs/config.json"
113
- Write-Host " Settings: ~/.ccs/*.settings.json"
114
- Write-Host " Environment: CCS_CONFIG (override config path)"
131
+ Write-Host " Config: ~/.ccs/config.json"
132
+ Write-Host " Profiles: ~/.ccs/profiles.json"
133
+ Write-Host " Settings: ~/.ccs/*.settings.json"
115
134
  Write-Host ""
116
135
  Write-ColorLine "Examples:" "Cyan"
117
- Write-Host " # Use default Claude subscription"
118
- Write-ColorLine " ccs 'Review this architecture'" "Yellow"
136
+ Write-Host " # Create work account profile"
137
+ Write-ColorLine " ccs auth create work" "Yellow"
138
+ Write-Host ""
139
+ Write-Host " # Use work account"
140
+ Write-ColorLine " ccs work 'Review architecture'" "Yellow"
119
141
  Write-Host ""
120
142
  Write-Host " # Switch to GLM for cost-effective tasks"
121
143
  Write-ColorLine " ccs glm 'Write unit tests'" "Yellow"
122
144
  Write-Host ""
123
- Write-Host " # Switch to Kimi for alternative option"
124
- Write-ColorLine " ccs kimi 'Write integration tests'" "Yellow"
125
- Write-Host ""
126
- Write-Host " # Use with verbose output"
127
- Write-ColorLine " ccs glm --verbose 'Debug error'" "Yellow"
128
- Write-ColorLine " ccs kimi --verbose 'Review code'" "Yellow"
129
- Write-Host ""
130
- Write-ColorLine "Uninstall:" "Cyan"
131
- Write-Host " macOS/Linux: curl -fsSL ccs.kaitran.ca/uninstall | bash"
132
- Write-Host " Windows: irm ccs.kaitran.ca/uninstall | iex"
133
- Write-Host " npm: npm uninstall -g @kaitranntt/ccs"
134
- Write-Host ""
135
145
  Write-ColorLine "Documentation:" "Cyan"
136
146
  Write-Host " GitHub: https://github.com/kaitranntt/ccs"
137
147
  Write-Host " Docs: https://github.com/kaitranntt/ccs/blob/main/README.md"
138
- Write-Host " Issues: https://github.com/kaitranntt/ccs/issues"
139
148
  Write-Host ""
140
149
  Write-ColorLine "License: MIT" "Cyan"
141
150
  }
142
151
 
143
- # Version (updated by scripts/bump-version.sh)
144
- $CcsVersion = "2.5.1"
145
- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
146
- $ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
147
-
148
152
  function Show-Version {
149
153
  $UseColors = $env:FORCE_COLOR -or ([Console]::IsOutputRedirected -eq $false -and -not $env:NO_COLOR)
150
154
 
@@ -205,8 +209,677 @@ function Show-Version {
205
209
  }
206
210
  }
207
211
 
212
+ # --- Auto-Recovery Functions ---
213
+
214
+ function Ensure-CcsDirectory {
215
+ if (Test-Path "$env:USERPROFILE\.ccs") { return $true }
216
+
217
+ try {
218
+ New-Item -ItemType Directory -Path "$env:USERPROFILE\.ccs" -Force | Out-Null
219
+ Write-Host "[i] Auto-recovery: Created ~/.ccs/ directory"
220
+ return $true
221
+ } catch {
222
+ Write-ErrorMsg "Cannot create ~/.ccs/ directory. Check permissions."
223
+ return $false
224
+ }
225
+ }
226
+
227
+ function Ensure-ConfigJson {
228
+ $ConfigFile = "$env:USERPROFILE\.ccs\config.json"
229
+
230
+ # Check if exists and valid
231
+ if (Test-Path $ConfigFile) {
232
+ try {
233
+ Get-Content $ConfigFile -Raw | ConvertFrom-Json | Out-Null
234
+ return $true
235
+ } catch {
236
+ # Corrupted - backup and recreate
237
+ $BackupFile = "$ConfigFile.backup.$(Get-Date -Format 'yyyyMMddHHmmss')"
238
+ Move-Item $ConfigFile $BackupFile -Force
239
+ Write-Host "[i] Auto-recovery: Backed up corrupted config.json"
240
+ }
241
+ }
242
+
243
+ # Create default config
244
+ $DefaultConfig = @{
245
+ profiles = @{
246
+ glm = "~/.ccs/glm.settings.json"
247
+ kimi = "~/.ccs/kimi.settings.json"
248
+ default = "~/.claude/settings.json"
249
+ }
250
+ }
251
+
252
+ $DefaultConfig | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile
253
+ Write-Host "[i] Auto-recovery: Created ~/.ccs/config.json"
254
+ return $true
255
+ }
256
+
257
+ function Ensure-ClaudeSettings {
258
+ $ClaudeDir = "$env:USERPROFILE\.claude"
259
+ $SettingsFile = "$ClaudeDir\settings.json"
260
+
261
+ # Create ~/.claude/ if missing
262
+ if (-not (Test-Path $ClaudeDir)) {
263
+ New-Item -ItemType Directory -Path $ClaudeDir -Force | Out-Null
264
+ Write-Host "[i] Auto-recovery: Created ~/.claude/ directory"
265
+ }
266
+
267
+ # Create settings.json if missing
268
+ if (-not (Test-Path $SettingsFile)) {
269
+ '{}' | Set-Content $SettingsFile
270
+ Write-Host "[i] Auto-recovery: Created ~/.claude/settings.json"
271
+ Write-Host "[i] Next step: Run 'claude /login' to authenticate"
272
+ return $true
273
+ }
274
+
275
+ return $false
276
+ }
277
+
278
+ function Invoke-AutoRecovery {
279
+ if (-not (Ensure-CcsDirectory)) { return $false }
280
+ if (-not (Ensure-ConfigJson)) { return $false }
281
+ Ensure-ClaudeSettings | Out-Null
282
+ return $true
283
+ }
284
+
285
+ # --- Profile Registry Functions (Phase 4) ---
286
+
287
+ function Initialize-ProfilesJson {
288
+ if (Test-Path $ProfilesJson) { return }
289
+
290
+ $InitData = @{
291
+ version = "2.0.0"
292
+ profiles = @{}
293
+ default = $null
294
+ }
295
+
296
+ $InitData | ConvertTo-Json -Depth 10 | Set-Content $ProfilesJson
297
+
298
+ # Set file permissions (user-only)
299
+ $Acl = Get-Acl $ProfilesJson
300
+ $Acl.SetAccessRuleProtection($true, $false)
301
+ $Acl.Access | ForEach-Object { $Acl.RemoveAccessRule($_) | Out-Null }
302
+ $CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
303
+ $Rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
304
+ $CurrentUser, "FullControl", "Allow"
305
+ )
306
+ $Acl.AddAccessRule($Rule)
307
+ Set-Acl -Path $ProfilesJson -AclObject $Acl
308
+ }
309
+
310
+ function Read-ProfilesJson {
311
+ Initialize-ProfilesJson
312
+ Get-Content $ProfilesJson -Raw | ConvertFrom-Json
313
+ }
314
+
315
+ function Write-ProfilesJson {
316
+ param([PSCustomObject]$Data)
317
+
318
+ $TempFile = "$ProfilesJson.tmp"
319
+
320
+ try {
321
+ $Data | ConvertTo-Json -Depth 10 | Set-Content $TempFile
322
+ Move-Item $TempFile $ProfilesJson -Force
323
+ } catch {
324
+ if (Test-Path $TempFile) { Remove-Item $TempFile -Force }
325
+ throw "Failed to write profiles registry: $_"
326
+ }
327
+ }
328
+
329
+ function Test-ProfileExists {
330
+ param([string]$ProfileName)
331
+
332
+ Initialize-ProfilesJson
333
+ $Data = Read-ProfilesJson
334
+ return $Data.profiles.PSObject.Properties.Name -contains $ProfileName
335
+ }
336
+
337
+ function Register-Profile {
338
+ param([string]$ProfileName)
339
+
340
+ if (Test-ProfileExists $ProfileName) {
341
+ throw "Profile already exists: $ProfileName"
342
+ }
343
+
344
+ $Data = Read-ProfilesJson
345
+ $Timestamp = (Get-Date).ToUniversalTime().ToString("o")
346
+
347
+ $Data.profiles | Add-Member -NotePropertyName $ProfileName -NotePropertyValue ([PSCustomObject]@{
348
+ type = "account"
349
+ created = $Timestamp
350
+ last_used = $null
351
+ })
352
+
353
+ # Set as default if none exists
354
+ if (-not $Data.default) {
355
+ $Data.default = $ProfileName
356
+ }
357
+
358
+ Write-ProfilesJson $Data
359
+ }
360
+
361
+ function Unregister-Profile {
362
+ param([string]$ProfileName)
363
+
364
+ if (-not (Test-ProfileExists $ProfileName)) { return } # Idempotent
365
+
366
+ $Data = Read-ProfilesJson
367
+
368
+ # Remove profile
369
+ $Data.profiles.PSObject.Properties.Remove($ProfileName)
370
+
371
+ # Update default if it was the deleted profile
372
+ if ($Data.default -eq $ProfileName) {
373
+ $Remaining = $Data.profiles.PSObject.Properties.Name
374
+ $Data.default = if ($Remaining.Count -gt 0) { $Remaining[0] } else { $null }
375
+ }
376
+
377
+ Write-ProfilesJson $Data
378
+ }
379
+
380
+ function Update-ProfileTimestamp {
381
+ param([string]$ProfileName)
382
+
383
+ if (-not (Test-ProfileExists $ProfileName)) { return } # Silent fail
384
+
385
+ $Data = Read-ProfilesJson
386
+ $Timestamp = (Get-Date).ToUniversalTime().ToString("o")
387
+
388
+ $Data.profiles.$ProfileName.last_used = $Timestamp
389
+
390
+ Write-ProfilesJson $Data
391
+ }
392
+
393
+ function Get-DefaultProfile {
394
+ Initialize-ProfilesJson
395
+ $Data = Read-ProfilesJson
396
+ return $Data.default
397
+ }
398
+
399
+ function Set-DefaultProfile {
400
+ param([string]$ProfileName)
401
+
402
+ if (-not (Test-ProfileExists $ProfileName)) {
403
+ throw "Profile not found: $ProfileName"
404
+ }
405
+
406
+ $Data = Read-ProfilesJson
407
+ $Data.default = $ProfileName
408
+
409
+ Write-ProfilesJson $Data
410
+ }
411
+
412
+ # --- Instance Management Functions (Phase 2) ---
413
+
414
+ function Get-SanitizedProfileName {
415
+ param([string]$Name)
416
+
417
+ # Replace unsafe chars, lowercase
418
+ return $Name.ToLower() -replace '[^a-z0-9_-]', '-'
419
+ }
420
+
421
+ function Set-InstancePermissions {
422
+ param([string]$Path)
423
+
424
+ # Get current user
425
+ $CurrentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
426
+
427
+ # Create new ACL with inheritance disabled
428
+ $Acl = Get-Acl $Path
429
+ $Acl.SetAccessRuleProtection($true, $false) # Disable inheritance
430
+ $Acl.Access | ForEach-Object { $Acl.RemoveAccessRule($_) | Out-Null }
431
+
432
+ # Grant full control to current user only
433
+ $Rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
434
+ $CurrentUser, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"
435
+ )
436
+ $Acl.AddAccessRule($Rule)
437
+
438
+ Set-Acl -Path $Path -AclObject $Acl
439
+ }
440
+
441
+ function Copy-GlobalConfigs {
442
+ param([string]$InstancePath)
443
+
444
+ $GlobalClaude = "$env:USERPROFILE\.claude"
445
+
446
+ # Copy settings.json
447
+ $GlobalSettings = Join-Path $GlobalClaude "settings.json"
448
+ if (Test-Path $GlobalSettings) {
449
+ Copy-Item $GlobalSettings -Destination (Join-Path $InstancePath "settings.json") -ErrorAction SilentlyContinue
450
+ }
451
+
452
+ # Copy commands/
453
+ $GlobalCommands = Join-Path $GlobalClaude "commands"
454
+ if (Test-Path $GlobalCommands) {
455
+ Copy-Item $GlobalCommands -Destination $InstancePath -Recurse -ErrorAction SilentlyContinue
456
+ }
457
+
458
+ # Copy skills/
459
+ $GlobalSkills = Join-Path $GlobalClaude "skills"
460
+ if (Test-Path $GlobalSkills) {
461
+ Copy-Item $GlobalSkills -Destination $InstancePath -Recurse -ErrorAction SilentlyContinue
462
+ }
463
+ }
464
+
465
+ function Initialize-Instance {
466
+ param([string]$InstancePath)
467
+
468
+ # Create base directory with user-only ACL
469
+ New-Item -ItemType Directory -Path $InstancePath -Force | Out-Null
470
+ Set-InstancePermissions $InstancePath
471
+
472
+ # Create subdirectories
473
+ $Subdirs = @('session-env', 'todos', 'logs', 'file-history',
474
+ 'shell-snapshots', 'debug', '.anthropic', 'commands', 'skills')
475
+
476
+ foreach ($Dir in $Subdirs) {
477
+ $DirPath = Join-Path $InstancePath $Dir
478
+ New-Item -ItemType Directory -Path $DirPath -Force | Out-Null
479
+ }
480
+
481
+ # Copy global configs
482
+ Copy-GlobalConfigs $InstancePath
483
+ }
484
+
485
+ function Validate-Instance {
486
+ param([string]$InstancePath)
487
+
488
+ $RequiredDirs = @('session-env', 'todos', 'logs', 'file-history',
489
+ 'shell-snapshots', 'debug', '.anthropic')
490
+
491
+ foreach ($Dir in $RequiredDirs) {
492
+ $DirPath = Join-Path $InstancePath $Dir
493
+ if (-not (Test-Path $DirPath)) {
494
+ New-Item -ItemType Directory -Path $DirPath -Force | Out-Null
495
+ }
496
+ }
497
+ }
498
+
499
+ function Ensure-Instance {
500
+ param([string]$ProfileName)
501
+
502
+ $SafeName = Get-SanitizedProfileName $ProfileName
503
+ $InstancePath = "$InstancesDir\$SafeName"
504
+
505
+ if (-not (Test-Path $InstancePath)) {
506
+ Initialize-Instance $InstancePath
507
+ }
508
+
509
+ Validate-Instance $InstancePath
510
+
511
+ return $InstancePath
512
+ }
513
+
514
+ # --- Profile Detection Logic (Phase 1) ---
515
+
516
+ function Get-AvailableProfiles {
517
+ $lines = @()
518
+
519
+ # Settings-based profiles
520
+ if (Test-Path $ConfigFile) {
521
+ try {
522
+ $Config = Get-Content $ConfigFile -Raw | ConvertFrom-Json
523
+ $SettingsProfiles = $Config.profiles.PSObject.Properties.Name
524
+
525
+ if ($SettingsProfiles.Count -gt 0) {
526
+ $lines += "Settings-based profiles (GLM, Kimi, etc.):"
527
+ foreach ($name in $SettingsProfiles) {
528
+ $lines += " - $name"
529
+ }
530
+ }
531
+ } catch {}
532
+ }
533
+
534
+ # Account-based profiles
535
+ if (Test-Path $ProfilesJson) {
536
+ try {
537
+ $Profiles = Read-ProfilesJson
538
+ $AccountProfiles = $Profiles.profiles.PSObject.Properties.Name
539
+
540
+ if ($AccountProfiles.Count -gt 0) {
541
+ $lines += "Account-based profiles:"
542
+ foreach ($name in $AccountProfiles) {
543
+ $IsDefault = if ($name -eq $Profiles.default) { " [DEFAULT]" } else { "" }
544
+ $lines += " - $name$IsDefault"
545
+ }
546
+ }
547
+ } catch {}
548
+ }
549
+
550
+ if ($lines.Count -eq 0) {
551
+ return " (no profiles configured)`n Run `"ccs auth create <profile>`" to create your first account profile."
552
+ } else {
553
+ return $lines -join "`n"
554
+ }
555
+ }
556
+
557
+ function Get-ProfileType {
558
+ param([string]$ProfileName)
559
+
560
+ # Special case: 'default' resolves to default profile
561
+ if ($ProfileName -eq "default") {
562
+ # Check account-based default first
563
+ if (Test-Path $ProfilesJson) {
564
+ $Profiles = Read-ProfilesJson
565
+ $DefaultAccount = $Profiles.default
566
+
567
+ if ($DefaultAccount -and (Test-ProfileExists $DefaultAccount)) {
568
+ return @{
569
+ Type = "account"
570
+ Name = $DefaultAccount
571
+ }
572
+ }
573
+ }
574
+
575
+ # Check settings-based default
576
+ if (Test-Path $ConfigFile) {
577
+ $Config = Get-Content $ConfigFile -Raw | ConvertFrom-Json
578
+ $DefaultSettings = $Config.profiles.default
579
+
580
+ if ($DefaultSettings) {
581
+ return @{
582
+ Type = "settings"
583
+ Path = $DefaultSettings
584
+ Name = "default"
585
+ }
586
+ }
587
+ }
588
+
589
+ # No default configured, use Claude's defaults
590
+ return @{
591
+ Type = "default"
592
+ Name = "default"
593
+ }
594
+ }
595
+
596
+ # Priority 1: Check settings-based profiles (backward compatibility)
597
+ if (Test-Path $ConfigFile) {
598
+ try {
599
+ $Config = Get-Content $ConfigFile -Raw | ConvertFrom-Json
600
+ $SettingsPath = $Config.profiles.$ProfileName
601
+
602
+ if ($SettingsPath) {
603
+ return @{
604
+ Type = "settings"
605
+ Path = $SettingsPath
606
+ Name = $ProfileName
607
+ }
608
+ }
609
+ } catch {}
610
+ }
611
+
612
+ # Priority 2: Check account-based profiles
613
+ if ((Test-Path $ProfilesJson) -and (Test-ProfileExists $ProfileName)) {
614
+ return @{
615
+ Type = "account"
616
+ Name = $ProfileName
617
+ }
618
+ }
619
+
620
+ # Not found
621
+ return @{
622
+ Type = "error"
623
+ }
624
+ }
625
+
626
+ # --- Auth Commands (Phase 3) ---
627
+
628
+ function Show-AuthHelp {
629
+ Write-Host ""
630
+ Write-Host "CCS Account Management" -ForegroundColor White
631
+ Write-Host ""
632
+ Write-Host "Usage:" -ForegroundColor Cyan
633
+ Write-Host " ccs auth <command> [options]" -ForegroundColor Yellow
634
+ Write-Host ""
635
+ Write-Host "Commands:" -ForegroundColor Cyan
636
+ Write-Host " create <profile> Create new profile and login" -ForegroundColor Yellow
637
+ Write-Host " list List all saved profiles" -ForegroundColor Yellow
638
+ Write-Host " show <profile> Show profile details" -ForegroundColor Yellow
639
+ Write-Host " remove <profile> Remove saved profile" -ForegroundColor Yellow
640
+ Write-Host " default <profile> Set default profile" -ForegroundColor Yellow
641
+ Write-Host ""
642
+ Write-Host "Examples:" -ForegroundColor Cyan
643
+ Write-Host " ccs auth create work" -ForegroundColor Yellow
644
+ Write-Host " ccs auth list" -ForegroundColor Yellow
645
+ Write-Host " ccs auth remove work --force" -ForegroundColor Yellow
646
+ Write-Host ""
647
+ }
648
+
649
+ function Invoke-AuthCreate {
650
+ param([string[]]$Args)
651
+
652
+ $ProfileName = ""
653
+ $Force = $false
654
+
655
+ foreach ($arg in $Args) {
656
+ if ($arg -eq "--force") {
657
+ $Force = $true
658
+ } elseif ($arg -match '^-') {
659
+ Write-ErrorMsg "Unknown option: $arg"
660
+ return 1
661
+ } else {
662
+ $ProfileName = $arg
663
+ }
664
+ }
665
+
666
+ if (-not $ProfileName) {
667
+ Write-ErrorMsg "Profile name is required`n`nUsage: ccs auth create <profile> [--force]"
668
+ return 1
669
+ }
670
+
671
+ if (-not $Force -and (Test-ProfileExists $ProfileName)) {
672
+ Write-ErrorMsg "Profile already exists: $ProfileName`nUse --force to overwrite"
673
+ return 1
674
+ }
675
+
676
+ # Create instance
677
+ Write-Host "[i] Creating profile: $ProfileName"
678
+ $InstancePath = Ensure-Instance $ProfileName
679
+ Write-Host "[i] Instance directory: $InstancePath"
680
+ Write-Host ""
681
+
682
+ # Register profile
683
+ Register-Profile $ProfileName
684
+
685
+ # Launch Claude for login
686
+ Write-Host "[i] Starting Claude in isolated instance..." -ForegroundColor Yellow
687
+ Write-Host "[i] You will be prompted to login with your account." -ForegroundColor Yellow
688
+ Write-Host ""
689
+
690
+ $env:CLAUDE_CONFIG_DIR = $InstancePath
691
+ $ClaudeCli = Find-ClaudeCli
692
+ & $ClaudeCli
693
+
694
+ if ($LASTEXITCODE -ne 0) {
695
+ Write-ErrorMsg "Login failed or cancelled`nTo retry: ccs auth create $ProfileName --force"
696
+ return 1
697
+ }
698
+
699
+ Write-Host ""
700
+ Write-Host "[OK] Profile created successfully" -ForegroundColor Green
701
+ Write-Host ""
702
+ Write-Host " Profile: $ProfileName"
703
+ Write-Host " Instance: $InstancePath"
704
+ Write-Host ""
705
+ Write-Host "Usage:"
706
+ Write-Host " ccs $ProfileName `"your prompt here`"" -ForegroundColor Yellow
707
+ Write-Host ""
708
+ }
709
+
710
+ function Invoke-AuthList {
711
+ param([string[]]$Args)
712
+
713
+ $Verbose = $Args -contains "--verbose"
714
+
715
+ if (-not (Test-Path $ProfilesJson)) {
716
+ Write-Host "No account profiles found" -ForegroundColor Yellow
717
+ Write-Host ""
718
+ Write-Host "To create your first profile:"
719
+ Write-Host " ccs auth create <profile>" -ForegroundColor Yellow
720
+ return
721
+ }
722
+
723
+ $Data = Read-ProfilesJson
724
+ $Profiles = $Data.profiles.PSObject.Properties.Name
725
+
726
+ if ($Profiles.Count -eq 0) {
727
+ Write-Host "No account profiles found" -ForegroundColor Yellow
728
+ return
729
+ }
730
+
731
+ Write-Host "Saved Account Profiles:" -ForegroundColor White
732
+ Write-Host ""
733
+
734
+ foreach ($profile in $Profiles) {
735
+ $IsDefault = $profile -eq $Data.default
736
+
737
+ if ($IsDefault) {
738
+ Write-Host "[*] " -ForegroundColor Green -NoNewline
739
+ Write-Host $profile -ForegroundColor Cyan -NoNewline
740
+ Write-Host " (default)" -ForegroundColor Green
741
+ } else {
742
+ Write-Host "[ ] " -NoNewline
743
+ Write-Host $profile -ForegroundColor Cyan
744
+ }
745
+
746
+ $Type = $Data.profiles.$profile.type
747
+ Write-Host " Type: $Type"
748
+
749
+ if ($Verbose) {
750
+ $Created = $Data.profiles.$profile.created
751
+ $LastUsed = $Data.profiles.$profile.last_used
752
+ if (-not $LastUsed) { $LastUsed = "Never" }
753
+ Write-Host " Created: $Created"
754
+ Write-Host " Last used: $LastUsed"
755
+ }
756
+
757
+ Write-Host ""
758
+ }
759
+ }
760
+
761
+ function Invoke-AuthShow {
762
+ param([string[]]$Args)
763
+
764
+ $ProfileName = $Args[0]
765
+
766
+ if (-not $ProfileName) {
767
+ Write-ErrorMsg "Profile name is required`nUsage: ccs auth show <profile>"
768
+ return 1
769
+ }
770
+
771
+ if (-not (Test-ProfileExists $ProfileName)) {
772
+ Write-ErrorMsg "Profile not found: $ProfileName"
773
+ return 1
774
+ }
775
+
776
+ $Data = Read-ProfilesJson
777
+ $IsDefault = $ProfileName -eq $Data.default
778
+
779
+ Write-Host "Profile: $ProfileName" -ForegroundColor White
780
+ Write-Host ""
781
+
782
+ $Type = $Data.profiles.$ProfileName.type
783
+ $Created = $Data.profiles.$ProfileName.created
784
+ $LastUsed = $Data.profiles.$ProfileName.last_used
785
+ if (-not $LastUsed) { $LastUsed = "Never" }
786
+ $InstancePath = "$InstancesDir\$(Get-SanitizedProfileName $ProfileName)"
787
+
788
+ Write-Host " Type: $Type"
789
+ Write-Host " Default: $(if ($IsDefault) { 'Yes' } else { 'No' })"
790
+ Write-Host " Instance: $InstancePath"
791
+ Write-Host " Created: $Created"
792
+ Write-Host " Last used: $LastUsed"
793
+ Write-Host ""
794
+ }
795
+
796
+ function Invoke-AuthRemove {
797
+ param([string[]]$Args)
798
+
799
+ $ProfileName = ""
800
+ $Force = $false
801
+
802
+ foreach ($arg in $Args) {
803
+ if ($arg -eq "--force") {
804
+ $Force = $true
805
+ } else {
806
+ $ProfileName = $arg
807
+ }
808
+ }
809
+
810
+ if (-not $ProfileName) {
811
+ Write-ErrorMsg "Profile name is required`nUsage: ccs auth remove <profile> --force"
812
+ return 1
813
+ }
814
+
815
+ if (-not (Test-ProfileExists $ProfileName)) {
816
+ Write-ErrorMsg "Profile not found: $ProfileName"
817
+ return 1
818
+ }
819
+
820
+ if (-not $Force) {
821
+ Write-ErrorMsg "Removal requires --force flag for safety`nRun: ccs auth remove $ProfileName --force"
822
+ return 1
823
+ }
824
+
825
+ # Delete instance directory
826
+ $InstancePath = "$InstancesDir\$(Get-SanitizedProfileName $ProfileName)"
827
+ if (Test-Path $InstancePath) {
828
+ Remove-Item $InstancePath -Recurse -Force
829
+ }
830
+
831
+ # Remove from registry
832
+ Unregister-Profile $ProfileName
833
+
834
+ Write-Host "[OK] Profile removed successfully" -ForegroundColor Green
835
+ Write-Host " Profile: $ProfileName"
836
+ Write-Host ""
837
+ }
838
+
839
+ function Invoke-AuthDefault {
840
+ param([string[]]$Args)
841
+
842
+ $ProfileName = $Args[0]
843
+
844
+ if (-not $ProfileName) {
845
+ Write-ErrorMsg "Profile name is required`nUsage: ccs auth default <profile>"
846
+ return 1
847
+ }
848
+
849
+ if (-not (Test-ProfileExists $ProfileName)) {
850
+ Write-ErrorMsg "Profile not found: $ProfileName"
851
+ return 1
852
+ }
853
+
854
+ Set-DefaultProfile $ProfileName
855
+
856
+ Write-Host "[OK] Default profile set" -ForegroundColor Green
857
+ Write-Host " Profile: $ProfileName"
858
+ Write-Host ""
859
+ Write-Host "Now you can use:"
860
+ Write-Host " ccs `"your prompt`" # Uses $ProfileName profile" -ForegroundColor Yellow
861
+ Write-Host ""
862
+ }
863
+
864
+ function Invoke-AuthCommands {
865
+ param([string[]]$Args)
866
+
867
+ $Subcommand = $Args[0]
868
+ $SubArgs = if ($Args.Count -gt 1) { $Args[1..($Args.Count-1)] } else { @() }
869
+
870
+ switch ($Subcommand) {
871
+ "create" { Invoke-AuthCreate $SubArgs }
872
+ "list" { Invoke-AuthList $SubArgs }
873
+ "show" { Invoke-AuthShow $SubArgs }
874
+ "remove" { Invoke-AuthRemove $SubArgs }
875
+ "default" { Invoke-AuthDefault $SubArgs }
876
+ default { Show-AuthHelp }
877
+ }
878
+ }
879
+
880
+ # --- Main Execution Logic ---
881
+
208
882
  # Special case: version command (check BEFORE profile detection)
209
- # Handle switch parameters and remaining arguments
210
883
  if ($Version) {
211
884
  Show-Version
212
885
  exit 0
@@ -230,30 +903,17 @@ if ($Help) {
230
903
  }
231
904
  }
232
905
 
233
- # Special case: install command (check BEFORE profile detection)
234
- if ($FirstArg -eq "--install") {
235
- Write-Host ""
236
- Write-Host "Feature not available" -ForegroundColor Yellow
237
- Write-Host ""
238
- Write-Host "The --install flag is currently under development."
239
- Write-Host ".claude/ integration testing is not complete."
240
- Write-Host ""
241
- Write-Host "For updates: https://github.com/kaitranntt/ccs/issues"
242
- Write-Host ""
243
- exit 0
906
+ # Special case: auth commands
907
+ if ($RemainingArgs.Count -gt 0 -and $RemainingArgs[0] -eq "auth") {
908
+ $AuthArgs = if ($RemainingArgs.Count -gt 1) { $RemainingArgs[1..($RemainingArgs.Count-1)] } else { @() }
909
+ Invoke-AuthCommands $AuthArgs
910
+ exit $LASTEXITCODE
244
911
  }
245
912
 
246
- # Special case: uninstall command (check BEFORE profile detection)
247
- if ($FirstArg -eq "--uninstall") {
248
- Write-Host ""
249
- Write-Host "Feature not available" -ForegroundColor Yellow
250
- Write-Host ""
251
- Write-Host "The --uninstall flag is currently under development."
252
- Write-Host ".claude/ integration testing is not complete."
253
- Write-Host ""
254
- Write-Host "For updates: https://github.com/kaitranntt/ccs/issues"
255
- Write-Host ""
256
- exit 0
913
+ # Run auto-recovery before main logic
914
+ if (-not (Invoke-AutoRecovery)) {
915
+ Write-ErrorMsg "Auto-recovery failed. Check permissions."
916
+ exit 1
257
917
  }
258
918
 
259
919
  # Smart profile detection: if first arg starts with '-', it's a flag not a profile
@@ -267,20 +927,6 @@ if ($RemainingArgs.Count -eq 0 -or $RemainingArgs[0] -match '^-') {
267
927
  $RemainingArgs = if ($RemainingArgs.Count -gt 1) { $RemainingArgs | Select-Object -Skip 1 } else { @() }
268
928
  }
269
929
 
270
- # Check config exists
271
- if (-not (Test-Path $ConfigFile)) {
272
- $ErrorMessage = "Config file not found: $ConfigFile" + "`n`n" +
273
- "Solutions:" + "`n" +
274
- " 1. Reinstall CCS:" + "`n" +
275
- " irm ccs.kaitran.ca/install | iex" + "`n`n" +
276
- " 2. Or create config manually:" + "`n" +
277
- " New-Item -ItemType Directory -Force -Path '$env:USERPROFILE\.ccs'" + "`n" +
278
- " Set-Content -Path '$env:USERPROFILE\.ccs\config.json' -Value '{`"profiles`":{`"glm`":`"~/.ccs/glm.settings.json`",`"kimi`":`"~/.ccs/kimi.settings.json`",`"default`":`"~/.claude/settings.json`"}}'"
279
-
280
- Write-ErrorMsg $ErrorMessage
281
- exit 1
282
- }
283
-
284
930
  # Validate profile name (alphanumeric, dash, underscore only)
285
931
  if ($Profile -notmatch '^[a-zA-Z0-9_-]+$') {
286
932
  $ErrorMessage = "Invalid profile name: $Profile" + "`n`n" +
@@ -290,66 +936,90 @@ if ($Profile -notmatch '^[a-zA-Z0-9_-]+$') {
290
936
  exit 1
291
937
  }
292
938
 
293
- # Read and parse JSON config, get profile path in one step
294
- try {
295
- $ConfigContent = Get-Content $ConfigFile -Raw -ErrorAction Stop
296
- $Config = $ConfigContent | ConvertFrom-Json -ErrorAction Stop
297
- $SettingsPath = $Config.profiles.$Profile
939
+ # Detect profile type
940
+ $ProfileInfo = Get-ProfileType $Profile
298
941
 
299
- if (-not $SettingsPath) {
300
- $AvailableProfiles = ($Config.profiles.PSObject.Properties.Name | ForEach-Object { " - $_" }) -join "`n"
301
- $ErrorMessage = "Profile '$Profile' not found in $ConfigFile" + "`n`n" +
302
- "Available profiles:" + "`n" +
303
- $AvailableProfiles
304
-
305
- Write-ErrorMsg $ErrorMessage
306
- exit 1
307
- }
308
- } catch {
309
- $ErrorMessage = "Invalid JSON in $ConfigFile" + "`n`n" +
310
- "Fix the JSON syntax or reinstall:" + "`n" +
311
- " irm ccs.kaitran.ca/install | iex"
942
+ if ($ProfileInfo.Type -eq "error") {
943
+ $ErrorMessage = "Profile '$Profile' not found" + "`n`n" +
944
+ "Available profiles:" + "`n" +
945
+ (Get-AvailableProfiles)
312
946
 
313
947
  Write-ErrorMsg $ErrorMessage
314
948
  exit 1
315
949
  }
316
950
 
317
- # Path expansion and normalization
318
- # 1. Handle Unix-style tilde expansion (~/path -> %USERPROFILE%\path)
319
- if ($SettingsPath -match '^~[/\\]') {
320
- $SettingsPath = $SettingsPath -replace '^~', $env:USERPROFILE
321
- }
951
+ # Detect Claude CLI executable
952
+ $ClaudeCli = Find-ClaudeCli
322
953
 
323
- # 2. Expand Windows environment variables (%USERPROFILE%, etc.)
324
- $SettingsPath = [System.Environment]::ExpandEnvironmentVariables($SettingsPath)
954
+ # Execute based on profile type (Phase 5)
955
+ switch ($ProfileInfo.Type) {
956
+ "account" {
957
+ # Account-based profile: use CLAUDE_CONFIG_DIR
958
+ $InstancePath = Ensure-Instance $ProfileInfo.Name
959
+ Update-ProfileTimestamp $ProfileInfo.Name # Update last_used
325
960
 
326
- # 3. Convert forward slashes to backslashes (Unix path compatibility)
327
- $SettingsPath = $SettingsPath -replace '/', '\'
961
+ # Execute Claude with isolated config
962
+ $env:CLAUDE_CONFIG_DIR = $InstancePath
328
963
 
329
- # Validate settings file exists
330
- if (-not (Test-Path $SettingsPath)) {
331
- $ErrorMessage = "Settings file not found: $SettingsPath" + "`n`n" +
332
- "Solutions:" + "`n" +
333
- " 1. Create the settings file for profile '$Profile'" + "`n" +
334
- " 2. Update the path in $ConfigFile" + "`n" +
335
- " 3. Or reinstall: irm ccs.kaitran.ca/install | iex"
964
+ try {
965
+ if ($RemainingArgs) {
966
+ & $ClaudeCli @RemainingArgs
967
+ } else {
968
+ & $ClaudeCli
969
+ }
970
+ exit $LASTEXITCODE
971
+ } catch {
972
+ Show-ClaudeNotFoundError
973
+ exit 1
974
+ }
975
+ }
336
976
 
337
- Write-ErrorMsg $ErrorMessage
338
- exit 1
339
- }
977
+ "settings" {
978
+ # Settings-based profile: use --settings flag
979
+ $SettingsPath = $ProfileInfo.Path
340
980
 
341
- # Detect Claude CLI executable
342
- $ClaudeCli = Find-ClaudeCli
981
+ # Path expansion and normalization
982
+ if ($SettingsPath -match '^~[/\\]') {
983
+ $SettingsPath = $SettingsPath -replace '^~', $env:USERPROFILE
984
+ }
985
+ $SettingsPath = [System.Environment]::ExpandEnvironmentVariables($SettingsPath)
986
+ $SettingsPath = $SettingsPath -replace '/', '\'
343
987
 
344
- # Execute Claude with the profile settings
345
- try {
346
- if ($RemainingArgs) {
347
- & $ClaudeCli --settings $SettingsPath @RemainingArgs
348
- } else {
349
- & $ClaudeCli --settings $SettingsPath
988
+ if (-not (Test-Path $SettingsPath)) {
989
+ Write-ErrorMsg "Settings file not found: $SettingsPath"
990
+ exit 1
991
+ }
992
+
993
+ try {
994
+ if ($RemainingArgs) {
995
+ & $ClaudeCli --settings $SettingsPath @RemainingArgs
996
+ } else {
997
+ & $ClaudeCli --settings $SettingsPath
998
+ }
999
+ exit $LASTEXITCODE
1000
+ } catch {
1001
+ Show-ClaudeNotFoundError
1002
+ exit 1
1003
+ }
350
1004
  }
351
- exit $LASTEXITCODE
352
- } catch {
353
- Show-ClaudeNotFoundError
354
- exit 1
355
- }
1005
+
1006
+ "default" {
1007
+ # Default: no special handling
1008
+ try {
1009
+ if ($RemainingArgs) {
1010
+ & $ClaudeCli @RemainingArgs
1011
+ } else {
1012
+ & $ClaudeCli
1013
+ }
1014
+ exit $LASTEXITCODE
1015
+ } catch {
1016
+ Show-ClaudeNotFoundError
1017
+ exit 1
1018
+ }
1019
+ }
1020
+
1021
+ default {
1022
+ Write-ErrorMsg "Unknown profile type: $($ProfileInfo.Type)"
1023
+ exit 1
1024
+ }
1025
+ }