@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,133 @@
|
|
|
1
|
+
import type { Checker, CheckResult, CheckerContext, CheckViolation } from "./types";
|
|
2
|
+
import type { Invariant } from "../generators/invariants";
|
|
3
|
+
import { Glob } from "bun";
|
|
4
|
+
import { readFile } from "fs/promises";
|
|
5
|
+
import { join, relative } from "path";
|
|
6
|
+
|
|
7
|
+
// PII field names to detect in log statements
|
|
8
|
+
const PII_PATTERNS = [
|
|
9
|
+
/\bemail\b/i,
|
|
10
|
+
/\bpassword\b/i,
|
|
11
|
+
/\bssn\b/i,
|
|
12
|
+
/\bsocialSecurityNumber\b/i,
|
|
13
|
+
/\bsocial_security_number\b/i,
|
|
14
|
+
/\bcreditCard\b/i,
|
|
15
|
+
/\bcredit_card\b/i,
|
|
16
|
+
/\bcardNumber\b/i,
|
|
17
|
+
/\bcard_number\b/i,
|
|
18
|
+
/\bcvv\b/i,
|
|
19
|
+
/\bpin\b/i,
|
|
20
|
+
/\bdateOfBirth\b/i,
|
|
21
|
+
/\bdate_of_birth\b/i,
|
|
22
|
+
/\bdob\b/i,
|
|
23
|
+
/\bphoneNumber\b/i,
|
|
24
|
+
/\bphone_number\b/i,
|
|
25
|
+
/\baddress\b/i,
|
|
26
|
+
/\bsecret\b/i,
|
|
27
|
+
/\btoken\b/i,
|
|
28
|
+
/\bapiKey\b/i,
|
|
29
|
+
/\bapi_key\b/i,
|
|
30
|
+
/\bprivateKey\b/i,
|
|
31
|
+
/\bprivate_key\b/i,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Log statement patterns
|
|
35
|
+
const LOG_PATTERNS = [
|
|
36
|
+
/console\.(log|warn|error|info|debug)\s*\([^)]*$/,
|
|
37
|
+
/console\.(log|warn|error|info|debug)\s*\(.*\)/,
|
|
38
|
+
/logger\.(log|warn|error|info|debug)\s*\(.*\)/,
|
|
39
|
+
/log\.(log|warn|error|info|debug)\s*\(.*\)/,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
interface LogViolation {
|
|
43
|
+
line: number;
|
|
44
|
+
piiField: string;
|
|
45
|
+
logContent: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function findPiiInLogStatements(content: string): LogViolation[] {
|
|
49
|
+
const violations: LogViolation[] = [];
|
|
50
|
+
const lines = content.split("\n");
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
const line = lines[i];
|
|
54
|
+
|
|
55
|
+
// Check if this line contains a log statement
|
|
56
|
+
const isLogStatement = LOG_PATTERNS.some((pattern) => pattern.test(line));
|
|
57
|
+
|
|
58
|
+
if (isLogStatement) {
|
|
59
|
+
// Check for PII patterns in the log statement
|
|
60
|
+
for (const piiPattern of PII_PATTERNS) {
|
|
61
|
+
if (piiPattern.test(line)) {
|
|
62
|
+
const match = line.match(piiPattern);
|
|
63
|
+
violations.push({
|
|
64
|
+
line: i + 1,
|
|
65
|
+
piiField: match ? match[0] : "unknown",
|
|
66
|
+
logContent: line.trim(),
|
|
67
|
+
});
|
|
68
|
+
break; // Only report one PII field per line
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return violations;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function scanSourceFiles(projectPath: string, sourceDirectories: string[]): Promise<string[]> {
|
|
78
|
+
const files: string[] = [];
|
|
79
|
+
const pattern = "**/*.{ts,tsx,js,jsx}";
|
|
80
|
+
|
|
81
|
+
for (const srcDir of sourceDirectories) {
|
|
82
|
+
const glob = new Glob(pattern);
|
|
83
|
+
const dirPath = join(projectPath, srcDir);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
for await (const file of glob.scan({ cwd: dirPath, absolute: true })) {
|
|
87
|
+
// Skip node_modules
|
|
88
|
+
if (!file.includes("node_modules")) {
|
|
89
|
+
files.push(file);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// Directory doesn't exist, skip
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return files;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const loggingChecker: Checker = {
|
|
101
|
+
category: "logging",
|
|
102
|
+
|
|
103
|
+
async check(invariant: Invariant, context: CheckerContext): Promise<CheckResult> {
|
|
104
|
+
const violations: CheckViolation[] = [];
|
|
105
|
+
const sourceFiles = await scanSourceFiles(
|
|
106
|
+
context.projectPath,
|
|
107
|
+
context.config.sourceDirectories
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
for (const filePath of sourceFiles) {
|
|
111
|
+
try {
|
|
112
|
+
const content = await readFile(filePath, "utf-8");
|
|
113
|
+
const piiViolations = findPiiInLogStatements(content);
|
|
114
|
+
|
|
115
|
+
for (const violation of piiViolations) {
|
|
116
|
+
violations.push({
|
|
117
|
+
filePath: relative(context.projectPath, filePath),
|
|
118
|
+
line: violation.line,
|
|
119
|
+
message: `Log statement contains PII field "${violation.piiField}"`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// File read error, skip
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
invariant,
|
|
129
|
+
status: violations.length > 0 ? "fail" : "pass",
|
|
130
|
+
violations,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Checker, CheckResult, CheckerContext } from "./types";
|
|
2
|
+
import type { Invariant } from "../generators/invariants";
|
|
3
|
+
import { authChecker } from "./auth";
|
|
4
|
+
import { schemaChecker } from "./schema";
|
|
5
|
+
import { loggingChecker } from "./logging";
|
|
6
|
+
|
|
7
|
+
const checkers: Map<string, Checker> = new Map();
|
|
8
|
+
|
|
9
|
+
export function registerChecker(checker: Checker): void {
|
|
10
|
+
checkers.set(checker.category, checker);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getChecker(category: string): Checker | undefined {
|
|
14
|
+
return checkers.get(category);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function hasChecker(category: string): boolean {
|
|
18
|
+
return checkers.has(category);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getAllCategories(): string[] {
|
|
22
|
+
return Array.from(checkers.keys());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runChecker(
|
|
26
|
+
invariant: Invariant,
|
|
27
|
+
context: CheckerContext
|
|
28
|
+
): Promise<CheckResult> {
|
|
29
|
+
const checker = getChecker(invariant.category);
|
|
30
|
+
|
|
31
|
+
if (!checker) {
|
|
32
|
+
return {
|
|
33
|
+
invariant,
|
|
34
|
+
status: "skipped",
|
|
35
|
+
violations: [],
|
|
36
|
+
message: `No checker registered for category "${invariant.category}"`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return checker.check(invariant, context);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Register built-in checkers
|
|
44
|
+
registerChecker(authChecker);
|
|
45
|
+
registerChecker(schemaChecker);
|
|
46
|
+
registerChecker(loggingChecker);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Checker, CheckResult, CheckerContext, CheckViolation } from "./types";
|
|
2
|
+
import type { Invariant } from "../generators/invariants";
|
|
3
|
+
import { readFile, access } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
interface PrismaModel {
|
|
7
|
+
name: string;
|
|
8
|
+
fields: string[];
|
|
9
|
+
startLine: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parsePrismaSchema(content: string): PrismaModel[] {
|
|
13
|
+
const models: PrismaModel[] = [];
|
|
14
|
+
const lines = content.split("\n");
|
|
15
|
+
|
|
16
|
+
let currentModel: PrismaModel | null = null;
|
|
17
|
+
let braceDepth = 0;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20
|
+
const line = lines[i];
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
|
|
23
|
+
// Start of a model
|
|
24
|
+
const modelMatch = trimmed.match(/^model\s+(\w+)\s*\{/);
|
|
25
|
+
if (modelMatch) {
|
|
26
|
+
currentModel = {
|
|
27
|
+
name: modelMatch[1],
|
|
28
|
+
fields: [],
|
|
29
|
+
startLine: i + 1,
|
|
30
|
+
};
|
|
31
|
+
braceDepth = 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (currentModel) {
|
|
36
|
+
// Track braces
|
|
37
|
+
for (const char of trimmed) {
|
|
38
|
+
if (char === "{") braceDepth++;
|
|
39
|
+
if (char === "}") braceDepth--;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse field name (first word of the line that isn't a comment or directive)
|
|
43
|
+
if (!trimmed.startsWith("//") && !trimmed.startsWith("@@")) {
|
|
44
|
+
const fieldMatch = trimmed.match(/^(\w+)\s+/);
|
|
45
|
+
if (fieldMatch) {
|
|
46
|
+
currentModel.fields.push(fieldMatch[1]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// End of model
|
|
51
|
+
if (braceDepth === 0) {
|
|
52
|
+
models.push(currentModel);
|
|
53
|
+
currentModel = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return models;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function checkModelTimestamps(model: PrismaModel): { hasCreatedAt: boolean; hasUpdatedAt: boolean } {
|
|
62
|
+
const fieldNames = model.fields.map((f) => f.toLowerCase());
|
|
63
|
+
return {
|
|
64
|
+
hasCreatedAt: fieldNames.includes("createdat"),
|
|
65
|
+
hasUpdatedAt: fieldNames.includes("updatedat"),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function findSchemaPath(projectPath: string, configSchemaPath?: string): Promise<string | null> {
|
|
70
|
+
// Try config path first
|
|
71
|
+
if (configSchemaPath) {
|
|
72
|
+
const fullPath = join(projectPath, configSchemaPath);
|
|
73
|
+
try {
|
|
74
|
+
await access(fullPath);
|
|
75
|
+
return fullPath;
|
|
76
|
+
} catch {
|
|
77
|
+
// Config path doesn't exist, try defaults
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Try common default locations
|
|
82
|
+
const defaultPaths = [
|
|
83
|
+
"prisma/schema.prisma",
|
|
84
|
+
"schema.prisma",
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
for (const defaultPath of defaultPaths) {
|
|
88
|
+
const fullPath = join(projectPath, defaultPath);
|
|
89
|
+
try {
|
|
90
|
+
await access(fullPath);
|
|
91
|
+
return fullPath;
|
|
92
|
+
} catch {
|
|
93
|
+
// Not found, try next
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const schemaChecker: Checker = {
|
|
101
|
+
category: "schema",
|
|
102
|
+
|
|
103
|
+
async check(invariant: Invariant, context: CheckerContext): Promise<CheckResult> {
|
|
104
|
+
const violations: CheckViolation[] = [];
|
|
105
|
+
|
|
106
|
+
// Find schema file
|
|
107
|
+
const schemaPath = await findSchemaPath(context.projectPath, context.config.schemaPath);
|
|
108
|
+
|
|
109
|
+
if (!schemaPath) {
|
|
110
|
+
return {
|
|
111
|
+
invariant,
|
|
112
|
+
status: "pass",
|
|
113
|
+
violations: [],
|
|
114
|
+
message: "No Prisma schema found",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const content = await readFile(schemaPath, "utf-8");
|
|
120
|
+
const models = parsePrismaSchema(content);
|
|
121
|
+
|
|
122
|
+
for (const model of models) {
|
|
123
|
+
const timestamps = checkModelTimestamps(model);
|
|
124
|
+
const missingFields: string[] = [];
|
|
125
|
+
|
|
126
|
+
if (!timestamps.hasCreatedAt) {
|
|
127
|
+
missingFields.push("createdAt");
|
|
128
|
+
}
|
|
129
|
+
if (!timestamps.hasUpdatedAt) {
|
|
130
|
+
missingFields.push("updatedAt");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (missingFields.length > 0) {
|
|
134
|
+
const relativePath = schemaPath.replace(context.projectPath + "/", "");
|
|
135
|
+
violations.push({
|
|
136
|
+
filePath: relativePath,
|
|
137
|
+
line: model.startLine,
|
|
138
|
+
message: `Model "${model.name}" is missing timestamp fields: ${missingFields.join(", ")}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return {
|
|
144
|
+
invariant,
|
|
145
|
+
status: "skipped",
|
|
146
|
+
violations: [],
|
|
147
|
+
message: `Failed to read schema: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
invariant,
|
|
153
|
+
status: violations.length > 0 ? "fail" : "pass",
|
|
154
|
+
violations,
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Invariant } from "../generators/invariants";
|
|
2
|
+
|
|
3
|
+
export interface CheckViolation {
|
|
4
|
+
filePath: string;
|
|
5
|
+
line?: number;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CheckResult {
|
|
10
|
+
invariant: Invariant;
|
|
11
|
+
status: "pass" | "fail" | "skipped" | "tested" | "enforced";
|
|
12
|
+
violations: CheckViolation[];
|
|
13
|
+
message?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CheckerContext {
|
|
17
|
+
projectPath: string;
|
|
18
|
+
config: BantayConfig;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BantayConfig {
|
|
22
|
+
sourceDirectories: string[];
|
|
23
|
+
routeDirectories?: string[];
|
|
24
|
+
schemaPath?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Checker {
|
|
28
|
+
category: string;
|
|
29
|
+
check(invariant: Invariant, context: CheckerContext): Promise<CheckResult>;
|
|
30
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { runInit } from "./commands/init";
|
|
3
|
+
import { runCheck, formatCheckResults, formatCheckResultsJson } from "./commands/check";
|
|
4
|
+
import { checkAllPrerequisites } from "./prerequisites";
|
|
5
|
+
import {
|
|
6
|
+
handleAideAdd,
|
|
7
|
+
handleAideUpdate,
|
|
8
|
+
handleAideRemove,
|
|
9
|
+
handleAideLink,
|
|
10
|
+
handleAideShow,
|
|
11
|
+
handleAideValidate,
|
|
12
|
+
handleAideLock,
|
|
13
|
+
printAideHelp,
|
|
14
|
+
} from "./commands/aide";
|
|
15
|
+
import { exportInvariants, exportClaude, exportCursor, exportAll } from "./export";
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const command = args[0];
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
22
|
+
printHelp();
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check prerequisites before any command runs
|
|
27
|
+
await runPrerequisiteCheck();
|
|
28
|
+
|
|
29
|
+
if (command === "init") {
|
|
30
|
+
await handleInit(args.slice(1));
|
|
31
|
+
} else if (command === "check") {
|
|
32
|
+
await handleCheck(args.slice(1));
|
|
33
|
+
} else if (command === "aide") {
|
|
34
|
+
await handleAide(args.slice(1));
|
|
35
|
+
} else if (command === "ci") {
|
|
36
|
+
console.error("bantay ci: Not yet implemented");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
} else if (command === "export") {
|
|
39
|
+
await handleExport(args.slice(1));
|
|
40
|
+
} else {
|
|
41
|
+
console.error(`Unknown command: ${command}`);
|
|
42
|
+
console.error('Run "bantay help" for usage information.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function runPrerequisiteCheck() {
|
|
48
|
+
console.error("Checking prerequisites...");
|
|
49
|
+
const prereqs = await checkAllPrerequisites();
|
|
50
|
+
|
|
51
|
+
if (!prereqs.passed) {
|
|
52
|
+
console.error("\nPrerequisite check failed:\n");
|
|
53
|
+
for (const { name, result } of prereqs.results) {
|
|
54
|
+
if (!result.available) {
|
|
55
|
+
console.error(` ${name}: FAILED`);
|
|
56
|
+
console.error(` ${result.error}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function printHelp() {
|
|
64
|
+
console.log(`
|
|
65
|
+
Bantay CLI - Enforce project invariants on every PR
|
|
66
|
+
|
|
67
|
+
Usage: bantay <command> [options]
|
|
68
|
+
|
|
69
|
+
Commands:
|
|
70
|
+
init Initialize Bantay in the current project
|
|
71
|
+
check Check all invariants against the codebase
|
|
72
|
+
aide Manage the aide entity tree (add, remove, link, show, validate, lock)
|
|
73
|
+
ci Generate CI workflow configuration
|
|
74
|
+
export Export invariants to agent context files
|
|
75
|
+
|
|
76
|
+
Options:
|
|
77
|
+
-h, --help Show this help message
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
bantay init Initialize in current directory
|
|
81
|
+
bantay check Run full invariant check
|
|
82
|
+
bantay check --diff HEAD~1 Check only affected invariants
|
|
83
|
+
bantay aide show Show the aide entity tree
|
|
84
|
+
bantay aide add inv_test --parent invariants --prop "statement=Test"
|
|
85
|
+
bantay ci --github-actions Generate GitHub Actions workflow
|
|
86
|
+
bantay export all Export all targets
|
|
87
|
+
bantay export invariants Generate invariants.md from bantay.aide
|
|
88
|
+
bantay export claude Export to CLAUDE.md
|
|
89
|
+
bantay export cursor Export to .cursorrules
|
|
90
|
+
|
|
91
|
+
Run "bantay aide help" for aide subcommand details.
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function handleInit(args: string[]) {
|
|
96
|
+
const projectPath = process.cwd();
|
|
97
|
+
const regenerateConfig = args.includes("--regenerate-config");
|
|
98
|
+
const dryRun = args.includes("--dry-run");
|
|
99
|
+
|
|
100
|
+
console.log("Initializing Bantay...\n");
|
|
101
|
+
|
|
102
|
+
if (dryRun) {
|
|
103
|
+
console.log("Dry run mode - no files will be created.");
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const result = await runInit(projectPath, { regenerateConfig });
|
|
109
|
+
|
|
110
|
+
// Display detection results
|
|
111
|
+
console.log("Stack Detection:");
|
|
112
|
+
if (result.detection.framework) {
|
|
113
|
+
console.log(` Framework: ${result.detection.framework.name} (${result.detection.framework.confidence} confidence)`);
|
|
114
|
+
if (result.detection.framework.router) {
|
|
115
|
+
console.log(` Router: ${result.detection.framework.router}`);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
console.log(" Framework: Not detected");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (result.detection.orm) {
|
|
122
|
+
console.log(` ORM: ${result.detection.orm.name} (${result.detection.orm.confidence} confidence)`);
|
|
123
|
+
if (result.detection.orm.schemaPath) {
|
|
124
|
+
console.log(` Schema: ${result.detection.orm.schemaPath}`);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
console.log(" ORM: Not detected");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (result.detection.auth) {
|
|
131
|
+
console.log(` Auth: ${result.detection.auth.name} (${result.detection.auth.confidence} confidence)`);
|
|
132
|
+
} else {
|
|
133
|
+
console.log(" Auth: Not detected");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log("");
|
|
137
|
+
|
|
138
|
+
// Display warnings
|
|
139
|
+
for (const warning of result.warnings) {
|
|
140
|
+
console.log(`Warning: ${warning}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Display created files
|
|
144
|
+
if (result.filesCreated.length > 0) {
|
|
145
|
+
console.log("\nCreated files:");
|
|
146
|
+
for (const file of result.filesCreated) {
|
|
147
|
+
console.log(` - ${file}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log("\nBantay initialized successfully!");
|
|
152
|
+
console.log("Edit invariants.md to customize your project invariants.");
|
|
153
|
+
console.log('Run "bantay check" to verify invariants.');
|
|
154
|
+
|
|
155
|
+
process.exit(0);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error("Error initializing Bantay:", error);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function handleCheck(args: string[]) {
|
|
163
|
+
const projectPath = process.cwd();
|
|
164
|
+
|
|
165
|
+
// Parse options
|
|
166
|
+
const idIndex = args.indexOf("--id");
|
|
167
|
+
const diffIndex = args.indexOf("--diff");
|
|
168
|
+
const jsonOutput = args.includes("--json");
|
|
169
|
+
|
|
170
|
+
const options: { id?: string; diff?: string } = {};
|
|
171
|
+
|
|
172
|
+
if (idIndex !== -1 && args[idIndex + 1]) {
|
|
173
|
+
options.id = args[idIndex + 1];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (diffIndex !== -1) {
|
|
177
|
+
options.diff = args[diffIndex + 1] || "HEAD";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const summary = await runCheck(projectPath, options);
|
|
182
|
+
|
|
183
|
+
if (jsonOutput) {
|
|
184
|
+
// JSON output to stdout, nothing to stderr
|
|
185
|
+
const jsonResult = await formatCheckResultsJson(summary, projectPath);
|
|
186
|
+
console.log(JSON.stringify(jsonResult, null, 2));
|
|
187
|
+
} else {
|
|
188
|
+
// Human-readable output to stderr
|
|
189
|
+
const output = formatCheckResults(summary);
|
|
190
|
+
console.error(output);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Exit non-zero if any invariants failed
|
|
194
|
+
if (summary.failed > 0) {
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
process.exit(0);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error instanceof Error) {
|
|
201
|
+
console.error(`Error: ${error.message}`);
|
|
202
|
+
} else {
|
|
203
|
+
console.error("Error running check:", error);
|
|
204
|
+
}
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function handleExport(args: string[]) {
|
|
210
|
+
const projectPath = process.cwd();
|
|
211
|
+
|
|
212
|
+
// Parse target from args
|
|
213
|
+
// Formats: bantay export invariants, bantay export claude, bantay export cursor
|
|
214
|
+
// Or: bantay export --target claude
|
|
215
|
+
const targetIndex = args.indexOf("--target");
|
|
216
|
+
let target: string;
|
|
217
|
+
|
|
218
|
+
if (targetIndex !== -1 && args[targetIndex + 1]) {
|
|
219
|
+
target = args[targetIndex + 1];
|
|
220
|
+
} else if (args[0] && !args[0].startsWith("-")) {
|
|
221
|
+
target = args[0];
|
|
222
|
+
} else {
|
|
223
|
+
console.error("Usage: bantay export <target>");
|
|
224
|
+
console.error("");
|
|
225
|
+
console.error("Targets:");
|
|
226
|
+
console.error(" all Export all targets (invariants, claude, cursor)");
|
|
227
|
+
console.error(" invariants Generate invariants.md from bantay.aide");
|
|
228
|
+
console.error(" claude Export to CLAUDE.md with section markers");
|
|
229
|
+
console.error(" cursor Export to .cursorrules with section markers");
|
|
230
|
+
console.error("");
|
|
231
|
+
console.error("Examples:");
|
|
232
|
+
console.error(" bantay export invariants");
|
|
233
|
+
console.error(" bantay export claude");
|
|
234
|
+
console.error(" bantay export --target cursor");
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const dryRun = args.includes("--dry-run");
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
if (target === "all") {
|
|
242
|
+
const results = await exportAll(projectPath, { dryRun });
|
|
243
|
+
console.log("Exported all targets:");
|
|
244
|
+
for (const r of results) {
|
|
245
|
+
console.log(` ${r.target}: ${r.outputPath} (${r.bytesWritten} bytes)`);
|
|
246
|
+
}
|
|
247
|
+
} else if (target === "invariants") {
|
|
248
|
+
const result = await exportInvariants(projectPath, { dryRun });
|
|
249
|
+
console.log(`Exported invariants to ${result.outputPath}`);
|
|
250
|
+
console.log(` ${result.bytesWritten} bytes written`);
|
|
251
|
+
} else if (target === "claude") {
|
|
252
|
+
const result = await exportClaude(projectPath, { dryRun });
|
|
253
|
+
console.log(`Exported to ${result.outputPath}`);
|
|
254
|
+
console.log(` ${result.bytesWritten} bytes written`);
|
|
255
|
+
} else if (target === "cursor") {
|
|
256
|
+
const result = await exportCursor(projectPath, { dryRun });
|
|
257
|
+
console.log(`Exported to ${result.outputPath}`);
|
|
258
|
+
console.log(` ${result.bytesWritten} bytes written`);
|
|
259
|
+
} else {
|
|
260
|
+
console.error(`Unknown export target: ${target}`);
|
|
261
|
+
console.error('Valid targets: all, invariants, claude, cursor');
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (dryRun) {
|
|
266
|
+
console.log("\n(Dry run - no files were written)");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
process.exit(0);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error instanceof Error) {
|
|
272
|
+
console.error(`Error: ${error.message}`);
|
|
273
|
+
} else {
|
|
274
|
+
console.error("Error running export:", error);
|
|
275
|
+
}
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function handleAide(args: string[]) {
|
|
281
|
+
const subcommand = args[0];
|
|
282
|
+
|
|
283
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
284
|
+
printAideHelp();
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const subArgs = args.slice(1);
|
|
289
|
+
|
|
290
|
+
if (subcommand === "add") {
|
|
291
|
+
await handleAideAdd(subArgs);
|
|
292
|
+
} else if (subcommand === "update") {
|
|
293
|
+
await handleAideUpdate(subArgs);
|
|
294
|
+
} else if (subcommand === "remove") {
|
|
295
|
+
await handleAideRemove(subArgs);
|
|
296
|
+
} else if (subcommand === "link") {
|
|
297
|
+
await handleAideLink(subArgs);
|
|
298
|
+
} else if (subcommand === "show") {
|
|
299
|
+
await handleAideShow(subArgs);
|
|
300
|
+
} else if (subcommand === "validate") {
|
|
301
|
+
await handleAideValidate(subArgs);
|
|
302
|
+
} else if (subcommand === "lock") {
|
|
303
|
+
await handleAideLock(subArgs);
|
|
304
|
+
} else {
|
|
305
|
+
console.error(`Unknown aide subcommand: ${subcommand}`);
|
|
306
|
+
console.error('Run "bantay aide help" for usage information.');
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
main().catch((error) => {
|
|
312
|
+
console.error("Fatal error:", error);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
});
|