/** * Navigation Status Routes Module * Aggregated status endpoint for navigation bar badge updates * * API Endpoints: * - GET /api/nav-status - Get aggregated navigation bar status (counts for all badges) */ import { existsSync, readFileSync, readdirSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import type { RouteContext } from './types.js'; // ========== Count Helper Functions ========== /** * Count issues from JSONL file */ function countIssues(projectPath: string): number { const issuesPath = join(projectPath, '.workflow', 'issues', 'issues.jsonl'); if (!existsSync(issuesPath)) return 0; try { const content = readFileSync(issuesPath, 'utf8'); return content.split('\n').filter(line => line.trim()).length; } catch { return 0; } } /** * Count discoveries from index or directory scan */ function countDiscoveries(projectPath: string): number { const discoveriesDir = join(projectPath, '.workflow', 'issues', 'discoveries'); const indexPath = join(discoveriesDir, 'index.json'); // Try index.json first if (existsSync(indexPath)) { try { const index = JSON.parse(readFileSync(indexPath, 'utf8')); return index.discoveries?.length || 0; } catch { /* fall through */ } } // Fallback: scan directory if (!existsSync(discoveriesDir)) return 0; try { const entries = readdirSync(discoveriesDir, { withFileTypes: true }); return entries.filter(e => e.isDirectory() && e.name.startsWith('DSC-')).length; } catch { return 0; } } /** * Recursively count command files in a directory */ function countCommandsInDir(dirPath: string): { enabled: number; disabled: number } { let enabled = 0; let disabled = 0; if (!existsSync(dirPath)) { return { enabled, disabled }; } try { const entries = readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dirPath, entry.name); if (entry.isDirectory()) { if (entry.name === '_disabled') { // Count disabled commands recursively disabled += countAllMdFiles(fullPath); } else { // Recursively count enabled commands const subCounts = countCommandsInDir(fullPath); enabled += subCounts.enabled; disabled += subCounts.disabled; } } else if (entry.isFile() && entry.name.endsWith('.md')) { enabled++; } } } catch { /* ignore */ } return { enabled, disabled }; } /** * Count all .md files recursively (for disabled directory) */ function countAllMdFiles(dirPath: string): number { let count = 0; if (!existsSync(dirPath)) return count; try { const entries = readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dirPath, entry.name); if (entry.isDirectory()) { count += countAllMdFiles(fullPath); } else if (entry.isFile() && entry.name.endsWith('.md')) { count++; } } } catch { /* ignore */ } return count; } /** * Count commands from project and user directories */ function countCommands(projectPath: string): { project: { enabled: number; disabled: number }; user: { enabled: number; disabled: number }; total: number; enabled: number; disabled: number; } { // Project commands const projectDir = join(projectPath, '.claude', 'commands'); const projectCounts = countCommandsInDir(projectDir); // User commands const userDir = join(homedir(), '.claude', 'commands'); const userCounts = countCommandsInDir(userDir); const totalEnabled = projectCounts.enabled + userCounts.enabled; const totalDisabled = projectCounts.disabled + userCounts.disabled; return { project: projectCounts, user: userCounts, total: totalEnabled + totalDisabled, enabled: totalEnabled, disabled: totalDisabled }; } /** * Count skills from project and user directories */ function countSkills(projectPath: string): { project: number; user: number; total: number } { let project = 0, user = 0; // Project skills const projectSkillsDir = join(projectPath, '.claude', 'skills'); if (existsSync(projectSkillsDir)) { try { const entries = readdirSync(projectSkillsDir, { withFileTypes: true }); project = entries.filter(e => e.isDirectory() && existsSync(join(projectSkillsDir, e.name, 'SKILL.md')) ).length; } catch { /* ignore */ } } // User skills const userSkillsDir = join(homedir(), '.claude', 'skills'); if (existsSync(userSkillsDir)) { try { const entries = readdirSync(userSkillsDir, { withFileTypes: true }); user = entries.filter(e => e.isDirectory() && existsSync(join(userSkillsDir, e.name, 'SKILL.md')) ).length; } catch { /* ignore */ } } return { project, user, total: project + user }; } /** * Recursively count rules in a directory */ function countRulesInDir(dirPath: string): number { if (!existsSync(dirPath)) return 0; let count = 0; try { const entries = readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && entry.name.endsWith('.md')) { count++; } else if (entry.isDirectory()) { count += countRulesInDir(join(dirPath, entry.name)); } } } catch { /* ignore */ } return count; } /** * Count rules from project and user directories */ function countRules(projectPath: string): { project: number; user: number; total: number } { const project = countRulesInDir(join(projectPath, '.claude', 'rules')); const user = countRulesInDir(join(homedir(), '.claude', 'rules')); return { project, user, total: project + user }; } /** * Count CLAUDE.md files */ function countClaudeFiles(projectPath: string): number { let count = 0; const EXCLUDES = ['.git', 'node_modules', 'dist', 'build', '.venv', 'venv', '__pycache__', 'coverage', '.workflow']; // User main if (existsSync(join(homedir(), '.claude', 'CLAUDE.md'))) count++; // Project main if (existsSync(join(projectPath, '.claude', 'CLAUDE.md'))) count++; // Root CLAUDE.md if (existsSync(join(projectPath, 'CLAUDE.md'))) count++; // Module-level (scan project subdirectories for CLAUDE.md files) function scanDir(dir: string, depth: number = 0) { if (depth > 3) return; // Limit recursion depth try { const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && !EXCLUDES.includes(entry.name) && !entry.name.startsWith('.')) { const subDir = join(dir, entry.name); if (existsSync(join(subDir, 'CLAUDE.md'))) count++; scanDir(subDir, depth + 1); } } } catch { /* ignore */ } } scanDir(projectPath); return count; } /** * Count hooks from settings object */ function countHooksFromSettings(settings: any): number { if (!settings?.hooks) return 0; let count = 0; for (const event of Object.keys(settings.hooks)) { const hookList = settings.hooks[event]; count += Array.isArray(hookList) ? hookList.length : 1; } return count; } /** * Count hooks from global and project settings */ function countHooks(projectPath: string): { global: number; project: number; total: number } { let global = 0, project = 0; // Global settings const globalSettingsPath = join(homedir(), '.claude', 'settings.json'); if (existsSync(globalSettingsPath)) { try { const settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); global = countHooksFromSettings(settings); } catch { /* ignore */ } } // Project settings const projectSettingsPath = join(projectPath, '.claude', 'settings.json'); if (existsSync(projectSettingsPath)) { try { const settings = JSON.parse(readFileSync(projectSettingsPath, 'utf8')); project = countHooksFromSettings(settings); } catch { /* ignore */ } } return { global, project, total: global + project }; } // ========== Route Handler ========== export async function handleNavStatusRoutes(ctx: RouteContext): Promise { const { pathname, url, res, initialPath } = ctx; // GET /api/nav-status - Aggregated navigation badge status if (pathname === '/api/nav-status' && ctx.req.method === 'GET') { try { const projectPath = url.searchParams.get('path') || initialPath; // Execute all counts (synchronous file reads wrapped in Promise.resolve for consistency) const [issues, discoveries, skills, commands, rules, claude, hooks] = await Promise.all([ Promise.resolve(countIssues(projectPath)), Promise.resolve(countDiscoveries(projectPath)), Promise.resolve(countSkills(projectPath)), Promise.resolve(countCommands(projectPath)), Promise.resolve(countRules(projectPath)), Promise.resolve(countClaudeFiles(projectPath)), Promise.resolve(countHooks(projectPath)) ]); const response = { issues: { count: issues }, discoveries: { count: discoveries }, skills: { count: skills.total, project: skills.project, user: skills.user }, commands: { count: commands.total, enabled: commands.enabled, disabled: commands.disabled, project: commands.project, user: commands.user }, rules: { count: rules.total, project: rules.project, user: rules.user }, claude: { count: claude }, hooks: { count: hooks.total, global: hooks.global, project: hooks.project }, timestamp: new Date().toISOString() }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(response)); return true; } catch (error) { console.error('[Nav Status] Error:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: (error as Error).message })); return true; } } return false; }