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/README.md +276 -91
- package/dist/index.js +734 -292
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
|
521
|
-
message: "Which AI coding
|
|
592
|
+
const ides = await p.multiselect({
|
|
593
|
+
message: "Which AI coding tools do you use? (select all that apply)",
|
|
522
594
|
options: ideOptions,
|
|
523
|
-
|
|
595
|
+
initialValues: defaults?.ides ?? (defaults?.ide ? [defaults.ide] : void 0),
|
|
596
|
+
required: true
|
|
524
597
|
});
|
|
525
|
-
if (p.isCancel(
|
|
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 (
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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(
|
|
610
|
+
if (p.isCancel(confirm3)) {
|
|
548
611
|
p.cancel("Cancelled.");
|
|
549
612
|
process.exit(0);
|
|
550
613
|
}
|
|
551
|
-
|
|
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
|
|
568
|
-
placeholder: "e.g. Zustand
|
|
569
|
-
defaultValue:
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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(
|
|
669
|
+
if (p.isCancel(snapshotChoice)) {
|
|
611
670
|
p.cancel("Cancelled.");
|
|
612
671
|
process.exit(0);
|
|
613
672
|
}
|
|
614
|
-
|
|
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
|
-
|
|
709
|
+
ides,
|
|
633
710
|
projectPurpose,
|
|
634
711
|
keyPatterns: keyPatterns ?? "",
|
|
635
|
-
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
|
|
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
|
-
|
|
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}
|
|
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
|
|
1367
|
-
|
|
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 +=
|
|
1380
|
-
|
|
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 +=
|
|
1385
|
-
|
|
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 +=
|
|
1390
|
-
|
|
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 +=
|
|
1395
|
-
|
|
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 +=
|
|
1400
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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()
|
|
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()
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1743
|
-
"- File-based routing in `pages
|
|
1744
|
-
"- Data fetching: `useFetch()` / `useAsyncData()
|
|
1745
|
-
"- Server routes in `server/api
|
|
1746
|
-
"- Middleware in `middleware
|
|
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()
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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.
|
|
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 ? `
|
|
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}
|
|
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}\`
|
|
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
|
|
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)}
|
|
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
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
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
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
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(
|
|
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}${
|
|
3215
|
+
console.log(`${row.indent}${theme.accent(row.name)}`);
|
|
2940
3216
|
} else {
|
|
2941
|
-
const status = row.isUpdated ?
|
|
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}${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3246
|
+
theme.muted(` Includes: ${parts.join(", ")}`)
|
|
2971
3247
|
);
|
|
2972
3248
|
}
|
|
2973
3249
|
}
|
|
2974
3250
|
console.log("");
|
|
2975
|
-
console.log(
|
|
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
|
|
2991
|
-
const beforeBar =
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
3364
|
+
console.log(theme.muted(` \u25CF ${f}`));
|
|
3074
3365
|
}
|
|
3075
3366
|
} else {
|
|
3076
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 " +
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
3418
|
-
p4.log.error(`No project found at ${
|
|
3419
|
-
p4.log.info(`Run ${
|
|
3420
|
-
${
|
|
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(
|
|
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(
|
|
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(
|
|
3888
|
+
p4.outro(theme.success("Snapshot refreshed!"));
|
|
3447
3889
|
return;
|
|
3448
3890
|
}
|
|
3449
3891
|
if (dryRun) {
|
|
3450
|
-
p4.log.warn(
|
|
3892
|
+
p4.log.warn(theme.warn("DRY RUN: no files will be written"));
|
|
3451
3893
|
}
|
|
3452
|
-
p4.log.info(`Analyzing ${
|
|
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
|
|
3914
|
+
if (lang) lines.push(` ${"Language"} ${theme.accentBold(lang)}`);
|
|
3472
3915
|
if (detected.frameworks.length > 0) {
|
|
3473
|
-
lines.push(` Frameworks
|
|
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
|
|
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
|
|
3922
|
+
lines.push(` ${"Pkg mgr"} ${theme.accentBold(detected.packageManager)}`);
|
|
3480
3923
|
}
|
|
3481
3924
|
if (detected.testFramework) {
|
|
3482
|
-
lines.push(` Testing
|
|
3925
|
+
lines.push(` ${"Testing"} ${theme.accentBold(detected.testFramework)}`);
|
|
3483
3926
|
}
|
|
3484
3927
|
if (detected.ciProvider) {
|
|
3485
|
-
lines.push(` CI
|
|
3928
|
+
lines.push(` ${"CI"} ${theme.accentBold(detected.ciProvider)}`);
|
|
3486
3929
|
}
|
|
3487
3930
|
if (detected.monorepo) {
|
|
3488
|
-
lines.push(` Monorepo
|
|
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
|
|
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
|
-
|
|
3941
|
+
await animatePageRank();
|
|
3499
3942
|
const hubFiles = getHubFiles(graph);
|
|
3500
3943
|
const topHubName = hubFiles[0]?.path ?? "";
|
|
3501
|
-
|
|
3502
|
-
hubFiles.length > 0 ? `${
|
|
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(
|
|
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
|
-
|
|
3512
|
-
|
|
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(
|
|
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
|
-
|
|
3522
|
-
|
|
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(
|
|
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
|
-
|
|
3533
|
-
highInstability.length > 0 ? `${
|
|
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(
|
|
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
|
-
|
|
3543
|
-
|
|
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(
|
|
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
|
-
|
|
3558
|
-
unusedCount > 0 ? `${
|
|
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(
|
|
4003
|
+
p4.log.info(theme.muted(` ${e.file}: ${e.totalExports - e.usedExports} unused of ${e.totalExports}`));
|
|
3563
4004
|
}
|
|
3564
4005
|
}
|
|
3565
4006
|
}
|
|
3566
|
-
|
|
3567
|
-
|
|
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
|
-
|
|
3571
|
-
`${
|
|
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(
|
|
4017
|
+
p4.log.info(theme.muted(` ${h.path} (${h.commits} commits, last: ${h.lastChanged})`));
|
|
3576
4018
|
}
|
|
3577
4019
|
}
|
|
3578
4020
|
} else {
|
|
3579
|
-
|
|
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
|
|
3585
|
-
reportLines.push(` Import edges
|
|
3586
|
-
reportLines.push(` External pkgs
|
|
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
|
|
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
|
|
4033
|
+
reportLines.push(` ${"Architecture"} ${theme.accentBold(layers.map((l) => l.name).join(" \u2192 "))}`);
|
|
3592
4034
|
}
|
|
3593
|
-
reportLines.push(` Circular deps
|
|
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)
|
|
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(
|
|
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
|
-
|
|
3614
|
-
`Code snapshot may be stale (source files changed${daysSince > 0 ? `, last generated ${daysSince}d ago` : ""}). Run with ${
|
|
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 ${
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
3672
|
-
"Your context files are ready. They are living documents
|
|
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(
|
|
4119
|
+
console.error(theme.error("Fatal error:"), err);
|
|
3678
4120
|
process.exit(1);
|
|
3679
4121
|
});
|
|
3680
4122
|
//# sourceMappingURL=index.js.map
|