codebrief 1.1.1 → 1.3.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 +253 -88
- package/dist/index.js +802 -316
- 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(
|
|
@@ -2983,53 +3259,41 @@ function printSummary(files, ctx, snapshot, analysis) {
|
|
|
2983
3259
|
const savings = Math.round(
|
|
2984
3260
|
(explorationTokens - afterTotal) / explorationTokens * 100
|
|
2985
3261
|
);
|
|
2986
|
-
const
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
const beforeLabel = "Exploration to understand codebase";
|
|
2999
|
-
const beforeGap = afterContentWidth - beforeLabel.length - beforeValue.length;
|
|
3000
|
-
console.log(pc.dim(" Before (no context files):"));
|
|
3001
|
-
if (beforeGap >= 2) {
|
|
3002
|
-
console.log(
|
|
3003
|
-
` ${beforeLabel}${" ".repeat(beforeGap)}${pc.red(beforeValue)}`
|
|
3004
|
-
);
|
|
3005
|
-
} else {
|
|
3006
|
-
console.log(
|
|
3007
|
-
` ${beforeLabel} ${pc.red(beforeValue)}`
|
|
3008
|
-
);
|
|
3009
|
-
}
|
|
3010
|
-
console.log("");
|
|
3011
|
-
console.log(pc.dim(" After:"));
|
|
3012
|
-
for (const row of costRows) {
|
|
3013
|
-
console.log(
|
|
3014
|
-
` ${row.label.padEnd(maxCostLabel)} ${row.desc.padEnd(maxCostDesc)} ${pc.green(row.value.padStart(maxCostValue))}`
|
|
3015
|
-
);
|
|
3016
|
-
}
|
|
3017
|
-
const totalText = `Total: ~${formatNumber(afterTotal)} tokens`;
|
|
3018
|
-
const totalPad = afterContentWidth > totalText.length ? " ".repeat(afterContentWidth - totalText.length) : "";
|
|
3019
|
-
console.log(
|
|
3020
|
-
` ${totalPad}${pc.bold(totalText)}`
|
|
3262
|
+
const BAR_MAX = 40;
|
|
3263
|
+
const maxVal = Math.max(explorationTokens, afterTotal);
|
|
3264
|
+
const beforeBarLen = Math.max(1, Math.round(explorationTokens / maxVal * BAR_MAX));
|
|
3265
|
+
const afterBarLen = Math.max(1, Math.round(afterTotal / maxVal * BAR_MAX));
|
|
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
|
|
3021
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`)}`);
|
|
3022
3286
|
console.log("");
|
|
3023
3287
|
if (savings > 0) {
|
|
3024
3288
|
console.log(
|
|
3025
|
-
|
|
3026
|
-
` Estimated savings: ~${savings}% fewer tokens
|
|
3289
|
+
theme.success(
|
|
3290
|
+
` Estimated savings: ~${savings}% fewer tokens`
|
|
3027
3291
|
)
|
|
3028
3292
|
);
|
|
3029
3293
|
}
|
|
3030
3294
|
if (analysis) {
|
|
3031
3295
|
console.log("");
|
|
3032
|
-
console.log(
|
|
3296
|
+
console.log(theme.brandBold(" What we analyzed:"));
|
|
3033
3297
|
const recapRows = [];
|
|
3034
3298
|
if (analysis.hubFiles.length > 0) {
|
|
3035
3299
|
recapRows.push({ label: "PageRank hub detection", result: `found ${analysis.hubFiles.length} key architectural files` });
|
|
@@ -3063,24 +3327,44 @@ function printSummary(files, ctx, snapshot, analysis) {
|
|
|
3063
3327
|
const maxRecapLabel = Math.max(...recapRows.map((r) => r.label.length));
|
|
3064
3328
|
for (const row of recapRows) {
|
|
3065
3329
|
console.log(
|
|
3066
|
-
|
|
3330
|
+
theme.muted(` ${row.label.padEnd(maxRecapLabel)} \u2192 ${row.result}`)
|
|
3067
3331
|
);
|
|
3068
3332
|
}
|
|
3069
3333
|
}
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
);
|
|
3334
|
+
if (analysis) {
|
|
3335
|
+
const findings = [];
|
|
3336
|
+
if (analysis.circularDeps.length > 0) {
|
|
3337
|
+
for (const c of analysis.circularDeps.slice(0, 3)) {
|
|
3338
|
+
const names = c.chain.map((f) => f.split("/").pop()?.replace(/\.[jt]sx?$/, "") ?? f);
|
|
3339
|
+
findings.push(`${analysis.circularDeps.length > 1 ? "" : ""}1 circular dependency chain (${names.slice(0, 2).join(" \u2194 ")})`);
|
|
3340
|
+
}
|
|
3341
|
+
if (analysis.circularDeps.length > 1) {
|
|
3342
|
+
findings[0] = `${analysis.circularDeps.length} circular dependency chain${analysis.circularDeps.length === 1 ? "" : "s"}`;
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
const highInstabilityFiles = analysis.instabilities.filter((f) => f.instability > 0.8);
|
|
3346
|
+
if (highInstabilityFiles.length > 0) {
|
|
3347
|
+
findings.push(`${highInstabilityFiles.length} high-instability file${highInstabilityFiles.length === 1 ? "" : "s"}`);
|
|
3348
|
+
}
|
|
3349
|
+
const ec = analysis.exportCoverage;
|
|
3350
|
+
if (ec && ec.length > 0) {
|
|
3351
|
+
const totalExports = ec.reduce((sum, e) => sum + e.totalExports, 0);
|
|
3352
|
+
const totalUsed = ec.reduce((sum, e) => sum + e.usedExports, 0);
|
|
3353
|
+
const unusedExports = totalExports - totalUsed;
|
|
3354
|
+
const filesWithUnused = ec.filter((e) => e.usedExports < e.totalExports).length;
|
|
3355
|
+
if (unusedExports > 0) {
|
|
3356
|
+
findings.push(`${unusedExports} unused export${unusedExports === 1 ? "" : "s"} in ${filesWithUnused} file${filesWithUnused === 1 ? "" : "s"}`);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
console.log("");
|
|
3360
|
+
if (findings.length > 0) {
|
|
3361
|
+
const findingsHeader = ` \u26A0 ${findings.length} finding${findings.length === 1 ? "" : "s"}`;
|
|
3362
|
+
console.log(theme.warn(findingsHeader));
|
|
3363
|
+
for (const f of findings) {
|
|
3364
|
+
console.log(theme.muted(` \u25CF ${f}`));
|
|
3365
|
+
}
|
|
3366
|
+
} else {
|
|
3367
|
+
console.log(theme.success(` \u2713 No structural issues detected`));
|
|
3084
3368
|
}
|
|
3085
3369
|
}
|
|
3086
3370
|
console.log("");
|
|
@@ -3113,8 +3397,10 @@ async function loadConfig(rootDir) {
|
|
|
3113
3397
|
const raw = await readJsonFile(configPath);
|
|
3114
3398
|
if (!raw) return null;
|
|
3115
3399
|
const cfg = raw;
|
|
3116
|
-
|
|
3400
|
+
const ides = cfg.ides ?? (cfg.ide ? [cfg.ide] : void 0);
|
|
3401
|
+
if (!ides || ides.length === 0 || !cfg.projectPurpose) return null;
|
|
3117
3402
|
return {
|
|
3403
|
+
ides,
|
|
3118
3404
|
ide: cfg.ide,
|
|
3119
3405
|
projectPurpose: cfg.projectPurpose,
|
|
3120
3406
|
keyPatterns: cfg.keyPatterns ?? "",
|
|
@@ -3132,7 +3418,7 @@ async function saveConfig(rootDir, answers, snapshotHash, language) {
|
|
|
3132
3418
|
const configPath = path6.join(rootDir, CONFIG_FILENAME);
|
|
3133
3419
|
const cfg = {
|
|
3134
3420
|
_version: CONFIG_VERSION,
|
|
3135
|
-
|
|
3421
|
+
ides: answers.ides,
|
|
3136
3422
|
projectPurpose: answers.projectPurpose,
|
|
3137
3423
|
keyPatterns: answers.keyPatterns,
|
|
3138
3424
|
gotchas: answers.gotchas,
|
|
@@ -3147,7 +3433,7 @@ async function saveConfig(rootDir, answers, snapshotHash, language) {
|
|
|
3147
3433
|
}
|
|
3148
3434
|
function configToAnswers(config) {
|
|
3149
3435
|
return {
|
|
3150
|
-
|
|
3436
|
+
ides: config.ides,
|
|
3151
3437
|
projectPurpose: config.projectPurpose,
|
|
3152
3438
|
keyPatterns: config.keyPatterns,
|
|
3153
3439
|
gotchas: config.gotchas,
|
|
@@ -3189,7 +3475,6 @@ async function computeSnapshotHash(rootDir, language) {
|
|
|
3189
3475
|
// src/refresh.ts
|
|
3190
3476
|
import path7 from "path";
|
|
3191
3477
|
import * as p3 from "@clack/prompts";
|
|
3192
|
-
import pc2 from "picocolors";
|
|
3193
3478
|
init_utils();
|
|
3194
3479
|
var CONTEXT_FILES = [
|
|
3195
3480
|
"CLAUDE.md",
|
|
@@ -3225,7 +3510,7 @@ async function refreshSnapshot(rootDir) {
|
|
|
3225
3510
|
const found = await findContextFile(rootDir);
|
|
3226
3511
|
if (!found) {
|
|
3227
3512
|
p3.log.error(
|
|
3228
|
-
"No context file found. Run " +
|
|
3513
|
+
"No context file found. Run " + theme.accent("codebrief") + " first to generate one."
|
|
3229
3514
|
);
|
|
3230
3515
|
process.exit(1);
|
|
3231
3516
|
}
|
|
@@ -3250,7 +3535,7 @@ async function refreshSnapshot(rootDir) {
|
|
|
3250
3535
|
process.exit(1);
|
|
3251
3536
|
}
|
|
3252
3537
|
}
|
|
3253
|
-
p3.log.info(`Refreshing snapshot in ${
|
|
3538
|
+
p3.log.info(`Refreshing snapshot in ${theme.accent(found.path)}`);
|
|
3254
3539
|
spinner3.start("Scanning source files...");
|
|
3255
3540
|
const progress = (msg) => spinner3.message(msg);
|
|
3256
3541
|
const detected = await detectContext(rootDir, progress);
|
|
@@ -3262,7 +3547,7 @@ async function refreshSnapshot(rootDir) {
|
|
|
3262
3547
|
snapshot.entries.length > 0 ? `Found ${snapshot.entries.length} type${snapshot.entries.length === 1 ? "" : "s"}/signature${snapshot.entries.length === 1 ? "" : "s"}.` : "No extractable types found."
|
|
3263
3548
|
);
|
|
3264
3549
|
if (snapshot.entries.length === 0) {
|
|
3265
|
-
p3.log.warn("No types found
|
|
3550
|
+
p3.log.warn("No types found. Snapshot section will be empty.");
|
|
3266
3551
|
}
|
|
3267
3552
|
let updated;
|
|
3268
3553
|
if (found.isAider) {
|
|
@@ -3300,7 +3585,7 @@ async function refreshSnapshot(rootDir) {
|
|
|
3300
3585
|
updated = content.slice(0, startIdx) + newBlock + content.slice(endIdx);
|
|
3301
3586
|
}
|
|
3302
3587
|
await writeFileSafe(absPath, updated);
|
|
3303
|
-
p3.log.success(`Updated snapshot in ${
|
|
3588
|
+
p3.log.success(`Updated snapshot in ${theme.accent(found.path)}`);
|
|
3304
3589
|
}
|
|
3305
3590
|
|
|
3306
3591
|
// src/git-analysis.ts
|
|
@@ -3401,9 +3686,133 @@ function analyzeChangeCoupling(rootDir) {
|
|
|
3401
3686
|
|
|
3402
3687
|
// src/index.ts
|
|
3403
3688
|
init_utils();
|
|
3689
|
+
|
|
3690
|
+
// src/animations.ts
|
|
3691
|
+
var isTTY2 = !!process.stdout.isTTY;
|
|
3692
|
+
var noColor2 = !!process.env.NO_COLOR;
|
|
3693
|
+
var HIDE_CURSOR = "\x1B[?25l";
|
|
3694
|
+
var SHOW_CURSOR = "\x1B[?25h";
|
|
3695
|
+
function clearLine() {
|
|
3696
|
+
return "\x1B[A\x1B[2K";
|
|
3697
|
+
}
|
|
3698
|
+
function sleep(ms) {
|
|
3699
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
3700
|
+
}
|
|
3701
|
+
async function renderFrames(frames, intervalMs) {
|
|
3702
|
+
if (!isTTY2 || noColor2) return;
|
|
3703
|
+
process.stdout.write(HIDE_CURSOR);
|
|
3704
|
+
try {
|
|
3705
|
+
for (let i = 0; i < frames.length; i++) {
|
|
3706
|
+
if (i > 0) process.stdout.write(clearLine());
|
|
3707
|
+
process.stdout.write(frames[i] + "\n");
|
|
3708
|
+
if (i < frames.length - 1) await sleep(intervalMs);
|
|
3709
|
+
}
|
|
3710
|
+
} finally {
|
|
3711
|
+
process.stdout.write(clearLine());
|
|
3712
|
+
process.stdout.write(SHOW_CURSOR);
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
var INTERVAL = 80;
|
|
3716
|
+
var WIDTH = 24;
|
|
3717
|
+
async function animateGraphBuild(_fileCount, _edgeCount) {
|
|
3718
|
+
const frames = [];
|
|
3719
|
+
const steps = 5;
|
|
3720
|
+
for (let n = 1; n <= steps; n++) {
|
|
3721
|
+
const filled = Math.round(n / steps * WIDTH);
|
|
3722
|
+
const remaining = WIDTH - filled;
|
|
3723
|
+
frames.push(` ${theme.brand("\u2501".repeat(filled))}${theme.muted("\u254C".repeat(remaining))}`);
|
|
3724
|
+
}
|
|
3725
|
+
await renderFrames(frames, INTERVAL);
|
|
3726
|
+
}
|
|
3727
|
+
async function animatePageRank() {
|
|
3728
|
+
const frames = [];
|
|
3729
|
+
const steps = 4;
|
|
3730
|
+
for (let n = 1; n <= steps; n++) {
|
|
3731
|
+
const half = Math.round(n / steps * (WIDTH / 2));
|
|
3732
|
+
const pad = WIDTH / 2 - half;
|
|
3733
|
+
frames.push(` ${theme.muted("\u254C".repeat(pad))}${theme.brand("\u2501".repeat(half * 2))}${theme.muted("\u254C".repeat(pad))}`);
|
|
3734
|
+
}
|
|
3735
|
+
await renderFrames(frames, INTERVAL);
|
|
3736
|
+
}
|
|
3737
|
+
async function animateCycleDetection(cycleCount) {
|
|
3738
|
+
const frames = [];
|
|
3739
|
+
const steps = 5;
|
|
3740
|
+
for (let n = 1; n <= steps; n++) {
|
|
3741
|
+
const filled = Math.round(n / steps * WIDTH);
|
|
3742
|
+
const remaining = WIDTH - filled;
|
|
3743
|
+
frames.push(` ${theme.brand("\u2501".repeat(filled))}${remaining > 0 ? theme.muted("\u254C".repeat(remaining)) : theme.brand("\u25B8")}`);
|
|
3744
|
+
}
|
|
3745
|
+
const complete = "\u2501".repeat(WIDTH) + "\u25B8";
|
|
3746
|
+
if (cycleCount > 0) {
|
|
3747
|
+
frames.push(` ${theme.warn(complete)}`);
|
|
3748
|
+
} else {
|
|
3749
|
+
frames.push(` ${theme.brand(complete)}`);
|
|
3750
|
+
}
|
|
3751
|
+
await renderFrames(frames, INTERVAL);
|
|
3752
|
+
}
|
|
3753
|
+
async function animateLayerStack(layerNames) {
|
|
3754
|
+
if (layerNames.length === 0) return;
|
|
3755
|
+
const frames = [];
|
|
3756
|
+
for (let i = 1; i <= layerNames.length; i++) {
|
|
3757
|
+
const visible = layerNames.slice(0, i);
|
|
3758
|
+
frames.push(` ${theme.brand(visible.join(` ${theme.muted("\u25B8")} `))}`);
|
|
3759
|
+
}
|
|
3760
|
+
await renderFrames(frames, INTERVAL);
|
|
3761
|
+
}
|
|
3762
|
+
async function animateCommunities(communityCount) {
|
|
3763
|
+
if (communityCount === 0) return;
|
|
3764
|
+
const count = Math.min(communityCount, 5);
|
|
3765
|
+
const frames = [];
|
|
3766
|
+
const scattered = Array.from({ length: count }, () => "\u254C\u254C\u254C").join(" ");
|
|
3767
|
+
frames.push(` ${theme.muted(scattered)}`);
|
|
3768
|
+
const partial = Array.from({ length: count }, () => "\u2501\u254C\u2501").join(" ");
|
|
3769
|
+
frames.push(` ${theme.brand(partial)}`);
|
|
3770
|
+
const solid = Array.from({ length: count }, () => "\u2501\u2501\u2501").join(" ");
|
|
3771
|
+
frames.push(` ${theme.brand(solid)}`);
|
|
3772
|
+
await renderFrames(frames, INTERVAL);
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
// src/index.ts
|
|
3776
|
+
var VERSION = true ? "1.3.0" : "0.0.0-dev";
|
|
3777
|
+
var NAME = true ? "codebrief" : "codebrief";
|
|
3778
|
+
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." : "";
|
|
3779
|
+
function printHelp() {
|
|
3780
|
+
console.log("");
|
|
3781
|
+
console.log(gradient(" codebrief ", [90, 130, 220], [137, 180, 250], theme.brandBold));
|
|
3782
|
+
console.log(theme.muted(" " + DESCRIPTION));
|
|
3783
|
+
console.log("");
|
|
3784
|
+
console.log(` ${theme.bold("Usage:")} npx ${NAME} [directory] [options]`);
|
|
3785
|
+
console.log("");
|
|
3786
|
+
console.log(` ${theme.bold("Options:")}`);
|
|
3787
|
+
console.log(` ${theme.accent("-h, --help")} Show this help message`);
|
|
3788
|
+
console.log(` ${theme.accent("-V, --version")} Show version number`);
|
|
3789
|
+
console.log(` ${theme.accent("--force")} Overwrite existing files without asking`);
|
|
3790
|
+
console.log(` ${theme.accent("--dry-run")} Preview what would be generated`);
|
|
3791
|
+
console.log(` ${theme.accent("--reconfigure")} Re-prompt even if .codebrief.json exists`);
|
|
3792
|
+
console.log(` ${theme.accent("--refresh-snapshot")} Re-scan source files, update code snapshot only`);
|
|
3793
|
+
console.log(` ${theme.accent("--check")} Exit 0 if snapshot is fresh, 1 if stale`);
|
|
3794
|
+
console.log(` ${theme.accent("--max-tokens=N")} Set the token budget for the code snapshot`);
|
|
3795
|
+
console.log(` ${theme.accent("--generate-skills")} Generate Claude Code skill files`);
|
|
3796
|
+
console.log(` ${theme.accent("-v, --verbose")} Show detailed progress output`);
|
|
3797
|
+
console.log("");
|
|
3798
|
+
console.log(` ${theme.bold("Examples:")}`);
|
|
3799
|
+
console.log(` ${theme.muted("$")} npx ${NAME} ${theme.muted("# analyze current directory")}`);
|
|
3800
|
+
console.log(` ${theme.muted("$")} npx ${NAME} ./my-project ${theme.muted("# analyze a specific project")}`);
|
|
3801
|
+
console.log(` ${theme.muted("$")} npx ${NAME} --dry-run ${theme.muted("# preview without writing files")}`);
|
|
3802
|
+
console.log(` ${theme.muted("$")} npx ${NAME} --refresh-snapshot ${theme.muted("# update code snapshot only")}`);
|
|
3803
|
+
console.log("");
|
|
3804
|
+
}
|
|
3404
3805
|
async function main() {
|
|
3405
3806
|
const startTime = performance.now();
|
|
3406
3807
|
const args = process.argv.slice(2);
|
|
3808
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3809
|
+
printHelp();
|
|
3810
|
+
process.exit(0);
|
|
3811
|
+
}
|
|
3812
|
+
if (args.includes("--version") || args.includes("-V")) {
|
|
3813
|
+
console.log(VERSION);
|
|
3814
|
+
process.exit(0);
|
|
3815
|
+
}
|
|
3407
3816
|
const force = args.includes("--force");
|
|
3408
3817
|
const dryRun = args.includes("--dry-run");
|
|
3409
3818
|
const refresh = args.includes("--refresh-snapshot");
|
|
@@ -3421,15 +3830,15 @@ async function main() {
|
|
|
3421
3830
|
)).some(Boolean);
|
|
3422
3831
|
if (!hasProjectMarker) {
|
|
3423
3832
|
console.log("");
|
|
3424
|
-
p4.intro(
|
|
3425
|
-
p4.log.error(`No project found at ${
|
|
3426
|
-
p4.log.info(`Run ${
|
|
3427
|
-
${
|
|
3833
|
+
p4.intro(theme.bold(" codebrief "));
|
|
3834
|
+
p4.log.error(`No project found at ${theme.accent(rootDir)}`);
|
|
3835
|
+
p4.log.info(`Run ${theme.bold("npx codebrief")} from a project directory, or pass a path:
|
|
3836
|
+
${theme.muted("npx codebrief ./my-project")}`);
|
|
3428
3837
|
p4.outro("");
|
|
3429
3838
|
process.exit(1);
|
|
3430
3839
|
}
|
|
3431
3840
|
const verboseLog = (msg) => {
|
|
3432
|
-
if (verbose) p4.log.info(
|
|
3841
|
+
if (verbose) p4.log.info(theme.muted(msg));
|
|
3433
3842
|
};
|
|
3434
3843
|
if (check) {
|
|
3435
3844
|
const config = await loadConfig(rootDir);
|
|
@@ -3447,16 +3856,17 @@ async function main() {
|
|
|
3447
3856
|
process.exit(0);
|
|
3448
3857
|
}
|
|
3449
3858
|
console.log("");
|
|
3450
|
-
p4.intro(
|
|
3859
|
+
p4.intro(gradient(" codebrief ", [90, 130, 220], [137, 180, 250], theme.brandBold));
|
|
3860
|
+
p4.log.info(theme.muted("code analysis for AI context"));
|
|
3451
3861
|
if (refresh) {
|
|
3452
3862
|
await refreshSnapshot(rootDir);
|
|
3453
|
-
p4.outro(
|
|
3863
|
+
p4.outro(theme.success("Snapshot refreshed!"));
|
|
3454
3864
|
return;
|
|
3455
3865
|
}
|
|
3456
3866
|
if (dryRun) {
|
|
3457
|
-
p4.log.warn(
|
|
3867
|
+
p4.log.warn(theme.warn("DRY RUN: no files will be written"));
|
|
3458
3868
|
}
|
|
3459
|
-
p4.log.info(`Analyzing ${
|
|
3869
|
+
p4.log.info(`Analyzing ${theme.accent(rootDir)}`);
|
|
3460
3870
|
const spinner3 = p4.spinner();
|
|
3461
3871
|
const spinnerProgress = (msg) => spinner3.message(msg);
|
|
3462
3872
|
spinner3.start("Detecting tech stack...");
|
|
@@ -3468,6 +3878,7 @@ async function main() {
|
|
|
3468
3878
|
spinner3.stop(
|
|
3469
3879
|
`Import graph: ${graph.edges.length} edges, ${graph.externalImportCounts.size} packages.` + (topHub ? ` Top hub: ${topHub.path}` : "")
|
|
3470
3880
|
);
|
|
3881
|
+
await animateGraphBuild(detected.sourceFileCount, graph.edges.length);
|
|
3471
3882
|
detected.frameworks = enrichFrameworksWithUsage(
|
|
3472
3883
|
detected.frameworks,
|
|
3473
3884
|
graph.externalImportCounts
|
|
@@ -3475,64 +3886,139 @@ async function main() {
|
|
|
3475
3886
|
{
|
|
3476
3887
|
const lines = [];
|
|
3477
3888
|
const lang = detected.hasTypeScript ? "TypeScript" : detected.language !== "other" ? detected.language.charAt(0).toUpperCase() + detected.language.slice(1) : "";
|
|
3478
|
-
if (lang) lines.push(` Language
|
|
3889
|
+
if (lang) lines.push(` ${"Language"} ${theme.accentBold(lang)}`);
|
|
3479
3890
|
if (detected.frameworks.length > 0) {
|
|
3480
|
-
lines.push(` Frameworks
|
|
3891
|
+
lines.push(` ${"Frameworks"} ${theme.accentBold(detected.frameworks.map((f) => f.name).join(", "))}`);
|
|
3481
3892
|
}
|
|
3482
3893
|
if (detected.linter !== "none") {
|
|
3483
|
-
lines.push(` Linter
|
|
3894
|
+
lines.push(` ${"Linter"} ${theme.accentBold(detected.linter.charAt(0).toUpperCase() + detected.linter.slice(1))}`);
|
|
3484
3895
|
}
|
|
3485
3896
|
if (detected.packageManager !== "none") {
|
|
3486
|
-
lines.push(` Pkg mgr
|
|
3897
|
+
lines.push(` ${"Pkg mgr"} ${theme.accentBold(detected.packageManager)}`);
|
|
3487
3898
|
}
|
|
3488
3899
|
if (detected.testFramework) {
|
|
3489
|
-
lines.push(` Testing
|
|
3900
|
+
lines.push(` ${"Testing"} ${theme.accentBold(detected.testFramework)}`);
|
|
3490
3901
|
}
|
|
3491
3902
|
if (detected.ciProvider) {
|
|
3492
|
-
lines.push(` CI
|
|
3903
|
+
lines.push(` ${"CI"} ${theme.accentBold(detected.ciProvider)}`);
|
|
3493
3904
|
}
|
|
3494
3905
|
if (detected.monorepo) {
|
|
3495
|
-
lines.push(` Monorepo
|
|
3906
|
+
lines.push(` ${"Monorepo"} ${theme.accentBold(`${detected.monorepo.type} (${detected.monorepo.packages.length} package${detected.monorepo.packages.length === 1 ? "" : "s"})`)}`);
|
|
3496
3907
|
}
|
|
3497
3908
|
if (detected.sourceFileCount > 0) {
|
|
3498
|
-
lines.push(` Files
|
|
3909
|
+
lines.push(` ${"Files"} ${theme.accentBold(`${detected.sourceFileCount}`)} ${theme.muted(`(${formatBytes(detected.totalSourceBytes)})`)}`);
|
|
3499
3910
|
}
|
|
3500
3911
|
if (lines.length > 0) {
|
|
3501
3912
|
p4.note(lines.join("\n"), "Detected Stack");
|
|
3502
3913
|
}
|
|
3503
3914
|
}
|
|
3504
3915
|
const fileCount = graph.centrality.size;
|
|
3505
|
-
|
|
3916
|
+
await animatePageRank();
|
|
3506
3917
|
const hubFiles = getHubFiles(graph);
|
|
3507
|
-
|
|
3508
|
-
|
|
3918
|
+
const topHubName = hubFiles[0]?.path ?? "";
|
|
3919
|
+
p4.log.step(
|
|
3920
|
+
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")}`
|
|
3921
|
+
);
|
|
3922
|
+
if (verbose && hubFiles.length > 0) {
|
|
3923
|
+
for (const h of hubFiles.slice(0, 5)) {
|
|
3924
|
+
p4.log.info(theme.muted(` ${h.path} (centrality: ${h.centrality.toFixed(3)}, imported by ${h.importedBy})`));
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3509
3927
|
const circularDeps = findCircularDeps(graph);
|
|
3510
|
-
|
|
3511
|
-
|
|
3928
|
+
await animateCycleDetection(circularDeps.length);
|
|
3929
|
+
p4.log.step(
|
|
3930
|
+
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`
|
|
3931
|
+
);
|
|
3932
|
+
if (verbose && circularDeps.length > 0) {
|
|
3933
|
+
for (const c of circularDeps.slice(0, 3)) {
|
|
3934
|
+
p4.log.info(theme.muted(` ${c.chain.join(" \u2192 ")}`));
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3512
3937
|
const { layers, layerEdges } = detectArchitecturalLayers(graph);
|
|
3513
|
-
|
|
3514
|
-
|
|
3938
|
+
await animateLayerStack(layers.map((l) => l.name));
|
|
3939
|
+
p4.log.step(
|
|
3940
|
+
layers.length > 0 ? `${theme.brand("Layers")} ${layers.map((l) => l.name).join(" \u2192 ")}` : `${theme.brand("Layers")} ${theme.muted("no clear layers detected")}`
|
|
3941
|
+
);
|
|
3942
|
+
if (verbose && layers.length > 0) {
|
|
3943
|
+
for (const l of layers) {
|
|
3944
|
+
p4.log.info(theme.muted(` ${l.name}: ${l.files.length} files, depends on: ${l.dependsOn.join(", ") || "none"}`));
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3515
3947
|
const instabilities = computeInstability(graph);
|
|
3516
|
-
|
|
3517
|
-
|
|
3948
|
+
const highInstability = instabilities.filter((f) => f.instability > 0.8);
|
|
3949
|
+
p4.log.step(
|
|
3950
|
+
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()}`
|
|
3951
|
+
);
|
|
3952
|
+
if (verbose && highInstability.length > 0) {
|
|
3953
|
+
for (const f of highInstability.slice(0, 5)) {
|
|
3954
|
+
p4.log.info(theme.muted(` ${f.path} (I=${f.instability.toFixed(2)}, fan-in=${f.fanIn}, fan-out=${f.fanOut})`));
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3518
3957
|
const communities = detectCommunities(graph);
|
|
3519
|
-
|
|
3520
|
-
|
|
3958
|
+
await animateCommunities(communities.length);
|
|
3959
|
+
p4.log.step(
|
|
3960
|
+
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")}`
|
|
3961
|
+
);
|
|
3962
|
+
if (verbose && communities.length > 0) {
|
|
3963
|
+
for (const c of communities.slice(0, 5)) {
|
|
3964
|
+
p4.log.info(theme.muted(` ${c.label} (${c.files.length} files)`));
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3521
3967
|
const exportCoverage = computeExportCoverage(graph);
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3968
|
+
{
|
|
3969
|
+
const totalExp = exportCoverage.reduce((s, e) => s + e.totalExports, 0);
|
|
3970
|
+
const totalUsed = exportCoverage.reduce((s, e) => s + e.usedExports, 0);
|
|
3971
|
+
const unusedCount = totalExp - totalUsed;
|
|
3972
|
+
const filesWithUnused = exportCoverage.filter((e) => e.usedExports < e.totalExports).length;
|
|
3973
|
+
p4.log.step(
|
|
3974
|
+
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()}`
|
|
3975
|
+
);
|
|
3976
|
+
if (verbose && unusedCount > 0) {
|
|
3977
|
+
for (const e of exportCoverage.filter((e2) => e2.usedExports < e2.totalExports).slice(0, 5)) {
|
|
3978
|
+
p4.log.info(theme.muted(` ${e.file}: ${e.totalExports - e.usedExports} unused of ${e.totalExports}`));
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
const noopProgress = () => {
|
|
3983
|
+
};
|
|
3984
|
+
const gitActivity = detected.isGitRepo ? await analyzeGitActivity(rootDir, verbose ? verboseLog : noopProgress) : null;
|
|
3985
|
+
if (gitActivity) {
|
|
3986
|
+
const coupledPairs = gitActivity.changeCoupling.length;
|
|
3987
|
+
p4.log.step(
|
|
3988
|
+
`${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"}`
|
|
3989
|
+
);
|
|
3990
|
+
if (verbose) {
|
|
3991
|
+
for (const h of gitActivity.hotFiles.slice(0, 5)) {
|
|
3992
|
+
p4.log.info(theme.muted(` ${h.path} (${h.commits} commits, last: ${h.lastChanged})`));
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
} else {
|
|
3996
|
+
p4.log.step(`${theme.brand("Git")} ${theme.muted("not a git repo, skipped")}`);
|
|
3997
|
+
}
|
|
3526
3998
|
const analysis = { hubFiles, circularDeps, layers, layerEdges, gitActivity, instabilities, communities, exportCoverage };
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3999
|
+
{
|
|
4000
|
+
const reportLines = [];
|
|
4001
|
+
reportLines.push(` ${"Files analyzed"} ${theme.brandBold(String(fileCount))}`);
|
|
4002
|
+
reportLines.push(` ${"Import edges"} ${theme.brandBold(String(graph.edges.length))}`);
|
|
4003
|
+
reportLines.push(` ${"External pkgs"} ${theme.brandBold(String(graph.externalImportCounts.size))}`);
|
|
4004
|
+
if (hubFiles.length > 0) {
|
|
4005
|
+
reportLines.push(` ${"Hub files"} ${theme.brandBold(String(hubFiles.length))}` + (hubFiles[0] ? ` ${theme.muted(`(most connected: ${hubFiles[0].path})`)}` : ""));
|
|
4006
|
+
}
|
|
4007
|
+
if (layers.length > 0) {
|
|
4008
|
+
reportLines.push(` ${"Architecture"} ${theme.accentBold(layers.map((l) => l.name).join(" \u2192 "))}`);
|
|
4009
|
+
}
|
|
4010
|
+
reportLines.push(` ${"Circular deps"} ${circularDeps.length === 0 ? theme.success("none") : theme.warn(`${circularDeps.length} chain${circularDeps.length === 1 ? "" : "s"}`)}`);
|
|
4011
|
+
if (gitActivity) {
|
|
4012
|
+
reportLines.push(` ${"Hot files (90d)"} ${theme.brandBold(String(gitActivity.hotFiles.length))}`);
|
|
4013
|
+
}
|
|
4014
|
+
p4.note(reportLines.join("\n"), "Analysis Report");
|
|
4015
|
+
if (circularDeps.length > 0) {
|
|
4016
|
+
for (const c of circularDeps.slice(0, 2)) {
|
|
4017
|
+
const shortChain = c.chain.map((f) => f.split("/").pop() ?? f);
|
|
4018
|
+
p4.log.warn(theme.warn(`Cycle: ${shortChain.join(" \u2192 ")}`));
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
3536
4022
|
const savedConfig = await loadConfig(rootDir);
|
|
3537
4023
|
if (savedConfig?.snapshotHash) {
|
|
3538
4024
|
const currentHash = await computeSnapshotHash(rootDir, detected.language);
|
|
@@ -3541,8 +4027,8 @@ async function main() {
|
|
|
3541
4027
|
(Date.now() - savedConfig.snapshotGeneratedAt) / (1e3 * 60 * 60 * 24)
|
|
3542
4028
|
);
|
|
3543
4029
|
p4.log.warn(
|
|
3544
|
-
|
|
3545
|
-
`Code snapshot may be stale (source files changed${daysSince > 0 ? `, last generated ${daysSince}d ago` : ""}). Run with ${
|
|
4030
|
+
theme.warn(
|
|
4031
|
+
`Code snapshot may be stale (source files changed${daysSince > 0 ? `, last generated ${daysSince}d ago` : ""}). Run with ${theme.bold("--refresh-snapshot")} to update.`
|
|
3546
4032
|
)
|
|
3547
4033
|
);
|
|
3548
4034
|
}
|
|
@@ -3550,18 +4036,18 @@ async function main() {
|
|
|
3550
4036
|
let answers;
|
|
3551
4037
|
if (savedConfig && !reconfigure) {
|
|
3552
4038
|
p4.log.info(
|
|
3553
|
-
`Using saved config from ${
|
|
4039
|
+
`Using saved config from ${theme.accent(".codebrief.json")} ` + theme.muted("(run with --reconfigure to change)")
|
|
3554
4040
|
);
|
|
3555
4041
|
answers = configToAnswers(savedConfig);
|
|
3556
4042
|
if (detected.monorepo && detected.monorepo.packages.length > 0 && !savedConfig.generatePerPackage) {
|
|
3557
4043
|
}
|
|
3558
4044
|
} else {
|
|
3559
|
-
answers = await runPrompts(detected, reconfigure ? savedConfig : null);
|
|
4045
|
+
answers = await runPrompts(detected, reconfigure ? savedConfig : null, reconfigure);
|
|
3560
4046
|
if (!dryRun) {
|
|
3561
4047
|
const hash = await computeSnapshotHash(rootDir, detected.language);
|
|
3562
4048
|
await saveConfig(rootDir, answers, hash, detected.language);
|
|
3563
4049
|
p4.log.info(
|
|
3564
|
-
|
|
4050
|
+
theme.muted("Saved config to .codebrief.json for future runs.")
|
|
3565
4051
|
);
|
|
3566
4052
|
}
|
|
3567
4053
|
}
|
|
@@ -3581,7 +4067,7 @@ async function main() {
|
|
|
3581
4067
|
spinner3.start(
|
|
3582
4068
|
dryRun ? "Preparing context files..." : "Generating context files..."
|
|
3583
4069
|
);
|
|
3584
|
-
const shouldGenerateSkills = generateSkills || answers.
|
|
4070
|
+
const shouldGenerateSkills = generateSkills || answers.ides.includes("claude");
|
|
3585
4071
|
const files = await generateFiles(detected, answers, snapshot, force, dryRun, analysis, shouldGenerateSkills, verbose ? verboseLog : void 0);
|
|
3586
4072
|
spinner3.stop(
|
|
3587
4073
|
dryRun ? `Would generate ${files.length} file${files.length === 1 ? "" : "s"}.` : `Generated ${files.length} file${files.length === 1 ? "" : "s"}.`
|
|
@@ -3594,18 +4080,18 @@ async function main() {
|
|
|
3594
4080
|
const elapsed = ((performance.now() - startTime) / 1e3).toFixed(1);
|
|
3595
4081
|
if (dryRun) {
|
|
3596
4082
|
p4.outro(
|
|
3597
|
-
|
|
4083
|
+
theme.warn("DRY RUN complete. ") + theme.muted(`no files were written. Remove --dry-run to generate. (${elapsed}s)`)
|
|
3598
4084
|
);
|
|
3599
4085
|
return;
|
|
3600
4086
|
}
|
|
3601
4087
|
p4.outro(
|
|
3602
|
-
|
|
3603
|
-
"Your context files are ready. They are living documents
|
|
4088
|
+
theme.success(`Done in ${elapsed}s! `) + theme.muted(
|
|
4089
|
+
"Your context files are ready. They are living documents: keep them up to date as your project evolves."
|
|
3604
4090
|
)
|
|
3605
4091
|
);
|
|
3606
4092
|
}
|
|
3607
4093
|
main().catch((err) => {
|
|
3608
|
-
console.error(
|
|
4094
|
+
console.error(theme.error("Fatal error:"), err);
|
|
3609
4095
|
process.exit(1);
|
|
3610
4096
|
});
|
|
3611
4097
|
//# sourceMappingURL=index.js.map
|