codebrief 1.2.0 → 1.4.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 CHANGED
@@ -79,10 +79,81 @@ var init_utils = __esm({
79
79
  });
80
80
 
81
81
  // src/index.ts
82
- init_utils();
83
82
  import path8 from "path";
84
83
  import * as p4 from "@clack/prompts";
85
- import pc3 from "picocolors";
84
+
85
+ // src/theme.ts
86
+ import pc from "picocolors";
87
+ var isTTY = !!process.stdout.isTTY;
88
+ var noColor = !!process.env.NO_COLOR;
89
+ var trueColor = !noColor && isTTY && (process.env.COLORTERM === "truecolor" || process.env.COLORTERM === "24bit" || (process.env.TERM ?? "").includes("256color"));
90
+ function rgb(r, g, b) {
91
+ if (noColor || !isTTY) return (t) => t;
92
+ if (!trueColor) {
93
+ return (t) => t;
94
+ }
95
+ const open = `\x1B[38;2;${r};${g};${b}m`;
96
+ const close = "\x1B[39m";
97
+ return (text2) => `${open}${text2}${close}`;
98
+ }
99
+ var palette = {
100
+ brand: rgb(122, 162, 247),
101
+ // #7aa2f7
102
+ accent: rgb(137, 180, 250),
103
+ // #89b4fa
104
+ muted: rgb(84, 92, 126),
105
+ // #545c7e
106
+ success: rgb(125, 207, 255),
107
+ // #7dcfff
108
+ warn: rgb(178, 174, 166),
109
+ // #b2aea6 (cool stone, no orange)
110
+ error: rgb(219, 75, 75)
111
+ // #db4b4b
112
+ };
113
+ var fallback = {
114
+ brand: pc.blue,
115
+ accent: pc.cyan,
116
+ muted: pc.dim,
117
+ success: pc.green,
118
+ warn: pc.yellow,
119
+ error: pc.red
120
+ };
121
+ function pick(key) {
122
+ if (noColor || !isTTY) return (t) => t;
123
+ return trueColor ? palette[key] : fallback[key];
124
+ }
125
+ function gradient(text2, from, to, fallbackFn) {
126
+ if (noColor || !isTTY) return text2;
127
+ if (!trueColor) return fallbackFn ? fallbackFn(text2) : text2;
128
+ const len = text2.length;
129
+ if (len === 0) return text2;
130
+ if (len === 1) return `\x1B[38;2;${from[0]};${from[1]};${from[2]}m${text2}\x1B[39m`;
131
+ let result = "";
132
+ for (let i = 0; i < len; i++) {
133
+ const ratio = i / (len - 1);
134
+ const r = Math.round(from[0] + (to[0] - from[0]) * ratio);
135
+ const g = Math.round(from[1] + (to[1] - from[1]) * ratio);
136
+ const b = Math.round(from[2] + (to[2] - from[2]) * ratio);
137
+ result += `\x1B[38;2;${r};${g};${b}m${text2[i]}`;
138
+ }
139
+ return result + "\x1B[39m";
140
+ }
141
+ var theme = {
142
+ brand: (text2) => pick("brand")(text2),
143
+ accent: (text2) => pick("accent")(text2),
144
+ muted: (text2) => pick("muted")(text2),
145
+ success: (text2) => pick("success")(text2),
146
+ warn: (text2) => pick("warn")(text2),
147
+ error: (text2) => pick("error")(text2),
148
+ bold: (text2) => noColor || !isTTY ? text2 : pc.bold(text2),
149
+ brandBold: (text2) => pick("brand")(noColor || !isTTY ? text2 : pc.bold(text2)),
150
+ accentBold: (text2) => pick("accent")(noColor || !isTTY ? text2 : pc.bold(text2)),
151
+ /** Styled checkmark */
152
+ check: () => pick("success")("\u2713")
153
+ };
154
+
155
+ // src/index.ts
156
+ init_utils();
86
157
 
87
158
  // src/detect.ts
88
159
  init_utils();
@@ -505,7 +576,8 @@ function summarizeDetection(ctx) {
505
576
 
506
577
  // src/prompts.ts
507
578
  import * as p from "@clack/prompts";
508
- async function runPrompts(detected, defaults) {
579
+ var SNAPSHOT_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "javascript", "python"]);
580
+ async function runPrompts(detected, defaults, isReconfigure = false) {
509
581
  const ideOptions = [
510
582
  { value: "claude", label: "Claude Code" },
511
583
  { value: "cursor", label: "Cursor" },
@@ -517,38 +589,41 @@ async function runPrompts(detected, defaults) {
517
589
  { value: "aider", label: "Aider" },
518
590
  { value: "generic", label: "Other (generic CONTEXT.md)" }
519
591
  ];
520
- const ide = await p.select({
521
- message: "Which AI coding tool are you using?",
592
+ const ides = await p.multiselect({
593
+ message: "Which AI coding tools do you use? (select all that apply)",
522
594
  options: ideOptions,
523
- initialValue: defaults?.ide
595
+ initialValues: defaults?.ides ?? (defaults?.ide ? [defaults.ide] : void 0),
596
+ required: true
524
597
  });
525
- if (p.isCancel(ide)) {
598
+ if (p.isCancel(ides)) {
526
599
  p.cancel("Cancelled.");
527
600
  process.exit(0);
528
601
  }
529
- const stackSummary = summarizeDetection(detected);
530
602
  let stackConfirmed = true;
531
603
  let stackCorrections = defaults?.stackCorrections ?? "";
532
- if (stackSummary) {
533
- const confirm3 = await p.confirm({
534
- message: `Detected: ${stackSummary}. Correct?`
535
- });
536
- if (p.isCancel(confirm3)) {
537
- p.cancel("Cancelled.");
538
- process.exit(0);
539
- }
540
- stackConfirmed = confirm3;
541
- if (!confirm3) {
542
- const corrections = await p.text({
543
- message: "What should I correct? (describe your actual stack)",
544
- placeholder: "e.g. It's actually Next.js 15 + Prisma, not plain React",
545
- defaultValue: defaults?.stackCorrections || void 0
604
+ if (isReconfigure) {
605
+ const stackSummary = summarizeDetection(detected);
606
+ if (stackSummary) {
607
+ const confirm3 = await p.confirm({
608
+ message: `Detected: ${stackSummary}. Correct?`
546
609
  });
547
- if (p.isCancel(corrections)) {
610
+ if (p.isCancel(confirm3)) {
548
611
  p.cancel("Cancelled.");
549
612
  process.exit(0);
550
613
  }
551
- stackCorrections = corrections;
614
+ stackConfirmed = confirm3;
615
+ if (!confirm3) {
616
+ const corrections = await p.text({
617
+ message: "What should I correct? (describe your actual stack)",
618
+ placeholder: "e.g. It's actually Next.js 15 + Prisma, not plain React",
619
+ defaultValue: defaults?.stackCorrections || void 0
620
+ });
621
+ if (p.isCancel(corrections)) {
622
+ p.cancel("Cancelled.");
623
+ process.exit(0);
624
+ }
625
+ stackCorrections = corrections;
626
+ }
552
627
  }
553
628
  }
554
629
  const projectPurpose = await p.text({
@@ -563,55 +638,57 @@ async function runPrompts(detected, defaults) {
563
638
  p.cancel("Cancelled.");
564
639
  process.exit(0);
565
640
  }
641
+ let patternsDefault = defaults?.keyPatterns || "";
642
+ if (defaults?.gotchas) {
643
+ patternsDefault = patternsDefault ? `${patternsDefault}
644
+ Gotchas: ${defaults.gotchas}` : `Gotchas: ${defaults.gotchas}`;
645
+ }
566
646
  const keyPatterns = await p.text({
567
- message: "Any key coding patterns or conventions? (optional, press Enter to skip)",
568
- placeholder: "e.g. Zustand slices for state, NativeWind for styling, expo/fetch for SSE",
569
- defaultValue: defaults?.keyPatterns || ""
647
+ message: "Any key patterns, conventions, or gotchas? (optional, press Enter to skip)",
648
+ placeholder: "e.g. Zustand for state, never use FadeIn on ternary, angular commit style",
649
+ defaultValue: patternsDefault
570
650
  });
571
651
  if (p.isCancel(keyPatterns)) {
572
652
  p.cancel("Cancelled.");
573
653
  process.exit(0);
574
654
  }
575
- const gotchas = await p.text({
576
- message: "Any critical gotchas or anti-patterns to avoid? (optional)",
577
- placeholder: "e.g. Never use FadeIn/FadeOut on ternary components, no @expo/vector-icons",
578
- defaultValue: defaults?.gotchas || ""
579
- });
580
- if (p.isCancel(gotchas)) {
581
- p.cancel("Cancelled.");
582
- process.exit(0);
583
- }
584
655
  let generateSnapshot2 = false;
585
656
  let snapshotPaths = defaults?.snapshotPaths ?? [];
586
- if (detected.language === "typescript" || detected.language === "javascript") {
587
- const snapshotChoice = await p.select({
588
- message: "Generate a code snapshot? (extracts types, store shapes, component props)",
589
- options: [
590
- { value: "auto", label: "Yes, auto-detect key files" },
591
- { value: "no", label: "No, skip code snapshot" },
592
- { value: "custom", label: "Yes, but let me specify paths" }
593
- ],
594
- initialValue: defaults?.generateSnapshot ? defaults.snapshotPaths.length > 0 ? "custom" : "auto" : void 0
595
- });
596
- if (p.isCancel(snapshotChoice)) {
597
- p.cancel("Cancelled.");
598
- process.exit(0);
599
- }
600
- if (snapshotChoice === "auto") {
601
- generateSnapshot2 = true;
602
- snapshotPaths = [];
603
- } else if (snapshotChoice === "custom") {
604
- generateSnapshot2 = true;
605
- const paths = await p.text({
606
- message: "Paths to scan (comma-separated, relative to project root)",
607
- placeholder: "e.g. src/types, src/stores, src/components",
608
- defaultValue: defaults?.snapshotPaths.length ? defaults.snapshotPaths.join(", ") : void 0
657
+ const supportsSnapshot = SNAPSHOT_LANGUAGES.has(detected.language);
658
+ if (supportsSnapshot) {
659
+ if (isReconfigure) {
660
+ const snapshotChoice = await p.select({
661
+ message: "Code snapshot (extracts types, function signatures, class definitions)",
662
+ options: [
663
+ { value: "auto", label: "Auto-detect key files" },
664
+ { value: "custom", label: "Custom paths" },
665
+ { value: "no", label: "Disable" }
666
+ ],
667
+ initialValue: defaults?.generateSnapshot ? defaults.snapshotPaths.length > 0 ? "custom" : "auto" : "auto"
609
668
  });
610
- if (p.isCancel(paths)) {
669
+ if (p.isCancel(snapshotChoice)) {
611
670
  p.cancel("Cancelled.");
612
671
  process.exit(0);
613
672
  }
614
- snapshotPaths = paths.split(",").map((s) => s.trim()).filter(Boolean);
673
+ if (snapshotChoice === "auto") {
674
+ generateSnapshot2 = true;
675
+ snapshotPaths = [];
676
+ } else if (snapshotChoice === "custom") {
677
+ generateSnapshot2 = true;
678
+ const paths = await p.text({
679
+ message: "Paths to scan (comma-separated, relative to project root)",
680
+ placeholder: "e.g. src/types, src/stores, src/components",
681
+ defaultValue: defaults?.snapshotPaths.length ? defaults.snapshotPaths.join(", ") : void 0
682
+ });
683
+ if (p.isCancel(paths)) {
684
+ p.cancel("Cancelled.");
685
+ process.exit(0);
686
+ }
687
+ snapshotPaths = paths.split(",").map((s) => s.trim()).filter(Boolean);
688
+ }
689
+ } else {
690
+ generateSnapshot2 = true;
691
+ snapshotPaths = defaults?.snapshotPaths ?? [];
615
692
  }
616
693
  }
617
694
  let generatePerPackage = false;
@@ -629,10 +706,11 @@ async function runPrompts(detected, defaults) {
629
706
  generatePerPackage = perPkg;
630
707
  }
631
708
  return {
632
- ide,
709
+ ides,
633
710
  projectPurpose,
634
711
  keyPatterns: keyPatterns ?? "",
635
- gotchas: gotchas ?? "",
712
+ gotchas: "",
713
+ // Folded into keyPatterns; kept for backward compat
636
714
  generateSnapshot: generateSnapshot2,
637
715
  snapshotPaths,
638
716
  stackConfirmed,
@@ -886,7 +964,7 @@ async function buildImportGraph(rootDir, language, onProgress) {
886
964
  } catch (err) {
887
965
  const code = err.code;
888
966
  if (code === "EPERM" || code === "EACCES") {
889
- onProgress?.("Warning: permission error scanning files \u2014 returning empty graph");
967
+ onProgress?.("Warning: permission error scanning files, returning empty graph");
890
968
  return { edges: [], inDegree: /* @__PURE__ */ new Map(), centrality: /* @__PURE__ */ new Map(), externalImportCounts: /* @__PURE__ */ new Map() };
891
969
  }
892
970
  throw err;
@@ -1233,6 +1311,12 @@ function computeExportCoverage(graph) {
1233
1311
 
1234
1312
  // src/snapshot.ts
1235
1313
  function getDefaultScanPaths(ctx) {
1314
+ if (ctx.language === "python") {
1315
+ return getDefaultPythonScanPaths(ctx);
1316
+ }
1317
+ return getDefaultJsTsScanPaths(ctx);
1318
+ }
1319
+ function getDefaultJsTsScanPaths(ctx) {
1236
1320
  const paths = [];
1237
1321
  const dirs = ctx.directories;
1238
1322
  for (const d of dirs) {
@@ -1258,6 +1342,32 @@ function getDefaultScanPaths(ctx) {
1258
1342
  }
1259
1343
  return paths;
1260
1344
  }
1345
+ function getDefaultPythonScanPaths(ctx) {
1346
+ const paths = [];
1347
+ const dirs = ctx.directories;
1348
+ for (const d of dirs) {
1349
+ const last = d.split("/").pop() ?? d;
1350
+ if ([
1351
+ "models",
1352
+ "schemas",
1353
+ "types",
1354
+ "services",
1355
+ "api",
1356
+ "core",
1357
+ "utils",
1358
+ "db",
1359
+ "routes",
1360
+ "routers",
1361
+ "views"
1362
+ ].includes(last)) {
1363
+ paths.push(d);
1364
+ }
1365
+ }
1366
+ if (paths.length === 0) {
1367
+ paths.push("src", "app", "lib", ".");
1368
+ }
1369
+ return paths;
1370
+ }
1261
1371
  var PATTERNS = {
1262
1372
  /** export interface Foo { ... } or export type Foo = ... */
1263
1373
  exportedType: /^export\s+(interface|type)\s+(\w+)/,
@@ -1352,23 +1462,160 @@ function extractSignatureLine(lines, startIdx) {
1352
1462
  }
1353
1463
  return sig;
1354
1464
  }
1355
- function annotateSignature(entry) {
1465
+ var PY_PATTERNS = {
1466
+ /** class Foo: or class Foo(Base): or class Foo(Base, Mixin): */
1467
+ classDef: /^class\s+(\w+)(?:\(([^)]*)\))?:/,
1468
+ /** Decorators (@dataclass, @app.route, etc.) */
1469
+ decorator: /^@(\S+)/,
1470
+ /** def foo(...) -> RetType: or async def foo(...): */
1471
+ funcDef: /^(async\s+)?def\s+(\w+)\s*\(/,
1472
+ /** TypeAlias: Foo = NewType/Union/Optional/Callable/Literal/TypeVar */
1473
+ typeAlias: /^(\w+)\s*(?::\s*TypeAlias\s*)?=\s*(?:NewType|Union|Optional|Callable|Literal|TypeVar|Annotated)\b/
1474
+ };
1475
+ var PY_TYPE_BASES = /* @__PURE__ */ new Set([
1476
+ "BaseModel",
1477
+ "TypedDict",
1478
+ "NamedTuple",
1479
+ "Protocol"
1480
+ ]);
1481
+ var PY_DATACLASS_DECORATORS = /* @__PURE__ */ new Set([
1482
+ "dataclass",
1483
+ "dataclasses.dataclass",
1484
+ "attrs",
1485
+ "attr.s",
1486
+ "define"
1487
+ ]);
1488
+ async function extractFromPythonFile(filePath, relPath) {
1489
+ const content = await readFileOr(filePath);
1490
+ if (!content) return [];
1491
+ const entries = [];
1492
+ const lines = content.split("\n");
1493
+ let pendingDecorators = [];
1494
+ for (let i = 0; i < lines.length; i++) {
1495
+ const line = lines[i];
1496
+ const trimmed = line.trimStart();
1497
+ const indent = line.length - trimmed.length;
1498
+ if (indent > 0 && !pendingDecorators.length) {
1499
+ if (indent > 4) continue;
1500
+ }
1501
+ const decoMatch = trimmed.match(PY_PATTERNS.decorator);
1502
+ if (decoMatch) {
1503
+ pendingDecorators.push(decoMatch[1]);
1504
+ continue;
1505
+ }
1506
+ if (indent === 0) {
1507
+ const classMatch = trimmed.match(PY_PATTERNS.classDef);
1508
+ if (classMatch) {
1509
+ const [, name, bases] = classMatch;
1510
+ const baseList = bases ? bases.split(",").map((b) => b.trim().split("[")[0].split("(")[0]) : [];
1511
+ let category = "type";
1512
+ const isEnum = baseList.some((b) => b === "Enum" || b === "IntEnum" || b === "StrEnum");
1513
+ const isProtocol = baseList.some((b) => b === "Protocol");
1514
+ const isDatalike = baseList.some((b) => PY_TYPE_BASES.has(b)) || pendingDecorators.some((d) => PY_DATACLASS_DECORATORS.has(d));
1515
+ if (isProtocol) {
1516
+ category = "interface";
1517
+ } else if (isEnum || isDatalike) {
1518
+ category = "type";
1519
+ }
1520
+ const block = extractPythonBlock(lines, i, pendingDecorators);
1521
+ entries.push({ file: relPath, category, signature: block });
1522
+ pendingDecorators = [];
1523
+ continue;
1524
+ }
1525
+ }
1526
+ if (indent === 0) {
1527
+ const funcMatch = trimmed.match(PY_PATTERNS.funcDef);
1528
+ if (funcMatch) {
1529
+ const [, , name] = funcMatch;
1530
+ if (name.startsWith("_") || name.startsWith("test_")) {
1531
+ pendingDecorators = [];
1532
+ continue;
1533
+ }
1534
+ const sig = extractPythonFuncSignature(lines, i, pendingDecorators);
1535
+ entries.push({ file: relPath, category: "function", signature: sig });
1536
+ pendingDecorators = [];
1537
+ continue;
1538
+ }
1539
+ }
1540
+ if (indent === 0) {
1541
+ const aliasMatch = trimmed.match(PY_PATTERNS.typeAlias);
1542
+ if (aliasMatch) {
1543
+ entries.push({ file: relPath, category: "type", signature: trimmed });
1544
+ pendingDecorators = [];
1545
+ continue;
1546
+ }
1547
+ }
1548
+ if (trimmed && !trimmed.startsWith("#")) {
1549
+ pendingDecorators = [];
1550
+ }
1551
+ }
1552
+ return entries;
1553
+ }
1554
+ function extractPythonBlock(lines, startIdx, decorators) {
1555
+ const maxLines = 30;
1556
+ const parts = [];
1557
+ for (const dec of decorators) {
1558
+ parts.push(`@${dec}`);
1559
+ }
1560
+ parts.push(lines[startIdx].trimStart());
1561
+ let bodyIndent = -1;
1562
+ for (let i = startIdx + 1; i < lines.length && i < startIdx + maxLines; i++) {
1563
+ const line = lines[i];
1564
+ const trimmed = line.trimStart();
1565
+ if (!trimmed || trimmed.startsWith("#")) continue;
1566
+ bodyIndent = line.length - trimmed.length;
1567
+ break;
1568
+ }
1569
+ if (bodyIndent <= 0) return parts.join("\n");
1570
+ for (let i = startIdx + 1; i < lines.length && parts.length < maxLines; i++) {
1571
+ const line = lines[i];
1572
+ const trimmed = line.trimStart();
1573
+ if (!trimmed) {
1574
+ parts.push("");
1575
+ continue;
1576
+ }
1577
+ const currentIndent = line.length - trimmed.length;
1578
+ if (currentIndent < bodyIndent) break;
1579
+ parts.push(line.trimStart());
1580
+ }
1581
+ while (parts.length > 0 && parts[parts.length - 1] === "") {
1582
+ parts.pop();
1583
+ }
1584
+ return parts.join("\n");
1585
+ }
1586
+ function extractPythonFuncSignature(lines, startIdx, decorators) {
1587
+ const parts = [];
1588
+ for (const dec of decorators) {
1589
+ parts.push(`@${dec}`);
1590
+ }
1591
+ let sig = "";
1592
+ for (let i = startIdx; i < lines.length && i < startIdx + 10; i++) {
1593
+ const trimmed = lines[i].trimStart();
1594
+ sig += (sig ? " " : "") + trimmed;
1595
+ if (sig.includes("):") || sig.includes(") ->")) {
1596
+ const colonIdx = sig.lastIndexOf(":");
1597
+ if (colonIdx >= 0) {
1598
+ sig = sig.slice(0, colonIdx + 1);
1599
+ }
1600
+ break;
1601
+ }
1602
+ }
1603
+ parts.push(sig);
1604
+ return parts.join("\n");
1605
+ }
1606
+ function annotateSignature(entry, commentPrefix = "//") {
1356
1607
  if (entry.importedByCount && entry.importedByCount > 2) {
1357
1608
  const firstLine = entry.signature.split("\n")[0];
1358
1609
  const rest = entry.signature.split("\n").slice(1);
1359
- const annotated = `${firstLine} // imported by ${entry.importedByCount} files`;
1610
+ const annotated = `${firstLine} ${commentPrefix} imported by ${entry.importedByCount} files`;
1360
1611
  return rest.length > 0 ? [annotated, ...rest].join("\n") : annotated;
1361
1612
  }
1362
1613
  return entry.signature;
1363
1614
  }
1364
- function renderSnapshot(entries) {
1615
+ function renderSnapshot(entries, language = "typescript") {
1365
1616
  if (entries.length === 0) return "";
1366
- const byFile = /* @__PURE__ */ new Map();
1367
- for (const e of entries) {
1368
- const list = byFile.get(e.file) ?? [];
1369
- list.push(e);
1370
- byFile.set(e.file, list);
1371
- }
1617
+ const lang = language === "python" ? "python" : "ts";
1618
+ const comment = language === "python" ? "#" : "//";
1372
1619
  let md = "";
1373
1620
  const types = entries.filter((e) => e.category === "type" || e.category === "interface");
1374
1621
  const stores = entries.filter((e) => e.category === "store");
@@ -1376,28 +1623,43 @@ function renderSnapshot(entries) {
1376
1623
  const components = entries.filter((e) => e.category === "component");
1377
1624
  const functions = entries.filter((e) => e.category === "function");
1378
1625
  if (types.length > 0) {
1379
- md += "### Core Types\n\n```ts\n";
1380
- md += types.map((e) => annotateSignature(e)).join("\n\n");
1626
+ md += `### Core Types
1627
+
1628
+ \`\`\`${lang}
1629
+ `;
1630
+ md += types.map((e) => annotateSignature(e, comment)).join("\n\n");
1381
1631
  md += "\n```\n\n";
1382
1632
  }
1383
1633
  if (stores.length > 0) {
1384
- md += "### Store Shape\n\n```ts\n";
1385
- md += stores.map((e) => annotateSignature(e)).join("\n\n");
1634
+ md += `### Store Shape
1635
+
1636
+ \`\`\`${lang}
1637
+ `;
1638
+ md += stores.map((e) => annotateSignature(e, comment)).join("\n\n");
1386
1639
  md += "\n```\n\n";
1387
1640
  }
1388
1641
  if (components.length > 0) {
1389
- md += "### Component Props\n\n```ts\n";
1390
- md += components.map((e) => annotateSignature(e)).join("\n\n");
1642
+ md += `### Component Props
1643
+
1644
+ \`\`\`${lang}
1645
+ `;
1646
+ md += components.map((e) => annotateSignature(e, comment)).join("\n\n");
1391
1647
  md += "\n```\n\n";
1392
1648
  }
1393
1649
  if (hooks.length > 0) {
1394
- md += "### Hooks\n\n```ts\n";
1395
- md += hooks.map((e) => annotateSignature(e)).join("\n\n");
1650
+ md += `### Hooks
1651
+
1652
+ \`\`\`${lang}
1653
+ `;
1654
+ md += hooks.map((e) => annotateSignature(e, comment)).join("\n\n");
1396
1655
  md += "\n```\n\n";
1397
1656
  }
1398
1657
  if (functions.length > 0) {
1399
- md += "### Key Functions\n\n```ts\n";
1400
- md += functions.map((e) => annotateSignature(e)).join("\n\n");
1658
+ md += `### Key Functions
1659
+
1660
+ \`\`\`${lang}
1661
+ `;
1662
+ md += functions.map((e) => annotateSignature(e, comment)).join("\n\n");
1401
1663
  md += "\n```\n\n";
1402
1664
  }
1403
1665
  return md.trimEnd();
@@ -1409,23 +1671,40 @@ async function generateSnapshot(ctx, customPaths, graph, maxTokens, onProgress,
1409
1671
  }
1410
1672
  const dirNames = scanPaths.map((p5) => p5.split("/").pop() ?? p5);
1411
1673
  onProgress?.(`Scanning ${scanPaths.length} directories: ${dirNames.join(", ")}...`);
1412
- const patterns = scanPaths.map((p5) => `${p5}/**/*.{ts,tsx,js,jsx}`);
1674
+ const isPython = ctx.language === "python";
1675
+ const fileGlob = isPython ? "**/*.py" : "**/*.{ts,tsx,js,jsx}";
1676
+ const patterns = scanPaths.map((p5) => `${p5}/${fileGlob}`);
1677
+ const ignorePatterns = [
1678
+ "**/node_modules/**",
1679
+ "**/dist/**",
1680
+ "**/build/**",
1681
+ "**/*.test.*",
1682
+ "**/*.spec.*",
1683
+ "**/__tests__/**",
1684
+ "**/.Trash/**",
1685
+ "**/Library/**",
1686
+ "**/.git/**"
1687
+ ];
1688
+ if (isPython) {
1689
+ ignorePatterns.push(
1690
+ "**/__pycache__/**",
1691
+ "**/venv/**",
1692
+ "**/.venv/**",
1693
+ "**/env/**",
1694
+ "**/migrations/**",
1695
+ "**/test_*.py",
1696
+ "**/tests/**",
1697
+ "**/conftest.py",
1698
+ "**/setup.py"
1699
+ );
1700
+ }
1413
1701
  const files = await fg3(patterns, {
1414
1702
  cwd: ctx.rootDir,
1415
- ignore: [
1416
- "**/node_modules/**",
1417
- "**/dist/**",
1418
- "**/build/**",
1419
- "**/*.test.*",
1420
- "**/*.spec.*",
1421
- "**/__tests__/**",
1422
- "**/.Trash/**",
1423
- "**/Library/**",
1424
- "**/.git/**"
1425
- ],
1703
+ ignore: ignorePatterns,
1426
1704
  absolute: false
1427
1705
  });
1428
1706
  const allEntries = [];
1707
+ const extractor = isPython ? extractFromPythonFile : extractFromFile;
1429
1708
  for (let i = 0; i < files.length; i++) {
1430
1709
  const file = files[i];
1431
1710
  if ((i + 1) % 20 === 0 || i === files.length - 1) {
@@ -1433,7 +1712,7 @@ async function generateSnapshot(ctx, customPaths, graph, maxTokens, onProgress,
1433
1712
  onProgress?.(`Extracting signatures... ${i + 1}/${files.length} files (${dir}/)`);
1434
1713
  }
1435
1714
  const absPath = path4.join(ctx.rootDir, file);
1436
- const entries = await extractFromFile(absPath, file);
1715
+ const entries = await extractor(absPath, file);
1437
1716
  allEntries.push(...entries);
1438
1717
  }
1439
1718
  if (graph) {
@@ -1449,7 +1728,7 @@ async function generateSnapshot(ctx, customPaths, graph, maxTokens, onProgress,
1449
1728
  const budget = maxTokens ?? Math.min(16e3, 4e3 + Math.floor(ctx.sourceFileCount / 25) * 500);
1450
1729
  onProgress?.(`Applying token budget (${budget.toLocaleString()} tokens)...`);
1451
1730
  const { selected, excluded } = applyTokenBudget(liveEntries, budget, graph, gitActivity);
1452
- const markdown = renderSnapshot(selected);
1731
+ const markdown = renderSnapshot(selected, ctx.language);
1453
1732
  return {
1454
1733
  entries: selected,
1455
1734
  markdown,
@@ -1464,13 +1743,23 @@ var ENTRY_POINT_PATTERNS = [
1464
1743
  /(?:^|\/)pages\//,
1465
1744
  /(?:^|\/)app\//,
1466
1745
  /(?:^|\/)routes?\//,
1467
- /(?:^|\/)middleware\//
1746
+ /(?:^|\/)middleware\//,
1747
+ // Python entry points
1748
+ /(?:^|\/)__init__\.py$/,
1749
+ /(?:^|\/)main\.py$/,
1750
+ /(?:^|\/)app\.py$/,
1751
+ /(?:^|\/)wsgi\.py$/,
1752
+ /(?:^|\/)asgi\.py$/
1468
1753
  ];
1469
1754
  function extractNameFromSignature(sig) {
1470
- const m = sig.match(
1755
+ const jsMatch = sig.match(
1471
1756
  /export\s+(?:default\s+)?(?:async\s+)?(?:interface|type|function|const|let|var|class|enum)\s+(\w+)/
1472
1757
  );
1473
- return m?.[1] ?? null;
1758
+ if (jsMatch) return jsMatch[1];
1759
+ const pyMatch = sig.match(
1760
+ /(?:class|(?:async\s+)?def)\s+(\w+)/
1761
+ );
1762
+ return pyMatch?.[1] ?? null;
1474
1763
  }
1475
1764
  function isEntryPoint(filePath) {
1476
1765
  return ENTRY_POINT_PATTERNS.some((p5) => p5.test(filePath));
@@ -1557,7 +1846,7 @@ var HINT_GENERATORS = [
1557
1846
  hints.push("### Next.js (App Router)");
1558
1847
  hints.push("");
1559
1848
  hints.push(
1560
- "- **App Router** \u2014 all routes in `app/` use React Server Components by default"
1849
+ "- **App Router**: all routes in `app/` use React Server Components by default"
1561
1850
  );
1562
1851
  hints.push(
1563
1852
  '- Add `"use client"` directive at the top of files that need browser APIs, hooks, or event handlers'
@@ -1580,7 +1869,7 @@ var HINT_GENERATORS = [
1580
1869
  } else if (hasPagesDir && !hasAppDir) {
1581
1870
  hints.push("### Next.js (Pages Router)");
1582
1871
  hints.push("");
1583
- hints.push("- **Pages Router** \u2014 routes in `pages/` directory");
1872
+ hints.push("- **Pages Router**: routes in `pages/` directory");
1584
1873
  hints.push(
1585
1874
  "- `getServerSideProps` for server-side data fetching, `getStaticProps` for static generation"
1586
1875
  );
@@ -1589,10 +1878,10 @@ var HINT_GENERATORS = [
1589
1878
  );
1590
1879
  hints.push("- API routes in `pages/api/`");
1591
1880
  } else if (hasAppDir && hasPagesDir) {
1592
- hints.push("### Next.js (Hybrid \u2014 App + Pages Router)");
1881
+ hints.push("### Next.js (Hybrid: App + Pages Router)");
1593
1882
  hints.push("");
1594
1883
  hints.push(
1595
- "- Both `app/` (App Router) and `pages/` (Pages Router) coexist \u2014 new routes should use App Router"
1884
+ "- Both `app/` (App Router) and `pages/` (Pages Router) coexist. New routes should use App Router"
1596
1885
  );
1597
1886
  hints.push(
1598
1887
  '- App Router components are server components by default; add `"use client"` for client components'
@@ -1623,7 +1912,7 @@ var HINT_GENERATORS = [
1623
1912
  "- Organize routes with `express.Router()` in separate files, mount with `app.use('/prefix', router)`",
1624
1913
  "- Validate request bodies/params at the route level (e.g. with zod, joi, or express-validator)",
1625
1914
  "- Use `async` handlers with try/catch or an async wrapper to avoid unhandled promise rejections",
1626
- "- Set `res.status()` before `res.json()` \u2014 don't rely on defaults for error responses"
1915
+ "- Set `res.status()` before `res.json()`. Don't rely on defaults for error responses"
1627
1916
  ]
1628
1917
  },
1629
1918
  {
@@ -1643,10 +1932,10 @@ var HINT_GENERATORS = [
1643
1932
  getHints: () => [
1644
1933
  "### Hono",
1645
1934
  "",
1646
- "- Middleware with `app.use()` \u2014 compose with `c.next()` pattern",
1935
+ "- Middleware with `app.use()`. Compose with `c.next()` pattern",
1647
1936
  "- Validators: use `hono/validator` or `@hono/zod-validator` for type-safe request parsing",
1648
1937
  "- Context (`c`): `c.json()`, `c.text()`, `c.html()` for responses; `c.req` for request",
1649
- "- Supports multiple runtimes (Node, Deno, Bun, Cloudflare Workers) \u2014 avoid Node-specific APIs"
1938
+ "- Supports multiple runtimes (Node, Deno, Bun, Cloudflare Workers). Avoid Node-specific APIs"
1650
1939
  ]
1651
1940
  },
1652
1941
  {
@@ -1656,7 +1945,7 @@ var HINT_GENERATORS = [
1656
1945
  "",
1657
1946
  "- **Modules** organize the app into cohesive blocks; every feature gets a module",
1658
1947
  "- **Controllers** handle HTTP requests (decorators: `@Get()`, `@Post()`, etc.)",
1659
- "- **Providers** (services) hold business logic \u2014 injected via constructor DI",
1948
+ "- **Providers** (services) hold business logic, injected via constructor DI",
1660
1949
  "- **Guards** for auth (`@UseGuards()`), **Pipes** for validation (`@UsePipes()`)",
1661
1950
  "- **Interceptors** for response transformation, logging, caching",
1662
1951
  "- DTOs with `class-validator` decorators for request validation",
@@ -1672,7 +1961,7 @@ var HINT_GENERATORS = [
1672
1961
  "- **expo-router** for file-based routing (if using); Stack, Tabs, Drawer navigators"
1673
1962
  );
1674
1963
  hints.push(
1675
- "- Expo Go has limited native module support \u2014 some packages require a dev build (`npx expo run:ios`)"
1964
+ "- Expo Go has limited native module support. Some packages require a dev build (`npx expo run:ios`)"
1676
1965
  );
1677
1966
  hints.push(
1678
1967
  "- Use `expo-constants`, `expo-device` etc. instead of raw RN APIs when available"
@@ -1685,7 +1974,7 @@ var HINT_GENERATORS = [
1685
1974
  '- **Reanimated**: worklet functions need the `"worklet"` directive on the first line'
1686
1975
  );
1687
1976
  hints.push(
1688
- "- Avoid `FadeIn`/`FadeOut` entering/exiting animations on conditionally rendered components \u2014 they cause flashes"
1977
+ "- Avoid `FadeIn`/`FadeOut` entering/exiting animations on conditionally rendered components. They cause flashes"
1689
1978
  );
1690
1979
  }
1691
1980
  return hints;
@@ -1698,11 +1987,11 @@ var HINT_GENERATORS = [
1698
1987
  return [
1699
1988
  "### React Native",
1700
1989
  "",
1701
- "- Use `StyleSheet.create()` for styles \u2014 avoid inline style objects in render",
1990
+ "- Use `StyleSheet.create()` for styles. Avoid inline style objects in render",
1702
1991
  "- Platform-specific: `*.ios.tsx` / `*.android.tsx` or `Platform.select()`",
1703
1992
  '- **Reanimated**: worklet functions need the `"worklet"` directive',
1704
1993
  "- Navigation: React Navigation with Stack/Tab/Drawer navigators",
1705
- "- Test with both iOS and Android \u2014 layout behavior differs"
1994
+ "- Test with both iOS and Android. Layout behavior differs"
1706
1995
  ];
1707
1996
  }
1708
1997
  },
@@ -1714,7 +2003,7 @@ var HINT_GENERATORS = [
1714
2003
  return [
1715
2004
  "### React",
1716
2005
  "",
1717
- "- Functional components with hooks \u2014 no class components",
2006
+ "- Functional components with hooks (no class components)",
1718
2007
  "- Use `React.memo()` for expensive renders, `useMemo`/`useCallback` for referential stability",
1719
2008
  "- Lift state up or use context for shared state; avoid prop drilling beyond 2-3 levels",
1720
2009
  "- Prefer controlled components for forms",
@@ -1739,11 +2028,11 @@ var HINT_GENERATORS = [
1739
2028
  getHints: () => [
1740
2029
  "### Nuxt",
1741
2030
  "",
1742
- "- Auto-imports: components, composables, and utils are auto-imported \u2014 no manual import needed",
1743
- "- File-based routing in `pages/` \u2014 dynamic params with `[id].vue` syntax",
1744
- "- Data fetching: `useFetch()` / `useAsyncData()` \u2014 they handle SSR hydration automatically",
1745
- "- Server routes in `server/api/` \u2014 auto-registered, use `defineEventHandler()`",
1746
- "- Middleware in `middleware/` \u2014 `defineNuxtRouteMiddleware()` for route guards",
2031
+ "- Auto-imports: components, composables, and utils are auto-imported (no manual import needed)",
2032
+ "- File-based routing in `pages/`. Dynamic params with `[id].vue` syntax",
2033
+ "- Data fetching: `useFetch()` / `useAsyncData()`. They handle SSR hydration automatically",
2034
+ "- Server routes in `server/api/`, auto-registered, use `defineEventHandler()`",
2035
+ "- Middleware in `middleware/`. `defineNuxtRouteMiddleware()` for route guards",
1747
2036
  "- State: `useState()` for SSR-safe shared state, Pinia for complex stores"
1748
2037
  ]
1749
2038
  },
@@ -1756,7 +2045,7 @@ var HINT_GENERATORS = [
1756
2045
  "",
1757
2046
  "- Reactive declarations with `$:` for derived state",
1758
2047
  "- Props: `export let propName` in component script",
1759
- "- Stores: `writable()`, `readable()`, `derived()` \u2014 auto-subscribe with `$store` syntax",
2048
+ "- Stores: `writable()`, `readable()`, `derived()`. Auto-subscribe with `$store` syntax",
1760
2049
  "- Use `{#if}`, `{#each}`, `{#await}` blocks for conditional/list/async rendering"
1761
2050
  ];
1762
2051
  }
@@ -1766,7 +2055,7 @@ var HINT_GENERATORS = [
1766
2055
  getHints: () => [
1767
2056
  "### SvelteKit",
1768
2057
  "",
1769
- "- File-based routing in `src/routes/` \u2014 `+page.svelte`, `+layout.svelte`, `+server.ts`",
2058
+ "- File-based routing in `src/routes/`: `+page.svelte`, `+layout.svelte`, `+server.ts`",
1770
2059
  "- `+page.ts` (universal) or `+page.server.ts` (server-only) for `load` functions",
1771
2060
  "- Form actions in `+page.server.ts` with `actions` export for progressive enhancement",
1772
2061
  "- Hooks in `src/hooks.server.ts` for auth, session, error handling",
@@ -1780,7 +2069,7 @@ var HINT_GENERATORS = [
1780
2069
  "",
1781
2070
  "- Components, services, pipes, directives all use decorators (`@Component`, `@Injectable`, etc.)",
1782
2071
  "- Dependency injection: provide services in module or component `providers` array",
1783
- "- RxJS Observables for async data \u2014 use `async` pipe in templates, unsubscribe on destroy",
2072
+ "- RxJS Observables for async data. Use `async` pipe in templates, unsubscribe on destroy",
1784
2073
  "- Lazy-load feature modules with `loadChildren` in routes",
1785
2074
  "- Use Angular CLI (`ng generate`) for scaffolding"
1786
2075
  ]
@@ -1792,9 +2081,9 @@ var HINT_GENERATORS = [
1792
2081
  "",
1793
2082
  "- Apps structure: each feature is a Django app with `models.py`, `views.py`, `urls.py`, `admin.py`",
1794
2083
  "- Models: define in `models.py`, create migrations with `python manage.py makemigrations`",
1795
- "- Views: function-based (FBV) or class-based (CBV) \u2014 CBV for CRUD, FBV for custom logic",
2084
+ "- Views: function-based (FBV) or class-based (CBV). CBV for CRUD, FBV for custom logic",
1796
2085
  "- URL routing in `urls.py` using `path()` and `include()` for app-level URLs",
1797
- "- Templates in `templates/` \u2014 use template inheritance with `{% extends %}` and `{% block %}`",
2086
+ "- Templates in `templates/`. Use template inheritance with `{% extends %}` and `{% block %}`",
1798
2087
  "- Management commands in `management/commands/` for custom CLI tasks",
1799
2088
  "- Settings: use `django-environ` or `python-decouple` for environment-based config"
1800
2089
  ]
@@ -1804,11 +2093,11 @@ var HINT_GENERATORS = [
1804
2093
  getHints: () => [
1805
2094
  "### Flask",
1806
2095
  "",
1807
- "- Blueprints for modular route organization \u2014 register with `app.register_blueprint()`",
2096
+ "- Blueprints for modular route organization. Register with `app.register_blueprint()`",
1808
2097
  "- Use application factory pattern (`create_app()`) for testing and config flexibility",
1809
2098
  "- Error handlers with `@app.errorhandler(404)` etc.",
1810
2099
  "- Use Flask-SQLAlchemy for ORM, Flask-Migrate for database migrations",
1811
- "- Request context: `request`, `g`, `session` globals \u2014 available inside request handlers"
2100
+ "- Request context: `request`, `g`, `session` globals, available inside request handlers"
1812
2101
  ]
1813
2102
  },
1814
2103
  {
@@ -1817,7 +2106,7 @@ var HINT_GENERATORS = [
1817
2106
  "### FastAPI",
1818
2107
  "",
1819
2108
  "- **Dependency injection**: use `Depends()` for shared logic (auth, DB sessions, validation)",
1820
- "- **Pydantic models** for request/response schemas \u2014 automatic validation and OpenAPI docs",
2109
+ "- **Pydantic models** for request/response schemas with automatic validation and OpenAPI docs",
1821
2110
  "- Async endpoints by default (`async def`); use sync `def` only for blocking I/O with threadpool",
1822
2111
  "- Routers: `APIRouter()` for modular route organization, mount with `app.include_router()`",
1823
2112
  '- Middleware with `@app.middleware("http")` or Starlette middleware classes',
@@ -1830,7 +2119,7 @@ var HINT_GENERATORS = [
1830
2119
  getHints: () => [
1831
2120
  "### Prisma",
1832
2121
  "",
1833
- "- Schema in `prisma/schema.prisma` \u2014 run `npx prisma generate` after changes",
2122
+ "- Schema in `prisma/schema.prisma`. Run `npx prisma generate` after changes",
1834
2123
  "- Migrations: `npx prisma migrate dev` for development, `npx prisma migrate deploy` for production",
1835
2124
  "- Use `prisma.$transaction()` for atomic operations",
1836
2125
  "- Relation queries: use `include` for eager loading, `select` for field filtering"
@@ -1841,7 +2130,7 @@ var HINT_GENERATORS = [
1841
2130
  getHints: () => [
1842
2131
  "### Drizzle",
1843
2132
  "",
1844
- "- Schema defined in TypeScript \u2014 type-safe queries with no code generation step",
2133
+ "- Schema defined in TypeScript. Type-safe queries with no code generation step",
1845
2134
  "- Migrations: `drizzle-kit generate` then `drizzle-kit migrate`",
1846
2135
  "- Use `db.select()`, `db.insert()`, `db.update()`, `db.delete()` for queries",
1847
2136
  "- Relations: define with `relations()` helper for type-safe joins"
@@ -1854,7 +2143,7 @@ var HINT_GENERATORS = [
1854
2143
  return [
1855
2144
  "### Tailwind CSS",
1856
2145
  "",
1857
- "- Utility-first: compose styles with `className` \u2014 avoid custom CSS unless truly needed",
2146
+ "- Utility-first: compose styles with `className`. Avoid custom CSS unless truly needed",
1858
2147
  "- Use `@apply` sparingly in CSS modules for repeated patterns",
1859
2148
  "- Responsive: mobile-first with `sm:`, `md:`, `lg:` breakpoint prefixes",
1860
2149
  "- Dark mode: `dark:` variant (class or media strategy per `tailwind.config`)",
@@ -1867,7 +2156,7 @@ var HINT_GENERATORS = [
1867
2156
  getHints: () => [
1868
2157
  "### Electron",
1869
2158
  "",
1870
- "- **Main process** (Node.js) and **renderer process** (Chromium) \u2014 communicate via IPC",
2159
+ "- **Main process** (Node.js) and **renderer process** (Chromium). Communicate via IPC",
1871
2160
  "- Use `contextBridge` + `preload.js` to expose safe APIs to renderer (no `nodeIntegration`)",
1872
2161
  "- `ipcMain.handle()` / `ipcRenderer.invoke()` for async request-response patterns",
1873
2162
  "- Package with `electron-builder` or `electron-forge`"
@@ -1885,7 +2174,7 @@ function buildMainContext(ctx, answers, snapshot, analysis) {
1885
2174
  sections.push(
1886
2175
  "> **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."
1887
2176
  );
1888
- if (answers.ide === "cursor") {
2177
+ if (answers.ides.includes("cursor")) {
1889
2178
  sections.push(
1890
2179
  "> Scoped rules are in `.cursor/rules/` -- update them when conventions change."
1891
2180
  );
@@ -1919,7 +2208,7 @@ function buildMainContext(ctx, answers, snapshot, analysis) {
1919
2208
  );
1920
2209
  sections.push("");
1921
2210
  for (const pkg of ctx.monorepo.packages) {
1922
- const fws = pkg.frameworks.length > 0 ? ` \u2014 ${pkg.frameworks.map((f) => f.name).join(", ")}` : "";
2211
+ const fws = pkg.frameworks.length > 0 ? ` (${pkg.frameworks.map((f) => f.name).join(", ")})` : "";
1923
2212
  sections.push(`- **${pkg.name}** (\`${pkg.path}\`)${fws}`);
1924
2213
  }
1925
2214
  sections.push("");
@@ -2329,7 +2618,7 @@ function buildGlobalRule(ctx, answers, analysis) {
2329
2618
  );
2330
2619
  bodyLines.push("");
2331
2620
  for (const inst of analysis.instabilities) {
2332
- bodyLines.push(`- \`${inst.path}\` \u2014 ${(inst.instability * 100).toFixed(0)}% unstable (${inst.fanIn} dependents, ${inst.fanOut} dependencies)`);
2621
+ bodyLines.push(`- \`${inst.path}\`: ${(inst.instability * 100).toFixed(0)}% unstable (${inst.fanIn} dependents, ${inst.fanOut} dependencies)`);
2333
2622
  }
2334
2623
  bodyLines.push("");
2335
2624
  }
@@ -2593,7 +2882,7 @@ function buildArchitectureSkill(analysis) {
2593
2882
  bodyLines.push("## Key Files (by centrality)");
2594
2883
  bodyLines.push("");
2595
2884
  for (const hub of analysis.hubFiles) {
2596
- bodyLines.push(`- \`${hub.path}\` \u2014 imported by ${hub.importedBy} file${hub.importedBy === 1 ? "" : "s"}`);
2885
+ bodyLines.push(`- \`${hub.path}\` (imported by ${hub.importedBy} file${hub.importedBy === 1 ? "" : "s"})`);
2597
2886
  }
2598
2887
  bodyLines.push("");
2599
2888
  }
@@ -2653,7 +2942,7 @@ function renderClaudeSkill(skill) {
2653
2942
  // src/templates/aider-context.ts
2654
2943
  function buildAiderContext(ctx, answers, snapshot, analysis) {
2655
2944
  const lines = [];
2656
- lines.push("# .aider.conf.yml \u2014 Generated by codebrief");
2945
+ lines.push("# .aider.conf.yml - Generated by codebrief");
2657
2946
  lines.push("# Keep this file up to date as the project evolves.");
2658
2947
  lines.push("");
2659
2948
  const stackSummary = answers.stackConfirmed ? summarizeDetection(ctx) : answers.stackCorrections || summarizeDetection(ctx);
@@ -2713,7 +3002,7 @@ function buildAiderContext(ctx, answers, snapshot, analysis) {
2713
3002
  }
2714
3003
  if (analysis?.instabilities && analysis.instabilities.length > 0) {
2715
3004
  for (const inst of analysis.instabilities) {
2716
- lines.push(` - "UNSTABLE: ${escapeYaml(inst.path)} \u2014 ${(inst.instability * 100).toFixed(0)}% unstable (${inst.fanIn} dependents, ${inst.fanOut} deps)"`);
3005
+ lines.push(` - "UNSTABLE: ${escapeYaml(inst.path)}: ${(inst.instability * 100).toFixed(0)}% unstable (${inst.fanIn} dependents, ${inst.fanOut} deps)"`);
2717
3006
  }
2718
3007
  }
2719
3008
  if (analysis?.gitActivity?.changeCoupling && analysis.gitActivity.changeCoupling.length > 0) {
@@ -2757,62 +3046,51 @@ function escapeYaml(s) {
2757
3046
 
2758
3047
  // src/generate.ts
2759
3048
  async function generateFiles(ctx, answers, snapshot, force = false, dryRun = false, analysis, generateSkills = false, onVerbose) {
2760
- const files = [];
2761
- const mainFilename = getMainContextFilename(answers.ide);
2762
- const mainPath = path5.join(ctx.rootDir, mainFilename);
2763
- const mainContent = answers.ide === "aider" ? buildAiderContext(ctx, answers, snapshot, analysis) : buildMainContext(ctx, answers, snapshot, analysis);
2764
- files.push({
2765
- path: mainFilename,
2766
- content: mainContent,
2767
- existed: await fileExists(mainPath)
2768
- });
2769
- onVerbose?.(`Prepared ${mainFilename} (${mainContent.length} bytes)`);
2770
- if (answers.ide === "cursor") {
2771
- const rules = buildCursorRules(ctx, answers, analysis);
2772
- for (const rule of rules) {
2773
- const rulePath = `.cursor/rules/${rule.filename}`;
2774
- const absPath = path5.join(ctx.rootDir, rulePath);
2775
- const ruleContent = renderCursorRule(rule);
2776
- files.push({
2777
- path: rulePath,
2778
- content: ruleContent,
2779
- existed: await fileExists(absPath)
2780
- });
2781
- onVerbose?.(`Prepared ${rulePath} (${ruleContent.length} bytes)`);
2782
- }
2783
- }
2784
- if (generateSkills) {
2785
- const pkgJson = await readJsonFile(path5.join(ctx.rootDir, "package.json"));
2786
- const scripts = pkgJson?.scripts ?? void 0;
2787
- const skills = buildClaudeSkills(ctx, answers, analysis, scripts);
2788
- for (const skill of skills) {
2789
- const skillPath = `.claude/skills/${skill.name}/SKILL.md`;
2790
- const absPath = path5.join(ctx.rootDir, skillPath);
2791
- const skillContent = renderClaudeSkill(skill);
2792
- files.push({
2793
- path: skillPath,
2794
- content: skillContent,
2795
- existed: await fileExists(absPath)
2796
- });
2797
- onVerbose?.(`Prepared ${skillPath} (${skillContent.length} bytes)`);
3049
+ const fileMap = /* @__PURE__ */ new Map();
3050
+ async function addFile(filePath, content) {
3051
+ if (fileMap.has(filePath)) return;
3052
+ const absPath = path5.join(ctx.rootDir, filePath);
3053
+ fileMap.set(filePath, {
3054
+ path: filePath,
3055
+ content,
3056
+ existed: await fileExists(absPath)
3057
+ });
3058
+ onVerbose?.(`Prepared ${filePath} (${content.length} bytes)`);
3059
+ }
3060
+ for (const ide of answers.ides) {
3061
+ const mainFilename = getMainContextFilename(ide);
3062
+ const mainContent = ide === "aider" ? buildAiderContext(ctx, answers, snapshot, analysis) : buildMainContext(ctx, answers, snapshot, analysis);
3063
+ await addFile(mainFilename, mainContent);
3064
+ if (ide === "cursor") {
3065
+ const rules = buildCursorRules(ctx, answers, analysis);
3066
+ for (const rule of rules) {
3067
+ const rulePath = `.cursor/rules/${rule.filename}`;
3068
+ const ruleContent = renderCursorRule(rule);
3069
+ await addFile(rulePath, ruleContent);
3070
+ }
2798
3071
  }
2799
- }
2800
- if (answers.ide === "opencode") {
2801
- const claudePath = path5.join(ctx.rootDir, "CLAUDE.md");
2802
- const claudeExists = await fileExists(claudePath);
2803
- if (!claudeExists) {
2804
- files.push({
2805
- path: "CLAUDE.md",
2806
- content: `# ${path5.basename(ctx.rootDir)}
3072
+ if (generateSkills && ide === "claude") {
3073
+ const pkgJson = await readJsonFile(path5.join(ctx.rootDir, "package.json"));
3074
+ const scripts = pkgJson?.scripts ?? void 0;
3075
+ const skills = buildClaudeSkills(ctx, answers, analysis, scripts);
3076
+ for (const skill of skills) {
3077
+ const skillPath = `.claude/skills/${skill.name}/SKILL.md`;
3078
+ const skillContent = renderClaudeSkill(skill);
3079
+ await addFile(skillPath, skillContent);
3080
+ }
3081
+ }
3082
+ if (ide === "opencode") {
3083
+ const claudePath = path5.join(ctx.rootDir, "CLAUDE.md");
3084
+ const claudeExists = await fileExists(claudePath);
3085
+ if (!claudeExists && !fileMap.has("CLAUDE.md")) {
3086
+ await addFile("CLAUDE.md", `# ${path5.basename(ctx.rootDir)}
2807
3087
 
2808
3088
  > See AGENTS.md for full project context.
2809
- `,
2810
- existed: false
2811
- });
3089
+ `);
3090
+ }
2812
3091
  }
2813
3092
  }
2814
3093
  if (answers.generatePerPackage && ctx.monorepo && ctx.monorepo.packages.length > 0) {
2815
- const pkgMainFilename = answers.ide === "aider" ? ".aider.conf.yml" : getMainContextFilename(answers.ide);
2816
3094
  for (const pkg of ctx.monorepo.packages) {
2817
3095
  const pkgRootDir = path5.join(ctx.rootDir, pkg.path);
2818
3096
  const pkgCtx = await detectContext(pkgRootDir);
@@ -2823,20 +3101,19 @@ async function generateFiles(ctx, answers, snapshot, force = false, dryRun = fal
2823
3101
  }
2824
3102
  const pkgAnswers = {
2825
3103
  ...answers,
2826
- projectPurpose: `${pkg.name} \u2014 part of the ${path5.basename(ctx.rootDir)} monorepo. ${answers.projectPurpose}`,
3104
+ projectPurpose: `${pkg.name}, part of the ${path5.basename(ctx.rootDir)} monorepo. ${answers.projectPurpose}`,
2827
3105
  generatePerPackage: false
2828
3106
  // don't recurse
2829
3107
  };
2830
- const pkgContent = answers.ide === "aider" ? buildAiderContext(pkgCtx, pkgAnswers, pkgSnapshot) : buildMainContext(pkgCtx, pkgAnswers, pkgSnapshot);
2831
- const pkgFilePath = path5.join(pkg.path, pkgMainFilename);
2832
- const pkgAbsPath = path5.join(ctx.rootDir, pkgFilePath);
2833
- files.push({
2834
- path: pkgFilePath,
2835
- content: pkgContent,
2836
- existed: await fileExists(pkgAbsPath)
2837
- });
3108
+ for (const ide of answers.ides) {
3109
+ const pkgMainFilename = ide === "aider" ? ".aider.conf.yml" : getMainContextFilename(ide);
3110
+ const pkgContent = ide === "aider" ? buildAiderContext(pkgCtx, pkgAnswers, pkgSnapshot) : buildMainContext(pkgCtx, pkgAnswers, pkgSnapshot);
3111
+ const pkgFilePath = path5.join(pkg.path, pkgMainFilename);
3112
+ await addFile(pkgFilePath, pkgContent);
3113
+ }
2838
3114
  }
2839
3115
  }
3116
+ const files = Array.from(fileMap.values());
2840
3117
  if (dryRun) {
2841
3118
  return files;
2842
3119
  }
@@ -2871,11 +3148,10 @@ ${existingFiles.map((f) => ` - ${f.path}`).join("\n")}`
2871
3148
 
2872
3149
  // src/summary.ts
2873
3150
  init_utils();
2874
- import pc from "picocolors";
2875
3151
  function printSummary(files, ctx, snapshot, analysis) {
2876
3152
  if (files.length === 0) return;
2877
3153
  console.log("");
2878
- console.log(pc.bold(" Files created:"));
3154
+ console.log(theme.brandBold(" Files created:"));
2879
3155
  console.log("");
2880
3156
  let totalBytes = 0;
2881
3157
  let totalTokens = 0;
@@ -2936,24 +3212,24 @@ function printSummary(files, ctx, snapshot, analysis) {
2936
3212
  const maxTokenWidth = Math.max(...dataFileRows.map((r) => r.tokens.length));
2937
3213
  for (const row of fileRows) {
2938
3214
  if (row.isHeader) {
2939
- console.log(`${row.indent}${pc.cyan(row.name)}`);
3215
+ console.log(`${row.indent}${theme.accent(row.name)}`);
2940
3216
  } else {
2941
- const status = row.isUpdated ? pc.yellow("(updated)") : pc.green("(new)");
3217
+ const status = row.isUpdated ? theme.muted("(updated)") : theme.success("(new)");
2942
3218
  const paddedName = row.name.padEnd(maxNameCol - row.indent.length);
2943
3219
  console.log(
2944
- `${row.indent}${pc.cyan(paddedName)} ${row.size.padStart(maxSizeWidth)} ${pc.dim(row.tokens.padEnd(maxTokenWidth))} ${status}`
3220
+ `${row.indent}${theme.accent(paddedName)} ${row.size.padStart(maxSizeWidth)} ${theme.muted(row.tokens.padEnd(maxTokenWidth))} ${status}`
2945
3221
  );
2946
3222
  }
2947
3223
  }
2948
3224
  console.log("");
2949
3225
  console.log(
2950
- pc.dim(
3226
+ theme.muted(
2951
3227
  ` Total: ${formatBytes(totalBytes)}, ~${formatNumber(totalTokens)} tokens`
2952
3228
  )
2953
3229
  );
2954
3230
  if (snapshot?.budgetExcluded && snapshot.budgetExcluded > 0) {
2955
3231
  console.log(
2956
- pc.dim(
3232
+ theme.muted(
2957
3233
  ` (${snapshot.budgetExcluded} snapshot entries excluded by token budget)`
2958
3234
  )
2959
3235
  );
@@ -2967,12 +3243,12 @@ function printSummary(files, ctx, snapshot, analysis) {
2967
3243
  if (analysis.gitActivity) parts.push(`${analysis.gitActivity.hotFiles.length} recently active files`);
2968
3244
  if (parts.length > 0) {
2969
3245
  console.log(
2970
- pc.dim(` Includes: ${parts.join(", ")}`)
3246
+ theme.muted(` Includes: ${parts.join(", ")}`)
2971
3247
  );
2972
3248
  }
2973
3249
  }
2974
3250
  console.log("");
2975
- console.log(pc.bold(" Estimated context cost per conversation:"));
3251
+ console.log(theme.brandBold(" Estimated context cost per conversation:"));
2976
3252
  console.log("");
2977
3253
  const explorationTokens = estimateExplorationCost(ctx);
2978
3254
  const avgScopedTokens = scopedRuleTokens.length > 0 ? Math.round(
@@ -2987,23 +3263,37 @@ function printSummary(files, ctx, snapshot, analysis) {
2987
3263
  const maxVal = Math.max(explorationTokens, afterTotal);
2988
3264
  const beforeBarLen = Math.max(1, Math.round(explorationTokens / maxVal * BAR_MAX));
2989
3265
  const afterBarLen = Math.max(1, Math.round(afterTotal / maxVal * BAR_MAX));
2990
- const BLOCK = "\u2588";
2991
- const beforeBar = BLOCK.repeat(beforeBarLen);
2992
- const afterBar = BLOCK.repeat(afterBarLen);
2993
- const afterPad = " ".repeat(Math.max(0, beforeBarLen - afterBarLen));
2994
- console.log(` Before: ${pc.red(beforeBar)} ~${formatNumber(explorationTokens)} tokens`);
2995
- console.log(` After: ${pc.green(afterBar)}${afterPad} ~${formatNumber(afterTotal)} tokens`);
3266
+ const savedLen = Math.max(0, beforeBarLen - afterBarLen);
3267
+ const beforeBar = gradient(
3268
+ "\u2588".repeat(beforeBarLen),
3269
+ [90, 135, 230],
3270
+ // slightly deeper than brand
3271
+ [122, 162, 247],
3272
+ // #7aa2f7 brand
3273
+ theme.brand
3274
+ );
3275
+ const afterBar = gradient(
3276
+ "\u2588".repeat(afterBarLen),
3277
+ [122, 162, 247],
3278
+ // #7aa2f7 brand
3279
+ [137, 180, 250],
3280
+ // #89b4fa accent
3281
+ theme.accent
3282
+ );
3283
+ const savedBar = theme.muted("\u2591".repeat(savedLen));
3284
+ console.log(` Before ${beforeBar} ${theme.muted(`~${formatNumber(explorationTokens)} tokens`)}`);
3285
+ console.log(` After ${afterBar}${savedBar} ${theme.muted(`~${formatNumber(afterTotal)} tokens`)}`);
2996
3286
  console.log("");
2997
3287
  if (savings > 0) {
2998
3288
  console.log(
2999
- pc.green(
3289
+ theme.success(
3000
3290
  ` Estimated savings: ~${savings}% fewer tokens`
3001
3291
  )
3002
3292
  );
3003
3293
  }
3004
3294
  if (analysis) {
3005
3295
  console.log("");
3006
- console.log(pc.bold(" What we analyzed:"));
3296
+ console.log(theme.brandBold(" What we analyzed:"));
3007
3297
  const recapRows = [];
3008
3298
  if (analysis.hubFiles.length > 0) {
3009
3299
  recapRows.push({ label: "PageRank hub detection", result: `found ${analysis.hubFiles.length} key architectural files` });
@@ -3037,7 +3327,7 @@ function printSummary(files, ctx, snapshot, analysis) {
3037
3327
  const maxRecapLabel = Math.max(...recapRows.map((r) => r.label.length));
3038
3328
  for (const row of recapRows) {
3039
3329
  console.log(
3040
- pc.dim(` ${row.label.padEnd(maxRecapLabel)} \u2192 ${row.result}`)
3330
+ theme.muted(` ${row.label.padEnd(maxRecapLabel)} \u2192 ${row.result}`)
3041
3331
  );
3042
3332
  }
3043
3333
  }
@@ -3068,12 +3358,13 @@ function printSummary(files, ctx, snapshot, analysis) {
3068
3358
  }
3069
3359
  console.log("");
3070
3360
  if (findings.length > 0) {
3071
- console.log(pc.yellow(` \u26A0 ${findings.length} finding${findings.length === 1 ? "" : "s"}`));
3361
+ const findingsHeader = ` \u26A0 ${findings.length} finding${findings.length === 1 ? "" : "s"}`;
3362
+ console.log(theme.warn(findingsHeader));
3072
3363
  for (const f of findings) {
3073
- console.log(pc.dim(` \u25CF ${f}`));
3364
+ console.log(theme.muted(` \u25CF ${f}`));
3074
3365
  }
3075
3366
  } else {
3076
- console.log(pc.green(` \u2713 No structural issues detected`));
3367
+ console.log(theme.success(` \u2713 No structural issues detected`));
3077
3368
  }
3078
3369
  }
3079
3370
  console.log("");
@@ -3106,8 +3397,10 @@ async function loadConfig(rootDir) {
3106
3397
  const raw = await readJsonFile(configPath);
3107
3398
  if (!raw) return null;
3108
3399
  const cfg = raw;
3109
- if (!cfg.ide || !cfg.projectPurpose) return null;
3400
+ const ides = cfg.ides ?? (cfg.ide ? [cfg.ide] : void 0);
3401
+ if (!ides || ides.length === 0 || !cfg.projectPurpose) return null;
3110
3402
  return {
3403
+ ides,
3111
3404
  ide: cfg.ide,
3112
3405
  projectPurpose: cfg.projectPurpose,
3113
3406
  keyPatterns: cfg.keyPatterns ?? "",
@@ -3118,14 +3411,16 @@ async function loadConfig(rootDir) {
3118
3411
  generatePerPackage: cfg.generatePerPackage ?? false,
3119
3412
  snapshotHash: cfg.snapshotHash,
3120
3413
  snapshotGeneratedAt: cfg.snapshotGeneratedAt,
3121
- language: cfg.language
3414
+ language: cfg.language,
3415
+ staleDays: cfg.staleDays
3122
3416
  };
3123
3417
  }
3124
3418
  async function saveConfig(rootDir, answers, snapshotHash, language) {
3125
3419
  const configPath = path6.join(rootDir, CONFIG_FILENAME);
3420
+ const existing = await readJsonFile(configPath);
3126
3421
  const cfg = {
3127
3422
  _version: CONFIG_VERSION,
3128
- ide: answers.ide,
3423
+ ides: answers.ides,
3129
3424
  projectPurpose: answers.projectPurpose,
3130
3425
  keyPatterns: answers.keyPatterns,
3131
3426
  gotchas: answers.gotchas,
@@ -3134,13 +3429,14 @@ async function saveConfig(rootDir, answers, snapshotHash, language) {
3134
3429
  stackCorrections: answers.stackCorrections,
3135
3430
  generatePerPackage: answers.generatePerPackage,
3136
3431
  ...snapshotHash ? { snapshotHash, snapshotGeneratedAt: Date.now() } : {},
3137
- ...language ? { language } : {}
3432
+ ...language ? { language } : {},
3433
+ ...existing?.staleDays != null ? { staleDays: existing.staleDays } : {}
3138
3434
  };
3139
3435
  await writeFileSafe(configPath, JSON.stringify(cfg, null, 2) + "\n");
3140
3436
  }
3141
3437
  function configToAnswers(config) {
3142
3438
  return {
3143
- ide: config.ide,
3439
+ ides: config.ides,
3144
3440
  projectPurpose: config.projectPurpose,
3145
3441
  keyPatterns: config.keyPatterns,
3146
3442
  gotchas: config.gotchas,
@@ -3182,7 +3478,6 @@ async function computeSnapshotHash(rootDir, language) {
3182
3478
  // src/refresh.ts
3183
3479
  import path7 from "path";
3184
3480
  import * as p3 from "@clack/prompts";
3185
- import pc2 from "picocolors";
3186
3481
  init_utils();
3187
3482
  var CONTEXT_FILES = [
3188
3483
  "CLAUDE.md",
@@ -3218,7 +3513,7 @@ async function refreshSnapshot(rootDir) {
3218
3513
  const found = await findContextFile(rootDir);
3219
3514
  if (!found) {
3220
3515
  p3.log.error(
3221
- "No context file found. Run " + pc2.cyan("codebrief") + " first to generate one."
3516
+ "No context file found. Run " + theme.accent("codebrief") + " first to generate one."
3222
3517
  );
3223
3518
  process.exit(1);
3224
3519
  }
@@ -3243,7 +3538,7 @@ async function refreshSnapshot(rootDir) {
3243
3538
  process.exit(1);
3244
3539
  }
3245
3540
  }
3246
- p3.log.info(`Refreshing snapshot in ${pc2.cyan(found.path)}`);
3541
+ p3.log.info(`Refreshing snapshot in ${theme.accent(found.path)}`);
3247
3542
  spinner3.start("Scanning source files...");
3248
3543
  const progress = (msg) => spinner3.message(msg);
3249
3544
  const detected = await detectContext(rootDir, progress);
@@ -3255,7 +3550,7 @@ async function refreshSnapshot(rootDir) {
3255
3550
  snapshot.entries.length > 0 ? `Found ${snapshot.entries.length} type${snapshot.entries.length === 1 ? "" : "s"}/signature${snapshot.entries.length === 1 ? "" : "s"}.` : "No extractable types found."
3256
3551
  );
3257
3552
  if (snapshot.entries.length === 0) {
3258
- p3.log.warn("No types found \u2014 snapshot section will be empty.");
3553
+ p3.log.warn("No types found. Snapshot section will be empty.");
3259
3554
  }
3260
3555
  let updated;
3261
3556
  if (found.isAider) {
@@ -3293,7 +3588,12 @@ async function refreshSnapshot(rootDir) {
3293
3588
  updated = content.slice(0, startIdx) + newBlock + content.slice(endIdx);
3294
3589
  }
3295
3590
  await writeFileSafe(absPath, updated);
3296
- p3.log.success(`Updated snapshot in ${pc2.cyan(found.path)}`);
3591
+ if (config) {
3592
+ const answers = configToAnswers(config);
3593
+ const newHash = await computeSnapshotHash(rootDir, config.language ?? detected.language);
3594
+ await saveConfig(rootDir, answers, newHash, config.language ?? detected.language);
3595
+ }
3596
+ p3.log.success(`Updated snapshot in ${theme.accent(found.path)}`);
3297
3597
  }
3298
3598
 
3299
3599
  // src/git-analysis.ts
@@ -3394,14 +3694,141 @@ function analyzeChangeCoupling(rootDir) {
3394
3694
 
3395
3695
  // src/index.ts
3396
3696
  init_utils();
3697
+
3698
+ // src/animations.ts
3699
+ var isTTY2 = !!process.stdout.isTTY;
3700
+ var noColor2 = !!process.env.NO_COLOR;
3701
+ var HIDE_CURSOR = "\x1B[?25l";
3702
+ var SHOW_CURSOR = "\x1B[?25h";
3703
+ function clearLine() {
3704
+ return "\x1B[A\x1B[2K";
3705
+ }
3706
+ function sleep(ms) {
3707
+ return new Promise((r) => setTimeout(r, ms));
3708
+ }
3709
+ async function renderFrames(frames, intervalMs) {
3710
+ if (!isTTY2 || noColor2) return;
3711
+ process.stdout.write(HIDE_CURSOR);
3712
+ try {
3713
+ for (let i = 0; i < frames.length; i++) {
3714
+ if (i > 0) process.stdout.write(clearLine());
3715
+ process.stdout.write(frames[i] + "\n");
3716
+ if (i < frames.length - 1) await sleep(intervalMs);
3717
+ }
3718
+ } finally {
3719
+ process.stdout.write(clearLine());
3720
+ process.stdout.write(SHOW_CURSOR);
3721
+ }
3722
+ }
3723
+ var INTERVAL = 80;
3724
+ var WIDTH = 24;
3725
+ async function animateGraphBuild(_fileCount, _edgeCount) {
3726
+ const frames = [];
3727
+ const steps = 5;
3728
+ for (let n = 1; n <= steps; n++) {
3729
+ const filled = Math.round(n / steps * WIDTH);
3730
+ const remaining = WIDTH - filled;
3731
+ frames.push(` ${theme.brand("\u2501".repeat(filled))}${theme.muted("\u254C".repeat(remaining))}`);
3732
+ }
3733
+ await renderFrames(frames, INTERVAL);
3734
+ }
3735
+ async function animatePageRank() {
3736
+ const frames = [];
3737
+ const steps = 4;
3738
+ for (let n = 1; n <= steps; n++) {
3739
+ const half = Math.round(n / steps * (WIDTH / 2));
3740
+ const pad = WIDTH / 2 - half;
3741
+ frames.push(` ${theme.muted("\u254C".repeat(pad))}${theme.brand("\u2501".repeat(half * 2))}${theme.muted("\u254C".repeat(pad))}`);
3742
+ }
3743
+ await renderFrames(frames, INTERVAL);
3744
+ }
3745
+ async function animateCycleDetection(cycleCount) {
3746
+ const frames = [];
3747
+ const steps = 5;
3748
+ for (let n = 1; n <= steps; n++) {
3749
+ const filled = Math.round(n / steps * WIDTH);
3750
+ const remaining = WIDTH - filled;
3751
+ frames.push(` ${theme.brand("\u2501".repeat(filled))}${remaining > 0 ? theme.muted("\u254C".repeat(remaining)) : theme.brand("\u25B8")}`);
3752
+ }
3753
+ const complete = "\u2501".repeat(WIDTH) + "\u25B8";
3754
+ if (cycleCount > 0) {
3755
+ frames.push(` ${theme.warn(complete)}`);
3756
+ } else {
3757
+ frames.push(` ${theme.brand(complete)}`);
3758
+ }
3759
+ await renderFrames(frames, INTERVAL);
3760
+ }
3761
+ async function animateLayerStack(layerNames) {
3762
+ if (layerNames.length === 0) return;
3763
+ const frames = [];
3764
+ for (let i = 1; i <= layerNames.length; i++) {
3765
+ const visible = layerNames.slice(0, i);
3766
+ frames.push(` ${theme.brand(visible.join(` ${theme.muted("\u25B8")} `))}`);
3767
+ }
3768
+ await renderFrames(frames, INTERVAL);
3769
+ }
3770
+ async function animateCommunities(communityCount) {
3771
+ if (communityCount === 0) return;
3772
+ const count = Math.min(communityCount, 5);
3773
+ const frames = [];
3774
+ const scattered = Array.from({ length: count }, () => "\u254C\u254C\u254C").join(" ");
3775
+ frames.push(` ${theme.muted(scattered)}`);
3776
+ const partial = Array.from({ length: count }, () => "\u2501\u254C\u2501").join(" ");
3777
+ frames.push(` ${theme.brand(partial)}`);
3778
+ const solid = Array.from({ length: count }, () => "\u2501\u2501\u2501").join(" ");
3779
+ frames.push(` ${theme.brand(solid)}`);
3780
+ await renderFrames(frames, INTERVAL);
3781
+ }
3782
+
3783
+ // src/index.ts
3784
+ var VERSION = true ? "1.4.0" : "0.0.0-dev";
3785
+ var NAME = true ? "codebrief" : "codebrief";
3786
+ var DESCRIPTION = true ? "Bootstrap optimized AI context files for any project. Auto-detect stack, generate code snapshots, produce config for Claude Code, Cursor, Copilot, Windsurf, Cline, Continue, Aider, and more." : "";
3787
+ function printHelp() {
3788
+ console.log("");
3789
+ console.log(gradient(" codebrief ", [90, 130, 220], [137, 180, 250], theme.brandBold));
3790
+ console.log(theme.muted(" " + DESCRIPTION));
3791
+ console.log("");
3792
+ console.log(` ${theme.bold("Usage:")} npx ${NAME} [directory] [options]`);
3793
+ console.log("");
3794
+ console.log(` ${theme.bold("Options:")}`);
3795
+ console.log(` ${theme.accent("-h, --help")} Show this help message`);
3796
+ console.log(` ${theme.accent("-V, --version")} Show version number`);
3797
+ console.log(` ${theme.accent("--force")} Overwrite existing files without asking`);
3798
+ console.log(` ${theme.accent("--dry-run")} Preview what would be generated`);
3799
+ console.log(` ${theme.accent("--reconfigure")} Re-prompt even if .codebrief.json exists`);
3800
+ console.log(` ${theme.accent("--refresh-snapshot")} Re-scan source files, update code snapshot only`);
3801
+ console.log(` ${theme.accent("--check")} Exit 0 if snapshot is fresh, 1 if stale (hash-based)`);
3802
+ console.log(` ${theme.accent("--check=timestamp")} Exit 0/1 based on age only (no Node.js needed in shell hooks)`);
3803
+ console.log(` ${theme.accent("--max-tokens=N")} Set the token budget for the code snapshot`);
3804
+ console.log(` ${theme.accent("--generate-skills")} Generate Claude Code skill files`);
3805
+ console.log(` ${theme.accent("-v, --verbose")} Show detailed progress output`);
3806
+ console.log("");
3807
+ console.log(` ${theme.bold("Examples:")}`);
3808
+ console.log(` ${theme.muted("$")} npx ${NAME} ${theme.muted("# analyze current directory")}`);
3809
+ console.log(` ${theme.muted("$")} npx ${NAME} ./my-project ${theme.muted("# analyze a specific project")}`);
3810
+ console.log(` ${theme.muted("$")} npx ${NAME} --dry-run ${theme.muted("# preview without writing files")}`);
3811
+ console.log(` ${theme.muted("$")} npx ${NAME} --refresh-snapshot ${theme.muted("# update code snapshot only")}`);
3812
+ console.log("");
3813
+ }
3397
3814
  async function main() {
3398
3815
  const startTime = performance.now();
3399
3816
  const args = process.argv.slice(2);
3817
+ if (args.includes("--help") || args.includes("-h")) {
3818
+ printHelp();
3819
+ process.exit(0);
3820
+ }
3821
+ if (args.includes("--version") || args.includes("-V")) {
3822
+ console.log(VERSION);
3823
+ process.exit(0);
3824
+ }
3400
3825
  const force = args.includes("--force");
3401
3826
  const dryRun = args.includes("--dry-run");
3402
3827
  const refresh = args.includes("--refresh-snapshot");
3403
3828
  const reconfigure = args.includes("--reconfigure");
3404
- const check = args.includes("--check");
3829
+ const checkArg = args.find((a) => a === "--check" || a.startsWith("--check="));
3830
+ const check = !!checkArg;
3831
+ const checkTimestamp = checkArg === "--check=timestamp";
3405
3832
  const verbose = args.includes("--verbose") || args.includes("-v");
3406
3833
  const generateSkills = args.includes("--generate-skills");
3407
3834
  const maxTokensArg = args.find((a) => a.startsWith("--max-tokens="));
@@ -3414,18 +3841,32 @@ async function main() {
3414
3841
  )).some(Boolean);
3415
3842
  if (!hasProjectMarker) {
3416
3843
  console.log("");
3417
- p4.intro(pc3.bold(" codebrief "));
3418
- p4.log.error(`No project found at ${pc3.cyan(rootDir)}`);
3419
- p4.log.info(`Run ${pc3.bold("npx codebrief")} from a project directory, or pass a path:
3420
- ${pc3.dim("npx codebrief ./my-project")}`);
3844
+ p4.intro(theme.bold(" codebrief "));
3845
+ p4.log.error(`No project found at ${theme.accent(rootDir)}`);
3846
+ p4.log.info(`Run ${theme.bold("npx codebrief")} from a project directory, or pass a path:
3847
+ ${theme.muted("npx codebrief ./my-project")}`);
3421
3848
  p4.outro("");
3422
3849
  process.exit(1);
3423
3850
  }
3424
3851
  const verboseLog = (msg) => {
3425
- if (verbose) p4.log.info(pc3.dim(msg));
3852
+ if (verbose) p4.log.info(theme.muted(msg));
3426
3853
  };
3427
3854
  if (check) {
3428
3855
  const config = await loadConfig(rootDir);
3856
+ if (checkTimestamp) {
3857
+ if (!config?.snapshotGeneratedAt) {
3858
+ process.exit(0);
3859
+ }
3860
+ const staleDays = config.staleDays ?? 7;
3861
+ const daysSince = Math.floor(
3862
+ (Date.now() - config.snapshotGeneratedAt) / (1e3 * 60 * 60 * 24)
3863
+ );
3864
+ if (daysSince > staleDays) {
3865
+ console.log(`codebrief: snapshot is ${daysSince}d old. Run: npx codebrief --refresh-snapshot`);
3866
+ process.exit(1);
3867
+ }
3868
+ process.exit(0);
3869
+ }
3429
3870
  if (!config?.snapshotHash) {
3430
3871
  process.exit(0);
3431
3872
  }
@@ -3440,16 +3881,17 @@ async function main() {
3440
3881
  process.exit(0);
3441
3882
  }
3442
3883
  console.log("");
3443
- p4.intro(pc3.bold(" codebrief "));
3884
+ p4.intro(gradient(" codebrief ", [90, 130, 220], [137, 180, 250], theme.brandBold));
3885
+ p4.log.info(theme.muted("code analysis for AI context"));
3444
3886
  if (refresh) {
3445
3887
  await refreshSnapshot(rootDir);
3446
- p4.outro(pc3.green("Snapshot refreshed!"));
3888
+ p4.outro(theme.success("Snapshot refreshed!"));
3447
3889
  return;
3448
3890
  }
3449
3891
  if (dryRun) {
3450
- p4.log.warn(pc3.yellow("DRY RUN \u2014 no files will be written"));
3892
+ p4.log.warn(theme.warn("DRY RUN: no files will be written"));
3451
3893
  }
3452
- p4.log.info(`Analyzing ${pc3.cyan(rootDir)}`);
3894
+ p4.log.info(`Analyzing ${theme.accent(rootDir)}`);
3453
3895
  const spinner3 = p4.spinner();
3454
3896
  const spinnerProgress = (msg) => spinner3.message(msg);
3455
3897
  spinner3.start("Detecting tech stack...");
@@ -3461,6 +3903,7 @@ async function main() {
3461
3903
  spinner3.stop(
3462
3904
  `Import graph: ${graph.edges.length} edges, ${graph.externalImportCounts.size} packages.` + (topHub ? ` Top hub: ${topHub.path}` : "")
3463
3905
  );
3906
+ await animateGraphBuild(detected.sourceFileCount, graph.edges.length);
3464
3907
  detected.frameworks = enrichFrameworksWithUsage(
3465
3908
  detected.frameworks,
3466
3909
  graph.externalImportCounts
@@ -3468,137 +3911,136 @@ async function main() {
3468
3911
  {
3469
3912
  const lines = [];
3470
3913
  const lang = detected.hasTypeScript ? "TypeScript" : detected.language !== "other" ? detected.language.charAt(0).toUpperCase() + detected.language.slice(1) : "";
3471
- if (lang) lines.push(` Language: ${lang}`);
3914
+ if (lang) lines.push(` ${"Language"} ${theme.accentBold(lang)}`);
3472
3915
  if (detected.frameworks.length > 0) {
3473
- lines.push(` Frameworks: ${detected.frameworks.map((f) => f.name).join(", ")}`);
3916
+ lines.push(` ${"Frameworks"} ${theme.accentBold(detected.frameworks.map((f) => f.name).join(", "))}`);
3474
3917
  }
3475
3918
  if (detected.linter !== "none") {
3476
- lines.push(` Linter: ${detected.linter.charAt(0).toUpperCase() + detected.linter.slice(1)}`);
3919
+ lines.push(` ${"Linter"} ${theme.accentBold(detected.linter.charAt(0).toUpperCase() + detected.linter.slice(1))}`);
3477
3920
  }
3478
3921
  if (detected.packageManager !== "none") {
3479
- lines.push(` Pkg mgr: ${detected.packageManager}`);
3922
+ lines.push(` ${"Pkg mgr"} ${theme.accentBold(detected.packageManager)}`);
3480
3923
  }
3481
3924
  if (detected.testFramework) {
3482
- lines.push(` Testing: ${detected.testFramework}`);
3925
+ lines.push(` ${"Testing"} ${theme.accentBold(detected.testFramework)}`);
3483
3926
  }
3484
3927
  if (detected.ciProvider) {
3485
- lines.push(` CI: ${detected.ciProvider}`);
3928
+ lines.push(` ${"CI"} ${theme.accentBold(detected.ciProvider)}`);
3486
3929
  }
3487
3930
  if (detected.monorepo) {
3488
- lines.push(` Monorepo: ${detected.monorepo.type} (${detected.monorepo.packages.length} package${detected.monorepo.packages.length === 1 ? "" : "s"})`);
3931
+ lines.push(` ${"Monorepo"} ${theme.accentBold(`${detected.monorepo.type} (${detected.monorepo.packages.length} package${detected.monorepo.packages.length === 1 ? "" : "s"})`)}`);
3489
3932
  }
3490
3933
  if (detected.sourceFileCount > 0) {
3491
- lines.push(` Files: ${detected.sourceFileCount} (${formatBytes(detected.totalSourceBytes)})`);
3934
+ lines.push(` ${"Files"} ${theme.accentBold(`${detected.sourceFileCount}`)} ${theme.muted(`(${formatBytes(detected.totalSourceBytes)})`)}`);
3492
3935
  }
3493
3936
  if (lines.length > 0) {
3494
3937
  p4.note(lines.join("\n"), "Detected Stack");
3495
3938
  }
3496
3939
  }
3497
3940
  const fileCount = graph.centrality.size;
3498
- spinner3.start(`Running PageRank on ${fileCount} files...`);
3941
+ await animatePageRank();
3499
3942
  const hubFiles = getHubFiles(graph);
3500
3943
  const topHubName = hubFiles[0]?.path ?? "";
3501
- spinner3.stop(
3502
- hubFiles.length > 0 ? `${pc3.green("PageRank")} found ${pc3.bold(String(hubFiles.length))} hub files` + (topHubName ? pc3.dim(` (top: ${topHubName})`) : "") : `${pc3.green("PageRank")} ${pc3.dim("no hub files detected")}`
3944
+ p4.log.step(
3945
+ hubFiles.length > 0 ? `${theme.brand("PageRank")} found ${theme.bold(String(hubFiles.length))} hub files` + (topHubName ? theme.muted(` (top: ${topHubName})`) : "") : `${theme.brand("PageRank")} ${theme.muted("no hub files detected")}`
3503
3946
  );
3504
3947
  if (verbose && hubFiles.length > 0) {
3505
3948
  for (const h of hubFiles.slice(0, 5)) {
3506
- p4.log.info(pc3.dim(` ${h.path} (centrality: ${h.centrality.toFixed(3)}, imported by ${h.importedBy})`));
3949
+ p4.log.info(theme.muted(` ${h.path} (centrality: ${h.centrality.toFixed(3)}, imported by ${h.importedBy})`));
3507
3950
  }
3508
3951
  }
3509
- spinner3.start("Finding circular dependencies...");
3510
3952
  const circularDeps = findCircularDeps(graph);
3511
- spinner3.stop(
3512
- circularDeps.length === 0 ? `${pc3.green("Tarjan SCC")} no cycles found ${pc3.green("\u2713")}` : `${pc3.yellow("Tarjan SCC")} ${pc3.bold(String(circularDeps.length))} cycle${circularDeps.length === 1 ? "" : "s"} found`
3953
+ await animateCycleDetection(circularDeps.length);
3954
+ p4.log.step(
3955
+ circularDeps.length === 0 ? `${theme.brand("Tarjan SCC")} no cycles found ${theme.check()}` : `${theme.warn("Tarjan SCC")} ${theme.bold(String(circularDeps.length))} cycle${circularDeps.length === 1 ? "" : "s"} found`
3513
3956
  );
3514
3957
  if (verbose && circularDeps.length > 0) {
3515
3958
  for (const c of circularDeps.slice(0, 3)) {
3516
- p4.log.info(pc3.dim(` ${c.chain.join(" \u2192 ")}`));
3959
+ p4.log.info(theme.muted(` ${c.chain.join(" \u2192 ")}`));
3517
3960
  }
3518
3961
  }
3519
- spinner3.start("Detecting architecture layers...");
3520
3962
  const { layers, layerEdges } = detectArchitecturalLayers(graph);
3521
- spinner3.stop(
3522
- layers.length > 0 ? `${pc3.green("Layers")} ${layers.map((l) => l.name).join(" \u2192 ")}` : `${pc3.green("Layers")} ${pc3.dim("no clear layers detected")}`
3963
+ await animateLayerStack(layers.map((l) => l.name));
3964
+ p4.log.step(
3965
+ layers.length > 0 ? `${theme.brand("Layers")} ${layers.map((l) => l.name).join(" \u2192 ")}` : `${theme.brand("Layers")} ${theme.muted("no clear layers detected")}`
3523
3966
  );
3524
3967
  if (verbose && layers.length > 0) {
3525
3968
  for (const l of layers) {
3526
- p4.log.info(pc3.dim(` ${l.name}: ${l.files.length} files, depends on: ${l.dependsOn.join(", ") || "none"}`));
3969
+ p4.log.info(theme.muted(` ${l.name}: ${l.files.length} files, depends on: ${l.dependsOn.join(", ") || "none"}`));
3527
3970
  }
3528
3971
  }
3529
- spinner3.start("Computing instability metrics...");
3530
3972
  const instabilities = computeInstability(graph);
3531
3973
  const highInstability = instabilities.filter((f) => f.instability > 0.8);
3532
- spinner3.stop(
3533
- highInstability.length > 0 ? `${pc3.yellow("Instability")} ${pc3.bold(String(highInstability.length))} high-risk file${highInstability.length === 1 ? "" : "s"}` : `${pc3.green("Instability")} ${pc3.dim("all files within healthy range")} ${pc3.green("\u2713")}`
3974
+ p4.log.step(
3975
+ highInstability.length > 0 ? `${theme.warn("Instability")} ${theme.bold(String(highInstability.length))} high-risk file${highInstability.length === 1 ? "" : "s"}` : `${theme.brand("Instability")} ${theme.muted("all files within healthy range")} ${theme.check()}`
3534
3976
  );
3535
3977
  if (verbose && highInstability.length > 0) {
3536
3978
  for (const f of highInstability.slice(0, 5)) {
3537
- p4.log.info(pc3.dim(` ${f.path} (I=${f.instability.toFixed(2)}, fan-in=${f.fanIn}, fan-out=${f.fanOut})`));
3979
+ p4.log.info(theme.muted(` ${f.path} (I=${f.instability.toFixed(2)}, fan-in=${f.fanIn}, fan-out=${f.fanOut})`));
3538
3980
  }
3539
3981
  }
3540
- spinner3.start("Detecting module communities...");
3541
3982
  const communities = detectCommunities(graph);
3542
- spinner3.stop(
3543
- communities.length > 0 ? `${pc3.green("Communities")} ${pc3.bold(String(communities.length))} module cluster${communities.length === 1 ? "" : "s"}` : `${pc3.green("Communities")} ${pc3.dim("single cohesive module")}`
3983
+ await animateCommunities(communities.length);
3984
+ p4.log.step(
3985
+ communities.length > 0 ? `${theme.brand("Communities")} ${theme.bold(String(communities.length))} module cluster${communities.length === 1 ? "" : "s"}` : `${theme.brand("Communities")} ${theme.muted("single cohesive module")}`
3544
3986
  );
3545
3987
  if (verbose && communities.length > 0) {
3546
3988
  for (const c of communities.slice(0, 5)) {
3547
- p4.log.info(pc3.dim(` ${c.label} (${c.files.length} files)`));
3989
+ p4.log.info(theme.muted(` ${c.label} (${c.files.length} files)`));
3548
3990
  }
3549
3991
  }
3550
- spinner3.start("Computing export coverage...");
3551
3992
  const exportCoverage = computeExportCoverage(graph);
3552
3993
  {
3553
3994
  const totalExp = exportCoverage.reduce((s, e) => s + e.totalExports, 0);
3554
3995
  const totalUsed = exportCoverage.reduce((s, e) => s + e.usedExports, 0);
3555
3996
  const unusedCount = totalExp - totalUsed;
3556
3997
  const filesWithUnused = exportCoverage.filter((e) => e.usedExports < e.totalExports).length;
3557
- spinner3.stop(
3558
- unusedCount > 0 ? `${pc3.yellow("Exports")} ${pc3.bold(String(unusedCount))} unused export${unusedCount === 1 ? "" : "s"} in ${filesWithUnused} file${filesWithUnused === 1 ? "" : "s"}` : `${pc3.green("Exports")} ${pc3.dim("all exports used")} ${pc3.green("\u2713")}`
3998
+ p4.log.step(
3999
+ unusedCount > 0 ? `${theme.warn("Exports")} ${theme.bold(String(unusedCount))} unused export${unusedCount === 1 ? "" : "s"} in ${filesWithUnused} file${filesWithUnused === 1 ? "" : "s"}` : `${theme.brand("Exports")} ${theme.muted("all exports used")} ${theme.check()}`
3559
4000
  );
3560
4001
  if (verbose && unusedCount > 0) {
3561
4002
  for (const e of exportCoverage.filter((e2) => e2.usedExports < e2.totalExports).slice(0, 5)) {
3562
- p4.log.info(pc3.dim(` ${e.file}: ${e.totalExports - e.usedExports} unused of ${e.totalExports}`));
4003
+ p4.log.info(theme.muted(` ${e.file}: ${e.totalExports - e.usedExports} unused of ${e.totalExports}`));
3563
4004
  }
3564
4005
  }
3565
4006
  }
3566
- spinner3.start("Analyzing git history...");
3567
- const gitActivity = detected.isGitRepo ? analyzeGitActivity(rootDir, verbose ? verboseLog : spinnerProgress) : null;
4007
+ const noopProgress = () => {
4008
+ };
4009
+ const gitActivity = detected.isGitRepo ? await analyzeGitActivity(rootDir, verbose ? verboseLog : noopProgress) : null;
3568
4010
  if (gitActivity) {
3569
4011
  const coupledPairs = gitActivity.changeCoupling.length;
3570
- spinner3.stop(
3571
- `${pc3.green("Git (90d)")} ${pc3.bold(String(gitActivity.hotFiles.length))} active file${gitActivity.hotFiles.length === 1 ? "" : "s"}, ${pc3.bold(String(coupledPairs))} coupled pair${coupledPairs === 1 ? "" : "s"}`
4012
+ p4.log.step(
4013
+ `${theme.brand("Git (90d)")} ${theme.bold(String(gitActivity.hotFiles.length))} active file${gitActivity.hotFiles.length === 1 ? "" : "s"}, ${theme.bold(String(coupledPairs))} coupled pair${coupledPairs === 1 ? "" : "s"}`
3572
4014
  );
3573
4015
  if (verbose) {
3574
4016
  for (const h of gitActivity.hotFiles.slice(0, 5)) {
3575
- p4.log.info(pc3.dim(` ${h.path} (${h.commits} commits, last: ${h.lastChanged})`));
4017
+ p4.log.info(theme.muted(` ${h.path} (${h.commits} commits, last: ${h.lastChanged})`));
3576
4018
  }
3577
4019
  }
3578
4020
  } else {
3579
- spinner3.stop(`${pc3.green("Git")} ${pc3.dim("not a git repo \u2014 skipped")}`);
4021
+ p4.log.step(`${theme.brand("Git")} ${theme.muted("not a git repo, skipped")}`);
3580
4022
  }
3581
4023
  const analysis = { hubFiles, circularDeps, layers, layerEdges, gitActivity, instabilities, communities, exportCoverage };
3582
4024
  {
3583
4025
  const reportLines = [];
3584
- reportLines.push(` Files analyzed: ${fileCount}`);
3585
- reportLines.push(` Import edges: ${graph.edges.length}`);
3586
- reportLines.push(` External pkgs: ${graph.externalImportCounts.size}`);
4026
+ reportLines.push(` ${"Files analyzed"} ${theme.brandBold(String(fileCount))}`);
4027
+ reportLines.push(` ${"Import edges"} ${theme.brandBold(String(graph.edges.length))}`);
4028
+ reportLines.push(` ${"External pkgs"} ${theme.brandBold(String(graph.externalImportCounts.size))}`);
3587
4029
  if (hubFiles.length > 0) {
3588
- reportLines.push(` Hub files: ${hubFiles.length}` + (hubFiles[0] ? ` (most connected: ${hubFiles[0].path})` : ""));
4030
+ reportLines.push(` ${"Hub files"} ${theme.brandBold(String(hubFiles.length))}` + (hubFiles[0] ? ` ${theme.muted(`(most connected: ${hubFiles[0].path})`)}` : ""));
3589
4031
  }
3590
4032
  if (layers.length > 0) {
3591
- reportLines.push(` Architecture: ${layers.map((l) => l.name).join(" \u2192 ")}`);
4033
+ reportLines.push(` ${"Architecture"} ${theme.accentBold(layers.map((l) => l.name).join(" \u2192 "))}`);
3592
4034
  }
3593
- reportLines.push(` Circular deps: ${circularDeps.length === 0 ? "none" : `${circularDeps.length} chain${circularDeps.length === 1 ? "" : "s"}`}`);
4035
+ reportLines.push(` ${"Circular deps"} ${circularDeps.length === 0 ? theme.success("none") : theme.warn(`${circularDeps.length} chain${circularDeps.length === 1 ? "" : "s"}`)}`);
3594
4036
  if (gitActivity) {
3595
- reportLines.push(` Hot files (90d): ${gitActivity.hotFiles.length}`);
4037
+ reportLines.push(` ${"Hot files (90d)"} ${theme.brandBold(String(gitActivity.hotFiles.length))}`);
3596
4038
  }
3597
4039
  p4.note(reportLines.join("\n"), "Analysis Report");
3598
4040
  if (circularDeps.length > 0) {
3599
4041
  for (const c of circularDeps.slice(0, 2)) {
3600
4042
  const shortChain = c.chain.map((f) => f.split("/").pop() ?? f);
3601
- p4.log.warn(pc3.yellow(`Cycle: ${shortChain.join(" \u2192 ")}`));
4043
+ p4.log.warn(theme.warn(`Cycle: ${shortChain.join(" \u2192 ")}`));
3602
4044
  }
3603
4045
  }
3604
4046
  }
@@ -3610,8 +4052,8 @@ async function main() {
3610
4052
  (Date.now() - savedConfig.snapshotGeneratedAt) / (1e3 * 60 * 60 * 24)
3611
4053
  );
3612
4054
  p4.log.warn(
3613
- pc3.yellow(
3614
- `Code snapshot may be stale (source files changed${daysSince > 0 ? `, last generated ${daysSince}d ago` : ""}). Run with ${pc3.bold("--refresh-snapshot")} to update.`
4055
+ theme.warn(
4056
+ `Code snapshot may be stale (source files changed${daysSince > 0 ? `, last generated ${daysSince}d ago` : ""}). Run with ${theme.bold("--refresh-snapshot")} to update.`
3615
4057
  )
3616
4058
  );
3617
4059
  }
@@ -3619,18 +4061,18 @@ async function main() {
3619
4061
  let answers;
3620
4062
  if (savedConfig && !reconfigure) {
3621
4063
  p4.log.info(
3622
- `Using saved config from ${pc3.cyan(".codebrief.json")} ` + pc3.dim("(run with --reconfigure to change)")
4064
+ `Using saved config from ${theme.accent(".codebrief.json")} ` + theme.muted("(run with --reconfigure to change)")
3623
4065
  );
3624
4066
  answers = configToAnswers(savedConfig);
3625
4067
  if (detected.monorepo && detected.monorepo.packages.length > 0 && !savedConfig.generatePerPackage) {
3626
4068
  }
3627
4069
  } else {
3628
- answers = await runPrompts(detected, reconfigure ? savedConfig : null);
4070
+ answers = await runPrompts(detected, reconfigure ? savedConfig : null, reconfigure);
3629
4071
  if (!dryRun) {
3630
4072
  const hash = await computeSnapshotHash(rootDir, detected.language);
3631
4073
  await saveConfig(rootDir, answers, hash, detected.language);
3632
4074
  p4.log.info(
3633
- pc3.dim("Saved config to .codebrief.json for future runs.")
4075
+ theme.muted("Saved config to .codebrief.json for future runs.")
3634
4076
  );
3635
4077
  }
3636
4078
  }
@@ -3650,7 +4092,7 @@ async function main() {
3650
4092
  spinner3.start(
3651
4093
  dryRun ? "Preparing context files..." : "Generating context files..."
3652
4094
  );
3653
- const shouldGenerateSkills = generateSkills || answers.ide === "claude";
4095
+ const shouldGenerateSkills = generateSkills || answers.ides.includes("claude");
3654
4096
  const files = await generateFiles(detected, answers, snapshot, force, dryRun, analysis, shouldGenerateSkills, verbose ? verboseLog : void 0);
3655
4097
  spinner3.stop(
3656
4098
  dryRun ? `Would generate ${files.length} file${files.length === 1 ? "" : "s"}.` : `Generated ${files.length} file${files.length === 1 ? "" : "s"}.`
@@ -3663,18 +4105,18 @@ async function main() {
3663
4105
  const elapsed = ((performance.now() - startTime) / 1e3).toFixed(1);
3664
4106
  if (dryRun) {
3665
4107
  p4.outro(
3666
- pc3.yellow("DRY RUN complete \u2014 ") + pc3.dim(`no files were written. Remove --dry-run to generate. (${elapsed}s)`)
4108
+ theme.warn("DRY RUN complete. ") + theme.muted(`no files were written. Remove --dry-run to generate. (${elapsed}s)`)
3667
4109
  );
3668
4110
  return;
3669
4111
  }
3670
4112
  p4.outro(
3671
- pc3.green(`Done in ${elapsed}s! `) + pc3.dim(
3672
- "Your context files are ready. They are living documents \u2014 keep them up to date as your project evolves."
4113
+ theme.success(`Done in ${elapsed}s! `) + theme.muted(
4114
+ "Your context files are ready. They are living documents: keep them up to date as your project evolves."
3673
4115
  )
3674
4116
  );
3675
4117
  }
3676
4118
  main().catch((err) => {
3677
- console.error(pc3.red("Fatal error:"), err);
4119
+ console.error(theme.error("Fatal error:"), err);
3678
4120
  process.exit(1);
3679
4121
  });
3680
4122
  //# sourceMappingURL=index.js.map