@bantay/cli 0.1.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/LICENSE +21 -0
- package/README.md +63 -0
- package/package.json +46 -0
- package/src/aide/index.ts +301 -0
- package/src/aide/types.ts +94 -0
- package/src/checkers/auth.ts +111 -0
- package/src/checkers/logging.ts +133 -0
- package/src/checkers/registry.ts +46 -0
- package/src/checkers/schema.ts +157 -0
- package/src/checkers/types.ts +30 -0
- package/src/cli.ts +314 -0
- package/src/commands/aide.ts +571 -0
- package/src/commands/check.ts +363 -0
- package/src/commands/init.ts +75 -0
- package/src/config.ts +63 -0
- package/src/detectors/index.ts +61 -0
- package/src/detectors/nextjs.ts +97 -0
- package/src/detectors/prisma.ts +80 -0
- package/src/detectors/types.ts +29 -0
- package/src/diff.ts +124 -0
- package/src/export/aide-reader.ts +112 -0
- package/src/export/all.ts +29 -0
- package/src/export/claude.ts +221 -0
- package/src/export/cursor.ts +69 -0
- package/src/export/index.ts +28 -0
- package/src/export/invariants.ts +92 -0
- package/src/export/types.ts +70 -0
- package/src/generators/config.ts +170 -0
- package/src/generators/invariants.ts +161 -0
- package/src/prerequisites.ts +33 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { readFile, access } from "fs/promises";
|
|
2
|
+
import { spawn } from "bun";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { parseInvariants, type Invariant } from "../generators/invariants";
|
|
5
|
+
import { runChecker, hasChecker } from "../checkers/registry";
|
|
6
|
+
import { loadConfig } from "../config";
|
|
7
|
+
import { getGitDiff, shouldCheckInvariant } from "../diff";
|
|
8
|
+
import type { CheckResult, CheckerContext } from "../checkers/types";
|
|
9
|
+
import { read as readAide } from "../aide";
|
|
10
|
+
|
|
11
|
+
export interface CheckOptions {
|
|
12
|
+
id?: string;
|
|
13
|
+
diff?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CheckSummary {
|
|
17
|
+
passed: number;
|
|
18
|
+
failed: number;
|
|
19
|
+
skipped: number;
|
|
20
|
+
tested: number;
|
|
21
|
+
enforced: number;
|
|
22
|
+
total: number;
|
|
23
|
+
results: CheckResult[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
27
|
+
try {
|
|
28
|
+
await access(path);
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ProjectCheckerResult {
|
|
36
|
+
pass: boolean;
|
|
37
|
+
violations: Array<{ file: string; line: number; message: string }>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface InvariantEnforcementInfo {
|
|
41
|
+
checker?: string;
|
|
42
|
+
test?: string;
|
|
43
|
+
enforced?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load checker and test paths from bantay.aide for each invariant
|
|
48
|
+
*/
|
|
49
|
+
async function getEnforcementInfo(
|
|
50
|
+
projectPath: string
|
|
51
|
+
): Promise<Map<string, InvariantEnforcementInfo>> {
|
|
52
|
+
const info = new Map<string, InvariantEnforcementInfo>();
|
|
53
|
+
const aidePath = join(projectPath, "bantay.aide");
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const tree = await readAide(aidePath);
|
|
57
|
+
for (const [id, entity] of Object.entries(tree.entities)) {
|
|
58
|
+
if (id.startsWith("inv_")) {
|
|
59
|
+
const enforcement: InvariantEnforcementInfo = {};
|
|
60
|
+
if (entity.props?.checker) {
|
|
61
|
+
enforcement.checker = entity.props.checker as string;
|
|
62
|
+
}
|
|
63
|
+
if (entity.props?.test) {
|
|
64
|
+
enforcement.test = entity.props.test as string;
|
|
65
|
+
}
|
|
66
|
+
if (entity.props?.enforced) {
|
|
67
|
+
enforcement.enforced = entity.props.enforced as string;
|
|
68
|
+
}
|
|
69
|
+
if (enforcement.checker || enforcement.test || enforcement.enforced) {
|
|
70
|
+
info.set(id, enforcement);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// No aide file or can't read it
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return info;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run a project checker from .bantay/checkers/
|
|
83
|
+
*/
|
|
84
|
+
async function runProjectChecker(
|
|
85
|
+
checkerPath: string,
|
|
86
|
+
projectPath: string
|
|
87
|
+
): Promise<ProjectCheckerResult> {
|
|
88
|
+
// checkerPath is like "./no-eval" or "./bin-field"
|
|
89
|
+
const checkerFile = checkerPath.startsWith("./")
|
|
90
|
+
? checkerPath.slice(2) + ".ts"
|
|
91
|
+
: checkerPath + ".ts";
|
|
92
|
+
const fullPath = join(projectPath, ".bantay", "checkers", checkerFile);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Import and run the checker
|
|
96
|
+
const checker = await import(fullPath);
|
|
97
|
+
|
|
98
|
+
if (typeof checker.check !== "function") {
|
|
99
|
+
return {
|
|
100
|
+
pass: false,
|
|
101
|
+
violations: [
|
|
102
|
+
{
|
|
103
|
+
file: checkerFile,
|
|
104
|
+
line: 0,
|
|
105
|
+
message: "Checker does not export a check() function",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = await checker.check({ projectPath });
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
pass: result.pass === true,
|
|
115
|
+
violations: Array.isArray(result.violations) ? result.violations : [],
|
|
116
|
+
};
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
pass: false,
|
|
120
|
+
violations: [
|
|
121
|
+
{
|
|
122
|
+
file: checkerFile,
|
|
123
|
+
line: 0,
|
|
124
|
+
message: `Failed to run checker: ${err instanceof Error ? err.message : err}`,
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function runCheck(
|
|
132
|
+
projectPath: string,
|
|
133
|
+
options: CheckOptions = {}
|
|
134
|
+
): Promise<CheckSummary> {
|
|
135
|
+
const invariantsPath = join(projectPath, "invariants.md");
|
|
136
|
+
|
|
137
|
+
// Check if invariants.md exists
|
|
138
|
+
if (!(await fileExists(invariantsPath))) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
'No invariants.md found. Run "bantay init" to create one.'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Load invariants
|
|
145
|
+
const content = await readFile(invariantsPath, "utf-8");
|
|
146
|
+
let invariants = parseInvariants(content);
|
|
147
|
+
|
|
148
|
+
// Filter by ID if specified
|
|
149
|
+
if (options.id) {
|
|
150
|
+
invariants = invariants.filter((inv) => inv.id === options.id);
|
|
151
|
+
if (invariants.length === 0) {
|
|
152
|
+
throw new Error(`Invariant with ID "${options.id}" not found.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Load config
|
|
157
|
+
const config = await loadConfig(projectPath);
|
|
158
|
+
|
|
159
|
+
const context: CheckerContext = {
|
|
160
|
+
projectPath,
|
|
161
|
+
config,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Get diff info if in diff mode
|
|
165
|
+
let affectedCategories: Set<string> | null = null;
|
|
166
|
+
if (options.diff) {
|
|
167
|
+
const diffResult = await getGitDiff(projectPath, options.diff);
|
|
168
|
+
affectedCategories = diffResult.categories;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Get enforcement info (checker and test paths) from bantay.aide
|
|
172
|
+
const enforcementInfo = await getEnforcementInfo(projectPath);
|
|
173
|
+
|
|
174
|
+
// Run checkers for each invariant
|
|
175
|
+
const results: CheckResult[] = [];
|
|
176
|
+
|
|
177
|
+
for (const invariant of invariants) {
|
|
178
|
+
// In diff mode, skip invariants for unaffected categories
|
|
179
|
+
if (affectedCategories && !shouldCheckInvariant(invariant.category, affectedCategories)) {
|
|
180
|
+
// Skip this invariant entirely - don't even report it
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Get enforcement info for this invariant
|
|
185
|
+
const enforcement = enforcementInfo.get(invariant.id);
|
|
186
|
+
|
|
187
|
+
if (enforcement?.checker) {
|
|
188
|
+
// Run project checker
|
|
189
|
+
const projectResult = await runProjectChecker(enforcement.checker, projectPath);
|
|
190
|
+
results.push({
|
|
191
|
+
invariant,
|
|
192
|
+
status: projectResult.pass ? "pass" : "fail",
|
|
193
|
+
violations: projectResult.violations.map(v => ({
|
|
194
|
+
filePath: v.file,
|
|
195
|
+
line: v.line,
|
|
196
|
+
message: v.message,
|
|
197
|
+
})),
|
|
198
|
+
message: projectResult.pass ? undefined : `Project checker: ${enforcement.checker}`,
|
|
199
|
+
});
|
|
200
|
+
} else if (enforcement?.test) {
|
|
201
|
+
// Invariant is enforced by a test, not a checker
|
|
202
|
+
results.push({
|
|
203
|
+
invariant,
|
|
204
|
+
status: "tested",
|
|
205
|
+
violations: [],
|
|
206
|
+
message: `Enforced by test: ${enforcement.test}`,
|
|
207
|
+
});
|
|
208
|
+
} else if (enforcement?.enforced) {
|
|
209
|
+
// Invariant is enforced by implementation code directly
|
|
210
|
+
results.push({
|
|
211
|
+
invariant,
|
|
212
|
+
status: "enforced",
|
|
213
|
+
violations: [],
|
|
214
|
+
message: `Enforced by: ${enforcement.enforced}`,
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
// Try built-in checker
|
|
218
|
+
const result = await runChecker(invariant, context);
|
|
219
|
+
results.push(result);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Calculate summary
|
|
224
|
+
const summary: CheckSummary = {
|
|
225
|
+
passed: results.filter((r) => r.status === "pass").length,
|
|
226
|
+
failed: results.filter((r) => r.status === "fail").length,
|
|
227
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
228
|
+
tested: results.filter((r) => r.status === "tested").length,
|
|
229
|
+
enforced: results.filter((r) => r.status === "enforced").length,
|
|
230
|
+
total: results.length,
|
|
231
|
+
results,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return summary;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface JsonCheckOutput {
|
|
238
|
+
timestamp: string;
|
|
239
|
+
commit: string | null;
|
|
240
|
+
results: Array<{
|
|
241
|
+
id: string;
|
|
242
|
+
status: "pass" | "fail" | "skipped" | "tested" | "enforced";
|
|
243
|
+
message?: string;
|
|
244
|
+
violations?: Array<{
|
|
245
|
+
file: string;
|
|
246
|
+
line?: number;
|
|
247
|
+
message: string;
|
|
248
|
+
}>;
|
|
249
|
+
}>;
|
|
250
|
+
summary: {
|
|
251
|
+
passed: number;
|
|
252
|
+
failed: number;
|
|
253
|
+
skipped: number;
|
|
254
|
+
tested: number;
|
|
255
|
+
enforced: number;
|
|
256
|
+
total: number;
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function getGitCommit(projectPath: string): Promise<string | null> {
|
|
261
|
+
try {
|
|
262
|
+
const proc = spawn({
|
|
263
|
+
cmd: ["git", "rev-parse", "HEAD"],
|
|
264
|
+
cwd: projectPath,
|
|
265
|
+
stdout: "pipe",
|
|
266
|
+
stderr: "pipe",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const [exitCode, stdout] = await Promise.all([
|
|
270
|
+
proc.exited,
|
|
271
|
+
new Response(proc.stdout).text(),
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
if (exitCode === 0) {
|
|
275
|
+
return stdout.trim();
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
} catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function formatCheckResultsJson(
|
|
284
|
+
summary: CheckSummary,
|
|
285
|
+
projectPath: string
|
|
286
|
+
): Promise<JsonCheckOutput> {
|
|
287
|
+
const commit = await getGitCommit(projectPath);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
timestamp: new Date().toISOString(),
|
|
291
|
+
commit,
|
|
292
|
+
results: summary.results.map((result) => ({
|
|
293
|
+
id: result.invariant.id,
|
|
294
|
+
status: result.status,
|
|
295
|
+
message: result.message,
|
|
296
|
+
violations:
|
|
297
|
+
result.violations.length > 0
|
|
298
|
+
? result.violations.map((v) => ({
|
|
299
|
+
file: v.filePath,
|
|
300
|
+
line: v.line,
|
|
301
|
+
message: v.message,
|
|
302
|
+
}))
|
|
303
|
+
: undefined,
|
|
304
|
+
})),
|
|
305
|
+
summary: {
|
|
306
|
+
passed: summary.passed,
|
|
307
|
+
failed: summary.failed,
|
|
308
|
+
skipped: summary.skipped,
|
|
309
|
+
tested: summary.tested,
|
|
310
|
+
enforced: summary.enforced,
|
|
311
|
+
total: summary.total,
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function formatCheckResults(summary: CheckSummary): string {
|
|
317
|
+
const lines: string[] = [];
|
|
318
|
+
|
|
319
|
+
lines.push("Invariant Check Results");
|
|
320
|
+
lines.push("=======================");
|
|
321
|
+
lines.push("");
|
|
322
|
+
|
|
323
|
+
for (const result of summary.results) {
|
|
324
|
+
const statusIcon = result.status === "pass" ? "✓"
|
|
325
|
+
: result.status === "fail" ? "✗"
|
|
326
|
+
: result.status === "tested" ? "~"
|
|
327
|
+
: result.status === "enforced" ? "◆"
|
|
328
|
+
: "○";
|
|
329
|
+
const statusText = result.status.toUpperCase();
|
|
330
|
+
|
|
331
|
+
lines.push(`${statusIcon} [${result.invariant.id}] ${statusText}`);
|
|
332
|
+
lines.push(` ${result.invariant.statement}`);
|
|
333
|
+
|
|
334
|
+
if (result.message) {
|
|
335
|
+
lines.push(` Note: ${result.message}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const violation of result.violations) {
|
|
339
|
+
const location = violation.line
|
|
340
|
+
? `${violation.filePath}:${violation.line}`
|
|
341
|
+
: violation.filePath;
|
|
342
|
+
lines.push(` - ${location}: ${violation.message}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
lines.push("");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
lines.push("Summary");
|
|
349
|
+
lines.push("-------");
|
|
350
|
+
const parts = [`${summary.passed} passed`, `${summary.failed} failed`];
|
|
351
|
+
if (summary.tested > 0) {
|
|
352
|
+
parts.push(`${summary.tested} tested`);
|
|
353
|
+
}
|
|
354
|
+
if (summary.enforced > 0) {
|
|
355
|
+
parts.push(`${summary.enforced} enforced`);
|
|
356
|
+
}
|
|
357
|
+
if (summary.skipped > 0) {
|
|
358
|
+
parts.push(`${summary.skipped} skipped`);
|
|
359
|
+
}
|
|
360
|
+
lines.push(parts.join(", "));
|
|
361
|
+
|
|
362
|
+
return lines.join("\n");
|
|
363
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { writeFile, access } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { detectStack, type StackDetectionResult } from "../detectors";
|
|
4
|
+
import { generateInvariants } from "../generators/invariants";
|
|
5
|
+
import { generateConfig, configToYaml } from "../generators/config";
|
|
6
|
+
|
|
7
|
+
export interface InitOptions {
|
|
8
|
+
regenerateConfig?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface InitResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
filesCreated: string[];
|
|
14
|
+
warnings: string[];
|
|
15
|
+
detection: StackDetectionResult;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
19
|
+
try {
|
|
20
|
+
await access(path);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runInit(
|
|
28
|
+
projectPath: string,
|
|
29
|
+
options?: InitOptions
|
|
30
|
+
): Promise<InitResult> {
|
|
31
|
+
const filesCreated: string[] = [];
|
|
32
|
+
const warnings: string[] = [];
|
|
33
|
+
|
|
34
|
+
// Detect stack
|
|
35
|
+
const detection = await detectStack(projectPath);
|
|
36
|
+
|
|
37
|
+
// Add warnings for missing detections
|
|
38
|
+
if (!detection.framework) {
|
|
39
|
+
warnings.push("No framework detected");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const invariantsPath = join(projectPath, "invariants.md");
|
|
43
|
+
const configPath = join(projectPath, "bantay.config.yml");
|
|
44
|
+
|
|
45
|
+
// Check if invariants.md already exists
|
|
46
|
+
const invariantsExists = await fileExists(invariantsPath);
|
|
47
|
+
|
|
48
|
+
if (invariantsExists) {
|
|
49
|
+
warnings.push("invariants.md already exists");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Generate invariants.md if it doesn't exist
|
|
53
|
+
if (!invariantsExists) {
|
|
54
|
+
const invariantsContent = await generateInvariants(detection);
|
|
55
|
+
await writeFile(invariantsPath, invariantsContent);
|
|
56
|
+
filesCreated.push("invariants.md");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Generate config (always, or when regenerateConfig is true)
|
|
60
|
+
const shouldGenerateConfig = !invariantsExists || options?.regenerateConfig;
|
|
61
|
+
|
|
62
|
+
if (shouldGenerateConfig) {
|
|
63
|
+
const config = await generateConfig(detection, projectPath);
|
|
64
|
+
const configContent = configToYaml(config);
|
|
65
|
+
await writeFile(configPath, configContent);
|
|
66
|
+
filesCreated.push("bantay.config.yml");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
filesCreated,
|
|
72
|
+
warnings,
|
|
73
|
+
detection,
|
|
74
|
+
};
|
|
75
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import type { BantayConfig } from "./checkers/types";
|
|
3
|
+
|
|
4
|
+
export async function loadConfig(projectPath: string): Promise<BantayConfig> {
|
|
5
|
+
const configPath = `${projectPath}/bantay.config.yml`;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const content = await readFile(configPath, "utf-8");
|
|
9
|
+
return parseYamlConfig(content);
|
|
10
|
+
} catch {
|
|
11
|
+
// Default config if file doesn't exist
|
|
12
|
+
return {
|
|
13
|
+
sourceDirectories: ["src"],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseYamlConfig(content: string): BantayConfig {
|
|
19
|
+
const config: BantayConfig = {
|
|
20
|
+
sourceDirectories: [],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const lines = content.split("\n");
|
|
24
|
+
let currentKey: string | null = null;
|
|
25
|
+
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
|
|
29
|
+
// Skip empty lines and comments
|
|
30
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check for list item
|
|
35
|
+
if (trimmed.startsWith("- ")) {
|
|
36
|
+
const value = trimmed.slice(2).trim();
|
|
37
|
+
if (currentKey === "sourceDirectories") {
|
|
38
|
+
config.sourceDirectories.push(value);
|
|
39
|
+
} else if (currentKey === "routeDirectories") {
|
|
40
|
+
if (!config.routeDirectories) {
|
|
41
|
+
config.routeDirectories = [];
|
|
42
|
+
}
|
|
43
|
+
config.routeDirectories.push(value);
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check for key
|
|
49
|
+
const colonIndex = trimmed.indexOf(":");
|
|
50
|
+
if (colonIndex > 0) {
|
|
51
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
52
|
+
const value = trimmed.slice(colonIndex + 1).trim();
|
|
53
|
+
|
|
54
|
+
if (key === "schemaPath" && value) {
|
|
55
|
+
config.schemaPath = value;
|
|
56
|
+
} else {
|
|
57
|
+
currentKey = key;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return config;
|
|
63
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
FrameworkDetection,
|
|
3
|
+
OrmDetection,
|
|
4
|
+
AuthDetection,
|
|
5
|
+
StackDetectionResult,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
import type { StackDetectionResult, FrameworkDetection, OrmDetection, AuthDetection } from "./types";
|
|
9
|
+
import { detect as detectNextjs } from "./nextjs";
|
|
10
|
+
import { detect as detectPrisma } from "./prisma";
|
|
11
|
+
|
|
12
|
+
// Registry of framework detectors
|
|
13
|
+
const frameworkDetectors: Array<() => typeof detectNextjs> = [
|
|
14
|
+
() => detectNextjs,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// Registry of ORM detectors
|
|
18
|
+
const ormDetectors: Array<() => typeof detectPrisma> = [
|
|
19
|
+
() => detectPrisma,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Registry of auth detectors (none yet)
|
|
23
|
+
const authDetectors: Array<() => (projectPath: string) => Promise<AuthDetection | null>> = [];
|
|
24
|
+
|
|
25
|
+
export async function detectStack(projectPath: string): Promise<StackDetectionResult> {
|
|
26
|
+
let framework: FrameworkDetection | null = null;
|
|
27
|
+
let orm: OrmDetection | null = null;
|
|
28
|
+
let auth: AuthDetection | null = null;
|
|
29
|
+
|
|
30
|
+
// Run framework detectors
|
|
31
|
+
for (const getDetector of frameworkDetectors) {
|
|
32
|
+
const detector = getDetector();
|
|
33
|
+
const result = await detector(projectPath);
|
|
34
|
+
if (result && (!framework || result.confidence === "high")) {
|
|
35
|
+
framework = result;
|
|
36
|
+
if (result.confidence === "high") break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Run ORM detectors
|
|
41
|
+
for (const getDetector of ormDetectors) {
|
|
42
|
+
const detector = getDetector();
|
|
43
|
+
const result = await detector(projectPath);
|
|
44
|
+
if (result && (!orm || result.confidence === "high")) {
|
|
45
|
+
orm = result;
|
|
46
|
+
if (result.confidence === "high") break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Run auth detectors
|
|
51
|
+
for (const getDetector of authDetectors) {
|
|
52
|
+
const detector = getDetector();
|
|
53
|
+
const result = await detector(projectPath);
|
|
54
|
+
if (result && (!auth || result.confidence === "high")) {
|
|
55
|
+
auth = result;
|
|
56
|
+
if (result.confidence === "high") break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { framework, orm, auth };
|
|
61
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readFile, access, readdir } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { FrameworkDetection } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface NextjsDetector {
|
|
6
|
+
name: "nextjs";
|
|
7
|
+
detect(projectPath: string): Promise<FrameworkDetection | null>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
11
|
+
try {
|
|
12
|
+
await access(path);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function dirExists(path: string): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
await access(path);
|
|
22
|
+
const entries = await readdir(path);
|
|
23
|
+
return entries.length >= 0; // It's a directory if readdir works
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readPackageJson(projectPath: string): Promise<Record<string, unknown> | null> {
|
|
30
|
+
try {
|
|
31
|
+
const content = await readFile(join(projectPath, "package.json"), "utf-8");
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function detect(projectPath: string): Promise<FrameworkDetection | null> {
|
|
39
|
+
const pkg = await readPackageJson(projectPath);
|
|
40
|
+
|
|
41
|
+
let version: string | undefined;
|
|
42
|
+
let confidence: "high" | "medium" | "low" = "low";
|
|
43
|
+
let detected = false;
|
|
44
|
+
|
|
45
|
+
// Check package.json for next dependency
|
|
46
|
+
if (pkg) {
|
|
47
|
+
const deps = (pkg.dependencies ?? {}) as Record<string, string>;
|
|
48
|
+
const devDeps = (pkg.devDependencies ?? {}) as Record<string, string>;
|
|
49
|
+
|
|
50
|
+
if (deps.next) {
|
|
51
|
+
version = deps.next;
|
|
52
|
+
confidence = "high";
|
|
53
|
+
detected = true;
|
|
54
|
+
} else if (devDeps.next) {
|
|
55
|
+
version = devDeps.next;
|
|
56
|
+
confidence = "high";
|
|
57
|
+
detected = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for next.config.js or next.config.mjs
|
|
62
|
+
const configFiles = ["next.config.js", "next.config.mjs", "next.config.ts"];
|
|
63
|
+
for (const configFile of configFiles) {
|
|
64
|
+
if (await fileExists(join(projectPath, configFile))) {
|
|
65
|
+
detected = true;
|
|
66
|
+
if (confidence === "low") {
|
|
67
|
+
confidence = "high";
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!detected) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Detect router type
|
|
78
|
+
let router: "app" | "pages" | undefined;
|
|
79
|
+
|
|
80
|
+
const hasAppDir = await dirExists(join(projectPath, "app"));
|
|
81
|
+
const hasPagesDir = await dirExists(join(projectPath, "pages"));
|
|
82
|
+
const hasSrcAppDir = await dirExists(join(projectPath, "src", "app"));
|
|
83
|
+
const hasSrcPagesDir = await dirExists(join(projectPath, "src", "pages"));
|
|
84
|
+
|
|
85
|
+
if (hasAppDir || hasSrcAppDir) {
|
|
86
|
+
router = "app";
|
|
87
|
+
} else if (hasPagesDir || hasSrcPagesDir) {
|
|
88
|
+
router = "pages";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
name: "nextjs",
|
|
93
|
+
version,
|
|
94
|
+
confidence,
|
|
95
|
+
router,
|
|
96
|
+
};
|
|
97
|
+
}
|