claude-manager 1.5.0 → 1.5.1

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/src/cli.js DELETED
@@ -1,770 +0,0 @@
1
- #!/usr/bin/env node
2
- import React, { useState, useEffect } from 'react';
3
- import { render, Box, Text, useInput, useApp } from 'ink';
4
- import SelectInput from 'ink-select-input';
5
- import TextInput from 'ink-text-input';
6
- import fs from 'fs';
7
- import path from 'path';
8
- import os from 'os';
9
- import { execSync, spawnSync } from 'child_process';
10
-
11
- const VERSION = "1.5.0";
12
- const PROFILES_DIR = path.join(os.homedir(), '.claude', 'profiles');
13
- const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
14
- const CLAUDE_JSON_PATH = path.join(os.homedir(), '.claude.json');
15
- const LAST_PROFILE_PATH = path.join(os.homedir(), '.claude', '.last-profile');
16
- const MCP_REGISTRY_URL = 'https://registry.modelcontextprotocol.io/v0/servers';
17
-
18
- const args = process.argv.slice(2);
19
- const cmd = args[0];
20
-
21
- // Ensure profiles directory exists
22
- if (!fs.existsSync(PROFILES_DIR)) fs.mkdirSync(PROFILES_DIR, { recursive: true });
23
-
24
- // CLI flags
25
- if (args.includes('-v') || args.includes('--version')) {
26
- console.log(`cm v${VERSION}`);
27
- process.exit(0);
28
- }
29
-
30
- if (args.includes('-h') || args.includes('--help')) {
31
- console.log(`cm v${VERSION} - Claude Settings Manager
32
-
33
- Usage: cm [command] [options]
34
-
35
- Commands:
36
- (none) Select profile interactively
37
- new Create a new profile
38
- edit <n> Edit profile (by name or number)
39
- delete <n> Delete profile (by name or number)
40
- status Show current settings
41
- list List all profiles
42
- mcp [query] Search and add MCP servers
43
- skills Browse and add Anthropic skills
44
-
45
- Options:
46
- --last, -l Use last profile without menu
47
- --skip-update Skip update check
48
- --yolo Run claude with --dangerously-skip-permissions
49
- -v, --version Show version
50
- -h, --help Show help`);
51
- process.exit(0);
52
- }
53
-
54
- const skipUpdate = args.includes('--skip-update');
55
- const useLast = args.includes('--last') || args.includes('-l');
56
- const dangerMode = args.includes('--dangerously-skip-permissions') || args.includes('--yolo');
57
-
58
- // Helper functions
59
- const loadProfiles = () => {
60
- const profiles = [];
61
- if (fs.existsSync(PROFILES_DIR)) {
62
- for (const file of fs.readdirSync(PROFILES_DIR).sort()) {
63
- if (file.endsWith('.json')) {
64
- try {
65
- const content = JSON.parse(fs.readFileSync(path.join(PROFILES_DIR, file), 'utf8'));
66
- profiles.push({
67
- label: content.name || file.replace('.json', ''),
68
- value: file,
69
- key: file,
70
- group: content.group || null,
71
- data: content,
72
- });
73
- } catch {}
74
- }
75
- }
76
- }
77
- return profiles;
78
- };
79
-
80
- const applyProfile = (filename) => {
81
- const profilePath = path.join(PROFILES_DIR, filename);
82
- const profile = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
83
- const { name, group, mcpServers, ...settings } = profile;
84
-
85
- // Write settings.json
86
- fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
87
-
88
- // Update MCP servers in .claude.json if specified
89
- if (mcpServers !== undefined) {
90
- try {
91
- const claudeJson = fs.existsSync(CLAUDE_JSON_PATH)
92
- ? JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, 'utf8'))
93
- : {};
94
- claudeJson.mcpServers = mcpServers;
95
- fs.writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2));
96
- } catch {}
97
- }
98
-
99
- fs.writeFileSync(LAST_PROFILE_PATH, filename);
100
- return name || filename;
101
- };
102
-
103
- const getLastProfile = () => {
104
- try { return fs.readFileSync(LAST_PROFILE_PATH, 'utf8').trim(); } catch { return null; }
105
- };
106
-
107
- const checkProjectProfile = () => {
108
- const localProfile = path.join(process.cwd(), '.claude-profile');
109
- if (fs.existsSync(localProfile)) {
110
- return fs.readFileSync(localProfile, 'utf8').trim();
111
- }
112
- return null;
113
- };
114
-
115
- const checkForUpdate = () => {
116
- if (skipUpdate) return { needsUpdate: false };
117
- try {
118
- const current = execSync('claude --version 2>/dev/null', { encoding: 'utf8' }).match(/(\d+\.\d+\.\d+)/)?.[1];
119
- const output = execSync('brew outdated claude-code 2>&1 || true', { encoding: 'utf8' }).trim();
120
- return { current, needsUpdate: output.includes('claude-code') };
121
- } catch {
122
- return { needsUpdate: false };
123
- }
124
- };
125
-
126
- const launchClaude = () => {
127
- try {
128
- const claudeArgs = dangerMode ? '--dangerously-skip-permissions' : '';
129
- execSync(`claude ${claudeArgs}`, { stdio: 'inherit' });
130
- } catch (e) {
131
- process.exit(e.status || 1);
132
- }
133
- process.exit(0);
134
- };
135
-
136
- // Handle --last flag
137
- if (useLast) {
138
- const last = getLastProfile();
139
- if (last && fs.existsSync(path.join(PROFILES_DIR, last))) {
140
- const name = applyProfile(last);
141
- console.log(`\x1b[32m✓\x1b[0m Applied: ${name}\n`);
142
- launchClaude();
143
- } else {
144
- console.log('\x1b[31mNo last profile found\x1b[0m');
145
- process.exit(1);
146
- }
147
- }
148
-
149
- // Handle project profile
150
- const projectProfile = checkProjectProfile();
151
- if (projectProfile && !cmd) {
152
- const profiles = loadProfiles();
153
- const match = profiles.find(p => p.label === projectProfile || p.value === projectProfile + '.json');
154
- if (match) {
155
- console.log(`\x1b[36mUsing project profile: ${match.label}\x1b[0m`);
156
- applyProfile(match.value);
157
- launchClaude();
158
- }
159
- }
160
-
161
- // Handle commands
162
- if (cmd === 'status') {
163
- const last = getLastProfile();
164
- const profiles = loadProfiles();
165
- const current = profiles.find(p => p.value === last);
166
- console.log(`\x1b[1m\x1b[36mClaude Settings Manager v${VERSION}\x1b[0m`);
167
- console.log(`─────────────────────────`);
168
- if (current) {
169
- console.log(`Current profile: \x1b[32m${current.label}\x1b[0m`);
170
- console.log(`Model: ${current.data.env?.ANTHROPIC_MODEL || 'default'}`);
171
- console.log(`Provider: ${current.data.env?.ANTHROPIC_BASE_URL || 'Anthropic Direct'}`);
172
- const mcpServers = current.data.mcpServers || {};
173
- if (Object.keys(mcpServers).length > 0) {
174
- console.log(`\nProfile MCP Servers (${Object.keys(mcpServers).length}):`);
175
- Object.keys(mcpServers).forEach(s => console.log(` - ${s}`));
176
- }
177
- } else {
178
- console.log('No profile active');
179
- }
180
- // Show installed skills from ~/.claude/skills/
181
- const skillsDir = path.join(os.homedir(), '.claude', 'skills');
182
- try {
183
- if (fs.existsSync(skillsDir)) {
184
- const installedSkills = fs.readdirSync(skillsDir).filter(f => {
185
- const p = path.join(skillsDir, f);
186
- return fs.statSync(p).isDirectory() && !f.startsWith('.');
187
- });
188
- if (installedSkills.length > 0) {
189
- console.log(`\nInstalled Skills (${installedSkills.length}):`);
190
- installedSkills.forEach(s => console.log(` - ${s}`));
191
- }
192
- }
193
- } catch {}
194
- // Show global MCP servers from ~/.claude.json
195
- try {
196
- const claudeJson = JSON.parse(fs.readFileSync(CLAUDE_JSON_PATH, 'utf8'));
197
- const globalMcp = claudeJson.mcpServers || {};
198
- if (Object.keys(globalMcp).length > 0) {
199
- console.log(`\nGlobal MCP Servers (${Object.keys(globalMcp).length}):`);
200
- Object.keys(globalMcp).forEach(s => console.log(` - ${s}`));
201
- }
202
- } catch {}
203
- try {
204
- const ver = execSync('claude --version 2>/dev/null', { encoding: 'utf8' }).trim();
205
- console.log(`\nClaude: ${ver}`);
206
- } catch {}
207
- process.exit(0);
208
- }
209
-
210
- if (cmd === 'list') {
211
- const profiles = loadProfiles();
212
- console.log(`\x1b[1m\x1b[36mProfiles\x1b[0m (${profiles.length})`);
213
- console.log(`─────────────────────────`);
214
- profiles.forEach((p, i) => {
215
- const group = p.group ? `\x1b[33m[${p.group}]\x1b[0m ` : '';
216
- console.log(`${i + 1}. ${group}${p.label}`);
217
- });
218
- process.exit(0);
219
- }
220
-
221
- if (cmd === 'delete') {
222
- const profiles = loadProfiles();
223
- const target = args[1];
224
- const idx = parseInt(target) - 1;
225
- const match = profiles[idx] || profiles.find(p => p.label.toLowerCase() === target?.toLowerCase());
226
- if (match) {
227
- fs.unlinkSync(path.join(PROFILES_DIR, match.value));
228
- console.log(`\x1b[32m✓\x1b[0m Deleted: ${match.label}`);
229
- } else {
230
- console.log(`\x1b[31mProfile not found: ${target}\x1b[0m`);
231
- }
232
- process.exit(0);
233
- }
234
-
235
- if (cmd === 'edit') {
236
- const profiles = loadProfiles();
237
- const target = args[1];
238
- const idx = parseInt(target) - 1;
239
- const match = profiles[idx] || profiles.find(p => p.label.toLowerCase() === target?.toLowerCase());
240
- if (match) {
241
- const editor = process.env.EDITOR || 'nano';
242
- spawnSync(editor, [path.join(PROFILES_DIR, match.value)], { stdio: 'inherit' });
243
- } else {
244
- console.log(`\x1b[31mProfile not found: ${target}\x1b[0m`);
245
- }
246
- process.exit(0);
247
- }
248
-
249
- // MCP server search and add
250
- const searchMcpServers = (query) => {
251
- try {
252
- const res = execSync(`curl -s "${MCP_REGISTRY_URL}?limit=100"`, { encoding: 'utf8', timeout: 10000 });
253
- const data = JSON.parse(res);
254
- const seen = new Set();
255
- return data.servers.filter(s => {
256
- if (seen.has(s.server.name)) return false;
257
- seen.add(s.server.name);
258
- const isLatest = s._meta?.['io.modelcontextprotocol.registry/official']?.isLatest !== false;
259
- const matchesQuery = !query ||
260
- s.server.name.toLowerCase().includes(query.toLowerCase()) ||
261
- s.server.description?.toLowerCase().includes(query.toLowerCase());
262
- return isLatest && matchesQuery;
263
- }).slice(0, 15);
264
- } catch {
265
- return [];
266
- }
267
- };
268
-
269
- const addMcpToProfile = (server, profileFile) => {
270
- const profilePath = path.join(PROFILES_DIR, profileFile);
271
- const profile = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
272
- if (!profile.mcpServers) profile.mcpServers = {};
273
-
274
- const s = server.server;
275
- const name = s.name.split('/').pop();
276
-
277
- if (s.remotes?.[0]) {
278
- const remote = s.remotes[0];
279
- profile.mcpServers[name] = {
280
- type: remote.type === 'streamable-http' ? 'http' : remote.type,
281
- url: remote.url,
282
- };
283
- } else if (s.packages?.[0]) {
284
- const pkg = s.packages[0];
285
- if (pkg.registryType === 'npm') {
286
- profile.mcpServers[name] = {
287
- type: 'stdio',
288
- command: 'npx',
289
- args: ['-y', pkg.identifier],
290
- };
291
- } else if (pkg.registryType === 'pypi') {
292
- profile.mcpServers[name] = {
293
- type: 'stdio',
294
- command: 'uvx',
295
- args: [pkg.identifier],
296
- };
297
- }
298
- }
299
-
300
- fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2));
301
- return name;
302
- };
303
-
304
- const McpSearch = () => {
305
- const { exit } = useApp();
306
- const [step, setStep] = useState(args[1] ? 'loading' : 'search');
307
- const [query, setQuery] = useState(args[1] || '');
308
- const [servers, setServers] = useState([]);
309
- const [selectedServer, setSelectedServer] = useState(null);
310
- const profiles = loadProfiles();
311
-
312
- useEffect(() => {
313
- if (args[1] && step === 'loading') {
314
- const results = searchMcpServers(args[1]);
315
- setServers(results);
316
- setStep('results');
317
- }
318
- }, []);
319
-
320
- const doSearch = () => {
321
- const results = searchMcpServers(query);
322
- setServers(results);
323
- setStep('results');
324
- };
325
-
326
- const serverItems = servers.map(s => ({
327
- label: `${s.server.name} - ${s.server.description?.slice(0, 50) || ''}`,
328
- value: s,
329
- key: s.server.name + s.server.version,
330
- }));
331
-
332
- const profileItems = profiles.map(p => ({ label: p.label, value: p.value, key: p.key }));
333
-
334
- if (step === 'search') {
335
- return (
336
- <Box flexDirection="column" padding={1}>
337
- <Text bold color="cyan">MCP Server Search</Text>
338
- <Text dimColor>─────────────────────────</Text>
339
- <Box marginTop={1}>
340
- <Text>Search: </Text>
341
- <TextInput value={query} onChange={setQuery} onSubmit={doSearch} />
342
- </Box>
343
- </Box>
344
- );
345
- }
346
-
347
- if (step === 'loading') {
348
- return <Box padding={1}><Text>Searching MCP registry...</Text></Box>;
349
- }
350
-
351
- if (step === 'results') {
352
- if (servers.length === 0) {
353
- return (
354
- <Box flexDirection="column" padding={1}>
355
- <Text color="yellow">No servers found for "{query}"</Text>
356
- </Box>
357
- );
358
- }
359
- return (
360
- <Box flexDirection="column" padding={1}>
361
- <Text bold color="cyan">MCP Servers</Text>
362
- <Text dimColor>─────────────────────────</Text>
363
- <Text dimColor>Found {servers.length} servers</Text>
364
- <Box flexDirection="column" marginTop={1}>
365
- <SelectInput
366
- items={serverItems}
367
- onSelect={(item) => { setSelectedServer(item.value); setStep('profile'); }}
368
- limit={10}
369
- />
370
- </Box>
371
- </Box>
372
- );
373
- }
374
-
375
- if (step === 'profile') {
376
- return (
377
- <Box flexDirection="column" padding={1}>
378
- <Text bold color="cyan">Add to Profile</Text>
379
- <Text dimColor>─────────────────────────</Text>
380
- <Text>Server: {selectedServer.server.name}</Text>
381
- <Box flexDirection="column" marginTop={1}>
382
- <Text>Select profile:</Text>
383
- <SelectInput
384
- items={profileItems}
385
- onSelect={(item) => {
386
- const name = addMcpToProfile(selectedServer, item.value);
387
- console.log(`\n\x1b[32m✓\x1b[0m Added ${name} to ${item.label}`);
388
- exit();
389
- }}
390
- />
391
- </Box>
392
- </Box>
393
- );
394
- }
395
-
396
- return null;
397
- };
398
-
399
- // Skills browser
400
- const SKILL_SOURCES = [
401
- { url: 'https://api.github.com/repos/anthropics/skills/contents/skills', base: 'https://github.com/anthropics/skills/tree/main/skills' },
402
- { url: 'https://api.github.com/repos/Prat011/awesome-llm-skills/contents/skills', base: 'https://github.com/Prat011/awesome-llm-skills/tree/main/skills' },
403
- { url: 'https://api.github.com/repos/skillcreatorai/Ai-Agent-Skills/contents/skills', base: 'https://github.com/skillcreatorai/Ai-Agent-Skills/tree/main/skills' },
404
- ];
405
-
406
- const fetchSkills = () => {
407
- const seen = new Set();
408
- const skills = [];
409
- for (const source of SKILL_SOURCES) {
410
- try {
411
- const res = execSync(`curl -s "${source.url}"`, { encoding: 'utf8', timeout: 10000 });
412
- const data = JSON.parse(res);
413
- if (Array.isArray(data)) {
414
- for (const s of data.filter(s => s.type === 'dir')) {
415
- if (!seen.has(s.name)) {
416
- seen.add(s.name);
417
- skills.push({
418
- label: s.name,
419
- value: `${source.base}/${s.name}`,
420
- key: s.name,
421
- });
422
- }
423
- }
424
- }
425
- } catch {}
426
- }
427
- return skills.sort((a, b) => a.label.localeCompare(b.label));
428
- };
429
-
430
- const SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills');
431
-
432
- const addSkillToClaudeJson = (skillName, skillUrl) => {
433
- try {
434
- // Ensure skills directory exists
435
- if (!fs.existsSync(SKILLS_DIR)) fs.mkdirSync(SKILLS_DIR, { recursive: true });
436
-
437
- const skillPath = path.join(SKILLS_DIR, skillName);
438
- if (fs.existsSync(skillPath)) {
439
- return { success: false, message: 'Skill already installed' };
440
- }
441
-
442
- // Convert GitHub URL to clone-friendly format
443
- // https://github.com/anthropics/skills/tree/main/skills/frontend-design
444
- // -> git clone with sparse checkout
445
- const match = skillUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/);
446
- if (!match) return { success: false, message: 'Invalid skill URL' };
447
-
448
- const [, owner, repo, branch, skillSubPath] = match;
449
- const tempDir = `/tmp/skill-clone-${Date.now()}`;
450
-
451
- // Sparse clone just the skill folder
452
- execSync(`git clone --depth 1 --filter=blob:none --sparse "https://github.com/${owner}/${repo}.git" "${tempDir}" 2>/dev/null`, { timeout: 30000 });
453
- execSync(`cd "${tempDir}" && git sparse-checkout set "${skillSubPath}" 2>/dev/null`, { timeout: 10000 });
454
-
455
- // Move skill to destination
456
- execSync(`mv "${tempDir}/${skillSubPath}" "${skillPath}"`, { timeout: 5000 });
457
- execSync(`rm -rf "${tempDir}"`, { timeout: 5000 });
458
-
459
- return { success: true };
460
- } catch (e) {
461
- return { success: false, message: 'Failed to download skill' };
462
- }
463
- };
464
-
465
- const SkillsBrowser = () => {
466
- const { exit } = useApp();
467
- const [skills, setSkills] = useState([]);
468
- const [loading, setLoading] = useState(true);
469
-
470
- useEffect(() => {
471
- const s = fetchSkills();
472
- setSkills(s);
473
- setLoading(false);
474
- }, []);
475
-
476
- if (loading) {
477
- return <Box padding={1}><Text>Loading skills...</Text></Box>;
478
- }
479
-
480
- if (skills.length === 0) {
481
- return (
482
- <Box flexDirection="column" padding={1}>
483
- <Text color="yellow">Could not fetch skills</Text>
484
- </Box>
485
- );
486
- }
487
-
488
- return (
489
- <Box flexDirection="column" padding={1}>
490
- <Text bold color="cyan">Anthropic Skills</Text>
491
- <Text dimColor>─────────────────────────</Text>
492
- <Text dimColor>Found {skills.length} skills</Text>
493
- <Box flexDirection="column" marginTop={1}>
494
- <SelectInput
495
- items={skills}
496
- onSelect={(item) => {
497
- const result = addSkillToClaudeJson(item.label, item.value);
498
- if (result.success) {
499
- console.log(`\n\x1b[32m✓\x1b[0m Installed skill: ${item.label}`);
500
- console.log(`\x1b[36mLocation: ~/.claude/skills/${item.label}/\x1b[0m`);
501
- } else {
502
- console.log(`\n\x1b[31m✗\x1b[0m ${result.message || 'Failed to install skill'}`);
503
- }
504
- exit();
505
- }}
506
- />
507
- </Box>
508
- </Box>
509
- );
510
- };
511
-
512
- if (cmd === 'skills') {
513
- render(<SkillsBrowser />);
514
- } else if (cmd === 'mcp') {
515
- render(<McpSearch />);
516
- } else if (cmd === 'new') {
517
- // New profile wizard
518
- const NewProfileWizard = () => {
519
- const { exit } = useApp();
520
- const [step, setStep] = useState('name');
521
- const [name, setName] = useState('');
522
- const [provider, setProvider] = useState('');
523
- const [apiKey, setApiKey] = useState('');
524
- const [model, setModel] = useState('');
525
- const [group, setGroup] = useState('');
526
-
527
- const providers = [
528
- { label: 'Anthropic (Direct)', value: 'anthropic', url: '', needsKey: true },
529
- { label: 'Amazon Bedrock', value: 'bedrock', url: '', needsKey: false },
530
- { label: 'Z.AI', value: 'zai', url: 'https://api.z.ai/api/anthropic', needsKey: true },
531
- { label: 'MiniMax', value: 'minimax', url: 'https://api.minimax.io/anthropic', needsKey: true },
532
- { label: 'Custom', value: 'custom', url: '', needsKey: true },
533
- ];
534
-
535
- const handleSave = () => {
536
- const prov = providers.find(p => p.value === provider);
537
- const profile = {
538
- name,
539
- group: group || undefined,
540
- env: {
541
- ...(apiKey && { ANTHROPIC_AUTH_TOKEN: apiKey }),
542
- ...(model && { ANTHROPIC_MODEL: model }),
543
- ...(prov?.url && { ANTHROPIC_BASE_URL: prov.url }),
544
- API_TIMEOUT_MS: '3000000',
545
- },
546
- model: 'opus',
547
- alwaysThinkingEnabled: true,
548
- defaultMode: 'bypassPermissions',
549
- };
550
- const filename = name.toLowerCase().replace(/\s+/g, '-') + '.json';
551
- fs.writeFileSync(path.join(PROFILES_DIR, filename), JSON.stringify(profile, null, 2));
552
- console.log(`\n\x1b[32m✓\x1b[0m Created: ${name}`);
553
- exit();
554
- };
555
-
556
- const handleProviderSelect = (item) => {
557
- setProvider(item.value);
558
- const prov = providers.find(p => p.value === item.value);
559
- setStep(prov.needsKey ? 'apikey' : 'model');
560
- };
561
-
562
- return (
563
- <Box flexDirection="column" padding={1}>
564
- <Text bold color="cyan">New Profile</Text>
565
- <Text dimColor>─────────────────────────</Text>
566
-
567
- {step === 'name' && (
568
- <Box marginTop={1}>
569
- <Text>Name: </Text>
570
- <TextInput value={name} onChange={setName} onSubmit={() => setStep('provider')} />
571
- </Box>
572
- )}
573
-
574
- {step === 'provider' && (
575
- <Box flexDirection="column" marginTop={1}>
576
- <Text>Provider:</Text>
577
- <SelectInput items={providers} onSelect={handleProviderSelect} />
578
- </Box>
579
- )}
580
-
581
- {step === 'apikey' && (
582
- <Box marginTop={1}>
583
- <Text>API Key: </Text>
584
- <TextInput value={apiKey} onChange={setApiKey} onSubmit={() => setStep('model')} mask="*" />
585
- </Box>
586
- )}
587
-
588
- {step === 'model' && (
589
- <Box marginTop={1}>
590
- <Text>Model ID (optional): </Text>
591
- <TextInput value={model} onChange={setModel} onSubmit={() => setStep('group')} />
592
- </Box>
593
- )}
594
-
595
- {step === 'group' && (
596
- <Box marginTop={1}>
597
- <Text>Group (optional): </Text>
598
- <TextInput value={group} onChange={setGroup} onSubmit={handleSave} />
599
- </Box>
600
- )}
601
- </Box>
602
- );
603
- };
604
- render(<NewProfileWizard />);
605
- } else {
606
- // Loading animation component
607
- const LoadingScreen = ({ message = 'Loading...' }) => {
608
- const [dots, setDots] = useState('');
609
- const [colorIdx, setColorIdx] = useState(0);
610
- const colors = ['cyan', 'blue', 'magenta', 'red', 'yellow', 'green'];
611
-
612
- useEffect(() => {
613
- const dotsInterval = setInterval(() => {
614
- setDots(d => d.length >= 3 ? '' : d + '.');
615
- }, 500);
616
- const colorInterval = setInterval(() => {
617
- setColorIdx(i => (i + 1) % colors.length);
618
- }, 200);
619
- return () => { clearInterval(dotsInterval); clearInterval(colorInterval); };
620
- }, []);
621
-
622
- return (
623
- <Box flexDirection="column" padding={1}>
624
- <Text bold color={colors[colorIdx]}>
625
- {`██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗
626
- ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝
627
- ██║ ██║ ███████║██║ ██║██║ ██║█████╗
628
- ██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝
629
- ╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗
630
- ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝`}
631
- </Text>
632
- <Text bold color={colors[(colorIdx + 3) % colors.length]}>MANAGER v{VERSION}</Text>
633
- <Text color="yellow" marginTop={1}>{message}{dots}</Text>
634
- </Box>
635
- );
636
- };
637
-
638
- // Main app
639
- const App = () => {
640
- const [step, setStep] = useState('select');
641
- const [updateInfo, setUpdateInfo] = useState(null);
642
- const [filter, setFilter] = useState('');
643
- const profiles = loadProfiles();
644
-
645
- useEffect(() => {
646
- // Show loading screen briefly, then go to select
647
- setTimeout(() => setStep('select'), 1500);
648
-
649
- // Check for updates in parallel (non-blocking)
650
- if (!skipUpdate) {
651
- Promise.resolve().then(() => {
652
- const info = checkForUpdate();
653
- setUpdateInfo(info);
654
- });
655
- }
656
- }, []);
657
-
658
- useInput((input, key) => {
659
- if (step === 'select') {
660
- // Number shortcuts
661
- const num = parseInt(input);
662
- if (num >= 1 && num <= 9 && num <= filteredProfiles.length) {
663
- const profile = filteredProfiles[num - 1];
664
- applyProfile(profile.value);
665
- console.log(`\n\x1b[32m✓\x1b[0m Applied: ${profile.label}\n`);
666
- launchClaude();
667
- }
668
- // Update shortcut
669
- if (input === 'u' && updateInfo?.needsUpdate) {
670
- console.log('\n\x1b[33mUpdating Claude...\x1b[0m\n');
671
- try {
672
- execSync('brew upgrade claude-code', { stdio: 'inherit' });
673
- console.log('\n\x1b[32m✓ Updated!\x1b[0m\n');
674
- setUpdateInfo({ ...updateInfo, needsUpdate: false });
675
- } catch {}
676
- }
677
- // Fuzzy filter
678
- if (input.match(/^[a-zA-Z]$/) && input !== 'u') {
679
- setFilter(f => f + input);
680
- }
681
- if (key.backspace || key.delete) {
682
- setFilter(f => f.slice(0, -1));
683
- }
684
- if (key.escape) {
685
- setFilter('');
686
- }
687
- }
688
- });
689
-
690
- // Group and filter profiles
691
- const filteredProfiles = profiles.filter(p =>
692
- !filter || p.label.toLowerCase().includes(filter.toLowerCase())
693
- );
694
-
695
- const groupedItems = [];
696
- const groups = [...new Set(filteredProfiles.map(p => p.group).filter(Boolean))];
697
-
698
- if (groups.length > 0) {
699
- groups.forEach(g => {
700
- groupedItems.push({ label: `── ${g} ──`, value: `group-${g}`, key: `group-${g}`, disabled: true });
701
- filteredProfiles.filter(p => p.group === g).forEach((p, i) => {
702
- groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` });
703
- });
704
- });
705
- const ungrouped = filteredProfiles.filter(p => !p.group);
706
- if (ungrouped.length > 0) {
707
- groupedItems.push({ label: '── Other ──', value: 'group-other', key: 'group-other', disabled: true });
708
- ungrouped.forEach((p, i) => groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` }));
709
- }
710
- } else {
711
- filteredProfiles.forEach((p, i) => groupedItems.push({ ...p, label: `${i + 1}. ${p.label}` }));
712
- }
713
-
714
- if (step === 'loading') {
715
- return <LoadingScreen message="Initializing Claude Manager" />;
716
- }
717
-
718
- if (profiles.length === 0) {
719
- return (
720
- <Box flexDirection="column" padding={1}>
721
- <Text bold color="cyan">CLAUDE MANAGER</Text>
722
- <Text dimColor>─────────────────────────</Text>
723
- <Text color="yellow" marginTop={1}>No profiles found!</Text>
724
- <Text>Run: cm new</Text>
725
- </Box>
726
- );
727
- }
728
-
729
- const handleSelect = (item) => {
730
- if (item.disabled) return;
731
- applyProfile(item.value);
732
- console.log(`\n\x1b[32m✓\x1b[0m Applied: ${item.label.replace(/^\d+\.\s*/, '')}\n`);
733
- launchClaude();
734
- };
735
-
736
- return (
737
- <Box flexDirection="column" padding={1}>
738
- <Text bold color="cyan">
739
- {`██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗
740
- ██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝
741
- ██║ ██║ ███████║██║ ██║██║ ██║█████╗
742
- ██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝
743
- ╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗
744
- ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝`}
745
- </Text>
746
- <Text bold color="magenta">MANAGER v{VERSION}</Text>
747
- <Text dimColor>─────────────────────────</Text>
748
- {updateInfo?.current && <Text dimColor>Claude v{updateInfo.current}</Text>}
749
- {updateInfo?.needsUpdate && (
750
- <Text color="yellow">⚠ Update available! Press 'u' to upgrade</Text>
751
- )}
752
- {filter && <Text color="yellow">Filter: {filter}</Text>}
753
- <Box flexDirection="column" marginTop={1}>
754
- <Text>Select Profile: <Text dimColor>(1-9 quick select, type to filter{updateInfo?.needsUpdate ? ', u to update' : ''})</Text></Text>
755
- <SelectInput
756
- items={groupedItems}
757
- onSelect={handleSelect}
758
- itemComponent={({ isSelected, label, disabled }) => (
759
- <Text color={disabled ? 'gray' : isSelected ? 'cyan' : 'white'} dimColor={disabled}>
760
- {disabled ? label : (isSelected ? '❯ ' : ' ') + label}
761
- </Text>
762
- )}
763
- />
764
- </Box>
765
- </Box>
766
- );
767
- };
768
-
769
- render(<App />);
770
- }