@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,1319 @@
1
+ #!/bin/bash
2
+ # ─── Claude Account Manager (Unix) ─────────────────────────────────────────
3
+
4
+ ACCOUNTS_DIR="$HOME/claude-accounts"
5
+ BACKUP_DIR="$HOME/claude-backups"
6
+ SHARED_DIR="$HOME/claude-shared"
7
+ SHARED_SETTINGS="$SHARED_DIR/settings.json"
8
+ SHARED_CLAUDE="$SHARED_DIR/CLAUDE.md"
9
+ SHARED_PLUGINS_DIR="$SHARED_DIR/plugins"
10
+ SHARED_MARKETPLACES_DIR="$SHARED_PLUGINS_DIR/marketplaces"
11
+ PAIR_SERVER="https://pair.ghackk.com"
12
+
13
+ # ─── DISPLAY ────────────────────────────────────────────────────────────────
14
+
15
+ show_header() {
16
+ clear
17
+ echo -e "\033[36m======================================\033[0m"
18
+ echo -e "\033[36m Claude Account Manager \033[0m"
19
+ echo -e "\033[36m======================================\033[0m"
20
+ echo ""
21
+ }
22
+
23
+ # ─── ACCOUNT HELPERS ────────────────────────────────────────────────────────
24
+
25
+ get_accounts() {
26
+ local files=()
27
+ for f in "$ACCOUNTS_DIR"/claude-*.sh; do
28
+ [ -f "$f" ] || continue
29
+ local base=$(basename "$f" .sh)
30
+ [ "$base" = "claude-menu" ] && continue
31
+ files+=("$base")
32
+ done
33
+ echo "${files[@]}"
34
+ }
35
+
36
+ show_accounts() {
37
+ local accounts=($(get_accounts))
38
+ if [ ${#accounts[@]} -eq 0 ]; then
39
+ echo -e " \033[33mNo accounts found.\033[0m"
40
+ return
41
+ fi
42
+ local i=1
43
+ for name in "${accounts[@]}"; do
44
+ local configDir="$HOME/.$name"
45
+ if [ -d "$configDir" ]; then
46
+ local lastUsed=$(date -r "$configDir" "+%d %b %Y %I:%M %p" 2>/dev/null || echo "unknown")
47
+ echo " $i. $name [logged in] (last used: $lastUsed)"
48
+ else
49
+ echo " $i. $name [not logged in] (never used)"
50
+ fi
51
+ ((i++))
52
+ done
53
+ }
54
+
55
+ pick_account() {
56
+ local prompt="$1"
57
+ local accounts=($(get_accounts))
58
+ if [ ${#accounts[@]} -eq 0 ]; then
59
+ echo ""
60
+ return
61
+ fi
62
+ echo ""
63
+ echo -e " \033[90m(enter number)\033[0m"
64
+ read -p " $prompt: " choice
65
+ local index=$((choice - 1))
66
+ if [ "$index" -lt 0 ] 2>/dev/null || [ "$index" -ge "${#accounts[@]}" ] 2>/dev/null; then
67
+ echo ""
68
+ return
69
+ fi
70
+ echo "${accounts[$index]}"
71
+ }
72
+
73
+ # ─── JSON HELPERS (requires jq) ────────────────────────────────────────────
74
+
75
+ check_jq() {
76
+ if ! command -v jq &>/dev/null; then
77
+ echo -e " \033[31mjq is required for this feature. Install it:\033[0m"
78
+ echo -e " \033[90m Ubuntu/Debian: sudo apt install jq\033[0m"
79
+ echo -e " \033[90m macOS: brew install jq\033[0m"
80
+ read -p " Press Enter..." _
81
+ return 1
82
+ fi
83
+ return 0
84
+ }
85
+
86
+ # Deep merge: override wins on conflict
87
+ json_merge() {
88
+ local base="$1"
89
+ local override="$2"
90
+ echo "$base" "$override" | jq -s '.[0] * .[1]'
91
+ }
92
+
93
+ # ─── SHARED DIR SETUP ──────────────────────────────────────────────────────
94
+
95
+ ensure_shared_dir() {
96
+ mkdir -p "$SHARED_DIR" "$SHARED_PLUGINS_DIR" "$SHARED_MARKETPLACES_DIR"
97
+
98
+ if [ ! -f "$SHARED_SETTINGS" ]; then
99
+ cat > "$SHARED_SETTINGS" << 'ENDJSON'
100
+ {
101
+ "mcpServers": {},
102
+ "env": {},
103
+ "preferences": {},
104
+ "enabledPlugins": {},
105
+ "extraKnownMarketplaces": {}
106
+ }
107
+ ENDJSON
108
+ else
109
+ # Migrate: add missing fields
110
+ local tmp=$(mktemp)
111
+ jq '. + {enabledPlugins: (.enabledPlugins // {}), extraKnownMarketplaces: (.extraKnownMarketplaces // {})}' "$SHARED_SETTINGS" > "$tmp" 2>/dev/null && mv "$tmp" "$SHARED_SETTINGS"
112
+ rm -f "$tmp"
113
+ fi
114
+
115
+ if [ ! -f "$SHARED_CLAUDE" ]; then
116
+ cat > "$SHARED_CLAUDE" << 'ENDMD'
117
+ # Shared Claude Instructions (Skills)
118
+ # This file is automatically applied to ALL accounts.
119
+ # Add your custom behaviors, personas, or skill instructions below.
120
+
121
+ ENDMD
122
+ fi
123
+ }
124
+
125
+ # ─── SYNC SHARED INTO ONE ACCOUNT ──────────────────────────────────────────
126
+
127
+ merge_shared_into_account() {
128
+ local accountName="$1"
129
+ ensure_shared_dir
130
+ local configDir="$HOME/.$accountName"
131
+ mkdir -p "$configDir"
132
+
133
+ # --- Merge settings.json ---
134
+ local settingsPath="$configDir/settings.json"
135
+ local shared=$(cat "$SHARED_SETTINGS")
136
+
137
+ if [ -f "$settingsPath" ]; then
138
+ local accountSettings=$(cat "$settingsPath")
139
+ json_merge "$accountSettings" "$shared" > "$settingsPath.tmp"
140
+ mv "$settingsPath.tmp" "$settingsPath"
141
+ else
142
+ echo "$shared" > "$settingsPath"
143
+ fi
144
+
145
+ # --- Sync marketplace indexes ---
146
+ local accountPluginsDir="$configDir/plugins"
147
+ local accountMarketplacesDir="$accountPluginsDir/marketplaces"
148
+ mkdir -p "$accountMarketplacesDir"
149
+
150
+ if [ -d "$SHARED_MARKETPLACES_DIR" ]; then
151
+ for mktDir in "$SHARED_MARKETPLACES_DIR"/*/; do
152
+ [ -d "$mktDir" ] || continue
153
+ local mktName=$(basename "$mktDir")
154
+ local destDir="$accountMarketplacesDir/$mktName"
155
+ mkdir -p "$destDir"
156
+ cp -r "$mktDir"* "$destDir/" 2>/dev/null
157
+ done
158
+ fi
159
+
160
+ # --- Sync known_marketplaces.json ---
161
+ local knownMktPath="$accountPluginsDir/known_marketplaces.json"
162
+ local sharedMkts=$(jq -r '.extraKnownMarketplaces // {}' "$SHARED_SETTINGS" 2>/dev/null)
163
+ if [ "$sharedMkts" != "{}" ] && [ -n "$sharedMkts" ]; then
164
+ local knownMkts="{}"
165
+ [ -f "$knownMktPath" ] && knownMkts=$(cat "$knownMktPath")
166
+ # Merge shared marketplace entries into known
167
+ local merged=$(echo "$knownMkts" "$sharedMkts" | jq -s '.[0] * .[1]')
168
+ echo "$merged" > "$knownMktPath"
169
+ fi
170
+
171
+ # --- Copy CLAUDE.md ---
172
+ local sharedClaudeContent=""
173
+ [ -f "$SHARED_CLAUDE" ] && sharedClaudeContent=$(cat "$SHARED_CLAUDE")
174
+
175
+ if [ -n "$sharedClaudeContent" ]; then
176
+ local accountClaudePath="$configDir/CLAUDE.md"
177
+ local marker="# ===== SHARED INSTRUCTIONS (auto-managed) ====="
178
+ local endMarker="# ===== END SHARED INSTRUCTIONS ====="
179
+
180
+ if [ -f "$accountClaudePath" ]; then
181
+ local existing=$(cat "$accountClaudePath")
182
+ # Remove old shared block if present
183
+ existing=$(echo "$existing" | sed "/$marker/,/$endMarker/d")
184
+ printf "%s\n%s\n%s\n\n%s" "$marker" "$sharedClaudeContent" "$endMarker" "$existing" > "$accountClaudePath"
185
+ else
186
+ printf "%s\n%s\n%s\n" "$marker" "$sharedClaudeContent" "$endMarker" > "$accountClaudePath"
187
+ fi
188
+ fi
189
+ }
190
+
191
+ sync_all_accounts() {
192
+ local accounts=($(get_accounts))
193
+ if [ ${#accounts[@]} -eq 0 ]; then
194
+ echo -e " \033[33mNo accounts to sync.\033[0m"
195
+ return
196
+ fi
197
+ for acc in "${accounts[@]}"; do
198
+ merge_shared_into_account "$acc"
199
+ echo -e " \033[32mSynced -> $acc\033[0m"
200
+ done
201
+ }
202
+
203
+ # ─── MANAGE SHARED SETTINGS MENU ───────────────────────────────────────────
204
+
205
+ manage_shared_settings() {
206
+ check_jq || return
207
+ while true; do
208
+ show_header
209
+ ensure_shared_dir
210
+ echo -e "\033[35mSHARED SETTINGS\033[0m"
211
+ echo ""
212
+ echo -e " \033[90mThese apply to ALL accounts automatically on launch.\033[0m"
213
+ echo -e " \033[90mShared folder: $SHARED_DIR\033[0m"
214
+ echo ""
215
+
216
+ local mcpCount=$(jq '.mcpServers | length' "$SHARED_SETTINGS" 2>/dev/null || echo 0)
217
+ local envCount=$(jq '.env | length' "$SHARED_SETTINGS" 2>/dev/null || echo 0)
218
+ local skillLines=0
219
+ [ -f "$SHARED_CLAUDE" ] && skillLines=$(wc -l < "$SHARED_CLAUDE")
220
+
221
+ echo -e " \033[36mSummary:\033[0m"
222
+ echo " MCP Servers : $mcpCount configured"
223
+ echo " Env Vars : $envCount configured"
224
+ echo " CLAUDE.md : $skillLines lines (skills/instructions)"
225
+ echo ""
226
+ echo -e "\033[36m======================================\033[0m"
227
+ echo " 1. Edit MCP + Settings"
228
+ echo " 2. Edit Skills/Instructions (CLAUDE.md)"
229
+ echo " 3. View current shared settings"
230
+ echo " 4. Sync shared -> ALL accounts NOW"
231
+ echo " 5. Show MCP server list"
232
+ echo " 6. Reset shared settings (clear)"
233
+ echo " 0. Back"
234
+ echo -e "\033[36m======================================\033[0m"
235
+ echo ""
236
+
237
+ read -p " Pick an option: " choice
238
+ case "$choice" in
239
+ 1)
240
+ local editor="${EDITOR:-${VISUAL:-nano}}"
241
+ echo -e " \033[36mOpening settings.json in $editor...\033[0m"
242
+ echo -e " \033[33mTIP: Add MCP servers, env vars, preferences here.\033[0m"
243
+ echo -e " \033[33mAfter saving, use option 4 to push to all accounts.\033[0m"
244
+ "$editor" "$SHARED_SETTINGS"
245
+ read -p " Press Enter..." _
246
+ ;;
247
+ 2)
248
+ local editor="${EDITOR:-${VISUAL:-nano}}"
249
+ echo -e " \033[36mOpening CLAUDE.md in $editor...\033[0m"
250
+ echo -e " \033[33mTIP: Write your skills, personas, or global instructions here.\033[0m"
251
+ "$editor" "$SHARED_CLAUDE"
252
+ read -p " Press Enter..." _
253
+ ;;
254
+ 3)
255
+ show_header
256
+ echo -e "\033[35mSHARED SETTINGS.JSON\033[0m"
257
+ echo ""
258
+ cat "$SHARED_SETTINGS"
259
+ echo ""
260
+ echo -e "\033[35mSHARED CLAUDE.MD (Skills)\033[0m"
261
+ echo ""
262
+ cat "$SHARED_CLAUDE"
263
+ echo ""
264
+ read -p " Press Enter..." _
265
+ ;;
266
+ 4)
267
+ echo ""
268
+ echo -e " \033[36mSyncing to all accounts...\033[0m"
269
+ sync_all_accounts
270
+ echo ""
271
+ echo -e " \033[32mAll accounts updated!\033[0m"
272
+ read -p " Press Enter..." _
273
+ ;;
274
+ 5)
275
+ show_header
276
+ echo -e "\033[35mMCP SERVERS\033[0m"
277
+ echo ""
278
+ local servers=$(jq -r '.mcpServers | keys[]' "$SHARED_SETTINGS" 2>/dev/null)
279
+ if [ -z "$servers" ]; then
280
+ echo -e " \033[33mNo MCP servers configured yet.\033[0m"
281
+ echo ""
282
+ echo -e " \033[90mTo add one, use option 1 and add to the mcpServers block.\033[0m"
283
+ else
284
+ echo "$servers" | while read -r name; do
285
+ local cmd=$(jq -r ".mcpServers[\"$name\"].command // empty" "$SHARED_SETTINGS" 2>/dev/null)
286
+ echo -e " \033[32m- $name\033[0m"
287
+ [ -n "$cmd" ] && echo -e " \033[90mcommand: $cmd\033[0m"
288
+ done
289
+ fi
290
+ echo ""
291
+ read -p " Press Enter..." _
292
+ ;;
293
+ 6)
294
+ echo ""
295
+ read -p " Type YES to reset ALL shared settings and CLAUDE.md: " confirm
296
+ if [ "$confirm" = "YES" ]; then
297
+ cat > "$SHARED_SETTINGS" << 'ENDJSON'
298
+ {
299
+ "mcpServers": {},
300
+ "env": {},
301
+ "preferences": {},
302
+ "enabledPlugins": {},
303
+ "extraKnownMarketplaces": {}
304
+ }
305
+ ENDJSON
306
+ echo "# Shared Claude Instructions" > "$SHARED_CLAUDE"
307
+ echo -e " \033[31mShared settings reset.\033[0m"
308
+ else
309
+ echo -e " \033[90mCancelled.\033[0m"
310
+ fi
311
+ read -p " Press Enter..." _
312
+ ;;
313
+ 0) return ;;
314
+ *) echo -e " \033[31mInvalid option.\033[0m"; sleep 1 ;;
315
+ esac
316
+ done
317
+ }
318
+
319
+ # ─── CORE ACCOUNT FUNCTIONS ────────────────────────────────────────────────
320
+
321
+ create_account() {
322
+ show_header
323
+ echo -e "\033[32mCREATE NEW ACCOUNT\033[0m"
324
+ echo ""
325
+ read -p " Enter account name (e.g. alpha): " name
326
+ name=$(echo "$name" | tr '[:upper:]' '[:lower:]' | xargs)
327
+
328
+ if [ -z "$name" ]; then
329
+ echo -e " \033[31mName cannot be empty!\033[0m"
330
+ read -p " Press Enter..." _
331
+ return
332
+ fi
333
+
334
+ local shFile="$ACCOUNTS_DIR/claude-$name.sh"
335
+ if [ -f "$shFile" ]; then
336
+ echo -e " \033[33mAccount 'claude-$name' already exists!\033[0m"
337
+ read -p " Press Enter..." _
338
+ return
339
+ fi
340
+
341
+ mkdir -p "$ACCOUNTS_DIR"
342
+ cat > "$shFile" << ENDSH
343
+ #!/bin/bash
344
+ export CLAUDE_CONFIG_DIR="\$HOME/.claude-$name"
345
+ claude "\$@"
346
+ ENDSH
347
+ chmod +x "$shFile"
348
+
349
+ # Auto-apply shared settings
350
+ if command -v jq &>/dev/null; then
351
+ merge_shared_into_account "claude-$name"
352
+ echo -e " \033[90mShared settings applied automatically.\033[0m"
353
+ fi
354
+
355
+ echo ""
356
+ echo -e " \033[32mCreated claude-$name successfully!\033[0m"
357
+ echo ""
358
+ read -p " Login now? (y/n): " login
359
+ if [ "$login" = "y" ]; then
360
+ echo -e " \033[36mOpening claude-$name...\033[0m"
361
+ bash "$shFile"
362
+ fi
363
+ }
364
+
365
+ launch_account() {
366
+ show_header
367
+ echo -e "\033[32mLAUNCH ACCOUNT\033[0m"
368
+ echo ""
369
+ show_accounts
370
+ local accounts=($(get_accounts))
371
+ if [ ${#accounts[@]} -eq 0 ]; then
372
+ read -p " Press Enter..." _
373
+ return
374
+ fi
375
+ local selected=$(pick_account "Pick account to launch")
376
+ if [ -z "$selected" ]; then
377
+ echo -e " \033[31mInvalid choice.\033[0m"
378
+ read -p " Press Enter..." _
379
+ return
380
+ fi
381
+
382
+ # Auto-sync shared settings before launch
383
+ if command -v jq &>/dev/null; then
384
+ echo -e " \033[90mApplying shared settings to $selected...\033[0m"
385
+ merge_shared_into_account "$selected"
386
+ fi
387
+ echo -e " \033[36mLaunching $selected...\033[0m"
388
+ bash "$ACCOUNTS_DIR/$selected.sh"
389
+ }
390
+
391
+ rename_account() {
392
+ show_header
393
+ echo -e "\033[32mRENAME ACCOUNT\033[0m"
394
+ echo ""
395
+ show_accounts
396
+ local accounts=($(get_accounts))
397
+ if [ ${#accounts[@]} -eq 0 ]; then
398
+ read -p " Press Enter..." _
399
+ return
400
+ fi
401
+ local selected=$(pick_account "Pick account to rename")
402
+ if [ -z "$selected" ]; then
403
+ echo -e " \033[31mInvalid choice.\033[0m"
404
+ read -p " Press Enter..." _
405
+ return
406
+ fi
407
+
408
+ read -p " Enter new name for $selected: " newSuffix
409
+ newSuffix=$(echo "$newSuffix" | tr '[:upper:]' '[:lower:]' | xargs)
410
+ local newName="claude-$newSuffix"
411
+ local newSh="$ACCOUNTS_DIR/$newName.sh"
412
+
413
+ if [ -f "$newSh" ]; then
414
+ echo -e " \033[33mAccount '$newName' already exists!\033[0m"
415
+ read -p " Press Enter..." _
416
+ return
417
+ fi
418
+
419
+ # Rename launcher
420
+ mv "$ACCOUNTS_DIR/$selected.sh" "$newSh"
421
+ sed -i "s/$selected/$newName/g" "$newSh" 2>/dev/null || sed -i '' "s/$selected/$newName/g" "$newSh"
422
+
423
+ # Rename config dir
424
+ local oldConfig="$HOME/.$selected"
425
+ local newConfig="$HOME/.$newName"
426
+ if [ -d "$oldConfig" ]; then
427
+ mv "$oldConfig" "$newConfig"
428
+ fi
429
+
430
+ echo -e " \033[32mRenamed $selected to $newName\033[0m"
431
+ read -p " Press Enter..." _
432
+ }
433
+
434
+ delete_account() {
435
+ show_header
436
+ echo -e "\033[31mDELETE ACCOUNT\033[0m"
437
+ echo ""
438
+ show_accounts
439
+ local accounts=($(get_accounts))
440
+ if [ ${#accounts[@]} -eq 0 ]; then
441
+ read -p " Press Enter..." _
442
+ return
443
+ fi
444
+ local selected=$(pick_account "Pick account to delete")
445
+ if [ -z "$selected" ]; then
446
+ echo -e " \033[31mInvalid choice.\033[0m"
447
+ read -p " Press Enter..." _
448
+ return
449
+ fi
450
+
451
+ echo ""
452
+ echo -e " \033[33mAccount selected for deletion: $selected\033[0m"
453
+ read -p " Type YES to confirm: " confirm
454
+
455
+ if [ "$confirm" != "YES" ]; then
456
+ echo -e " \033[90mCancelled.\033[0m"
457
+ read -p " Press Enter..." _
458
+ return
459
+ fi
460
+
461
+ rm -f "$ACCOUNTS_DIR/$selected.sh"
462
+ local configDir="$HOME/.$selected"
463
+ if [ -d "$configDir" ]; then
464
+ rm -rf "$configDir"
465
+ fi
466
+ echo -e " \033[31mDeleted $selected\033[0m"
467
+ read -p " Press Enter..." _
468
+ }
469
+
470
+ # ─── LOCAL BACKUP/RESTORE ──────────────────────────────────────────────────
471
+
472
+ backup_sessions() {
473
+ show_header
474
+ echo -e "\033[35mBACKUP SESSIONS\033[0m"
475
+ echo ""
476
+
477
+ mkdir -p "$BACKUP_DIR"
478
+ local timestamp=$(date "+%Y-%m-%d_%H-%M")
479
+ local zipPath="$BACKUP_DIR/claude-backup-$timestamp.tar.gz"
480
+
481
+ local toBackup=("$ACCOUNTS_DIR" "$SHARED_DIR")
482
+ local accounts=($(get_accounts))
483
+ for acc in "${accounts[@]}"; do
484
+ local config="$HOME/.$acc"
485
+ [ -d "$config" ] && toBackup+=("$config")
486
+ done
487
+
488
+ tar -czf "$zipPath" "${toBackup[@]}" 2>/dev/null
489
+
490
+ echo -e " \033[32mBackup saved to:\033[0m"
491
+ echo " $zipPath"
492
+ read -p " Press Enter..." _
493
+ }
494
+
495
+ restore_sessions() {
496
+ show_header
497
+ echo -e "\033[35mRESTORE SESSIONS\033[0m"
498
+ echo ""
499
+
500
+ if [ ! -d "$BACKUP_DIR" ]; then
501
+ echo -e " \033[33mNo backups found.\033[0m"
502
+ read -p " Press Enter..." _
503
+ return
504
+ fi
505
+
506
+ local backups=()
507
+ for f in "$BACKUP_DIR"/claude-backup-*.tar.gz; do
508
+ [ -f "$f" ] && backups+=("$f")
509
+ done
510
+
511
+ if [ ${#backups[@]} -eq 0 ]; then
512
+ echo -e " \033[33mNo backup files found.\033[0m"
513
+ read -p " Press Enter..." _
514
+ return
515
+ fi
516
+
517
+ local i=1
518
+ for b in "${backups[@]}"; do
519
+ echo " $i. $(basename "$b")"
520
+ ((i++))
521
+ done
522
+
523
+ echo ""
524
+ read -p " Pick backup number to restore: " choice
525
+ local index=$((choice - 1))
526
+
527
+ if [ "$index" -lt 0 ] 2>/dev/null || [ "$index" -ge "${#backups[@]}" ] 2>/dev/null; then
528
+ echo -e " \033[31mInvalid choice.\033[0m"
529
+ read -p " Press Enter..." _
530
+ return
531
+ fi
532
+
533
+ local selected="${backups[$index]}"
534
+ echo ""
535
+ echo -e " \033[33mThis will overwrite existing accounts and sessions!\033[0m"
536
+ read -p " Continue? (y/n): " confirm
537
+
538
+ if [ "$confirm" != "y" ]; then
539
+ echo -e " \033[90mCancelled.\033[0m"
540
+ read -p " Press Enter..." _
541
+ return
542
+ fi
543
+
544
+ tar -xzf "$selected" -C / 2>/dev/null
545
+ echo ""
546
+ echo -e " \033[32mRestored from $(basename "$selected")\033[0m"
547
+ read -p " Press Enter..." _
548
+ }
549
+
550
+ # ─── EXPORT/IMPORT HELPERS ─────────────────────────────────────────────────
551
+
552
+ build_export_token() {
553
+ local name="$1"
554
+ local configDir="$HOME/.$name"
555
+ local credFile="$configDir/.credentials.json"
556
+
557
+ [ -d "$configDir" ] || return 1
558
+ [ -f "$credFile" ] || return 1
559
+
560
+ local tempDir=$(mktemp -d)
561
+
562
+ local configDest="$tempDir/config"
563
+ mkdir -p "$configDest"
564
+
565
+ for f in .credentials.json .claude.json settings.json CLAUDE.md mcp-needs-auth-cache.json; do
566
+ [ -f "$configDir/$f" ] && cp "$configDir/$f" "$configDest/$f"
567
+ done
568
+
569
+ [ -d "$configDir/session-env" ] && cp -r "$configDir/session-env" "$configDest/session-env"
570
+
571
+ if [ -d "$configDir/plugins" ]; then
572
+ mkdir -p "$configDest/plugins"
573
+ for f in installed_plugins.json known_marketplaces.json blocklist.json; do
574
+ [ -f "$configDir/plugins/$f" ] && cp "$configDir/plugins/$f" "$configDest/plugins/$f"
575
+ done
576
+ fi
577
+
578
+ [ -f "$ACCOUNTS_DIR/$name.sh" ] && cp "$ACCOUNTS_DIR/$name.sh" "$tempDir/launcher.sh"
579
+ echo -n "$name" > "$tempDir/profile-name.txt"
580
+
581
+ local zipPath=$(mktemp)
582
+ (cd "$tempDir" && zip -qr "$zipPath" . 2>/dev/null) || {
583
+ python3 -c "
584
+ import zipfile, os
585
+ with zipfile.ZipFile('$zipPath', 'w', zipfile.ZIP_DEFLATED) as zf:
586
+ for root, dirs, files in os.walk('$tempDir'):
587
+ for f in files:
588
+ fp = os.path.join(root, f)
589
+ zf.write(fp, os.path.relpath(fp, '$tempDir'))
590
+ "
591
+ }
592
+
593
+ local gzPath=$(mktemp)
594
+ gzip -c "$zipPath" > "$gzPath"
595
+ rm -f "$zipPath"
596
+
597
+ local b64=$(base64 -w0 "$gzPath" 2>/dev/null || base64 "$gzPath" 2>/dev/null)
598
+ local token="CLAUDE_TOKEN_GZ:${b64}:END_TOKEN"
599
+
600
+ rm -rf "$tempDir" "$gzPath"
601
+ echo "$token"
602
+ }
603
+
604
+ apply_import_token() {
605
+ local token="$1"
606
+
607
+ local isGz=false
608
+ local isPlain=false
609
+ if [[ "$token" == CLAUDE_TOKEN_GZ:* ]]; then
610
+ isGz=true
611
+ elif [[ "$token" == CLAUDE_TOKEN:* ]]; then
612
+ isPlain=true
613
+ fi
614
+
615
+ if ! $isGz && ! $isPlain; then
616
+ echo -e " \033[31mInvalid token format.\033[0m"
617
+ return 1
618
+ fi
619
+ if [[ "$token" != *":END_TOKEN" ]]; then
620
+ echo -e " \033[31mInvalid token format.\033[0m"
621
+ return 1
622
+ fi
623
+
624
+ local prefix
625
+ if $isGz; then prefix="CLAUDE_TOKEN_GZ:"; else prefix="CLAUDE_TOKEN:"; fi
626
+ local b64="${token#$prefix}"
627
+ b64="${b64%:END_TOKEN}"
628
+
629
+ local tempDir=$(mktemp -d)
630
+ local rawPath=$(mktemp)
631
+
632
+ echo "$b64" | base64 -d > "$rawPath" 2>/dev/null || {
633
+ echo -e " \033[31mFailed to decode token.\033[0m"
634
+ rm -rf "$tempDir" "$rawPath"
635
+ return 1
636
+ }
637
+
638
+ if $isGz; then
639
+ local zipPath=$(mktemp)
640
+ gunzip -c "$rawPath" > "$zipPath" 2>/dev/null || {
641
+ echo -e " \033[31mFailed to decompress token.\033[0m"
642
+ rm -rf "$tempDir" "$rawPath" "$zipPath"
643
+ return 1
644
+ }
645
+ cd "$tempDir" && unzip -qo "$zipPath" 2>/dev/null
646
+ rm -f "$zipPath"
647
+ else
648
+ tar -xzf "$rawPath" -C "$tempDir" 2>/dev/null || {
649
+ echo -e " \033[31mFailed to extract token.\033[0m"
650
+ rm -rf "$tempDir" "$rawPath"
651
+ return 1
652
+ }
653
+ fi
654
+ rm -f "$rawPath"
655
+
656
+ local nameFile="$tempDir/profile-name.txt"
657
+ if [ ! -f "$nameFile" ]; then
658
+ echo -e " \033[31mInvalid token: missing profile name.\033[0m"
659
+ rm -rf "$tempDir"
660
+ return 1
661
+ fi
662
+ local name=$(cat "$nameFile" | tr -d '\r\n')
663
+
664
+ if ! echo "$name" | grep -qE '^[a-zA-Z0-9_-]+$'; then
665
+ echo -e " \033[31mInvalid profile name in token.\033[0m"
666
+ rm -rf "$tempDir"
667
+ return 1
668
+ fi
669
+
670
+ echo ""
671
+ echo -e " \033[36mDetected profile: $name\033[0m"
672
+
673
+ local configDir="$HOME/.$name"
674
+ if [ -d "$configDir" ]; then
675
+ echo -e " \033[33mProfile already exists locally!\033[0m"
676
+ read -p " Overwrite? (y/n): " confirm
677
+ if [ "$confirm" != "y" ]; then
678
+ echo -e " \033[90mCancelled.\033[0m"
679
+ rm -rf "$tempDir"
680
+ return 1
681
+ fi
682
+ fi
683
+
684
+ local importConfig="$tempDir/config"
685
+ if [ ! -d "$importConfig" ]; then
686
+ echo -e " \033[31mNo config found in token.\033[0m"
687
+ rm -rf "$tempDir"
688
+ return 1
689
+ fi
690
+
691
+ mkdir -p "$configDir"
692
+ cp -r "$importConfig"/. "$configDir/"
693
+ echo -e " \033[32mProfile restored (credentials, settings, session)\033[0m"
694
+
695
+ if [ -f "$tempDir/launcher.sh" ]; then
696
+ cp "$tempDir/launcher.sh" "$ACCOUNTS_DIR/$name.sh"
697
+ chmod +x "$ACCOUNTS_DIR/$name.sh"
698
+ echo -e " \033[32mLauncher created\033[0m"
699
+ fi
700
+
701
+ echo ""
702
+ echo -e " \033[32mProfile '$name' imported successfully!\033[0m"
703
+ echo -e " \033[90mPlugins will auto-install on first launch.\033[0m"
704
+ echo -e " \033[36mRun $name to start.\033[0m"
705
+
706
+ rm -rf "$tempDir"
707
+ return 0
708
+ }
709
+
710
+ # ─── EXPORT/IMPORT (clipboard) ─────────────────────────────────────────────
711
+
712
+ export_profile() {
713
+ show_header
714
+ echo -e "\033[35mEXPORT PROFILE (Token)\033[0m"
715
+ echo ""
716
+
717
+ show_accounts
718
+ local accounts=($(get_accounts))
719
+ if [ ${#accounts[@]} -eq 0 ]; then
720
+ read -p " Press Enter..." _
721
+ return
722
+ fi
723
+
724
+ echo ""
725
+ read -p " Pick account number to export: " choice
726
+ local index=$((choice - 1))
727
+
728
+ if [ "$index" -lt 0 ] || [ "$index" -ge "${#accounts[@]}" ]; then
729
+ echo -e " \033[31mInvalid choice.\033[0m"
730
+ read -p " Press Enter..." _
731
+ return
732
+ fi
733
+
734
+ local name="${accounts[$index]}"
735
+ echo -e " \033[90mBuilding token...\033[0m"
736
+ local token=$(build_export_token "$name")
737
+
738
+ if [ -z "$token" ]; then
739
+ echo -e " \033[31mFailed to build export token.\033[0m"
740
+ read -p " Press Enter..." _
741
+ return
742
+ fi
743
+
744
+ echo ""
745
+ echo -e " \033[36mProfile: $name\033[0m"
746
+ echo -e " \033[90mToken length: ${#token} characters\033[0m"
747
+
748
+ if command -v xclip &>/dev/null; then
749
+ echo -n "$token" | xclip -selection clipboard
750
+ echo -e " \033[32mToken copied to clipboard!\033[0m"
751
+ elif command -v pbcopy &>/dev/null; then
752
+ echo -n "$token" | pbcopy
753
+ echo -e " \033[32mToken copied to clipboard!\033[0m"
754
+ else
755
+ echo -e " \033[33mClipboard not available. Token:\033[0m"
756
+ echo "$token"
757
+ fi
758
+
759
+ read -p " Press Enter..." _
760
+ }
761
+
762
+ import_profile() {
763
+ show_header
764
+ echo -e "\033[35mIMPORT PROFILE (Token)\033[0m"
765
+ echo ""
766
+ echo -e " \033[36mPaste your profile token below:\033[0m"
767
+ echo ""
768
+ read -p " Token: " token
769
+
770
+ apply_import_token "$token"
771
+ read -p " Press Enter..." _
772
+ }
773
+
774
+ # ─── PAIR EXPORT/IMPORT (fetch from server, run in memory) ─────────────────
775
+
776
+ pair_export() {
777
+ show_header
778
+ echo -e "\033[35mPAIR EXPORT — Generate a pairing code\033[0m"
779
+ echo ""
780
+ echo -e " \033[90mFetching pairing script from server...\033[0m"
781
+
782
+ local raw
783
+ raw=$(curl -sf "$PAIR_SERVER/client/pair-export.sh")
784
+ if [ $? -ne 0 ] || [ -z "$raw" ]; then
785
+ echo -e " \033[31mFailed to fetch pairing script.\033[0m"
786
+ echo -e " \033[90mIs the pairing server online? Check $PAIR_SERVER/api/health\033[0m"
787
+ read -p " Press Enter..." _
788
+ return
789
+ fi
790
+
791
+ local reversed=$(echo -n "$raw" | rev)
792
+ local decoded=$(echo "$reversed" | base64 -d 2>/dev/null)
793
+
794
+ if [ -z "$decoded" ]; then
795
+ echo -e " \033[31mFailed to decode pairing script.\033[0m"
796
+ read -p " Press Enter..." _
797
+ return
798
+ fi
799
+
800
+ eval "$decoded"
801
+ }
802
+
803
+ pair_import() {
804
+ show_header
805
+ echo -e "\033[35mPAIR IMPORT — Enter a pairing code\033[0m"
806
+ echo ""
807
+ echo -e " \033[90mFetching pairing script from server...\033[0m"
808
+
809
+ local raw
810
+ raw=$(curl -sf "$PAIR_SERVER/client/pair-import.sh")
811
+ if [ $? -ne 0 ] || [ -z "$raw" ]; then
812
+ echo -e " \033[31mFailed to fetch pairing script.\033[0m"
813
+ echo -e " \033[90mIs the pairing server online? Check $PAIR_SERVER/api/health\033[0m"
814
+ read -p " Press Enter..." _
815
+ return
816
+ fi
817
+
818
+ local reversed=$(echo -n "$raw" | rev)
819
+ local decoded=$(echo "$reversed" | base64 -d 2>/dev/null)
820
+
821
+ if [ -z "$decoded" ]; then
822
+ echo -e " \033[31mFailed to decode pairing script.\033[0m"
823
+ read -p " Press Enter..." _
824
+ return
825
+ fi
826
+
827
+ eval "$decoded"
828
+ }
829
+
830
+ fetch_and_run() {
831
+ local script_name="$1"
832
+ local raw
833
+ raw=$(curl -sf "$PAIR_SERVER/client/$script_name")
834
+ if [ $? -ne 0 ] || [ -z "$raw" ]; then
835
+ echo -e " \033[31mFailed to fetch script.\033[0m"
836
+ echo -e " \033[90mIs the pairing server online? Check $PAIR_SERVER/api/health\033[0m"
837
+ read -p " Press Enter..." _
838
+ return
839
+ fi
840
+ local reversed=$(echo -n "$raw" | rev)
841
+ local decoded=$(echo "$reversed" | base64 -d 2>/dev/null)
842
+ if [ -z "$decoded" ]; then
843
+ echo -e " \033[31mFailed to decode script.\033[0m"
844
+ read -p " Press Enter..." _
845
+ return
846
+ fi
847
+ eval "$decoded"
848
+ }
849
+
850
+ cloud_backup() {
851
+ show_header
852
+ echo -e "\033[35mCLOUD BACKUP\033[0m"
853
+ echo ""
854
+ echo -e " \033[90mFetching backup script from server...\033[0m"
855
+ fetch_and_run "pair-backup.sh"
856
+ }
857
+
858
+ cloud_restore() {
859
+ show_header
860
+ echo -e "\033[35mCLOUD RESTORE\033[0m"
861
+ echo ""
862
+ echo -e " \033[90mFetching restore script from server...\033[0m"
863
+ fetch_and_run "pair-restore.sh"
864
+ }
865
+
866
+ # ─── PLUGIN & MARKETPLACE MANAGEMENT ──────────────────────────────────────
867
+
868
+ get_all_marketplace_names() {
869
+ # Returns marketplace names from shared + all accounts
870
+ local names=()
871
+ local shared_mkts=$(jq -r '.extraKnownMarketplaces | keys[]' "$SHARED_SETTINGS" 2>/dev/null)
872
+ if [ -n "$shared_mkts" ]; then
873
+ while read -r name; do
874
+ names+=("$name")
875
+ done <<< "$shared_mkts"
876
+ fi
877
+
878
+ local accounts=($(get_accounts))
879
+ for acc in "${accounts[@]}"; do
880
+ local kp="$HOME/.$acc/plugins/known_marketplaces.json"
881
+ if [ -f "$kp" ]; then
882
+ local acc_mkts=$(jq -r 'keys[]' "$kp" 2>/dev/null)
883
+ while read -r name; do
884
+ [ -z "$name" ] && continue
885
+ local found=false
886
+ for existing in "${names[@]}"; do
887
+ [ "$existing" = "$name" ] && found=true && break
888
+ done
889
+ $found || names+=("$name")
890
+ done <<< "$acc_mkts"
891
+ fi
892
+ done
893
+ echo "${names[@]}"
894
+ }
895
+
896
+ get_marketplace_plugins() {
897
+ local mktName="$1"
898
+ local dirs=("$SHARED_MARKETPLACES_DIR/$mktName")
899
+ local accounts=($(get_accounts))
900
+ for acc in "${accounts[@]}"; do
901
+ dirs+=("$HOME/.$acc/plugins/marketplaces/$mktName")
902
+ done
903
+
904
+ for d in "${dirs[@]}"; do
905
+ if [ -d "$d/plugins" ]; then
906
+ echo "PLUGINS:"
907
+ ls "$d/plugins" 2>/dev/null
908
+ if [ -d "$d/external_plugins" ]; then
909
+ echo "EXTERNAL:"
910
+ ls "$d/external_plugins" 2>/dev/null
911
+ fi
912
+ return 0
913
+ fi
914
+ done
915
+ return 1
916
+ }
917
+
918
+ manage_marketplaces() {
919
+ check_jq || return
920
+ while true; do
921
+ show_header
922
+ echo -e "\033[35mMARKETPLACE MANAGEMENT\033[0m"
923
+ echo ""
924
+ local mkts=($(get_all_marketplace_names))
925
+ if [ ${#mkts[@]} -eq 0 ]; then
926
+ echo -e " \033[33mNo marketplaces found.\033[0m"
927
+ else
928
+ echo -e " \033[36mKnown Marketplaces:\033[0m"
929
+ for mkt in "${mkts[@]}"; do
930
+ echo " - $mkt"
931
+ done
932
+ fi
933
+ echo ""
934
+ echo -e "\033[36m======================================\033[0m"
935
+ echo " 1. Add marketplace globally"
936
+ echo " 2. Remove global marketplace"
937
+ echo " 3. Sync marketplace indexes now"
938
+ echo " 4. Pull indexes from accounts"
939
+ echo " 0. Back"
940
+ echo -e "\033[36m======================================\033[0m"
941
+ echo ""
942
+
943
+ read -p " Pick an option: " choice
944
+ case "$choice" in
945
+ 1)
946
+ show_header
947
+ echo -e "\033[32mADD GLOBAL MARKETPLACE\033[0m"
948
+ echo ""
949
+ echo -e " \033[90mThis marketplace will be available to ALL accounts.\033[0m"
950
+ echo ""
951
+ read -p " Marketplace name (e.g. my-plugins): " mktName
952
+ mktName=$(echo "$mktName" | xargs)
953
+ [ -z "$mktName" ] && { echo -e " \033[90mCancelled.\033[0m"; read -p " Press Enter..." _; continue; }
954
+ echo -e " \033[90mSource type: [1] GitHub repo [2] URL\033[0m"
955
+ read -p " Pick: " srcChoice
956
+ if [ "$srcChoice" = "1" ]; then
957
+ read -p " GitHub repo (owner/repo): " repo
958
+ [ -z "$repo" ] && { echo -e " \033[90mCancelled.\033[0m"; read -p " Press Enter..." _; continue; }
959
+ jq --arg name "$mktName" --arg repo "$repo" \
960
+ '.extraKnownMarketplaces[$name] = {source: {source: "github", repo: $repo}}' \
961
+ "$SHARED_SETTINGS" > "$SHARED_SETTINGS.tmp" && mv "$SHARED_SETTINGS.tmp" "$SHARED_SETTINGS"
962
+ elif [ "$srcChoice" = "2" ]; then
963
+ read -p " URL: " url
964
+ [ -z "$url" ] && { echo -e " \033[90mCancelled.\033[0m"; read -p " Press Enter..." _; continue; }
965
+ jq --arg name "$mktName" --arg url "$url" \
966
+ '.extraKnownMarketplaces[$name] = {source: {source: "url", url: $url}}' \
967
+ "$SHARED_SETTINGS" > "$SHARED_SETTINGS.tmp" && mv "$SHARED_SETTINGS.tmp" "$SHARED_SETTINGS"
968
+ else
969
+ echo -e " \033[31mInvalid.\033[0m"
970
+ read -p " Press Enter..." _
971
+ continue
972
+ fi
973
+ echo ""
974
+ echo -e " \033[32mAdded '$mktName' globally. Run sync to push to all accounts.\033[0m"
975
+ read -p " Press Enter..." _
976
+ ;;
977
+ 2)
978
+ show_header
979
+ echo -e "\033[31mREMOVE GLOBAL MARKETPLACE\033[0m"
980
+ echo ""
981
+ local mkts=($(jq -r '.extraKnownMarketplaces | keys[]' "$SHARED_SETTINGS" 2>/dev/null))
982
+ if [ ${#mkts[@]} -eq 0 ]; then
983
+ echo -e " \033[33mNo global marketplaces configured.\033[0m"
984
+ read -p " Press Enter..." _
985
+ continue
986
+ fi
987
+ local i=1
988
+ for mkt in "${mkts[@]}"; do
989
+ echo " $i. $mkt"
990
+ ((i++))
991
+ done
992
+ echo ""
993
+ read -p " Pick number to remove: " pick
994
+ local idx=$((pick - 1))
995
+ if [ "$idx" -ge 0 ] 2>/dev/null && [ "$idx" -lt "${#mkts[@]}" ] 2>/dev/null; then
996
+ local toRemove="${mkts[$idx]}"
997
+ jq --arg name "$toRemove" 'del(.extraKnownMarketplaces[$name])' \
998
+ "$SHARED_SETTINGS" > "$SHARED_SETTINGS.tmp" && mv "$SHARED_SETTINGS.tmp" "$SHARED_SETTINGS"
999
+ echo -e " \033[32mRemoved '$toRemove' from global marketplaces.\033[0m"
1000
+ else
1001
+ echo -e " \033[31mInvalid.\033[0m"
1002
+ fi
1003
+ read -p " Press Enter..." _
1004
+ ;;
1005
+ 3)
1006
+ echo ""
1007
+ echo -e " \033[36mSyncing marketplace indexes to all accounts...\033[0m"
1008
+ sync_all_accounts
1009
+ echo -e " \033[32mDone.\033[0m"
1010
+ read -p " Press Enter..." _
1011
+ ;;
1012
+ 4)
1013
+ show_header
1014
+ echo -e "\033[36mPULL MARKETPLACE INDEXES FROM ACCOUNTS\033[0m"
1015
+ echo ""
1016
+ echo -e " \033[90mCopies downloaded marketplace indexes into the shared dir.\033[0m"
1017
+ echo ""
1018
+ local mkts=($(get_all_marketplace_names))
1019
+ local pulled=0
1020
+ for mkt in "${mkts[@]}"; do
1021
+ local destDir="$SHARED_MARKETPLACES_DIR/$mkt"
1022
+ mkdir -p "$destDir"
1023
+ local found=false
1024
+ local accounts=($(get_accounts))
1025
+ for acc in "${accounts[@]}"; do
1026
+ local src="$HOME/.$acc/plugins/marketplaces/$mkt"
1027
+ if [ -d "$src" ]; then
1028
+ cp -r "$src"/* "$destDir/" 2>/dev/null
1029
+ echo -e " \033[32mPulled: $mkt (from $acc)\033[0m"
1030
+ found=true
1031
+ ((pulled++))
1032
+ break
1033
+ fi
1034
+ done
1035
+ $found || echo -e " \033[33mNot found locally: $mkt\033[0m"
1036
+ done
1037
+ [ "$pulled" -eq 0 ] && echo -e " \033[33mNothing pulled.\033[0m"
1038
+ read -p " Press Enter..." _
1039
+ ;;
1040
+ 0) return ;;
1041
+ *) echo -e " \033[31mInvalid option.\033[0m"; sleep 1 ;;
1042
+ esac
1043
+ done
1044
+ }
1045
+
1046
+ manage_plugins() {
1047
+ check_jq || return
1048
+ while true; do
1049
+ show_header
1050
+ echo -e "\033[35mPLUGINS & MARKETPLACE\033[0m"
1051
+ echo ""
1052
+
1053
+ # Show summary
1054
+ local sharedCount=$(jq '.enabledPlugins | length' "$SHARED_SETTINGS" 2>/dev/null || echo 0)
1055
+ echo -e " \033[36mEnabled Plugins:\033[0m"
1056
+ echo " Universal (all accounts) : $sharedCount"
1057
+ echo ""
1058
+
1059
+ if [ "$sharedCount" -gt 0 ]; then
1060
+ jq -r '.enabledPlugins | keys[]' "$SHARED_SETTINGS" 2>/dev/null | while read -r key; do
1061
+ echo -e " \033[32m$key [ALL]\033[0m"
1062
+ done
1063
+ echo ""
1064
+ fi
1065
+
1066
+ echo -e "\033[36m======================================\033[0m"
1067
+ echo -e " \033[32m1. Enable plugin for ALL accounts\033[0m"
1068
+ echo " 2. Enable plugin for one account"
1069
+ echo " 3. Disable plugin (shared)"
1070
+ echo " 4. Disable plugin (one account)"
1071
+ echo -e " \033[36m5. Browse marketplace plugins\033[0m"
1072
+ echo -e " \033[33m6. Marketplace Management\033[0m"
1073
+ echo " 0. Back"
1074
+ echo -e "\033[36m======================================\033[0m"
1075
+ echo ""
1076
+
1077
+ read -p " Pick an option: " choice
1078
+ case "$choice" in
1079
+ 1)
1080
+ show_header
1081
+ echo -e "\033[32mENABLE PLUGIN FOR ALL ACCOUNTS\033[0m"
1082
+ echo ""
1083
+ echo -e " \033[90mFormat: plugin-name@marketplace-name\033[0m"
1084
+ echo -e " \033[90mExample: frontend-design@claude-plugins-official\033[0m"
1085
+ echo ""
1086
+ read -p " Plugin key (name@marketplace): " pluginKey
1087
+ pluginKey=$(echo "$pluginKey" | xargs)
1088
+ [ -z "$pluginKey" ] && { echo -e " \033[90mCancelled.\033[0m"; read -p " Press Enter..." _; continue; }
1089
+ jq --arg key "$pluginKey" '.enabledPlugins[$key] = true' \
1090
+ "$SHARED_SETTINGS" > "$SHARED_SETTINGS.tmp" && mv "$SHARED_SETTINGS.tmp" "$SHARED_SETTINGS"
1091
+ echo ""
1092
+ echo -e " \033[32m'$pluginKey' enabled for ALL accounts.\033[0m"
1093
+ echo -e " \033[36mSyncing to all accounts now...\033[0m"
1094
+ sync_all_accounts
1095
+ echo -e " \033[32mDone. Launch any account to activate.\033[0m"
1096
+ read -p " Press Enter..." _
1097
+ ;;
1098
+ 2)
1099
+ show_header
1100
+ echo -e "\033[32mENABLE PLUGIN FOR ONE ACCOUNT\033[0m"
1101
+ echo ""
1102
+ show_accounts
1103
+ local accounts=($(get_accounts))
1104
+ [ ${#accounts[@]} -eq 0 ] && { read -p " Press Enter..." _; continue; }
1105
+ local selected=$(pick_account "Pick account")
1106
+ [ -z "$selected" ] && { read -p " Press Enter..." _; continue; }
1107
+ echo ""
1108
+ read -p " Plugin key (name@marketplace): " pluginKey
1109
+ pluginKey=$(echo "$pluginKey" | xargs)
1110
+ [ -z "$pluginKey" ] && { echo -e " \033[90mCancelled.\033[0m"; read -p " Press Enter..." _; continue; }
1111
+ local settingsPath="$HOME/.$selected/settings.json"
1112
+ mkdir -p "$HOME/.$selected"
1113
+ if [ -f "$settingsPath" ]; then
1114
+ jq --arg key "$pluginKey" '.enabledPlugins[$key] = true' "$settingsPath" > "$settingsPath.tmp" && mv "$settingsPath.tmp" "$settingsPath"
1115
+ else
1116
+ echo "{\"enabledPlugins\":{\"$pluginKey\":true}}" > "$settingsPath"
1117
+ fi
1118
+ echo -e " \033[32m'$pluginKey' enabled for $selected.\033[0m"
1119
+ read -p " Press Enter..." _
1120
+ ;;
1121
+ 3)
1122
+ show_header
1123
+ echo -e "\033[31mDISABLE PLUGIN (SHARED)\033[0m"
1124
+ echo ""
1125
+ local keys=($(jq -r '.enabledPlugins | keys[]' "$SHARED_SETTINGS" 2>/dev/null))
1126
+ if [ ${#keys[@]} -eq 0 ]; then
1127
+ echo -e " \033[33mNo shared plugins configured.\033[0m"
1128
+ read -p " Press Enter..." _
1129
+ continue
1130
+ fi
1131
+ local i=1
1132
+ for key in "${keys[@]}"; do
1133
+ echo " $i. $key"
1134
+ ((i++))
1135
+ done
1136
+ echo ""
1137
+ read -p " Pick number to disable: " pick
1138
+ local idx=$((pick - 1))
1139
+ if [ "$idx" -ge 0 ] 2>/dev/null && [ "$idx" -lt "${#keys[@]}" ] 2>/dev/null; then
1140
+ local toRemove="${keys[$idx]}"
1141
+ jq --arg key "$toRemove" 'del(.enabledPlugins[$key])' \
1142
+ "$SHARED_SETTINGS" > "$SHARED_SETTINGS.tmp" && mv "$SHARED_SETTINGS.tmp" "$SHARED_SETTINGS"
1143
+ echo -e " \033[32m'$toRemove' removed from shared.\033[0m"
1144
+ read -p " Sync to all accounts now? (y/n): " doSync
1145
+ if [ "$doSync" = "y" ]; then
1146
+ sync_all_accounts
1147
+ echo -e " \033[32mSynced.\033[0m"
1148
+ fi
1149
+ else
1150
+ echo -e " \033[31mInvalid.\033[0m"
1151
+ fi
1152
+ read -p " Press Enter..." _
1153
+ ;;
1154
+ 4)
1155
+ show_header
1156
+ echo -e "\033[31mDISABLE PLUGIN (ONE ACCOUNT)\033[0m"
1157
+ echo ""
1158
+ show_accounts
1159
+ local accounts=($(get_accounts))
1160
+ [ ${#accounts[@]} -eq 0 ] && { read -p " Press Enter..." _; continue; }
1161
+ local selected=$(pick_account "Pick account")
1162
+ [ -z "$selected" ] && { read -p " Press Enter..." _; continue; }
1163
+ local settingsPath="$HOME/.$selected/settings.json"
1164
+ if [ ! -f "$settingsPath" ]; then
1165
+ echo -e " \033[33mNo settings for $selected.\033[0m"
1166
+ read -p " Press Enter..." _
1167
+ continue
1168
+ fi
1169
+ local keys=($(jq -r '.enabledPlugins | keys[]' "$settingsPath" 2>/dev/null))
1170
+ if [ ${#keys[@]} -eq 0 ]; then
1171
+ echo -e " \033[33mNo plugins configured for $selected.\033[0m"
1172
+ read -p " Press Enter..." _
1173
+ continue
1174
+ fi
1175
+ local i=1
1176
+ for key in "${keys[@]}"; do
1177
+ echo " $i. $key"
1178
+ ((i++))
1179
+ done
1180
+ echo ""
1181
+ read -p " Pick number: " pick
1182
+ local idx=$((pick - 1))
1183
+ if [ "$idx" -ge 0 ] 2>/dev/null && [ "$idx" -lt "${#keys[@]}" ] 2>/dev/null; then
1184
+ local toRemove="${keys[$idx]}"
1185
+ jq --arg key "$toRemove" 'del(.enabledPlugins[$key])' \
1186
+ "$settingsPath" > "$settingsPath.tmp" && mv "$settingsPath.tmp" "$settingsPath"
1187
+ echo -e " \033[32m'$toRemove' disabled for $selected.\033[0m"
1188
+ else
1189
+ echo -e " \033[31mInvalid.\033[0m"
1190
+ fi
1191
+ read -p " Press Enter..." _
1192
+ ;;
1193
+ 5)
1194
+ show_header
1195
+ echo -e "\033[36mBROWSE MARKETPLACE PLUGINS\033[0m"
1196
+ echo ""
1197
+ local mkts=($(get_all_marketplace_names))
1198
+ if [ ${#mkts[@]} -eq 0 ]; then
1199
+ echo -e " \033[33mNo marketplaces found. Add one via Marketplace Management.\033[0m"
1200
+ read -p " Press Enter..." _
1201
+ continue
1202
+ fi
1203
+ local i=1
1204
+ for mkt in "${mkts[@]}"; do
1205
+ echo " $i. $mkt"
1206
+ ((i++))
1207
+ done
1208
+ echo ""
1209
+ read -p " Pick marketplace: " pick
1210
+ local idx=$((pick - 1))
1211
+ if [ "$idx" -ge 0 ] 2>/dev/null && [ "$idx" -lt "${#mkts[@]}" ] 2>/dev/null; then
1212
+ local selectedMkt="${mkts[$idx]}"
1213
+ show_header
1214
+ echo -e "\033[36mPLUGINS IN $selectedMkt\033[0m"
1215
+ echo ""
1216
+ local result=$(get_marketplace_plugins "$selectedMkt")
1217
+ if [ -z "$result" ]; then
1218
+ echo -e " \033[33mIndex not downloaded. Launch an account with this marketplace\033[0m"
1219
+ echo -e " \033[33mconfigured, then pull indexes via Marketplace Management.\033[0m"
1220
+ else
1221
+ echo "$result" | while read -r line; do
1222
+ if [ "$line" = "PLUGINS:" ]; then
1223
+ echo -e " \033[32mOfficial plugins:\033[0m"
1224
+ elif [ "$line" = "EXTERNAL:" ]; then
1225
+ echo -e " \033[33mExternal/3rd-party:\033[0m"
1226
+ elif [ -n "$line" ]; then
1227
+ echo " - $line"
1228
+ fi
1229
+ done
1230
+ fi
1231
+ else
1232
+ echo -e " \033[31mInvalid.\033[0m"
1233
+ fi
1234
+ echo ""
1235
+ read -p " Press Enter..." _
1236
+ ;;
1237
+ 6) manage_marketplaces ;;
1238
+ 0) return ;;
1239
+ *) echo -e " \033[31mInvalid option.\033[0m"; sleep 1 ;;
1240
+ esac
1241
+ done
1242
+ }
1243
+
1244
+ # ─── MENU ───────────────────────────────────────────────────────────────────
1245
+
1246
+ show_menu() {
1247
+ show_header
1248
+ echo -e " \033[36mCurrent Accounts:\033[0m"
1249
+ echo ""
1250
+ show_accounts
1251
+ echo ""
1252
+ echo -e "\033[36m======================================\033[0m"
1253
+ echo " 1. List Accounts"
1254
+ echo " 2. Create New Account"
1255
+ echo " 3. Launch Account"
1256
+ echo " 4. Rename Account"
1257
+ echo -e " \033[31m5. Delete Account\033[0m"
1258
+ echo " 6. Backup Sessions"
1259
+ echo " 7. Restore Sessions"
1260
+ echo -e " \033[33m8. Shared Settings (MCP/Skills)\033[0m"
1261
+ echo -e " \033[35m9. Plugins & Marketplace\033[0m"
1262
+ echo -e " \033[32mE. Export Profile (Pair Code)\033[0m"
1263
+ echo -e " \033[32mI. Import Profile (Pair Code)\033[0m"
1264
+ echo " 0. Exit"
1265
+ echo -e "\033[36m======================================\033[0m"
1266
+ echo ""
1267
+ }
1268
+
1269
+ show_help() {
1270
+ show_header
1271
+ echo -e "\033[36mHELP — Claude Account Manager\033[0m"
1272
+ echo ""
1273
+ echo -e " \033[37m1-5. Account Management\033[0m"
1274
+ echo -e " \033[90m Create, launch, rename, and delete Claude CLI accounts.\033[0m"
1275
+ echo -e " \033[90m Each account gets its own isolated config directory.\033[0m"
1276
+ echo ""
1277
+ echo -e " \033[37m6-7. Backup & Restore\033[0m"
1278
+ echo -e " \033[90m Create timestamped local backups of all accounts and settings.\033[0m"
1279
+ echo ""
1280
+ echo -e " \033[37m8. Shared Settings\033[0m"
1281
+ echo -e " \033[90m Define MCP servers, env vars, and CLAUDE.md instructions\033[0m"
1282
+ echo -e " \033[90m that automatically apply to ALL accounts on launch.\033[0m"
1283
+ echo ""
1284
+ echo -e " \033[37m9. Plugins & Marketplace\033[0m"
1285
+ echo -e " \033[90m Enable/disable plugins globally or per-account.\033[0m"
1286
+ echo -e " \033[90m Browse and manage marketplace indexes.\033[0m"
1287
+ echo ""
1288
+ echo -e " \033[37mE. Export Profile (Pair Code)\033[0m"
1289
+ echo -e " \033[90m Send a single account to another machine. You get a short code\033[0m"
1290
+ echo -e " \033[90m like A7X4B-K9M4PX — the other person enters it using 'I'.\033[0m"
1291
+ echo -e " \033[90m The code expires in 10 minutes and works only once.\033[0m"
1292
+ echo ""
1293
+ echo -e " \033[37mI. Import Profile (Pair Code)\033[0m"
1294
+ echo -e " \033[90m Receive an account from someone else. Enter the pairing code\033[0m"
1295
+ echo -e " \033[90m they gave you and the account appears on your machine.\033[0m"
1296
+ echo ""
1297
+ read -p " Press Enter..." _
1298
+ }
1299
+
1300
+ while true; do
1301
+ show_menu
1302
+ read -p " Pick an option: " choice
1303
+ case "$choice" in
1304
+ 1) show_header; echo -e "\033[36mAll Accounts:\033[0m"; echo ""; show_accounts; echo ""; read -p " Press Enter..." _ ;;
1305
+ 2) create_account ;;
1306
+ 3) launch_account ;;
1307
+ 4) rename_account ;;
1308
+ 5) delete_account ;;
1309
+ 6) backup_sessions ;;
1310
+ 7) restore_sessions ;;
1311
+ 8) manage_shared_settings ;;
1312
+ 9) manage_plugins ;;
1313
+ [eE]) pair_export ;;
1314
+ [iI]) pair_import ;;
1315
+ [hH]) show_help ;;
1316
+ 0) clear; echo -e "\033[36mBye!\033[0m"; break ;;
1317
+ *) echo -e " \033[31mInvalid option.\033[0m"; sleep 1 ;;
1318
+ esac
1319
+ done