@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.
- package/LICENSE +21 -0
- package/README.md +313 -0
- package/bin/claude-multi.js +18 -0
- package/claude-menu.bat +2 -0
- package/claude-menu.ps1 +1324 -0
- package/package.json +33 -0
- package/unix/claude-menu.sh +1319 -0
- package/unix/install.sh +22 -0
- package/windows/claude-menu.bat +2 -0
- package/windows/claude-menu.ps1 +1230 -0
|
@@ -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
|
+
}
|