@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,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export invariants.md from the aide tree
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile, writeFile } from "fs/promises";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { read as readAide } from "../aide";
|
|
8
|
+
import { extractInvariants, groupBy } from "./aide-reader";
|
|
9
|
+
import type { ExportOptions, ExportResult, ExtractedInvariant } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate invariants.md content from the aide tree
|
|
13
|
+
*/
|
|
14
|
+
export function generateInvariantsMd(invariants: ExtractedInvariant[]): string {
|
|
15
|
+
const lines: string[] = [];
|
|
16
|
+
|
|
17
|
+
lines.push("# Invariants");
|
|
18
|
+
lines.push("");
|
|
19
|
+
lines.push("Rules this project must never break. Generated from bantay.aide.");
|
|
20
|
+
lines.push("");
|
|
21
|
+
|
|
22
|
+
// Group by category
|
|
23
|
+
const byCategory = groupBy(invariants, (inv) => inv.category);
|
|
24
|
+
|
|
25
|
+
// Sort categories for consistent output
|
|
26
|
+
const sortedCategories = Array.from(byCategory.keys()).sort();
|
|
27
|
+
|
|
28
|
+
for (const category of sortedCategories) {
|
|
29
|
+
const categoryInvariants = byCategory.get(category) || [];
|
|
30
|
+
|
|
31
|
+
lines.push(`## ${formatCategoryTitle(category)}`);
|
|
32
|
+
lines.push("");
|
|
33
|
+
|
|
34
|
+
for (const inv of categoryInvariants) {
|
|
35
|
+
// Format: - [ ] ID: statement
|
|
36
|
+
const checkbox = inv.done ? "[x]" : "[ ]";
|
|
37
|
+
lines.push(`- ${checkbox} **${inv.id}**: ${inv.statement}`);
|
|
38
|
+
|
|
39
|
+
// Add threat signal as sub-item if present
|
|
40
|
+
if (inv.threatSignal) {
|
|
41
|
+
lines.push(` - Threat: ${inv.threatSignal}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
lines.push("");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format a category ID into a readable title
|
|
53
|
+
* e.g., "correctness" -> "Correctness"
|
|
54
|
+
*/
|
|
55
|
+
function formatCategoryTitle(category: string): string {
|
|
56
|
+
return category
|
|
57
|
+
.split("_")
|
|
58
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
59
|
+
.join(" ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Export invariants.md from the aide file
|
|
64
|
+
*/
|
|
65
|
+
export async function exportInvariants(
|
|
66
|
+
projectPath: string,
|
|
67
|
+
options: ExportOptions = {}
|
|
68
|
+
): Promise<ExportResult> {
|
|
69
|
+
const aidePath = options.aidePath || join(projectPath, "bantay.aide");
|
|
70
|
+
const outputPath = options.outputPath || join(projectPath, "invariants.md");
|
|
71
|
+
|
|
72
|
+
// Read the aide tree
|
|
73
|
+
const tree = await readAide(aidePath);
|
|
74
|
+
|
|
75
|
+
// Extract invariants
|
|
76
|
+
const invariants = extractInvariants(tree);
|
|
77
|
+
|
|
78
|
+
// Generate content
|
|
79
|
+
const content = generateInvariantsMd(invariants);
|
|
80
|
+
|
|
81
|
+
// Write unless dry run
|
|
82
|
+
if (!options.dryRun) {
|
|
83
|
+
await writeFile(outputPath, content, "utf-8");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
target: "invariants",
|
|
88
|
+
outputPath,
|
|
89
|
+
content,
|
|
90
|
+
bytesWritten: Buffer.byteLength(content, "utf-8"),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export types for bantay export command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An invariant extracted from the aide tree
|
|
7
|
+
*/
|
|
8
|
+
export interface ExtractedInvariant {
|
|
9
|
+
id: string;
|
|
10
|
+
statement: string;
|
|
11
|
+
category: string;
|
|
12
|
+
threatSignal?: string;
|
|
13
|
+
done?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A constraint extracted from the aide tree
|
|
18
|
+
*/
|
|
19
|
+
export interface ExtractedConstraint {
|
|
20
|
+
id: string;
|
|
21
|
+
text: string;
|
|
22
|
+
domain: string;
|
|
23
|
+
rationale?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A foundation extracted from the aide tree
|
|
28
|
+
*/
|
|
29
|
+
export interface ExtractedFoundation {
|
|
30
|
+
id: string;
|
|
31
|
+
text: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A wisdom entry extracted from the aide tree
|
|
36
|
+
*/
|
|
37
|
+
export interface ExtractedWisdom {
|
|
38
|
+
id: string;
|
|
39
|
+
text: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Export target types
|
|
44
|
+
*/
|
|
45
|
+
export type ExportTarget = "invariants" | "claude" | "cursor" | "codex";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Options for export commands
|
|
49
|
+
*/
|
|
50
|
+
export interface ExportOptions {
|
|
51
|
+
aidePath?: string;
|
|
52
|
+
outputPath?: string;
|
|
53
|
+
dryRun?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Result of an export operation
|
|
58
|
+
*/
|
|
59
|
+
export interface ExportResult {
|
|
60
|
+
target: ExportTarget;
|
|
61
|
+
outputPath: string;
|
|
62
|
+
content: string;
|
|
63
|
+
bytesWritten: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Section markers for agent context files
|
|
68
|
+
*/
|
|
69
|
+
export const SECTION_START = "<!-- bantay:start -->";
|
|
70
|
+
export const SECTION_END = "<!-- bantay:end -->";
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { StackDetectionResult } from "../detectors";
|
|
2
|
+
|
|
3
|
+
export interface BantayConfig {
|
|
4
|
+
source: {
|
|
5
|
+
include: string[];
|
|
6
|
+
exclude?: string[];
|
|
7
|
+
};
|
|
8
|
+
schema?: {
|
|
9
|
+
prisma?: string;
|
|
10
|
+
};
|
|
11
|
+
routes?: {
|
|
12
|
+
include: string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function generateConfig(
|
|
17
|
+
stack: StackDetectionResult,
|
|
18
|
+
_projectPath: string
|
|
19
|
+
): Promise<BantayConfig> {
|
|
20
|
+
const config: BantayConfig = {
|
|
21
|
+
source: {
|
|
22
|
+
include: ["src/**/*"],
|
|
23
|
+
exclude: ["node_modules/**", "dist/**", ".next/**", "build/**"],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Add app directory for Next.js app router
|
|
28
|
+
if (stack.framework?.name === "nextjs") {
|
|
29
|
+
if (stack.framework.router === "app") {
|
|
30
|
+
config.source.include.push("app/**/*");
|
|
31
|
+
config.routes = {
|
|
32
|
+
include: ["app/**/route.ts", "app/**/route.js"],
|
|
33
|
+
};
|
|
34
|
+
} else if (stack.framework.router === "pages") {
|
|
35
|
+
config.source.include.push("pages/**/*");
|
|
36
|
+
config.routes = {
|
|
37
|
+
include: ["pages/api/**/*.ts", "pages/api/**/*.js"],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add Prisma schema path
|
|
43
|
+
if (stack.orm?.name === "prisma" && stack.orm.schemaPath) {
|
|
44
|
+
config.schema = {
|
|
45
|
+
prisma: stack.orm.schemaPath,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function configToYaml(config: BantayConfig): string {
|
|
53
|
+
const lines: string[] = [
|
|
54
|
+
"# Bantay configuration",
|
|
55
|
+
"# Edit this file to customize invariant checking",
|
|
56
|
+
"",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Source section
|
|
60
|
+
lines.push("source:");
|
|
61
|
+
lines.push(" include:");
|
|
62
|
+
for (const pattern of config.source.include) {
|
|
63
|
+
lines.push(` - ${pattern}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (config.source.exclude && config.source.exclude.length > 0) {
|
|
67
|
+
lines.push(" exclude:");
|
|
68
|
+
for (const pattern of config.source.exclude) {
|
|
69
|
+
lines.push(` - ${pattern}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Schema section
|
|
74
|
+
if (config.schema) {
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push("schema:");
|
|
77
|
+
if (config.schema.prisma) {
|
|
78
|
+
lines.push(` prisma: ${config.schema.prisma}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Routes section
|
|
83
|
+
if (config.routes) {
|
|
84
|
+
lines.push("");
|
|
85
|
+
lines.push("routes:");
|
|
86
|
+
lines.push(" include:");
|
|
87
|
+
for (const pattern of config.routes.include) {
|
|
88
|
+
lines.push(` - ${pattern}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
lines.push("");
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parseConfig(yaml: string): BantayConfig {
|
|
97
|
+
const config: BantayConfig = {
|
|
98
|
+
source: {
|
|
99
|
+
include: [],
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const lines = yaml.split("\n");
|
|
104
|
+
let currentSection: string | null = null;
|
|
105
|
+
let currentSubsection: string | null = null;
|
|
106
|
+
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
|
|
110
|
+
// Skip comments and empty lines
|
|
111
|
+
if (trimmed.startsWith("#") || trimmed === "") {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check for top-level sections
|
|
116
|
+
if (!line.startsWith(" ") && !line.startsWith("\t")) {
|
|
117
|
+
if (trimmed.startsWith("source:")) {
|
|
118
|
+
currentSection = "source";
|
|
119
|
+
currentSubsection = null;
|
|
120
|
+
} else if (trimmed.startsWith("schema:")) {
|
|
121
|
+
currentSection = "schema";
|
|
122
|
+
currentSubsection = null;
|
|
123
|
+
} else if (trimmed.startsWith("routes:")) {
|
|
124
|
+
currentSection = "routes";
|
|
125
|
+
currentSubsection = null;
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check for subsections
|
|
131
|
+
if (trimmed.startsWith("include:")) {
|
|
132
|
+
currentSubsection = "include";
|
|
133
|
+
continue;
|
|
134
|
+
} else if (trimmed.startsWith("exclude:")) {
|
|
135
|
+
currentSubsection = "exclude";
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle list items
|
|
140
|
+
if (trimmed.startsWith("- ")) {
|
|
141
|
+
const value = trimmed.slice(2);
|
|
142
|
+
|
|
143
|
+
if (currentSection === "source" && currentSubsection === "include") {
|
|
144
|
+
config.source.include.push(value);
|
|
145
|
+
} else if (currentSection === "source" && currentSubsection === "exclude") {
|
|
146
|
+
config.source.exclude = config.source.exclude ?? [];
|
|
147
|
+
config.source.exclude.push(value);
|
|
148
|
+
} else if (currentSection === "routes" && currentSubsection === "include") {
|
|
149
|
+
config.routes = config.routes ?? { include: [] };
|
|
150
|
+
config.routes.include.push(value);
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle key-value pairs
|
|
156
|
+
if (trimmed.includes(":")) {
|
|
157
|
+
const [key, ...rest] = trimmed.split(":");
|
|
158
|
+
const value = rest.join(":").trim();
|
|
159
|
+
|
|
160
|
+
if (currentSection === "schema") {
|
|
161
|
+
if (key.trim() === "prisma") {
|
|
162
|
+
config.schema = config.schema ?? {};
|
|
163
|
+
config.schema.prisma = value;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return config;
|
|
170
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { StackDetectionResult } from "../detectors";
|
|
2
|
+
|
|
3
|
+
export interface Invariant {
|
|
4
|
+
id: string;
|
|
5
|
+
category: string;
|
|
6
|
+
statement: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface InvariantTemplate {
|
|
10
|
+
id: string;
|
|
11
|
+
category: string;
|
|
12
|
+
statement: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Universal invariants that apply to all projects
|
|
16
|
+
const universalInvariants: InvariantTemplate[] = [
|
|
17
|
+
{
|
|
18
|
+
id: "INV-001",
|
|
19
|
+
category: "security",
|
|
20
|
+
statement: "No secrets or credentials committed to version control",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "INV-002",
|
|
24
|
+
category: "security",
|
|
25
|
+
statement: "All user input must be validated before processing",
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Next.js specific invariants
|
|
30
|
+
const nextjsInvariants: InvariantTemplate[] = [
|
|
31
|
+
{
|
|
32
|
+
id: "INV-010",
|
|
33
|
+
category: "auth",
|
|
34
|
+
statement: "All API routes must check authentication before processing requests (auth-on-routes)",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "INV-011",
|
|
38
|
+
category: "auth",
|
|
39
|
+
statement: "Protected pages must redirect unauthenticated users",
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Prisma specific invariants
|
|
44
|
+
const prismaInvariants: InvariantTemplate[] = [
|
|
45
|
+
{
|
|
46
|
+
id: "INV-020",
|
|
47
|
+
category: "schema",
|
|
48
|
+
statement: "All database tables must have createdAt and updatedAt timestamps (timestamps-on-tables)",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "INV-021",
|
|
52
|
+
category: "schema",
|
|
53
|
+
statement: "All database tables must use soft-delete pattern with deletedAt column (soft-delete)",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "INV-022",
|
|
57
|
+
category: "schema",
|
|
58
|
+
statement: "No raw SQL queries - use Prisma client methods only (no-raw-sql)",
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
function collectInvariants(stack: StackDetectionResult): InvariantTemplate[] {
|
|
63
|
+
const invariants: InvariantTemplate[] = [...universalInvariants];
|
|
64
|
+
|
|
65
|
+
if (stack.framework?.name === "nextjs") {
|
|
66
|
+
invariants.push(...nextjsInvariants);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (stack.orm?.name === "prisma") {
|
|
70
|
+
invariants.push(...prismaInvariants);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return invariants;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function groupByCategory(invariants: InvariantTemplate[]): Map<string, InvariantTemplate[]> {
|
|
77
|
+
const grouped = new Map<string, InvariantTemplate[]>();
|
|
78
|
+
|
|
79
|
+
for (const inv of invariants) {
|
|
80
|
+
const existing = grouped.get(inv.category) ?? [];
|
|
81
|
+
existing.push(inv);
|
|
82
|
+
grouped.set(inv.category, existing);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return grouped;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatCategoryName(category: string): string {
|
|
89
|
+
return category.charAt(0).toUpperCase() + category.slice(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function generateInvariants(stack: StackDetectionResult): Promise<string> {
|
|
93
|
+
const invariants = collectInvariants(stack);
|
|
94
|
+
const grouped = groupByCategory(invariants);
|
|
95
|
+
|
|
96
|
+
const lines: string[] = [
|
|
97
|
+
"# Project Invariants",
|
|
98
|
+
"",
|
|
99
|
+
"This file defines the invariants that must hold for this project.",
|
|
100
|
+
"Each invariant is checked by `bantay check` on every PR.",
|
|
101
|
+
"",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
for (const [category, invs] of grouped) {
|
|
105
|
+
lines.push(`## ${formatCategoryName(category)}`);
|
|
106
|
+
lines.push("");
|
|
107
|
+
|
|
108
|
+
for (const inv of invs) {
|
|
109
|
+
lines.push(`- [${inv.id}] ${inv.category} | ${inv.statement}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.push("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function parseInvariants(markdown: string): Invariant[] {
|
|
119
|
+
const invariants: Invariant[] = [];
|
|
120
|
+
|
|
121
|
+
// Pattern 1: Old format - [INV-XXX] category | statement
|
|
122
|
+
const oldRegex = /^-\s*\[([A-Z]+-\d{3})\]\s*(\w+)\s*\|\s*(.+)$/gm;
|
|
123
|
+
|
|
124
|
+
// Pattern 2: Aide-generated format - [ ] **inv_id**: statement
|
|
125
|
+
// Category is determined by the preceding ## Header
|
|
126
|
+
const lines = markdown.split("\n");
|
|
127
|
+
let currentCategory = "uncategorized";
|
|
128
|
+
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
// Check for category header (## Category Name)
|
|
131
|
+
const headerMatch = line.match(/^##\s+(.+)$/);
|
|
132
|
+
if (headerMatch) {
|
|
133
|
+
// Convert "Auditability" to "auditability"
|
|
134
|
+
currentCategory = headerMatch[1].trim().toLowerCase().replace(/\s+/g, "_");
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for old format
|
|
139
|
+
const oldMatch = line.match(/^-\s*\[([A-Z]+-\d{3})\]\s*(\w+)\s*\|\s*(.+)$/);
|
|
140
|
+
if (oldMatch) {
|
|
141
|
+
invariants.push({
|
|
142
|
+
id: oldMatch[1],
|
|
143
|
+
category: oldMatch[2],
|
|
144
|
+
statement: oldMatch[3].trim(),
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check for aide-generated format: - [ ] **inv_id**: statement or - [x] **inv_id**: statement
|
|
150
|
+
const aideMatch = line.match(/^-\s*\[[x ]\]\s*\*\*([^*]+)\*\*:\s*(.+)$/);
|
|
151
|
+
if (aideMatch) {
|
|
152
|
+
invariants.push({
|
|
153
|
+
id: aideMatch[1],
|
|
154
|
+
category: currentCategory,
|
|
155
|
+
statement: aideMatch[2].trim(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return invariants;
|
|
161
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface PrerequisiteResult {
|
|
2
|
+
available: boolean;
|
|
3
|
+
version?: string;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function checkBunRuntime(): Promise<PrerequisiteResult> {
|
|
8
|
+
// Check if we're running in Bun by checking for Bun global
|
|
9
|
+
if (typeof Bun !== "undefined" && Bun.version) {
|
|
10
|
+
return {
|
|
11
|
+
available: true,
|
|
12
|
+
version: Bun.version,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
available: false,
|
|
18
|
+
error: "Bun runtime not found. Install from https://bun.sh",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function checkAllPrerequisites(): Promise<{
|
|
23
|
+
passed: boolean;
|
|
24
|
+
results: { name: string; result: PrerequisiteResult }[];
|
|
25
|
+
}> {
|
|
26
|
+
const bunResult = await checkBunRuntime();
|
|
27
|
+
|
|
28
|
+
const results = [{ name: "Bun Runtime", result: bunResult }];
|
|
29
|
+
|
|
30
|
+
const passed = results.every((r) => r.result.available);
|
|
31
|
+
|
|
32
|
+
return { passed, results };
|
|
33
|
+
}
|