codebrief 1.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,3578 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/utils.ts
13
+ var utils_exports = {};
14
+ __export(utils_exports, {
15
+ ensureDir: () => ensureDir,
16
+ estimateTokens: () => estimateTokens,
17
+ fileExists: () => fileExists,
18
+ formatBytes: () => formatBytes,
19
+ readDirSafe: () => readDirSafe,
20
+ readFileOr: () => readFileOr,
21
+ readJsonFile: () => readJsonFile,
22
+ writeFileSafe: () => writeFileSafe
23
+ });
24
+ import fs from "fs/promises";
25
+ import path from "path";
26
+ async function fileExists(filePath) {
27
+ try {
28
+ await fs.access(filePath);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+ async function readFileOr(filePath) {
35
+ try {
36
+ return await fs.readFile(filePath, "utf-8");
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+ async function readJsonFile(filePath) {
42
+ const content = await readFileOr(filePath);
43
+ if (!content) return null;
44
+ try {
45
+ return JSON.parse(content);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ function estimateTokens(text2) {
51
+ if (text2.length === 0) return 0;
52
+ const symbolCount = text2.replace(/[\w\s]/g, "").length;
53
+ const symbolRatio = symbolCount / text2.length;
54
+ const charsPerToken = symbolRatio > 0.08 ? 3.2 : 3.5;
55
+ return Math.ceil(text2.length / charsPerToken);
56
+ }
57
+ function formatBytes(bytes) {
58
+ if (bytes < 1024) return `${bytes} B`;
59
+ return `${(bytes / 1024).toFixed(1)} KB`;
60
+ }
61
+ async function ensureDir(dirPath) {
62
+ await fs.mkdir(dirPath, { recursive: true });
63
+ }
64
+ async function writeFileSafe(filePath, content) {
65
+ await ensureDir(path.dirname(filePath));
66
+ await fs.writeFile(filePath, content, "utf-8");
67
+ }
68
+ async function readDirSafe(dirPath) {
69
+ try {
70
+ return await fs.readdir(dirPath);
71
+ } catch {
72
+ return [];
73
+ }
74
+ }
75
+ var init_utils = __esm({
76
+ "src/utils.ts"() {
77
+ "use strict";
78
+ }
79
+ });
80
+
81
+ // src/index.ts
82
+ import path8 from "path";
83
+ import * as p4 from "@clack/prompts";
84
+ import pc3 from "picocolors";
85
+
86
+ // src/detect.ts
87
+ init_utils();
88
+ import path2 from "path";
89
+ import fg from "fast-glob";
90
+ var KNOWN_DIRS = [
91
+ "src",
92
+ "app",
93
+ "pages",
94
+ "components",
95
+ "services",
96
+ "stores",
97
+ "store",
98
+ "lib",
99
+ "utils",
100
+ "hooks",
101
+ "api",
102
+ "tests",
103
+ "__tests__",
104
+ "test",
105
+ "public",
106
+ "assets",
107
+ "styles",
108
+ "config",
109
+ "scripts",
110
+ "docs",
111
+ "types"
112
+ ];
113
+ var FRAMEWORK_MAP = {
114
+ // JS/TS
115
+ expo: "Expo",
116
+ "react-native": "React Native",
117
+ next: "Next.js",
118
+ react: "React",
119
+ vue: "Vue",
120
+ nuxt: "Nuxt",
121
+ svelte: "Svelte",
122
+ "@sveltejs/kit": "SvelteKit",
123
+ angular: "Angular",
124
+ "@angular/core": "Angular",
125
+ express: "Express",
126
+ fastify: "Fastify",
127
+ hono: "Hono",
128
+ "nestjs/core": "NestJS",
129
+ "@nestjs/core": "NestJS",
130
+ electron: "Electron",
131
+ tauri: "Tauri",
132
+ // State management
133
+ zustand: "Zustand",
134
+ redux: "Redux",
135
+ "@reduxjs/toolkit": "Redux Toolkit",
136
+ pinia: "Pinia",
137
+ mobx: "MobX",
138
+ jotai: "Jotai",
139
+ recoil: "Recoil",
140
+ // Testing
141
+ jest: "Jest",
142
+ vitest: "Vitest",
143
+ playwright: "Playwright",
144
+ cypress: "Cypress",
145
+ // Styling
146
+ tailwindcss: "Tailwind CSS",
147
+ nativewind: "NativeWind",
148
+ "styled-components": "styled-components",
149
+ "@emotion/react": "Emotion",
150
+ // ORM/DB
151
+ prisma: "Prisma",
152
+ "@prisma/client": "Prisma",
153
+ drizzle: "Drizzle",
154
+ "drizzle-orm": "Drizzle",
155
+ typeorm: "TypeORM",
156
+ mongoose: "Mongoose"
157
+ };
158
+ var PYTHON_FRAMEWORK_MAP = {
159
+ django: "Django",
160
+ flask: "Flask",
161
+ fastapi: "FastAPI",
162
+ starlette: "Starlette",
163
+ sqlalchemy: "SQLAlchemy",
164
+ pydantic: "Pydantic",
165
+ pytest: "pytest",
166
+ celery: "Celery"
167
+ };
168
+ async function detectContext(rootDir, onProgress) {
169
+ const ctx = {
170
+ rootDir,
171
+ language: "other",
172
+ hasTypeScript: false,
173
+ packageManager: "none",
174
+ linter: "none",
175
+ frameworks: [],
176
+ directories: [],
177
+ dependencies: [],
178
+ isGitRepo: false,
179
+ totalSourceBytes: 0,
180
+ sourceFileCount: 0,
181
+ monorepo: null
182
+ };
183
+ onProgress?.("Checking project markers...");
184
+ const [
185
+ hasGit,
186
+ hasPackageJson,
187
+ hasGoMod,
188
+ hasCargo,
189
+ hasPyproject,
190
+ hasRequirements,
191
+ hasTsConfig,
192
+ hasBiome,
193
+ hasPnpmLock,
194
+ hasYarnLock,
195
+ hasBunLock,
196
+ topEntries
197
+ ] = await Promise.all([
198
+ fileExists(path2.join(rootDir, ".git")),
199
+ readJsonFile(path2.join(rootDir, "package.json")),
200
+ fileExists(path2.join(rootDir, "go.mod")),
201
+ fileExists(path2.join(rootDir, "Cargo.toml")),
202
+ readJsonFile(path2.join(rootDir, "pyproject.toml")),
203
+ // won't parse TOML but that's ok
204
+ fileExists(path2.join(rootDir, "requirements.txt")),
205
+ fileExists(path2.join(rootDir, "tsconfig.json")),
206
+ fileExists(path2.join(rootDir, "biome.json")),
207
+ fileExists(path2.join(rootDir, "pnpm-lock.yaml")),
208
+ fileExists(path2.join(rootDir, "yarn.lock")),
209
+ fileExists(path2.join(rootDir, "bun.lockb")),
210
+ readDirSafe(rootDir)
211
+ ]);
212
+ ctx.isGitRepo = hasGit;
213
+ if (hasPackageJson) {
214
+ const pkg = hasPackageJson;
215
+ ctx.language = hasTsConfig ? "typescript" : "javascript";
216
+ ctx.hasTypeScript = hasTsConfig;
217
+ if (hasPnpmLock) ctx.packageManager = "pnpm";
218
+ else if (hasYarnLock) ctx.packageManager = "yarn";
219
+ else if (hasBunLock) ctx.packageManager = "bun";
220
+ else ctx.packageManager = "npm";
221
+ const deps = {
222
+ ...pkg.dependencies,
223
+ ...pkg.devDependencies
224
+ };
225
+ ctx.dependencies = Object.keys(deps);
226
+ const seen = /* @__PURE__ */ new Set();
227
+ for (const dep of ctx.dependencies) {
228
+ const framework = FRAMEWORK_MAP[dep];
229
+ if (framework && !seen.has(framework)) {
230
+ seen.add(framework);
231
+ const version = deps[dep]?.replace(/^[\^~>=<]/, "");
232
+ ctx.frameworks.push({ name: framework, version });
233
+ }
234
+ }
235
+ if (hasBiome) {
236
+ ctx.linter = "biome";
237
+ } else {
238
+ const hasEslint = ctx.dependencies.includes("eslint") || topEntries.some((e) => e.startsWith(".eslintrc"));
239
+ const hasPrettier = ctx.dependencies.includes("prettier") || topEntries.some((e) => e.startsWith(".prettierrc"));
240
+ if (hasEslint) ctx.linter = "eslint";
241
+ else if (hasPrettier) ctx.linter = "prettier";
242
+ }
243
+ } else if (hasGoMod) {
244
+ ctx.language = "go";
245
+ ctx.packageManager = "go";
246
+ ctx.linter = "gofmt";
247
+ } else if (hasCargo) {
248
+ ctx.language = "rust";
249
+ ctx.packageManager = "cargo";
250
+ ctx.linter = "rustfmt";
251
+ } else if (hasPyproject || hasRequirements) {
252
+ ctx.language = "python";
253
+ if (topEntries.includes("poetry.lock")) ctx.packageManager = "poetry";
254
+ else ctx.packageManager = "pip";
255
+ if (hasRequirements) {
256
+ const { readFileOr: readFileOr2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
257
+ const reqContent = await readFileOr2(path2.join(rootDir, "requirements.txt"));
258
+ if (reqContent) {
259
+ const pkgs = reqContent.split("\n").map((l) => l.trim().split(/[=<>!~[]/)[0].toLowerCase()).filter(Boolean);
260
+ for (const pkg of pkgs) {
261
+ const framework = PYTHON_FRAMEWORK_MAP[pkg];
262
+ if (framework) {
263
+ ctx.frameworks.push({ name: framework });
264
+ }
265
+ }
266
+ ctx.dependencies = pkgs;
267
+ }
268
+ }
269
+ const hasRuff = topEntries.includes("ruff.toml") || topEntries.some((e) => e === "pyproject.toml");
270
+ if (hasRuff) ctx.linter = "ruff";
271
+ else ctx.linter = "none";
272
+ }
273
+ if (ctx.frameworks.length > 0) {
274
+ const fwNames = ctx.frameworks.map((f) => f.name).join(", ");
275
+ const lang = ctx.hasTypeScript ? "TypeScript" : ctx.language !== "other" ? ctx.language : "";
276
+ const parts = [lang, fwNames].filter(Boolean);
277
+ onProgress?.(`Detected: ${parts.join(" + ")}`);
278
+ }
279
+ onProgress?.("Scanning directories...");
280
+ for (const dir of KNOWN_DIRS) {
281
+ if (topEntries.includes(dir)) {
282
+ ctx.directories.push(dir);
283
+ }
284
+ }
285
+ if (topEntries.includes("src")) {
286
+ const srcEntries = await readDirSafe(path2.join(rootDir, "src"));
287
+ for (const entry of srcEntries) {
288
+ if (KNOWN_DIRS.includes(entry)) {
289
+ ctx.directories.push(`src/${entry}`);
290
+ }
291
+ }
292
+ }
293
+ try {
294
+ const extensions = getExtensionsForLanguage(ctx.language);
295
+ const sourceFiles = await fg(
296
+ extensions.map((ext) => `**/*${ext}`),
297
+ {
298
+ cwd: rootDir,
299
+ ignore: [
300
+ "**/node_modules/**",
301
+ "**/dist/**",
302
+ "**/build/**",
303
+ "**/.next/**",
304
+ "**/target/**",
305
+ "**/vendor/**",
306
+ "**/__pycache__/**",
307
+ "**/venv/**",
308
+ "**/.venv/**"
309
+ ],
310
+ stats: true
311
+ }
312
+ );
313
+ ctx.sourceFileCount = sourceFiles.length;
314
+ onProgress?.(`Counting ${sourceFiles.length} source files...`);
315
+ ctx.totalSourceBytes = sourceFiles.reduce(
316
+ (sum, f) => sum + (f.stats?.size ?? 0),
317
+ 0
318
+ );
319
+ } catch {
320
+ }
321
+ ctx.testFramework = detectTestFramework(ctx.dependencies);
322
+ ctx.ciProvider = await detectCiProvider(rootDir, topEntries);
323
+ ctx.monorepo = await detectMonorepo(rootDir, topEntries);
324
+ return ctx;
325
+ }
326
+ var TEST_FRAMEWORK_MAP = {
327
+ vitest: "Vitest",
328
+ jest: "Jest",
329
+ playwright: "Playwright",
330
+ cypress: "Cypress",
331
+ mocha: "Mocha",
332
+ pytest: "pytest"
333
+ };
334
+ function detectTestFramework(dependencies) {
335
+ for (const [dep, name] of Object.entries(TEST_FRAMEWORK_MAP)) {
336
+ if (dependencies.includes(dep)) return name;
337
+ }
338
+ return void 0;
339
+ }
340
+ var CI_PATTERNS = [
341
+ { path: ".github/workflows", name: "GitHub Actions", isDir: true },
342
+ { path: ".gitlab-ci.yml", name: "GitLab CI" },
343
+ { path: ".circleci", name: "CircleCI", isDir: true },
344
+ { path: "Jenkinsfile", name: "Jenkins" },
345
+ { path: ".travis.yml", name: "Travis CI" }
346
+ ];
347
+ async function detectCiProvider(rootDir, topEntries) {
348
+ for (const ci of CI_PATTERNS) {
349
+ if (ci.isDir) {
350
+ if (await fileExists(path2.join(rootDir, ci.path))) return ci.name;
351
+ } else {
352
+ if (topEntries.includes(ci.path)) return ci.name;
353
+ }
354
+ }
355
+ return void 0;
356
+ }
357
+ function getExtensionsForLanguage(lang) {
358
+ switch (lang) {
359
+ case "typescript":
360
+ return [".ts", ".tsx"];
361
+ case "javascript":
362
+ return [".js", ".jsx", ".mjs"];
363
+ case "python":
364
+ return [".py"];
365
+ case "go":
366
+ return [".go"];
367
+ case "rust":
368
+ return [".rs"];
369
+ case "java":
370
+ return [".java"];
371
+ default:
372
+ return [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"];
373
+ }
374
+ }
375
+ async function detectMonorepo(rootDir, topEntries) {
376
+ const hasTurboJson = topEntries.includes("turbo.json");
377
+ const hasNxJson = topEntries.includes("nx.json");
378
+ const hasPnpmWorkspace = topEntries.includes("pnpm-workspace.yaml");
379
+ let type = null;
380
+ if (hasTurboJson) type = "turborepo";
381
+ else if (hasNxJson) type = "nx";
382
+ else if (hasPnpmWorkspace) type = "pnpm-workspaces";
383
+ if (!type) return null;
384
+ let packageGlobs = [];
385
+ if (hasPnpmWorkspace || hasTurboJson) {
386
+ const yamlContent = await readFileOr(
387
+ path2.join(rootDir, "pnpm-workspace.yaml")
388
+ );
389
+ if (yamlContent) {
390
+ const lines = yamlContent.split("\n");
391
+ let inPackages = false;
392
+ for (const line of lines) {
393
+ if (/^packages:/i.test(line.trim())) {
394
+ inPackages = true;
395
+ continue;
396
+ }
397
+ if (inPackages) {
398
+ const match = line.match(/^\s+-\s+['"]?([^'"]+)['"]?/);
399
+ if (match) {
400
+ packageGlobs.push(match[1].trim());
401
+ } else if (line.trim() && !line.startsWith(" ") && !line.startsWith(" ")) {
402
+ break;
403
+ }
404
+ }
405
+ }
406
+ }
407
+ }
408
+ if (packageGlobs.length === 0 && hasNxJson) {
409
+ for (const dir of ["packages", "libs", "apps"]) {
410
+ if (topEntries.includes(dir)) {
411
+ packageGlobs.push(`${dir}/*`);
412
+ }
413
+ }
414
+ }
415
+ if (packageGlobs.length === 0) {
416
+ const pkg = await readJsonFile(path2.join(rootDir, "package.json"));
417
+ if (pkg) {
418
+ const workspaces = pkg.workspaces;
419
+ if (Array.isArray(workspaces)) {
420
+ packageGlobs = workspaces;
421
+ } else if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
422
+ packageGlobs = workspaces.packages;
423
+ }
424
+ }
425
+ }
426
+ if (packageGlobs.length === 0) return null;
427
+ const resolvedDirs = await fg(packageGlobs, {
428
+ cwd: rootDir,
429
+ onlyDirectories: true,
430
+ ignore: ["**/node_modules/**"],
431
+ absolute: false
432
+ });
433
+ const packages = [];
434
+ for (const dir of resolvedDirs) {
435
+ const pkgJsonPath = path2.join(rootDir, dir, "package.json");
436
+ const pkgJson = await readJsonFile(pkgJsonPath);
437
+ if (!pkgJson) continue;
438
+ const deps = {
439
+ ...pkgJson.dependencies,
440
+ ...pkgJson.devDependencies
441
+ };
442
+ const depNames = Object.keys(deps);
443
+ const frameworks = [];
444
+ const seen = /* @__PURE__ */ new Set();
445
+ for (const dep of depNames) {
446
+ const framework = FRAMEWORK_MAP[dep];
447
+ if (framework && !seen.has(framework)) {
448
+ seen.add(framework);
449
+ const version = deps[dep]?.replace(/^[\^~>=<]/, "");
450
+ frameworks.push({ name: framework, version });
451
+ }
452
+ }
453
+ packages.push({
454
+ name: pkgJson.name ?? path2.basename(dir),
455
+ path: dir,
456
+ dependencies: depNames,
457
+ frameworks
458
+ });
459
+ }
460
+ if (packages.length === 0) return null;
461
+ return { type, packages };
462
+ }
463
+ function buildReverseFrameworkMap() {
464
+ const reverse = /* @__PURE__ */ new Map();
465
+ for (const [dep, name] of Object.entries(FRAMEWORK_MAP)) {
466
+ const deps = reverse.get(name) ?? [];
467
+ deps.push(dep);
468
+ reverse.set(name, deps);
469
+ }
470
+ return reverse;
471
+ }
472
+ function enrichFrameworksWithUsage(frameworks, externalImportCounts) {
473
+ const reverseMap = buildReverseFrameworkMap();
474
+ return frameworks.map((fw) => {
475
+ const depNames = reverseMap.get(fw.name) ?? [];
476
+ let totalCount = 0;
477
+ for (const dep of depNames) {
478
+ totalCount += externalImportCounts.get(dep) ?? 0;
479
+ }
480
+ return { ...fw, importCount: totalCount };
481
+ }).filter((fw) => fw.importCount === void 0 || fw.importCount > 0);
482
+ }
483
+ function summarizeDetection(ctx) {
484
+ const parts = [];
485
+ if (ctx.frameworks.length > 0) {
486
+ parts.push(ctx.frameworks.map((f) => f.name).join(" + "));
487
+ }
488
+ if (ctx.hasTypeScript) {
489
+ parts.push("TypeScript");
490
+ } else if (ctx.language !== "other") {
491
+ parts.push(ctx.language.charAt(0).toUpperCase() + ctx.language.slice(1));
492
+ }
493
+ if (ctx.linter !== "none") {
494
+ parts.push(ctx.linter.charAt(0).toUpperCase() + ctx.linter.slice(1));
495
+ }
496
+ if (ctx.packageManager !== "none") {
497
+ parts.push(ctx.packageManager);
498
+ }
499
+ return parts.join(" + ");
500
+ }
501
+
502
+ // src/prompts.ts
503
+ import * as p from "@clack/prompts";
504
+ async function runPrompts(detected, defaults) {
505
+ const ideOptions = [
506
+ { value: "claude", label: "Claude Code" },
507
+ { value: "cursor", label: "Cursor" },
508
+ { value: "opencode", label: "OpenCode" },
509
+ { value: "copilot", label: "GitHub Copilot" },
510
+ { value: "windsurf", label: "Windsurf" },
511
+ { value: "cline", label: "Cline" },
512
+ { value: "continue", label: "Continue.dev" },
513
+ { value: "aider", label: "Aider" },
514
+ { value: "generic", label: "Other (generic CONTEXT.md)" }
515
+ ];
516
+ const ide = await p.select({
517
+ message: "Which AI coding tool are you using?",
518
+ options: ideOptions,
519
+ initialValue: defaults?.ide
520
+ });
521
+ if (p.isCancel(ide)) {
522
+ p.cancel("Cancelled.");
523
+ process.exit(0);
524
+ }
525
+ const stackSummary = summarizeDetection(detected);
526
+ let stackConfirmed = true;
527
+ let stackCorrections = defaults?.stackCorrections ?? "";
528
+ if (stackSummary) {
529
+ const confirm3 = await p.confirm({
530
+ message: `Detected: ${stackSummary}. Correct?`
531
+ });
532
+ if (p.isCancel(confirm3)) {
533
+ p.cancel("Cancelled.");
534
+ process.exit(0);
535
+ }
536
+ stackConfirmed = confirm3;
537
+ if (!confirm3) {
538
+ const corrections = await p.text({
539
+ message: "What should I correct? (describe your actual stack)",
540
+ placeholder: "e.g. It's actually Next.js 15 + Prisma, not plain React",
541
+ defaultValue: defaults?.stackCorrections || void 0
542
+ });
543
+ if (p.isCancel(corrections)) {
544
+ p.cancel("Cancelled.");
545
+ process.exit(0);
546
+ }
547
+ stackCorrections = corrections;
548
+ }
549
+ }
550
+ const projectPurpose = await p.text({
551
+ message: "What does this project do? (1-2 sentences)",
552
+ placeholder: "e.g. A mobile AI chat app connecting to OpenAI, Anthropic, and Google APIs",
553
+ defaultValue: defaults?.projectPurpose || void 0,
554
+ validate: (value) => {
555
+ if (!value?.trim()) return "Please describe your project briefly.";
556
+ }
557
+ });
558
+ if (p.isCancel(projectPurpose)) {
559
+ p.cancel("Cancelled.");
560
+ process.exit(0);
561
+ }
562
+ const keyPatterns = await p.text({
563
+ message: "Any key coding patterns or conventions? (optional, press Enter to skip)",
564
+ placeholder: "e.g. Zustand slices for state, NativeWind for styling, expo/fetch for SSE",
565
+ defaultValue: defaults?.keyPatterns || ""
566
+ });
567
+ if (p.isCancel(keyPatterns)) {
568
+ p.cancel("Cancelled.");
569
+ process.exit(0);
570
+ }
571
+ const gotchas = await p.text({
572
+ message: "Any critical gotchas or anti-patterns to avoid? (optional)",
573
+ placeholder: "e.g. Never use FadeIn/FadeOut on ternary components, no @expo/vector-icons",
574
+ defaultValue: defaults?.gotchas || ""
575
+ });
576
+ if (p.isCancel(gotchas)) {
577
+ p.cancel("Cancelled.");
578
+ process.exit(0);
579
+ }
580
+ let generateSnapshot2 = false;
581
+ let snapshotPaths = defaults?.snapshotPaths ?? [];
582
+ if (detected.language === "typescript" || detected.language === "javascript") {
583
+ const snapshotChoice = await p.select({
584
+ message: "Generate a code snapshot? (extracts types, store shapes, component props)",
585
+ options: [
586
+ { value: "auto", label: "Yes, auto-detect key files" },
587
+ { value: "no", label: "No, skip code snapshot" },
588
+ { value: "custom", label: "Yes, but let me specify paths" }
589
+ ],
590
+ initialValue: defaults?.generateSnapshot ? defaults.snapshotPaths.length > 0 ? "custom" : "auto" : void 0
591
+ });
592
+ if (p.isCancel(snapshotChoice)) {
593
+ p.cancel("Cancelled.");
594
+ process.exit(0);
595
+ }
596
+ if (snapshotChoice === "auto") {
597
+ generateSnapshot2 = true;
598
+ snapshotPaths = [];
599
+ } else if (snapshotChoice === "custom") {
600
+ generateSnapshot2 = true;
601
+ const paths = await p.text({
602
+ message: "Paths to scan (comma-separated, relative to project root)",
603
+ placeholder: "e.g. src/types, src/stores, src/components",
604
+ defaultValue: defaults?.snapshotPaths.length ? defaults.snapshotPaths.join(", ") : void 0
605
+ });
606
+ if (p.isCancel(paths)) {
607
+ p.cancel("Cancelled.");
608
+ process.exit(0);
609
+ }
610
+ snapshotPaths = paths.split(",").map((s) => s.trim()).filter(Boolean);
611
+ }
612
+ }
613
+ let generatePerPackage = false;
614
+ if (detected.monorepo && detected.monorepo.packages.length > 0) {
615
+ const mono = detected.monorepo;
616
+ const pkgNames = mono.packages.map((pkg) => pkg.name).join(", ");
617
+ const perPkg = await p.confirm({
618
+ message: `Monorepo detected (${mono.type}, ${mono.packages.length} packages: ${pkgNames}). Generate per-package context files?`,
619
+ initialValue: defaults?.generatePerPackage ?? false
620
+ });
621
+ if (p.isCancel(perPkg)) {
622
+ p.cancel("Cancelled.");
623
+ process.exit(0);
624
+ }
625
+ generatePerPackage = perPkg;
626
+ }
627
+ return {
628
+ ide,
629
+ projectPurpose,
630
+ keyPatterns: keyPatterns ?? "",
631
+ gotchas: gotchas ?? "",
632
+ generateSnapshot: generateSnapshot2,
633
+ snapshotPaths,
634
+ stackConfirmed,
635
+ stackCorrections,
636
+ generatePerPackage
637
+ };
638
+ }
639
+
640
+ // src/snapshot.ts
641
+ init_utils();
642
+ import path4 from "path";
643
+ import fg3 from "fast-glob";
644
+
645
+ // src/graph.ts
646
+ init_utils();
647
+ import path3 from "path";
648
+ import fg2 from "fast-glob";
649
+ var JS_IMPORT_FROM = /import\s+(?:type\s+)?(?:\{([^}]*)\}|(\*\s+as\s+\w+|\w+)(?:\s*,\s*\{([^}]*)\})?)\s+from\s+['"]([^'"]+)['"]/g;
650
+ var JS_IMPORT_SIDE = /import\s+['"]([^'"]+)['"]/g;
651
+ var JS_REQUIRE = /require\(\s*['"]([^'"]+)['"]\s*\)/g;
652
+ var JS_DYNAMIC = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
653
+ var PY_FROM_IMPORT = /^from\s+(\.+[\w.]*|[\w][\w.]*)\s+import\s+(.+)/gm;
654
+ var PY_IMPORT = /^import\s+([\w., ]+)/gm;
655
+ var GO_IMPORT_SINGLE = /import\s+"([^"]+)"/g;
656
+ var GO_IMPORT_BLOCK = /import\s*\(([^)]+)\)/gs;
657
+ var RUST_USE = /(?:pub\s+)?use\s+((?:crate|super|self)(?:::\w+)*(?:::\{[^}]*\})?)/g;
658
+ var RUST_MOD = /mod\s+(\w+)\s*;/g;
659
+ var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
660
+ var INDEX_FILES = JS_EXTENSIONS.map((e) => `/index${e}`);
661
+ function getSourceGlob(lang) {
662
+ switch (lang) {
663
+ case "typescript":
664
+ case "javascript":
665
+ return ["**/*.{ts,tsx,js,jsx,mjs}"];
666
+ case "python":
667
+ return ["**/*.py"];
668
+ case "go":
669
+ return ["**/*.go"];
670
+ case "rust":
671
+ return ["**/*.rs"];
672
+ default:
673
+ return ["**/*.{ts,tsx,js,jsx,py,go,rs}"];
674
+ }
675
+ }
676
+ function parseJsImports(content) {
677
+ const imports = [];
678
+ for (const m of content.matchAll(JS_IMPORT_FROM)) {
679
+ const names = [];
680
+ if (m[1]) names.push(...m[1].split(",").map((n) => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean));
681
+ if (m[2]) {
682
+ const group2 = m[2].trim();
683
+ if (!group2.startsWith("*")) {
684
+ names.push(group2);
685
+ }
686
+ }
687
+ if (m[3]) names.push(...m[3].split(",").map((n) => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean));
688
+ imports.push({ specifier: m[4], importedNames: names });
689
+ }
690
+ for (const m of content.matchAll(JS_IMPORT_SIDE)) {
691
+ if (!content.includes(`from '${m[1]}'`) && !content.includes(`from "${m[1]}"`)) {
692
+ imports.push({ specifier: m[1], importedNames: [] });
693
+ }
694
+ }
695
+ for (const m of content.matchAll(JS_REQUIRE)) {
696
+ imports.push({ specifier: m[1], importedNames: [] });
697
+ }
698
+ for (const m of content.matchAll(JS_DYNAMIC)) {
699
+ imports.push({ specifier: m[1], importedNames: [] });
700
+ }
701
+ return imports;
702
+ }
703
+ function parsePythonImports(content) {
704
+ const imports = [];
705
+ for (const m of content.matchAll(PY_FROM_IMPORT)) {
706
+ const module = m[1];
707
+ const names = m[2].split(",").map((n) => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
708
+ imports.push({ specifier: module, importedNames: names });
709
+ }
710
+ for (const m of content.matchAll(PY_IMPORT)) {
711
+ const modules = m[1].split(",").map((n) => n.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
712
+ for (const mod of modules) {
713
+ imports.push({ specifier: mod, importedNames: [] });
714
+ }
715
+ }
716
+ return imports;
717
+ }
718
+ function parseGoImports(content) {
719
+ const imports = [];
720
+ for (const m of content.matchAll(GO_IMPORT_SINGLE)) {
721
+ imports.push({ specifier: m[1], importedNames: [] });
722
+ }
723
+ for (const m of content.matchAll(GO_IMPORT_BLOCK)) {
724
+ const block = m[1];
725
+ for (const line of block.split("\n")) {
726
+ if (line.trim().startsWith("//")) continue;
727
+ const match = line.match(/["']([^"']+)["']/);
728
+ if (match) {
729
+ imports.push({ specifier: match[1], importedNames: [] });
730
+ }
731
+ }
732
+ }
733
+ return imports;
734
+ }
735
+ function parseRustImports(content) {
736
+ const imports = [];
737
+ for (const m of content.matchAll(RUST_USE)) {
738
+ const usePath = m[1];
739
+ const globMatch = usePath.match(/::\{([^}]*)\}$/);
740
+ if (globMatch) {
741
+ const names = globMatch[1].split(",").map((n) => n.trim()).filter(Boolean);
742
+ imports.push({ specifier: usePath, importedNames: names });
743
+ } else {
744
+ const parts = usePath.split("::");
745
+ const name = parts[parts.length - 1];
746
+ imports.push({ specifier: usePath, importedNames: name ? [name] : [] });
747
+ }
748
+ }
749
+ for (const m of content.matchAll(RUST_MOD)) {
750
+ imports.push({ specifier: m[1], importedNames: [] });
751
+ }
752
+ return imports;
753
+ }
754
+ function parseImports(content, lang) {
755
+ switch (lang) {
756
+ case "typescript":
757
+ case "javascript":
758
+ return parseJsImports(content);
759
+ case "python":
760
+ return parsePythonImports(content);
761
+ case "go":
762
+ return parseGoImports(content);
763
+ case "rust":
764
+ return parseRustImports(content);
765
+ default:
766
+ return parseJsImports(content);
767
+ }
768
+ }
769
+ function isRelativeSpecifier(spec, lang) {
770
+ if (lang === "typescript" || lang === "javascript") {
771
+ return spec.startsWith("./") || spec.startsWith("../");
772
+ }
773
+ if (lang === "python") {
774
+ return spec.startsWith(".");
775
+ }
776
+ if (lang === "rust") {
777
+ return spec.startsWith("crate::") || spec.startsWith("super::") || spec.startsWith("self::");
778
+ }
779
+ return spec.startsWith("./") || spec.startsWith("../");
780
+ }
781
+ function resolveJsImport(specifier, fromFile, allFiles) {
782
+ const dir = path3.dirname(fromFile);
783
+ const raw = path3.join(dir, specifier).replace(/\\/g, "/");
784
+ const stripped = raw.replace(/\.(jsx?|mjs)$/, "");
785
+ const bases = stripped !== raw ? [raw, stripped] : [raw];
786
+ for (const base of bases) {
787
+ if (allFiles.has(base)) return base;
788
+ for (const ext of JS_EXTENSIONS) {
789
+ if (allFiles.has(base + ext)) return base + ext;
790
+ }
791
+ for (const idx of INDEX_FILES) {
792
+ if (allFiles.has(base + idx)) return base + idx;
793
+ }
794
+ }
795
+ return null;
796
+ }
797
+ function resolvePythonImport(specifier, fromFile, allFiles) {
798
+ if (!specifier.startsWith(".")) return null;
799
+ const dir = path3.dirname(fromFile);
800
+ let dots = 0;
801
+ while (specifier[dots] === ".") dots++;
802
+ const modulePath = specifier.slice(dots).replace(/\./g, "/");
803
+ let baseDir = dir;
804
+ for (let i = 1; i < dots; i++) {
805
+ baseDir = path3.dirname(baseDir);
806
+ }
807
+ const base = modulePath ? path3.join(baseDir, modulePath).replace(/\\/g, "/") : baseDir;
808
+ if (allFiles.has(base + ".py")) return base + ".py";
809
+ if (allFiles.has(base + "/__init__.py")) return base + "/__init__.py";
810
+ return null;
811
+ }
812
+ function resolveImport(specifier, fromFile, lang, allFiles) {
813
+ switch (lang) {
814
+ case "typescript":
815
+ case "javascript":
816
+ return resolveJsImport(specifier, fromFile, allFiles);
817
+ case "python":
818
+ return resolvePythonImport(specifier, fromFile, allFiles);
819
+ default:
820
+ return null;
821
+ }
822
+ }
823
+ function computePageRank(files, edges, iterations = 5, damping = 0.85) {
824
+ const n = files.length;
825
+ if (n === 0) return /* @__PURE__ */ new Map();
826
+ const outLinks = /* @__PURE__ */ new Map();
827
+ for (const file of files) outLinks.set(file, []);
828
+ for (const edge of edges) {
829
+ if (!edge.isExternal && outLinks.has(edge.from)) {
830
+ outLinks.get(edge.from).push(edge.to);
831
+ }
832
+ }
833
+ let scores = /* @__PURE__ */ new Map();
834
+ const init = 1 / n;
835
+ for (const file of files) scores.set(file, init);
836
+ for (let iter = 0; iter < iterations; iter++) {
837
+ const next = /* @__PURE__ */ new Map();
838
+ for (const file of files) next.set(file, (1 - damping) / n);
839
+ for (const file of files) {
840
+ const links = outLinks.get(file) ?? [];
841
+ if (links.length === 0) continue;
842
+ const share = damping * (scores.get(file) ?? 0) / links.length;
843
+ for (const target of links) {
844
+ next.set(target, (next.get(target) ?? 0) + share);
845
+ }
846
+ }
847
+ scores = next;
848
+ }
849
+ let max = 0;
850
+ for (const v of scores.values()) {
851
+ if (v > max) max = v;
852
+ }
853
+ if (max > 0) {
854
+ for (const [k, v] of scores) {
855
+ scores.set(k, v / max);
856
+ }
857
+ }
858
+ return scores;
859
+ }
860
+ async function buildImportGraph(rootDir, language, onProgress) {
861
+ const globs = getSourceGlob(language);
862
+ const files = await fg2(globs, {
863
+ cwd: rootDir,
864
+ ignore: [
865
+ "**/node_modules/**",
866
+ "**/dist/**",
867
+ "**/build/**",
868
+ "**/.next/**",
869
+ "**/target/**",
870
+ "**/vendor/**",
871
+ "**/__pycache__/**",
872
+ "**/venv/**",
873
+ "**/.venv/**"
874
+ ],
875
+ absolute: false
876
+ });
877
+ onProgress?.(`Found ${files.length} source files to analyze`);
878
+ const fileSet = new Set(files);
879
+ const edges = [];
880
+ const inDegree = /* @__PURE__ */ new Map();
881
+ const externalImportCounts = /* @__PURE__ */ new Map();
882
+ for (const file of files) inDegree.set(file, 0);
883
+ for (let i = 0; i < files.length; i++) {
884
+ const file = files[i];
885
+ if ((i + 1) % 50 === 0 || i === files.length - 1) {
886
+ onProgress?.(`Parsing imports... ${i + 1}/${files.length} files`);
887
+ }
888
+ const absPath = path3.join(rootDir, file);
889
+ const content = await readFileOr(absPath);
890
+ if (!content) continue;
891
+ const rawImports = parseImports(content, language);
892
+ for (const raw of rawImports) {
893
+ const isRelative = isRelativeSpecifier(raw.specifier, language);
894
+ if (isRelative) {
895
+ const resolved = resolveImport(raw.specifier, file, language, fileSet);
896
+ if (resolved) {
897
+ edges.push({
898
+ from: file,
899
+ to: resolved,
900
+ isExternal: false,
901
+ specifier: raw.specifier,
902
+ importedNames: raw.importedNames
903
+ });
904
+ inDegree.set(resolved, (inDegree.get(resolved) ?? 0) + 1);
905
+ }
906
+ } else {
907
+ const pkgName = getPackageName(raw.specifier);
908
+ edges.push({
909
+ from: file,
910
+ to: pkgName,
911
+ isExternal: true,
912
+ specifier: raw.specifier,
913
+ importedNames: raw.importedNames
914
+ });
915
+ externalImportCounts.set(
916
+ pkgName,
917
+ (externalImportCounts.get(pkgName) ?? 0) + 1
918
+ );
919
+ }
920
+ }
921
+ }
922
+ onProgress?.("Computing centrality (PageRank)...");
923
+ const centrality = computePageRank(files, edges);
924
+ return { edges, inDegree, centrality, externalImportCounts };
925
+ }
926
+ function getPackageName(specifier) {
927
+ if (specifier.startsWith("@")) {
928
+ const parts = specifier.split("/");
929
+ return parts.slice(0, 2).join("/");
930
+ }
931
+ return specifier.split("/")[0];
932
+ }
933
+ function findUsedExports(edges) {
934
+ const used = /* @__PURE__ */ new Set();
935
+ for (const edge of edges) {
936
+ if (edge.isExternal) continue;
937
+ for (const name of edge.importedNames) {
938
+ used.add(`${edge.to}::${name}`);
939
+ }
940
+ }
941
+ return used;
942
+ }
943
+ function getHubFiles(graph, limit = 8) {
944
+ const outCount = /* @__PURE__ */ new Map();
945
+ for (const edge of graph.edges) {
946
+ if (!edge.isExternal) {
947
+ outCount.set(edge.from, (outCount.get(edge.from) ?? 0) + 1);
948
+ }
949
+ }
950
+ const files = [];
951
+ for (const [filePath, centrality] of graph.centrality) {
952
+ const importedBy = graph.inDegree.get(filePath) ?? 0;
953
+ const imports = outCount.get(filePath) ?? 0;
954
+ if (importedBy > 0 || imports > 0) {
955
+ files.push({ path: filePath, centrality, importedBy, imports });
956
+ }
957
+ }
958
+ files.sort((a, b) => b.centrality - a.centrality);
959
+ return files.slice(0, limit);
960
+ }
961
+ function findSCCs(graph) {
962
+ const adj = /* @__PURE__ */ new Map();
963
+ const allFiles = /* @__PURE__ */ new Set();
964
+ for (const edge of graph.edges) {
965
+ if (edge.isExternal) continue;
966
+ allFiles.add(edge.from);
967
+ allFiles.add(edge.to);
968
+ const list = adj.get(edge.from) ?? [];
969
+ list.push(edge.to);
970
+ adj.set(edge.from, list);
971
+ }
972
+ let index = 0;
973
+ const indices = /* @__PURE__ */ new Map();
974
+ const lowlinks = /* @__PURE__ */ new Map();
975
+ const onStack = /* @__PURE__ */ new Set();
976
+ const stack = [];
977
+ const sccs = [];
978
+ function strongconnect(v) {
979
+ indices.set(v, index);
980
+ lowlinks.set(v, index);
981
+ index++;
982
+ stack.push(v);
983
+ onStack.add(v);
984
+ for (const w of adj.get(v) ?? []) {
985
+ if (!indices.has(w)) {
986
+ strongconnect(w);
987
+ lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
988
+ } else if (onStack.has(w)) {
989
+ lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
990
+ }
991
+ }
992
+ if (lowlinks.get(v) === indices.get(v)) {
993
+ const scc = [];
994
+ let w;
995
+ do {
996
+ w = stack.pop();
997
+ onStack.delete(w);
998
+ scc.push(w);
999
+ } while (w !== v);
1000
+ if (scc.length > 1) {
1001
+ sccs.push(scc);
1002
+ }
1003
+ }
1004
+ }
1005
+ for (const file of allFiles) {
1006
+ if (!indices.has(file)) {
1007
+ strongconnect(file);
1008
+ }
1009
+ }
1010
+ return sccs;
1011
+ }
1012
+ function findCircularDeps(graph, maxCycles = 10) {
1013
+ const sccs = findSCCs(graph);
1014
+ sccs.sort((a, b) => a.length - b.length);
1015
+ const cycles = [];
1016
+ for (const scc of sccs) {
1017
+ if (cycles.length >= maxCycles) break;
1018
+ cycles.push({ chain: [...scc, scc[0]] });
1019
+ }
1020
+ return cycles;
1021
+ }
1022
+ var LAYER_PATTERNS = [
1023
+ { name: "types", pattern: /(?:^|\/)types?\// },
1024
+ { name: "stores", pattern: /(?:^|\/)stores?\// },
1025
+ { name: "hooks", pattern: /(?:^|\/)hooks?\// },
1026
+ { name: "services", pattern: /(?:^|\/)(?:services?|api)\// },
1027
+ { name: "components", pattern: /(?:^|\/)components?\// },
1028
+ { name: "pages", pattern: /(?:^|\/)(?:pages?|app|routes?)\// },
1029
+ { name: "utils", pattern: /(?:^|\/)(?:utils?|lib|helpers?)\// },
1030
+ { name: "config", pattern: /(?:^|\/)config\// }
1031
+ ];
1032
+ function detectArchitecturalLayers(graph) {
1033
+ const layerFiles = /* @__PURE__ */ new Map();
1034
+ const fileToLayer = /* @__PURE__ */ new Map();
1035
+ for (const [filePath] of graph.centrality) {
1036
+ for (const { name, pattern } of LAYER_PATTERNS) {
1037
+ if (pattern.test(filePath)) {
1038
+ const files = layerFiles.get(name) ?? [];
1039
+ files.push(filePath);
1040
+ layerFiles.set(name, files);
1041
+ fileToLayer.set(filePath, name);
1042
+ break;
1043
+ }
1044
+ }
1045
+ }
1046
+ const layerImportedBy = /* @__PURE__ */ new Map();
1047
+ const layerDependsOn = /* @__PURE__ */ new Map();
1048
+ for (const name of layerFiles.keys()) {
1049
+ layerImportedBy.set(name, /* @__PURE__ */ new Set());
1050
+ layerDependsOn.set(name, /* @__PURE__ */ new Set());
1051
+ }
1052
+ for (const edge of graph.edges) {
1053
+ if (edge.isExternal) continue;
1054
+ const fromLayer = fileToLayer.get(edge.from);
1055
+ const toLayer = fileToLayer.get(edge.to);
1056
+ if (fromLayer && toLayer && fromLayer !== toLayer) {
1057
+ layerImportedBy.get(toLayer)?.add(fromLayer);
1058
+ layerDependsOn.get(fromLayer)?.add(toLayer);
1059
+ }
1060
+ }
1061
+ const layerEdges = [];
1062
+ const edgeSet = /* @__PURE__ */ new Set();
1063
+ for (const [from, deps] of layerDependsOn) {
1064
+ for (const to of deps) {
1065
+ const key = `${from}->${to}`;
1066
+ if (!edgeSet.has(key)) {
1067
+ edgeSet.add(key);
1068
+ layerEdges.push({ from, to });
1069
+ }
1070
+ }
1071
+ }
1072
+ const layers = [];
1073
+ for (const [name, files] of layerFiles) {
1074
+ layers.push({
1075
+ name,
1076
+ files,
1077
+ importedByLayers: layerImportedBy.get(name)?.size ?? 0,
1078
+ dependsOn: [...layerDependsOn.get(name) ?? []]
1079
+ });
1080
+ }
1081
+ layers.sort((a, b) => b.importedByLayers - a.importedByLayers || a.name.localeCompare(b.name));
1082
+ return { layers, layerEdges };
1083
+ }
1084
+ function computeInstability(graph) {
1085
+ const fanOutMap = /* @__PURE__ */ new Map();
1086
+ for (const edge of graph.edges) {
1087
+ if (!edge.isExternal) {
1088
+ fanOutMap.set(edge.from, (fanOutMap.get(edge.from) ?? 0) + 1);
1089
+ }
1090
+ }
1091
+ const results = [];
1092
+ for (const [filePath, fanIn] of graph.inDegree) {
1093
+ const fanOut = fanOutMap.get(filePath) ?? 0;
1094
+ const total = fanIn + fanOut;
1095
+ if (total === 0) continue;
1096
+ const instability = fanOut / total;
1097
+ if (instability > 0.7 && fanIn >= 3) {
1098
+ results.push({ path: filePath, fanIn, fanOut, instability });
1099
+ }
1100
+ }
1101
+ results.sort((a, b) => b.instability - a.instability);
1102
+ return results;
1103
+ }
1104
+ function detectCommunities(graph) {
1105
+ const adj = /* @__PURE__ */ new Map();
1106
+ const allFiles = /* @__PURE__ */ new Set();
1107
+ for (const edge of graph.edges) {
1108
+ if (edge.isExternal) continue;
1109
+ allFiles.add(edge.from);
1110
+ allFiles.add(edge.to);
1111
+ if (!adj.has(edge.from)) adj.set(edge.from, /* @__PURE__ */ new Set());
1112
+ if (!adj.has(edge.to)) adj.set(edge.to, /* @__PURE__ */ new Set());
1113
+ adj.get(edge.from).add(edge.to);
1114
+ adj.get(edge.to).add(edge.from);
1115
+ }
1116
+ const files = [...allFiles];
1117
+ if (files.length === 0) return [];
1118
+ const labels = /* @__PURE__ */ new Map();
1119
+ for (let i = 0; i < files.length; i++) {
1120
+ labels.set(files[i], i);
1121
+ }
1122
+ for (let iter = 0; iter < 10; iter++) {
1123
+ let changed = false;
1124
+ const shuffled = [...files];
1125
+ for (let i = shuffled.length - 1; i > 0; i--) {
1126
+ const j = Math.floor(Math.random() * (i + 1));
1127
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
1128
+ }
1129
+ for (const file of shuffled) {
1130
+ const neighbors = adj.get(file);
1131
+ if (!neighbors || neighbors.size === 0) continue;
1132
+ const labelCounts = /* @__PURE__ */ new Map();
1133
+ for (const neighbor of neighbors) {
1134
+ const lbl = labels.get(neighbor);
1135
+ labelCounts.set(lbl, (labelCounts.get(lbl) ?? 0) + 1);
1136
+ }
1137
+ let maxCount = 0;
1138
+ let bestLabel = labels.get(file);
1139
+ for (const [lbl, count] of labelCounts) {
1140
+ if (count > maxCount) {
1141
+ maxCount = count;
1142
+ bestLabel = lbl;
1143
+ }
1144
+ }
1145
+ if (bestLabel !== labels.get(file)) {
1146
+ labels.set(file, bestLabel);
1147
+ changed = true;
1148
+ }
1149
+ }
1150
+ if (!changed) break;
1151
+ }
1152
+ const groups = /* @__PURE__ */ new Map();
1153
+ for (const [file, label] of labels) {
1154
+ const group = groups.get(label) ?? [];
1155
+ group.push(file);
1156
+ groups.set(label, group);
1157
+ }
1158
+ const communities = [];
1159
+ let id = 0;
1160
+ for (const files2 of groups.values()) {
1161
+ if (files2.length < 3) continue;
1162
+ const label = deriveLabel(files2);
1163
+ communities.push({ id: id++, files: files2.sort(), label });
1164
+ }
1165
+ communities.sort((a, b) => b.files.length - a.files.length);
1166
+ return communities;
1167
+ }
1168
+ function deriveLabel(files) {
1169
+ if (files.length === 0) return "unknown";
1170
+ const dirs = files.map((f) => {
1171
+ const parts = f.split("/");
1172
+ return parts.slice(0, -1).join("/");
1173
+ });
1174
+ const first = dirs[0];
1175
+ let prefixLen = first.length;
1176
+ for (const dir of dirs) {
1177
+ let i = 0;
1178
+ while (i < prefixLen && i < dir.length && first[i] === dir[i]) i++;
1179
+ prefixLen = i;
1180
+ }
1181
+ let common = first.slice(0, prefixLen);
1182
+ if (common.includes("/")) {
1183
+ common = common.slice(0, common.lastIndexOf("/") + 1);
1184
+ }
1185
+ common = common.replace(/\/$/, "");
1186
+ return common || files[0].split("/")[0] || "root";
1187
+ }
1188
+ function computeExportCoverage(graph) {
1189
+ const usedExports = findUsedExports(graph.edges);
1190
+ const allExportsByFile = /* @__PURE__ */ new Map();
1191
+ for (const edge of graph.edges) {
1192
+ if (edge.isExternal) continue;
1193
+ for (const name of edge.importedNames) {
1194
+ if (!allExportsByFile.has(edge.to)) allExportsByFile.set(edge.to, /* @__PURE__ */ new Set());
1195
+ allExportsByFile.get(edge.to).add(name);
1196
+ }
1197
+ }
1198
+ const results = [];
1199
+ for (const [file, exports] of allExportsByFile) {
1200
+ const totalExports = exports.size;
1201
+ if (totalExports === 0) continue;
1202
+ let usedCount = 0;
1203
+ for (const name of exports) {
1204
+ if (usedExports.has(`${file}::${name}`)) usedCount++;
1205
+ }
1206
+ results.push({
1207
+ file,
1208
+ totalExports,
1209
+ usedExports: usedCount,
1210
+ coverage: usedCount / totalExports
1211
+ });
1212
+ }
1213
+ results.sort((a, b) => a.coverage - b.coverage);
1214
+ return results;
1215
+ }
1216
+
1217
+ // src/snapshot.ts
1218
+ function getDefaultScanPaths(ctx) {
1219
+ const paths = [];
1220
+ const dirs = ctx.directories;
1221
+ for (const d of dirs) {
1222
+ if (d.endsWith("types") || d.endsWith("typings")) paths.push(d);
1223
+ }
1224
+ for (const d of dirs) {
1225
+ if (d.endsWith("stores") || d.endsWith("store")) paths.push(d);
1226
+ }
1227
+ for (const d of dirs) {
1228
+ if (d.endsWith("services") || d.endsWith("api")) paths.push(d);
1229
+ }
1230
+ for (const d of dirs) {
1231
+ if (d.endsWith("hooks")) paths.push(d);
1232
+ }
1233
+ for (const d of dirs) {
1234
+ if (d.endsWith("components")) paths.push(d);
1235
+ }
1236
+ for (const d of dirs) {
1237
+ if (d.endsWith("lib") || d.endsWith("utils")) paths.push(d);
1238
+ }
1239
+ if (paths.length === 0) {
1240
+ paths.push("src", "app", "lib");
1241
+ }
1242
+ return paths;
1243
+ }
1244
+ var PATTERNS = {
1245
+ /** export interface Foo { ... } or export type Foo = ... */
1246
+ exportedType: /^export\s+(interface|type)\s+(\w+)/,
1247
+ /** interface FooProps { ... } (component props, even if not exported) */
1248
+ propsInterface: /^(?:export\s+)?interface\s+(\w+Props)\s*\{/,
1249
+ /** export function foo(...) or export const foo = */
1250
+ exportedFunction: /^export\s+(?:async\s+)?(?:function|const)\s+(\w+)/,
1251
+ /** StateCreator<...> pattern (Zustand slices) */
1252
+ zustandSlice: /StateCreator<\s*(\w+)/,
1253
+ /** export interface FooSlice { ... } */
1254
+ sliceInterface: /^export\s+interface\s+(\w+Slice)\s*\{/
1255
+ };
1256
+ async function extractFromFile(filePath, relPath) {
1257
+ const content = await readFileOr(filePath);
1258
+ if (!content) return [];
1259
+ const entries = [];
1260
+ const lines = content.split("\n");
1261
+ const isStore = /stores?[/\\]/.test(relPath);
1262
+ const isHook = /hooks?[/\\]/.test(relPath) || relPath.includes("use");
1263
+ const isComponent = /components?[/\\]/.test(relPath);
1264
+ const isService = /services?[/\\]|api[/\\]/.test(relPath);
1265
+ const isType = /types?[/\\]/.test(relPath) || relPath.endsWith(".types.ts");
1266
+ for (let i = 0; i < lines.length; i++) {
1267
+ const line = lines[i];
1268
+ const trimmed = line.trimStart();
1269
+ const typeMatch = trimmed.match(PATTERNS.exportedType);
1270
+ if (typeMatch) {
1271
+ const [, kind, name] = typeMatch;
1272
+ const category = name.endsWith("Slice") ? "store" : name.endsWith("Props") ? "component" : kind === "interface" ? "interface" : "type";
1273
+ const block = extractBlock(lines, i);
1274
+ entries.push({ file: relPath, category, signature: block });
1275
+ continue;
1276
+ }
1277
+ if (isComponent) {
1278
+ const propsMatch = trimmed.match(PATTERNS.propsInterface);
1279
+ if (propsMatch && !trimmed.startsWith("export")) {
1280
+ const block = extractBlock(lines, i);
1281
+ entries.push({ file: relPath, category: "component", signature: block });
1282
+ continue;
1283
+ }
1284
+ }
1285
+ const funcMatch = trimmed.match(PATTERNS.exportedFunction);
1286
+ if (funcMatch) {
1287
+ const [, name] = funcMatch;
1288
+ if (isComponent && name[0] === name[0].toUpperCase() && !name.startsWith("use")) {
1289
+ continue;
1290
+ }
1291
+ let category = "function";
1292
+ if (isHook || name.startsWith("use")) category = "hook";
1293
+ else if (isStore) category = "store";
1294
+ const sig = extractSignatureLine(lines, i);
1295
+ entries.push({ file: relPath, category, signature: sig });
1296
+ }
1297
+ }
1298
+ return entries;
1299
+ }
1300
+ function extractBlock(lines, startIdx) {
1301
+ const firstLine = lines[startIdx];
1302
+ if (!firstLine.includes("{")) {
1303
+ let result2 = "";
1304
+ for (let i = startIdx; i < lines.length && i < startIdx + 10; i++) {
1305
+ result2 += (result2 ? "\n" : "") + lines[i];
1306
+ if (lines[i].includes(";")) break;
1307
+ }
1308
+ return result2.trim();
1309
+ }
1310
+ let depth = 0;
1311
+ let result = "";
1312
+ const maxLines = 30;
1313
+ for (let i = startIdx; i < lines.length && i < startIdx + maxLines; i++) {
1314
+ const line = lines[i];
1315
+ result += (result ? "\n" : "") + line;
1316
+ for (const ch of line) {
1317
+ if (ch === "{") depth++;
1318
+ if (ch === "}") depth--;
1319
+ }
1320
+ if (depth <= 0 && i > startIdx) break;
1321
+ }
1322
+ return result.trim();
1323
+ }
1324
+ function extractSignatureLine(lines, startIdx) {
1325
+ let sig = "";
1326
+ for (let i = startIdx; i < lines.length && i < startIdx + 5; i++) {
1327
+ sig += (sig ? " " : "") + lines[i].trim();
1328
+ if (sig.includes("{") || sig.includes("=>")) {
1329
+ const braceIdx = sig.indexOf("{");
1330
+ const arrowIdx = sig.indexOf("=>");
1331
+ const cutIdx = braceIdx >= 0 && arrowIdx >= 0 ? Math.min(braceIdx, arrowIdx) : braceIdx >= 0 ? braceIdx : arrowIdx >= 0 ? arrowIdx + 2 : sig.length;
1332
+ sig = sig.slice(0, cutIdx).trim();
1333
+ break;
1334
+ }
1335
+ }
1336
+ return sig;
1337
+ }
1338
+ function annotateSignature(entry) {
1339
+ if (entry.importedByCount && entry.importedByCount > 2) {
1340
+ const firstLine = entry.signature.split("\n")[0];
1341
+ const rest = entry.signature.split("\n").slice(1);
1342
+ const annotated = `${firstLine} // imported by ${entry.importedByCount} files`;
1343
+ return rest.length > 0 ? [annotated, ...rest].join("\n") : annotated;
1344
+ }
1345
+ return entry.signature;
1346
+ }
1347
+ function renderSnapshot(entries) {
1348
+ if (entries.length === 0) return "";
1349
+ const byFile = /* @__PURE__ */ new Map();
1350
+ for (const e of entries) {
1351
+ const list = byFile.get(e.file) ?? [];
1352
+ list.push(e);
1353
+ byFile.set(e.file, list);
1354
+ }
1355
+ let md = "";
1356
+ const types = entries.filter((e) => e.category === "type" || e.category === "interface");
1357
+ const stores = entries.filter((e) => e.category === "store");
1358
+ const hooks = entries.filter((e) => e.category === "hook");
1359
+ const components = entries.filter((e) => e.category === "component");
1360
+ const functions = entries.filter((e) => e.category === "function");
1361
+ if (types.length > 0) {
1362
+ md += "### Core Types\n\n```ts\n";
1363
+ md += types.map((e) => annotateSignature(e)).join("\n\n");
1364
+ md += "\n```\n\n";
1365
+ }
1366
+ if (stores.length > 0) {
1367
+ md += "### Store Shape\n\n```ts\n";
1368
+ md += stores.map((e) => annotateSignature(e)).join("\n\n");
1369
+ md += "\n```\n\n";
1370
+ }
1371
+ if (components.length > 0) {
1372
+ md += "### Component Props\n\n```ts\n";
1373
+ md += components.map((e) => annotateSignature(e)).join("\n\n");
1374
+ md += "\n```\n\n";
1375
+ }
1376
+ if (hooks.length > 0) {
1377
+ md += "### Hooks\n\n```ts\n";
1378
+ md += hooks.map((e) => annotateSignature(e)).join("\n\n");
1379
+ md += "\n```\n\n";
1380
+ }
1381
+ if (functions.length > 0) {
1382
+ md += "### Key Functions\n\n```ts\n";
1383
+ md += functions.map((e) => annotateSignature(e)).join("\n\n");
1384
+ md += "\n```\n\n";
1385
+ }
1386
+ return md.trimEnd();
1387
+ }
1388
+ async function generateSnapshot(ctx, customPaths, graph, maxTokens, onProgress, gitActivity) {
1389
+ const scanPaths = customPaths.length > 0 ? customPaths : getDefaultScanPaths(ctx);
1390
+ if (scanPaths.length === 0) {
1391
+ return { entries: [], markdown: "" };
1392
+ }
1393
+ const dirNames = scanPaths.map((p5) => p5.split("/").pop() ?? p5);
1394
+ onProgress?.(`Scanning ${scanPaths.length} directories: ${dirNames.join(", ")}...`);
1395
+ const patterns = scanPaths.map((p5) => `${p5}/**/*.{ts,tsx,js,jsx}`);
1396
+ const files = await fg3(patterns, {
1397
+ cwd: ctx.rootDir,
1398
+ ignore: [
1399
+ "**/node_modules/**",
1400
+ "**/dist/**",
1401
+ "**/build/**",
1402
+ "**/*.test.*",
1403
+ "**/*.spec.*",
1404
+ "**/__tests__/**"
1405
+ ],
1406
+ absolute: false
1407
+ });
1408
+ const allEntries = [];
1409
+ for (let i = 0; i < files.length; i++) {
1410
+ const file = files[i];
1411
+ if ((i + 1) % 20 === 0 || i === files.length - 1) {
1412
+ const dir = path4.dirname(file).split("/").pop() ?? "";
1413
+ onProgress?.(`Extracting signatures... ${i + 1}/${files.length} files (${dir}/)`);
1414
+ }
1415
+ const absPath = path4.join(ctx.rootDir, file);
1416
+ const entries = await extractFromFile(absPath, file);
1417
+ allEntries.push(...entries);
1418
+ }
1419
+ if (graph) {
1420
+ for (const entry of allEntries) {
1421
+ const count = graph.inDegree.get(entry.file) ?? 0;
1422
+ if (count > 0) {
1423
+ entry.importedByCount = count;
1424
+ }
1425
+ }
1426
+ }
1427
+ onProgress?.("Filtering dead exports...");
1428
+ const liveEntries = filterDeadExports(allEntries, graph);
1429
+ const budget = maxTokens ?? Math.min(16e3, 4e3 + Math.floor(ctx.sourceFileCount / 25) * 500);
1430
+ onProgress?.(`Applying token budget (${budget.toLocaleString()} tokens)...`);
1431
+ const { selected, excluded } = applyTokenBudget(liveEntries, budget, graph, gitActivity);
1432
+ const markdown = renderSnapshot(selected);
1433
+ return {
1434
+ entries: selected,
1435
+ markdown,
1436
+ budgetExcluded: excluded,
1437
+ estimatedTokens: estimateTokens(markdown)
1438
+ };
1439
+ }
1440
+ var ENTRY_POINT_PATTERNS = [
1441
+ /(?:^|\/)index\.[jt]sx?$/,
1442
+ /(?:^|\/)App\.[jt]sx?$/,
1443
+ /(?:^|\/)main\.[jt]sx?$/,
1444
+ /(?:^|\/)pages\//,
1445
+ /(?:^|\/)app\//,
1446
+ /(?:^|\/)routes?\//,
1447
+ /(?:^|\/)middleware\//
1448
+ ];
1449
+ function extractNameFromSignature(sig) {
1450
+ const m = sig.match(
1451
+ /export\s+(?:default\s+)?(?:async\s+)?(?:interface|type|function|const|let|var|class|enum)\s+(\w+)/
1452
+ );
1453
+ return m?.[1] ?? null;
1454
+ }
1455
+ function isEntryPoint(filePath) {
1456
+ return ENTRY_POINT_PATTERNS.some((p5) => p5.test(filePath));
1457
+ }
1458
+ function filterDeadExports(entries, graph) {
1459
+ if (!graph || graph.edges.length === 0) return entries;
1460
+ const usedExports = findUsedExports(graph.edges);
1461
+ return entries.filter((entry) => {
1462
+ if (isEntryPoint(entry.file)) return true;
1463
+ const name = extractNameFromSignature(entry.signature);
1464
+ if (!name) return true;
1465
+ return usedExports.has(`${entry.file}::${name}`);
1466
+ });
1467
+ }
1468
+ function applyTokenBudget(entries, budget, graph, gitActivity) {
1469
+ if (entries.length === 0) return { selected: [], excluded: 0 };
1470
+ const scored = entries.map((entry) => {
1471
+ const tokens = Math.max(1, estimateTokens(entry.signature));
1472
+ const centrality = graph?.centrality.get(entry.file) ?? 0.5;
1473
+ let categoryBoost = 1;
1474
+ if (entry.category === "type" || entry.category === "interface") categoryBoost = 1.3;
1475
+ let gitBoost = 1;
1476
+ if (gitActivity) {
1477
+ const commits = gitActivity.commitCounts.get(entry.file) ?? 0;
1478
+ gitBoost = 1 + Math.min(0.5, commits / 20);
1479
+ }
1480
+ const value = centrality * categoryBoost * gitBoost / tokens;
1481
+ return { entry, tokens, value };
1482
+ });
1483
+ scored.sort((a, b) => b.value - a.value);
1484
+ let remaining = budget;
1485
+ const selected = [];
1486
+ for (const { entry, tokens } of scored) {
1487
+ if (tokens <= remaining) {
1488
+ selected.push(entry);
1489
+ remaining -= tokens;
1490
+ }
1491
+ }
1492
+ return {
1493
+ selected,
1494
+ excluded: entries.length - selected.length
1495
+ };
1496
+ }
1497
+
1498
+ // src/generate.ts
1499
+ init_utils();
1500
+ import path5 from "path";
1501
+ import * as p2 from "@clack/prompts";
1502
+
1503
+ // src/templates/framework-hints.ts
1504
+ function getFrameworkHints(ctx) {
1505
+ const hints = [];
1506
+ const frameworkNames = new Set(ctx.frameworks.map((f) => f.name));
1507
+ for (const gen of HINT_GENERATORS) {
1508
+ if (frameworkNames.has(gen.name)) {
1509
+ const lines = gen.getHints(ctx);
1510
+ if (lines.length > 0) hints.push(...lines);
1511
+ }
1512
+ }
1513
+ return hints;
1514
+ }
1515
+ function getFrameworkHintsSection(ctx) {
1516
+ const hints = getFrameworkHints(ctx);
1517
+ if (hints.length === 0) return "";
1518
+ const lines = ["## Framework Conventions", ""];
1519
+ for (const h of hints) {
1520
+ lines.push(h);
1521
+ }
1522
+ lines.push("");
1523
+ return lines.join("\n");
1524
+ }
1525
+ var HINT_GENERATORS = [
1526
+ {
1527
+ name: "Next.js",
1528
+ getHints: (ctx) => {
1529
+ const hints = [];
1530
+ const hasAppDir = ctx.directories.some(
1531
+ (d) => d === "app" || d.startsWith("app/") || d === "src/app"
1532
+ );
1533
+ const hasPagesDir = ctx.directories.some(
1534
+ (d) => d === "pages" || d.startsWith("pages/") || d === "src/pages"
1535
+ );
1536
+ if (hasAppDir && !hasPagesDir) {
1537
+ hints.push("### Next.js (App Router)");
1538
+ hints.push("");
1539
+ hints.push(
1540
+ "- **App Router** \u2014 all routes in `app/` use React Server Components by default"
1541
+ );
1542
+ hints.push(
1543
+ '- Add `"use client"` directive at the top of files that need browser APIs, hooks, or event handlers'
1544
+ );
1545
+ hints.push(
1546
+ "- `layout.tsx` wraps child routes; `page.tsx` is the route's UI; `loading.tsx` / `error.tsx` for boundaries"
1547
+ );
1548
+ hints.push(
1549
+ "- Data fetching: `async` server components with direct `fetch()` or DB calls (no `getServerSideProps`)"
1550
+ );
1551
+ hints.push(
1552
+ "- Route handlers: `app/api/*/route.ts` with exported `GET`, `POST`, etc. functions"
1553
+ );
1554
+ hints.push(
1555
+ "- Middleware in `middleware.ts` at project root for auth, redirects, rewrites"
1556
+ );
1557
+ hints.push(
1558
+ "- Use `next/image` for optimized images, `next/font` for font loading"
1559
+ );
1560
+ } else if (hasPagesDir && !hasAppDir) {
1561
+ hints.push("### Next.js (Pages Router)");
1562
+ hints.push("");
1563
+ hints.push("- **Pages Router** \u2014 routes in `pages/` directory");
1564
+ hints.push(
1565
+ "- `getServerSideProps` for server-side data fetching, `getStaticProps` for static generation"
1566
+ );
1567
+ hints.push(
1568
+ "- `_app.tsx` for global layout/providers, `_document.tsx` for HTML customization"
1569
+ );
1570
+ hints.push("- API routes in `pages/api/`");
1571
+ } else if (hasAppDir && hasPagesDir) {
1572
+ hints.push("### Next.js (Hybrid \u2014 App + Pages Router)");
1573
+ hints.push("");
1574
+ hints.push(
1575
+ "- Both `app/` (App Router) and `pages/` (Pages Router) coexist \u2014 new routes should use App Router"
1576
+ );
1577
+ hints.push(
1578
+ '- App Router components are server components by default; add `"use client"` for client components'
1579
+ );
1580
+ hints.push(
1581
+ "- Pages Router uses `getServerSideProps` / `getStaticProps` for data fetching"
1582
+ );
1583
+ } else {
1584
+ hints.push("### Next.js");
1585
+ hints.push("");
1586
+ hints.push(
1587
+ "- Use `next/image` for optimized images, `next/font` for font loading"
1588
+ );
1589
+ hints.push(
1590
+ "- Middleware in `middleware.ts` at project root for auth, redirects, rewrites"
1591
+ );
1592
+ }
1593
+ return hints;
1594
+ }
1595
+ },
1596
+ {
1597
+ name: "Express",
1598
+ getHints: () => [
1599
+ "### Express",
1600
+ "",
1601
+ "- Middleware chain: `app.use()` for global, router-level for scoped. Order matters.",
1602
+ "- Error handling: define a 4-arg middleware `(err, req, res, next)` at the end of the chain",
1603
+ "- Organize routes with `express.Router()` in separate files, mount with `app.use('/prefix', router)`",
1604
+ "- Validate request bodies/params at the route level (e.g. with zod, joi, or express-validator)",
1605
+ "- Use `async` handlers with try/catch or an async wrapper to avoid unhandled promise rejections",
1606
+ "- Set `res.status()` before `res.json()` \u2014 don't rely on defaults for error responses"
1607
+ ]
1608
+ },
1609
+ {
1610
+ name: "Fastify",
1611
+ getHints: () => [
1612
+ "### Fastify",
1613
+ "",
1614
+ "- Plugin architecture: register plugins with `fastify.register()` for encapsulation",
1615
+ "- Schema-based validation: use JSON Schema for request/response validation (built-in)",
1616
+ "- Hooks: `onRequest`, `preHandler`, `onSend` etc. for request lifecycle",
1617
+ "- Use `fastify-plugin` for plugins that should not be encapsulated",
1618
+ "- Serialization: define response schemas for 2x faster JSON serialization"
1619
+ ]
1620
+ },
1621
+ {
1622
+ name: "Hono",
1623
+ getHints: () => [
1624
+ "### Hono",
1625
+ "",
1626
+ "- Middleware with `app.use()` \u2014 compose with `c.next()` pattern",
1627
+ "- Validators: use `hono/validator` or `@hono/zod-validator` for type-safe request parsing",
1628
+ "- Context (`c`): `c.json()`, `c.text()`, `c.html()` for responses; `c.req` for request",
1629
+ "- Supports multiple runtimes (Node, Deno, Bun, Cloudflare Workers) \u2014 avoid Node-specific APIs"
1630
+ ]
1631
+ },
1632
+ {
1633
+ name: "NestJS",
1634
+ getHints: () => [
1635
+ "### NestJS",
1636
+ "",
1637
+ "- **Modules** organize the app into cohesive blocks; every feature gets a module",
1638
+ "- **Controllers** handle HTTP requests (decorators: `@Get()`, `@Post()`, etc.)",
1639
+ "- **Providers** (services) hold business logic \u2014 injected via constructor DI",
1640
+ "- **Guards** for auth (`@UseGuards()`), **Pipes** for validation (`@UsePipes()`)",
1641
+ "- **Interceptors** for response transformation, logging, caching",
1642
+ "- DTOs with `class-validator` decorators for request validation",
1643
+ "- Use `@Injectable()` on all services/providers"
1644
+ ]
1645
+ },
1646
+ {
1647
+ name: "Expo",
1648
+ getHints: (ctx) => {
1649
+ const hasRN = ctx.frameworks.some((f) => f.name === "React Native");
1650
+ const hints = ["### Expo / React Native", ""];
1651
+ hints.push(
1652
+ "- **expo-router** for file-based routing (if using); Stack, Tabs, Drawer navigators"
1653
+ );
1654
+ hints.push(
1655
+ "- Expo Go has limited native module support \u2014 some packages require a dev build (`npx expo run:ios`)"
1656
+ );
1657
+ hints.push(
1658
+ "- Use `expo-constants`, `expo-device` etc. instead of raw RN APIs when available"
1659
+ );
1660
+ hints.push(
1661
+ "- Platform-specific files: `*.ios.tsx` / `*.android.tsx` or `Platform.select()`"
1662
+ );
1663
+ if (hasRN) {
1664
+ hints.push(
1665
+ '- **Reanimated**: worklet functions need the `"worklet"` directive on the first line'
1666
+ );
1667
+ hints.push(
1668
+ "- Avoid `FadeIn`/`FadeOut` entering/exiting animations on conditionally rendered components \u2014 they cause flashes"
1669
+ );
1670
+ }
1671
+ return hints;
1672
+ }
1673
+ },
1674
+ {
1675
+ name: "React Native",
1676
+ getHints: (ctx) => {
1677
+ if (ctx.frameworks.some((f) => f.name === "Expo")) return [];
1678
+ return [
1679
+ "### React Native",
1680
+ "",
1681
+ "- Use `StyleSheet.create()` for styles \u2014 avoid inline style objects in render",
1682
+ "- Platform-specific: `*.ios.tsx` / `*.android.tsx` or `Platform.select()`",
1683
+ '- **Reanimated**: worklet functions need the `"worklet"` directive',
1684
+ "- Navigation: React Navigation with Stack/Tab/Drawer navigators",
1685
+ "- Test with both iOS and Android \u2014 layout behavior differs"
1686
+ ];
1687
+ }
1688
+ },
1689
+ {
1690
+ name: "React",
1691
+ getHints: (ctx) => {
1692
+ const skip = ["Next.js", "Expo", "React Native"];
1693
+ if (ctx.frameworks.some((f) => skip.includes(f.name))) return [];
1694
+ return [
1695
+ "### React",
1696
+ "",
1697
+ "- Functional components with hooks \u2014 no class components",
1698
+ "- Use `React.memo()` for expensive renders, `useMemo`/`useCallback` for referential stability",
1699
+ "- Lift state up or use context for shared state; avoid prop drilling beyond 2-3 levels",
1700
+ "- Prefer controlled components for forms",
1701
+ "- Use `Suspense` boundaries with `lazy()` for code splitting"
1702
+ ];
1703
+ }
1704
+ },
1705
+ {
1706
+ name: "Vue",
1707
+ getHints: () => [
1708
+ "### Vue",
1709
+ "",
1710
+ "- Composition API with `<script setup>` for new components",
1711
+ "- Reactive state: `ref()` for primitives, `reactive()` for objects",
1712
+ "- `computed()` for derived state, `watch()` / `watchEffect()` for side effects",
1713
+ "- Props: define with `defineProps<T>()`, emits with `defineEmits<T>()`",
1714
+ "- Use `provide` / `inject` for deep dependency injection"
1715
+ ]
1716
+ },
1717
+ {
1718
+ name: "Nuxt",
1719
+ getHints: () => [
1720
+ "### Nuxt",
1721
+ "",
1722
+ "- Auto-imports: components, composables, and utils are auto-imported \u2014 no manual import needed",
1723
+ "- File-based routing in `pages/` \u2014 dynamic params with `[id].vue` syntax",
1724
+ "- Data fetching: `useFetch()` / `useAsyncData()` \u2014 they handle SSR hydration automatically",
1725
+ "- Server routes in `server/api/` \u2014 auto-registered, use `defineEventHandler()`",
1726
+ "- Middleware in `middleware/` \u2014 `defineNuxtRouteMiddleware()` for route guards",
1727
+ "- State: `useState()` for SSR-safe shared state, Pinia for complex stores"
1728
+ ]
1729
+ },
1730
+ {
1731
+ name: "Svelte",
1732
+ getHints: (ctx) => {
1733
+ if (ctx.frameworks.some((f) => f.name === "SvelteKit")) return [];
1734
+ return [
1735
+ "### Svelte",
1736
+ "",
1737
+ "- Reactive declarations with `$:` for derived state",
1738
+ "- Props: `export let propName` in component script",
1739
+ "- Stores: `writable()`, `readable()`, `derived()` \u2014 auto-subscribe with `$store` syntax",
1740
+ "- Use `{#if}`, `{#each}`, `{#await}` blocks for conditional/list/async rendering"
1741
+ ];
1742
+ }
1743
+ },
1744
+ {
1745
+ name: "SvelteKit",
1746
+ getHints: () => [
1747
+ "### SvelteKit",
1748
+ "",
1749
+ "- File-based routing in `src/routes/` \u2014 `+page.svelte`, `+layout.svelte`, `+server.ts`",
1750
+ "- `+page.ts` (universal) or `+page.server.ts` (server-only) for `load` functions",
1751
+ "- Form actions in `+page.server.ts` with `actions` export for progressive enhancement",
1752
+ "- Hooks in `src/hooks.server.ts` for auth, session, error handling",
1753
+ "- Use `$app/stores` for page, navigating, updated stores"
1754
+ ]
1755
+ },
1756
+ {
1757
+ name: "Angular",
1758
+ getHints: () => [
1759
+ "### Angular",
1760
+ "",
1761
+ "- Components, services, pipes, directives all use decorators (`@Component`, `@Injectable`, etc.)",
1762
+ "- Dependency injection: provide services in module or component `providers` array",
1763
+ "- RxJS Observables for async data \u2014 use `async` pipe in templates, unsubscribe on destroy",
1764
+ "- Lazy-load feature modules with `loadChildren` in routes",
1765
+ "- Use Angular CLI (`ng generate`) for scaffolding"
1766
+ ]
1767
+ },
1768
+ {
1769
+ name: "Django",
1770
+ getHints: () => [
1771
+ "### Django",
1772
+ "",
1773
+ "- Apps structure: each feature is a Django app with `models.py`, `views.py`, `urls.py`, `admin.py`",
1774
+ "- Models: define in `models.py`, create migrations with `python manage.py makemigrations`",
1775
+ "- Views: function-based (FBV) or class-based (CBV) \u2014 CBV for CRUD, FBV for custom logic",
1776
+ "- URL routing in `urls.py` using `path()` and `include()` for app-level URLs",
1777
+ "- Templates in `templates/` \u2014 use template inheritance with `{% extends %}` and `{% block %}`",
1778
+ "- Management commands in `management/commands/` for custom CLI tasks",
1779
+ "- Settings: use `django-environ` or `python-decouple` for environment-based config"
1780
+ ]
1781
+ },
1782
+ {
1783
+ name: "Flask",
1784
+ getHints: () => [
1785
+ "### Flask",
1786
+ "",
1787
+ "- Blueprints for modular route organization \u2014 register with `app.register_blueprint()`",
1788
+ "- Use application factory pattern (`create_app()`) for testing and config flexibility",
1789
+ "- Error handlers with `@app.errorhandler(404)` etc.",
1790
+ "- Use Flask-SQLAlchemy for ORM, Flask-Migrate for database migrations",
1791
+ "- Request context: `request`, `g`, `session` globals \u2014 available inside request handlers"
1792
+ ]
1793
+ },
1794
+ {
1795
+ name: "FastAPI",
1796
+ getHints: () => [
1797
+ "### FastAPI",
1798
+ "",
1799
+ "- **Dependency injection**: use `Depends()` for shared logic (auth, DB sessions, validation)",
1800
+ "- **Pydantic models** for request/response schemas \u2014 automatic validation and OpenAPI docs",
1801
+ "- Async endpoints by default (`async def`); use sync `def` only for blocking I/O with threadpool",
1802
+ "- Routers: `APIRouter()` for modular route organization, mount with `app.include_router()`",
1803
+ '- Middleware with `@app.middleware("http")` or Starlette middleware classes',
1804
+ "- Background tasks with `BackgroundTasks` parameter for fire-and-forget work",
1805
+ "- Auto-generated docs at `/docs` (Swagger) and `/redoc`"
1806
+ ]
1807
+ },
1808
+ {
1809
+ name: "Prisma",
1810
+ getHints: () => [
1811
+ "### Prisma",
1812
+ "",
1813
+ "- Schema in `prisma/schema.prisma` \u2014 run `npx prisma generate` after changes",
1814
+ "- Migrations: `npx prisma migrate dev` for development, `npx prisma migrate deploy` for production",
1815
+ "- Use `prisma.$transaction()` for atomic operations",
1816
+ "- Relation queries: use `include` for eager loading, `select` for field filtering"
1817
+ ]
1818
+ },
1819
+ {
1820
+ name: "Drizzle",
1821
+ getHints: () => [
1822
+ "### Drizzle",
1823
+ "",
1824
+ "- Schema defined in TypeScript \u2014 type-safe queries with no code generation step",
1825
+ "- Migrations: `drizzle-kit generate` then `drizzle-kit migrate`",
1826
+ "- Use `db.select()`, `db.insert()`, `db.update()`, `db.delete()` for queries",
1827
+ "- Relations: define with `relations()` helper for type-safe joins"
1828
+ ]
1829
+ },
1830
+ {
1831
+ name: "Tailwind CSS",
1832
+ getHints: (ctx) => {
1833
+ if (ctx.frameworks.some((f) => f.name === "NativeWind")) return [];
1834
+ return [
1835
+ "### Tailwind CSS",
1836
+ "",
1837
+ "- Utility-first: compose styles with `className` \u2014 avoid custom CSS unless truly needed",
1838
+ "- Use `@apply` sparingly in CSS modules for repeated patterns",
1839
+ "- Responsive: mobile-first with `sm:`, `md:`, `lg:` breakpoint prefixes",
1840
+ "- Dark mode: `dark:` variant (class or media strategy per `tailwind.config`)",
1841
+ "- Extract reusable component classes into shared components, not `@apply` blocks"
1842
+ ];
1843
+ }
1844
+ },
1845
+ {
1846
+ name: "Electron",
1847
+ getHints: () => [
1848
+ "### Electron",
1849
+ "",
1850
+ "- **Main process** (Node.js) and **renderer process** (Chromium) \u2014 communicate via IPC",
1851
+ "- Use `contextBridge` + `preload.js` to expose safe APIs to renderer (no `nodeIntegration`)",
1852
+ "- `ipcMain.handle()` / `ipcRenderer.invoke()` for async request-response patterns",
1853
+ "- Package with `electron-builder` or `electron-forge`"
1854
+ ]
1855
+ }
1856
+ ];
1857
+
1858
+ // src/templates/main-context.ts
1859
+ function buildMainContext(ctx, answers, snapshot, analysis) {
1860
+ const projectName = getProjectName(ctx);
1861
+ const stackSummary = answers.stackConfirmed ? summarizeDetection(ctx) : answers.stackCorrections || summarizeDetection(ctx);
1862
+ const sections = [];
1863
+ sections.push(`# ${projectName}`);
1864
+ sections.push("");
1865
+ sections.push(
1866
+ "> **Keep this file up to date.** When you change the architecture, add a dependency, create a new pattern, or learn a gotcha, update this file in the same step. This is the source of truth for how the project works."
1867
+ );
1868
+ if (answers.ide === "cursor") {
1869
+ sections.push(
1870
+ "> Scoped rules are in `.cursor/rules/` -- update them when conventions change."
1871
+ );
1872
+ }
1873
+ sections.push("");
1874
+ sections.push("## What Is This");
1875
+ sections.push("");
1876
+ sections.push(answers.projectPurpose);
1877
+ sections.push("");
1878
+ sections.push("## Tech Stack");
1879
+ sections.push("");
1880
+ sections.push(buildTechStackSection(ctx, stackSummary));
1881
+ sections.push("");
1882
+ const fwHints = getFrameworkHintsSection(ctx);
1883
+ if (fwHints) {
1884
+ sections.push(fwHints);
1885
+ }
1886
+ if (ctx.directories.length > 0) {
1887
+ sections.push("## Project Structure");
1888
+ sections.push("");
1889
+ sections.push("```");
1890
+ sections.push(buildStructureTree(ctx));
1891
+ sections.push("```");
1892
+ sections.push("");
1893
+ }
1894
+ if (ctx.monorepo && ctx.monorepo.packages.length > 0) {
1895
+ sections.push("## Monorepo Structure");
1896
+ sections.push("");
1897
+ sections.push(
1898
+ `${ctx.monorepo.type} workspace with ${ctx.monorepo.packages.length} packages:`
1899
+ );
1900
+ sections.push("");
1901
+ for (const pkg of ctx.monorepo.packages) {
1902
+ const fws = pkg.frameworks.length > 0 ? ` \u2014 ${pkg.frameworks.map((f) => f.name).join(", ")}` : "";
1903
+ sections.push(`- **${pkg.name}** (\`${pkg.path}\`)${fws}`);
1904
+ }
1905
+ sections.push("");
1906
+ }
1907
+ if (snapshot?.markdown) {
1908
+ sections.push("## Code Snapshot");
1909
+ sections.push("");
1910
+ sections.push(
1911
+ "<!-- CODE SNAPSHOT (auto-generated, update when types/stores/services change) -->"
1912
+ );
1913
+ sections.push("");
1914
+ sections.push(snapshot.markdown);
1915
+ sections.push("");
1916
+ sections.push("<!-- /CODE SNAPSHOT -->");
1917
+ sections.push("");
1918
+ }
1919
+ if (analysis?.hubFiles && analysis.hubFiles.length > 0) {
1920
+ const instabilityMap = /* @__PURE__ */ new Map();
1921
+ if (analysis.instabilities) {
1922
+ for (const inst of analysis.instabilities) {
1923
+ instabilityMap.set(inst.path, inst.instability);
1924
+ }
1925
+ }
1926
+ sections.push("## Key Files");
1927
+ sections.push("");
1928
+ sections.push(
1929
+ "These are the most interconnected files. Read these first for architectural understanding."
1930
+ );
1931
+ sections.push("");
1932
+ sections.push("| File | Imported By | Stability |");
1933
+ sections.push("|------|-------------|-----------|");
1934
+ for (const hub of analysis.hubFiles) {
1935
+ const inst = instabilityMap.get(hub.path);
1936
+ const stabilityCell = inst != null ? `${(inst * 100).toFixed(0)}% unstable \u26A0\uFE0F` : "stable";
1937
+ sections.push(
1938
+ `| \`${hub.path}\` | ${hub.importedBy} file${hub.importedBy === 1 ? "" : "s"} | ${stabilityCell} |`
1939
+ );
1940
+ }
1941
+ sections.push("");
1942
+ }
1943
+ if (analysis?.layers && analysis.layers.length > 1) {
1944
+ sections.push("## Architecture");
1945
+ sections.push("");
1946
+ const diagram = renderArchitectureDiagram(analysis.layers, analysis.layerEdges ?? []);
1947
+ sections.push(diagram);
1948
+ sections.push("");
1949
+ }
1950
+ if (analysis?.gitActivity && analysis.gitActivity.hotFiles.length > 0) {
1951
+ sections.push("## Recently Active Files");
1952
+ sections.push("");
1953
+ sections.push("| File | Commits (90d) | Last Changed |");
1954
+ sections.push("|------|--------------|--------------|");
1955
+ for (const hot of analysis.gitActivity.hotFiles.slice(0, 10)) {
1956
+ sections.push(
1957
+ `| \`${hot.path}\` | ${hot.commits} | ${hot.lastChanged} |`
1958
+ );
1959
+ }
1960
+ sections.push("");
1961
+ }
1962
+ if (analysis?.gitActivity?.changeCoupling && analysis.gitActivity.changeCoupling.length > 0) {
1963
+ sections.push("## Change Coupling");
1964
+ sections.push("");
1965
+ sections.push(
1966
+ "Files that frequently change together. Consider whether they should be colocated or decoupled."
1967
+ );
1968
+ sections.push("");
1969
+ sections.push("| File A | File B | Co-changes | Confidence |");
1970
+ sections.push("|--------|--------|------------|------------|");
1971
+ for (const pair of analysis.gitActivity.changeCoupling) {
1972
+ sections.push(
1973
+ `| \`${pair.fileA}\` | \`${pair.fileB}\` | ${pair.coChangeCount} | ${(pair.confidence * 100).toFixed(0)}% |`
1974
+ );
1975
+ }
1976
+ sections.push("");
1977
+ }
1978
+ if (analysis?.circularDeps && analysis.circularDeps.length > 0) {
1979
+ sections.push("## Circular Dependencies");
1980
+ sections.push("");
1981
+ sections.push(
1982
+ "> These circular import chains may cause unexpected behavior when modified."
1983
+ );
1984
+ sections.push("");
1985
+ for (const dep of analysis.circularDeps) {
1986
+ sections.push(`- ${dep.chain.map((f) => `\`${f}\``).join(" -> ")}`);
1987
+ }
1988
+ sections.push("");
1989
+ }
1990
+ if (analysis?.exportCoverage && analysis.exportCoverage.length > 0) {
1991
+ const underCovered = analysis.exportCoverage.filter((e) => e.coverage < 1);
1992
+ if (underCovered.length > 0) {
1993
+ sections.push("## Export Coverage");
1994
+ sections.push("");
1995
+ sections.push(
1996
+ "Files with unused exports. Consider removing dead exports to reduce surface area."
1997
+ );
1998
+ sections.push("");
1999
+ sections.push("| File | Used | Total | Coverage |");
2000
+ sections.push("|------|------|-------|----------|");
2001
+ for (const entry of underCovered.slice(0, 10)) {
2002
+ sections.push(
2003
+ `| \`${entry.file}\` | ${entry.usedExports} | ${entry.totalExports} | ${(entry.coverage * 100).toFixed(0)}% |`
2004
+ );
2005
+ }
2006
+ sections.push("");
2007
+ }
2008
+ }
2009
+ if (analysis?.communities && analysis.communities.length > 0) {
2010
+ sections.push("## Module Clusters");
2011
+ sections.push("");
2012
+ sections.push(
2013
+ "Automatically detected groups of tightly-connected files."
2014
+ );
2015
+ sections.push("");
2016
+ for (const community of analysis.communities) {
2017
+ sections.push(`- **${community.label}** (${community.files.length} files): ${community.files.map((f) => `\`${f}\``).join(", ")}`);
2018
+ }
2019
+ sections.push("");
2020
+ }
2021
+ if (answers.keyPatterns) {
2022
+ sections.push("## Key Patterns");
2023
+ sections.push("");
2024
+ const patterns = answers.keyPatterns.split(/[.\n]/).map((s) => s.trim()).filter(Boolean);
2025
+ for (const p5 of patterns) {
2026
+ sections.push(`- ${p5}`);
2027
+ }
2028
+ sections.push("");
2029
+ }
2030
+ if (answers.gotchas) {
2031
+ sections.push("## Gotchas");
2032
+ sections.push("");
2033
+ const gotchas = answers.gotchas.split(/[.\n]/).map((s) => s.trim()).filter(Boolean);
2034
+ for (const g of gotchas) {
2035
+ sections.push(`- ${g}`);
2036
+ }
2037
+ sections.push("");
2038
+ }
2039
+ sections.push("## Development");
2040
+ sections.push("");
2041
+ sections.push(buildDevSection(ctx));
2042
+ sections.push("");
2043
+ return sections.join("\n").trimEnd() + "\n";
2044
+ }
2045
+ function renderArchitectureDiagram(layers, layerEdges) {
2046
+ if (layers.length > 6 || layerEdges.length === 0) {
2047
+ const layerNames = layers.map((l) => `\`${l.name}\``);
2048
+ return "Dependency flow (foundational \u2192 consumer):\n\n" + layerNames.join(" \u2192 ");
2049
+ }
2050
+ const maxPerRow = 3;
2051
+ const rows = [];
2052
+ for (let i = 0; i < layers.length; i += maxPerRow) {
2053
+ rows.push(layers.slice(i, i + maxPerRow));
2054
+ }
2055
+ const boxWidth = 14;
2056
+ const boxOuter = boxWidth + 2;
2057
+ const gap = 4;
2058
+ const layerSet = new Set(layers.map((l) => l.name));
2059
+ const layerRowIndex = /* @__PURE__ */ new Map();
2060
+ const layerColIndex = /* @__PURE__ */ new Map();
2061
+ for (let r = 0; r < rows.length; r++) {
2062
+ for (let c = 0; c < rows[r].length; c++) {
2063
+ layerRowIndex.set(rows[r][c].name, r);
2064
+ layerColIndex.set(rows[r][c].name, c);
2065
+ }
2066
+ }
2067
+ const horizontalEdges = [];
2068
+ const verticalEdges = [];
2069
+ for (const edge of layerEdges) {
2070
+ if (!layerSet.has(edge.from) || !layerSet.has(edge.to)) continue;
2071
+ const fromRow = layerRowIndex.get(edge.from);
2072
+ const toRow = layerRowIndex.get(edge.to);
2073
+ if (fromRow === toRow) {
2074
+ horizontalEdges.push(edge);
2075
+ } else {
2076
+ verticalEdges.push(edge);
2077
+ }
2078
+ }
2079
+ const lines = [];
2080
+ lines.push("```");
2081
+ for (let r = 0; r < rows.length; r++) {
2082
+ const row = rows[r];
2083
+ const topLine = row.map((l) => {
2084
+ const name = l.name;
2085
+ const pad = boxWidth - name.length;
2086
+ return "\u250C" + "\u2500".repeat(boxWidth) + "\u2510";
2087
+ }).join(" ".repeat(gap));
2088
+ lines.push(topLine);
2089
+ const midLine = row.map((l) => {
2090
+ const name = l.name;
2091
+ const padLeft = Math.floor((boxWidth - name.length) / 2);
2092
+ const padRight = boxWidth - name.length - padLeft;
2093
+ return "\u2502" + " ".repeat(padLeft) + name + " ".repeat(padRight) + "\u2502";
2094
+ }).join(" ".repeat(gap));
2095
+ lines.push(midLine);
2096
+ const botLine = row.map(() => "\u2514" + "\u2500".repeat(boxWidth) + "\u2518").join(" ".repeat(gap));
2097
+ lines.push(botLine);
2098
+ if (r < rows.length - 1) {
2099
+ const nextRow = rows[r + 1];
2100
+ const downEdges = verticalEdges.filter((e) => {
2101
+ const fromR = layerRowIndex.get(e.from);
2102
+ const toR = layerRowIndex.get(e.to);
2103
+ return fromR === r && toR === r + 1 || fromR === r + 1 && toR === r;
2104
+ });
2105
+ if (downEdges.length > 0) {
2106
+ const rowWidth = row.length * boxOuter + (row.length - 1) * gap;
2107
+ const arrowLine1 = buildArrowLine(row, "\u2502", boxOuter, gap, rowWidth);
2108
+ const arrowLine2 = buildArrowLine(nextRow.length > row.length ? nextRow : row, "\u25BC", boxOuter, gap, rowWidth);
2109
+ lines.push(arrowLine1);
2110
+ lines.push(arrowLine2);
2111
+ }
2112
+ }
2113
+ }
2114
+ if (horizontalEdges.length > 0) {
2115
+ lines.push("");
2116
+ for (const edge of horizontalEdges) {
2117
+ lines.push(` ${edge.from} \u2500\u2500\u2500\u2500\u25B6 ${edge.to}`);
2118
+ }
2119
+ }
2120
+ lines.push("```");
2121
+ return lines.join("\n");
2122
+ }
2123
+ function buildArrowLine(row, char, boxOuter, gap, _totalWidth) {
2124
+ const parts = [];
2125
+ for (let c = 0; c < row.length; c++) {
2126
+ const center = Math.floor(boxOuter / 2);
2127
+ if (c > 0) {
2128
+ parts.push(" ".repeat(gap));
2129
+ }
2130
+ parts.push(" ".repeat(center) + char + " ".repeat(boxOuter - center - 1));
2131
+ }
2132
+ return parts.join("");
2133
+ }
2134
+ function getProjectName(ctx) {
2135
+ const dirName = ctx.rootDir.split("/").pop() ?? "Project";
2136
+ return dirName.charAt(0).toUpperCase() + dirName.slice(1);
2137
+ }
2138
+ function buildTechStackSection(ctx, summary) {
2139
+ const lines = [];
2140
+ if (ctx.frameworks.length > 0) {
2141
+ for (const fw of ctx.frameworks) {
2142
+ const ver = fw.version ? ` ${fw.version}` : "";
2143
+ const usage = fw.importCount != null ? ` (used in ${fw.importCount} file${fw.importCount === 1 ? "" : "s"})` : "";
2144
+ lines.push(`- **${fw.name}**${ver}${usage}`);
2145
+ }
2146
+ }
2147
+ if (ctx.hasTypeScript) {
2148
+ lines.push("- **TypeScript**");
2149
+ }
2150
+ if (ctx.linter !== "none") {
2151
+ const name = ctx.linter.charAt(0).toUpperCase() + ctx.linter.slice(1);
2152
+ lines.push(`- **${name}** (linter/formatter)`);
2153
+ }
2154
+ if (ctx.packageManager !== "none") {
2155
+ lines.push(`- **${ctx.packageManager}** (package manager)`);
2156
+ }
2157
+ if (lines.length === 0) {
2158
+ lines.push(`Stack: ${summary}`);
2159
+ }
2160
+ return lines.join("\n");
2161
+ }
2162
+ function buildStructureTree(ctx) {
2163
+ const lines = [];
2164
+ const grouped = /* @__PURE__ */ new Map();
2165
+ for (const dir of ctx.directories) {
2166
+ const parts = dir.split("/");
2167
+ if (parts.length === 1) {
2168
+ if (!grouped.has(dir)) grouped.set(dir, []);
2169
+ } else {
2170
+ const parent = parts[0];
2171
+ const child = parts.slice(1).join("/");
2172
+ const children = grouped.get(parent) ?? [];
2173
+ children.push(child);
2174
+ grouped.set(parent, children);
2175
+ }
2176
+ }
2177
+ for (const [dir, children] of grouped) {
2178
+ lines.push(`${dir}/`);
2179
+ for (const child of children) {
2180
+ lines.push(` ${child}/`);
2181
+ }
2182
+ }
2183
+ return lines.join("\n");
2184
+ }
2185
+ function buildDevSection(ctx) {
2186
+ const lines = [];
2187
+ switch (ctx.packageManager) {
2188
+ case "pnpm":
2189
+ lines.push("```bash");
2190
+ lines.push("pnpm install");
2191
+ lines.push("pnpm dev");
2192
+ lines.push("```");
2193
+ break;
2194
+ case "yarn":
2195
+ lines.push("```bash");
2196
+ lines.push("yarn install");
2197
+ lines.push("yarn dev");
2198
+ lines.push("```");
2199
+ break;
2200
+ case "bun":
2201
+ lines.push("```bash");
2202
+ lines.push("bun install");
2203
+ lines.push("bun dev");
2204
+ lines.push("```");
2205
+ break;
2206
+ case "npm":
2207
+ lines.push("```bash");
2208
+ lines.push("npm install");
2209
+ lines.push("npm run dev");
2210
+ lines.push("```");
2211
+ break;
2212
+ case "pip":
2213
+ case "poetry":
2214
+ lines.push("```bash");
2215
+ lines.push(
2216
+ ctx.packageManager === "poetry" ? "poetry install" : "pip install -r requirements.txt"
2217
+ );
2218
+ lines.push("```");
2219
+ break;
2220
+ case "cargo":
2221
+ lines.push("```bash");
2222
+ lines.push("cargo build");
2223
+ lines.push("cargo run");
2224
+ lines.push("```");
2225
+ break;
2226
+ case "go":
2227
+ lines.push("```bash");
2228
+ lines.push("go build ./...");
2229
+ lines.push("go run .");
2230
+ lines.push("```");
2231
+ break;
2232
+ default:
2233
+ lines.push("(add your build/run commands here)");
2234
+ }
2235
+ if (ctx.linter !== "none") {
2236
+ lines.push("");
2237
+ lines.push(`Linter: **${ctx.linter}**`);
2238
+ }
2239
+ return lines.join("\n");
2240
+ }
2241
+ function getMainContextFilename(ide) {
2242
+ switch (ide) {
2243
+ case "claude":
2244
+ return "CLAUDE.md";
2245
+ case "cursor":
2246
+ return "CLAUDE.md";
2247
+ case "opencode":
2248
+ return "AGENTS.md";
2249
+ case "copilot":
2250
+ return ".github/copilot-instructions.md";
2251
+ case "windsurf":
2252
+ return ".windsurfrules";
2253
+ case "cline":
2254
+ return ".clinerules";
2255
+ case "continue":
2256
+ return ".continuerules";
2257
+ case "aider":
2258
+ return ".aider.conf.yml";
2259
+ case "generic":
2260
+ return "CONTEXT.md";
2261
+ }
2262
+ }
2263
+
2264
+ // src/templates/cursor-rules.ts
2265
+ function buildCursorRules(ctx, answers, analysis) {
2266
+ const rules = [];
2267
+ rules.push(buildGlobalRule(ctx, answers, analysis));
2268
+ const hasComponents = ctx.directories.some(
2269
+ (d) => d.endsWith("components") || d.includes("components/")
2270
+ );
2271
+ if (hasComponents) {
2272
+ rules.push(buildComponentsRule(ctx));
2273
+ }
2274
+ const hasServices = ctx.directories.some(
2275
+ (d) => d.endsWith("services") || d.endsWith("api") || d.includes("services/") || d.includes("api/")
2276
+ );
2277
+ if (hasServices) {
2278
+ rules.push(buildServicesRule(ctx));
2279
+ }
2280
+ const hasStores = ctx.directories.some(
2281
+ (d) => d.endsWith("stores") || d.endsWith("store") || d.includes("stores/") || d.includes("store/")
2282
+ );
2283
+ if (hasStores) {
2284
+ rules.push(buildStoresRule(ctx));
2285
+ }
2286
+ return rules;
2287
+ }
2288
+ function buildGlobalRule(ctx, answers, analysis) {
2289
+ const bodyLines = [
2290
+ "# Global Rules",
2291
+ "",
2292
+ `> Update this rule and the main context file after any architectural or convention change.`,
2293
+ ""
2294
+ ];
2295
+ if (answers.gotchas) {
2296
+ bodyLines.push("## Gotchas");
2297
+ bodyLines.push("");
2298
+ const gotchas = answers.gotchas.split(/[.\n]/).map((s) => s.trim()).filter(Boolean);
2299
+ for (const g of gotchas) {
2300
+ bodyLines.push(`- ${g}`);
2301
+ }
2302
+ bodyLines.push("");
2303
+ }
2304
+ if (analysis?.instabilities && analysis.instabilities.length > 0) {
2305
+ bodyLines.push("## High-Instability Files");
2306
+ bodyLines.push("");
2307
+ bodyLines.push(
2308
+ "> These files have many dependents but also many dependencies (unstable). Changes here have high blast radius."
2309
+ );
2310
+ bodyLines.push("");
2311
+ for (const inst of analysis.instabilities) {
2312
+ bodyLines.push(`- \`${inst.path}\` \u2014 ${(inst.instability * 100).toFixed(0)}% unstable (${inst.fanIn} dependents, ${inst.fanOut} dependencies)`);
2313
+ }
2314
+ bodyLines.push("");
2315
+ }
2316
+ if (analysis?.gitActivity?.changeCoupling && analysis.gitActivity.changeCoupling.length > 0) {
2317
+ bodyLines.push("## Change Coupling");
2318
+ bodyLines.push("");
2319
+ bodyLines.push(
2320
+ "> These file pairs frequently change together. When modifying one, check the other."
2321
+ );
2322
+ bodyLines.push("");
2323
+ for (const pair of analysis.gitActivity.changeCoupling.slice(0, 5)) {
2324
+ bodyLines.push(`- \`${pair.fileA}\` \u2194 \`${pair.fileB}\` (${pair.coChangeCount} co-changes, ${(pair.confidence * 100).toFixed(0)}% confidence)`);
2325
+ }
2326
+ bodyLines.push("");
2327
+ }
2328
+ if (analysis?.circularDeps && analysis.circularDeps.length > 0) {
2329
+ bodyLines.push("## Circular Dependencies");
2330
+ bodyLines.push("");
2331
+ bodyLines.push(
2332
+ "> These circular import chains may cause unexpected behavior. Avoid adding to them."
2333
+ );
2334
+ bodyLines.push("");
2335
+ for (const dep of analysis.circularDeps) {
2336
+ bodyLines.push(`- ${dep.chain.join(" -> ")}`);
2337
+ }
2338
+ bodyLines.push("");
2339
+ }
2340
+ if (analysis?.communities && analysis.communities.length > 0) {
2341
+ bodyLines.push("## Module Clusters");
2342
+ bodyLines.push("");
2343
+ bodyLines.push(
2344
+ "> Tightly-connected file groups. Changes in one file likely affect others in the same cluster."
2345
+ );
2346
+ bodyLines.push("");
2347
+ for (const community of analysis.communities.slice(0, 5)) {
2348
+ bodyLines.push(`- **${community.label}** (${community.files.length} files): ${community.files.map((f) => `\`${f}\``).join(", ")}`);
2349
+ }
2350
+ bodyLines.push("");
2351
+ }
2352
+ const fwHints = getFrameworkHints(ctx);
2353
+ if (fwHints.length > 0) {
2354
+ bodyLines.push("## Framework Conventions");
2355
+ bodyLines.push("");
2356
+ for (const hint of fwHints) {
2357
+ bodyLines.push(hint);
2358
+ }
2359
+ bodyLines.push("");
2360
+ }
2361
+ if (ctx.linter !== "none") {
2362
+ bodyLines.push(
2363
+ `- Linter: **${ctx.linter}**. Run lint before committing.`
2364
+ );
2365
+ }
2366
+ bodyLines.push(
2367
+ "- After any architectural or convention change, update the relevant context files."
2368
+ );
2369
+ const ext = getExtGlob(ctx);
2370
+ return {
2371
+ filename: "global.md",
2372
+ description: "Universal project rules",
2373
+ globs: `**/*.${ext}`,
2374
+ body: bodyLines.join("\n")
2375
+ };
2376
+ }
2377
+ function buildComponentsRule(ctx) {
2378
+ const compDir = ctx.directories.find(
2379
+ (d) => d.endsWith("components") || d.includes("components/")
2380
+ ) ?? "src/components";
2381
+ const bodyLines = [
2382
+ "# UI Components",
2383
+ "",
2384
+ "> Update this rule when component conventions change.",
2385
+ "",
2386
+ "## Conventions",
2387
+ ""
2388
+ ];
2389
+ const hasReact = ctx.frameworks.some(
2390
+ (f) => f.name === "React" || f.name === "React Native"
2391
+ );
2392
+ const hasTailwind = ctx.frameworks.some(
2393
+ (f) => f.name === "Tailwind CSS" || f.name === "NativeWind"
2394
+ );
2395
+ if (hasReact) {
2396
+ bodyLines.push("- Functional components with hooks (no class components)");
2397
+ bodyLines.push("- Use `memo()` for expensive renders");
2398
+ }
2399
+ if (hasTailwind) {
2400
+ bodyLines.push(
2401
+ "- Use `className` for layout/styling via Tailwind. Inline `style` only for dynamic/theme values."
2402
+ );
2403
+ }
2404
+ bodyLines.push(
2405
+ "- Props interfaces defined adjacent to the component"
2406
+ );
2407
+ bodyLines.push("- Keep components focused -- extract sub-components when complexity grows");
2408
+ const appGlob = ctx.directories.includes("app") ? ", app/**/*.tsx" : "";
2409
+ return {
2410
+ filename: "ui-components.md",
2411
+ description: "Component conventions and patterns",
2412
+ globs: `${compDir}/**/*.{ts,tsx,js,jsx}${appGlob}`,
2413
+ body: bodyLines.join("\n")
2414
+ };
2415
+ }
2416
+ function buildServicesRule(ctx) {
2417
+ const svcDir = ctx.directories.find(
2418
+ (d) => d.endsWith("services") || d.endsWith("api") || d.includes("services/") || d.includes("api/")
2419
+ ) ?? "src/services";
2420
+ const hooksGlob = ctx.directories.some((d) => d.endsWith("hooks")) ? `, ${ctx.directories.find((d) => d.endsWith("hooks"))}/**/*.{ts,js}` : "";
2421
+ const bodyLines = [
2422
+ "# Services & API Layer",
2423
+ "",
2424
+ "> Update this rule when service patterns or API conventions change.",
2425
+ "",
2426
+ "## Conventions",
2427
+ "",
2428
+ "- Services should be pure functions or classes, not React hooks",
2429
+ "- Error handling: always catch and provide meaningful error messages",
2430
+ "- Keep service functions focused on a single responsibility"
2431
+ ];
2432
+ return {
2433
+ filename: "services.md",
2434
+ description: "Service and API layer patterns",
2435
+ globs: `${svcDir}/**/*.{ts,js}${hooksGlob}`,
2436
+ body: bodyLines.join("\n")
2437
+ };
2438
+ }
2439
+ function buildStoresRule(ctx) {
2440
+ const storeDir = ctx.directories.find(
2441
+ (d) => d.endsWith("stores") || d.endsWith("store") || d.includes("stores/") || d.includes("store/")
2442
+ ) ?? "src/stores";
2443
+ const bodyLines = [
2444
+ "# State Management",
2445
+ "",
2446
+ "> Update this rule when state patterns change.",
2447
+ "",
2448
+ "## Conventions",
2449
+ ""
2450
+ ];
2451
+ const hasZustand = ctx.frameworks.some((f) => f.name === "Zustand");
2452
+ const hasRedux = ctx.frameworks.some(
2453
+ (f) => f.name === "Redux" || f.name === "Redux Toolkit"
2454
+ );
2455
+ const hasPinia = ctx.frameworks.some((f) => f.name === "Pinia");
2456
+ if (hasZustand) {
2457
+ bodyLines.push(
2458
+ "- **Zustand** slice architecture: each slice is a `StateCreator` accessing full state via `get()`"
2459
+ );
2460
+ bodyLines.push("- Immutable updates: always spread state");
2461
+ bodyLines.push("- Cross-slice access via `get()` (not separate store imports)");
2462
+ } else if (hasRedux) {
2463
+ bodyLines.push("- **Redux Toolkit** slices with `createSlice`");
2464
+ bodyLines.push("- Async logic in thunks (`createAsyncThunk`)");
2465
+ bodyLines.push("- Selectors for reading state, dispatches for writing");
2466
+ } else if (hasPinia) {
2467
+ bodyLines.push("- **Pinia** stores with composition API");
2468
+ bodyLines.push("- Getters for derived state, actions for mutations");
2469
+ } else {
2470
+ bodyLines.push("- Keep state updates immutable");
2471
+ bodyLines.push("- Separate concerns into distinct store modules");
2472
+ }
2473
+ bodyLines.push("- Silent failures for persistence (app should work even if storage fails)");
2474
+ return {
2475
+ filename: "stores.md",
2476
+ description: "State management patterns",
2477
+ globs: `${storeDir}/**/*.{ts,js}`,
2478
+ body: bodyLines.join("\n")
2479
+ };
2480
+ }
2481
+ function getExtGlob(ctx) {
2482
+ switch (ctx.language) {
2483
+ case "typescript":
2484
+ return "{ts,tsx}";
2485
+ case "javascript":
2486
+ return "{js,jsx}";
2487
+ case "python":
2488
+ return "py";
2489
+ case "go":
2490
+ return "go";
2491
+ case "rust":
2492
+ return "rs";
2493
+ default:
2494
+ return "{ts,tsx,js,jsx}";
2495
+ }
2496
+ }
2497
+ function renderCursorRule(rule) {
2498
+ return [
2499
+ "---",
2500
+ `description: ${rule.description}`,
2501
+ `globs: ${rule.globs}`,
2502
+ "---",
2503
+ "",
2504
+ rule.body,
2505
+ ""
2506
+ ].join("\n");
2507
+ }
2508
+
2509
+ // src/templates/claude-skills.ts
2510
+ function buildClaudeSkills(ctx, answers, analysis, scripts) {
2511
+ const skills = [];
2512
+ if (scripts) {
2513
+ skills.push(...buildScriptSkills(ctx, scripts));
2514
+ }
2515
+ const archSkill = buildArchitectureSkill(analysis);
2516
+ if (archSkill) skills.push(archSkill);
2517
+ return skills;
2518
+ }
2519
+ function buildScriptSkills(ctx, scripts) {
2520
+ const skills = [];
2521
+ const scriptMap = {
2522
+ test: { description: "Run the test suite", keywords: ["test", "test:unit", "test:e2e"] },
2523
+ build: { description: "Build the project", keywords: ["build", "build:prod"] },
2524
+ lint: { description: "Run linter", keywords: ["lint", "lint:fix"] },
2525
+ dev: { description: "Start development server", keywords: ["dev", "start:dev"] },
2526
+ typecheck: { description: "Run type checking", keywords: ["typecheck", "type-check", "check-types"] }
2527
+ };
2528
+ const runCmd = getRunCommand(ctx);
2529
+ for (const [skillName, config] of Object.entries(scriptMap)) {
2530
+ const matchedScript = config.keywords.find((kw) => kw in scripts);
2531
+ if (!matchedScript) continue;
2532
+ const command = `${runCmd} ${matchedScript}`;
2533
+ skills.push({
2534
+ name: skillName,
2535
+ description: config.description,
2536
+ disableModelInvocation: true,
2537
+ body: [
2538
+ `# ${config.description}`,
2539
+ "",
2540
+ `Run: \`${command}\``
2541
+ ].join("\n")
2542
+ });
2543
+ }
2544
+ return skills;
2545
+ }
2546
+ function getRunCommand(ctx) {
2547
+ switch (ctx.packageManager) {
2548
+ case "pnpm":
2549
+ return "pnpm";
2550
+ case "yarn":
2551
+ return "yarn";
2552
+ case "bun":
2553
+ return "bun run";
2554
+ case "npm":
2555
+ return "npm run";
2556
+ case "cargo":
2557
+ return "cargo";
2558
+ case "go":
2559
+ return "go";
2560
+ default:
2561
+ return "npm run";
2562
+ }
2563
+ }
2564
+ function buildArchitectureSkill(analysis) {
2565
+ if (!analysis) return null;
2566
+ const bodyLines = [
2567
+ "# Architecture Explorer",
2568
+ "",
2569
+ "Use this skill to understand the project architecture before making changes.",
2570
+ ""
2571
+ ];
2572
+ if (analysis.hubFiles.length > 0) {
2573
+ bodyLines.push("## Key Files (by centrality)");
2574
+ bodyLines.push("");
2575
+ for (const hub of analysis.hubFiles) {
2576
+ bodyLines.push(`- \`${hub.path}\` \u2014 imported by ${hub.importedBy} file${hub.importedBy === 1 ? "" : "s"}`);
2577
+ }
2578
+ bodyLines.push("");
2579
+ }
2580
+ if (analysis.layers.length > 0) {
2581
+ bodyLines.push("## Architecture Layers");
2582
+ bodyLines.push("");
2583
+ bodyLines.push("Dependency flow (foundational \u2192 consumer):");
2584
+ bodyLines.push("");
2585
+ bodyLines.push(analysis.layers.map((l) => `\`${l.name}\``).join(" \u2192 "));
2586
+ bodyLines.push("");
2587
+ }
2588
+ if (analysis.circularDeps.length > 0) {
2589
+ bodyLines.push("## Circular Dependencies");
2590
+ bodyLines.push("");
2591
+ for (const dep of analysis.circularDeps) {
2592
+ bodyLines.push(`- ${dep.chain.map((f) => `\`${f}\``).join(" -> ")}`);
2593
+ }
2594
+ bodyLines.push("");
2595
+ }
2596
+ if (analysis.communities.length > 0) {
2597
+ bodyLines.push("## Module Clusters");
2598
+ bodyLines.push("");
2599
+ for (const community of analysis.communities) {
2600
+ bodyLines.push(`- **${community.label}** (${community.files.length} files)`);
2601
+ }
2602
+ bodyLines.push("");
2603
+ }
2604
+ if (analysis.hubFiles.length === 0 && analysis.layers.length === 0) {
2605
+ return null;
2606
+ }
2607
+ return {
2608
+ name: "architecture",
2609
+ description: "Explore project architecture and key files",
2610
+ disableModelInvocation: false,
2611
+ allowedTools: "Read, Grep, Glob",
2612
+ body: bodyLines.join("\n")
2613
+ };
2614
+ }
2615
+ function renderClaudeSkill(skill) {
2616
+ const lines = [
2617
+ "---",
2618
+ `description: ${skill.description}`
2619
+ ];
2620
+ if (skill.disableModelInvocation) {
2621
+ lines.push("disable-model-invocation: true");
2622
+ }
2623
+ if (skill.allowedTools) {
2624
+ lines.push(`allowed-tools: ${skill.allowedTools}`);
2625
+ }
2626
+ lines.push("---");
2627
+ lines.push("");
2628
+ lines.push(skill.body);
2629
+ lines.push("");
2630
+ return lines.join("\n");
2631
+ }
2632
+
2633
+ // src/templates/aider-context.ts
2634
+ function buildAiderContext(ctx, answers, snapshot, analysis) {
2635
+ const lines = [];
2636
+ lines.push("# .aider.conf.yml \u2014 Generated by codebrief");
2637
+ lines.push("# Keep this file up to date as the project evolves.");
2638
+ lines.push("");
2639
+ const stackSummary = answers.stackConfirmed ? summarizeDetection(ctx) : answers.stackCorrections || summarizeDetection(ctx);
2640
+ lines.push("conventions:");
2641
+ lines.push(` - "Project: ${escapeYaml(answers.projectPurpose)}"`);
2642
+ lines.push(` - "Stack: ${escapeYaml(stackSummary)}"`);
2643
+ if (ctx.frameworks.length > 0) {
2644
+ const fws = ctx.frameworks.map((f) => `${f.name}${f.version ? ` ${f.version}` : ""}`).join(", ");
2645
+ lines.push(` - "Frameworks: ${escapeYaml(fws)}"`);
2646
+ }
2647
+ if (ctx.packageManager !== "none") {
2648
+ lines.push(` - "Package manager: ${ctx.packageManager}"`);
2649
+ }
2650
+ if (ctx.linter !== "none") {
2651
+ lines.push(` - "Linter: ${ctx.linter}"`);
2652
+ }
2653
+ if (ctx.directories.length > 0) {
2654
+ const dirs = ctx.directories.join(", ");
2655
+ lines.push(` - "Key directories: ${escapeYaml(dirs)}"`);
2656
+ }
2657
+ if (answers.keyPatterns) {
2658
+ const patterns = answers.keyPatterns.split(/[.\n]/).map((s) => s.trim()).filter(Boolean);
2659
+ for (const p5 of patterns) {
2660
+ lines.push(` - "${escapeYaml(p5)}"`);
2661
+ }
2662
+ }
2663
+ if (answers.gotchas) {
2664
+ const gotchas = answers.gotchas.split(/[.\n]/).map((s) => s.trim()).filter(Boolean);
2665
+ for (const g of gotchas) {
2666
+ lines.push(` - "GOTCHA: ${escapeYaml(g)}"`);
2667
+ }
2668
+ }
2669
+ const fwHints = getFrameworkHints(ctx);
2670
+ if (fwHints.length > 0) {
2671
+ for (const hint of fwHints) {
2672
+ const trimmed = hint.trim();
2673
+ if (trimmed && trimmed.startsWith("- ")) {
2674
+ lines.push(` - "${escapeYaml(trimmed.slice(2))}"`);
2675
+ }
2676
+ }
2677
+ }
2678
+ if (ctx.monorepo && ctx.monorepo.packages.length > 0) {
2679
+ lines.push(
2680
+ ` - "Monorepo: ${ctx.monorepo.type} with ${ctx.monorepo.packages.length} packages"`
2681
+ );
2682
+ for (const pkg of ctx.monorepo.packages) {
2683
+ const fws = pkg.frameworks.length > 0 ? ` (${pkg.frameworks.map((f) => f.name).join(", ")})` : "";
2684
+ lines.push(` - "Package: ${escapeYaml(pkg.name)} at ${escapeYaml(pkg.path)}${escapeYaml(fws)}"`);
2685
+ }
2686
+ }
2687
+ if (analysis?.hubFiles && analysis.hubFiles.length > 0) {
2688
+ lines.push(` - "Key files (read these first for architecture): ${escapeYaml(analysis.hubFiles.map((h) => h.path).join(", "))}"`);
2689
+ }
2690
+ if (analysis?.layers && analysis.layers.length > 1) {
2691
+ const flow = analysis.layers.map((l) => l.name).join(" \u2192 ");
2692
+ lines.push(` - "Architecture dependency flow: ${escapeYaml(flow)}"`);
2693
+ }
2694
+ if (analysis?.instabilities && analysis.instabilities.length > 0) {
2695
+ for (const inst of analysis.instabilities) {
2696
+ lines.push(` - "UNSTABLE: ${escapeYaml(inst.path)} \u2014 ${(inst.instability * 100).toFixed(0)}% unstable (${inst.fanIn} dependents, ${inst.fanOut} deps)"`);
2697
+ }
2698
+ }
2699
+ if (analysis?.gitActivity?.changeCoupling && analysis.gitActivity.changeCoupling.length > 0) {
2700
+ for (const pair of analysis.gitActivity.changeCoupling.slice(0, 5)) {
2701
+ lines.push(` - "CO-CHANGE: ${escapeYaml(pair.fileA)} <-> ${escapeYaml(pair.fileB)} (${pair.coChangeCount} co-changes, ${(pair.confidence * 100).toFixed(0)}% confidence)"`);
2702
+ }
2703
+ }
2704
+ if (analysis?.circularDeps && analysis.circularDeps.length > 0) {
2705
+ for (const dep of analysis.circularDeps) {
2706
+ lines.push(` - "CIRCULAR DEP: ${escapeYaml(dep.chain.join(" -> "))}"`);
2707
+ }
2708
+ }
2709
+ if (analysis?.gitActivity && analysis.gitActivity.hotFiles.length > 0) {
2710
+ const hot = analysis.gitActivity.hotFiles.slice(0, 5).map((h) => `${h.path} (${h.commits} commits)`).join(", ");
2711
+ lines.push(` - "Recently active: ${escapeYaml(hot)}"`);
2712
+ }
2713
+ if (snapshot?.markdown) {
2714
+ lines.push("");
2715
+ lines.push(
2716
+ "# CODE SNAPSHOT (auto-generated, update when types/stores/services change)"
2717
+ );
2718
+ lines.push("# Formatted as a multi-line read block for Aider context.");
2719
+ lines.push("read:");
2720
+ lines.push(
2721
+ " # To include the code snapshot, create a file and add it here:"
2722
+ );
2723
+ lines.push(" # - .aider.context.md");
2724
+ lines.push("");
2725
+ lines.push("# --- Code Snapshot (for reference) ---");
2726
+ for (const line of snapshot.markdown.split("\n")) {
2727
+ lines.push(`# ${line}`);
2728
+ }
2729
+ lines.push("# --- /Code Snapshot ---");
2730
+ }
2731
+ lines.push("");
2732
+ return lines.join("\n");
2733
+ }
2734
+ function escapeYaml(s) {
2735
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2736
+ }
2737
+
2738
+ // src/generate.ts
2739
+ async function generateFiles(ctx, answers, snapshot, force = false, dryRun = false, analysis, generateSkills = false, onVerbose) {
2740
+ const files = [];
2741
+ const mainFilename = getMainContextFilename(answers.ide);
2742
+ const mainPath = path5.join(ctx.rootDir, mainFilename);
2743
+ const mainContent = answers.ide === "aider" ? buildAiderContext(ctx, answers, snapshot, analysis) : buildMainContext(ctx, answers, snapshot, analysis);
2744
+ files.push({
2745
+ path: mainFilename,
2746
+ content: mainContent,
2747
+ existed: await fileExists(mainPath)
2748
+ });
2749
+ onVerbose?.(`Prepared ${mainFilename} (${mainContent.length} bytes)`);
2750
+ if (answers.ide === "cursor") {
2751
+ const rules = buildCursorRules(ctx, answers, analysis);
2752
+ for (const rule of rules) {
2753
+ const rulePath = `.cursor/rules/${rule.filename}`;
2754
+ const absPath = path5.join(ctx.rootDir, rulePath);
2755
+ const ruleContent = renderCursorRule(rule);
2756
+ files.push({
2757
+ path: rulePath,
2758
+ content: ruleContent,
2759
+ existed: await fileExists(absPath)
2760
+ });
2761
+ onVerbose?.(`Prepared ${rulePath} (${ruleContent.length} bytes)`);
2762
+ }
2763
+ }
2764
+ if (generateSkills) {
2765
+ const pkgJson = await readJsonFile(path5.join(ctx.rootDir, "package.json"));
2766
+ const scripts = pkgJson?.scripts ?? void 0;
2767
+ const skills = buildClaudeSkills(ctx, answers, analysis, scripts);
2768
+ for (const skill of skills) {
2769
+ const skillPath = `.claude/skills/${skill.name}/SKILL.md`;
2770
+ const absPath = path5.join(ctx.rootDir, skillPath);
2771
+ const skillContent = renderClaudeSkill(skill);
2772
+ files.push({
2773
+ path: skillPath,
2774
+ content: skillContent,
2775
+ existed: await fileExists(absPath)
2776
+ });
2777
+ onVerbose?.(`Prepared ${skillPath} (${skillContent.length} bytes)`);
2778
+ }
2779
+ }
2780
+ if (answers.ide === "opencode") {
2781
+ const claudePath = path5.join(ctx.rootDir, "CLAUDE.md");
2782
+ const claudeExists = await fileExists(claudePath);
2783
+ if (!claudeExists) {
2784
+ files.push({
2785
+ path: "CLAUDE.md",
2786
+ content: `# ${path5.basename(ctx.rootDir)}
2787
+
2788
+ > See AGENTS.md for full project context.
2789
+ `,
2790
+ existed: false
2791
+ });
2792
+ }
2793
+ }
2794
+ if (answers.generatePerPackage && ctx.monorepo && ctx.monorepo.packages.length > 0) {
2795
+ const pkgMainFilename = answers.ide === "aider" ? ".aider.conf.yml" : getMainContextFilename(answers.ide);
2796
+ for (const pkg of ctx.monorepo.packages) {
2797
+ const pkgRootDir = path5.join(ctx.rootDir, pkg.path);
2798
+ const pkgCtx = await detectContext(pkgRootDir);
2799
+ let pkgSnapshot = null;
2800
+ if (answers.generateSnapshot) {
2801
+ pkgSnapshot = await generateSnapshot(pkgCtx, []);
2802
+ if (pkgSnapshot.entries.length === 0) pkgSnapshot = null;
2803
+ }
2804
+ const pkgAnswers = {
2805
+ ...answers,
2806
+ projectPurpose: `${pkg.name} \u2014 part of the ${path5.basename(ctx.rootDir)} monorepo. ${answers.projectPurpose}`,
2807
+ generatePerPackage: false
2808
+ // don't recurse
2809
+ };
2810
+ const pkgContent = answers.ide === "aider" ? buildAiderContext(pkgCtx, pkgAnswers, pkgSnapshot) : buildMainContext(pkgCtx, pkgAnswers, pkgSnapshot);
2811
+ const pkgFilePath = path5.join(pkg.path, pkgMainFilename);
2812
+ const pkgAbsPath = path5.join(ctx.rootDir, pkgFilePath);
2813
+ files.push({
2814
+ path: pkgFilePath,
2815
+ content: pkgContent,
2816
+ existed: await fileExists(pkgAbsPath)
2817
+ });
2818
+ }
2819
+ }
2820
+ if (dryRun) {
2821
+ return files;
2822
+ }
2823
+ const existingFiles = files.filter((f) => f.existed);
2824
+ if (existingFiles.length > 0 && !force) {
2825
+ p2.log.warn(
2826
+ `The following files already exist:
2827
+ ${existingFiles.map((f) => ` - ${f.path}`).join("\n")}`
2828
+ );
2829
+ const overwrite = await p2.confirm({
2830
+ message: "Overwrite existing files?"
2831
+ });
2832
+ if (p2.isCancel(overwrite) || !overwrite) {
2833
+ const newFiles = files.filter((f) => !f.existed);
2834
+ if (newFiles.length === 0) {
2835
+ p2.log.info("No new files to write. Exiting.");
2836
+ return [];
2837
+ }
2838
+ p2.log.info(`Writing ${newFiles.length} new file(s), skipping existing.`);
2839
+ for (const file of newFiles) {
2840
+ await writeFileSafe(path5.join(ctx.rootDir, file.path), file.content);
2841
+ }
2842
+ return newFiles;
2843
+ }
2844
+ }
2845
+ for (const file of files) {
2846
+ await writeFileSafe(path5.join(ctx.rootDir, file.path), file.content);
2847
+ onVerbose?.(`Wrote ${file.path}`);
2848
+ }
2849
+ return files;
2850
+ }
2851
+
2852
+ // src/summary.ts
2853
+ init_utils();
2854
+ import pc from "picocolors";
2855
+ function printSummary(files, ctx, snapshot, analysis) {
2856
+ if (files.length === 0) return;
2857
+ console.log("");
2858
+ console.log(pc.bold(" Files created:"));
2859
+ console.log("");
2860
+ let totalBytes = 0;
2861
+ let totalTokens = 0;
2862
+ let alwaysOnTokens = 0;
2863
+ let scopedRuleTokens = [];
2864
+ const mainFiles = files.filter((f) => !f.path.includes(".cursor/rules/"));
2865
+ const ruleFiles = files.filter((f) => f.path.includes(".cursor/rules/"));
2866
+ const fileRows = [];
2867
+ for (const file of mainFiles) {
2868
+ const bytes = Buffer.byteLength(file.content, "utf-8");
2869
+ const tokens = estimateTokens(file.content);
2870
+ totalBytes += bytes;
2871
+ totalTokens += tokens;
2872
+ alwaysOnTokens += tokens;
2873
+ fileRows.push({
2874
+ indent: " ",
2875
+ name: file.path,
2876
+ size: formatBytes(bytes),
2877
+ tokens: `(~${formatNumber(tokens)} tokens)`,
2878
+ isUpdated: !!file.existed,
2879
+ isHeader: false
2880
+ });
2881
+ }
2882
+ if (ruleFiles.length > 0) {
2883
+ fileRows.push({
2884
+ indent: " ",
2885
+ name: ".cursor/rules/",
2886
+ size: "",
2887
+ tokens: "",
2888
+ isUpdated: false,
2889
+ isHeader: true
2890
+ });
2891
+ for (const file of ruleFiles) {
2892
+ const bytes = Buffer.byteLength(file.content, "utf-8");
2893
+ const tokens = estimateTokens(file.content);
2894
+ totalBytes += bytes;
2895
+ totalTokens += tokens;
2896
+ const filename = file.path.split("/").pop() ?? file.path;
2897
+ const isGlobal = filename === "global.md";
2898
+ if (isGlobal) {
2899
+ alwaysOnTokens += tokens;
2900
+ } else {
2901
+ scopedRuleTokens.push(tokens);
2902
+ }
2903
+ fileRows.push({
2904
+ indent: " ",
2905
+ name: filename,
2906
+ size: formatBytes(bytes),
2907
+ tokens: `(~${formatNumber(tokens)} tokens)`,
2908
+ isUpdated: !!file.existed,
2909
+ isHeader: false
2910
+ });
2911
+ }
2912
+ }
2913
+ const dataFileRows = fileRows.filter((r) => !r.isHeader);
2914
+ const maxNameCol = Math.max(...fileRows.map((r) => r.indent.length + r.name.length));
2915
+ const maxSizeWidth = Math.max(...dataFileRows.map((r) => r.size.length));
2916
+ const maxTokenWidth = Math.max(...dataFileRows.map((r) => r.tokens.length));
2917
+ for (const row of fileRows) {
2918
+ if (row.isHeader) {
2919
+ console.log(`${row.indent}${pc.cyan(row.name)}`);
2920
+ } else {
2921
+ const status = row.isUpdated ? pc.yellow("(updated)") : pc.green("(new)");
2922
+ const paddedName = row.name.padEnd(maxNameCol - row.indent.length);
2923
+ console.log(
2924
+ `${row.indent}${pc.cyan(paddedName)} ${row.size.padStart(maxSizeWidth)} ${pc.dim(row.tokens.padEnd(maxTokenWidth))} ${status}`
2925
+ );
2926
+ }
2927
+ }
2928
+ console.log("");
2929
+ console.log(
2930
+ pc.dim(
2931
+ ` Total: ${formatBytes(totalBytes)}, ~${formatNumber(totalTokens)} tokens`
2932
+ )
2933
+ );
2934
+ if (snapshot?.budgetExcluded && snapshot.budgetExcluded > 0) {
2935
+ console.log(
2936
+ pc.dim(
2937
+ ` (${snapshot.budgetExcluded} snapshot entries excluded by token budget)`
2938
+ )
2939
+ );
2940
+ }
2941
+ if (analysis) {
2942
+ const parts = [];
2943
+ if (analysis.hubFiles.length > 0) parts.push(`${analysis.hubFiles.length} key files`);
2944
+ if (analysis.layers.length > 0) parts.push(`${analysis.layers.length} architecture layers`);
2945
+ if (analysis.circularDeps.length > 0) parts.push(`${analysis.circularDeps.length} circular deps`);
2946
+ if (analysis.communities.length > 0) parts.push(`${analysis.communities.length} module clusters`);
2947
+ if (analysis.gitActivity) parts.push(`${analysis.gitActivity.hotFiles.length} recently active files`);
2948
+ if (parts.length > 0) {
2949
+ console.log(
2950
+ pc.dim(` Includes: ${parts.join(", ")}`)
2951
+ );
2952
+ }
2953
+ }
2954
+ console.log("");
2955
+ console.log(pc.bold(" Estimated context cost per conversation:"));
2956
+ console.log("");
2957
+ const explorationTokens = estimateExplorationCost(ctx);
2958
+ const avgScopedTokens = scopedRuleTokens.length > 0 ? Math.round(
2959
+ scopedRuleTokens.reduce((a, b) => a + b, 0) / scopedRuleTokens.length
2960
+ ) : 0;
2961
+ const residualExploration = Math.round(explorationTokens * 0.05);
2962
+ const afterTotal = alwaysOnTokens + avgScopedTokens + residualExploration;
2963
+ const savings = Math.round(
2964
+ (explorationTokens - afterTotal) / explorationTokens * 100
2965
+ );
2966
+ const costRows = [
2967
+ { label: "Always loaded", desc: "main context + global rule", value: `~${formatNumber(alwaysOnTokens)} tokens` }
2968
+ ];
2969
+ if (avgScopedTokens > 0) {
2970
+ costRows.push({ label: "Per-task (avg)", desc: "1 scoped rule", value: `~${formatNumber(avgScopedTokens)} tokens` });
2971
+ }
2972
+ costRows.push({ label: "Exploration", desc: "mostly eliminated", value: `~${formatNumber(residualExploration)} tokens` });
2973
+ const maxCostLabel = Math.max(...costRows.map((r) => r.label.length));
2974
+ const maxCostDesc = Math.max(...costRows.map((r) => r.desc.length));
2975
+ const maxCostValue = Math.max(...costRows.map((r) => r.value.length));
2976
+ const afterContentWidth = maxCostLabel + 3 + maxCostDesc + 2 + maxCostValue;
2977
+ const beforeValue = `~${formatNumber(explorationTokens)} tokens`;
2978
+ const beforeLabel = "Exploration to understand codebase";
2979
+ const beforeGap = afterContentWidth - beforeLabel.length - beforeValue.length;
2980
+ console.log(pc.dim(" Before (no context files):"));
2981
+ if (beforeGap >= 2) {
2982
+ console.log(
2983
+ ` ${beforeLabel}${" ".repeat(beforeGap)}${pc.red(beforeValue)}`
2984
+ );
2985
+ } else {
2986
+ console.log(
2987
+ ` ${beforeLabel} ${pc.red(beforeValue)}`
2988
+ );
2989
+ }
2990
+ console.log("");
2991
+ console.log(pc.dim(" After:"));
2992
+ for (const row of costRows) {
2993
+ console.log(
2994
+ ` ${row.label.padEnd(maxCostLabel)} ${row.desc.padEnd(maxCostDesc)} ${pc.green(row.value.padStart(maxCostValue))}`
2995
+ );
2996
+ }
2997
+ const totalText = `Total: ~${formatNumber(afterTotal)} tokens`;
2998
+ const totalPad = afterContentWidth > totalText.length ? " ".repeat(afterContentWidth - totalText.length) : "";
2999
+ console.log(
3000
+ ` ${totalPad}${pc.bold(totalText)}`
3001
+ );
3002
+ console.log("");
3003
+ if (savings > 0) {
3004
+ console.log(
3005
+ pc.green(
3006
+ ` Estimated savings: ~${savings}% fewer tokens before real work begins`
3007
+ )
3008
+ );
3009
+ }
3010
+ if (analysis) {
3011
+ console.log("");
3012
+ console.log(pc.bold(" What we analyzed:"));
3013
+ const recapRows = [];
3014
+ if (analysis.hubFiles.length > 0) {
3015
+ recapRows.push({ label: "PageRank hub detection", result: `found ${analysis.hubFiles.length} key architectural files` });
3016
+ }
3017
+ recapRows.push({
3018
+ label: "Tarjan cycle detection",
3019
+ result: analysis.circularDeps.length === 0 ? "no circular dependencies found" : `${analysis.circularDeps.length} circular dep${analysis.circularDeps.length === 1 ? "" : "s"} found`
3020
+ });
3021
+ if (analysis.layers.length > 0) {
3022
+ const tierNames = analysis.layers.map((l) => l.name).join(" \u2192 ");
3023
+ recapRows.push({ label: "Layer analysis", result: `${analysis.layers.length} tiers: ${tierNames}` });
3024
+ }
3025
+ if (analysis.gitActivity) {
3026
+ const coupledPairs = analysis.gitActivity.changeCoupling.length;
3027
+ recapRows.push({
3028
+ label: "Git history (90 days)",
3029
+ result: `${analysis.gitActivity.hotFiles.length} hot files, ${coupledPairs} change-coupled pair${coupledPairs === 1 ? "" : "s"}`
3030
+ });
3031
+ }
3032
+ const ec = analysis.exportCoverage;
3033
+ if (ec && ec.length > 0) {
3034
+ const totalExports = ec.reduce((sum, e) => sum + e.totalExports, 0);
3035
+ const totalUsed = ec.reduce((sum, e) => sum + e.usedExports, 0);
3036
+ const coveragePct = totalExports > 0 ? Math.round(totalUsed / totalExports * 100) : 100;
3037
+ const unused = totalExports - totalUsed;
3038
+ recapRows.push({ label: "Export coverage", result: `${coveragePct}% (${unused} unused export${unused === 1 ? "" : "s"})` });
3039
+ }
3040
+ if (analysis.communities.length > 0) {
3041
+ recapRows.push({ label: "Community detection", result: `${analysis.communities.length} module cluster${analysis.communities.length === 1 ? "" : "s"}` });
3042
+ }
3043
+ const maxRecapLabel = Math.max(...recapRows.map((r) => r.label.length));
3044
+ for (const row of recapRows) {
3045
+ console.log(
3046
+ pc.dim(` ${row.label.padEnd(maxRecapLabel)} \u2192 ${row.result}`)
3047
+ );
3048
+ }
3049
+ }
3050
+ const exportCoverage = analysis?.exportCoverage;
3051
+ if (exportCoverage && exportCoverage.length > 0) {
3052
+ const totalExports = exportCoverage.reduce((sum, e) => sum + e.totalExports, 0);
3053
+ const totalUsed = exportCoverage.reduce((sum, e) => sum + e.usedExports, 0);
3054
+ const unusedExports = totalExports - totalUsed;
3055
+ const coveragePct = totalExports > 0 ? Math.round(totalUsed / totalExports * 100) : 100;
3056
+ const filesWithUnused = exportCoverage.filter((e) => e.usedExports < e.totalExports).length;
3057
+ if (unusedExports > 0) {
3058
+ console.log("");
3059
+ console.log(
3060
+ pc.dim(
3061
+ ` Export coverage: ${coveragePct}% of exports are used (${unusedExports} unused exports in ${filesWithUnused} file${filesWithUnused === 1 ? "" : "s"})`
3062
+ )
3063
+ );
3064
+ }
3065
+ }
3066
+ console.log("");
3067
+ }
3068
+ function estimateExplorationCost(ctx) {
3069
+ if (ctx.totalSourceBytes === 0 || ctx.sourceFileCount === 0) {
3070
+ return 15e3;
3071
+ }
3072
+ const bytesRead = ctx.totalSourceBytes * 0.4;
3073
+ const readTokens = Math.ceil(bytesRead / 3.2);
3074
+ const overhead = Math.ceil(readTokens * 0.3);
3075
+ return Math.max(5e3, readTokens + overhead);
3076
+ }
3077
+ function formatNumber(n) {
3078
+ if (n >= 1e3) {
3079
+ return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}k`;
3080
+ }
3081
+ return String(n);
3082
+ }
3083
+
3084
+ // src/config.ts
3085
+ init_utils();
3086
+ import { createHash } from "crypto";
3087
+ import path6 from "path";
3088
+ import fg4 from "fast-glob";
3089
+ var CONFIG_FILENAME = ".codebrief.json";
3090
+ var CONFIG_VERSION = 1;
3091
+ async function loadConfig(rootDir) {
3092
+ const configPath = path6.join(rootDir, CONFIG_FILENAME);
3093
+ const raw = await readJsonFile(configPath);
3094
+ if (!raw) return null;
3095
+ const cfg = raw;
3096
+ if (!cfg.ide || !cfg.projectPurpose) return null;
3097
+ return {
3098
+ ide: cfg.ide,
3099
+ projectPurpose: cfg.projectPurpose,
3100
+ keyPatterns: cfg.keyPatterns ?? "",
3101
+ gotchas: cfg.gotchas ?? "",
3102
+ generateSnapshot: cfg.generateSnapshot ?? false,
3103
+ snapshotPaths: cfg.snapshotPaths ?? [],
3104
+ stackCorrections: cfg.stackCorrections ?? "",
3105
+ generatePerPackage: cfg.generatePerPackage ?? false,
3106
+ snapshotHash: cfg.snapshotHash,
3107
+ snapshotGeneratedAt: cfg.snapshotGeneratedAt,
3108
+ language: cfg.language
3109
+ };
3110
+ }
3111
+ async function saveConfig(rootDir, answers, snapshotHash, language) {
3112
+ const configPath = path6.join(rootDir, CONFIG_FILENAME);
3113
+ const cfg = {
3114
+ _version: CONFIG_VERSION,
3115
+ ide: answers.ide,
3116
+ projectPurpose: answers.projectPurpose,
3117
+ keyPatterns: answers.keyPatterns,
3118
+ gotchas: answers.gotchas,
3119
+ generateSnapshot: answers.generateSnapshot,
3120
+ snapshotPaths: answers.snapshotPaths,
3121
+ stackCorrections: answers.stackCorrections,
3122
+ generatePerPackage: answers.generatePerPackage,
3123
+ ...snapshotHash ? { snapshotHash, snapshotGeneratedAt: Date.now() } : {},
3124
+ ...language ? { language } : {}
3125
+ };
3126
+ await writeFileSafe(configPath, JSON.stringify(cfg, null, 2) + "\n");
3127
+ }
3128
+ function configToAnswers(config) {
3129
+ return {
3130
+ ide: config.ide,
3131
+ projectPurpose: config.projectPurpose,
3132
+ keyPatterns: config.keyPatterns,
3133
+ gotchas: config.gotchas,
3134
+ generateSnapshot: config.generateSnapshot,
3135
+ snapshotPaths: config.snapshotPaths,
3136
+ stackConfirmed: true,
3137
+ stackCorrections: config.stackCorrections,
3138
+ generatePerPackage: config.generatePerPackage
3139
+ };
3140
+ }
3141
+ async function computeSnapshotHash(rootDir, language) {
3142
+ const extMap = {
3143
+ typescript: ["**/*.{ts,tsx}"],
3144
+ javascript: ["**/*.{js,jsx,mjs}"],
3145
+ python: ["**/*.py"],
3146
+ go: ["**/*.go"],
3147
+ rust: ["**/*.rs"],
3148
+ java: ["**/*.java"],
3149
+ other: ["**/*.{ts,tsx,js,jsx,py,go,rs}"]
3150
+ };
3151
+ const files = await fg4(extMap[language] ?? extMap.other, {
3152
+ cwd: rootDir,
3153
+ ignore: [
3154
+ "**/node_modules/**",
3155
+ "**/dist/**",
3156
+ "**/build/**",
3157
+ "**/.next/**",
3158
+ "**/target/**",
3159
+ "**/vendor/**"
3160
+ ],
3161
+ stats: true,
3162
+ absolute: false
3163
+ });
3164
+ const entries = files.map((f) => `${f.path}:${f.stats?.mtimeMs ?? 0}`).sort();
3165
+ const hash = createHash("sha256").update(entries.join("\n")).digest("hex").slice(0, 16);
3166
+ return hash;
3167
+ }
3168
+
3169
+ // src/refresh.ts
3170
+ import path7 from "path";
3171
+ import * as p3 from "@clack/prompts";
3172
+ import pc2 from "picocolors";
3173
+ init_utils();
3174
+ var CONTEXT_FILES = [
3175
+ "CLAUDE.md",
3176
+ "AGENTS.md",
3177
+ ".github/copilot-instructions.md",
3178
+ ".windsurfrules",
3179
+ ".clinerules",
3180
+ ".continuerules",
3181
+ "CONTEXT.md"
3182
+ ];
3183
+ var MD_START = /<!-- CODE SNAPSHOT[^>]*-->/;
3184
+ var MD_END = /<!-- \/CODE SNAPSHOT -->/;
3185
+ var AIDER_START = /^# --- Code Snapshot/m;
3186
+ var AIDER_END = /^# --- \/Code Snapshot ---$/m;
3187
+ async function findContextFile(rootDir) {
3188
+ const aiderPath = path7.join(rootDir, ".aider.conf.yml");
3189
+ if (await fileExists(aiderPath)) {
3190
+ const content = await readFileOr(aiderPath);
3191
+ if (content && AIDER_START.test(content)) {
3192
+ return { path: ".aider.conf.yml", isAider: true };
3193
+ }
3194
+ }
3195
+ for (const file of CONTEXT_FILES) {
3196
+ const absPath = path7.join(rootDir, file);
3197
+ if (await fileExists(absPath)) {
3198
+ return { path: file, isAider: false };
3199
+ }
3200
+ }
3201
+ return null;
3202
+ }
3203
+ async function refreshSnapshot(rootDir) {
3204
+ const spinner3 = p3.spinner();
3205
+ const found = await findContextFile(rootDir);
3206
+ if (!found) {
3207
+ p3.log.error(
3208
+ "No context file found. Run " + pc2.cyan("codebrief") + " first to generate one."
3209
+ );
3210
+ process.exit(1);
3211
+ }
3212
+ const absPath = path7.join(rootDir, found.path);
3213
+ const content = await readFileOr(absPath);
3214
+ if (!content) {
3215
+ p3.log.error(`Could not read ${found.path}`);
3216
+ process.exit(1);
3217
+ }
3218
+ if (found.isAider) {
3219
+ if (!AIDER_START.test(content) || !AIDER_END.test(content)) {
3220
+ p3.log.error(
3221
+ `No code snapshot markers found in ${found.path}. Re-generate the file with code snapshot enabled.`
3222
+ );
3223
+ process.exit(1);
3224
+ }
3225
+ } else {
3226
+ if (!MD_START.test(content) || !MD_END.test(content)) {
3227
+ p3.log.error(
3228
+ `No code snapshot markers found in ${found.path}. Re-generate the file with code snapshot enabled.`
3229
+ );
3230
+ process.exit(1);
3231
+ }
3232
+ }
3233
+ p3.log.info(`Refreshing snapshot in ${pc2.cyan(found.path)}`);
3234
+ spinner3.start("Scanning source files...");
3235
+ const progress = (msg) => spinner3.message(msg);
3236
+ const detected = await detectContext(rootDir, progress);
3237
+ const graph = await buildImportGraph(rootDir, detected.language, progress);
3238
+ const config = await loadConfig(rootDir);
3239
+ const snapshotPaths = config?.snapshotPaths ?? [];
3240
+ const snapshot = await generateSnapshot(detected, snapshotPaths, graph, void 0, progress);
3241
+ spinner3.stop(
3242
+ snapshot.entries.length > 0 ? `Found ${snapshot.entries.length} type${snapshot.entries.length === 1 ? "" : "s"}/signature${snapshot.entries.length === 1 ? "" : "s"}.` : "No extractable types found."
3243
+ );
3244
+ if (snapshot.entries.length === 0) {
3245
+ p3.log.warn("No types found \u2014 snapshot section will be empty.");
3246
+ }
3247
+ let updated;
3248
+ if (found.isAider) {
3249
+ const startMatch = content.match(AIDER_START);
3250
+ const endMatch = content.match(AIDER_END);
3251
+ if (!startMatch || !endMatch) {
3252
+ p3.log.error("Failed to parse snapshot markers.");
3253
+ process.exit(1);
3254
+ }
3255
+ const startIdx = content.indexOf(startMatch[0]);
3256
+ const endIdx = content.indexOf(endMatch[0]) + endMatch[0].length;
3257
+ let newBlock = "# --- Code Snapshot (for reference) ---";
3258
+ if (snapshot.markdown) {
3259
+ for (const line of snapshot.markdown.split("\n")) {
3260
+ newBlock += `
3261
+ # ${line}`;
3262
+ }
3263
+ }
3264
+ newBlock += "\n# --- /Code Snapshot ---";
3265
+ updated = content.slice(0, startIdx) + newBlock + content.slice(endIdx);
3266
+ } else {
3267
+ const startMatch = content.match(MD_START);
3268
+ const endMatch = content.match(MD_END);
3269
+ if (!startMatch || !endMatch) {
3270
+ p3.log.error("Failed to parse snapshot markers.");
3271
+ process.exit(1);
3272
+ }
3273
+ const startIdx = content.indexOf(startMatch[0]);
3274
+ const endIdx = content.indexOf(endMatch[0]) + endMatch[0].length;
3275
+ let newBlock = "<!-- CODE SNAPSHOT (auto-generated, update when types/stores/services change) -->";
3276
+ if (snapshot.markdown) {
3277
+ newBlock += "\n\n" + snapshot.markdown + "\n";
3278
+ }
3279
+ newBlock += "\n<!-- /CODE SNAPSHOT -->";
3280
+ updated = content.slice(0, startIdx) + newBlock + content.slice(endIdx);
3281
+ }
3282
+ await writeFileSafe(absPath, updated);
3283
+ p3.log.success(`Updated snapshot in ${pc2.cyan(found.path)}`);
3284
+ }
3285
+
3286
+ // src/git-analysis.ts
3287
+ import { execSync } from "child_process";
3288
+ function analyzeGitActivity(rootDir, onProgress) {
3289
+ try {
3290
+ onProgress?.("Analyzing git history (last 90 days)...");
3291
+ const logOutput = execSync(
3292
+ 'git log --since="90 days ago" --name-only --pretty=format:""',
3293
+ { cwd: rootDir, encoding: "utf-8", timeout: 1e4 }
3294
+ );
3295
+ const commitCounts = /* @__PURE__ */ new Map();
3296
+ for (const line of logOutput.split("\n")) {
3297
+ const file = line.trim();
3298
+ if (file && !file.startsWith("commit ")) {
3299
+ commitCounts.set(file, (commitCounts.get(file) ?? 0) + 1);
3300
+ }
3301
+ }
3302
+ if (commitCounts.size === 0) {
3303
+ return null;
3304
+ }
3305
+ onProgress?.(`Found activity in ${commitCounts.size} files`);
3306
+ const sorted = [...commitCounts.entries()].sort((a, b) => b[1] - a[1]);
3307
+ const hotFiles = [];
3308
+ for (const [filePath, commits] of sorted.slice(0, 15)) {
3309
+ let lastChanged = "";
3310
+ try {
3311
+ lastChanged = execSync(
3312
+ `git log -1 --format="%ar" -- "${filePath}"`,
3313
+ { cwd: rootDir, encoding: "utf-8", timeout: 5e3 }
3314
+ ).trim();
3315
+ } catch {
3316
+ }
3317
+ hotFiles.push({ path: filePath, commits, lastChanged });
3318
+ }
3319
+ onProgress?.("Analyzing change coupling...");
3320
+ const changeCoupling = analyzeChangeCoupling(rootDir);
3321
+ return { commitCounts, hotFiles, changeCoupling };
3322
+ } catch {
3323
+ return null;
3324
+ }
3325
+ }
3326
+ function analyzeChangeCoupling(rootDir) {
3327
+ try {
3328
+ const hashOutput = execSync(
3329
+ 'git log --format="%H" --since="90 days ago"',
3330
+ { cwd: rootDir, encoding: "utf-8", timeout: 1e4 }
3331
+ ).trim();
3332
+ if (!hashOutput) return [];
3333
+ const hashes = hashOutput.split("\n").filter(Boolean);
3334
+ if (hashes.length === 0) return [];
3335
+ const filesPerCommit = [];
3336
+ const commitCountPerFile = /* @__PURE__ */ new Map();
3337
+ for (const hash of hashes) {
3338
+ try {
3339
+ const filesOutput = execSync(
3340
+ `git show --name-only --format="" ${hash}`,
3341
+ { cwd: rootDir, encoding: "utf-8", timeout: 5e3 }
3342
+ ).trim();
3343
+ const files = filesOutput.split("\n").map((f) => f.trim()).filter(Boolean);
3344
+ if (files.length >= 2 && files.length <= 20) {
3345
+ filesPerCommit.push(files);
3346
+ for (const file of files) {
3347
+ commitCountPerFile.set(file, (commitCountPerFile.get(file) ?? 0) + 1);
3348
+ }
3349
+ }
3350
+ } catch {
3351
+ }
3352
+ }
3353
+ const coChanges = /* @__PURE__ */ new Map();
3354
+ for (const files of filesPerCommit) {
3355
+ for (let i = 0; i < files.length; i++) {
3356
+ for (let j = i + 1; j < files.length; j++) {
3357
+ const key = [files[i], files[j]].sort().join("||");
3358
+ coChanges.set(key, (coChanges.get(key) ?? 0) + 1);
3359
+ }
3360
+ }
3361
+ }
3362
+ const results = [];
3363
+ const totalCommits = filesPerCommit.length;
3364
+ for (const [key, coChangeCount] of coChanges) {
3365
+ if (coChangeCount < 3) continue;
3366
+ const [fileA, fileB] = key.split("||");
3367
+ const commitsA = commitCountPerFile.get(fileA) ?? 0;
3368
+ const commitsB = commitCountPerFile.get(fileB) ?? 0;
3369
+ const confidence = coChangeCount / Math.max(commitsA, commitsB);
3370
+ const support = coChangeCount / totalCommits;
3371
+ if (confidence >= 0.5) {
3372
+ results.push({ fileA, fileB, coChangeCount, support, confidence });
3373
+ }
3374
+ }
3375
+ results.sort((a, b) => b.confidence - a.confidence);
3376
+ return results.slice(0, 10);
3377
+ } catch {
3378
+ return [];
3379
+ }
3380
+ }
3381
+
3382
+ // src/index.ts
3383
+ init_utils();
3384
+ async function main() {
3385
+ const startTime = performance.now();
3386
+ const args = process.argv.slice(2);
3387
+ const force = args.includes("--force");
3388
+ const dryRun = args.includes("--dry-run");
3389
+ const refresh = args.includes("--refresh-snapshot");
3390
+ const reconfigure = args.includes("--reconfigure");
3391
+ const check = args.includes("--check");
3392
+ const verbose = args.includes("--verbose") || args.includes("-v");
3393
+ const generateSkills = args.includes("--generate-skills");
3394
+ const maxTokensArg = args.find((a) => a.startsWith("--max-tokens="));
3395
+ const maxTokens = maxTokensArg ? parseInt(maxTokensArg.split("=")[1], 10) : void 0;
3396
+ const targetDir = args.find((a) => !a.startsWith("-") && a !== "-v") ?? process.cwd();
3397
+ const rootDir = path8.resolve(targetDir);
3398
+ const verboseLog = (msg) => {
3399
+ if (verbose) p4.log.info(pc3.dim(msg));
3400
+ };
3401
+ if (check) {
3402
+ const config = await loadConfig(rootDir);
3403
+ if (!config?.snapshotHash) {
3404
+ process.exit(0);
3405
+ }
3406
+ const lang = config.language ?? "other";
3407
+ const currentHash = await computeSnapshotHash(rootDir, lang);
3408
+ if (currentHash !== config.snapshotHash) {
3409
+ const daysSince = config.snapshotGeneratedAt ? Math.floor((Date.now() - config.snapshotGeneratedAt) / (1e3 * 60 * 60 * 24)) : 0;
3410
+ const staleMsg = daysSince > 0 ? ` (last generated ${daysSince}d ago)` : "";
3411
+ console.log(`codebrief: snapshot is stale${staleMsg}. Run npx codebrief --refresh-snapshot`);
3412
+ process.exit(1);
3413
+ }
3414
+ process.exit(0);
3415
+ }
3416
+ console.log("");
3417
+ p4.intro(pc3.bold(" codebrief "));
3418
+ if (refresh) {
3419
+ await refreshSnapshot(rootDir);
3420
+ p4.outro(pc3.green("Snapshot refreshed!"));
3421
+ return;
3422
+ }
3423
+ if (dryRun) {
3424
+ p4.log.warn(pc3.yellow("DRY RUN \u2014 no files will be written"));
3425
+ }
3426
+ p4.log.info(`Analyzing ${pc3.cyan(rootDir)}`);
3427
+ const spinner3 = p4.spinner();
3428
+ const spinnerProgress = (msg) => spinner3.message(msg);
3429
+ spinner3.start("Detecting tech stack...");
3430
+ const detected = await detectContext(rootDir, spinnerProgress);
3431
+ spinner3.stop("Detection complete.");
3432
+ spinner3.start(`Building import graph (${detected.sourceFileCount} files)...`);
3433
+ const graph = await buildImportGraph(rootDir, detected.language, verbose ? verboseLog : spinnerProgress);
3434
+ const topHub = getHubFiles(graph, 1)[0];
3435
+ spinner3.stop(
3436
+ `Import graph: ${graph.edges.length} edges, ${graph.externalImportCounts.size} packages.` + (topHub ? ` Top hub: ${topHub.path}` : "")
3437
+ );
3438
+ detected.frameworks = enrichFrameworksWithUsage(
3439
+ detected.frameworks,
3440
+ graph.externalImportCounts
3441
+ );
3442
+ {
3443
+ const lines = [];
3444
+ const lang = detected.hasTypeScript ? "TypeScript" : detected.language !== "other" ? detected.language.charAt(0).toUpperCase() + detected.language.slice(1) : "";
3445
+ if (lang) lines.push(` Language: ${lang}`);
3446
+ if (detected.frameworks.length > 0) {
3447
+ lines.push(` Frameworks: ${detected.frameworks.map((f) => f.name).join(", ")}`);
3448
+ }
3449
+ if (detected.linter !== "none") {
3450
+ lines.push(` Linter: ${detected.linter.charAt(0).toUpperCase() + detected.linter.slice(1)}`);
3451
+ }
3452
+ if (detected.packageManager !== "none") {
3453
+ lines.push(` Pkg mgr: ${detected.packageManager}`);
3454
+ }
3455
+ if (detected.testFramework) {
3456
+ lines.push(` Testing: ${detected.testFramework}`);
3457
+ }
3458
+ if (detected.ciProvider) {
3459
+ lines.push(` CI: ${detected.ciProvider}`);
3460
+ }
3461
+ if (detected.monorepo) {
3462
+ lines.push(` Monorepo: ${detected.monorepo.type} (${detected.monorepo.packages.length} package${detected.monorepo.packages.length === 1 ? "" : "s"})`);
3463
+ }
3464
+ if (detected.sourceFileCount > 0) {
3465
+ lines.push(` Files: ${detected.sourceFileCount} (${formatBytes(detected.totalSourceBytes)})`);
3466
+ }
3467
+ if (lines.length > 0) {
3468
+ p4.note(lines.join("\n"), "Detected Stack");
3469
+ }
3470
+ }
3471
+ const fileCount = graph.centrality.size;
3472
+ spinner3.start(`Running PageRank on ${fileCount} files...`);
3473
+ const hubFiles = getHubFiles(graph);
3474
+ verboseLog(`PageRank: found ${hubFiles.length} hub files`);
3475
+ spinner3.message("Finding circular dependencies...");
3476
+ const circularDeps = findCircularDeps(graph);
3477
+ verboseLog(`Tarjan SCC: ${circularDeps.length === 0 ? "no cycles found" : `${circularDeps.length} cycle(s)`}`);
3478
+ spinner3.message("Detecting architecture layers...");
3479
+ const { layers, layerEdges } = detectArchitecturalLayers(graph);
3480
+ verboseLog(`Layers: ${layers.map((l) => l.name).join(", ") || "none detected"}`);
3481
+ spinner3.message("Computing instability metrics...");
3482
+ const instabilities = computeInstability(graph);
3483
+ verboseLog(`Instability: ${instabilities.length} high-risk file(s)`);
3484
+ spinner3.message("Detecting module communities...");
3485
+ const communities = detectCommunities(graph);
3486
+ verboseLog(`Communities: ${communities.length} cluster(s)`);
3487
+ spinner3.message("Computing export coverage...");
3488
+ const exportCoverage = computeExportCoverage(graph);
3489
+ verboseLog(`Export coverage: ${exportCoverage.length} files analyzed`);
3490
+ spinner3.message("Analyzing git history...");
3491
+ const gitActivity = detected.isGitRepo ? analyzeGitActivity(rootDir, verbose ? verboseLog : spinnerProgress) : null;
3492
+ if (gitActivity) verboseLog(`Git: ${gitActivity.hotFiles.length} active files, ${gitActivity.changeCoupling.length} coupled pairs`);
3493
+ const analysis = { hubFiles, circularDeps, layers, layerEdges, gitActivity, instabilities, communities, exportCoverage };
3494
+ const analysisParts = [];
3495
+ if (hubFiles.length > 0) analysisParts.push(`${hubFiles.length} hub files`);
3496
+ if (layers.length > 0) analysisParts.push(`${layers.length} layers`);
3497
+ if (circularDeps.length > 0) analysisParts.push(`${circularDeps.length} circular dep${circularDeps.length === 1 ? "" : "s"}`);
3498
+ if (communities.length > 0) analysisParts.push(`${communities.length} module cluster${communities.length === 1 ? "" : "s"}`);
3499
+ if (gitActivity) analysisParts.push(`${gitActivity.hotFiles.length} active files`);
3500
+ spinner3.stop(
3501
+ analysisParts.length > 0 ? `Analysis: ${analysisParts.join(", ")}.` : "Analysis complete."
3502
+ );
3503
+ const savedConfig = await loadConfig(rootDir);
3504
+ if (savedConfig?.snapshotHash) {
3505
+ const currentHash = await computeSnapshotHash(rootDir, detected.language);
3506
+ if (currentHash !== savedConfig.snapshotHash && savedConfig.snapshotGeneratedAt) {
3507
+ const daysSince = Math.floor(
3508
+ (Date.now() - savedConfig.snapshotGeneratedAt) / (1e3 * 60 * 60 * 24)
3509
+ );
3510
+ p4.log.warn(
3511
+ pc3.yellow(
3512
+ `Code snapshot may be stale (source files changed${daysSince > 0 ? `, last generated ${daysSince}d ago` : ""}). Run with ${pc3.bold("--refresh-snapshot")} to update.`
3513
+ )
3514
+ );
3515
+ }
3516
+ }
3517
+ let answers;
3518
+ if (savedConfig && !reconfigure) {
3519
+ p4.log.info(
3520
+ `Using saved config from ${pc3.cyan(".codebrief.json")} ` + pc3.dim("(run with --reconfigure to change)")
3521
+ );
3522
+ answers = configToAnswers(savedConfig);
3523
+ if (detected.monorepo && detected.monorepo.packages.length > 0 && !savedConfig.generatePerPackage) {
3524
+ }
3525
+ } else {
3526
+ answers = await runPrompts(detected, reconfigure ? savedConfig : null);
3527
+ if (!dryRun) {
3528
+ const hash = await computeSnapshotHash(rootDir, detected.language);
3529
+ await saveConfig(rootDir, answers, hash, detected.language);
3530
+ p4.log.info(
3531
+ pc3.dim("Saved config to .codebrief.json for future runs.")
3532
+ );
3533
+ }
3534
+ }
3535
+ let snapshot = null;
3536
+ if (answers.generateSnapshot) {
3537
+ spinner3.start("Scanning source files for code snapshot...");
3538
+ snapshot = await generateSnapshot(detected, answers.snapshotPaths, graph, maxTokens, verbose ? verboseLog : spinnerProgress, gitActivity);
3539
+ const count = snapshot.entries.length;
3540
+ const budgetNote = snapshot.budgetExcluded ? ` (${snapshot.budgetExcluded} excluded by token budget)` : "";
3541
+ spinner3.stop(
3542
+ count > 0 ? `Found ${count} type${count === 1 ? "" : "s"}/signature${count === 1 ? "" : "s"}.${budgetNote}` : "No extractable types found (snapshot will be skipped)."
3543
+ );
3544
+ if (count === 0) {
3545
+ snapshot = null;
3546
+ }
3547
+ }
3548
+ spinner3.start(
3549
+ dryRun ? "Preparing context files..." : "Generating context files..."
3550
+ );
3551
+ const shouldGenerateSkills = generateSkills || answers.ide === "claude";
3552
+ const files = await generateFiles(detected, answers, snapshot, force, dryRun, analysis, shouldGenerateSkills, verbose ? verboseLog : void 0);
3553
+ spinner3.stop(
3554
+ dryRun ? `Would generate ${files.length} file${files.length === 1 ? "" : "s"}.` : `Generated ${files.length} file${files.length === 1 ? "" : "s"}.`
3555
+ );
3556
+ if (files.length === 0) {
3557
+ p4.outro("Nothing to write. Done!");
3558
+ return;
3559
+ }
3560
+ printSummary(files, detected, snapshot, analysis);
3561
+ const elapsed = ((performance.now() - startTime) / 1e3).toFixed(1);
3562
+ if (dryRun) {
3563
+ p4.outro(
3564
+ pc3.yellow("DRY RUN complete \u2014 ") + pc3.dim(`no files were written. Remove --dry-run to generate. (${elapsed}s)`)
3565
+ );
3566
+ return;
3567
+ }
3568
+ p4.outro(
3569
+ pc3.green(`Done in ${elapsed}s! `) + pc3.dim(
3570
+ "Your context files are ready. They are living documents \u2014 keep them up to date as your project evolves."
3571
+ )
3572
+ );
3573
+ }
3574
+ main().catch((err) => {
3575
+ console.error(pc3.red("Fatal error:"), err);
3576
+ process.exit(1);
3577
+ });
3578
+ //# sourceMappingURL=index.js.map