repomap-cli 0.1.0

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.
Files changed (65) hide show
  1. package/dist/analyzer/imports-analyzer.d.ts +27 -0
  2. package/dist/analyzer/imports-analyzer.d.ts.map +1 -0
  3. package/dist/analyzer/imports-analyzer.js +74 -0
  4. package/dist/analyzer/imports-analyzer.js.map +1 -0
  5. package/dist/analyzer/tree-sitter-analyzer.d.ts +37 -0
  6. package/dist/analyzer/tree-sitter-analyzer.d.ts.map +1 -0
  7. package/dist/analyzer/tree-sitter-analyzer.js +531 -0
  8. package/dist/analyzer/tree-sitter-analyzer.js.map +1 -0
  9. package/dist/commands/init.d.ts +6 -0
  10. package/dist/commands/init.d.ts.map +1 -0
  11. package/dist/commands/init.js +202 -0
  12. package/dist/commands/init.js.map +1 -0
  13. package/dist/commands/login.d.ts +9 -0
  14. package/dist/commands/login.d.ts.map +1 -0
  15. package/dist/commands/login.js +106 -0
  16. package/dist/commands/login.js.map +1 -0
  17. package/dist/commands/mcp.d.ts +4 -0
  18. package/dist/commands/mcp.d.ts.map +1 -0
  19. package/dist/commands/mcp.js +511 -0
  20. package/dist/commands/mcp.js.map +1 -0
  21. package/dist/commands/snapshot.d.ts +8 -0
  22. package/dist/commands/snapshot.d.ts.map +1 -0
  23. package/dist/commands/snapshot.js +314 -0
  24. package/dist/commands/snapshot.js.map +1 -0
  25. package/dist/commands/status.d.ts +5 -0
  26. package/dist/commands/status.d.ts.map +1 -0
  27. package/dist/commands/status.js +108 -0
  28. package/dist/commands/status.js.map +1 -0
  29. package/dist/commands/watch.d.ts +5 -0
  30. package/dist/commands/watch.d.ts.map +1 -0
  31. package/dist/commands/watch.js +367 -0
  32. package/dist/commands/watch.js.map +1 -0
  33. package/dist/generator/claude-md.d.ts +19 -0
  34. package/dist/generator/claude-md.d.ts.map +1 -0
  35. package/dist/generator/claude-md.js +656 -0
  36. package/dist/generator/claude-md.js.map +1 -0
  37. package/dist/generator/map-json.d.ts +35 -0
  38. package/dist/generator/map-json.d.ts.map +1 -0
  39. package/dist/generator/map-json.js +130 -0
  40. package/dist/generator/map-json.js.map +1 -0
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +91 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/scanner/file-scanner.d.ts +9 -0
  46. package/dist/scanner/file-scanner.d.ts.map +1 -0
  47. package/dist/scanner/file-scanner.js +76 -0
  48. package/dist/scanner/file-scanner.js.map +1 -0
  49. package/dist/scanner/stack-detector.d.ts +10 -0
  50. package/dist/scanner/stack-detector.d.ts.map +1 -0
  51. package/dist/scanner/stack-detector.js +153 -0
  52. package/dist/scanner/stack-detector.js.map +1 -0
  53. package/dist/utils/credentials.d.ts +11 -0
  54. package/dist/utils/credentials.d.ts.map +1 -0
  55. package/dist/utils/credentials.js +34 -0
  56. package/dist/utils/credentials.js.map +1 -0
  57. package/dist/utils/health-score.d.ts +10 -0
  58. package/dist/utils/health-score.d.ts.map +1 -0
  59. package/dist/utils/health-score.js +44 -0
  60. package/dist/utils/health-score.js.map +1 -0
  61. package/dist/utils/ui.d.ts +14 -0
  62. package/dist/utils/ui.d.ts.map +1 -0
  63. package/dist/utils/ui.js +45 -0
  64. package/dist/utils/ui.js.map +1 -0
  65. package/package.json +54 -0
@@ -0,0 +1,656 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ // ─── README parsing ──────────────────────────────────────────────────────────
4
+ function parseReadmeDescription(rootDir) {
5
+ const readmePaths = ['README.md', 'readme.md', 'README.MD', 'Readme.md'];
6
+ for (const name of readmePaths) {
7
+ const p = path.join(rootDir, name);
8
+ if (!fs.existsSync(p))
9
+ continue;
10
+ try {
11
+ const content = fs.readFileSync(p, 'utf-8');
12
+ const lines = content.split('\n');
13
+ for (const line of lines) {
14
+ const trimmed = line.trim();
15
+ if (!trimmed)
16
+ continue;
17
+ if (trimmed.startsWith('#'))
18
+ continue;
19
+ if (trimmed.startsWith('![') || trimmed.startsWith('[!['))
20
+ continue;
21
+ if (trimmed.includes('shields.io') || trimmed.includes('badge'))
22
+ continue;
23
+ if (trimmed.length < 10)
24
+ continue;
25
+ const text = trimmed.startsWith('> ') ? trimmed.slice(2).trim() : trimmed;
26
+ return text.replace(/\*\*/g, '').slice(0, 120);
27
+ }
28
+ }
29
+ catch {
30
+ // ignore
31
+ }
32
+ }
33
+ return '';
34
+ }
35
+ // ─── Stack formatter ─────────────────────────────────────────────────────────
36
+ function formatStackOneLine(stack) {
37
+ const priorityOrder = [
38
+ 'next.js', 'react', 'vue', 'nuxt', 'svelte', 'astro', 'remix',
39
+ 'express', 'fastify', 'hono', 'nestjs',
40
+ ];
41
+ const skip = new Set(['vitest', 'jest', 'playwright', 'cypress', 'webpack', 'vite', 'turbopack']);
42
+ const parts = [];
43
+ for (const fw of priorityOrder) {
44
+ if (stack.frameworks.includes(fw))
45
+ parts.push(fw);
46
+ }
47
+ for (const fw of stack.frameworks) {
48
+ if (!priorityOrder.includes(fw) && !skip.has(fw) && !parts.includes(fw)) {
49
+ parts.push(fw);
50
+ }
51
+ }
52
+ if (stack.languages.includes('typescript') && !parts.includes('typescript')) {
53
+ parts.push('TypeScript');
54
+ }
55
+ else if (stack.languages.includes('python')) {
56
+ parts.push('Python');
57
+ }
58
+ else if (stack.languages.includes('go')) {
59
+ parts.push('Go');
60
+ }
61
+ return parts.slice(0, 8).join(', ');
62
+ }
63
+ function detectArchitecture(rootDir, files, stack) {
64
+ // Monorepo / pattern
65
+ let pattern = 'monolith';
66
+ try {
67
+ const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
68
+ if (pkg.workspaces)
69
+ pattern = 'monorepo';
70
+ }
71
+ catch { /* ignore */ }
72
+ if (fs.existsSync(path.join(rootDir, 'turbo.json')))
73
+ pattern = 'monorepo (turborepo)';
74
+ if (fs.existsSync(path.join(rootDir, 'apps')) && fs.existsSync(path.join(rootDir, 'packages'))) {
75
+ if (pattern === 'monolith')
76
+ pattern = 'monorepo';
77
+ }
78
+ // Routing
79
+ let routing = '';
80
+ const hasAppRouter = files.some((f) => /^(src\/)?app\/(layout|page)\.(tsx?|jsx?)/.test(f.relativePath));
81
+ const hasPagesRouter = files.some((f) => /^(src\/)?pages\//.test(f.relativePath));
82
+ if (hasAppRouter)
83
+ routing = 'Next.js App Router';
84
+ else if (hasPagesRouter)
85
+ routing = 'Next.js Pages Router';
86
+ else if (stack.frameworks.includes('nestjs'))
87
+ routing = 'NestJS controllers';
88
+ else if (stack.frameworks.includes('fastify'))
89
+ routing = 'Fastify routes';
90
+ else if (stack.frameworks.includes('hono'))
91
+ routing = 'Hono routes';
92
+ else if (stack.frameworks.includes('express'))
93
+ routing = 'Express routes';
94
+ // Data pattern
95
+ let dataPattern = '';
96
+ const hasActionsDir = files.some((f) => f.relativePath.includes('/actions/') || f.relativePath.endsWith('actions.ts'));
97
+ const hasApiRoutes = files.some((f) => /\/(api|routes?)\//.test(f.relativePath) &&
98
+ (f.relativePath.endsWith('route.ts') || f.relativePath.endsWith('route.js') || f.relativePath.includes('[')));
99
+ if (stack.frameworks.includes('trpc'))
100
+ dataPattern = 'tRPC';
101
+ else if (hasActionsDir && hasAppRouter)
102
+ dataPattern = 'Server Actions + API routes';
103
+ else if (hasApiRoutes)
104
+ dataPattern = 'REST API routes';
105
+ else if (stack.frameworks.includes('graphql'))
106
+ dataPattern = 'GraphQL';
107
+ // Auth
108
+ let auth = '';
109
+ if (stack.frameworks.includes('supabase'))
110
+ auth = 'Supabase Auth';
111
+ else if (stack.frameworks.includes('clerk'))
112
+ auth = 'Clerk';
113
+ else if (stack.frameworks.includes('auth.js'))
114
+ auth = 'Auth.js (NextAuth)';
115
+ // ORM / DB
116
+ let orm = '';
117
+ if (stack.frameworks.includes('prisma'))
118
+ orm = 'Prisma';
119
+ else if (stack.frameworks.includes('drizzle'))
120
+ orm = 'Drizzle ORM';
121
+ else if (stack.frameworks.includes('supabase') && !auth.includes('Supabase'))
122
+ orm = 'Supabase (PostgreSQL)';
123
+ else if (stack.frameworks.includes('supabase'))
124
+ orm = 'Supabase (PostgreSQL)';
125
+ // Testing
126
+ let testing = '';
127
+ if (stack.frameworks.includes('vitest'))
128
+ testing = 'Vitest';
129
+ else if (stack.frameworks.includes('jest'))
130
+ testing = 'Jest';
131
+ if (files.some((f) => /playwright|e2e/.test(f.relativePath)))
132
+ testing += (testing ? ' + ' : '') + 'Playwright';
133
+ if (files.some((f) => /cypress/.test(f.relativePath)))
134
+ testing += (testing ? ' + ' : '') + 'Cypress';
135
+ return { pattern, routing, dataPattern, auth, orm, testing };
136
+ }
137
+ // ─── Directory tree with descriptions ────────────────────────────────────────
138
+ const DIR_DESCRIPTIONS = {
139
+ app: 'Next.js App Router — pages, layouts, API routes',
140
+ pages: 'Next.js Pages Router',
141
+ src: 'source root',
142
+ components: 'React components',
143
+ ui: 'UI component library',
144
+ lib: 'utilities and shared logic',
145
+ utils: 'utility functions',
146
+ hooks: 'React hooks',
147
+ api: 'API handlers',
148
+ types: 'TypeScript types',
149
+ styles: 'CSS / styling',
150
+ public: 'static assets',
151
+ tests: 'test files',
152
+ __tests__: 'test files',
153
+ e2e: 'end-to-end tests',
154
+ config: 'configuration',
155
+ scripts: 'utility scripts',
156
+ docs: 'documentation',
157
+ prisma: 'Prisma schema + migrations',
158
+ drizzle: 'Drizzle schema + migrations',
159
+ migrations: 'database migrations',
160
+ middleware: 'request middleware',
161
+ store: 'state management',
162
+ context: 'React context providers',
163
+ server: 'server-side code',
164
+ client: 'client-side code',
165
+ providers: 'context / service providers',
166
+ actions: 'Server Actions',
167
+ emails: 'email templates',
168
+ packages: 'monorepo packages',
169
+ apps: 'monorepo applications',
170
+ features: 'feature modules',
171
+ modules: 'domain modules',
172
+ };
173
+ function inferDirDescription(dirName, files) {
174
+ if (DIR_DESCRIPTIONS[dirName])
175
+ return DIR_DESCRIPTIONS[dirName];
176
+ const dirFiles = files.filter((f) => f.relativePath.startsWith(dirName + '/'));
177
+ if (dirFiles.length === 0)
178
+ return '';
179
+ const typeCounts = {};
180
+ for (const f of dirFiles)
181
+ typeCounts[f.type] = (typeCounts[f.type] ?? 0) + 1;
182
+ const dominant = Object.entries(typeCounts).sort((a, b) => b[1] - a[1])[0]?.[0];
183
+ const map = {
184
+ component: 'React components',
185
+ hook: 'React hooks',
186
+ api: 'API handlers',
187
+ page: 'pages',
188
+ types: 'type definitions',
189
+ utility: 'utility functions',
190
+ config: 'configuration',
191
+ };
192
+ return map[dominant ?? ''] ?? `${dirFiles.length} files`;
193
+ }
194
+ function buildDirectoryTree(files) {
195
+ const topDirSet = new Set();
196
+ for (const f of files) {
197
+ const parts = f.relativePath.split('/');
198
+ if (parts.length > 1)
199
+ topDirSet.add(parts[0]);
200
+ }
201
+ const topDirs = [...topDirSet].slice(0, 12);
202
+ const lines = ['```'];
203
+ lines.push('.');
204
+ for (let i = 0; i < topDirs.length; i++) {
205
+ const dir = topDirs[i];
206
+ const isLast = i === topDirs.length - 1;
207
+ const prefix = isLast ? '└── ' : '├── ';
208
+ const desc = inferDirDescription(dir, files);
209
+ lines.push(`${prefix}${dir}/ ${desc ? `← ${desc}` : ''}`);
210
+ // Add up to 3 important subdirs
211
+ const subDirSet = new Set();
212
+ for (const f of files) {
213
+ const parts = f.relativePath.split('/');
214
+ if (parts[0] === dir && parts.length > 2)
215
+ subDirSet.add(parts[1]);
216
+ }
217
+ const subDirs = [...subDirSet].slice(0, 3);
218
+ if (subDirs.length > 0 && lines.length < 30) {
219
+ const connector = isLast ? ' ' : '│ ';
220
+ for (let j = 0; j < subDirs.length; j++) {
221
+ const sub = subDirs[j];
222
+ const subIsLast = j === subDirs.length - 1;
223
+ const subPrefix = subIsLast ? '└── ' : '├── ';
224
+ const subDesc = inferDirDescription(sub, files);
225
+ lines.push(`${connector}${subPrefix}${sub}/ ${subDesc ? `← ${subDesc}` : ''}`);
226
+ }
227
+ }
228
+ }
229
+ lines.push('```');
230
+ return lines;
231
+ }
232
+ function computeFileHeat(files) {
233
+ const heatMap = new Map();
234
+ for (const file of files) {
235
+ const basename = path.basename(file.relativePath, path.extname(file.relativePath));
236
+ for (const other of files) {
237
+ if (other.relativePath === file.relativePath)
238
+ continue;
239
+ if (other.imports.some((imp) => imp.includes(basename) || imp.endsWith(file.relativePath))) {
240
+ heatMap.set(file.relativePath, (heatMap.get(file.relativePath) ?? 0) + 1);
241
+ }
242
+ }
243
+ }
244
+ return heatMap;
245
+ }
246
+ function getTopModules(files) {
247
+ const heatMap = computeFileHeat(files);
248
+ const moduleMap = new Map();
249
+ for (const file of files) {
250
+ const parts = file.relativePath.split('/');
251
+ let moduleName = parts[0];
252
+ if (['src', 'app', 'pages', 'lib', 'components'].includes(parts[0]) && parts.length >= 3) {
253
+ moduleName = parts[1];
254
+ }
255
+ if (!moduleMap.has(moduleName))
256
+ moduleMap.set(moduleName, { files: [], totalHeat: 0 });
257
+ const mod = moduleMap.get(moduleName);
258
+ mod.files.push(file);
259
+ mod.totalHeat += heatMap.get(file.relativePath) ?? 0;
260
+ }
261
+ return [...moduleMap.entries()]
262
+ .sort((a, b) => b[1].totalHeat - a[1].totalHeat)
263
+ .slice(0, 5)
264
+ .map(([name, data]) => {
265
+ const hottest = [...data.files].sort((a, b) => (heatMap.get(b.relativePath) ?? 0) - (heatMap.get(a.relativePath) ?? 0))[0];
266
+ return {
267
+ name,
268
+ keyFile: hottest?.relativePath ?? '',
269
+ description: inferDirDescription(name, data.files),
270
+ fileCount: data.files.length,
271
+ heat: data.totalHeat,
272
+ };
273
+ });
274
+ }
275
+ // ─── Statistical convention detection ────────────────────────────────────────
276
+ function detectConventions(files) {
277
+ const conventions = [];
278
+ // App Router vs Pages Router
279
+ const hasAppRouter = files.some((f) => /^(src\/)?app\//.test(f.relativePath));
280
+ const hasPagesRouter = files.some((f) => /^(src\/)?pages\//.test(f.relativePath));
281
+ if (hasAppRouter)
282
+ conventions.push("Next.js App Router — Server Components by default, 'use client' for interactive");
283
+ if (hasPagesRouter)
284
+ conventions.push('Next.js Pages Router');
285
+ // Import alias analysis (sample 30 files × 10 imports)
286
+ let absoluteCount = 0;
287
+ let relativeCount = 0;
288
+ for (const f of files.slice(0, 30)) {
289
+ for (const imp of f.importDetails.slice(0, 10)) {
290
+ if (!imp.isExternal) {
291
+ if (imp.source.startsWith('@/') || imp.source.startsWith('~/') || imp.source.startsWith('#')) {
292
+ absoluteCount++;
293
+ }
294
+ else if (imp.source.startsWith('.')) {
295
+ relativeCount++;
296
+ }
297
+ }
298
+ }
299
+ }
300
+ if (absoluteCount > relativeCount && absoluteCount > 0) {
301
+ conventions.push(`Absolute imports (@/ alias) — ${absoluteCount} occurrences`);
302
+ }
303
+ else if (relativeCount > 0) {
304
+ conventions.push('Relative imports (./, ../)');
305
+ }
306
+ // Named vs default exports
307
+ let namedCount = 0;
308
+ let defaultCount = 0;
309
+ for (const f of files.slice(0, 40)) {
310
+ for (const exp of f.exportDetails) {
311
+ if (exp.type === 'default')
312
+ defaultCount++;
313
+ else
314
+ namedCount++;
315
+ }
316
+ }
317
+ if (namedCount > defaultCount * 2) {
318
+ conventions.push('Named exports preferred over default exports');
319
+ }
320
+ else if (defaultCount > namedCount) {
321
+ conventions.push('Default exports for pages/components, named for utilities');
322
+ }
323
+ // Barrel files
324
+ const barrelCount = files.filter((f) => /index\.(ts|tsx|js|jsx)$/.test(f.relativePath)).length;
325
+ if (barrelCount > 3)
326
+ conventions.push(`Barrel exports via index files (${barrelCount} found)`);
327
+ // TypeScript ratio
328
+ const tsCount = files.filter((f) => f.language === 'typescript').length;
329
+ if (tsCount / Math.max(files.length, 1) > 0.7) {
330
+ conventions.push('TypeScript strict — avoid `any`, use explicit types');
331
+ }
332
+ // Feature-based org
333
+ if (files.some((f) => /\/(features|modules|domain)\//.test(f.relativePath))) {
334
+ conventions.push('Feature-based module organization');
335
+ }
336
+ return conventions.slice(0, 6);
337
+ }
338
+ // ─── Commands from package.json ───────────────────────────────────────────────
339
+ function getCommands(rootDir) {
340
+ try {
341
+ const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
342
+ const scripts = pkg.scripts ?? {};
343
+ const result = {};
344
+ const wanted = ['dev', 'start', 'build', 'test', 'lint', 'typecheck', 'type-check', 'format'];
345
+ for (const key of wanted) {
346
+ if (scripts[key])
347
+ result[key] = scripts[key];
348
+ }
349
+ return result;
350
+ }
351
+ catch {
352
+ return {};
353
+ }
354
+ }
355
+ function detectApiEndpoints(files) {
356
+ const endpoints = [];
357
+ for (const file of files) {
358
+ // App Router: app/api/**/route.ts
359
+ const appRoute = file.relativePath.match(/^(?:src\/)?app\/(api\/.+)\/route\.(ts|js)$/);
360
+ if (appRoute) {
361
+ const routePath = '/' + appRoute[1].replace(/\[([^\]]+)\]/g, ':$1');
362
+ const methods = file.exportDetails
363
+ .filter((e) => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(e.name))
364
+ .map((e) => e.name);
365
+ if (methods.length === 0) {
366
+ endpoints.push({ method: 'GET', path: routePath, file: file.relativePath });
367
+ }
368
+ else {
369
+ for (const method of methods) {
370
+ endpoints.push({ method, path: routePath, file: file.relativePath });
371
+ }
372
+ }
373
+ continue;
374
+ }
375
+ // Pages Router: pages/api/*.ts
376
+ const pagesApi = file.relativePath.match(/^(?:src\/)?pages\/(api\/.+)\.(ts|js)$/);
377
+ if (pagesApi) {
378
+ const routePath = '/' + pagesApi[1].replace(/\[([^\]]+)\]/g, ':$1');
379
+ endpoints.push({ method: 'GET|POST', path: routePath, file: file.relativePath });
380
+ }
381
+ }
382
+ return endpoints.slice(0, 40);
383
+ }
384
+ function detectComponents(files) {
385
+ const components = [];
386
+ for (const file of files) {
387
+ if (!['component', 'page', 'layout'].includes(file.type))
388
+ continue;
389
+ const defaultExp = file.exportDetails.find((e) => e.type === 'default');
390
+ const name = defaultExp?.name ?? path.basename(file.relativePath, path.extname(file.relativePath));
391
+ const props = defaultExp?.params.slice(0, 5) ?? file.functions[0]?.params.slice(0, 5) ?? [];
392
+ components.push({
393
+ name,
394
+ file: file.relativePath,
395
+ props,
396
+ isLayout: file.type === 'layout',
397
+ isPage: file.type === 'page',
398
+ });
399
+ }
400
+ return components
401
+ .sort((a, b) => {
402
+ if (a.isLayout !== b.isLayout)
403
+ return a.isLayout ? -1 : 1;
404
+ if (a.isPage !== b.isPage)
405
+ return a.isPage ? -1 : 1;
406
+ return 0;
407
+ })
408
+ .slice(0, 25);
409
+ }
410
+ // ─── Main generator ───────────────────────────────────────────────────────────
411
+ export function generateClaudeMd(opts) {
412
+ const { rootDir, projectName, stack, files } = opts;
413
+ const description = parseReadmeDescription(rootDir);
414
+ const stackLine = formatStackOneLine(stack);
415
+ const arch = detectArchitecture(rootDir, files, stack);
416
+ const conventions = detectConventions(files);
417
+ const commands = getCommands(rootDir);
418
+ const topModules = getTopModules(files);
419
+ const dirTree = buildDirectoryTree(files);
420
+ const apiEndpoints = detectApiEndpoints(files);
421
+ const components = detectComponents(files);
422
+ const lines = [];
423
+ // Header
424
+ lines.push(`# ${projectName}`);
425
+ lines.push('');
426
+ if (description) {
427
+ lines.push(description);
428
+ lines.push('');
429
+ }
430
+ // Stack — one line
431
+ lines.push('## Stack');
432
+ lines.push(stackLine);
433
+ lines.push('');
434
+ // Structure — tree with descriptions
435
+ lines.push('## Structure');
436
+ for (const treeLine of dirTree)
437
+ lines.push(treeLine);
438
+ lines.push('');
439
+ // Architecture
440
+ lines.push('## Architecture');
441
+ if (arch.pattern)
442
+ lines.push(`- Pattern: ${arch.pattern}`);
443
+ if (arch.routing)
444
+ lines.push(`- Routing: ${arch.routing}`);
445
+ if (arch.dataPattern)
446
+ lines.push(`- Data: ${arch.dataPattern}`);
447
+ if (arch.auth)
448
+ lines.push(`- Auth: ${arch.auth}`);
449
+ if (arch.orm)
450
+ lines.push(`- DB: ${arch.orm}`);
451
+ if (arch.testing)
452
+ lines.push(`- Testing: ${arch.testing}`);
453
+ lines.push('');
454
+ // Modules — top 5 by heat
455
+ if (topModules.length > 0) {
456
+ lines.push('## Modules');
457
+ for (const mod of topModules.slice(0, 5)) {
458
+ const desc = mod.description ? ` — ${mod.description}` : '';
459
+ const key = mod.keyFile ? ` (key: \`${mod.keyFile}\`)` : '';
460
+ lines.push(`- **${mod.name}/**${desc}${key}`);
461
+ }
462
+ lines.push('');
463
+ }
464
+ // Conventions — real, detected
465
+ if (conventions.length > 0) {
466
+ lines.push('## Conventions');
467
+ for (const c of conventions)
468
+ lines.push(`- ${c}`);
469
+ lines.push('');
470
+ }
471
+ // Commands
472
+ const cmdEntries = Object.entries(commands);
473
+ if (cmdEntries.length > 0) {
474
+ lines.push('## Commands');
475
+ for (const [cmd, script] of cmdEntries) {
476
+ lines.push(`- \`npm run ${cmd}\` — \`${script}\``);
477
+ }
478
+ lines.push('');
479
+ }
480
+ // Extended docs
481
+ const hasApis = apiEndpoints.length > 0;
482
+ const hasFrontend = files.some((f) => ['component', 'page', 'layout'].includes(f.type));
483
+ lines.push('## More context');
484
+ lines.push('- `.repomap/docs/architecture.md` — dependency graph, data flow, error handling');
485
+ lines.push('- `.repomap/docs/conventions.md` — naming, imports, file organization, testing');
486
+ if (hasApis)
487
+ lines.push('- `.repomap/docs/api-reference.md` — all API endpoints');
488
+ if (hasFrontend)
489
+ lines.push('- `.repomap/docs/components.md` — component hierarchy and props');
490
+ lines.push('');
491
+ // Enforce ≤ 80 lines
492
+ const main = lines.slice(0, 80).join('\n');
493
+ // ── Overflow: architecture.md ─────────────────────────────────────────────
494
+ const archLines = ['# Architecture', ''];
495
+ archLines.push(`## Pattern: ${arch.pattern}`, '');
496
+ if (arch.routing)
497
+ archLines.push(`**Routing:** ${arch.routing}`);
498
+ if (arch.dataPattern)
499
+ archLines.push(`**Data:** ${arch.dataPattern}`);
500
+ if (arch.auth)
501
+ archLines.push(`**Auth:** ${arch.auth}`);
502
+ if (arch.orm)
503
+ archLines.push(`**DB/ORM:** ${arch.orm}`);
504
+ archLines.push('');
505
+ archLines.push('## Module Heat Map', '');
506
+ archLines.push('(Files imported by the most other files = highest heat)', '');
507
+ for (const mod of topModules) {
508
+ archLines.push(`### ${mod.name}/ — ${mod.fileCount} files, heat: ${mod.heat}`);
509
+ if (mod.description)
510
+ archLines.push(mod.description);
511
+ if (mod.keyFile)
512
+ archLines.push(`Key file: \`${mod.keyFile}\``);
513
+ archLines.push('');
514
+ }
515
+ archLines.push('## Data Flow', '');
516
+ if (arch.routing.includes('App Router')) {
517
+ archLines.push('1. HTTP request → Next.js middleware (if any)');
518
+ archLines.push('2. → Server Component — fetch data inline');
519
+ if (arch.dataPattern.includes('Server Actions'))
520
+ archLines.push('3. → Server Actions for mutations');
521
+ if (arch.dataPattern.includes('API'))
522
+ archLines.push('3. → API route handlers for external/client calls');
523
+ if (arch.orm)
524
+ archLines.push(`4. → ${arch.orm} → database`);
525
+ }
526
+ else if (arch.routing.includes('Express') || arch.routing.includes('Fastify') || arch.routing.includes('Hono') || arch.routing.includes('NestJS')) {
527
+ archLines.push('1. HTTP request → middleware chain');
528
+ archLines.push('2. → route handler / controller');
529
+ if (arch.orm)
530
+ archLines.push(`3. → ${arch.orm} → database`);
531
+ archLines.push('4. → JSON response');
532
+ }
533
+ else {
534
+ archLines.push('See route handlers for data flow details.');
535
+ }
536
+ archLines.push('');
537
+ archLines.push('## Error Handling', '');
538
+ if (files.some((f) => /error\.(tsx?|jsx?)/.test(f.relativePath))) {
539
+ archLines.push('- Next.js error.tsx boundaries (per-route)');
540
+ }
541
+ if (files.some((f) => f.relativePath.includes('not-found'))) {
542
+ archLines.push('- Custom 404 not-found page');
543
+ }
544
+ archLines.push('- Check route handlers for try/catch patterns');
545
+ // ── Overflow: conventions.md ──────────────────────────────────────────────
546
+ const convLines = ['# Conventions', '', '## Detected Patterns', ''];
547
+ for (const conv of conventions)
548
+ convLines.push(`- ${conv}`);
549
+ convLines.push('');
550
+ convLines.push('## Naming', '');
551
+ const tsxNames = files.filter((f) => f.relativePath.endsWith('.tsx')).map((f) => path.basename(f.relativePath));
552
+ const utilNames = files.filter((f) => f.type === 'utility').map((f) => path.basename(f.relativePath));
553
+ const hasPascal = tsxNames.filter((n) => /^[A-Z]/.test(n)).length > tsxNames.length / 2;
554
+ const hasKebab = utilNames.filter((n) => n.includes('-')).length > utilNames.length / 2;
555
+ if (hasPascal)
556
+ convLines.push('- Components: PascalCase (Button.tsx, UserCard.tsx)');
557
+ if (hasKebab)
558
+ convLines.push('- Utilities: kebab-case (use-auth.ts, format-date.ts)');
559
+ convLines.push('- Variables/functions: camelCase');
560
+ convLines.push('- TypeScript types/interfaces: PascalCase');
561
+ convLines.push('');
562
+ convLines.push('## Imports', '');
563
+ const sampleImports = files.slice(0, 25).flatMap((f) => f.importDetails.slice(0, 5));
564
+ const hasAlias = sampleImports.some((i) => i.source.startsWith('@/') || i.source.startsWith('~/'));
565
+ convLines.push(hasAlias ? '- Path alias: `@/` maps to source root' : '- Relative imports: ./ and ../');
566
+ convLines.push('- External packages by bare name (react, next, etc.)');
567
+ convLines.push('');
568
+ convLines.push('## File Organization', '');
569
+ const hasSrc = files.some((f) => f.relativePath.startsWith('src/'));
570
+ convLines.push(hasSrc ? '- Source files under `src/`' : '- Source files at project root');
571
+ const barrelCount = files.filter((f) => /index\.(ts|tsx|js|jsx)$/.test(f.relativePath)).length;
572
+ if (barrelCount > 2)
573
+ convLines.push(`- ${barrelCount} barrel files (index.ts) for module re-exports`);
574
+ convLines.push('');
575
+ if (arch.testing) {
576
+ convLines.push('## Testing', '');
577
+ convLines.push(`- Framework: ${arch.testing}`);
578
+ const testFiles = files.filter((f) => /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(f.relativePath));
579
+ convLines.push(`- ${testFiles.length} test files detected`);
580
+ const hasCollocated = testFiles.some((f) => {
581
+ const dir = path.dirname(f.relativePath);
582
+ return files.some((o) => o !== f && path.dirname(o.relativePath) === dir);
583
+ });
584
+ convLines.push(hasCollocated ? '- Tests colocated with source files' : '- Tests in separate directory');
585
+ }
586
+ // ── Overflow: api-reference.md ────────────────────────────────────────────
587
+ const apiLines = ['# API Reference', ''];
588
+ if (apiEndpoints.length > 0) {
589
+ apiLines.push(`${apiEndpoints.length} endpoint(s) detected.`, '');
590
+ const grouped = new Map();
591
+ for (const ep of apiEndpoints) {
592
+ const base = ep.path.split('/').slice(0, 3).join('/') || '/';
593
+ if (!grouped.has(base))
594
+ grouped.set(base, []);
595
+ grouped.get(base).push(ep);
596
+ }
597
+ for (const [base, eps] of grouped.entries()) {
598
+ apiLines.push(`## ${base}`);
599
+ for (const ep of eps) {
600
+ apiLines.push(`- \`${ep.method} ${ep.path}\` → \`${ep.file}\``);
601
+ }
602
+ apiLines.push('');
603
+ }
604
+ }
605
+ else {
606
+ apiLines.push('No API routes detected.');
607
+ }
608
+ // ── Overflow: components.md ───────────────────────────────────────────────
609
+ const compLines = ['# Components', ''];
610
+ const layouts = components.filter((c) => c.isLayout);
611
+ const pages = components.filter((c) => c.isPage && !c.isLayout);
612
+ const uiComps = components.filter((c) => !c.isLayout && !c.isPage);
613
+ if (layouts.length > 0) {
614
+ compLines.push('## Layout Hierarchy', '');
615
+ for (const l of layouts) {
616
+ compLines.push(`### ${l.name}`);
617
+ compLines.push(`File: \`${l.file}\``);
618
+ if (l.props.length > 0)
619
+ compLines.push(`Props: ${l.props.join(', ')}`);
620
+ compLines.push('');
621
+ }
622
+ }
623
+ if (pages.length > 0) {
624
+ compLines.push('## Pages', '');
625
+ for (const p of pages.slice(0, 20)) {
626
+ compLines.push(`- \`${p.file}\` — ${p.name}`);
627
+ }
628
+ compLines.push('');
629
+ }
630
+ if (uiComps.length > 0) {
631
+ compLines.push('## UI Components', '');
632
+ for (const comp of uiComps.slice(0, 25)) {
633
+ const propsStr = comp.props.length > 0 ? ` (${comp.props.join(', ')})` : '';
634
+ compLines.push(`- **${comp.name}**${propsStr} — \`${comp.file}\``);
635
+ }
636
+ }
637
+ return {
638
+ main,
639
+ overflow: {
640
+ 'architecture.md': archLines.join('\n'),
641
+ 'conventions.md': convLines.join('\n'),
642
+ 'api-reference.md': apiLines.join('\n'),
643
+ 'components.md': compLines.join('\n'),
644
+ },
645
+ };
646
+ }
647
+ export function writeClaudeMd(rootDir, content) {
648
+ const claudeMdPath = path.join(rootDir, 'CLAUDE.md');
649
+ fs.writeFileSync(claudeMdPath, content.main, 'utf-8');
650
+ const docsDir = path.join(rootDir, '.repomap', 'docs');
651
+ fs.mkdirSync(docsDir, { recursive: true });
652
+ for (const [filename, fileContent] of Object.entries(content.overflow)) {
653
+ fs.writeFileSync(path.join(docsDir, filename), fileContent, 'utf-8');
654
+ }
655
+ }
656
+ //# sourceMappingURL=claude-md.js.map