mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 01:12:46 +00:00
- Changed the legacy runtime path from 'legacy-runtime.js' to 'index.js' in runtime configuration. - Added a new start script (start.js) to check for the existence of the built TypeScript server entrypoint and import it.
658 lines
21 KiB
JavaScript
658 lines
21 KiB
JavaScript
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('<br>') || '-'} | ${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}`
|
|
);
|