@chappibunny/repolens 0.4.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/CHANGELOG.md +219 -0
- package/README.md +899 -0
- package/RELEASE.md +52 -0
- package/bin/repolens.js +2 -0
- package/package.json +61 -0
- package/src/ai/document-plan.js +133 -0
- package/src/ai/generate-sections.js +271 -0
- package/src/ai/prompts.js +312 -0
- package/src/ai/provider.js +134 -0
- package/src/analyzers/context-builder.js +146 -0
- package/src/analyzers/domain-inference.js +127 -0
- package/src/analyzers/flow-inference.js +198 -0
- package/src/cli.js +271 -0
- package/src/core/config-schema.js +266 -0
- package/src/core/config.js +18 -0
- package/src/core/diff.js +45 -0
- package/src/core/scan.js +312 -0
- package/src/delivery/comment.js +139 -0
- package/src/docs/generate-doc-set.js +123 -0
- package/src/docs/write-doc-set.js +85 -0
- package/src/doctor.js +174 -0
- package/src/init.js +540 -0
- package/src/migrate.js +251 -0
- package/src/publishers/index.js +33 -0
- package/src/publishers/markdown.js +32 -0
- package/src/publishers/notion.js +325 -0
- package/src/publishers/publish.js +31 -0
- package/src/renderers/render.js +256 -0
- package/src/renderers/renderDiff.js +139 -0
- package/src/renderers/renderMap.js +224 -0
- package/src/utils/branch.js +93 -0
- package/src/utils/logger.js +26 -0
- package/src/utils/retry.js +55 -0
- package/src/utils/update-check.js +150 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RepoLens Configuration Schema Validator
|
|
3
|
+
*
|
|
4
|
+
* Schema Version: 1
|
|
5
|
+
*
|
|
6
|
+
* This validator ensures .repolens.yml files conform to the expected structure.
|
|
7
|
+
* Breaking changes to this schema will require a major version bump.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const CURRENT_SCHEMA_VERSION = 1;
|
|
11
|
+
const SUPPORTED_PUBLISHERS = ["notion", "markdown"];
|
|
12
|
+
const SUPPORTED_PAGE_KEYS = [
|
|
13
|
+
"system_overview",
|
|
14
|
+
"module_catalog",
|
|
15
|
+
"api_surface",
|
|
16
|
+
"arch_diff",
|
|
17
|
+
"route_map",
|
|
18
|
+
"system_map",
|
|
19
|
+
// New AI-enhanced document types
|
|
20
|
+
"executive_summary",
|
|
21
|
+
"business_domains",
|
|
22
|
+
"architecture_overview",
|
|
23
|
+
"data_flows",
|
|
24
|
+
"change_impact",
|
|
25
|
+
"developer_onboarding"
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
class ValidationError extends Error {
|
|
29
|
+
constructor(message, path) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "ValidationError";
|
|
32
|
+
this.path = path;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validate the complete config object
|
|
38
|
+
*/
|
|
39
|
+
export function validateConfig(config) {
|
|
40
|
+
const errors = [];
|
|
41
|
+
|
|
42
|
+
// Check schema version
|
|
43
|
+
if (config.configVersion !== undefined) {
|
|
44
|
+
if (typeof config.configVersion !== "number") {
|
|
45
|
+
errors.push("configVersion must be a number");
|
|
46
|
+
} else if (config.configVersion > CURRENT_SCHEMA_VERSION) {
|
|
47
|
+
errors.push(
|
|
48
|
+
`Config schema version ${config.configVersion} is not supported. ` +
|
|
49
|
+
`This version of RepoLens supports schema version ${CURRENT_SCHEMA_VERSION}. ` +
|
|
50
|
+
`Please upgrade RepoLens or downgrade your config.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate project section
|
|
56
|
+
if (!config.project) {
|
|
57
|
+
errors.push("Missing required section: project");
|
|
58
|
+
} else {
|
|
59
|
+
if (!config.project.name || typeof config.project.name !== "string") {
|
|
60
|
+
errors.push("project.name is required and must be a string");
|
|
61
|
+
}
|
|
62
|
+
if (config.project.docs_title_prefix && typeof config.project.docs_title_prefix !== "string") {
|
|
63
|
+
errors.push("project.docs_title_prefix must be a string");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate publishers
|
|
68
|
+
if (!config.publishers) {
|
|
69
|
+
errors.push("Missing required section: publishers");
|
|
70
|
+
} else if (!Array.isArray(config.publishers)) {
|
|
71
|
+
errors.push("publishers must be an array");
|
|
72
|
+
} else if (config.publishers.length === 0) {
|
|
73
|
+
errors.push("publishers array cannot be empty");
|
|
74
|
+
} else {
|
|
75
|
+
config.publishers.forEach((pub, idx) => {
|
|
76
|
+
if (!SUPPORTED_PUBLISHERS.includes(pub)) {
|
|
77
|
+
errors.push(
|
|
78
|
+
`publishers[${idx}]: "${pub}" is not a valid publisher. ` +
|
|
79
|
+
`Supported: ${SUPPORTED_PUBLISHERS.join(", ")}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Validate scan section
|
|
86
|
+
if (!config.scan) {
|
|
87
|
+
errors.push("Missing required section: scan");
|
|
88
|
+
} else {
|
|
89
|
+
if (!config.scan.include || !Array.isArray(config.scan.include)) {
|
|
90
|
+
errors.push("scan.include is required and must be an array");
|
|
91
|
+
} else if (config.scan.include.length === 0) {
|
|
92
|
+
errors.push("scan.include cannot be empty");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!config.scan.ignore || !Array.isArray(config.scan.ignore)) {
|
|
96
|
+
errors.push("scan.ignore is required and must be an array");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate module_roots (optional but must be array if present)
|
|
101
|
+
if (config.module_roots !== undefined) {
|
|
102
|
+
if (!Array.isArray(config.module_roots)) {
|
|
103
|
+
errors.push("module_roots must be an array");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validate outputs section
|
|
108
|
+
if (!config.outputs) {
|
|
109
|
+
errors.push("Missing required section: outputs");
|
|
110
|
+
} else {
|
|
111
|
+
if (!config.outputs.pages || !Array.isArray(config.outputs.pages)) {
|
|
112
|
+
errors.push("outputs.pages is required and must be an array");
|
|
113
|
+
} else if (config.outputs.pages.length === 0) {
|
|
114
|
+
errors.push("outputs.pages cannot be empty");
|
|
115
|
+
} else {
|
|
116
|
+
config.outputs.pages.forEach((page, idx) => {
|
|
117
|
+
if (!page.key || typeof page.key !== "string") {
|
|
118
|
+
errors.push(`outputs.pages[${idx}]: missing required field "key"`);
|
|
119
|
+
} else if (!SUPPORTED_PAGE_KEYS.includes(page.key)) {
|
|
120
|
+
errors.push(
|
|
121
|
+
`outputs.pages[${idx}]: "${page.key}" is not a valid page key. ` +
|
|
122
|
+
`Supported: ${SUPPORTED_PAGE_KEYS.join(", ")}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!page.title || typeof page.title !== "string") {
|
|
127
|
+
errors.push(`outputs.pages[${idx}]: missing required field "title"`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (page.description && typeof page.description !== "string") {
|
|
131
|
+
errors.push(`outputs.pages[${idx}]: description must be a string`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Validate notion configuration (optional)
|
|
138
|
+
if (config.notion !== undefined) {
|
|
139
|
+
if (typeof config.notion !== "object" || Array.isArray(config.notion)) {
|
|
140
|
+
errors.push("notion must be an object");
|
|
141
|
+
} else {
|
|
142
|
+
// Validate branches filter
|
|
143
|
+
if (config.notion.branches !== undefined) {
|
|
144
|
+
if (!Array.isArray(config.notion.branches)) {
|
|
145
|
+
errors.push("notion.branches must be an array");
|
|
146
|
+
} else {
|
|
147
|
+
config.notion.branches.forEach((branch, idx) => {
|
|
148
|
+
if (typeof branch !== "string") {
|
|
149
|
+
errors.push(`notion.branches[${idx}] must be a string`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Validate includeBranchInTitle
|
|
156
|
+
if (config.notion.includeBranchInTitle !== undefined) {
|
|
157
|
+
if (typeof config.notion.includeBranchInTitle !== "boolean") {
|
|
158
|
+
errors.push("notion.includeBranchInTitle must be a boolean");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Validate feature flags (optional)
|
|
165
|
+
if (config.features !== undefined) {
|
|
166
|
+
if (typeof config.features !== "object" || Array.isArray(config.features)) {
|
|
167
|
+
errors.push("features must be an object");
|
|
168
|
+
} else {
|
|
169
|
+
Object.entries(config.features).forEach(([key, value]) => {
|
|
170
|
+
if (typeof value !== "boolean") {
|
|
171
|
+
errors.push(`features.${key} must be a boolean`);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Validate AI configuration (optional)
|
|
178
|
+
if (config.ai !== undefined) {
|
|
179
|
+
if (typeof config.ai !== "object" || Array.isArray(config.ai)) {
|
|
180
|
+
errors.push("ai must be an object");
|
|
181
|
+
} else {
|
|
182
|
+
if (config.ai.enabled !== undefined && typeof config.ai.enabled !== "boolean") {
|
|
183
|
+
errors.push("ai.enabled must be a boolean");
|
|
184
|
+
}
|
|
185
|
+
if (config.ai.mode !== undefined && !["hybrid", "full", "off"].includes(config.ai.mode)) {
|
|
186
|
+
errors.push("ai.mode must be one of: hybrid, full, off");
|
|
187
|
+
}
|
|
188
|
+
if (config.ai.temperature !== undefined && typeof config.ai.temperature !== "number") {
|
|
189
|
+
errors.push("ai.temperature must be a number");
|
|
190
|
+
}
|
|
191
|
+
if (config.ai.max_tokens !== undefined && typeof config.ai.max_tokens !== "number") {
|
|
192
|
+
errors.push("ai.max_tokens must be a number");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Validate documentation configuration (optional)
|
|
198
|
+
if (config.documentation !== undefined) {
|
|
199
|
+
if (typeof config.documentation !== "object" || Array.isArray(config.documentation)) {
|
|
200
|
+
errors.push("documentation must be an object");
|
|
201
|
+
} else {
|
|
202
|
+
if (config.documentation.output_dir && typeof config.documentation.output_dir !== "string") {
|
|
203
|
+
errors.push("documentation.output_dir must be a string");
|
|
204
|
+
}
|
|
205
|
+
if (config.documentation.include_artifacts !== undefined && typeof config.documentation.include_artifacts !== "boolean") {
|
|
206
|
+
errors.push("documentation.include_artifacts must be a boolean");
|
|
207
|
+
}
|
|
208
|
+
if (config.documentation.sections !== undefined) {
|
|
209
|
+
if (!Array.isArray(config.documentation.sections)) {
|
|
210
|
+
errors.push("documentation.sections must be an array");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Validate domains configuration (optional)
|
|
217
|
+
if (config.domains !== undefined) {
|
|
218
|
+
if (typeof config.domains !== "object" || Array.isArray(config.domains)) {
|
|
219
|
+
errors.push("domains must be an object");
|
|
220
|
+
} else {
|
|
221
|
+
Object.entries(config.domains).forEach(([domainKey, domain]) => {
|
|
222
|
+
if (typeof domain !== "object") {
|
|
223
|
+
errors.push(`domains.${domainKey} must be an object`);
|
|
224
|
+
} else {
|
|
225
|
+
if (!domain.match || !Array.isArray(domain.match)) {
|
|
226
|
+
errors.push(`domains.${domainKey}.match is required and must be an array`);
|
|
227
|
+
}
|
|
228
|
+
if (domain.description && typeof domain.description !== "string") {
|
|
229
|
+
errors.push(`domains.${domainKey}.description must be a string`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (errors.length > 0) {
|
|
237
|
+
const errorMessage = [
|
|
238
|
+
"Invalid .repolens.yml configuration:",
|
|
239
|
+
"",
|
|
240
|
+
...errors.map(e => ` • ${e}`),
|
|
241
|
+
"",
|
|
242
|
+
"See https://github.com/CHAPIBUNNY/repolens#configuration for documentation."
|
|
243
|
+
].join("\n");
|
|
244
|
+
|
|
245
|
+
throw new ValidationError(errorMessage);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get the schema version this validator supports
|
|
253
|
+
*/
|
|
254
|
+
export function getSchemaVersion() {
|
|
255
|
+
return CURRENT_SCHEMA_VERSION;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Check if a feature is enabled (with default fallback)
|
|
260
|
+
*/
|
|
261
|
+
export function isFeatureEnabled(config, featureName, defaultValue = true) {
|
|
262
|
+
if (!config.features) return defaultValue;
|
|
263
|
+
return config.features[featureName] !== undefined
|
|
264
|
+
? config.features[featureName]
|
|
265
|
+
: defaultValue;
|
|
266
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { validateConfig } from "./config-schema.js";
|
|
5
|
+
|
|
6
|
+
export async function loadConfig(configPath) {
|
|
7
|
+
const absoluteConfigPath = path.resolve(configPath);
|
|
8
|
+
const raw = await fs.readFile(absoluteConfigPath, "utf8");
|
|
9
|
+
const cfg = yaml.load(raw);
|
|
10
|
+
|
|
11
|
+
// Validate config against schema
|
|
12
|
+
validateConfig(cfg);
|
|
13
|
+
|
|
14
|
+
cfg.__configPath = absoluteConfigPath;
|
|
15
|
+
cfg.__repoRoot = path.dirname(absoluteConfigPath);
|
|
16
|
+
|
|
17
|
+
return cfg;
|
|
18
|
+
}
|
package/src/core/diff.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { warn } from "../utils/logger.js";
|
|
3
|
+
|
|
4
|
+
export function getGitDiff(baseRef = "origin/main") {
|
|
5
|
+
let output = "";
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
output = execSync(`git diff --name-status ${baseRef}`, {
|
|
9
|
+
encoding: "utf8"
|
|
10
|
+
});
|
|
11
|
+
} catch (error) {
|
|
12
|
+
warn("git diff failed, returning empty diff.");
|
|
13
|
+
return {
|
|
14
|
+
added: [],
|
|
15
|
+
removed: [],
|
|
16
|
+
modified: []
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lines = output.split("\n").filter(Boolean);
|
|
21
|
+
|
|
22
|
+
const added = [];
|
|
23
|
+
const removed = [];
|
|
24
|
+
const modified = [];
|
|
25
|
+
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const [status, file] = line.split("\t");
|
|
28
|
+
|
|
29
|
+
if (!file) continue;
|
|
30
|
+
|
|
31
|
+
if (status === "A") {
|
|
32
|
+
added.push(file);
|
|
33
|
+
} else if (status === "D") {
|
|
34
|
+
removed.push(file);
|
|
35
|
+
} else {
|
|
36
|
+
modified.push(file);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
added,
|
|
42
|
+
removed,
|
|
43
|
+
modified
|
|
44
|
+
};
|
|
45
|
+
}
|
package/src/core/scan.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import fg from "fast-glob";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { info, warn } from "../utils/logger.js";
|
|
5
|
+
|
|
6
|
+
const norm = (p) => p.replace(/\\/g, "/");
|
|
7
|
+
|
|
8
|
+
// Performance guardrails
|
|
9
|
+
const MAX_FILES_WARNING = 10000;
|
|
10
|
+
const MAX_FILES_LIMIT = 50000;
|
|
11
|
+
|
|
12
|
+
function isNextRoute(file) {
|
|
13
|
+
const f = norm(file);
|
|
14
|
+
return (
|
|
15
|
+
f.includes("/pages/api/") ||
|
|
16
|
+
(f.includes("/app/") && (f.endsWith("/route.ts") || f.endsWith("/route.js")))
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isNextPage(file) {
|
|
21
|
+
const f = norm(file);
|
|
22
|
+
|
|
23
|
+
if (f.includes("/app/")) {
|
|
24
|
+
return (
|
|
25
|
+
f.endsWith("/page.tsx") ||
|
|
26
|
+
f.endsWith("/page.jsx") ||
|
|
27
|
+
f.endsWith("/page.ts") ||
|
|
28
|
+
f.endsWith("/page.js")
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (f.includes("/pages/") && !f.includes("/pages/api/")) {
|
|
33
|
+
return (
|
|
34
|
+
f.endsWith(".tsx") ||
|
|
35
|
+
f.endsWith(".jsx") ||
|
|
36
|
+
f.endsWith(".ts") ||
|
|
37
|
+
f.endsWith(".js")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function readFileSafe(file) {
|
|
45
|
+
try {
|
|
46
|
+
return await fs.readFile(file, "utf8");
|
|
47
|
+
} catch {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function moduleKeyForFile(file, moduleRoots) {
|
|
53
|
+
const normalized = norm(file);
|
|
54
|
+
|
|
55
|
+
const sortedRoots = [...moduleRoots].sort((a, b) => b.length - a.length);
|
|
56
|
+
|
|
57
|
+
for (const root of sortedRoots) {
|
|
58
|
+
if (normalized === root) return root;
|
|
59
|
+
if (normalized.startsWith(`${root}/`)) {
|
|
60
|
+
const remainder = normalized.slice(root.length + 1);
|
|
61
|
+
const nextSegment = remainder.split("/")[0];
|
|
62
|
+
return nextSegment ? `${root}/${nextSegment}` : root;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const parts = normalized.split("/");
|
|
67
|
+
return parts.length > 1 ? parts[0] : "root";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function routePathFromFile(file) {
|
|
71
|
+
const f = norm(file);
|
|
72
|
+
|
|
73
|
+
if (f.includes("/app/")) {
|
|
74
|
+
const appIndex = f.indexOf("/app/");
|
|
75
|
+
const relative = f.slice(appIndex + 5);
|
|
76
|
+
|
|
77
|
+
const cleaned = relative
|
|
78
|
+
.replace(/\/page\.(ts|tsx|js|jsx)$/, "")
|
|
79
|
+
.replace(/\/route\.(ts|tsx|js|jsx)$/, "")
|
|
80
|
+
.replace(/\[(.*?)\]/g, ":$1")
|
|
81
|
+
.replace(/\/$/, "");
|
|
82
|
+
|
|
83
|
+
return cleaned ? `/${cleaned}` : "/";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (f.includes("/pages/api/")) {
|
|
87
|
+
const apiIndex = f.indexOf("/pages/api/");
|
|
88
|
+
const relative = f.slice(apiIndex + 11);
|
|
89
|
+
|
|
90
|
+
return "/api/" + relative.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (f.includes("/pages/")) {
|
|
94
|
+
const pagesIndex = f.indexOf("/pages/");
|
|
95
|
+
const relative = f.slice(pagesIndex + 7);
|
|
96
|
+
|
|
97
|
+
const cleaned = relative
|
|
98
|
+
.replace(/\.(ts|tsx|js|jsx)$/, "")
|
|
99
|
+
.replace(/\/index$/, "")
|
|
100
|
+
.replace(/\[(.*?)\]/g, ":$1")
|
|
101
|
+
.replace(/\/$/, "");
|
|
102
|
+
|
|
103
|
+
return cleaned ? `/${cleaned}` : "/";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return file;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function extractRepoMetadata(repoRoot) {
|
|
110
|
+
const metadata = {
|
|
111
|
+
hasPackageJson: false,
|
|
112
|
+
frameworks: [],
|
|
113
|
+
languages: new Set(),
|
|
114
|
+
buildTools: [],
|
|
115
|
+
testFrameworks: []
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Try to read package.json
|
|
119
|
+
try {
|
|
120
|
+
const pkgPath = path.join(repoRoot, "package.json");
|
|
121
|
+
const pkgContent = await fs.readFile(pkgPath, "utf8");
|
|
122
|
+
const pkg = JSON.parse(pkgContent);
|
|
123
|
+
metadata.hasPackageJson = true;
|
|
124
|
+
|
|
125
|
+
const allDeps = {
|
|
126
|
+
...pkg.dependencies,
|
|
127
|
+
...pkg.devDependencies,
|
|
128
|
+
...pkg.optionalDependencies
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Detect frameworks
|
|
132
|
+
if (allDeps["next"]) metadata.frameworks.push("Next.js");
|
|
133
|
+
if (allDeps["react"]) metadata.frameworks.push("React");
|
|
134
|
+
if (allDeps["vue"]) metadata.frameworks.push("Vue");
|
|
135
|
+
if (allDeps["angular"] || allDeps["@angular/core"]) metadata.frameworks.push("Angular");
|
|
136
|
+
if (allDeps["express"]) metadata.frameworks.push("Express");
|
|
137
|
+
if (allDeps["fastify"]) metadata.frameworks.push("Fastify");
|
|
138
|
+
if (allDeps["nestjs"] || allDeps["@nestjs/core"]) metadata.frameworks.push("NestJS");
|
|
139
|
+
if (allDeps["svelte"]) metadata.frameworks.push("Svelte");
|
|
140
|
+
if (allDeps["solid-js"]) metadata.frameworks.push("Solid");
|
|
141
|
+
|
|
142
|
+
// Detect test frameworks
|
|
143
|
+
if (allDeps["vitest"]) metadata.testFrameworks.push("Vitest");
|
|
144
|
+
if (allDeps["jest"]) metadata.testFrameworks.push("Jest");
|
|
145
|
+
if (allDeps["mocha"]) metadata.testFrameworks.push("Mocha");
|
|
146
|
+
if (allDeps["playwright"]) metadata.testFrameworks.push("Playwright");
|
|
147
|
+
if (allDeps["cypress"]) metadata.testFrameworks.push("Cypress");
|
|
148
|
+
|
|
149
|
+
// Detect build tools
|
|
150
|
+
if (allDeps["vite"]) metadata.buildTools.push("Vite");
|
|
151
|
+
if (allDeps["webpack"]) metadata.buildTools.push("Webpack");
|
|
152
|
+
if (allDeps["rollup"]) metadata.buildTools.push("Rollup");
|
|
153
|
+
if (allDeps["esbuild"]) metadata.buildTools.push("esbuild");
|
|
154
|
+
if (allDeps["turbo"]) metadata.buildTools.push("Turborepo");
|
|
155
|
+
|
|
156
|
+
// Detect TypeScript
|
|
157
|
+
if (allDeps["typescript"]) metadata.languages.add("TypeScript");
|
|
158
|
+
} catch {
|
|
159
|
+
// No package.json or invalid JSON
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return metadata;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function scanRepo(cfg) {
|
|
166
|
+
const repoRoot = cfg.__repoRoot;
|
|
167
|
+
|
|
168
|
+
const files = await fg(cfg.scan.include, {
|
|
169
|
+
cwd: repoRoot,
|
|
170
|
+
ignore: cfg.scan.ignore,
|
|
171
|
+
onlyFiles: true
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Performance guardrails
|
|
175
|
+
if (files.length > MAX_FILES_LIMIT) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Repository too large: ${files.length} files matched scan patterns. ` +
|
|
178
|
+
`Maximum supported: ${MAX_FILES_LIMIT}. Consider refining scan.include patterns.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (files.length > MAX_FILES_WARNING) {
|
|
183
|
+
warn(`Large repository detected: ${files.length} files. Scan may take longer than usual.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const moduleCounts = new Map();
|
|
187
|
+
|
|
188
|
+
for (const file of files) {
|
|
189
|
+
const key = moduleKeyForFile(file, cfg.module_roots || []);
|
|
190
|
+
moduleCounts.set(key, (moduleCounts.get(key) || 0) + 1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const modules = [...moduleCounts.entries()]
|
|
194
|
+
.map(([key, fileCount]) => ({ key, fileCount }))
|
|
195
|
+
.sort((a, b) => b.fileCount - a.fileCount);
|
|
196
|
+
|
|
197
|
+
const apiFiles = files.filter(isNextRoute);
|
|
198
|
+
const api = [];
|
|
199
|
+
|
|
200
|
+
for (const file of apiFiles) {
|
|
201
|
+
const absoluteFile = path.join(repoRoot, file);
|
|
202
|
+
const content = await readFileSafe(absoluteFile);
|
|
203
|
+
const methods = [];
|
|
204
|
+
|
|
205
|
+
if (content.includes("export async function GET")) methods.push("GET");
|
|
206
|
+
if (content.includes("export async function POST")) methods.push("POST");
|
|
207
|
+
if (content.includes("export async function PUT")) methods.push("PUT");
|
|
208
|
+
if (content.includes("export async function PATCH")) methods.push("PATCH");
|
|
209
|
+
if (content.includes("export async function DELETE")) methods.push("DELETE");
|
|
210
|
+
|
|
211
|
+
api.push({
|
|
212
|
+
file,
|
|
213
|
+
path: routePathFromFile(file),
|
|
214
|
+
methods: methods.length ? methods : ["UNKNOWN"]
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const pageFiles = files.filter(isNextPage);
|
|
219
|
+
const pages = pageFiles.map((file) => ({
|
|
220
|
+
file,
|
|
221
|
+
path: routePathFromFile(file)
|
|
222
|
+
}));
|
|
223
|
+
|
|
224
|
+
// Extract repository metadata
|
|
225
|
+
const metadata = await extractRepoMetadata(repoRoot);
|
|
226
|
+
|
|
227
|
+
// Detect external API integrations
|
|
228
|
+
const externalApis = await detectExternalApis(files, repoRoot);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
filesCount: files.length,
|
|
232
|
+
modules,
|
|
233
|
+
api,
|
|
234
|
+
pages,
|
|
235
|
+
metadata,
|
|
236
|
+
externalApis
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function detectExternalApis(files, repoRoot) {
|
|
241
|
+
const integrations = [];
|
|
242
|
+
const detectedServices = new Set();
|
|
243
|
+
|
|
244
|
+
// Common API patterns to detect
|
|
245
|
+
const apiPatterns = [
|
|
246
|
+
{
|
|
247
|
+
name: "OpenAI API",
|
|
248
|
+
patterns: [
|
|
249
|
+
/api\.openai\.com/,
|
|
250
|
+
/chat\/completions/,
|
|
251
|
+
/OPENAI.*API_KEY/,
|
|
252
|
+
/REPOLENS_AI_API_KEY/
|
|
253
|
+
],
|
|
254
|
+
category: "AI/ML"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "Notion API",
|
|
258
|
+
patterns: [
|
|
259
|
+
/api\.notion\.com/,
|
|
260
|
+
/NOTION_TOKEN/,
|
|
261
|
+
/notion.*pages/
|
|
262
|
+
],
|
|
263
|
+
category: "Publishing"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "npm Registry",
|
|
267
|
+
patterns: [
|
|
268
|
+
/registry\.npmjs\.org/,
|
|
269
|
+
/npm.*latest/
|
|
270
|
+
],
|
|
271
|
+
category: "Package Management"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: "GitHub API",
|
|
275
|
+
patterns: [
|
|
276
|
+
/api\.github\.com/,
|
|
277
|
+
/GITHUB_TOKEN/
|
|
278
|
+
],
|
|
279
|
+
category: "Version Control"
|
|
280
|
+
}
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
// Only scan JavaScript/TypeScript files for performance
|
|
284
|
+
const jsFiles = files.filter(f =>
|
|
285
|
+
f.endsWith('.js') || f.endsWith('.ts') ||
|
|
286
|
+
f.endsWith('.jsx') || f.endsWith('.tsx')
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
for (const file of jsFiles) {
|
|
290
|
+
const absoluteFile = path.join(repoRoot, file);
|
|
291
|
+
const content = await readFileSafe(absoluteFile);
|
|
292
|
+
|
|
293
|
+
if (!content) continue;
|
|
294
|
+
|
|
295
|
+
for (const { name, patterns, category } of apiPatterns) {
|
|
296
|
+
if (detectedServices.has(name)) continue;
|
|
297
|
+
|
|
298
|
+
const matched = patterns.some(pattern => pattern.test(content));
|
|
299
|
+
|
|
300
|
+
if (matched) {
|
|
301
|
+
integrations.push({
|
|
302
|
+
name,
|
|
303
|
+
category,
|
|
304
|
+
detectedIn: file
|
|
305
|
+
});
|
|
306
|
+
detectedServices.add(name);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return integrations;
|
|
312
|
+
}
|