import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.resolve(__dirname, '..'); const serverRoot = path.join(projectRoot, 'server'); const clientRoot = path.join(projectRoot, 'src'); const docsRoot = path.join(projectRoot, 'docs', 'backend'); const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch']; const routeDefinitionPattern = /\b(app|router)\.(get|post|put|delete|patch)\(\s*(['"`])(.+?)\3/g; const defaultImportPattern = /^import\s+([A-Za-z0-9_$]+)(?:\s*,\s*\{[^}]+\})?\s+from\s+['"](.+?)['"];$/gm; const incomingRealtimePattern = /data\.type === '([^']+)'/g; const outgoingRealtimePattern = /type:\s*'([^']+)'/g; fs.mkdirSync(docsRoot, { recursive: true }); function toPosix(value) { return value.split(path.sep).join('/'); } function readText(filePath) { return fs.readFileSync(filePath, 'utf8'); } function walkFiles(dirPath, files = []) { for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) { if (entry.name === 'dist' || entry.name === 'node_modules') { continue; } const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { walkFiles(fullPath, files); continue; } files.push(fullPath); } return files; } function getLineNumber(content, index) { return content.slice(0, index).split(/\r?\n/).length; } function splitArgs(argumentSource) { return argumentSource .split(',') .map(part => part.trim()) .filter(Boolean); } function sanitizeObjectKey(key) { return key .replace(/^[\s{]+|[\s}]+$/g, '') .replace(/=.*$/, '') .replace(/:.+$/, '') .replace(/\?/g, '') .trim(); } function collectObjectKeys(block, accessor) { const keys = new Set(); const directPattern = new RegExp(`req\\.${accessor}\\.([A-Za-z0-9_]+)`, 'g'); const destructuringPattern = new RegExp(`\\{([^}]*)\\}\\s*=\\s*req\\.${accessor}`, 'gs'); for (const match of block.matchAll(directPattern)) { keys.add(match[1]); } for (const match of block.matchAll(destructuringPattern)) { for (const rawKey of match[1].split(',')) { const key = sanitizeObjectKey(rawKey); if (key) { keys.add(key); } } } return [...keys].sort(); } function normalizeJoinedPath(basePath, routePath) { const safeBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath; if (!routePath || routePath === '/') { return safeBase || '/'; } if (routePath === '*') { return routePath; } const safeRoute = routePath.startsWith('/') ? routePath : `/${routePath}`; return `${safeBase}${safeRoute}` || '/'; } function getStaticSearchTokens(routePath) { const cleaned = routePath.replace(/:[A-Za-z0-9_]+/g, '').replace(/\*/g, ''); const segments = cleaned.split('/').filter(Boolean); const tokens = new Set(); if (cleaned && cleaned !== '/') { tokens.add(cleaned.endsWith('/') ? cleaned : `${cleaned}`); } for (let index = segments.length; index >= 2; index -= 1) { tokens.add(`/${segments.slice(0, index).join('/')}/`); } if (segments.length > 0) { tokens.add(`/${segments.slice(0, 1).join('/')}/`); } return [...tokens].filter(Boolean); } function classifyTag(routePath) { if (routePath === '*' || routePath === '/health' || routePath.startsWith('/api/system')) { return 'System'; } if (routePath.startsWith('/api/auth')) return 'Auth'; if (routePath.startsWith('/api/user')) return 'User'; if (routePath.startsWith('/api/settings')) return 'Settings'; if (routePath.startsWith('/api/git')) return 'Git'; if (routePath.startsWith('/api/taskmaster')) return 'TaskMaster'; if (routePath.startsWith('/api/plugins')) return 'Plugins'; if (routePath.startsWith('/api/agent')) return 'Agent'; if (routePath.startsWith('/api/commands')) return 'Commands'; if (routePath.startsWith('/api/mcp')) return 'MCP'; if (routePath.startsWith('/api/cli')) return 'CLI Auth'; if ( routePath.startsWith('/api/cursor') || routePath.startsWith('/api/codex') || routePath.startsWith('/api/gemini') ) { return 'Providers'; } if (routePath.startsWith('/api/search') || routePath.includes('/sessions')) { return 'Sessions'; } if (routePath.includes('/files') || routePath.includes('/file') || routePath.includes('/upload')) { return 'Files'; } if (routePath.startsWith('/api/projects') || routePath.startsWith('/api/create-folder')) { return 'Projects'; } return 'Realtime'; } function classifyPriority(tag, routePath) { if ( tag === 'Agent' || tag === 'TaskMaster' || tag === 'Git' || routePath.startsWith('/api/projects') || routePath.startsWith('/api/search') ) { return 'high'; } if ( tag === 'Providers' || tag === 'Commands' || tag === 'MCP' || tag === 'Plugins' || tag === 'Settings' || tag === 'Auth' || tag === 'User' ) { return 'medium'; } return 'low'; } function describePurpose(method, routePath) { const verb = method.toUpperCase(); if (routePath === '/health') { return 'Expose server health, timestamp, and install mode for diagnostics.'; } if (routePath === '*') { return 'Serve the React application fallback for non-API routes.'; } if (routePath.startsWith('/api/system/update')) { return 'Run the application update workflow on the host machine.'; } if (routePath.startsWith('/api/auth/status')) return 'Report whether authentication is configured.'; if (routePath.startsWith('/api/auth/register')) return 'Create the first local user account.'; if (routePath.startsWith('/api/auth/login')) return 'Authenticate a local user and issue a token.'; if (routePath.startsWith('/api/auth/user')) return 'Return the currently authenticated user.'; if (routePath.startsWith('/api/auth/logout')) return 'Invalidate the current authenticated session.'; if (routePath.startsWith('/api/user/git-config')) return 'Read or update stored git identity settings.'; if (routePath.startsWith('/api/user/complete-onboarding')) return 'Mark onboarding as completed for the current user.'; if (routePath.startsWith('/api/user/onboarding-status')) return 'Return onboarding completion status for the current user.'; if (routePath.startsWith('/api/settings/api-keys')) return 'Manage local API keys used to access the backend.'; if (routePath.startsWith('/api/settings/credentials')) return 'Manage stored provider and GitHub credentials.'; if (routePath.startsWith('/api/projects/create-workspace')) { return 'Create or register a workspace and optionally clone a GitHub repository into it.'; } if (routePath.startsWith('/api/projects/clone-progress')) { return 'Stream workspace cloning progress events to the frontend.'; } if (routePath === '/api/projects') return 'List detected projects and workspaces.'; if (routePath.startsWith('/api/projects/create')) return 'Manually add a project path to the workspace list.'; if (routePath.startsWith('/api/projects/:projectName/sessions/:sessionId/token-usage')) { return 'Report token usage for a stored provider session.'; } if (routePath.includes('/sessions/:sessionId/messages')) { return 'Return paginated messages for a stored session.'; } if (routePath.includes('/sessions')) { return 'List or manage sessions associated with a project or provider.'; } if (routePath.includes('/files') || routePath.includes('/file')) { return 'Read, write, create, rename, delete, or upload project files.'; } if (routePath.startsWith('/api/search/conversations')) { return 'Search conversation history across stored projects and stream results.'; } if (routePath.startsWith('/api/browse-filesystem')) { return 'Browse local directories so the UI can suggest workspace locations.'; } if (routePath.startsWith('/api/create-folder')) { return 'Create a new directory on the local filesystem.'; } if (routePath.startsWith('/api/transcribe')) { return 'Transcribe uploaded audio and optionally enhance the result for prompts or tasks.'; } if (routePath.includes('/upload-images')) { return 'Upload images for chat use and return browser-safe data URLs.'; } if (routePath.startsWith('/api/git/status')) return 'Read git status information for a project.'; if (routePath.startsWith('/api/git/diff')) return 'Return git diff output for a project or file.'; if (routePath.startsWith('/api/git/file-with-diff')) return 'Return file content together with diff context.'; if (routePath.startsWith('/api/git/branches')) return 'List git branches for a project.'; if (routePath.startsWith('/api/git/commits')) return 'List recent commits for a project.'; if (routePath.startsWith('/api/git/commit-diff')) return 'Return diff details for a specific commit.'; if (routePath.startsWith('/api/git/remote-status')) return 'Report remote sync status for a project repository.'; if (routePath.startsWith('/api/git/generate-commit-message')) return 'Generate an AI-assisted commit message from the current diff.'; if (routePath.startsWith('/api/taskmaster')) { return 'Manage TaskMaster detection, PRDs, tasks, templates, and automation for a project.'; } if (routePath.startsWith('/api/commands')) { return 'List, load, or execute slash commands available to the chat experience.'; } if (routePath.startsWith('/api/mcp-utils')) { return 'Return MCP helper information used by setup flows.'; } if (routePath.startsWith('/api/mcp')) { return 'Manage Claude MCP CLI and configuration state.'; } if (routePath.startsWith('/api/cursor')) { return 'Manage Cursor configuration, MCP settings, and stored sessions.'; } if (routePath.startsWith('/api/codex')) { return 'Manage Codex configuration, MCP settings, and stored sessions.'; } if (routePath.startsWith('/api/gemini')) { return 'Manage Gemini session history for the UI.'; } if (routePath.startsWith('/api/cli')) { return 'Report local authentication status for provider CLIs.'; } if (routePath.startsWith('/api/plugins')) { return 'List, install, update, serve, enable, or remove plugins.'; } if (routePath.startsWith('/api/agent')) { return 'Accept external agent jobs that run a provider against a local or cloned project.'; } return `${verb} ${routePath} for backend runtime support.`; } function describeSuccessShape(block, transport) { if (transport === 'sse' || block.includes('text/event-stream')) { return 'Server-sent events stream with progress/result/error events.'; } if (block.includes('res.sendFile')) { return 'Static file or HTML response.'; } if (block.includes('res.redirect')) { return 'HTTP redirect response.'; } if (block.includes('res.json({ success: true')) { return 'JSON object with an explicit success flag and payload.'; } if (block.includes('res.json({')) { return 'Structured JSON object response.'; } if (block.includes('res.json(')) { return 'JSON payload returned directly from service logic.'; } return 'Mixed response shape; inspect handler during refactor.'; } function describeErrorShape(block, transport) { if (transport === 'sse' || block.includes('text/event-stream')) { return 'Streamed error event or JSON error fallback.'; } if (block.includes("res.status(500).json({ error:")) { return 'JSON object with error message and optional details.'; } if (block.includes("res.status(400).json({ error:")) { return 'JSON validation error response.'; } if (block.includes('res.status(')) { return 'JSON error response with HTTP status code.'; } return 'Handler-specific error behavior.'; } function describeSideEffects(method, routePath) { const effects = []; if (method !== 'get') { effects.push('Mutates backend or external state.'); } if (routePath.includes('/git')) effects.push('Touches git repositories or local git config.'); if (routePath.includes('/projects') || routePath.includes('/file') || routePath.includes('/files')) { effects.push('Touches local workspace files or directories.'); } if (routePath.includes('/agent')) effects.push('Invokes external AI providers and may modify project files.'); if (routePath.includes('/taskmaster')) effects.push('Reads or writes TaskMaster project assets.'); if (routePath.includes('/plugins')) effects.push('Installs, updates, or serves plugin assets/processes.'); if (routePath.includes('/settings') || routePath.includes('/auth') || routePath.includes('/credentials')) { effects.push('Reads or writes local authentication or credential state.'); } if (routePath.includes('/mcp')) effects.push('Reads or writes MCP CLI configuration.'); if (routePath.includes('/transcribe')) effects.push('Processes uploaded files and external model responses.'); return effects.length > 0 ? effects : ['Read-only backend query.']; } function collectFrontendConsumers(routePath, clientFiles) { const tokens = getStaticSearchTokens(routePath); const consumers = new Set(); for (const file of clientFiles) { const content = readText(file); if (tokens.some(token => token && content.includes(token))) { consumers.add(toPosix(path.relative(projectRoot, file))); } } return [...consumers].sort(); } function detectTransport(block) { if (block.includes('text/event-stream')) { return 'sse'; } return 'http'; } function parseMounts(runtimeContent) { const routeImports = new Map(); for (const match of runtimeContent.matchAll(defaultImportPattern)) { if (match[2].includes('/routes/')) { routeImports.set(match[1], match[2]); } } const mounts = new Map(); const mountPattern = /app\.use\(\s*(['"`])([^'"`]+)\1\s*,\s*([^)]+?)\);/g; for (const match of runtimeContent.matchAll(mountPattern)) { const basePath = match[2]; const args = splitArgs(match[3]); const routeVariable = args.at(-1); if (!routeVariable || !routeImports.has(routeVariable)) { continue; } mounts.set(routeVariable, { basePath, routeImport: routeImports.get(routeVariable), authMode: args.includes('authenticateToken') ? 'bearer_token' : args.includes('validateExternalApiKey') ? 'api_key_or_platform' : 'public_or_optional_api_key', }); } return mounts; } function parseRoutes(filePath, fullPathPrefix, authMode, clientFiles) { const content = readText(filePath); const matches = [...content.matchAll(routeDefinitionPattern)]; const routes = []; for (let index = 0; index < matches.length; index += 1) { const match = matches[index]; const nextMatch = matches[index + 1]; const routeMethod = match[2].toUpperCase(); const routePath = match[4]; const startIndex = match.index ?? 0; const endIndex = nextMatch?.index ?? content.length; const block = content.slice(startIndex, endIndex); const declarationEnd = block.indexOf('=>'); const declarationSnippet = declarationEnd === -1 ? block : block.slice(0, declarationEnd); const fullPath = fullPathPrefix ? normalizeJoinedPath(fullPathPrefix, routePath) : routePath; const transport = detectTransport(block); const tag = classifyTag(fullPath); const pathParams = [...fullPath.matchAll(/:([A-Za-z0-9_]+)/g)].map(token => token[1]); const queryParams = collectObjectKeys(block, 'query'); const bodyHints = collectObjectKeys(block, 'body'); const localAuthMode = fullPath === '/health' || fullPath === '/api/auth/status' || fullPath === '/api/auth/register' || fullPath === '/api/auth/login' || fullPath === '*' ? 'public' : declarationSnippet.includes('authenticateToken') ? 'bearer_token' : declarationSnippet.includes('validateExternalApiKey') ? 'api_key_or_platform' : authMode; routes.push({ transport, method: routeMethod, path: fullPath, tag, authMode: localAuthMode, sourceFile: toPosix(path.relative(projectRoot, filePath)), sourceLine: getLineNumber(content, startIndex), purpose: describePurpose(routeMethod, fullPath), consumerFiles: collectFrontendConsumers(fullPath, clientFiles), inputs: { pathParams, queryParams, bodyHints, }, successShape: describeSuccessShape(block, transport), errorShape: describeErrorShape(block, transport), sideEffects: describeSideEffects(routeMethod.toLowerCase(), fullPath), priority: classifyPriority(tag, fullPath), }); } return routes; } function parseRealtimeContracts(runtimeFile) { const content = readText(runtimeFile); const incoming = new Set(); const outgoing = new Set(); for (const match of content.matchAll(incomingRealtimePattern)) { incoming.add(match[1]); } const websocketSectionIndex = content.indexOf("wss.on('connection'"); const websocketSection = websocketSectionIndex === -1 ? content : content.slice(websocketSectionIndex); for (const match of websocketSection.matchAll(outgoingRealtimePattern)) { outgoing.add(match[1]); } return { incomingMessageTypes: [...incoming].sort(), outgoingMessageTypes: [...outgoing].sort(), }; } function escapeCsv(value) { const stringValue = Array.isArray(value) ? value.join('; ') : String(value ?? ''); const escaped = stringValue.replace(/"/g, '""'); return `"${escaped}"`; } function writeCsv(filePath, records) { const header = [ 'transport', 'method', 'path', 'tag', 'authMode', 'sourceFile', 'sourceLine', 'purpose', 'consumerFiles', 'pathParams', 'queryParams', 'bodyHints', 'successShape', 'errorShape', 'sideEffects', 'priority', ]; const rows = [ header.join(','), ...records.map(record => [ record.transport, record.method, record.path, record.tag, record.authMode, record.sourceFile, record.sourceLine, record.purpose, record.consumerFiles, record.inputs.pathParams, record.inputs.queryParams, record.inputs.bodyHints, record.successShape, record.errorShape, record.sideEffects, record.priority, ].map(escapeCsv).join(',')), ]; fs.writeFileSync(filePath, `${rows.join('\n')}\n`); } function writeMarkdown(filePath, summary, records, realtimeContracts) { const grouped = new Map(); for (const record of records) { if (!grouped.has(record.tag)) { grouped.set(record.tag, []); } grouped.get(record.tag).push(record); } const lines = [ '# Backend Inventory', '', `Generated on ${summary.generatedAt}.`, '', '## Summary', '', `- HTTP routes: ${summary.httpRoutes}`, `- SSE routes: ${summary.sseRoutes}`, `- Modular routes: ${summary.modularRoutes}`, `- Inline routes: ${summary.inlineRoutes}`, `- Route files scanned: ${summary.routeFilesScanned}`, '', '## Realtime Contracts', '', `- Incoming websocket message types (${realtimeContracts.incomingMessageTypes.length}): ${realtimeContracts.incomingMessageTypes.join(', ')}`, `- Outgoing websocket message types (${realtimeContracts.outgoingMessageTypes.length}): ${realtimeContracts.outgoingMessageTypes.join(', ')}`, '', ]; for (const [tag, tagRecords] of [...grouped.entries()].sort(([left], [right]) => left.localeCompare(right))) { lines.push(`## ${tag}`); lines.push(''); lines.push('| Method | Path | Auth | Purpose | Consumers | Source |'); lines.push('| --- | --- | --- | --- | --- | --- |'); for (const record of tagRecords.sort((left, right) => left.path.localeCompare(right.path))) { lines.push( `| ${record.method} | \`${record.path}\` | ${record.authMode} | ${record.purpose} | ${record.consumerFiles.join('
') || '-'} | ${record.sourceFile}:${record.sourceLine} |` ); } lines.push(''); } fs.writeFileSync(filePath, `${lines.join('\n')}\n`); } const clientFiles = walkFiles(clientRoot).filter(filePath => /\.(js|jsx|ts|tsx)$/.test(filePath)); const legacyRuntimePath = path.join(serverRoot, 'index.js'); const runtimeContent = readText(legacyRuntimePath); const mounts = parseMounts(runtimeContent); const records = []; records.push(...parseRoutes(legacyRuntimePath, '', 'mixed_or_inline', clientFiles)); for (const [routeVariable, mount] of mounts.entries()) { const relativeImport = mount.routeImport.replace('./', ''); const routeFilePath = path.join(serverRoot, relativeImport); records.push(...parseRoutes(routeFilePath, mount.basePath, mount.authMode, clientFiles)); } const realtimeContracts = parseRealtimeContracts(legacyRuntimePath); const summary = { generatedAt: new Date().toISOString(), httpRoutes: records.filter(record => record.transport === 'http').length, sseRoutes: records.filter(record => record.transport === 'sse').length, modularRoutes: records.filter(record => record.sourceFile.includes('/routes/')).length, inlineRoutes: records.filter(record => record.sourceFile === 'server/index.js').length, routeFilesScanned: new Set(records.map(record => record.sourceFile)).size, }; fs.writeFileSync( path.join(docsRoot, 'endpoint-inventory.json'), JSON.stringify({ summary, realtimeContracts, records }, null, 2) ); writeCsv(path.join(docsRoot, 'endpoint-inventory.csv'), records); writeMarkdown(path.join(docsRoot, 'endpoint-inventory.md'), summary, records, realtimeContracts); console.log('[inventory] Generated docs/backend/endpoint-inventory.{json,csv,md}'); console.log( `[inventory] HTTP=${summary.httpRoutes} SSE=${summary.sseRoutes} Modular=${summary.modularRoutes} Inline=${summary.inlineRoutes}` );