sourcebook 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/license.d.ts +4 -0
- package/dist/auth/license.js +23 -9
- package/dist/commands/activate.js +7 -5
- package/dist/generators/claude.js +14 -8
- package/dist/generators/shared.d.ts +12 -0
- package/dist/generators/shared.js +108 -0
- package/dist/scanner/build.js +7 -0
- package/dist/scanner/frameworks.js +37 -23
- package/dist/scanner/git.js +12 -12
- package/dist/scanner/graph.js +10 -1
- package/dist/scanner/index.js +24 -0
- package/dist/scanner/patterns.js +226 -4
- package/dist/types.d.ts +2 -0
- package/dist/utils/output.js +6 -2
- package/package.json +1 -1
package/dist/auth/license.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export declare function checkLicense(): Promise<LicenseInfo>;
|
|
|
18
18
|
* Save a license key to disk.
|
|
19
19
|
*/
|
|
20
20
|
export declare function saveLicenseKey(key: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Remove the license key from disk.
|
|
23
|
+
*/
|
|
24
|
+
export declare function removeLicenseKey(): void;
|
|
21
25
|
/**
|
|
22
26
|
* Gate a feature behind Pro license.
|
|
23
27
|
* Prints upgrade message and exits if not licensed.
|
package/dist/auth/license.js
CHANGED
|
@@ -44,12 +44,13 @@ export async function checkLicense() {
|
|
|
44
44
|
catch {
|
|
45
45
|
// Network error or timeout — fall back to cache or offline validation
|
|
46
46
|
if (cached && cached.key === key) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
// Only grant offline access if last validation was within 7 days
|
|
48
|
+
const OFFLINE_GRACE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
49
|
+
if (Date.now() - cached.timestamp <= OFFLINE_GRACE_MS) {
|
|
50
|
+
return cached.info;
|
|
51
|
+
}
|
|
52
52
|
}
|
|
53
|
+
// No valid cached validation within 7 days — deny access
|
|
53
54
|
}
|
|
54
55
|
return { valid: false, tier: "free" };
|
|
55
56
|
}
|
|
@@ -58,9 +59,22 @@ export async function checkLicense() {
|
|
|
58
59
|
*/
|
|
59
60
|
export function saveLicenseKey(key) {
|
|
60
61
|
if (!fs.existsSync(LICENSE_DIR)) {
|
|
61
|
-
fs.mkdirSync(LICENSE_DIR, { recursive: true });
|
|
62
|
+
fs.mkdirSync(LICENSE_DIR, { recursive: true, mode: 0o700 });
|
|
63
|
+
}
|
|
64
|
+
fs.writeFileSync(LICENSE_FILE, key.trim(), { encoding: "utf-8", mode: 0o600 });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Remove the license key from disk.
|
|
68
|
+
*/
|
|
69
|
+
export function removeLicenseKey() {
|
|
70
|
+
try {
|
|
71
|
+
if (fs.existsSync(LICENSE_FILE)) {
|
|
72
|
+
fs.unlinkSync(LICENSE_FILE);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// ignore cleanup errors
|
|
62
77
|
}
|
|
63
|
-
fs.writeFileSync(LICENSE_FILE, key.trim(), "utf-8");
|
|
64
78
|
}
|
|
65
79
|
/**
|
|
66
80
|
* Read the license key from disk.
|
|
@@ -93,10 +107,10 @@ function readCache() {
|
|
|
93
107
|
}
|
|
94
108
|
function writeCache(key, info) {
|
|
95
109
|
if (!fs.existsSync(LICENSE_DIR)) {
|
|
96
|
-
fs.mkdirSync(LICENSE_DIR, { recursive: true });
|
|
110
|
+
fs.mkdirSync(LICENSE_DIR, { recursive: true, mode: 0o700 });
|
|
97
111
|
}
|
|
98
112
|
const entry = { key, info, timestamp: Date.now() };
|
|
99
|
-
fs.writeFileSync(CACHE_FILE, JSON.stringify(entry), "utf-8");
|
|
113
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(entry), { encoding: "utf-8", mode: 0o600 });
|
|
100
114
|
}
|
|
101
115
|
function isCacheExpired(timestamp) {
|
|
102
116
|
return Date.now() - timestamp > CACHE_TTL_MS;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { saveLicenseKey, checkLicense } from "../auth/license.js";
|
|
2
|
+
import { saveLicenseKey, removeLicenseKey, checkLicense } from "../auth/license.js";
|
|
3
3
|
export async function activate(key) {
|
|
4
4
|
if (!key || key.trim().length === 0) {
|
|
5
5
|
console.log(chalk.red("\nNo license key provided."));
|
|
@@ -9,9 +9,9 @@ export async function activate(key) {
|
|
|
9
9
|
}
|
|
10
10
|
console.log(chalk.bold("\nsourcebook activate"));
|
|
11
11
|
console.log(chalk.dim("Validating license key...\n"));
|
|
12
|
-
//
|
|
12
|
+
// Validate first, only save if valid
|
|
13
|
+
// Temporarily save so checkLicense can read it, then remove if invalid
|
|
13
14
|
saveLicenseKey(key);
|
|
14
|
-
// Validate it
|
|
15
15
|
const license = await checkLicense();
|
|
16
16
|
if (license.tier === "pro" || license.tier === "team") {
|
|
17
17
|
console.log(chalk.green("✓") +
|
|
@@ -30,9 +30,11 @@ export async function activate(key) {
|
|
|
30
30
|
console.log("");
|
|
31
31
|
}
|
|
32
32
|
else {
|
|
33
|
+
// Validation failed — remove the saved key to prevent offline bypass
|
|
34
|
+
removeLicenseKey();
|
|
33
35
|
console.log(chalk.yellow("⚠") +
|
|
34
|
-
" License key
|
|
35
|
-
console.log(chalk.dim(" This may be a network issue.
|
|
36
|
+
" License key could not be validated and was not saved.");
|
|
37
|
+
console.log(chalk.dim(" This may be a network issue. Please try again when you have an internet connection."));
|
|
36
38
|
console.log(chalk.dim(" If the problem persists, contact roy@maroond.ai\n"));
|
|
37
39
|
}
|
|
38
40
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { groupByCategory, hasCommands, categorizeFindings, enforceTokenBudget, } from "./shared.js";
|
|
1
|
+
import { groupByCategory, hasCommands, categorizeFindings, enforceTokenBudget, buildQuickReference, getModePriorities, } from "./shared.js";
|
|
2
2
|
/**
|
|
3
3
|
* Generate a CLAUDE.md file from scan results.
|
|
4
4
|
*
|
|
@@ -10,6 +10,7 @@ import { groupByCategory, hasCommands, categorizeFindings, enforceTokenBudget, }
|
|
|
10
10
|
*/
|
|
11
11
|
export function generateClaude(scan, budget) {
|
|
12
12
|
const { critical, important, supplementary } = categorizeFindings(scan.findings);
|
|
13
|
+
const priorities = getModePriorities(scan.repoMode);
|
|
13
14
|
const sections = [];
|
|
14
15
|
// ============================================
|
|
15
16
|
// BEGINNING: Most critical info goes here
|
|
@@ -23,7 +24,12 @@ export function generateClaude(scan, budget) {
|
|
|
23
24
|
"",
|
|
24
25
|
].join("\n");
|
|
25
26
|
sections.push({ key: "header", content: header, priority: 100 });
|
|
26
|
-
//
|
|
27
|
+
// Quick Reference — the "30-second handoff" (highest priority in app mode)
|
|
28
|
+
const quickRef = buildQuickReference(scan.findings);
|
|
29
|
+
if (quickRef) {
|
|
30
|
+
sections.push({ key: "quick_reference", content: quickRef, priority: priorities.quick_reference });
|
|
31
|
+
}
|
|
32
|
+
// Commands — most immediately actionable
|
|
27
33
|
if (hasCommands(scan.commands)) {
|
|
28
34
|
const lines = ["## Commands", ""];
|
|
29
35
|
if (scan.commands.dev)
|
|
@@ -49,7 +55,7 @@ export function generateClaude(scan, budget) {
|
|
|
49
55
|
lines.push(`- **${finding.category}:** ${finding.description}`);
|
|
50
56
|
}
|
|
51
57
|
lines.push("");
|
|
52
|
-
sections.push({ key: "critical", content: lines.join("\n"), priority:
|
|
58
|
+
sections.push({ key: "critical", content: lines.join("\n"), priority: priorities.critical });
|
|
53
59
|
}
|
|
54
60
|
// ============================================
|
|
55
61
|
// MIDDLE: Less critical but useful info
|
|
@@ -58,7 +64,7 @@ export function generateClaude(scan, budget) {
|
|
|
58
64
|
// Stack (brief)
|
|
59
65
|
if (scan.frameworks.length > 0) {
|
|
60
66
|
const content = ["## Stack", "", scan.frameworks.join(", "), ""].join("\n");
|
|
61
|
-
sections.push({ key: "stack", content, priority:
|
|
67
|
+
sections.push({ key: "stack", content, priority: priorities.stack });
|
|
62
68
|
}
|
|
63
69
|
// Key directories (only non-obvious ones)
|
|
64
70
|
if (Object.keys(scan.structure.directories).length > 0) {
|
|
@@ -69,7 +75,7 @@ export function generateClaude(scan, budget) {
|
|
|
69
75
|
lines.push(`- \`${dir}/\` — ${purpose}`);
|
|
70
76
|
}
|
|
71
77
|
lines.push("");
|
|
72
|
-
sections.push({ key: "structure", content: lines.join("\n"), priority:
|
|
78
|
+
sections.push({ key: "structure", content: lines.join("\n"), priority: priorities.structure });
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
// Core modules (from PageRank)
|
|
@@ -80,7 +86,7 @@ export function generateClaude(scan, budget) {
|
|
|
80
86
|
lines.push(`- \`${file}\``);
|
|
81
87
|
}
|
|
82
88
|
lines.push("");
|
|
83
|
-
sections.push({ key: "core_modules", content: lines.join("\n"), priority:
|
|
89
|
+
sections.push({ key: "core_modules", content: lines.join("\n"), priority: priorities.core_modules });
|
|
84
90
|
}
|
|
85
91
|
// Important findings (high confidence, non-critical)
|
|
86
92
|
if (important.length > 0) {
|
|
@@ -98,7 +104,7 @@ export function generateClaude(scan, budget) {
|
|
|
98
104
|
}
|
|
99
105
|
}
|
|
100
106
|
lines.push("");
|
|
101
|
-
sections.push({ key: "conventions", content: lines.join("\n"), priority:
|
|
107
|
+
sections.push({ key: "conventions", content: lines.join("\n"), priority: priorities.conventions });
|
|
102
108
|
}
|
|
103
109
|
// Supplementary findings (medium confidence)
|
|
104
110
|
if (supplementary.length > 0) {
|
|
@@ -116,7 +122,7 @@ export function generateClaude(scan, budget) {
|
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
lines.push("");
|
|
119
|
-
sections.push({ key: "supplementary", content: lines.join("\n"), priority:
|
|
125
|
+
sections.push({ key: "supplementary", content: lines.join("\n"), priority: priorities.supplementary });
|
|
120
126
|
}
|
|
121
127
|
// ============================================
|
|
122
128
|
// END: Important reminders go here
|
|
@@ -27,6 +27,18 @@ export declare function categorizeFindings(findings: Finding[]): {
|
|
|
27
27
|
* 6. Supplementary findings (drop first)
|
|
28
28
|
* 7. Footer/manual section (always keep — end of context = high retention)
|
|
29
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* Build a Quick Reference section from dominant pattern findings.
|
|
32
|
+
* This is the "30-second senior engineer handoff" — the single most
|
|
33
|
+
* actionable section in the output.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildQuickReference(findings: Finding[]): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Get priority adjustments based on repo mode.
|
|
38
|
+
* App repos: boost dominant patterns, demote structural.
|
|
39
|
+
* Library repos: boost structural, demote patterns.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getModePriorities(repoMode?: "app" | "library" | "monorepo"): Record<string, number>;
|
|
30
42
|
export declare function enforceTokenBudget(sections: {
|
|
31
43
|
key: string;
|
|
32
44
|
content: string;
|
|
@@ -10,6 +10,7 @@ const CRITICAL_CATEGORIES = new Set([
|
|
|
10
10
|
"Git history",
|
|
11
11
|
"Commit conventions",
|
|
12
12
|
"Anti-patterns",
|
|
13
|
+
"Critical constraints",
|
|
13
14
|
]);
|
|
14
15
|
const CRITICAL_KEYWORDS = [
|
|
15
16
|
"breaking", "blast radius", "deprecated", "don't", "must",
|
|
@@ -62,6 +63,113 @@ export function categorizeFindings(findings) {
|
|
|
62
63
|
* 6. Supplementary findings (drop first)
|
|
63
64
|
* 7. Footer/manual section (always keep — end of context = high retention)
|
|
64
65
|
*/
|
|
66
|
+
/**
|
|
67
|
+
* Build a Quick Reference section from dominant pattern findings.
|
|
68
|
+
* This is the "30-second senior engineer handoff" — the single most
|
|
69
|
+
* actionable section in the output.
|
|
70
|
+
*/
|
|
71
|
+
export function buildQuickReference(findings) {
|
|
72
|
+
const patterns = findings.filter((f) => f.category === "Dominant patterns");
|
|
73
|
+
if (patterns.length < 2)
|
|
74
|
+
return null;
|
|
75
|
+
const lines = ["## Quick Reference", ""];
|
|
76
|
+
for (const p of patterns) {
|
|
77
|
+
// Extract a short label from the description
|
|
78
|
+
const desc = p.description;
|
|
79
|
+
let label = "";
|
|
80
|
+
let value = desc;
|
|
81
|
+
if (desc.includes("internationalization") || desc.includes("i18n") || desc.includes("translation")) {
|
|
82
|
+
label = "i18n";
|
|
83
|
+
}
|
|
84
|
+
else if (desc.includes("route") || desc.includes("endpoint") || desc.includes("API")) {
|
|
85
|
+
label = "routing";
|
|
86
|
+
}
|
|
87
|
+
else if (desc.includes("validation") || desc.includes("schema") || desc.includes("Zod") || desc.includes("Pydantic")) {
|
|
88
|
+
label = "validation";
|
|
89
|
+
}
|
|
90
|
+
else if ((desc.includes("auth") || desc.includes("Auth") || desc.includes("session")) && !desc.includes("integration")) {
|
|
91
|
+
label = "auth";
|
|
92
|
+
}
|
|
93
|
+
else if (desc.includes("Test") || desc.includes("test")) {
|
|
94
|
+
label = "testing";
|
|
95
|
+
}
|
|
96
|
+
else if (desc.includes("Tailwind") || desc.includes("styled") || desc.includes("CSS")) {
|
|
97
|
+
label = "styling";
|
|
98
|
+
}
|
|
99
|
+
else if (desc.includes("database") || desc.includes("Database") || desc.includes("Prisma") || desc.includes("ORM")) {
|
|
100
|
+
label = "database";
|
|
101
|
+
}
|
|
102
|
+
else if (desc.includes("fetching") || desc.includes("Query") || desc.includes("SWR")) {
|
|
103
|
+
label = "data fetching";
|
|
104
|
+
}
|
|
105
|
+
else if (desc.includes("Route definitions") || desc.includes("Add new endpoints")) {
|
|
106
|
+
label = "routes";
|
|
107
|
+
}
|
|
108
|
+
else if (desc.includes("integration") || desc.includes("Third-party")) {
|
|
109
|
+
label = "integrations";
|
|
110
|
+
}
|
|
111
|
+
else if (desc.includes("components") || desc.includes("UI")) {
|
|
112
|
+
label = "components";
|
|
113
|
+
}
|
|
114
|
+
else if (desc.includes("Generated") || desc.includes("generated") || desc.includes("DO NOT")) {
|
|
115
|
+
label = "generated";
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
continue; // Skip findings we can't label cleanly
|
|
119
|
+
}
|
|
120
|
+
// Compress the description to a short actionable line
|
|
121
|
+
const short = desc
|
|
122
|
+
.replace(/\. Follow this pattern.*$/, "")
|
|
123
|
+
.replace(/\. This is the project's standard.*$/, "")
|
|
124
|
+
.replace(/\. Each integration has.*$/, "");
|
|
125
|
+
lines.push(`- **${label}:** ${short}`);
|
|
126
|
+
}
|
|
127
|
+
if (lines.length <= 2)
|
|
128
|
+
return null; // Nothing useful
|
|
129
|
+
// Deduplicate by label — keep only first occurrence of each
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const deduped = [lines[0], lines[1]]; // Keep header
|
|
132
|
+
for (let i = 2; i < lines.length; i++) {
|
|
133
|
+
const labelMatch = lines[i].match(/^\- \*\*(\w[\w\s]*)\:\*\*/);
|
|
134
|
+
if (labelMatch) {
|
|
135
|
+
const lbl = labelMatch[1];
|
|
136
|
+
if (seen.has(lbl))
|
|
137
|
+
continue;
|
|
138
|
+
seen.add(lbl);
|
|
139
|
+
}
|
|
140
|
+
deduped.push(lines[i]);
|
|
141
|
+
}
|
|
142
|
+
deduped.push("");
|
|
143
|
+
return deduped.join("\n");
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get priority adjustments based on repo mode.
|
|
147
|
+
* App repos: boost dominant patterns, demote structural.
|
|
148
|
+
* Library repos: boost structural, demote patterns.
|
|
149
|
+
*/
|
|
150
|
+
export function getModePriorities(repoMode) {
|
|
151
|
+
if (repoMode === "library") {
|
|
152
|
+
return {
|
|
153
|
+
quick_reference: 85,
|
|
154
|
+
critical: 92,
|
|
155
|
+
core_modules: 90,
|
|
156
|
+
conventions: 80,
|
|
157
|
+
stack: 50,
|
|
158
|
+
structure: 40,
|
|
159
|
+
supplementary: 25,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Default: app mode (boost patterns, Quick Reference highest)
|
|
163
|
+
return {
|
|
164
|
+
quick_reference: 96,
|
|
165
|
+
critical: 92,
|
|
166
|
+
core_modules: 50,
|
|
167
|
+
conventions: 85,
|
|
168
|
+
stack: 45,
|
|
169
|
+
structure: 60,
|
|
170
|
+
supplementary: 20,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
65
173
|
export function enforceTokenBudget(sections, budget) {
|
|
66
174
|
// Sort by priority descending (highest priority = keep)
|
|
67
175
|
const sorted = [...sections].sort((a, b) => b.priority - a.priority);
|
package/dist/scanner/build.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
function safePath(dir, file) {
|
|
4
|
+
const resolved = path.resolve(path.join(dir, file));
|
|
5
|
+
if (!resolved.startsWith(path.resolve(dir) + path.sep) && resolved !== path.resolve(dir)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return resolved;
|
|
9
|
+
}
|
|
3
10
|
export async function detectBuildCommands(dir) {
|
|
4
11
|
const commands = {};
|
|
5
12
|
// Check package.json scripts
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
function safePath(dir, file) {
|
|
4
|
+
const resolved = path.resolve(path.join(dir, file));
|
|
5
|
+
if (!resolved.startsWith(path.resolve(dir) + path.sep) && resolved !== path.resolve(dir)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return resolved;
|
|
9
|
+
}
|
|
3
10
|
export async function detectFrameworks(dir, files) {
|
|
4
11
|
const detected = [];
|
|
5
12
|
// Read all package.json files (root + workspaces/sub-packages)
|
|
@@ -8,7 +15,9 @@ export async function detectFrameworks(dir, files) {
|
|
|
8
15
|
pkgFiles.push("package.json");
|
|
9
16
|
const allDeps = {};
|
|
10
17
|
for (const pkgFile of pkgFiles) {
|
|
11
|
-
const pkgPath =
|
|
18
|
+
const pkgPath = safePath(dir, pkgFile);
|
|
19
|
+
if (!pkgPath)
|
|
20
|
+
continue;
|
|
12
21
|
if (fs.existsSync(pkgPath)) {
|
|
13
22
|
try {
|
|
14
23
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -44,29 +53,31 @@ export async function detectFrameworks(dir, files) {
|
|
|
44
53
|
// Check for next.config
|
|
45
54
|
const nextConfig = files.find((f) => /^next\.config\.(js|mjs|ts)$/.test(f));
|
|
46
55
|
if (nextConfig) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
const safeNextConfig = safePath(dir, nextConfig);
|
|
57
|
+
if (safeNextConfig)
|
|
58
|
+
try {
|
|
59
|
+
const configContent = fs.readFileSync(safeNextConfig, "utf-8");
|
|
60
|
+
if (configContent.includes("output:") && configContent.includes("standalone")) {
|
|
61
|
+
findings.push({
|
|
62
|
+
category: "Next.js deployment",
|
|
63
|
+
description: "Standalone output mode is enabled. Build produces a self-contained server in .next/standalone.",
|
|
64
|
+
confidence: "high",
|
|
65
|
+
discoverable: false,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (configContent.includes("images") && configContent.includes("remotePatterns")) {
|
|
69
|
+
findings.push({
|
|
70
|
+
category: "Next.js images",
|
|
71
|
+
description: "Remote image patterns are configured. New image domains must be added to next.config before use.",
|
|
72
|
+
rationale: "Agents will try to use next/image with arbitrary URLs and get 400 errors without this config.",
|
|
73
|
+
confidence: "high",
|
|
74
|
+
discoverable: false,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
56
77
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
category: "Next.js images",
|
|
60
|
-
description: "Remote image patterns are configured. New image domains must be added to next.config before use.",
|
|
61
|
-
rationale: "Agents will try to use next/image with arbitrary URLs and get 400 errors without this config.",
|
|
62
|
-
confidence: "high",
|
|
63
|
-
discoverable: false,
|
|
64
|
-
});
|
|
78
|
+
catch {
|
|
79
|
+
// can't read config
|
|
65
80
|
}
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
// can't read config
|
|
69
|
-
}
|
|
70
81
|
}
|
|
71
82
|
detected.push({
|
|
72
83
|
name: "Next.js",
|
|
@@ -157,7 +168,10 @@ export async function detectFrameworks(dir, files) {
|
|
|
157
168
|
if (hasTwConfig) {
|
|
158
169
|
try {
|
|
159
170
|
const configPath = files.find((f) => /^tailwind\.config\.(js|ts|mjs|cjs)$/.test(f));
|
|
160
|
-
const
|
|
171
|
+
const safeConfigPath = safePath(dir, configPath);
|
|
172
|
+
if (!safeConfigPath)
|
|
173
|
+
throw new Error("path escape");
|
|
174
|
+
const content = fs.readFileSync(safeConfigPath, "utf-8");
|
|
161
175
|
if (content.includes("extend") && content.includes("colors")) {
|
|
162
176
|
findings.push({
|
|
163
177
|
category: "Tailwind",
|
package/dist/scanner/git.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
/**
|
|
4
4
|
* Mine git history for non-obvious context:
|
|
@@ -33,7 +33,7 @@ export async function analyzeGitHistory(dir) {
|
|
|
33
33
|
}
|
|
34
34
|
function isGitRepo(dir) {
|
|
35
35
|
try {
|
|
36
|
-
|
|
36
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
37
37
|
cwd: dir,
|
|
38
38
|
stdio: "pipe",
|
|
39
39
|
});
|
|
@@ -45,7 +45,7 @@ function isGitRepo(dir) {
|
|
|
45
45
|
}
|
|
46
46
|
function git(dir, args) {
|
|
47
47
|
try {
|
|
48
|
-
return
|
|
48
|
+
return execFileSync("git", args, {
|
|
49
49
|
cwd: dir,
|
|
50
50
|
stdio: "pipe",
|
|
51
51
|
maxBuffer: 10 * 1024 * 1024,
|
|
@@ -60,7 +60,7 @@ function git(dir, args) {
|
|
|
60
60
|
*/
|
|
61
61
|
function detectRevertedPatterns(dir, revertedPatterns) {
|
|
62
62
|
const findings = [];
|
|
63
|
-
const revertLog = git(dir,
|
|
63
|
+
const revertLog = git(dir, ["log", "--grep=^Revert", "--oneline", "--since=1 year ago", "-50"]);
|
|
64
64
|
if (!revertLog.trim())
|
|
65
65
|
return findings;
|
|
66
66
|
const reverts = revertLog.trim().split("\n").filter(Boolean);
|
|
@@ -94,7 +94,7 @@ function detectRevertedPatterns(dir, revertedPatterns) {
|
|
|
94
94
|
function detectAntiPatterns(dir) {
|
|
95
95
|
const findings = [];
|
|
96
96
|
// Extract detailed info from reverted commits
|
|
97
|
-
const revertLog = git(dir,
|
|
97
|
+
const revertLog = git(dir, ["log", "--grep=^Revert", "--format=%s", "--since=1 year ago", "-20"]);
|
|
98
98
|
if (revertLog.trim()) {
|
|
99
99
|
const antiPatterns = [];
|
|
100
100
|
for (const line of revertLog.trim().split("\n").filter(Boolean)) {
|
|
@@ -116,13 +116,13 @@ function detectAntiPatterns(dir) {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
// Detect files deleted in bulk (abandoned features/approaches)
|
|
119
|
-
const deletedLog = git(dir,
|
|
119
|
+
const deletedLog = git(dir, ["log", "--diff-filter=D", "--name-only", "--pretty=format:COMMIT %s", "--since=6 months ago", "-50"]);
|
|
120
120
|
if (deletedLog.trim()) {
|
|
121
121
|
const deletionBatches = [];
|
|
122
122
|
let currentMessage = "";
|
|
123
123
|
let currentFiles = [];
|
|
124
124
|
for (const line of deletedLog.split("\n")) {
|
|
125
|
-
const commitMatch = line.match(/^
|
|
125
|
+
const commitMatch = line.match(/^COMMIT (.+)$/);
|
|
126
126
|
if (commitMatch) {
|
|
127
127
|
if (currentFiles.length >= 3) {
|
|
128
128
|
deletionBatches.push({ message: currentMessage, files: currentFiles });
|
|
@@ -159,7 +159,7 @@ function detectAntiPatterns(dir) {
|
|
|
159
159
|
function detectActiveAreas(dir, activeAreas) {
|
|
160
160
|
const findings = [];
|
|
161
161
|
// Get files changed in the last 30 days, count changes per directory
|
|
162
|
-
const recentChanges = git(dir,
|
|
162
|
+
const recentChanges = git(dir, ["log", "--since=30 days ago", "--name-only", "--pretty=format:", "--diff-filter=AMRC"]);
|
|
163
163
|
if (!recentChanges.trim())
|
|
164
164
|
return findings;
|
|
165
165
|
const dirCounts = new Map();
|
|
@@ -198,14 +198,14 @@ function detectActiveAreas(dir, activeAreas) {
|
|
|
198
198
|
function detectCoChangeCoupling(dir, clusters) {
|
|
199
199
|
const findings = [];
|
|
200
200
|
// Get the last 200 commits with their changed files
|
|
201
|
-
const log = git(dir,
|
|
201
|
+
const log = git(dir, ["log", "--name-only", "--pretty=format:COMMIT", "--since=6 months ago", "-200"]);
|
|
202
202
|
if (!log.trim())
|
|
203
203
|
return findings;
|
|
204
204
|
// Parse commits into file groups
|
|
205
205
|
const commits = [];
|
|
206
206
|
let current = [];
|
|
207
207
|
for (const line of log.split("\n")) {
|
|
208
|
-
if (line.trim() ===
|
|
208
|
+
if (line.trim() === "COMMIT") {
|
|
209
209
|
if (current.length > 0)
|
|
210
210
|
commits.push(current);
|
|
211
211
|
current = [];
|
|
@@ -293,7 +293,7 @@ function detectCoChangeCoupling(dir, clusters) {
|
|
|
293
293
|
function detectRapidReEdits(dir) {
|
|
294
294
|
const findings = [];
|
|
295
295
|
// Get files with high commit frequency in short windows
|
|
296
|
-
const log = git(dir,
|
|
296
|
+
const log = git(dir, ["log", "--format=%H %aI", "--name-only", "--since=3 months ago", "-300"]);
|
|
297
297
|
if (!log.trim())
|
|
298
298
|
return findings;
|
|
299
299
|
// Track edits per file with timestamps
|
|
@@ -354,7 +354,7 @@ function detectRapidReEdits(dir) {
|
|
|
354
354
|
*/
|
|
355
355
|
function detectCommitPatterns(dir) {
|
|
356
356
|
const findings = [];
|
|
357
|
-
const log = git(dir,
|
|
357
|
+
const log = git(dir, ["log", "--oneline", "--since=6 months ago", "-200"]);
|
|
358
358
|
if (!log.trim())
|
|
359
359
|
return findings;
|
|
360
360
|
const messages = log.trim().split("\n").filter(Boolean);
|
package/dist/scanner/graph.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
function safePath(dir, file) {
|
|
4
|
+
const resolved = path.resolve(path.join(dir, file));
|
|
5
|
+
if (!resolved.startsWith(path.resolve(dir) + path.sep) && resolved !== path.resolve(dir)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return resolved;
|
|
9
|
+
}
|
|
3
10
|
/**
|
|
4
11
|
* Build an import/dependency graph and run PageRank to identify
|
|
5
12
|
* the most structurally important files. Conventions found in
|
|
@@ -16,7 +23,9 @@ export async function analyzeImportGraph(dir, files) {
|
|
|
16
23
|
const edges = [];
|
|
17
24
|
const fileSet = new Set(sourceFiles);
|
|
18
25
|
for (const file of sourceFiles) {
|
|
19
|
-
const filePath =
|
|
26
|
+
const filePath = safePath(dir, file);
|
|
27
|
+
if (!filePath)
|
|
28
|
+
continue;
|
|
20
29
|
let content;
|
|
21
30
|
try {
|
|
22
31
|
content = fs.readFileSync(filePath, "utf-8");
|
package/dist/scanner/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export async function scanProject(dir) {
|
|
|
28
28
|
nodir: true,
|
|
29
29
|
ignore: IGNORE_PATTERNS,
|
|
30
30
|
dot: true,
|
|
31
|
+
follow: false,
|
|
31
32
|
});
|
|
32
33
|
// Detect languages from file extensions
|
|
33
34
|
const languages = detectLanguages(files);
|
|
@@ -51,6 +52,8 @@ export async function scanProject(dir) {
|
|
|
51
52
|
...gitAnalysis.findings,
|
|
52
53
|
...graphAnalysis.findings,
|
|
53
54
|
];
|
|
55
|
+
// Detect repo mode for prioritization
|
|
56
|
+
const repoMode = detectRepoMode(dir, files, frameworks.map((f) => f.name));
|
|
54
57
|
return {
|
|
55
58
|
dir,
|
|
56
59
|
files,
|
|
@@ -60,8 +63,29 @@ export async function scanProject(dir) {
|
|
|
60
63
|
structure,
|
|
61
64
|
findings,
|
|
62
65
|
rankedFiles: graphAnalysis.rankedFiles,
|
|
66
|
+
repoMode,
|
|
63
67
|
};
|
|
64
68
|
}
|
|
69
|
+
function detectRepoMode(dir, files, frameworks) {
|
|
70
|
+
// Monorepo detection
|
|
71
|
+
const hasWorkspaces = files.some((f) => f === "pnpm-workspace.yaml" || f === "lerna.json" || f === "nx.json");
|
|
72
|
+
if (hasWorkspaces)
|
|
73
|
+
return "monorepo";
|
|
74
|
+
// Library detection
|
|
75
|
+
const hasPublishConfig = files.some((f) => f === "setup.py" || f === "pyproject.toml" || f === "setup.cfg");
|
|
76
|
+
const hasSrcLib = files.some((f) => f.startsWith("src/lib/") || f.startsWith("lib/"));
|
|
77
|
+
const hasExportsField = false; // would need to read package.json, but frameworks already detected
|
|
78
|
+
const isLibraryFramework = frameworks.some((f) => ["FastAPI", "Flask", "Django", "Hono", "Express"].includes(f));
|
|
79
|
+
// If it has a setup.py/pyproject.toml AND no app/ or pages/ → library
|
|
80
|
+
const hasAppDirs = files.some((f) => f.startsWith("app/") || f.startsWith("pages/") || f.startsWith("src/app/") || f.startsWith("src/pages/"));
|
|
81
|
+
const hasComponents = files.some((f) => f.includes("/components/"));
|
|
82
|
+
if (hasPublishConfig && !hasAppDirs && !hasComponents)
|
|
83
|
+
return "library";
|
|
84
|
+
if (hasSrcLib && !hasAppDirs && !hasComponents && !isLibraryFramework)
|
|
85
|
+
return "library";
|
|
86
|
+
// Default: app
|
|
87
|
+
return "app";
|
|
88
|
+
}
|
|
65
89
|
function detectLanguages(files) {
|
|
66
90
|
const extMap = {
|
|
67
91
|
".ts": "TypeScript",
|
package/dist/scanner/patterns.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
function safePath(dir, file) {
|
|
4
|
+
const resolved = path.resolve(path.join(dir, file));
|
|
5
|
+
if (!resolved.startsWith(path.resolve(dir) + path.sep) && resolved !== path.resolve(dir)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return resolved;
|
|
9
|
+
}
|
|
3
10
|
/**
|
|
4
11
|
* Detect code patterns and conventions that are non-obvious.
|
|
5
12
|
* This is the core intelligence layer -- finding things agents miss.
|
|
@@ -17,8 +24,11 @@ export async function detectPatterns(dir, files, frameworks) {
|
|
|
17
24
|
const sampled = sampleFiles(sourceFiles, 50);
|
|
18
25
|
const fileContents = new Map();
|
|
19
26
|
for (const file of sampled) {
|
|
27
|
+
const safe = safePath(dir, file);
|
|
28
|
+
if (!safe)
|
|
29
|
+
continue;
|
|
20
30
|
try {
|
|
21
|
-
const content = fs.readFileSync(
|
|
31
|
+
const content = fs.readFileSync(safe, "utf-8");
|
|
22
32
|
fileContents.set(file, content);
|
|
23
33
|
}
|
|
24
34
|
catch {
|
|
@@ -275,8 +285,11 @@ function detectDominantPatterns(dir, files, contents, frameworks) {
|
|
|
275
285
|
const allContents = new Map(contents);
|
|
276
286
|
for (const file of extraSample) {
|
|
277
287
|
if (!allContents.has(file)) {
|
|
288
|
+
const safe = safePath(dir, file);
|
|
289
|
+
if (!safe)
|
|
290
|
+
continue;
|
|
278
291
|
try {
|
|
279
|
-
const content = fs.readFileSync(
|
|
292
|
+
const content = fs.readFileSync(safe, "utf-8");
|
|
280
293
|
allContents.set(file, content);
|
|
281
294
|
}
|
|
282
295
|
catch { /* skip */ }
|
|
@@ -432,8 +445,11 @@ function detectDominantPatterns(dir, files, contents, frameworks) {
|
|
|
432
445
|
.slice(0, 10);
|
|
433
446
|
for (const file of testSampled) {
|
|
434
447
|
if (!allContents.has(file)) {
|
|
448
|
+
const safe = safePath(dir, file);
|
|
449
|
+
if (!safe)
|
|
450
|
+
continue;
|
|
435
451
|
try {
|
|
436
|
-
const content = fs.readFileSync(
|
|
452
|
+
const content = fs.readFileSync(safe, "utf-8");
|
|
437
453
|
allContents.set(file, content);
|
|
438
454
|
}
|
|
439
455
|
catch { /* skip */ }
|
|
@@ -467,7 +483,213 @@ function detectDominantPatterns(dir, files, contents, frameworks) {
|
|
|
467
483
|
});
|
|
468
484
|
}
|
|
469
485
|
// ========================================
|
|
470
|
-
// 6.
|
|
486
|
+
// 6. AUTH PATTERNS
|
|
487
|
+
// ========================================
|
|
488
|
+
const authPatterns = [
|
|
489
|
+
{ pattern: "useAuth|useSession|useUser", name: "auth hooks (useAuth/useSession/useUser)", count: 0 },
|
|
490
|
+
{ pattern: "withAuth|authMiddleware|requireAuth", name: "auth middleware", count: 0 },
|
|
491
|
+
{ pattern: "passport\\.authenticate", name: "Passport.js", count: 0 },
|
|
492
|
+
{ pattern: "jwt\\.verify|jwt\\.sign|jsonwebtoken", name: "JWT (jsonwebtoken)", count: 0 },
|
|
493
|
+
{ pattern: "@login_required|LoginRequiredMixin", name: "Django login_required", count: 0 },
|
|
494
|
+
{ pattern: "IsAuthenticated|AllowAny|BasePermission", name: "DRF permissions", count: 0 },
|
|
495
|
+
{ pattern: "NextAuth|getServerSession", name: "NextAuth.js", count: 0 },
|
|
496
|
+
{ pattern: "supabase\\.auth|useSupabaseClient", name: "Supabase Auth", count: 0 },
|
|
497
|
+
{ pattern: "clerk|useClerk|ClerkProvider", name: "Clerk", count: 0 },
|
|
498
|
+
];
|
|
499
|
+
for (const [, content] of allContents) {
|
|
500
|
+
for (const p of authPatterns) {
|
|
501
|
+
if (new RegExp(p.pattern).test(content)) {
|
|
502
|
+
p.count++;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const dominantAuth = authPatterns.filter((p) => p.count >= 2).sort((a, b) => b.count - a.count);
|
|
507
|
+
if (dominantAuth.length > 0) {
|
|
508
|
+
const primary = dominantAuth[0];
|
|
509
|
+
// Find auth middleware/guard files
|
|
510
|
+
const authFiles = files.filter((f) => (f.includes("auth") || f.includes("middleware") || f.includes("guard") || f.includes("session")) &&
|
|
511
|
+
!f.includes("node_modules") && !f.includes(".test.") &&
|
|
512
|
+
(f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".js") || f.endsWith(".py")));
|
|
513
|
+
const authEntrypoint = authFiles.find((f) => f.includes("middleware") || f.includes("guard") || f.includes("auth/index"));
|
|
514
|
+
let desc = `Auth uses ${primary.name}.`;
|
|
515
|
+
if (authEntrypoint) {
|
|
516
|
+
desc += ` Auth logic lives in ${authEntrypoint}.`;
|
|
517
|
+
}
|
|
518
|
+
findings.push({
|
|
519
|
+
category: "Dominant patterns",
|
|
520
|
+
description: desc,
|
|
521
|
+
evidence: `${primary.count} files`,
|
|
522
|
+
confidence: "high",
|
|
523
|
+
discoverable: false,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
// ========================================
|
|
527
|
+
// 7. STYLING CONVENTIONS
|
|
528
|
+
// ========================================
|
|
529
|
+
const stylePatterns = [
|
|
530
|
+
{ pattern: "className=|class=.*tw-", name: "Tailwind CSS", desc: "Styling uses Tailwind CSS utility classes", count: 0 },
|
|
531
|
+
{ pattern: "styled\\.|styled\\(|css`", name: "styled-components/Emotion", desc: "Styling uses CSS-in-JS (styled-components or Emotion)", count: 0 },
|
|
532
|
+
{ pattern: "styles\\.\\w+|from.*\\.module\\.(css|scss)", name: "CSS Modules", desc: "Styling uses CSS Modules (*.module.css)", count: 0 },
|
|
533
|
+
];
|
|
534
|
+
for (const [f, content] of allContents) {
|
|
535
|
+
for (const p of stylePatterns) {
|
|
536
|
+
if (new RegExp(p.pattern).test(content)) {
|
|
537
|
+
p.count++;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const dominantStyle = stylePatterns.filter((p) => p.count >= 3).sort((a, b) => b.count - a.count);
|
|
542
|
+
if (dominantStyle.length > 0) {
|
|
543
|
+
const primary = dominantStyle[0];
|
|
544
|
+
let desc = `${primary.desc}.`;
|
|
545
|
+
// For Tailwind, check for custom tokens
|
|
546
|
+
if (primary.name === "Tailwind CSS") {
|
|
547
|
+
const twConfig = files.find((f) => f.includes("tailwind.config"));
|
|
548
|
+
if (twConfig) {
|
|
549
|
+
const safeTw = safePath(dir, twConfig);
|
|
550
|
+
if (safeTw)
|
|
551
|
+
try {
|
|
552
|
+
const configContent = fs.readFileSync(safeTw, "utf-8");
|
|
553
|
+
if (configContent.includes("colors") || configContent.includes("extend")) {
|
|
554
|
+
desc += ` Custom design tokens defined in ${twConfig} — use these instead of arbitrary values.`;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch { /* skip */ }
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
findings.push({
|
|
561
|
+
category: "Dominant patterns",
|
|
562
|
+
description: desc,
|
|
563
|
+
evidence: `${primary.count} files`,
|
|
564
|
+
confidence: "high",
|
|
565
|
+
discoverable: false,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
// ========================================
|
|
569
|
+
// 8. DATABASE / ORM PATTERNS
|
|
570
|
+
// ========================================
|
|
571
|
+
const dbPatterns = [
|
|
572
|
+
{ pattern: "prisma\\.|PrismaClient|\\$queryRaw", name: "Prisma", entryHint: "prisma/schema.prisma", count: 0 },
|
|
573
|
+
{ pattern: "drizzle\\(|pgTable|sqliteTable", name: "Drizzle ORM", entryHint: "drizzle.config.ts", count: 0 },
|
|
574
|
+
{ pattern: "knex\\(|knex\\.schema", name: "Knex.js", entryHint: "knexfile", count: 0 },
|
|
575
|
+
{ pattern: "sequelize\\.define|Model\\.init", name: "Sequelize", entryHint: "models/", count: 0 },
|
|
576
|
+
{ pattern: "TypeORM|@Entity|getRepository", name: "TypeORM", entryHint: "entities/", count: 0 },
|
|
577
|
+
{ pattern: "mongoose\\.model|Schema\\(\\{", name: "Mongoose", entryHint: "models/", count: 0 },
|
|
578
|
+
{ pattern: "from django\\.db|models\\.Model", name: "Django ORM", entryHint: "models.py", count: 0 },
|
|
579
|
+
{ pattern: "SQLAlchemy|declarative_base|sessionmaker", name: "SQLAlchemy", entryHint: "models/", count: 0 },
|
|
580
|
+
{ pattern: "from tortoise|tortoise\\.models", name: "Tortoise ORM", entryHint: "models/", count: 0 },
|
|
581
|
+
];
|
|
582
|
+
for (const [, content] of allContents) {
|
|
583
|
+
for (const p of dbPatterns) {
|
|
584
|
+
if (new RegExp(p.pattern).test(content)) {
|
|
585
|
+
p.count++;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const dominantDB = dbPatterns.filter((p) => p.count >= 2).sort((a, b) => b.count - a.count);
|
|
590
|
+
if (dominantDB.length > 0) {
|
|
591
|
+
const primary = dominantDB[0];
|
|
592
|
+
// Try to find the actual entrypoint file
|
|
593
|
+
const dbEntryFile = files.find((f) => f.includes(primary.entryHint) && !f.includes("node_modules"));
|
|
594
|
+
let desc = `Database access uses ${primary.name}.`;
|
|
595
|
+
if (dbEntryFile) {
|
|
596
|
+
desc += ` Schema/models defined in ${dbEntryFile}.`;
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
desc += ` Look for schemas in ${primary.entryHint}.`;
|
|
600
|
+
}
|
|
601
|
+
findings.push({
|
|
602
|
+
category: "Dominant patterns",
|
|
603
|
+
description: desc,
|
|
604
|
+
evidence: `${primary.count} files`,
|
|
605
|
+
confidence: "high",
|
|
606
|
+
discoverable: false,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
// ========================================
|
|
610
|
+
// 9. GENERATED / DO-NOT-EDIT FILES
|
|
611
|
+
// ========================================
|
|
612
|
+
const generatedFiles = [];
|
|
613
|
+
for (const [file, content] of allContents) {
|
|
614
|
+
const firstLines = content.slice(0, 500);
|
|
615
|
+
if (/@generated/.test(firstLines) ||
|
|
616
|
+
/DO NOT EDIT/i.test(firstLines) ||
|
|
617
|
+
/auto-generated/i.test(firstLines) ||
|
|
618
|
+
/this file is generated/i.test(firstLines) ||
|
|
619
|
+
/generated by/i.test(firstLines)) {
|
|
620
|
+
generatedFiles.push(file);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// Also check for common generated file patterns in the full file list
|
|
624
|
+
const knownGenerated = files.filter((f) => !f.includes("node_modules") &&
|
|
625
|
+
(f.includes(".generated.") ||
|
|
626
|
+
f.includes(".gen.") ||
|
|
627
|
+
f.endsWith(".d.ts") && f.includes("generated") ||
|
|
628
|
+
f.includes("__generated__") ||
|
|
629
|
+
f.includes("codegen")));
|
|
630
|
+
const allGenerated = [...new Set([...generatedFiles, ...knownGenerated])];
|
|
631
|
+
if (allGenerated.length >= 2) {
|
|
632
|
+
const samples = allGenerated.slice(0, 5).join(", ");
|
|
633
|
+
findings.push({
|
|
634
|
+
category: "Critical constraints",
|
|
635
|
+
description: `Generated files detected (${samples}${allGenerated.length > 5 ? ", ..." : ""}). Do NOT edit these directly — modify the source/schema they are generated from.`,
|
|
636
|
+
evidence: `${allGenerated.length} generated files`,
|
|
637
|
+
confidence: "high",
|
|
638
|
+
discoverable: false,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
// ========================================
|
|
642
|
+
// 10. EDIT ENTRYPOINTS (where changes usually land)
|
|
643
|
+
// ========================================
|
|
644
|
+
// For routing — find where route definitions live
|
|
645
|
+
if (dominantRouter.length > 0) {
|
|
646
|
+
const routeDirs = files
|
|
647
|
+
.filter((f) => (f.includes("routes") || f.includes("routers") || f.includes("api/") || f.includes("app/api/")) &&
|
|
648
|
+
!f.includes("node_modules") && !f.includes(".test.") &&
|
|
649
|
+
(f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".py") || f.endsWith(".go")))
|
|
650
|
+
.map((f) => {
|
|
651
|
+
const parts = f.split("/");
|
|
652
|
+
// Get the directory containing route files
|
|
653
|
+
return parts.slice(0, -1).join("/");
|
|
654
|
+
})
|
|
655
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
656
|
+
.slice(0, 3);
|
|
657
|
+
if (routeDirs.length > 0) {
|
|
658
|
+
findings.push({
|
|
659
|
+
category: "Dominant patterns",
|
|
660
|
+
description: `Route definitions live in: ${routeDirs.join(", ")}. Add new endpoints here.`,
|
|
661
|
+
evidence: `${routeDirs.length} route directories`,
|
|
662
|
+
confidence: "high",
|
|
663
|
+
discoverable: false,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// For components — find where UI components live
|
|
668
|
+
const componentDirs = files
|
|
669
|
+
.filter((f) => (f.includes("/components/") || f.includes("/ui/")) &&
|
|
670
|
+
!f.includes("node_modules") && !f.includes(".test.") &&
|
|
671
|
+
(f.endsWith(".tsx") || f.endsWith(".jsx") || f.endsWith(".vue") || f.endsWith(".svelte")))
|
|
672
|
+
.map((f) => {
|
|
673
|
+
const match = f.match(/(.*\/(?:components|ui))\//);
|
|
674
|
+
return match ? match[1] : null;
|
|
675
|
+
})
|
|
676
|
+
.filter((v) => v !== null)
|
|
677
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
678
|
+
.slice(0, 3);
|
|
679
|
+
if (componentDirs.length > 0 && componentDirs.some((d) => !d.includes("node_modules"))) {
|
|
680
|
+
const filtered = componentDirs.filter((d) => !d.includes("node_modules"));
|
|
681
|
+
if (filtered.length > 0) {
|
|
682
|
+
findings.push({
|
|
683
|
+
category: "Dominant patterns",
|
|
684
|
+
description: `UI components live in: ${filtered.join(", ")}. Add new components here.`,
|
|
685
|
+
evidence: `${filtered.length} component directories`,
|
|
686
|
+
confidence: "medium",
|
|
687
|
+
discoverable: false,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// ========================================
|
|
692
|
+
// 11. KEY DIRECTORY PURPOSES (app-specific)
|
|
471
693
|
// ========================================
|
|
472
694
|
// Detect directories with clear domain purposes
|
|
473
695
|
const dirPurposes = [];
|
package/dist/types.d.ts
CHANGED
package/dist/utils/output.js
CHANGED
|
@@ -2,9 +2,13 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
export async function writeOutput(dir, filename, content) {
|
|
4
4
|
const filePath = path.join(dir, filename);
|
|
5
|
-
const
|
|
5
|
+
const resolved = path.resolve(filePath);
|
|
6
|
+
if (!resolved.startsWith(path.resolve(dir) + path.sep)) {
|
|
7
|
+
throw new Error(`Output path escapes target directory: ${filename}`);
|
|
8
|
+
}
|
|
9
|
+
const parentDir = path.dirname(resolved);
|
|
6
10
|
if (!fs.existsSync(parentDir)) {
|
|
7
11
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
8
12
|
}
|
|
9
|
-
fs.writeFileSync(
|
|
13
|
+
fs.writeFileSync(resolved, content, "utf-8");
|
|
10
14
|
}
|