@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,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
|