@kaitranntt/ccs 3.0.0 → 3.0.2

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