@ghackk/multi-claude 1.0.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.
@@ -0,0 +1,1230 @@
1
+ $ACCOUNTS_DIR = "$HOME\claude-accounts"
2
+ $BACKUP_DIR = "$HOME\claude-backups"
3
+ $SHARED_DIR = "$HOME\claude-shared"
4
+ $SHARED_SETTINGS = "$SHARED_DIR\settings.json"
5
+ $SHARED_CLAUDE = "$SHARED_DIR\CLAUDE.md"
6
+ $SHARED_PLUGINS_DIR = "$SHARED_DIR\plugins"
7
+ $SHARED_MARKETPLACES_DIR = "$SHARED_PLUGINS_DIR\marketplaces"
8
+
9
+ # ─── DISPLAY ─────────────────────────────────────────────────────────────────
10
+
11
+ function Show-Header {
12
+ Clear-Host
13
+ Write-Host "======================================" -ForegroundColor Cyan
14
+ Write-Host " Claude Account Manager " -ForegroundColor Cyan
15
+ Write-Host "======================================" -ForegroundColor Cyan
16
+ Write-Host ""
17
+ }
18
+
19
+ # ─── ACCOUNT HELPERS ─────────────────────────────────────────────────────────
20
+
21
+ function Get-Accounts {
22
+ return Get-ChildItem "$ACCOUNTS_DIR\claude-*.bat" -Exclude "claude-menu.bat" -ErrorAction SilentlyContinue
23
+ }
24
+
25
+ function Show-Accounts {
26
+ $accounts = Get-Accounts
27
+ if ($accounts.Count -eq 0) {
28
+ Write-Host " No accounts found." -ForegroundColor Yellow
29
+ return $accounts
30
+ }
31
+ $i = 1
32
+ foreach ($f in $accounts) {
33
+ $name = $f.BaseName
34
+ $configDir = "$HOME\.$name"
35
+ $loggedIn = if (Test-Path $configDir) { "[logged in]" } else { "[not logged in]" }
36
+ $lastUsed = if (Test-Path $configDir) {
37
+ $d = (Get-Item $configDir).LastWriteTime
38
+ "last used: $($d.ToString('dd MMM yyyy hh:mm tt'))"
39
+ } else { "never used" }
40
+ Write-Host " $i. $name $loggedIn ($lastUsed)" -ForegroundColor White
41
+ $i++
42
+ }
43
+ return $accounts
44
+ }
45
+
46
+ function Pick-Accounts($prompt, $single = $false) {
47
+ $accounts = Get-Accounts
48
+ if ($accounts.Count -eq 0) { return $null }
49
+ Write-Host ""
50
+ if ($single) {
51
+ Write-Host " (enter number)" -ForegroundColor Gray
52
+ } else {
53
+ Write-Host " (enter number or comma separated e.g. 1,2,3)" -ForegroundColor Gray
54
+ }
55
+ $userInput = Read-Host $prompt
56
+ $selected = @()
57
+ $parts = $userInput -split "," | ForEach-Object { $_.Trim() }
58
+ foreach ($part in $parts) {
59
+ $index = [int]$part - 1
60
+ if ($index -lt 0 -or $index -ge $accounts.Count) {
61
+ Write-Host " Skipping invalid choice: $part" -ForegroundColor Yellow
62
+ } else {
63
+ $selected += $accounts[$index]
64
+ }
65
+ }
66
+ if ($selected.Count -eq 0) { return $null }
67
+ return $selected
68
+ }
69
+
70
+ # ─── DEEP MERGE (shared wins on conflict) ────────────────────────────────────
71
+
72
+ function Merge-JsonObjects($base, $override) {
73
+ if ($null -eq $base) { return $override }
74
+ if ($null -eq $override) { return $base }
75
+
76
+ $result = $base.PSObject.Copy()
77
+ foreach ($prop in $override.PSObject.Properties) {
78
+ $key = $prop.Name
79
+ $overrideVal = $prop.Value
80
+ if ($result.PSObject.Properties[$key]) {
81
+ $baseVal = $result.$key
82
+ if ($baseVal -is [PSCustomObject] -and $overrideVal -is [PSCustomObject]) {
83
+ $result.$key = Merge-JsonObjects $baseVal $overrideVal
84
+ } else {
85
+ $result.$key = $overrideVal
86
+ }
87
+ } else {
88
+ $result | Add-Member -MemberType NoteProperty -Name $key -Value $overrideVal
89
+ }
90
+ }
91
+ return $result
92
+ }
93
+
94
+ # ─── SHARED DIR SETUP ────────────────────────────────────────────────────────
95
+
96
+ function Ensure-SharedDir {
97
+ if (!(Test-Path $SHARED_DIR)) {
98
+ New-Item -ItemType Directory -Path $SHARED_DIR | Out-Null
99
+ }
100
+ # Create default shared settings.json if missing
101
+ if (!(Test-Path $SHARED_SETTINGS)) {
102
+ [PSCustomObject]@{
103
+ mcpServers = [PSCustomObject]@{}
104
+ env = [PSCustomObject]@{}
105
+ preferences = [PSCustomObject]@{}
106
+ enabledPlugins = [PSCustomObject]@{}
107
+ extraKnownMarketplaces = [PSCustomObject]@{}
108
+ } | ConvertTo-Json -Depth 10 | ForEach-Object { Write-JsonNoBOM $SHARED_SETTINGS $_ }
109
+ } else {
110
+ # Migrate existing shared settings to include new fields if missing
111
+ $s = Get-Content $SHARED_SETTINGS -Raw | ConvertFrom-Json
112
+ $changed = $false
113
+ foreach ($field in @('enabledPlugins','extraKnownMarketplaces')) {
114
+ if (-not $s.PSObject.Properties[$field]) {
115
+ $s | Add-Member -MemberType NoteProperty -Name $field -Value ([PSCustomObject]@{})
116
+ $changed = $true
117
+ }
118
+ }
119
+ if ($changed) { Write-JsonNoBOM $SHARED_SETTINGS ($s | ConvertTo-Json -Depth 10) }
120
+ }
121
+ # Create shared plugins/marketplaces dirs
122
+ if (!(Test-Path $SHARED_PLUGINS_DIR)) { New-Item -ItemType Directory -Path $SHARED_PLUGINS_DIR | Out-Null }
123
+ if (!(Test-Path $SHARED_MARKETPLACES_DIR)) { New-Item -ItemType Directory -Path $SHARED_MARKETPLACES_DIR | Out-Null }
124
+ # Create empty CLAUDE.md if missing
125
+ if (!(Test-Path $SHARED_CLAUDE)) {
126
+ @"
127
+ # Shared Claude Instructions (Skills)
128
+ # This file is automatically applied to ALL accounts.
129
+ # Add your custom behaviors, personas, or skill instructions below.
130
+
131
+ "@ | Set-Content $SHARED_CLAUDE -Encoding UTF8
132
+ }
133
+ }
134
+
135
+ # ─── SYNC SHARED INTO ONE ACCOUNT ───────────────────────────────────────────
136
+
137
+ function Merge-SharedIntoAccount($accountName) {
138
+ Ensure-SharedDir
139
+ $configDir = "$HOME\.$accountName"
140
+ if (!(Test-Path $configDir)) {
141
+ New-Item -ItemType Directory -Path $configDir | Out-Null
142
+ }
143
+
144
+ # --- Merge settings.json (MCP, env, preferences) ---
145
+ $settingsPath = "$configDir\settings.json"
146
+ $shared = Get-Content $SHARED_SETTINGS -Raw | ConvertFrom-Json
147
+
148
+ if (Test-Path $settingsPath) {
149
+ $accountSettings = Get-Content $settingsPath -Raw | ConvertFrom-Json
150
+ } else {
151
+ $accountSettings = [PSCustomObject]@{}
152
+ }
153
+
154
+ $merged = Merge-JsonObjects $accountSettings $shared
155
+ Write-JsonNoBOM $settingsPath ($merged | ConvertTo-Json -Depth 10)
156
+
157
+ # --- Sync marketplace indexes from shared dir ---
158
+ $accountPluginsDir = "$configDir\plugins"
159
+ $accountMarketplacesDir = "$accountPluginsDir\marketplaces"
160
+ if (!(Test-Path $accountPluginsDir)) { New-Item -ItemType Directory -Path $accountPluginsDir | Out-Null }
161
+ if (!(Test-Path $accountMarketplacesDir)) { New-Item -ItemType Directory -Path $accountMarketplacesDir | Out-Null }
162
+
163
+ # Copy each shared marketplace index into account
164
+ if (Test-Path $SHARED_MARKETPLACES_DIR) {
165
+ Get-ChildItem $SHARED_MARKETPLACES_DIR -Directory | ForEach-Object {
166
+ $mktName = $_.Name
167
+ $destMktDir = "$accountMarketplacesDir\$mktName"
168
+ if (!(Test-Path $destMktDir)) { New-Item -ItemType Directory -Path $destMktDir | Out-Null }
169
+ Copy-Item "$($_.FullName)\*" $destMktDir -Recurse -Force -ErrorAction SilentlyContinue
170
+ }
171
+ }
172
+
173
+ # Seed / update account known_marketplaces.json from shared extraKnownMarketplaces
174
+ $knownMktPath = "$accountPluginsDir\known_marketplaces.json"
175
+ $sharedMkts = if ($merged.PSObject.Properties['extraKnownMarketplaces']) { $merged.extraKnownMarketplaces } else { $null }
176
+ if ($sharedMkts) {
177
+ $knownMkts = if (Test-Path $knownMktPath) {
178
+ Get-Content $knownMktPath -Raw | ConvertFrom-Json
179
+ } else { [PSCustomObject]@{} }
180
+
181
+ foreach ($prop in $sharedMkts.PSObject.Properties) {
182
+ $mktName = $prop.Name
183
+ $installLoc = "$accountMarketplacesDir\$mktName"
184
+ if (-not $knownMkts.PSObject.Properties[$mktName]) {
185
+ $entry = [PSCustomObject]@{
186
+ source = $prop.Value.source
187
+ installLocation = $installLoc
188
+ lastUpdated = (Get-Date -Format "o")
189
+ }
190
+ $knownMkts | Add-Member -MemberType NoteProperty -Name $mktName -Value $entry
191
+ }
192
+ }
193
+ Write-JsonNoBOM $knownMktPath ($knownMkts | ConvertTo-Json -Depth 10)
194
+ }
195
+
196
+ # --- Copy CLAUDE.md (skills/instructions) ---
197
+ $sharedClaudeContent = Get-Content $SHARED_CLAUDE -Raw -ErrorAction SilentlyContinue
198
+ $accountClaudePath = "$configDir\CLAUDE.md"
199
+
200
+ if (![string]::IsNullOrWhiteSpace($sharedClaudeContent)) {
201
+ if (Test-Path $accountClaudePath) {
202
+ # Read existing, strip old shared block if present, then prepend fresh
203
+ $existing = Get-Content $accountClaudePath -Raw
204
+ $marker = "# ===== SHARED INSTRUCTIONS (auto-managed) ====="
205
+ $endMarker = "# ===== END SHARED INSTRUCTIONS ====="
206
+ if ($existing -match [regex]::Escape($marker)) {
207
+ # Remove old shared block
208
+ $existing = $existing -replace "(?s)$([regex]::Escape($marker)).*?$([regex]::Escape($endMarker))\r?\n?", ""
209
+ }
210
+ $newContent = "$marker`n$sharedClaudeContent`n$endMarker`n`n$existing"
211
+ $newContent | Set-Content $accountClaudePath -Encoding UTF8
212
+ } else {
213
+ # No existing file, just write shared content
214
+ $marker = "# ===== SHARED INSTRUCTIONS (auto-managed) ====="
215
+ $endMarker = "# ===== END SHARED INSTRUCTIONS ====="
216
+ "$marker`n$sharedClaudeContent`n$endMarker`n" | Set-Content $accountClaudePath -Encoding UTF8
217
+ }
218
+ }
219
+ }
220
+
221
+ # ─── SYNC ALL ACCOUNTS ───────────────────────────────────────────────────────
222
+
223
+ function Sync-AllAccounts {
224
+ $accounts = Get-Accounts
225
+ if ($accounts.Count -eq 0) {
226
+ Write-Host " No accounts to sync." -ForegroundColor Yellow
227
+ return
228
+ }
229
+ foreach ($acc in $accounts) {
230
+ Merge-SharedIntoAccount $acc.BaseName
231
+ Write-Host " Synced -> $($acc.BaseName)" -ForegroundColor Green
232
+ }
233
+ }
234
+
235
+ # ─── MANAGE SHARED SETTINGS MENU ─────────────────────────────────────────────
236
+
237
+ function Manage-SharedSettings {
238
+ while ($true) {
239
+ Show-Header
240
+ Ensure-SharedDir
241
+ Write-Host "SHARED SETTINGS" -ForegroundColor Magenta
242
+ Write-Host ""
243
+ Write-Host " These apply to ALL accounts automatically on launch." -ForegroundColor Gray
244
+ Write-Host ""
245
+ Write-Host " Shared folder: $SHARED_DIR" -ForegroundColor Gray
246
+ Write-Host ""
247
+
248
+ # Quick summary
249
+ $shared = Get-Content $SHARED_SETTINGS -Raw | ConvertFrom-Json
250
+ $mcpCount = ($shared.mcpServers.PSObject.Properties | Measure-Object).Count
251
+ $envCount = ($shared.env.PSObject.Properties | Measure-Object).Count
252
+ $claudeMd = Get-Content $SHARED_CLAUDE -Raw -ErrorAction SilentlyContinue
253
+ $skillLines = if ($claudeMd) { ($claudeMd -split "`n").Count } else { 0 }
254
+
255
+ Write-Host " Summary:" -ForegroundColor Cyan
256
+ Write-Host " MCP Servers : $mcpCount configured" -ForegroundColor White
257
+ Write-Host " Env Vars : $envCount configured" -ForegroundColor White
258
+ Write-Host " CLAUDE.md : $skillLines lines (skills/instructions)" -ForegroundColor White
259
+ Write-Host ""
260
+ Write-Host "======================================" -ForegroundColor Cyan
261
+ Write-Host " 1. Edit MCP + Settings (Notepad) " -ForegroundColor White
262
+ Write-Host " 2. Edit Skills/Instructions (Notepad)" -ForegroundColor White
263
+ Write-Host " 3. View current shared settings " -ForegroundColor White
264
+ Write-Host " 4. Sync shared -> ALL accounts NOW " -ForegroundColor White
265
+ Write-Host " 5. Show MCP server list " -ForegroundColor White
266
+ Write-Host " 6. Reset shared settings (clear) " -ForegroundColor White
267
+ Write-Host " 0. Back " -ForegroundColor White
268
+ Write-Host "======================================" -ForegroundColor Cyan
269
+ Write-Host ""
270
+
271
+ $choice = Read-Host " Pick an option"
272
+ switch ($choice) {
273
+ "1" {
274
+ Write-Host " Opening settings.json in Notepad..." -ForegroundColor Cyan
275
+ Write-Host " TIP: Add MCP servers, env vars, preferences here." -ForegroundColor Yellow
276
+ Write-Host " After saving, use option 4 to push to all accounts." -ForegroundColor Yellow
277
+ Start-Process notepad $SHARED_SETTINGS -Wait
278
+ pause
279
+ }
280
+ "2" {
281
+ Write-Host " Opening CLAUDE.md in Notepad..." -ForegroundColor Cyan
282
+ Write-Host " TIP: Write your skills, personas, or global instructions here." -ForegroundColor Yellow
283
+ Write-Host " After saving, use option 4 to push to all accounts." -ForegroundColor Yellow
284
+ Start-Process notepad $SHARED_CLAUDE -Wait
285
+ pause
286
+ }
287
+ "3" {
288
+ Show-Header
289
+ Write-Host "SHARED SETTINGS.JSON" -ForegroundColor Magenta
290
+ Write-Host ""
291
+ Get-Content $SHARED_SETTINGS | Write-Host
292
+ Write-Host ""
293
+ Write-Host "SHARED CLAUDE.MD (Skills)" -ForegroundColor Magenta
294
+ Write-Host ""
295
+ Get-Content $SHARED_CLAUDE | Write-Host
296
+ Write-Host ""
297
+ pause
298
+ }
299
+ "4" {
300
+ Write-Host ""
301
+ Write-Host " Syncing to all accounts..." -ForegroundColor Cyan
302
+ Sync-AllAccounts
303
+ Write-Host ""
304
+ Write-Host " All accounts updated!" -ForegroundColor Green
305
+ pause
306
+ }
307
+ "5" {
308
+ Show-Header
309
+ Write-Host "MCP SERVERS" -ForegroundColor Magenta
310
+ Write-Host ""
311
+ $s = Get-Content $SHARED_SETTINGS -Raw | ConvertFrom-Json
312
+ $props = $s.mcpServers.PSObject.Properties
313
+ if (($props | Measure-Object).Count -eq 0) {
314
+ Write-Host " No MCP servers configured yet." -ForegroundColor Yellow
315
+ Write-Host ""
316
+ Write-Host " To add one, use option 1 and add to the mcpServers block like:" -ForegroundColor Gray
317
+ Write-Host ' "mcpServers": { "myserver": { "command": "npx", "args": ["-y", "my-mcp-package"] } }' -ForegroundColor Gray
318
+ } else {
319
+ foreach ($p in $props) {
320
+ Write-Host " - $($p.Name)" -ForegroundColor Green
321
+ if ($p.Value.command) {
322
+ Write-Host " command: $($p.Value.command)" -ForegroundColor Gray
323
+ }
324
+ }
325
+ }
326
+ Write-Host ""
327
+ pause
328
+ }
329
+ "6" {
330
+ Write-Host ""
331
+ $confirm = Read-Host " Type YES to reset ALL shared settings and CLAUDE.md"
332
+ if ($confirm.Trim() -eq "YES") {
333
+ $resetObj = [PSCustomObject]@{
334
+ mcpServers = [PSCustomObject]@{}
335
+ env = [PSCustomObject]@{}
336
+ preferences = [PSCustomObject]@{}
337
+ enabledPlugins = [PSCustomObject]@{}
338
+ extraKnownMarketplaces = [PSCustomObject]@{}
339
+ }
340
+ Write-JsonNoBOM $SHARED_SETTINGS ($resetObj | ConvertTo-Json -Depth 10)
341
+ "# Shared Claude Instructions`n" | Set-Content $SHARED_CLAUDE -Encoding UTF8
342
+ Write-Host " Shared settings reset." -ForegroundColor Red
343
+ } else {
344
+ Write-Host " Cancelled." -ForegroundColor Gray
345
+ }
346
+ pause
347
+ }
348
+ "0" { return }
349
+ default { Write-Host " Invalid option." -ForegroundColor Red; Start-Sleep 1 }
350
+ }
351
+ }
352
+ }
353
+
354
+ # ─── CORE ACCOUNT FUNCTIONS ───────────────────────────────────────────────────
355
+
356
+ function Create-Account {
357
+ Show-Header
358
+ Write-Host "CREATE NEW ACCOUNT" -ForegroundColor Green
359
+ Write-Host ""
360
+ $name = Read-Host " Enter account name (e.g. alpha)"
361
+ $name = $name.Trim().ToLower()
362
+
363
+ if ([string]::IsNullOrEmpty($name)) {
364
+ Write-Host " Name cannot be empty!" -ForegroundColor Red
365
+ pause; return
366
+ }
367
+
368
+ $batFile = "$ACCOUNTS_DIR\claude-$name.bat"
369
+ if (Test-Path $batFile) {
370
+ Write-Host " Account 'claude-$name' already exists!" -ForegroundColor Yellow
371
+ pause; return
372
+ }
373
+
374
+ $content = "@echo off`r`nset CLAUDE_CONFIG_DIR=%USERPROFILE%\.claude-$name`r`nclaude %*"
375
+ [System.IO.File]::WriteAllText($batFile, $content, [System.Text.Encoding]::ASCII)
376
+
377
+ # Auto-apply shared settings to brand new account
378
+ Merge-SharedIntoAccount "claude-$name"
379
+ Write-Host " Shared settings applied automatically." -ForegroundColor Gray
380
+
381
+ Write-Host ""
382
+ Write-Host " Created claude-$name successfully!" -ForegroundColor Green
383
+ Write-Host ""
384
+ $login = Read-Host " Login now? (y/n)"
385
+ if ($login -eq "y") {
386
+ Write-Host " Opening claude-$name..." -ForegroundColor Cyan
387
+ & $batFile
388
+ }
389
+ }
390
+
391
+ function Launch-Account {
392
+ Show-Header
393
+ Write-Host "LAUNCH ACCOUNT" -ForegroundColor Green
394
+ Write-Host ""
395
+ Show-Accounts | Out-Null
396
+ $accs = Pick-Accounts " Pick account(s) to launch" $true
397
+ if ($null -eq $accs) { pause; return }
398
+ foreach ($acc in $accs) {
399
+ # Auto-sync shared settings before every launch
400
+ Write-Host " Applying shared settings to $($acc.BaseName)..." -ForegroundColor Gray
401
+ Merge-SharedIntoAccount $acc.BaseName
402
+ Write-Host " Launching $($acc.BaseName)..." -ForegroundColor Cyan
403
+ & $acc.FullName
404
+ }
405
+ }
406
+
407
+ function Rename-Account {
408
+ Show-Header
409
+ Write-Host "RENAME ACCOUNT" -ForegroundColor Green
410
+ Write-Host ""
411
+ Show-Accounts | Out-Null
412
+ $accs = Pick-Accounts " Pick account(s) to rename"
413
+ if ($null -eq $accs) { pause; return }
414
+
415
+ foreach ($acc in $accs) {
416
+ $oldName = $acc.BaseName
417
+ $newSuffix = Read-Host " Enter new name for $oldName"
418
+ $newSuffix = $newSuffix.Trim().ToLower()
419
+ $newName = "claude-$newSuffix"
420
+ $newBat = "$ACCOUNTS_DIR\$newName.bat"
421
+
422
+ if (Test-Path $newBat) {
423
+ Write-Host " Account '$newName' already exists! Skipping." -ForegroundColor Yellow
424
+ continue
425
+ }
426
+
427
+ Rename-Item $acc.FullName $newBat
428
+ (Get-Content $newBat) -replace $oldName, $newName | Set-Content $newBat -Encoding ascii
429
+
430
+ $oldConfig = "$HOME\.$oldName"
431
+ $newConfig = "$HOME\.$newName"
432
+ if (Test-Path $oldConfig) { Rename-Item $oldConfig $newConfig }
433
+
434
+ Write-Host " Renamed $oldName to $newName" -ForegroundColor Green
435
+ }
436
+ pause
437
+ }
438
+
439
+ function Delete-Account {
440
+ Show-Header
441
+ Write-Host "DELETE ACCOUNT" -ForegroundColor Red
442
+ Write-Host ""
443
+ Show-Accounts | Out-Null
444
+ $accs = Pick-Accounts " Pick account(s) to delete"
445
+ if ($null -eq $accs) { pause; return }
446
+
447
+ Write-Host ""
448
+ Write-Host " Accounts selected for deletion:" -ForegroundColor Yellow
449
+ foreach ($acc in $accs) { Write-Host " - $($acc.BaseName)" -ForegroundColor White }
450
+ Write-Host ""
451
+ $confirm = Read-Host " Type YES to confirm deleting all selected"
452
+
453
+ if ($confirm.Trim() -ne "YES") {
454
+ Write-Host " Cancelled." -ForegroundColor Gray
455
+ pause; return
456
+ }
457
+
458
+ foreach ($acc in $accs) {
459
+ $name = $acc.BaseName
460
+ Remove-Item $acc.FullName -Force
461
+ $configDir = "$HOME\.$name"
462
+ if (Test-Path $configDir) { Remove-Item $configDir -Recurse -Force }
463
+ Write-Host " Deleted $name" -ForegroundColor Red
464
+ }
465
+ pause
466
+ }
467
+
468
+ function Backup-Sessions {
469
+ Show-Header
470
+ Write-Host "BACKUP SESSIONS" -ForegroundColor Magenta
471
+ Write-Host ""
472
+
473
+ if (!(Test-Path $BACKUP_DIR)) { New-Item -ItemType Directory -Path $BACKUP_DIR | Out-Null }
474
+
475
+ $timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm"
476
+ $zipPath = "$BACKUP_DIR\claude-backup-$timestamp.zip"
477
+
478
+ # Include accounts dir, shared dir, and all account config dirs
479
+ $toZip = @($ACCOUNTS_DIR, $SHARED_DIR)
480
+ Get-Accounts | ForEach-Object {
481
+ $config = "$HOME\.$($_.BaseName)"
482
+ if (Test-Path $config) { $toZip += $config }
483
+ }
484
+
485
+ Compress-Archive -Path $toZip -DestinationPath $zipPath -Force
486
+
487
+ Write-Host " Backup saved to:" -ForegroundColor Green
488
+ Write-Host " $zipPath" -ForegroundColor White
489
+ pause
490
+ }
491
+
492
+ function Restore-Sessions {
493
+ Show-Header
494
+ Write-Host "RESTORE SESSIONS" -ForegroundColor Magenta
495
+ Write-Host ""
496
+
497
+ if (!(Test-Path $BACKUP_DIR)) {
498
+ Write-Host " No backups found." -ForegroundColor Yellow
499
+ pause; return
500
+ }
501
+
502
+ $backups = Get-ChildItem "$BACKUP_DIR\claude-backup-*.zip"
503
+ if ($backups.Count -eq 0) {
504
+ Write-Host " No backup files found." -ForegroundColor Yellow
505
+ pause; return
506
+ }
507
+
508
+ $i = 1
509
+ foreach ($b in $backups) {
510
+ Write-Host " $i. $($b.Name)" -ForegroundColor White
511
+ $i++
512
+ }
513
+
514
+ Write-Host ""
515
+ $choice = Read-Host " Pick backup number to restore"
516
+ $index = [int]$choice - 1
517
+
518
+ if ($index -lt 0 -or $index -ge $backups.Count) {
519
+ Write-Host " Invalid choice." -ForegroundColor Red
520
+ pause; return
521
+ }
522
+
523
+ $selected = $backups[$index]
524
+ Write-Host ""
525
+ Write-Host " This will overwrite existing accounts and sessions!" -ForegroundColor Yellow
526
+ $confirm = Read-Host " Continue? (y/n)"
527
+
528
+ if ($confirm -ne "y") { Write-Host " Cancelled." -ForegroundColor Gray; pause; return }
529
+
530
+ Expand-Archive -Path $selected.FullName -DestinationPath $HOME -Force
531
+ Write-Host ""
532
+ Write-Host " Restored from $($selected.Name)" -ForegroundColor Green
533
+ pause
534
+ }
535
+
536
+ # ─── EXPORT / IMPORT PROFILE (TOKEN) ────────────────────────────────────────
537
+
538
+ function Export-Profile {
539
+ Show-Header
540
+ Write-Host "EXPORT PROFILE (Token)" -ForegroundColor Magenta
541
+ Write-Host ""
542
+
543
+ $accounts = Show-Accounts
544
+ if ($accounts.Count -eq 0) { pause; return }
545
+
546
+ Write-Host ""
547
+ $choice = Read-Host " Pick account number to export"
548
+ $index = [int]$choice - 1
549
+
550
+ if ($index -lt 0 -or $index -ge $accounts.Count) {
551
+ Write-Host " Invalid choice." -ForegroundColor Red
552
+ pause; return
553
+ }
554
+
555
+ $selected = $accounts[$index]
556
+ $name = $selected.BaseName
557
+ $configDir = "$HOME\.$name"
558
+
559
+ if (!(Test-Path $configDir)) {
560
+ Write-Host " Config dir not found: $configDir" -ForegroundColor Red
561
+ pause; return
562
+ }
563
+
564
+ # Build a temp dir with only essential files
565
+ $tempDir = "$env:TEMP\claude-export-$name"
566
+ if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force }
567
+ New-Item -ItemType Directory -Path $tempDir | Out-Null
568
+ New-Item -ItemType Directory -Path "$tempDir\config" | Out-Null
569
+
570
+ # Copy credentials
571
+ $credFile = "$configDir\.credentials.json"
572
+ if (Test-Path $credFile) {
573
+ Copy-Item $credFile "$tempDir\config\.credentials.json"
574
+ } else {
575
+ Write-Host " No credentials found for $name - nothing to export." -ForegroundColor Yellow
576
+ Remove-Item $tempDir -Recurse -Force
577
+ pause; return
578
+ }
579
+
580
+ # Copy settings
581
+ $settingsFile = "$configDir\settings.json"
582
+ if (Test-Path $settingsFile) {
583
+ Copy-Item $settingsFile "$tempDir\config\settings.json"
584
+ }
585
+
586
+ # Copy CLAUDE.md if exists
587
+ $claudeMd = "$configDir\CLAUDE.md"
588
+ if (Test-Path $claudeMd) {
589
+ Copy-Item $claudeMd "$tempDir\config\CLAUDE.md"
590
+ }
591
+
592
+ # Copy launcher bat
593
+ Copy-Item $selected.FullName "$tempDir\launcher.bat"
594
+
595
+ # Save profile name
596
+ $name | Out-File "$tempDir\profile-name.txt" -Encoding UTF8 -NoNewline
597
+
598
+ # Zip and base64
599
+ $zipPath = "$env:TEMP\claude-export-$name.zip"
600
+ if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
601
+
602
+ try {
603
+ Compress-Archive -Path "$tempDir\*" -DestinationPath $zipPath -Force -ErrorAction Stop
604
+ $bytes = [System.IO.File]::ReadAllBytes($zipPath)
605
+ $b64 = [Convert]::ToBase64String($bytes)
606
+ $token = "CLAUDE_TOKEN:" + $b64 + ":END_TOKEN"
607
+ } catch {
608
+ Write-Host " Error creating token: $_" -ForegroundColor Red
609
+ Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
610
+ Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
611
+ pause; return
612
+ }
613
+
614
+ # Copy to clipboard
615
+ Set-Clipboard -Value $token
616
+
617
+ Write-Host ""
618
+ Write-Host " Profile: $name" -ForegroundColor Cyan
619
+ Write-Host " Token length: $($token.Length) characters" -ForegroundColor Gray
620
+ Write-Host " Token copied to clipboard!" -ForegroundColor Green
621
+ Write-Host ""
622
+ Write-Host " Press 'c' to copy again, or any key to continue..." -ForegroundColor Gray
623
+ $key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown").Character
624
+ if ($key -eq 'c' -or $key -eq 'C') {
625
+ Set-Clipboard -Value $token
626
+ Write-Host " Copied again!" -ForegroundColor Green
627
+ pause
628
+ }
629
+
630
+ # Cleanup temp
631
+ Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
632
+ Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
633
+ }
634
+
635
+ function Import-Profile {
636
+ Show-Header
637
+ Write-Host "IMPORT PROFILE (Token)" -ForegroundColor Magenta
638
+ Write-Host ""
639
+ Write-Host " Paste your profile token below:" -ForegroundColor Cyan
640
+ Write-Host ""
641
+ $token = Read-Host " Token"
642
+
643
+ if (-not $token.StartsWith("CLAUDE_TOKEN:") -or -not $token.EndsWith(":END_TOKEN")) {
644
+ Write-Host ""
645
+ Write-Host " Invalid token format." -ForegroundColor Red
646
+ Write-Host " Token must start with CLAUDE_TOKEN: and end with :END_TOKEN" -ForegroundColor Gray
647
+ pause; return
648
+ }
649
+
650
+ # Extract base64
651
+ $b64 = $token.Substring("CLAUDE_TOKEN:".Length)
652
+ $b64 = $b64.Substring(0, $b64.Length - ":END_TOKEN".Length)
653
+
654
+ try {
655
+ $bytes = [Convert]::FromBase64String($b64)
656
+ } catch {
657
+ Write-Host " Failed to decode token. It may be corrupted or truncated." -ForegroundColor Red
658
+ pause; return
659
+ }
660
+
661
+ # Write zip and extract
662
+ $zipPath = "$env:TEMP\claude-import.zip"
663
+ [System.IO.File]::WriteAllBytes($zipPath, $bytes)
664
+
665
+ $extractDir = "$env:TEMP\claude-import"
666
+ if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
667
+ Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force
668
+
669
+ # Read profile name
670
+ $nameFile = "$extractDir\profile-name.txt"
671
+ if (!(Test-Path $nameFile)) {
672
+ Write-Host " Invalid token: missing profile name." -ForegroundColor Red
673
+ Remove-Item $extractDir -Recurse -Force
674
+ Remove-Item $zipPath -Force
675
+ pause; return
676
+ }
677
+ $name = (Get-Content $nameFile -Raw).Trim()
678
+
679
+ Write-Host ""
680
+ Write-Host " Detected profile: $name" -ForegroundColor Cyan
681
+
682
+ # Check if profile already exists
683
+ $configDir = "$HOME\.$name"
684
+ if (Test-Path $configDir) {
685
+ Write-Host " Profile already exists locally!" -ForegroundColor Yellow
686
+ $confirm = Read-Host " Overwrite? (y/n)"
687
+ if ($confirm -ne "y") {
688
+ Write-Host " Cancelled." -ForegroundColor Gray
689
+ Remove-Item $extractDir -Recurse -Force
690
+ Remove-Item $zipPath -Force
691
+ pause; return
692
+ }
693
+ }
694
+
695
+ # Create config dir and copy files
696
+ if (!(Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir | Out-Null }
697
+
698
+ $importConfig = "$extractDir\config"
699
+ if (Test-Path "$importConfig\.credentials.json") {
700
+ Copy-Item "$importConfig\.credentials.json" "$configDir\.credentials.json" -Force
701
+ Write-Host " Credentials restored" -ForegroundColor Green
702
+ }
703
+ if (Test-Path "$importConfig\settings.json") {
704
+ Copy-Item "$importConfig\settings.json" "$configDir\settings.json" -Force
705
+ Write-Host " Settings restored" -ForegroundColor Green
706
+ }
707
+ if (Test-Path "$importConfig\CLAUDE.md") {
708
+ Copy-Item "$importConfig\CLAUDE.md" "$configDir\CLAUDE.md" -Force
709
+ Write-Host " CLAUDE.md restored" -ForegroundColor Green
710
+ }
711
+
712
+ # Copy launcher bat
713
+ $launcherSrc = "$extractDir\launcher.bat"
714
+ $launcherDest = "$ACCOUNTS_DIR\$name.bat"
715
+ if (Test-Path $launcherSrc) {
716
+ Copy-Item $launcherSrc $launcherDest -Force
717
+ Write-Host " Launcher created" -ForegroundColor Green
718
+ }
719
+
720
+ Write-Host ""
721
+ Write-Host " Profile '$name' imported successfully!" -ForegroundColor Green
722
+ Write-Host " Plugins will auto-install on first launch." -ForegroundColor Gray
723
+ Write-Host " Run $name to start." -ForegroundColor Cyan
724
+
725
+ # Cleanup
726
+ Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue
727
+ Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
728
+ pause
729
+ }
730
+
731
+ # ─── PLUGIN & MARKETPLACE HELPERS ────────────────────────────────────────────
732
+
733
+ function Write-JsonNoBOM($path, $content) {
734
+ $utf8NoBOM = New-Object System.Text.UTF8Encoding $false
735
+ [System.IO.File]::WriteAllText($path, $content, $utf8NoBOM)
736
+ }
737
+
738
+ function Read-SharedSettings {
739
+ Ensure-SharedDir
740
+ return Get-Content $SHARED_SETTINGS -Raw | ConvertFrom-Json
741
+ }
742
+
743
+ function Write-SharedSettings($obj) {
744
+ Write-JsonNoBOM $SHARED_SETTINGS ($obj | ConvertTo-Json -Depth 10)
745
+ }
746
+
747
+ function Read-AccountSettings($accountName) {
748
+ $path = "$HOME\.$accountName\settings.json"
749
+ if (Test-Path $path) { return Get-Content $path -Raw | ConvertFrom-Json }
750
+ return [PSCustomObject]@{}
751
+ }
752
+
753
+ function Write-AccountSettings($accountName, $obj) {
754
+ $dir = "$HOME\.$accountName"
755
+ if (!(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null }
756
+ Write-JsonNoBOM "$dir\settings.json" ($obj | ConvertTo-Json -Depth 10)
757
+ }
758
+
759
+ # Pull a marketplace index into the shared dir (copies from the first account that has it)
760
+ function Pull-MarketplaceToShared($mktName) {
761
+ $destDir = "$SHARED_MARKETPLACES_DIR\$mktName"
762
+ if (!(Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir | Out-Null }
763
+ foreach ($acc in (Get-Accounts)) {
764
+ $src = "$HOME\.$($acc.BaseName)\plugins\marketplaces\$mktName"
765
+ if (Test-Path $src) {
766
+ Copy-Item "$src\*" $destDir -Recurse -Force -ErrorAction SilentlyContinue
767
+ return $true
768
+ }
769
+ }
770
+ return $false
771
+ }
772
+
773
+ # List all plugins available in a marketplace (reads from shared dir or any account that has it)
774
+ function Get-MarketplacePlugins($mktName) {
775
+ $dirs = @(
776
+ "$SHARED_MARKETPLACES_DIR\$mktName",
777
+ (Get-Accounts | ForEach-Object { "$HOME\.$($_.BaseName)\plugins\marketplaces\$mktName" })
778
+ )
779
+ foreach ($d in $dirs) {
780
+ if (Test-Path "$d\plugins") {
781
+ $plugins = Get-ChildItem "$d\plugins" -Directory | Select-Object -ExpandProperty Name
782
+ $external = if (Test-Path "$d\external_plugins") {
783
+ Get-ChildItem "$d\external_plugins" -Directory | Select-Object -ExpandProperty Name
784
+ } else { @() }
785
+ return @{ plugins = $plugins; external = $external }
786
+ }
787
+ }
788
+ return $null
789
+ }
790
+
791
+ # Get all known marketplace names (shared + all accounts)
792
+ function Get-AllMarketplaceNames {
793
+ $names = @{}
794
+ $s = Read-SharedSettings
795
+ if ($s.PSObject.Properties['extraKnownMarketplaces']) {
796
+ $s.extraKnownMarketplaces.PSObject.Properties | ForEach-Object { $names[$_.Name] = "shared" }
797
+ }
798
+ foreach ($acc in (Get-Accounts)) {
799
+ $kp = "$HOME\.$($acc.BaseName)\plugins\known_marketplaces.json"
800
+ if (Test-Path $kp) {
801
+ (Get-Content $kp -Raw | ConvertFrom-Json).PSObject.Properties | ForEach-Object {
802
+ if (-not $names[$_.Name]) { $names[$_.Name] = $acc.BaseName }
803
+ }
804
+ }
805
+ }
806
+ return $names
807
+ }
808
+
809
+ # Get enabled plugins across shared + all accounts
810
+ function Get-AllEnabledPlugins {
811
+ $result = @{}
812
+ $s = Read-SharedSettings
813
+ if ($s.PSObject.Properties['enabledPlugins']) {
814
+ $s.enabledPlugins.PSObject.Properties | ForEach-Object {
815
+ $result[$_.Name] = @{ scope = "shared (all accounts)"; enabled = $_.Value }
816
+ }
817
+ }
818
+ foreach ($acc in (Get-Accounts)) {
819
+ $as = Read-AccountSettings $acc.BaseName
820
+ if ($as.PSObject.Properties['enabledPlugins']) {
821
+ $as.enabledPlugins.PSObject.Properties | ForEach-Object {
822
+ if (-not $result[$_.Name]) {
823
+ $result[$_.Name] = @{ scope = $acc.BaseName; enabled = $_.Value }
824
+ } elseif ($result[$_.Name].scope -eq "shared (all accounts)") {
825
+ # already shared, skip
826
+ } else {
827
+ $result[$_.Name].scope += ", $($acc.BaseName)"
828
+ }
829
+ }
830
+ }
831
+ }
832
+ return $result
833
+ }
834
+
835
+ # ─── MARKETPLACE MANAGEMENT ───────────────────────────────────────────────────
836
+
837
+ function Manage-Marketplaces {
838
+ while ($true) {
839
+ Show-Header
840
+ Write-Host "MARKETPLACE MANAGEMENT" -ForegroundColor Magenta
841
+ Write-Host ""
842
+ $allMkts = Get-AllMarketplaceNames
843
+ if ($allMkts.Count -eq 0) {
844
+ Write-Host " No marketplaces found." -ForegroundColor Yellow
845
+ } else {
846
+ Write-Host " Known Marketplaces:" -ForegroundColor Cyan
847
+ foreach ($kv in $allMkts.GetEnumerator()) {
848
+ $tag = if ($kv.Value -eq "shared") { "[ALL ACCOUNTS]" } else { "[$($kv.Value)]" }
849
+ Write-Host " - $($kv.Key) $tag" -ForegroundColor White
850
+ }
851
+ }
852
+ Write-Host ""
853
+ Write-Host "======================================" -ForegroundColor Cyan
854
+ Write-Host " 1. Add marketplace globally " -ForegroundColor White
855
+ Write-Host " 2. Add marketplace to one account " -ForegroundColor White
856
+ Write-Host " 3. Remove global marketplace " -ForegroundColor White
857
+ Write-Host " 4. Sync marketplace indexes now " -ForegroundColor White
858
+ Write-Host " 5. Pull indexes from accounts " -ForegroundColor White
859
+ Write-Host " 0. Back " -ForegroundColor White
860
+ Write-Host "======================================" -ForegroundColor Cyan
861
+ Write-Host ""
862
+
863
+ $choice = Read-Host " Pick an option"
864
+ switch ($choice) {
865
+ "1" {
866
+ Show-Header
867
+ Write-Host "ADD GLOBAL MARKETPLACE" -ForegroundColor Green
868
+ Write-Host ""
869
+ Write-Host " This marketplace will be available to ALL accounts." -ForegroundColor Gray
870
+ Write-Host ""
871
+ $mktName = (Read-Host " Marketplace name (e.g. my-plugins)").Trim()
872
+ if ([string]::IsNullOrEmpty($mktName)) { Write-Host " Cancelled." -ForegroundColor Gray; pause; break }
873
+ Write-Host " Source type: [1] GitHub repo [2] URL" -ForegroundColor Gray
874
+ $srcChoice = Read-Host " Pick"
875
+ $s = Read-SharedSettings
876
+ if (-not $s.PSObject.Properties['extraKnownMarketplaces']) {
877
+ $s | Add-Member -MemberType NoteProperty -Name 'extraKnownMarketplaces' -Value ([PSCustomObject]@{})
878
+ }
879
+ if ($srcChoice -eq "1") {
880
+ $repo = (Read-Host " GitHub repo (owner/repo)").Trim()
881
+ if ([string]::IsNullOrEmpty($repo)) { Write-Host " Cancelled." -ForegroundColor Gray; pause; break }
882
+ $entry = [PSCustomObject]@{ source = [PSCustomObject]@{ source = "github"; repo = $repo } }
883
+ $s.extraKnownMarketplaces | Add-Member -MemberType NoteProperty -Name $mktName -Value $entry -Force
884
+ } elseif ($srcChoice -eq "2") {
885
+ $url = (Read-Host " URL").Trim()
886
+ if ([string]::IsNullOrEmpty($url)) { Write-Host " Cancelled." -ForegroundColor Gray; pause; break }
887
+ $entry = [PSCustomObject]@{ source = [PSCustomObject]@{ source = "url"; url = $url } }
888
+ $s.extraKnownMarketplaces | Add-Member -MemberType NoteProperty -Name $mktName -Value $entry -Force
889
+ } else { Write-Host " Invalid." -ForegroundColor Red; pause; break }
890
+ Write-SharedSettings $s
891
+ Write-Host ""
892
+ Write-Host " Added '$mktName' globally. Run sync to push to all accounts." -ForegroundColor Green
893
+ pause
894
+ }
895
+ "2" {
896
+ Show-Header
897
+ Write-Host "ADD MARKETPLACE TO ONE ACCOUNT" -ForegroundColor Green
898
+ Write-Host ""
899
+ Show-Accounts | Out-Null
900
+ $accs = Pick-Accounts " Pick account" $true
901
+ if ($null -eq $accs) { pause; break }
902
+ $acc = $accs[0]
903
+ $mktName = (Read-Host " Marketplace name").Trim()
904
+ Write-Host " Source: [1] GitHub [2] URL" -ForegroundColor Gray
905
+ $srcChoice = Read-Host " Pick"
906
+ $as = Read-AccountSettings $acc.BaseName
907
+ if (-not $as.PSObject.Properties['extraKnownMarketplaces']) {
908
+ $as | Add-Member -MemberType NoteProperty -Name 'extraKnownMarketplaces' -Value ([PSCustomObject]@{})
909
+ }
910
+ if ($srcChoice -eq "1") {
911
+ $repo = (Read-Host " GitHub repo (owner/repo)").Trim()
912
+ $entry = [PSCustomObject]@{ source = [PSCustomObject]@{ source = "github"; repo = $repo } }
913
+ $as.extraKnownMarketplaces | Add-Member -MemberType NoteProperty -Name $mktName -Value $entry -Force
914
+ } elseif ($srcChoice -eq "2") {
915
+ $url = (Read-Host " URL").Trim()
916
+ $entry = [PSCustomObject]@{ source = [PSCustomObject]@{ source = "url"; url = $url } }
917
+ $as.extraKnownMarketplaces | Add-Member -MemberType NoteProperty -Name $mktName -Value $entry -Force
918
+ } else { Write-Host " Invalid." -ForegroundColor Red; pause; break }
919
+ Write-AccountSettings $acc.BaseName $as
920
+ Write-Host " Added '$mktName' to $($acc.BaseName)." -ForegroundColor Green
921
+ pause
922
+ }
923
+ "3" {
924
+ Show-Header
925
+ Write-Host "REMOVE GLOBAL MARKETPLACE" -ForegroundColor Red
926
+ Write-Host ""
927
+ $s = Read-SharedSettings
928
+ if (-not $s.PSObject.Properties['extraKnownMarketplaces'] -or
929
+ ($s.extraKnownMarketplaces.PSObject.Properties | Measure-Object).Count -eq 0) {
930
+ Write-Host " No global marketplaces configured." -ForegroundColor Yellow
931
+ pause; break
932
+ }
933
+ $mkts = $s.extraKnownMarketplaces.PSObject.Properties | ForEach-Object { $_.Name }
934
+ $i = 1; $mkts | ForEach-Object { Write-Host " $i. $_" -ForegroundColor White; $i++ }
935
+ Write-Host ""
936
+ $pick = [int](Read-Host " Pick number") - 1
937
+ if ($pick -lt 0 -or $pick -ge $mkts.Count) { Write-Host " Invalid." -ForegroundColor Red; pause; break }
938
+ $toRemove = $mkts[$pick]
939
+ $s.extraKnownMarketplaces.PSObject.Properties.Remove($toRemove)
940
+ Write-SharedSettings $s
941
+ Write-Host " Removed '$toRemove' from global marketplaces." -ForegroundColor Green
942
+ pause
943
+ }
944
+ "4" {
945
+ Write-Host ""
946
+ Write-Host " Syncing marketplace indexes to all accounts..." -ForegroundColor Cyan
947
+ Sync-AllAccounts
948
+ Write-Host " Done." -ForegroundColor Green
949
+ pause
950
+ }
951
+ "5" {
952
+ Show-Header
953
+ Write-Host "PULL MARKETPLACE INDEXES FROM ACCOUNTS" -ForegroundColor Cyan
954
+ Write-Host ""
955
+ Write-Host " This copies downloaded marketplace indexes into the shared dir" -ForegroundColor Gray
956
+ Write-Host " so they can be distributed to other accounts without re-downloading." -ForegroundColor Gray
957
+ Write-Host ""
958
+ $pulled = 0
959
+ (Get-AllMarketplaceNames).Keys | ForEach-Object {
960
+ $ok = Pull-MarketplaceToShared $_
961
+ if ($ok) { Write-Host " Pulled: $_" -ForegroundColor Green; $pulled++ }
962
+ else { Write-Host " Not found locally: $_" -ForegroundColor Yellow }
963
+ }
964
+ if ($pulled -eq 0) { Write-Host " Nothing pulled." -ForegroundColor Yellow }
965
+ pause
966
+ }
967
+ "0" { return }
968
+ default { Write-Host " Invalid option." -ForegroundColor Red; Start-Sleep 1 }
969
+ }
970
+ }
971
+ }
972
+
973
+ # ─── PLUGIN MANAGEMENT ────────────────────────────────────────────────────────
974
+
975
+ function Manage-Plugins {
976
+ while ($true) {
977
+ Show-Header
978
+ Write-Host "PLUGINS & MARKETPLACE" -ForegroundColor Magenta
979
+ Write-Host ""
980
+
981
+ # Show summary
982
+ $allPlugins = Get-AllEnabledPlugins
983
+ $sharedCount = ($allPlugins.Values | Where-Object { $_.scope -eq "shared (all accounts)" } | Measure-Object).Count
984
+ $specificCount = $allPlugins.Count - $sharedCount
985
+ Write-Host " Enabled Plugins:" -ForegroundColor Cyan
986
+ Write-Host " Universal (all accounts) : $sharedCount" -ForegroundColor White
987
+ Write-Host " Account-specific : $specificCount" -ForegroundColor White
988
+ Write-Host ""
989
+ if ($allPlugins.Count -gt 0) {
990
+ foreach ($kv in ($allPlugins.GetEnumerator() | Sort-Object Key)) {
991
+ $tag = if ($kv.Value.scope -eq "shared (all accounts)") { "[ALL]" } else { "[$($kv.Value.scope)]" }
992
+ $color = if ($kv.Value.scope -eq "shared (all accounts)") { "Green" } else { "White" }
993
+ Write-Host " $($kv.Key) $tag" -ForegroundColor $color
994
+ }
995
+ Write-Host ""
996
+ }
997
+
998
+ Write-Host "======================================" -ForegroundColor Cyan
999
+ Write-Host " 1. Enable plugin for ALL accounts " -ForegroundColor Green
1000
+ Write-Host " 2. Enable plugin for one account " -ForegroundColor White
1001
+ Write-Host " 3. Disable plugin (shared) " -ForegroundColor White
1002
+ Write-Host " 4. Disable plugin (one account) " -ForegroundColor White
1003
+ Write-Host " 5. Browse marketplace plugins " -ForegroundColor Cyan
1004
+ Write-Host " 6. Marketplace Management " -ForegroundColor Yellow
1005
+ Write-Host " 0. Back " -ForegroundColor White
1006
+ Write-Host "======================================" -ForegroundColor Cyan
1007
+ Write-Host ""
1008
+
1009
+ $choice = Read-Host " Pick an option"
1010
+ switch ($choice) {
1011
+ "1" {
1012
+ Show-Header
1013
+ Write-Host "ENABLE PLUGIN FOR ALL ACCOUNTS" -ForegroundColor Green
1014
+ Write-Host ""
1015
+ Write-Host " Format: plugin-name@marketplace-name" -ForegroundColor Gray
1016
+ Write-Host " Example: frontend-design@claude-plugins-official" -ForegroundColor Gray
1017
+ Write-Host ""
1018
+
1019
+ # Offer to browse if marketplaces available
1020
+ $allMkts = Get-AllMarketplaceNames
1021
+ if ($allMkts.Count -gt 0) {
1022
+ Write-Host " Or pick from marketplace:" -ForegroundColor Cyan
1023
+ $mktList = @($allMkts.Keys)
1024
+ $i = 1; $mktList | ForEach-Object { Write-Host " $i. $_" -ForegroundColor White; $i++ }
1025
+ Write-Host " 0. Enter manually" -ForegroundColor Gray
1026
+ Write-Host ""
1027
+ $mktPick = Read-Host " Marketplace (0 to enter manually)"
1028
+ if ($mktPick -ne "0" -and $mktPick -match '^\d+$') {
1029
+ $mktIdx = [int]$mktPick - 1
1030
+ if ($mktIdx -ge 0 -and $mktIdx -lt $mktList.Count) {
1031
+ $selectedMkt = $mktList[$mktIdx]
1032
+ $available = Get-MarketplacePlugins $selectedMkt
1033
+ if ($available) {
1034
+ Show-Header
1035
+ Write-Host "PLUGINS IN $selectedMkt" -ForegroundColor Cyan
1036
+ Write-Host ""
1037
+ $allAvail = @()
1038
+ if ($available.plugins.Count -gt 0) {
1039
+ Write-Host " Official:" -ForegroundColor Green
1040
+ $available.plugins | ForEach-Object { Write-Host " - $_" -ForegroundColor White; $allAvail += "$_@$selectedMkt" }
1041
+ }
1042
+ if ($available.external.Count -gt 0) {
1043
+ Write-Host " External:" -ForegroundColor Yellow
1044
+ $available.external | ForEach-Object { Write-Host " - $_" -ForegroundColor White; $allAvail += "$_@$selectedMkt" }
1045
+ }
1046
+ Write-Host ""
1047
+ $pName = (Read-Host " Enter plugin name (without @marketplace)").Trim()
1048
+ if ([string]::IsNullOrEmpty($pName)) { pause; break }
1049
+ $pluginKey = "$pName@$selectedMkt"
1050
+ } else {
1051
+ Write-Host " Marketplace index not downloaded yet. Enter manually." -ForegroundColor Yellow
1052
+ $pluginKey = (Read-Host " Plugin key (name@marketplace)").Trim()
1053
+ }
1054
+ } else { $pluginKey = (Read-Host " Plugin key (name@marketplace)").Trim() }
1055
+ } else { $pluginKey = (Read-Host " Plugin key (name@marketplace)").Trim() }
1056
+ } else {
1057
+ $pluginKey = (Read-Host " Plugin key (name@marketplace)").Trim()
1058
+ }
1059
+
1060
+ if ([string]::IsNullOrEmpty($pluginKey)) { Write-Host " Cancelled." -ForegroundColor Gray; pause; break }
1061
+ $s = Read-SharedSettings
1062
+ if (-not $s.PSObject.Properties['enabledPlugins']) {
1063
+ $s | Add-Member -MemberType NoteProperty -Name 'enabledPlugins' -Value ([PSCustomObject]@{})
1064
+ }
1065
+ $s.enabledPlugins | Add-Member -MemberType NoteProperty -Name $pluginKey -Value $true -Force
1066
+ Write-SharedSettings $s
1067
+ Write-Host ""
1068
+ Write-Host " '$pluginKey' enabled for ALL accounts." -ForegroundColor Green
1069
+ Write-Host " Syncing to all accounts now..." -ForegroundColor Cyan
1070
+ Sync-AllAccounts
1071
+ Write-Host " Done. Launch any account to activate." -ForegroundColor Green
1072
+ pause
1073
+ }
1074
+ "2" {
1075
+ Show-Header
1076
+ Write-Host "ENABLE PLUGIN FOR ONE ACCOUNT" -ForegroundColor Green
1077
+ Write-Host ""
1078
+ Show-Accounts | Out-Null
1079
+ $accs = Pick-Accounts " Pick account" $true
1080
+ if ($null -eq $accs) { pause; break }
1081
+ $acc = $accs[0]
1082
+ Write-Host ""
1083
+ $pluginKey = (Read-Host " Plugin key (name@marketplace)").Trim()
1084
+ if ([string]::IsNullOrEmpty($pluginKey)) { Write-Host " Cancelled." -ForegroundColor Gray; pause; break }
1085
+ $as = Read-AccountSettings $acc.BaseName
1086
+ if (-not $as.PSObject.Properties['enabledPlugins']) {
1087
+ $as | Add-Member -MemberType NoteProperty -Name 'enabledPlugins' -Value ([PSCustomObject]@{})
1088
+ }
1089
+ $as.enabledPlugins | Add-Member -MemberType NoteProperty -Name $pluginKey -Value $true -Force
1090
+ Write-AccountSettings $acc.BaseName $as
1091
+ Write-Host " '$pluginKey' enabled for $($acc.BaseName)." -ForegroundColor Green
1092
+ pause
1093
+ }
1094
+ "3" {
1095
+ Show-Header
1096
+ Write-Host "DISABLE PLUGIN (SHARED)" -ForegroundColor Red
1097
+ Write-Host ""
1098
+ $s = Read-SharedSettings
1099
+ if (-not $s.PSObject.Properties['enabledPlugins'] -or
1100
+ ($s.enabledPlugins.PSObject.Properties | Measure-Object).Count -eq 0) {
1101
+ Write-Host " No shared plugins configured." -ForegroundColor Yellow
1102
+ pause; break
1103
+ }
1104
+ $keys = @($s.enabledPlugins.PSObject.Properties | ForEach-Object { $_.Name })
1105
+ $i = 1; $keys | ForEach-Object { Write-Host " $i. $_" -ForegroundColor White; $i++ }
1106
+ Write-Host ""
1107
+ $pick = [int](Read-Host " Pick number to disable") - 1
1108
+ if ($pick -lt 0 -or $pick -ge $keys.Count) { Write-Host " Invalid." -ForegroundColor Red; pause; break }
1109
+ $toRemove = $keys[$pick]
1110
+ $s.enabledPlugins.PSObject.Properties.Remove($toRemove)
1111
+ Write-SharedSettings $s
1112
+ Write-Host " '$toRemove' removed from shared. Sync to propagate." -ForegroundColor Green
1113
+ $doSync = Read-Host " Sync to all accounts now? (y/n)"
1114
+ if ($doSync -eq "y") { Sync-AllAccounts; Write-Host " Synced." -ForegroundColor Green }
1115
+ pause
1116
+ }
1117
+ "4" {
1118
+ Show-Header
1119
+ Write-Host "DISABLE PLUGIN (ONE ACCOUNT)" -ForegroundColor Red
1120
+ Write-Host ""
1121
+ Show-Accounts | Out-Null
1122
+ $accs = Pick-Accounts " Pick account" $true
1123
+ if ($null -eq $accs) { pause; break }
1124
+ $acc = $accs[0]
1125
+ $as = Read-AccountSettings $acc.BaseName
1126
+ if (-not $as.PSObject.Properties['enabledPlugins'] -or
1127
+ ($as.enabledPlugins.PSObject.Properties | Measure-Object).Count -eq 0) {
1128
+ Write-Host " No plugins configured for $($acc.BaseName)." -ForegroundColor Yellow
1129
+ pause; break
1130
+ }
1131
+ $keys = @($as.enabledPlugins.PSObject.Properties | ForEach-Object { $_.Name })
1132
+ $i = 1; $keys | ForEach-Object { Write-Host " $i. $_" -ForegroundColor White; $i++ }
1133
+ Write-Host ""
1134
+ $pick = [int](Read-Host " Pick number") - 1
1135
+ if ($pick -lt 0 -or $pick -ge $keys.Count) { Write-Host " Invalid." -ForegroundColor Red; pause; break }
1136
+ $toRemove = $keys[$pick]
1137
+ $as.enabledPlugins.PSObject.Properties.Remove($toRemove)
1138
+ Write-AccountSettings $acc.BaseName $as
1139
+ Write-Host " '$toRemove' disabled for $($acc.BaseName)." -ForegroundColor Green
1140
+ pause
1141
+ }
1142
+ "5" {
1143
+ Show-Header
1144
+ Write-Host "BROWSE MARKETPLACE PLUGINS" -ForegroundColor Cyan
1145
+ Write-Host ""
1146
+ $allMkts = Get-AllMarketplaceNames
1147
+ if ($allMkts.Count -eq 0) {
1148
+ Write-Host " No marketplaces found. Add one via Marketplace Management." -ForegroundColor Yellow
1149
+ pause; break
1150
+ }
1151
+ $mktList = @($allMkts.Keys)
1152
+ $i = 1; $mktList | ForEach-Object { Write-Host " $i. $_" -ForegroundColor White; $i++ }
1153
+ Write-Host ""
1154
+ $pick = [int](Read-Host " Pick marketplace") - 1
1155
+ if ($pick -lt 0 -or $pick -ge $mktList.Count) { Write-Host " Invalid." -ForegroundColor Red; pause; break }
1156
+ $selectedMkt = $mktList[$pick]
1157
+ $available = Get-MarketplacePlugins $selectedMkt
1158
+ Show-Header
1159
+ Write-Host "PLUGINS IN $selectedMkt" -ForegroundColor Cyan
1160
+ Write-Host ""
1161
+ if ($null -eq $available) {
1162
+ Write-Host " Index not downloaded. Launch an account with this marketplace configured to download it," -ForegroundColor Yellow
1163
+ Write-Host " then use option 5 in Marketplace Management to pull indexes to shared." -ForegroundColor Yellow
1164
+ } else {
1165
+ if ($available.plugins.Count -gt 0) {
1166
+ Write-Host " Official plugins:" -ForegroundColor Green
1167
+ $available.plugins | ForEach-Object { Write-Host " - $_" -ForegroundColor White }
1168
+ }
1169
+ if ($available.external.Count -gt 0) {
1170
+ Write-Host " External/3rd-party:" -ForegroundColor Yellow
1171
+ $available.external | ForEach-Object { Write-Host " - $_" -ForegroundColor White }
1172
+ }
1173
+ }
1174
+ Write-Host ""
1175
+ pause
1176
+ }
1177
+ "6" { Manage-Marketplaces }
1178
+ "0" { return }
1179
+ default { Write-Host " Invalid option." -ForegroundColor Red; Start-Sleep 1 }
1180
+ }
1181
+ }
1182
+ }
1183
+
1184
+ # ─── MAIN MENU ───────────────────────────────────────────────────────────────
1185
+
1186
+ function Show-Menu {
1187
+ Show-Header
1188
+ Write-Host " Current Accounts:" -ForegroundColor Cyan
1189
+ Write-Host ""
1190
+ Show-Accounts | Out-Null
1191
+ Write-Host ""
1192
+ Write-Host "======================================" -ForegroundColor Cyan
1193
+ Write-Host " 1. List Accounts " -ForegroundColor White
1194
+ Write-Host " 2. Create New Account " -ForegroundColor White
1195
+ Write-Host " 3. Launch Account " -ForegroundColor White
1196
+ Write-Host " 4. Rename Account " -ForegroundColor White
1197
+ Write-Host " 5. Delete Account " -ForegroundColor White
1198
+ Write-Host " 6. Backup Sessions " -ForegroundColor White
1199
+ Write-Host " 7. Restore Sessions " -ForegroundColor White
1200
+ Write-Host " 8. Shared Settings (MCP/Skills) " -ForegroundColor Yellow
1201
+ Write-Host " 9. Plugins & Marketplace " -ForegroundColor Magenta
1202
+ Write-Host " E. Export Profile (Token) " -ForegroundColor Green
1203
+ Write-Host " I. Import Profile (Token) " -ForegroundColor Green
1204
+ Write-Host " 0. Exit " -ForegroundColor White
1205
+ Write-Host "======================================" -ForegroundColor Cyan
1206
+ Write-Host ""
1207
+ }
1208
+
1209
+ while ($true) {
1210
+ Show-Menu
1211
+ $choice = Read-Host " Pick an option"
1212
+ switch ($choice) {
1213
+ "1" { Show-Header; Write-Host "All Accounts:`n" -ForegroundColor Cyan; Show-Accounts | Out-Null; Write-Host ""; pause }
1214
+ "2" { Create-Account }
1215
+ "3" { Launch-Account }
1216
+ "4" { Rename-Account }
1217
+ "5" { Delete-Account }
1218
+ "6" { Backup-Sessions }
1219
+ "7" { Restore-Sessions }
1220
+ "8" { Manage-SharedSettings }
1221
+ "9" { Manage-Plugins }
1222
+ "e" { Export-Profile }
1223
+ "E" { Export-Profile }
1224
+ "i" { Import-Profile }
1225
+ "I" { Import-Profile }
1226
+ "0" { Clear-Host; Write-Host "Bye!" -ForegroundColor Cyan; break }
1227
+ default { Write-Host " Invalid option." -ForegroundColor Red; Start-Sleep 1 }
1228
+ }
1229
+ if ($choice -eq "0") { break }
1230
+ }