codebrief 1.2.0 → 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 +706 -289
- 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 ?? "",
|
|
@@ -3125,7 +3418,7 @@ async function saveConfig(rootDir, answers, snapshotHash, language) {
|
|
|
3125
3418
|
const configPath = path6.join(rootDir, CONFIG_FILENAME);
|
|
3126
3419
|
const cfg = {
|
|
3127
3420
|
_version: CONFIG_VERSION,
|
|
3128
|
-
|
|
3421
|
+
ides: answers.ides,
|
|
3129
3422
|
projectPurpose: answers.projectPurpose,
|
|
3130
3423
|
keyPatterns: answers.keyPatterns,
|
|
3131
3424
|
gotchas: answers.gotchas,
|
|
@@ -3140,7 +3433,7 @@ async function saveConfig(rootDir, answers, snapshotHash, language) {
|
|
|
3140
3433
|
}
|
|
3141
3434
|
function configToAnswers(config) {
|
|
3142
3435
|
return {
|
|
3143
|
-
|
|
3436
|
+
ides: config.ides,
|
|
3144
3437
|
projectPurpose: config.projectPurpose,
|
|
3145
3438
|
keyPatterns: config.keyPatterns,
|
|
3146
3439
|
gotchas: config.gotchas,
|
|
@@ -3182,7 +3475,6 @@ async function computeSnapshotHash(rootDir, language) {
|
|
|
3182
3475
|
// src/refresh.ts
|
|
3183
3476
|
import path7 from "path";
|
|
3184
3477
|
import * as p3 from "@clack/prompts";
|
|
3185
|
-
import pc2 from "picocolors";
|
|
3186
3478
|
init_utils();
|
|
3187
3479
|
var CONTEXT_FILES = [
|
|
3188
3480
|
"CLAUDE.md",
|
|
@@ -3218,7 +3510,7 @@ async function refreshSnapshot(rootDir) {
|
|
|
3218
3510
|
const found = await findContextFile(rootDir);
|
|
3219
3511
|
if (!found) {
|
|
3220
3512
|
p3.log.error(
|
|
3221
|
-
"No context file found. Run " +
|
|
3513
|
+
"No context file found. Run " + theme.accent("codebrief") + " first to generate one."
|
|
3222
3514
|
);
|
|
3223
3515
|
process.exit(1);
|
|
3224
3516
|
}
|
|
@@ -3243,7 +3535,7 @@ async function refreshSnapshot(rootDir) {
|
|
|
3243
3535
|
process.exit(1);
|
|
3244
3536
|
}
|
|
3245
3537
|
}
|
|
3246
|
-
p3.log.info(`Refreshing snapshot in ${
|
|
3538
|
+
p3.log.info(`Refreshing snapshot in ${theme.accent(found.path)}`);
|
|
3247
3539
|
spinner3.start("Scanning source files...");
|
|
3248
3540
|
const progress = (msg) => spinner3.message(msg);
|
|
3249
3541
|
const detected = await detectContext(rootDir, progress);
|
|
@@ -3255,7 +3547,7 @@ async function refreshSnapshot(rootDir) {
|
|
|
3255
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."
|
|
3256
3548
|
);
|
|
3257
3549
|
if (snapshot.entries.length === 0) {
|
|
3258
|
-
p3.log.warn("No types found
|
|
3550
|
+
p3.log.warn("No types found. Snapshot section will be empty.");
|
|
3259
3551
|
}
|
|
3260
3552
|
let updated;
|
|
3261
3553
|
if (found.isAider) {
|
|
@@ -3293,7 +3585,7 @@ async function refreshSnapshot(rootDir) {
|
|
|
3293
3585
|
updated = content.slice(0, startIdx) + newBlock + content.slice(endIdx);
|
|
3294
3586
|
}
|
|
3295
3587
|
await writeFileSafe(absPath, updated);
|
|
3296
|
-
p3.log.success(`Updated snapshot in ${
|
|
3588
|
+
p3.log.success(`Updated snapshot in ${theme.accent(found.path)}`);
|
|
3297
3589
|
}
|
|
3298
3590
|
|
|
3299
3591
|
// src/git-analysis.ts
|
|
@@ -3394,9 +3686,133 @@ function analyzeChangeCoupling(rootDir) {
|
|
|
3394
3686
|
|
|
3395
3687
|
// src/index.ts
|
|
3396
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
|
+
}
|
|
3397
3805
|
async function main() {
|
|
3398
3806
|
const startTime = performance.now();
|
|
3399
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
|
+
}
|
|
3400
3816
|
const force = args.includes("--force");
|
|
3401
3817
|
const dryRun = args.includes("--dry-run");
|
|
3402
3818
|
const refresh = args.includes("--refresh-snapshot");
|
|
@@ -3414,15 +3830,15 @@ async function main() {
|
|
|
3414
3830
|
)).some(Boolean);
|
|
3415
3831
|
if (!hasProjectMarker) {
|
|
3416
3832
|
console.log("");
|
|
3417
|
-
p4.intro(
|
|
3418
|
-
p4.log.error(`No project found at ${
|
|
3419
|
-
p4.log.info(`Run ${
|
|
3420
|
-
${
|
|
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")}`);
|
|
3421
3837
|
p4.outro("");
|
|
3422
3838
|
process.exit(1);
|
|
3423
3839
|
}
|
|
3424
3840
|
const verboseLog = (msg) => {
|
|
3425
|
-
if (verbose) p4.log.info(
|
|
3841
|
+
if (verbose) p4.log.info(theme.muted(msg));
|
|
3426
3842
|
};
|
|
3427
3843
|
if (check) {
|
|
3428
3844
|
const config = await loadConfig(rootDir);
|
|
@@ -3440,16 +3856,17 @@ async function main() {
|
|
|
3440
3856
|
process.exit(0);
|
|
3441
3857
|
}
|
|
3442
3858
|
console.log("");
|
|
3443
|
-
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"));
|
|
3444
3861
|
if (refresh) {
|
|
3445
3862
|
await refreshSnapshot(rootDir);
|
|
3446
|
-
p4.outro(
|
|
3863
|
+
p4.outro(theme.success("Snapshot refreshed!"));
|
|
3447
3864
|
return;
|
|
3448
3865
|
}
|
|
3449
3866
|
if (dryRun) {
|
|
3450
|
-
p4.log.warn(
|
|
3867
|
+
p4.log.warn(theme.warn("DRY RUN: no files will be written"));
|
|
3451
3868
|
}
|
|
3452
|
-
p4.log.info(`Analyzing ${
|
|
3869
|
+
p4.log.info(`Analyzing ${theme.accent(rootDir)}`);
|
|
3453
3870
|
const spinner3 = p4.spinner();
|
|
3454
3871
|
const spinnerProgress = (msg) => spinner3.message(msg);
|
|
3455
3872
|
spinner3.start("Detecting tech stack...");
|
|
@@ -3461,6 +3878,7 @@ async function main() {
|
|
|
3461
3878
|
spinner3.stop(
|
|
3462
3879
|
`Import graph: ${graph.edges.length} edges, ${graph.externalImportCounts.size} packages.` + (topHub ? ` Top hub: ${topHub.path}` : "")
|
|
3463
3880
|
);
|
|
3881
|
+
await animateGraphBuild(detected.sourceFileCount, graph.edges.length);
|
|
3464
3882
|
detected.frameworks = enrichFrameworksWithUsage(
|
|
3465
3883
|
detected.frameworks,
|
|
3466
3884
|
graph.externalImportCounts
|
|
@@ -3468,137 +3886,136 @@ async function main() {
|
|
|
3468
3886
|
{
|
|
3469
3887
|
const lines = [];
|
|
3470
3888
|
const lang = detected.hasTypeScript ? "TypeScript" : detected.language !== "other" ? detected.language.charAt(0).toUpperCase() + detected.language.slice(1) : "";
|
|
3471
|
-
if (lang) lines.push(` Language
|
|
3889
|
+
if (lang) lines.push(` ${"Language"} ${theme.accentBold(lang)}`);
|
|
3472
3890
|
if (detected.frameworks.length > 0) {
|
|
3473
|
-
lines.push(` Frameworks
|
|
3891
|
+
lines.push(` ${"Frameworks"} ${theme.accentBold(detected.frameworks.map((f) => f.name).join(", "))}`);
|
|
3474
3892
|
}
|
|
3475
3893
|
if (detected.linter !== "none") {
|
|
3476
|
-
lines.push(` Linter
|
|
3894
|
+
lines.push(` ${"Linter"} ${theme.accentBold(detected.linter.charAt(0).toUpperCase() + detected.linter.slice(1))}`);
|
|
3477
3895
|
}
|
|
3478
3896
|
if (detected.packageManager !== "none") {
|
|
3479
|
-
lines.push(` Pkg mgr
|
|
3897
|
+
lines.push(` ${"Pkg mgr"} ${theme.accentBold(detected.packageManager)}`);
|
|
3480
3898
|
}
|
|
3481
3899
|
if (detected.testFramework) {
|
|
3482
|
-
lines.push(` Testing
|
|
3900
|
+
lines.push(` ${"Testing"} ${theme.accentBold(detected.testFramework)}`);
|
|
3483
3901
|
}
|
|
3484
3902
|
if (detected.ciProvider) {
|
|
3485
|
-
lines.push(` CI
|
|
3903
|
+
lines.push(` ${"CI"} ${theme.accentBold(detected.ciProvider)}`);
|
|
3486
3904
|
}
|
|
3487
3905
|
if (detected.monorepo) {
|
|
3488
|
-
lines.push(` Monorepo
|
|
3906
|
+
lines.push(` ${"Monorepo"} ${theme.accentBold(`${detected.monorepo.type} (${detected.monorepo.packages.length} package${detected.monorepo.packages.length === 1 ? "" : "s"})`)}`);
|
|
3489
3907
|
}
|
|
3490
3908
|
if (detected.sourceFileCount > 0) {
|
|
3491
|
-
lines.push(` Files
|
|
3909
|
+
lines.push(` ${"Files"} ${theme.accentBold(`${detected.sourceFileCount}`)} ${theme.muted(`(${formatBytes(detected.totalSourceBytes)})`)}`);
|
|
3492
3910
|
}
|
|
3493
3911
|
if (lines.length > 0) {
|
|
3494
3912
|
p4.note(lines.join("\n"), "Detected Stack");
|
|
3495
3913
|
}
|
|
3496
3914
|
}
|
|
3497
3915
|
const fileCount = graph.centrality.size;
|
|
3498
|
-
|
|
3916
|
+
await animatePageRank();
|
|
3499
3917
|
const hubFiles = getHubFiles(graph);
|
|
3500
3918
|
const topHubName = hubFiles[0]?.path ?? "";
|
|
3501
|
-
|
|
3502
|
-
hubFiles.length > 0 ? `${
|
|
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")}`
|
|
3503
3921
|
);
|
|
3504
3922
|
if (verbose && hubFiles.length > 0) {
|
|
3505
3923
|
for (const h of hubFiles.slice(0, 5)) {
|
|
3506
|
-
p4.log.info(
|
|
3924
|
+
p4.log.info(theme.muted(` ${h.path} (centrality: ${h.centrality.toFixed(3)}, imported by ${h.importedBy})`));
|
|
3507
3925
|
}
|
|
3508
3926
|
}
|
|
3509
|
-
spinner3.start("Finding circular dependencies...");
|
|
3510
3927
|
const circularDeps = findCircularDeps(graph);
|
|
3511
|
-
|
|
3512
|
-
|
|
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`
|
|
3513
3931
|
);
|
|
3514
3932
|
if (verbose && circularDeps.length > 0) {
|
|
3515
3933
|
for (const c of circularDeps.slice(0, 3)) {
|
|
3516
|
-
p4.log.info(
|
|
3934
|
+
p4.log.info(theme.muted(` ${c.chain.join(" \u2192 ")}`));
|
|
3517
3935
|
}
|
|
3518
3936
|
}
|
|
3519
|
-
spinner3.start("Detecting architecture layers...");
|
|
3520
3937
|
const { layers, layerEdges } = detectArchitecturalLayers(graph);
|
|
3521
|
-
|
|
3522
|
-
|
|
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")}`
|
|
3523
3941
|
);
|
|
3524
3942
|
if (verbose && layers.length > 0) {
|
|
3525
3943
|
for (const l of layers) {
|
|
3526
|
-
p4.log.info(
|
|
3944
|
+
p4.log.info(theme.muted(` ${l.name}: ${l.files.length} files, depends on: ${l.dependsOn.join(", ") || "none"}`));
|
|
3527
3945
|
}
|
|
3528
3946
|
}
|
|
3529
|
-
spinner3.start("Computing instability metrics...");
|
|
3530
3947
|
const instabilities = computeInstability(graph);
|
|
3531
3948
|
const highInstability = instabilities.filter((f) => f.instability > 0.8);
|
|
3532
|
-
|
|
3533
|
-
highInstability.length > 0 ? `${
|
|
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()}`
|
|
3534
3951
|
);
|
|
3535
3952
|
if (verbose && highInstability.length > 0) {
|
|
3536
3953
|
for (const f of highInstability.slice(0, 5)) {
|
|
3537
|
-
p4.log.info(
|
|
3954
|
+
p4.log.info(theme.muted(` ${f.path} (I=${f.instability.toFixed(2)}, fan-in=${f.fanIn}, fan-out=${f.fanOut})`));
|
|
3538
3955
|
}
|
|
3539
3956
|
}
|
|
3540
|
-
spinner3.start("Detecting module communities...");
|
|
3541
3957
|
const communities = detectCommunities(graph);
|
|
3542
|
-
|
|
3543
|
-
|
|
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")}`
|
|
3544
3961
|
);
|
|
3545
3962
|
if (verbose && communities.length > 0) {
|
|
3546
3963
|
for (const c of communities.slice(0, 5)) {
|
|
3547
|
-
p4.log.info(
|
|
3964
|
+
p4.log.info(theme.muted(` ${c.label} (${c.files.length} files)`));
|
|
3548
3965
|
}
|
|
3549
3966
|
}
|
|
3550
|
-
spinner3.start("Computing export coverage...");
|
|
3551
3967
|
const exportCoverage = computeExportCoverage(graph);
|
|
3552
3968
|
{
|
|
3553
3969
|
const totalExp = exportCoverage.reduce((s, e) => s + e.totalExports, 0);
|
|
3554
3970
|
const totalUsed = exportCoverage.reduce((s, e) => s + e.usedExports, 0);
|
|
3555
3971
|
const unusedCount = totalExp - totalUsed;
|
|
3556
3972
|
const filesWithUnused = exportCoverage.filter((e) => e.usedExports < e.totalExports).length;
|
|
3557
|
-
|
|
3558
|
-
unusedCount > 0 ? `${
|
|
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()}`
|
|
3559
3975
|
);
|
|
3560
3976
|
if (verbose && unusedCount > 0) {
|
|
3561
3977
|
for (const e of exportCoverage.filter((e2) => e2.usedExports < e2.totalExports).slice(0, 5)) {
|
|
3562
|
-
p4.log.info(
|
|
3978
|
+
p4.log.info(theme.muted(` ${e.file}: ${e.totalExports - e.usedExports} unused of ${e.totalExports}`));
|
|
3563
3979
|
}
|
|
3564
3980
|
}
|
|
3565
3981
|
}
|
|
3566
|
-
|
|
3567
|
-
|
|
3982
|
+
const noopProgress = () => {
|
|
3983
|
+
};
|
|
3984
|
+
const gitActivity = detected.isGitRepo ? await analyzeGitActivity(rootDir, verbose ? verboseLog : noopProgress) : null;
|
|
3568
3985
|
if (gitActivity) {
|
|
3569
3986
|
const coupledPairs = gitActivity.changeCoupling.length;
|
|
3570
|
-
|
|
3571
|
-
`${
|
|
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"}`
|
|
3572
3989
|
);
|
|
3573
3990
|
if (verbose) {
|
|
3574
3991
|
for (const h of gitActivity.hotFiles.slice(0, 5)) {
|
|
3575
|
-
p4.log.info(
|
|
3992
|
+
p4.log.info(theme.muted(` ${h.path} (${h.commits} commits, last: ${h.lastChanged})`));
|
|
3576
3993
|
}
|
|
3577
3994
|
}
|
|
3578
3995
|
} else {
|
|
3579
|
-
|
|
3996
|
+
p4.log.step(`${theme.brand("Git")} ${theme.muted("not a git repo, skipped")}`);
|
|
3580
3997
|
}
|
|
3581
3998
|
const analysis = { hubFiles, circularDeps, layers, layerEdges, gitActivity, instabilities, communities, exportCoverage };
|
|
3582
3999
|
{
|
|
3583
4000
|
const reportLines = [];
|
|
3584
|
-
reportLines.push(` Files analyzed
|
|
3585
|
-
reportLines.push(` Import edges
|
|
3586
|
-
reportLines.push(` External pkgs
|
|
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))}`);
|
|
3587
4004
|
if (hubFiles.length > 0) {
|
|
3588
|
-
reportLines.push(` Hub files
|
|
4005
|
+
reportLines.push(` ${"Hub files"} ${theme.brandBold(String(hubFiles.length))}` + (hubFiles[0] ? ` ${theme.muted(`(most connected: ${hubFiles[0].path})`)}` : ""));
|
|
3589
4006
|
}
|
|
3590
4007
|
if (layers.length > 0) {
|
|
3591
|
-
reportLines.push(` Architecture
|
|
4008
|
+
reportLines.push(` ${"Architecture"} ${theme.accentBold(layers.map((l) => l.name).join(" \u2192 "))}`);
|
|
3592
4009
|
}
|
|
3593
|
-
reportLines.push(` Circular deps
|
|
4010
|
+
reportLines.push(` ${"Circular deps"} ${circularDeps.length === 0 ? theme.success("none") : theme.warn(`${circularDeps.length} chain${circularDeps.length === 1 ? "" : "s"}`)}`);
|
|
3594
4011
|
if (gitActivity) {
|
|
3595
|
-
reportLines.push(` Hot files (90d)
|
|
4012
|
+
reportLines.push(` ${"Hot files (90d)"} ${theme.brandBold(String(gitActivity.hotFiles.length))}`);
|
|
3596
4013
|
}
|
|
3597
4014
|
p4.note(reportLines.join("\n"), "Analysis Report");
|
|
3598
4015
|
if (circularDeps.length > 0) {
|
|
3599
4016
|
for (const c of circularDeps.slice(0, 2)) {
|
|
3600
4017
|
const shortChain = c.chain.map((f) => f.split("/").pop() ?? f);
|
|
3601
|
-
p4.log.warn(
|
|
4018
|
+
p4.log.warn(theme.warn(`Cycle: ${shortChain.join(" \u2192 ")}`));
|
|
3602
4019
|
}
|
|
3603
4020
|
}
|
|
3604
4021
|
}
|
|
@@ -3610,8 +4027,8 @@ async function main() {
|
|
|
3610
4027
|
(Date.now() - savedConfig.snapshotGeneratedAt) / (1e3 * 60 * 60 * 24)
|
|
3611
4028
|
);
|
|
3612
4029
|
p4.log.warn(
|
|
3613
|
-
|
|
3614
|
-
`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.`
|
|
3615
4032
|
)
|
|
3616
4033
|
);
|
|
3617
4034
|
}
|
|
@@ -3619,18 +4036,18 @@ async function main() {
|
|
|
3619
4036
|
let answers;
|
|
3620
4037
|
if (savedConfig && !reconfigure) {
|
|
3621
4038
|
p4.log.info(
|
|
3622
|
-
`Using saved config from ${
|
|
4039
|
+
`Using saved config from ${theme.accent(".codebrief.json")} ` + theme.muted("(run with --reconfigure to change)")
|
|
3623
4040
|
);
|
|
3624
4041
|
answers = configToAnswers(savedConfig);
|
|
3625
4042
|
if (detected.monorepo && detected.monorepo.packages.length > 0 && !savedConfig.generatePerPackage) {
|
|
3626
4043
|
}
|
|
3627
4044
|
} else {
|
|
3628
|
-
answers = await runPrompts(detected, reconfigure ? savedConfig : null);
|
|
4045
|
+
answers = await runPrompts(detected, reconfigure ? savedConfig : null, reconfigure);
|
|
3629
4046
|
if (!dryRun) {
|
|
3630
4047
|
const hash = await computeSnapshotHash(rootDir, detected.language);
|
|
3631
4048
|
await saveConfig(rootDir, answers, hash, detected.language);
|
|
3632
4049
|
p4.log.info(
|
|
3633
|
-
|
|
4050
|
+
theme.muted("Saved config to .codebrief.json for future runs.")
|
|
3634
4051
|
);
|
|
3635
4052
|
}
|
|
3636
4053
|
}
|
|
@@ -3650,7 +4067,7 @@ async function main() {
|
|
|
3650
4067
|
spinner3.start(
|
|
3651
4068
|
dryRun ? "Preparing context files..." : "Generating context files..."
|
|
3652
4069
|
);
|
|
3653
|
-
const shouldGenerateSkills = generateSkills || answers.
|
|
4070
|
+
const shouldGenerateSkills = generateSkills || answers.ides.includes("claude");
|
|
3654
4071
|
const files = await generateFiles(detected, answers, snapshot, force, dryRun, analysis, shouldGenerateSkills, verbose ? verboseLog : void 0);
|
|
3655
4072
|
spinner3.stop(
|
|
3656
4073
|
dryRun ? `Would generate ${files.length} file${files.length === 1 ? "" : "s"}.` : `Generated ${files.length} file${files.length === 1 ? "" : "s"}.`
|
|
@@ -3663,18 +4080,18 @@ async function main() {
|
|
|
3663
4080
|
const elapsed = ((performance.now() - startTime) / 1e3).toFixed(1);
|
|
3664
4081
|
if (dryRun) {
|
|
3665
4082
|
p4.outro(
|
|
3666
|
-
|
|
4083
|
+
theme.warn("DRY RUN complete. ") + theme.muted(`no files were written. Remove --dry-run to generate. (${elapsed}s)`)
|
|
3667
4084
|
);
|
|
3668
4085
|
return;
|
|
3669
4086
|
}
|
|
3670
4087
|
p4.outro(
|
|
3671
|
-
|
|
3672
|
-
"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."
|
|
3673
4090
|
)
|
|
3674
4091
|
);
|
|
3675
4092
|
}
|
|
3676
4093
|
main().catch((err) => {
|
|
3677
|
-
console.error(
|
|
4094
|
+
console.error(theme.error("Fatal error:"), err);
|
|
3678
4095
|
process.exit(1);
|
|
3679
4096
|
});
|
|
3680
4097
|
//# sourceMappingURL=index.js.map
|