claude-cli-advanced-starter-pack 1.8.3 → 1.8.5

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.
@@ -28,6 +28,8 @@ import {
28
28
  generateMergeExplanation,
29
29
  formatMergeOptions,
30
30
  } from '../utils/smart-merge.js';
31
+ import { registerProject } from '../utils/global-registry.js';
32
+ import { replacePlaceholders } from '../utils/template-engine.js';
31
33
 
32
34
  const __filename = fileURLToPath(import.meta.url);
33
35
  const __dirname = dirname(__filename);
@@ -169,6 +171,18 @@ const AVAILABLE_COMMANDS = [
169
171
  category: 'GitHub',
170
172
  selected: true,
171
173
  },
174
+ {
175
+ name: 'menu-issues-list',
176
+ description: 'Mobile-friendly menu of open GitHub issues',
177
+ category: 'GitHub',
178
+ selected: true,
179
+ },
180
+ {
181
+ name: 'create-task-list-for-issue',
182
+ description: 'Start working on a GitHub issue by number',
183
+ category: 'GitHub',
184
+ selected: true,
185
+ },
172
186
  {
173
187
  name: 'phase-dev-plan',
174
188
  description: 'Create phased development plans (95%+ success rate)',
@@ -1410,10 +1424,206 @@ function generateSettingsLocalJson() {
1410
1424
  }, null, 2);
1411
1425
  }
1412
1426
 
1427
+ /**
1428
+ * Run dev mode - rapid template testing workflow
1429
+ * Loads existing tech-stack.json, processes templates, overwrites commands
1430
+ */
1431
+ async function runDevMode(options = {}) {
1432
+ const cwd = process.cwd();
1433
+ const projectName = basename(cwd);
1434
+ const claudeDir = join(cwd, '.claude');
1435
+ const commandsDir = join(claudeDir, 'commands');
1436
+ const hooksDir = join(claudeDir, 'hooks');
1437
+ const configDir = join(claudeDir, 'config');
1438
+ const techStackPath = join(configDir, 'tech-stack.json');
1439
+
1440
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1441
+ console.log(chalk.magenta.bold(' 🔧 DEV MODE - Template Testing'));
1442
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1443
+ console.log('');
1444
+ console.log(chalk.cyan(` Project: ${chalk.bold(projectName)}`));
1445
+ console.log(chalk.cyan(` Location: ${cwd}`));
1446
+ console.log('');
1447
+
1448
+ // Load existing tech-stack.json
1449
+ let techStack = {};
1450
+ if (existsSync(techStackPath)) {
1451
+ try {
1452
+ techStack = JSON.parse(readFileSync(techStackPath, 'utf8'));
1453
+ console.log(chalk.green(' ✓ Loaded existing tech-stack.json'));
1454
+ } catch (err) {
1455
+ console.log(chalk.yellow(` ⚠ Could not parse tech-stack.json: ${err.message}`));
1456
+ }
1457
+ } else {
1458
+ console.log(chalk.yellow(' ⚠ No tech-stack.json found - templates will have unprocessed placeholders'));
1459
+ }
1460
+
1461
+ // Ensure directories exist
1462
+ if (!existsSync(commandsDir)) {
1463
+ mkdirSync(commandsDir, { recursive: true });
1464
+ console.log(chalk.green(' ✓ Created .claude/commands/'));
1465
+ }
1466
+ if (!existsSync(hooksDir)) {
1467
+ mkdirSync(hooksDir, { recursive: true });
1468
+ console.log(chalk.green(' ✓ Created .claude/hooks/'));
1469
+ }
1470
+
1471
+ // Identify custom commands (no matching template) to preserve
1472
+ const templatesDir = join(__dirname, '..', '..', 'templates', 'commands');
1473
+ const hooksTemplatesDir = join(__dirname, '..', '..', 'templates', 'hooks');
1474
+
1475
+ const templateCommandNames = existsSync(templatesDir)
1476
+ ? readdirSync(templatesDir).filter(f => f.endsWith('.template.md')).map(f => f.replace('.template.md', ''))
1477
+ : [];
1478
+ const templateHookNames = existsSync(hooksTemplatesDir)
1479
+ ? readdirSync(hooksTemplatesDir).filter(f => f.endsWith('.template.js')).map(f => f.replace('.template.js', ''))
1480
+ : [];
1481
+
1482
+ // Find existing custom commands (those without matching templates)
1483
+ const existingCommands = existsSync(commandsDir)
1484
+ ? readdirSync(commandsDir).filter(f => f.endsWith('.md')).map(f => f.replace('.md', ''))
1485
+ : [];
1486
+ const customCommands = existingCommands.filter(cmd =>
1487
+ !templateCommandNames.includes(cmd) &&
1488
+ cmd !== 'menu' &&
1489
+ cmd !== 'INDEX' &&
1490
+ cmd !== 'README'
1491
+ );
1492
+
1493
+ if (customCommands.length > 0) {
1494
+ console.log(chalk.blue(` 📌 Preserving ${customCommands.length} custom command(s):`));
1495
+ for (const cmd of customCommands) {
1496
+ console.log(chalk.dim(` • /${cmd}`));
1497
+ }
1498
+ console.log('');
1499
+ }
1500
+
1501
+ console.log(chalk.bold('Processing and deploying templates...\n'));
1502
+
1503
+ const spinner = ora('Processing templates...').start();
1504
+ const deployed = { commands: [], hooks: [], preserved: customCommands };
1505
+ const failed = [];
1506
+
1507
+ // Get all command templates
1508
+ if (existsSync(templatesDir)) {
1509
+ const templateFiles = readdirSync(templatesDir).filter(f => f.endsWith('.template.md'));
1510
+
1511
+ for (const templateFile of templateFiles) {
1512
+ const cmdName = templateFile.replace('.template.md', '');
1513
+ const templatePath = join(templatesDir, templateFile);
1514
+ const outputPath = join(commandsDir, `${cmdName}.md`);
1515
+
1516
+ try {
1517
+ let content = readFileSync(templatePath, 'utf8');
1518
+
1519
+ // Process template with tech-stack values
1520
+ const { content: processed, warnings } = replacePlaceholders(content, techStack, {
1521
+ preserveUnknown: false,
1522
+ warnOnMissing: false,
1523
+ });
1524
+
1525
+ writeFileSync(outputPath, processed, 'utf8');
1526
+ deployed.commands.push(cmdName);
1527
+ } catch (err) {
1528
+ failed.push({ name: cmdName, type: 'command', error: err.message });
1529
+ }
1530
+ }
1531
+ }
1532
+
1533
+ // Also process hook templates
1534
+ if (existsSync(hooksTemplatesDir)) {
1535
+ const hookFiles = readdirSync(hooksTemplatesDir).filter(f => f.endsWith('.template.js'));
1536
+
1537
+ for (const hookFile of hookFiles) {
1538
+ const hookName = hookFile.replace('.template.js', '');
1539
+ const templatePath = join(hooksTemplatesDir, hookFile);
1540
+ const outputPath = join(hooksDir, `${hookName}.js`);
1541
+
1542
+ try {
1543
+ let content = readFileSync(templatePath, 'utf8');
1544
+
1545
+ // Process template with tech-stack values
1546
+ const { content: processed } = replacePlaceholders(content, techStack, {
1547
+ preserveUnknown: false,
1548
+ warnOnMissing: false,
1549
+ });
1550
+
1551
+ writeFileSync(outputPath, processed, 'utf8');
1552
+ deployed.hooks.push(hookName);
1553
+ } catch (err) {
1554
+ failed.push({ name: hookName, type: 'hook', error: err.message });
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ // Generate menu command from scratch (uses COMMAND_TEMPLATES)
1560
+ const menuTemplate = COMMAND_TEMPLATES['menu'];
1561
+ if (menuTemplate) {
1562
+ const installedAgents = existsSync(join(claudeDir, 'agents'))
1563
+ ? readdirSync(join(claudeDir, 'agents')).filter(f => f.endsWith('.md')).map(f => f.replace('.md', ''))
1564
+ : [];
1565
+ const installedSkills = existsSync(join(claudeDir, 'skills'))
1566
+ ? readdirSync(join(claudeDir, 'skills')).filter(f => !f.startsWith('.'))
1567
+ : [];
1568
+ const installedHooks = existsSync(hooksDir)
1569
+ ? readdirSync(hooksDir).filter(f => f.endsWith('.js')).map(f => f.replace('.js', ''))
1570
+ : [];
1571
+
1572
+ const menuContent = generateMenuCommand(projectName, deployed.commands, installedAgents, installedSkills, installedHooks);
1573
+ writeFileSync(join(commandsDir, 'menu.md'), menuContent, 'utf8');
1574
+ deployed.commands.push('menu');
1575
+ }
1576
+
1577
+ // Generate INDEX.md
1578
+ const indexContent = generateIndexFile(deployed.commands, projectName);
1579
+ writeFileSync(join(commandsDir, 'INDEX.md'), indexContent, 'utf8');
1580
+
1581
+ // Generate README.md
1582
+ const readmeContent = generateReadmeFile(deployed.commands, projectName);
1583
+ writeFileSync(join(commandsDir, 'README.md'), readmeContent, 'utf8');
1584
+
1585
+ spinner.stop();
1586
+
1587
+ // Summary
1588
+ console.log('');
1589
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1590
+ console.log(chalk.green.bold(' ✓ DEV MODE: Templates Deployed'));
1591
+ console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
1592
+ console.log('');
1593
+ console.log(chalk.cyan(` Commands: ${deployed.commands.length} deployed`));
1594
+ console.log(chalk.cyan(` Hooks: ${deployed.hooks.length} deployed`));
1595
+ if (deployed.preserved && deployed.preserved.length > 0) {
1596
+ console.log(chalk.blue(` Custom: ${deployed.preserved.length} preserved`));
1597
+ }
1598
+ if (failed.length > 0) {
1599
+ console.log(chalk.yellow(` Failed: ${failed.length}`));
1600
+ for (const f of failed) {
1601
+ console.log(chalk.red(` • ${f.type}/${f.name}: ${f.error}`));
1602
+ }
1603
+ }
1604
+ console.log('');
1605
+ console.log(chalk.dim(' tech-stack.json: Preserved'));
1606
+ console.log(chalk.dim(' settings.json: Preserved'));
1607
+ if (deployed.preserved && deployed.preserved.length > 0) {
1608
+ console.log(chalk.dim(` Custom commands: ${deployed.preserved.join(', ')}`));
1609
+ }
1610
+ console.log('');
1611
+ console.log(chalk.yellow.bold(' ⚠ Restart Claude Code CLI to use new commands'));
1612
+ console.log('');
1613
+
1614
+ return { deployed, failed };
1615
+ }
1616
+
1413
1617
  /**
1414
1618
  * Run the init wizard
1415
1619
  */
1416
1620
  export async function runInit(options = {}) {
1621
+ // DEV MODE: Fast path for template testing
1622
+ if (options.dev) {
1623
+ showHeader('Claude CLI Advanced Starter Pack - DEV MODE');
1624
+ return runDevMode(options);
1625
+ }
1626
+
1417
1627
  showHeader('Claude CLI Advanced Starter Pack - Project Setup');
1418
1628
 
1419
1629
  const cwd = process.cwd();
@@ -2416,6 +2626,20 @@ export async function runInit(options = {}) {
2416
2626
  writeFileSync(ccaspStatePath, JSON.stringify(ccaspState, null, 2), 'utf8');
2417
2627
  console.log(chalk.green(` ✓ Updated ccasp-state.json (v${currentVersion})`));
2418
2628
 
2629
+ // Register project in global registry (unless --no-register flag is set)
2630
+ if (!options.noRegister) {
2631
+ const isNewProject = registerProject(cwd, {
2632
+ name: projectName,
2633
+ version: currentVersion,
2634
+ features: selectedFeatures
2635
+ });
2636
+ if (isNewProject) {
2637
+ console.log(chalk.green(` ✓ Registered project in global CCASP registry`));
2638
+ } else {
2639
+ console.log(chalk.dim(` ○ Updated project in global CCASP registry`));
2640
+ }
2641
+ }
2642
+
2419
2643
  // Show next steps
2420
2644
  console.log(chalk.bold('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
2421
2645
  console.log(chalk.bold('Next Steps:\n'));
@@ -20,6 +20,7 @@ import {
20
20
  ensureQueueDir,
21
21
  } from '../panel/queue.js';
22
22
  import { getVersion } from '../utils.js';
23
+ import { isHappyMode } from '../utils/happy-detect.js';
23
24
 
24
25
  // Panel ASCII Banner
25
26
  const PANEL_BANNER = `
@@ -249,8 +250,15 @@ export async function runPanel(options = {}) {
249
250
 
250
251
  /**
251
252
  * Launch panel in a new terminal window
253
+ * Automatically detects Happy CLI and falls back to inline mode
252
254
  */
253
255
  export async function launchPanel() {
256
+ // If Happy mode detected, use inline panel instead of new window
257
+ if (isHappyMode()) {
258
+ console.log(chalk.cyan('\n Happy CLI detected - using inline panel\n'));
259
+ return launchPanelInline();
260
+ }
261
+
254
262
  console.log(chalk.cyan('\n Launching CCASP Panel in new window...\n'));
255
263
 
256
264
  const ccaspPath = join(process.cwd(), 'node_modules', '.bin', 'ccasp');
@@ -295,3 +303,79 @@ export async function launchPanel() {
295
303
  console.log(chalk.dim(` Run manually: ${chalk.white('ccasp panel')}\n`));
296
304
  }
297
305
  }
306
+
307
+ /**
308
+ * Launch panel inline (mobile-friendly, no new window)
309
+ * Used when Happy CLI is detected or explicitly requested
310
+ */
311
+ export async function launchPanelInline() {
312
+ const MOBILE_PANEL_BANNER = `
313
+ ${chalk.cyan('╔══════════════════════════════════╗')}
314
+ ${chalk.cyan('║')} ${chalk.bold('Panel')} ${chalk.dim('(inline)')}${' '.repeat(17)}${chalk.cyan('║')}
315
+ ${chalk.cyan('╚══════════════════════════════════╝')}`;
316
+
317
+ console.clear();
318
+ console.log(MOBILE_PANEL_BANNER);
319
+ console.log('');
320
+
321
+ // Simplified single-column panel for mobile
322
+ const sections = [
323
+ { header: 'Agents', items: [
324
+ { key: 'A', label: 'Create Agent', cmd: '/create-agent' },
325
+ { key: 'H', label: 'Create Hook', cmd: '/create-hook' },
326
+ { key: 'S', label: 'Create Skill', cmd: '/create-skill' },
327
+ { key: 'M', label: 'Explore MCP', cmd: '/explore-mcp' },
328
+ ]},
329
+ { header: 'Actions', items: [
330
+ { key: 'P', label: 'Phase Dev', cmd: '/phase-dev-plan' },
331
+ { key: 'G', label: 'GitHub Task', cmd: '/github-task' },
332
+ { key: 'T', label: 'E2E Tests', cmd: '/e2e-test' },
333
+ ]},
334
+ ];
335
+
336
+ // Build choices
337
+ const choices = [];
338
+ for (const section of sections) {
339
+ choices.push(new inquirer.Separator(chalk.cyan(` ${section.header}`)));
340
+ for (const item of section.items) {
341
+ choices.push({
342
+ name: `${chalk.yellow(item.key + ')')} ${item.label}`,
343
+ value: item.cmd,
344
+ });
345
+ }
346
+ }
347
+ choices.push(new inquirer.Separator(chalk.dim('─'.repeat(34))));
348
+ choices.push({ name: `${chalk.yellow('B)')} Back`, value: 'back' });
349
+
350
+ const { action } = await inquirer.prompt([{
351
+ type: 'list',
352
+ name: 'action',
353
+ message: 'Select:',
354
+ choices,
355
+ pageSize: 12,
356
+ }]);
357
+
358
+ if (action === 'back') {
359
+ return;
360
+ }
361
+
362
+ // Show command and copy to clipboard
363
+ console.log('');
364
+ console.log(chalk.cyan(`Command: ${action}`));
365
+
366
+ const copied = copyToClipboard(action);
367
+ if (copied) {
368
+ console.log(chalk.green('✓ Copied to clipboard'));
369
+ }
370
+ console.log(chalk.dim('Paste in Claude Code'));
371
+ console.log('');
372
+
373
+ await inquirer.prompt([{
374
+ type: 'input',
375
+ name: 'continue',
376
+ message: 'Enter to continue...',
377
+ }]);
378
+
379
+ // Loop back to panel
380
+ return launchPanelInline();
381
+ }
@@ -8,6 +8,7 @@ import chalk from 'chalk';
8
8
  import { existsSync, readFileSync, readdirSync, unlinkSync, rmdirSync, copyFileSync, statSync } from 'fs';
9
9
  import { join, basename, dirname } from 'path';
10
10
  import { createInterface } from 'readline';
11
+ import { unregisterProject } from '../utils/global-registry.js';
11
12
 
12
13
  /**
13
14
  * Prompt user for confirmation
@@ -396,8 +397,14 @@ export async function runUninstall(options = {}) {
396
397
  }
397
398
  }
398
399
 
400
+ // Remove from global registry
401
+ const wasRegistered = unregisterProject(projectDir);
402
+ if (wasRegistered) {
403
+ console.log(chalk.green(' ✓ Removed from global CCASP registry'));
404
+ }
405
+
399
406
  // Done
400
- console.log(chalk.green.bold(' ✓ CCASP uninstalled successfully!\n'));
407
+ console.log(chalk.green.bold('\n ✓ CCASP uninstalled successfully!\n'));
401
408
 
402
409
  if (backups.size > 0) {
403
410
  console.log(chalk.dim(' Your backed-up files have been restored.'));
@@ -1,5 +1,35 @@
1
1
  {
2
2
  "releases": [
3
+ {
4
+ "version": "1.8.5",
5
+ "date": "2026-01-31",
6
+ "summary": "Release notes pending",
7
+ "highlights": [],
8
+ "newFeatures": {
9
+ "commands": [],
10
+ "agents": [],
11
+ "skills": [],
12
+ "hooks": [],
13
+ "other": []
14
+ },
15
+ "breaking": [],
16
+ "deprecated": []
17
+ },
18
+ {
19
+ "version": "1.8.4",
20
+ "date": "2026-01-31",
21
+ "summary": "Release notes pending",
22
+ "highlights": [],
23
+ "newFeatures": {
24
+ "commands": [],
25
+ "agents": [],
26
+ "skills": [],
27
+ "hooks": [],
28
+ "other": []
29
+ },
30
+ "breaking": [],
31
+ "deprecated": []
32
+ },
3
33
  {
4
34
  "version": "1.5.0",
5
35
  "date": "2026-01-30",
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Global Project Registry
3
+ * Tracks all projects configured with CCASP across the system
4
+ * Location: ~/.claude/ccasp-registry.json
5
+ */
6
+
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import {
10
+ globalClaudePath,
11
+ getHomeDir,
12
+ toForwardSlashes,
13
+ pathsEqual,
14
+ getPathSegment
15
+ } from './paths.js';
16
+
17
+ /**
18
+ * Get the path to the global registry file
19
+ * @returns {string} Path to ~/.claude/ccasp-registry.json
20
+ */
21
+ export function getRegistryPath() {
22
+ return globalClaudePath('ccasp-registry.json');
23
+ }
24
+
25
+ /**
26
+ * Get the global .claude directory path
27
+ * @returns {string} Path to ~/.claude/
28
+ */
29
+ export function getGlobalClaudeDir() {
30
+ return join(getHomeDir(), '.claude');
31
+ }
32
+
33
+ /**
34
+ * Ensure the global .claude directory exists
35
+ */
36
+ function ensureGlobalDir() {
37
+ const globalDir = getGlobalClaudeDir();
38
+ if (!existsSync(globalDir)) {
39
+ mkdirSync(globalDir, { recursive: true });
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Load the global registry
45
+ * @returns {Object} Registry object with projects array and metadata
46
+ */
47
+ export function loadRegistry() {
48
+ const registryPath = getRegistryPath();
49
+
50
+ if (!existsSync(registryPath)) {
51
+ return {
52
+ version: '1.0.0',
53
+ projects: [],
54
+ lastModified: null
55
+ };
56
+ }
57
+
58
+ try {
59
+ const content = readFileSync(registryPath, 'utf-8');
60
+ return JSON.parse(content);
61
+ } catch (err) {
62
+ console.error(`Warning: Could not parse registry file: ${err.message}`);
63
+ return {
64
+ version: '1.0.0',
65
+ projects: [],
66
+ lastModified: null
67
+ };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Save the global registry
73
+ * @param {Object} registry - Registry object to save
74
+ */
75
+ export function saveRegistry(registry) {
76
+ ensureGlobalDir();
77
+ const registryPath = getRegistryPath();
78
+
79
+ registry.lastModified = new Date().toISOString();
80
+
81
+ writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf-8');
82
+ }
83
+
84
+ /**
85
+ * Register a project in the global registry
86
+ * @param {string} projectPath - Absolute path to the project
87
+ * @param {Object} metadata - Optional metadata (name, version, features)
88
+ * @returns {boolean} True if newly registered, false if already existed
89
+ */
90
+ export function registerProject(projectPath, metadata = {}) {
91
+ const registry = loadRegistry();
92
+
93
+ // Check if already registered (using cross-platform path comparison)
94
+ const existingIndex = registry.projects.findIndex(
95
+ p => pathsEqual(p.path, projectPath)
96
+ );
97
+
98
+ const projectEntry = {
99
+ path: projectPath,
100
+ name: metadata.name || getPathSegment(projectPath),
101
+ registeredAt: new Date().toISOString(),
102
+ lastInitAt: new Date().toISOString(),
103
+ ccaspVersion: metadata.version || 'unknown',
104
+ features: metadata.features || []
105
+ };
106
+
107
+ if (existingIndex >= 0) {
108
+ // Update existing entry
109
+ registry.projects[existingIndex] = {
110
+ ...registry.projects[existingIndex],
111
+ ...projectEntry,
112
+ registeredAt: registry.projects[existingIndex].registeredAt // Keep original registration date
113
+ };
114
+ saveRegistry(registry);
115
+ return false;
116
+ }
117
+
118
+ // Add new entry
119
+ registry.projects.push(projectEntry);
120
+ saveRegistry(registry);
121
+ return true;
122
+ }
123
+
124
+ /**
125
+ * Unregister a project from the global registry
126
+ * @param {string} projectPath - Absolute path to the project
127
+ * @returns {boolean} True if removed, false if not found
128
+ */
129
+ export function unregisterProject(projectPath) {
130
+ const registry = loadRegistry();
131
+
132
+ const initialLength = registry.projects.length;
133
+ registry.projects = registry.projects.filter(
134
+ p => !pathsEqual(p.path, projectPath)
135
+ );
136
+
137
+ if (registry.projects.length < initialLength) {
138
+ saveRegistry(registry);
139
+ return true;
140
+ }
141
+
142
+ return false;
143
+ }
144
+
145
+ /**
146
+ * Get all registered projects
147
+ * @param {Object} options - Filter options
148
+ * @param {boolean} options.existsOnly - Only return projects that still exist on disk
149
+ * @returns {Array} Array of project entries
150
+ */
151
+ export function getRegisteredProjects(options = {}) {
152
+ const registry = loadRegistry();
153
+ let projects = registry.projects;
154
+
155
+ if (options.existsOnly) {
156
+ projects = projects.filter(p => existsSync(p.path));
157
+ }
158
+
159
+ return projects;
160
+ }
161
+
162
+ /**
163
+ * Check if a project is registered
164
+ * @param {string} projectPath - Absolute path to the project
165
+ * @returns {boolean} True if registered
166
+ */
167
+ export function isProjectRegistered(projectPath) {
168
+ const registry = loadRegistry();
169
+
170
+ return registry.projects.some(
171
+ p => pathsEqual(p.path, projectPath)
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Get registry statistics
177
+ * @returns {Object} Stats about registered projects
178
+ */
179
+ export function getRegistryStats() {
180
+ const registry = loadRegistry();
181
+ const projects = registry.projects;
182
+
183
+ let existingCount = 0;
184
+ let missingCount = 0;
185
+
186
+ for (const project of projects) {
187
+ if (existsSync(project.path)) {
188
+ existingCount++;
189
+ } else {
190
+ missingCount++;
191
+ }
192
+ }
193
+
194
+ return {
195
+ total: projects.length,
196
+ existing: existingCount,
197
+ missing: missingCount,
198
+ lastModified: registry.lastModified
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Clean up registry by removing projects that no longer exist
204
+ * @returns {number} Number of projects removed
205
+ */
206
+ export function cleanupRegistry() {
207
+ const registry = loadRegistry();
208
+ const initialLength = registry.projects.length;
209
+
210
+ registry.projects = registry.projects.filter(p => existsSync(p.path));
211
+
212
+ const removed = initialLength - registry.projects.length;
213
+ if (removed > 0) {
214
+ saveRegistry(registry);
215
+ }
216
+
217
+ return removed;
218
+ }
219
+
220
+ /**
221
+ * Clear the entire registry
222
+ */
223
+ export function clearRegistry() {
224
+ const registry = {
225
+ version: '1.0.0',
226
+ projects: [],
227
+ lastModified: new Date().toISOString()
228
+ };
229
+ saveRegistry(registry);
230
+ }