@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,80 @@
|
|
|
1
|
+
import { readFile, access } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { OrmDetection } from "./types";
|
|
4
|
+
|
|
5
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
6
|
+
try {
|
|
7
|
+
await access(path);
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function readPackageJson(projectPath: string): Promise<Record<string, unknown> | null> {
|
|
15
|
+
try {
|
|
16
|
+
const content = await readFile(join(projectPath, "package.json"), "utf-8");
|
|
17
|
+
return JSON.parse(content);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function detect(projectPath: string): Promise<OrmDetection | null> {
|
|
24
|
+
const pkg = await readPackageJson(projectPath);
|
|
25
|
+
|
|
26
|
+
let version: string | undefined;
|
|
27
|
+
let confidence: "high" | "medium" | "low" = "low";
|
|
28
|
+
let detected = false;
|
|
29
|
+
|
|
30
|
+
// Check package.json for prisma dependencies
|
|
31
|
+
if (pkg) {
|
|
32
|
+
const deps = (pkg.dependencies ?? {}) as Record<string, string>;
|
|
33
|
+
const devDeps = (pkg.devDependencies ?? {}) as Record<string, string>;
|
|
34
|
+
|
|
35
|
+
if (deps["@prisma/client"]) {
|
|
36
|
+
version = deps["@prisma/client"];
|
|
37
|
+
confidence = "high";
|
|
38
|
+
detected = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (devDeps.prisma) {
|
|
42
|
+
version = version ?? devDeps.prisma;
|
|
43
|
+
confidence = "high";
|
|
44
|
+
detected = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!detected) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Find schema path
|
|
53
|
+
let schemaPath: string | undefined;
|
|
54
|
+
|
|
55
|
+
// Check for custom path in package.json
|
|
56
|
+
if (pkg?.prisma && typeof pkg.prisma === "object") {
|
|
57
|
+
const prismaConfig = pkg.prisma as Record<string, unknown>;
|
|
58
|
+
if (typeof prismaConfig.schema === "string") {
|
|
59
|
+
const customPath = prismaConfig.schema;
|
|
60
|
+
if (await fileExists(join(projectPath, customPath))) {
|
|
61
|
+
schemaPath = customPath;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check default location if no custom path
|
|
67
|
+
if (!schemaPath) {
|
|
68
|
+
const defaultPath = "prisma/schema.prisma";
|
|
69
|
+
if (await fileExists(join(projectPath, defaultPath))) {
|
|
70
|
+
schemaPath = defaultPath;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
name: "prisma",
|
|
76
|
+
version,
|
|
77
|
+
confidence,
|
|
78
|
+
schemaPath,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface FrameworkDetection {
|
|
2
|
+
name: string;
|
|
3
|
+
version?: string;
|
|
4
|
+
confidence: "high" | "medium" | "low";
|
|
5
|
+
router?: "app" | "pages";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface OrmDetection {
|
|
9
|
+
name: string;
|
|
10
|
+
version?: string;
|
|
11
|
+
confidence: "high" | "medium" | "low";
|
|
12
|
+
schemaPath?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AuthDetection {
|
|
16
|
+
name: string;
|
|
17
|
+
version?: string;
|
|
18
|
+
confidence: "high" | "medium" | "low";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StackDetectionResult {
|
|
22
|
+
framework: FrameworkDetection | null;
|
|
23
|
+
orm: OrmDetection | null;
|
|
24
|
+
auth: AuthDetection | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Detector<T> {
|
|
28
|
+
detect(projectPath: string): Promise<T | null>;
|
|
29
|
+
}
|
package/src/diff.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
|
|
3
|
+
export interface DiffResult {
|
|
4
|
+
changedFiles: string[];
|
|
5
|
+
categories: Set<string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Maps file patterns to invariant categories they affect
|
|
9
|
+
const FILE_CATEGORY_MAP: Array<{ pattern: RegExp; category: string }> = [
|
|
10
|
+
// Route files affect auth invariants
|
|
11
|
+
{ pattern: /\/(api|routes)\/.*\.(ts|tsx|js|jsx)$/, category: "auth" },
|
|
12
|
+
{ pattern: /route\.(ts|tsx|js|jsx)$/, category: "auth" },
|
|
13
|
+
{ pattern: /page\.(ts|tsx|js|jsx)$/, category: "auth" },
|
|
14
|
+
|
|
15
|
+
// Schema/migration files affect schema invariants
|
|
16
|
+
{ pattern: /schema\.prisma$/, category: "schema" },
|
|
17
|
+
{ pattern: /migrations?\//, category: "schema" },
|
|
18
|
+
{ pattern: /\.sql$/, category: "schema" },
|
|
19
|
+
|
|
20
|
+
// Log-related files affect logging invariants
|
|
21
|
+
{ pattern: /log(ger)?\./, category: "logging" },
|
|
22
|
+
{ pattern: /console\./, category: "logging" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export async function getGitDiff(
|
|
26
|
+
projectPath: string,
|
|
27
|
+
ref: string = "HEAD"
|
|
28
|
+
): Promise<DiffResult> {
|
|
29
|
+
const changedFiles: string[] = [];
|
|
30
|
+
const categories = new Set<string>();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Get list of changed files
|
|
34
|
+
const proc = Bun.spawn(["git", "diff", "--name-only", ref], {
|
|
35
|
+
cwd: projectPath,
|
|
36
|
+
stdout: "pipe",
|
|
37
|
+
stderr: "pipe",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const output = await new Response(proc.stdout).text();
|
|
41
|
+
const exitCode = await proc.exited;
|
|
42
|
+
|
|
43
|
+
if (exitCode !== 0) {
|
|
44
|
+
// Also try with staged files for uncommitted changes
|
|
45
|
+
const stagedProc = Bun.spawn(["git", "diff", "--name-only", "--cached"], {
|
|
46
|
+
cwd: projectPath,
|
|
47
|
+
stdout: "pipe",
|
|
48
|
+
stderr: "pipe",
|
|
49
|
+
});
|
|
50
|
+
const stagedOutput = await new Response(stagedProc.stdout).text();
|
|
51
|
+
await stagedProc.exited;
|
|
52
|
+
|
|
53
|
+
const unstagedProc = Bun.spawn(["git", "diff", "--name-only"], {
|
|
54
|
+
cwd: projectPath,
|
|
55
|
+
stdout: "pipe",
|
|
56
|
+
stderr: "pipe",
|
|
57
|
+
});
|
|
58
|
+
const unstagedOutput = await new Response(unstagedProc.stdout).text();
|
|
59
|
+
await unstagedProc.exited;
|
|
60
|
+
|
|
61
|
+
const files = new Set([
|
|
62
|
+
...stagedOutput.trim().split("\n").filter(Boolean),
|
|
63
|
+
...unstagedOutput.trim().split("\n").filter(Boolean),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
changedFiles.push(...files);
|
|
67
|
+
} else {
|
|
68
|
+
changedFiles.push(...output.trim().split("\n").filter(Boolean));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Also include untracked files that are new
|
|
72
|
+
const untrackedProc = Bun.spawn(
|
|
73
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
74
|
+
{
|
|
75
|
+
cwd: projectPath,
|
|
76
|
+
stdout: "pipe",
|
|
77
|
+
stderr: "pipe",
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
const untrackedOutput = await new Response(untrackedProc.stdout).text();
|
|
81
|
+
await untrackedProc.exited;
|
|
82
|
+
|
|
83
|
+
const untrackedFiles = untrackedOutput.trim().split("\n").filter(Boolean);
|
|
84
|
+
for (const file of untrackedFiles) {
|
|
85
|
+
if (!changedFiles.includes(file)) {
|
|
86
|
+
changedFiles.push(file);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Determine which categories are affected
|
|
91
|
+
for (const file of changedFiles) {
|
|
92
|
+
for (const { pattern, category } of FILE_CATEGORY_MAP) {
|
|
93
|
+
if (pattern.test(file)) {
|
|
94
|
+
categories.add(category);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If no specific categories matched but there are changes, include all
|
|
100
|
+
// This ensures we don't miss invariants for file types we don't recognize
|
|
101
|
+
if (changedFiles.length > 0 && categories.size === 0) {
|
|
102
|
+
// Default: check all categories when we can't determine specifics
|
|
103
|
+
categories.add("*");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { changedFiles, categories };
|
|
107
|
+
} catch {
|
|
108
|
+
// Not a git repo or git not available - check everything
|
|
109
|
+
return { changedFiles: [], categories: new Set(["*"]) };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function shouldCheckInvariant(
|
|
114
|
+
invariantCategory: string,
|
|
115
|
+
affectedCategories: Set<string>
|
|
116
|
+
): boolean {
|
|
117
|
+
// If "*" is in affected categories, check everything
|
|
118
|
+
if (affectedCategories.has("*")) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if this invariant's category is affected
|
|
123
|
+
return affectedCategories.has(invariantCategory);
|
|
124
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers to extract entities from the aide tree for export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AideTree, Entity } from "../aide";
|
|
6
|
+
import type {
|
|
7
|
+
ExtractedInvariant,
|
|
8
|
+
ExtractedConstraint,
|
|
9
|
+
ExtractedFoundation,
|
|
10
|
+
ExtractedWisdom,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract all invariants from the aide tree
|
|
15
|
+
* Invariants are entities with parent "invariants"
|
|
16
|
+
*/
|
|
17
|
+
export function extractInvariants(tree: AideTree): ExtractedInvariant[] {
|
|
18
|
+
const invariants: ExtractedInvariant[] = [];
|
|
19
|
+
|
|
20
|
+
for (const [id, entity] of Object.entries(tree.entities)) {
|
|
21
|
+
if (entity.parent === "invariants" && entity.props) {
|
|
22
|
+
const props = entity.props;
|
|
23
|
+
invariants.push({
|
|
24
|
+
id,
|
|
25
|
+
statement: String(props.statement || ""),
|
|
26
|
+
category: String(props.category || "uncategorized"),
|
|
27
|
+
threatSignal: props.threat_signal ? String(props.threat_signal) : undefined,
|
|
28
|
+
done: props.done === true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return invariants;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract all constraints from the aide tree
|
|
38
|
+
* Constraints are entities with parent "constraints"
|
|
39
|
+
*/
|
|
40
|
+
export function extractConstraints(tree: AideTree): ExtractedConstraint[] {
|
|
41
|
+
const constraints: ExtractedConstraint[] = [];
|
|
42
|
+
|
|
43
|
+
for (const [id, entity] of Object.entries(tree.entities)) {
|
|
44
|
+
if (entity.parent === "constraints" && entity.props) {
|
|
45
|
+
const props = entity.props;
|
|
46
|
+
constraints.push({
|
|
47
|
+
id,
|
|
48
|
+
text: String(props.text || ""),
|
|
49
|
+
domain: String(props.domain || "general"),
|
|
50
|
+
rationale: props.rationale ? String(props.rationale) : undefined,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return constraints;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract all foundations from the aide tree
|
|
60
|
+
* Foundations are entities with parent "foundations"
|
|
61
|
+
*/
|
|
62
|
+
export function extractFoundations(tree: AideTree): ExtractedFoundation[] {
|
|
63
|
+
const foundations: ExtractedFoundation[] = [];
|
|
64
|
+
|
|
65
|
+
for (const [id, entity] of Object.entries(tree.entities)) {
|
|
66
|
+
if (entity.parent === "foundations" && entity.props) {
|
|
67
|
+
const props = entity.props;
|
|
68
|
+
foundations.push({
|
|
69
|
+
id,
|
|
70
|
+
text: String(props.text || ""),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return foundations;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Extract all wisdom entries from the aide tree
|
|
80
|
+
* Wisdom entries are entities with parent "wisdom"
|
|
81
|
+
*/
|
|
82
|
+
export function extractWisdom(tree: AideTree): ExtractedWisdom[] {
|
|
83
|
+
const wisdom: ExtractedWisdom[] = [];
|
|
84
|
+
|
|
85
|
+
for (const [id, entity] of Object.entries(tree.entities)) {
|
|
86
|
+
if (entity.parent === "wisdom" && entity.props) {
|
|
87
|
+
const props = entity.props;
|
|
88
|
+
wisdom.push({
|
|
89
|
+
id,
|
|
90
|
+
text: String(props.text || ""),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return wisdom;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Group items by a key
|
|
100
|
+
*/
|
|
101
|
+
export function groupBy<T>(items: T[], keyFn: (item: T) => string): Map<string, T[]> {
|
|
102
|
+
const groups = new Map<string, T[]>();
|
|
103
|
+
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
const key = keyFn(item);
|
|
106
|
+
const existing = groups.get(key) || [];
|
|
107
|
+
existing.push(item);
|
|
108
|
+
groups.set(key, existing);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return groups;
|
|
112
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export all targets at once
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { exportInvariants } from "./invariants";
|
|
6
|
+
import { exportClaude } from "./claude";
|
|
7
|
+
import { exportCursor } from "./cursor";
|
|
8
|
+
import type { ExportOptions, ExportResult } from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Export all targets: invariants.md, CLAUDE.md, .cursorrules
|
|
12
|
+
*/
|
|
13
|
+
export async function exportAll(
|
|
14
|
+
projectPath: string,
|
|
15
|
+
options: ExportOptions = {}
|
|
16
|
+
): Promise<ExportResult[]> {
|
|
17
|
+
const results: ExportResult[] = [];
|
|
18
|
+
|
|
19
|
+
// Export invariants.md
|
|
20
|
+
results.push(await exportInvariants(projectPath, options));
|
|
21
|
+
|
|
22
|
+
// Export CLAUDE.md
|
|
23
|
+
results.push(await exportClaude(projectPath, options));
|
|
24
|
+
|
|
25
|
+
// Export .cursorrules
|
|
26
|
+
results.push(await exportCursor(projectPath, options));
|
|
27
|
+
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export to CLAUDE.md with section markers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile, writeFile, access } from "fs/promises";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { read as readAide } from "../aide";
|
|
8
|
+
import {
|
|
9
|
+
extractConstraints,
|
|
10
|
+
extractFoundations,
|
|
11
|
+
extractInvariants,
|
|
12
|
+
groupBy,
|
|
13
|
+
} from "./aide-reader";
|
|
14
|
+
import {
|
|
15
|
+
type ExportOptions,
|
|
16
|
+
type ExportResult,
|
|
17
|
+
type ExtractedConstraint,
|
|
18
|
+
type ExtractedFoundation,
|
|
19
|
+
type ExtractedInvariant,
|
|
20
|
+
SECTION_START,
|
|
21
|
+
SECTION_END,
|
|
22
|
+
} from "./types";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate the Bantay section content for CLAUDE.md
|
|
26
|
+
*/
|
|
27
|
+
export function generateClaudeSection(
|
|
28
|
+
constraints: ExtractedConstraint[],
|
|
29
|
+
foundations: ExtractedFoundation[],
|
|
30
|
+
invariants: ExtractedInvariant[]
|
|
31
|
+
): string {
|
|
32
|
+
const lines: string[] = [];
|
|
33
|
+
|
|
34
|
+
lines.push(SECTION_START);
|
|
35
|
+
lines.push("");
|
|
36
|
+
lines.push("## Bantay Project Rules");
|
|
37
|
+
lines.push("");
|
|
38
|
+
lines.push("*Auto-generated from bantay.aide. Do not edit manually.*");
|
|
39
|
+
lines.push("");
|
|
40
|
+
|
|
41
|
+
// Foundations as principles
|
|
42
|
+
if (foundations.length > 0) {
|
|
43
|
+
lines.push("### Design Principles");
|
|
44
|
+
lines.push("");
|
|
45
|
+
for (const f of foundations) {
|
|
46
|
+
lines.push(`- ${f.text}`);
|
|
47
|
+
}
|
|
48
|
+
lines.push("");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Constraints grouped by domain
|
|
52
|
+
if (constraints.length > 0) {
|
|
53
|
+
lines.push("### Architectural Constraints");
|
|
54
|
+
lines.push("");
|
|
55
|
+
|
|
56
|
+
const byDomain = groupBy(constraints, (c) => c.domain);
|
|
57
|
+
const sortedDomains = Array.from(byDomain.keys()).sort();
|
|
58
|
+
|
|
59
|
+
for (const domain of sortedDomains) {
|
|
60
|
+
const domainConstraints = byDomain.get(domain) || [];
|
|
61
|
+
|
|
62
|
+
lines.push(`#### ${formatDomainTitle(domain)}`);
|
|
63
|
+
lines.push("");
|
|
64
|
+
|
|
65
|
+
for (const c of domainConstraints) {
|
|
66
|
+
lines.push(`- **${c.id}**: ${c.text}`);
|
|
67
|
+
if (c.rationale) {
|
|
68
|
+
lines.push(` - *Rationale*: ${c.rationale}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
lines.push("");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Invariants as rules
|
|
76
|
+
if (invariants.length > 0) {
|
|
77
|
+
lines.push("### Invariants (Rules You Must Follow)");
|
|
78
|
+
lines.push("");
|
|
79
|
+
|
|
80
|
+
const byCategory = groupBy(invariants, (inv) => inv.category);
|
|
81
|
+
const sortedCategories = Array.from(byCategory.keys()).sort();
|
|
82
|
+
|
|
83
|
+
for (const category of sortedCategories) {
|
|
84
|
+
const categoryInvariants = byCategory.get(category) || [];
|
|
85
|
+
|
|
86
|
+
lines.push(`#### ${formatCategoryTitle(category)}`);
|
|
87
|
+
lines.push("");
|
|
88
|
+
|
|
89
|
+
for (const inv of categoryInvariants) {
|
|
90
|
+
lines.push(`- **${inv.id}**: ${inv.statement}`);
|
|
91
|
+
}
|
|
92
|
+
lines.push("");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
lines.push(SECTION_END);
|
|
97
|
+
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Format a domain ID into a readable title
|
|
103
|
+
*/
|
|
104
|
+
function formatDomainTitle(domain: string): string {
|
|
105
|
+
return domain
|
|
106
|
+
.split("_")
|
|
107
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
108
|
+
.join(" ");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Format a category ID into a readable title
|
|
113
|
+
*/
|
|
114
|
+
function formatCategoryTitle(category: string): string {
|
|
115
|
+
return category
|
|
116
|
+
.split("_")
|
|
117
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
118
|
+
.join(" ");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Find the position of a marker at the start of a line (not inline)
|
|
123
|
+
* Returns -1 if not found
|
|
124
|
+
*/
|
|
125
|
+
function findMarkerPosition(content: string, marker: string): number {
|
|
126
|
+
// Match marker at start of line (with optional leading whitespace)
|
|
127
|
+
const regex = new RegExp(`^[ \\t]*${escapeRegex(marker)}`, "m");
|
|
128
|
+
const match = content.match(regex);
|
|
129
|
+
if (match && match.index !== undefined) {
|
|
130
|
+
// Return the position of the marker itself, not the leading whitespace
|
|
131
|
+
return content.indexOf(marker, match.index);
|
|
132
|
+
}
|
|
133
|
+
return -1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Escape special regex characters
|
|
138
|
+
*/
|
|
139
|
+
function escapeRegex(str: string): string {
|
|
140
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Insert or replace the Bantay section in a file
|
|
145
|
+
* Only matches markers at the start of a line, not inline markers
|
|
146
|
+
*/
|
|
147
|
+
export function insertSection(existingContent: string, newSection: string): string {
|
|
148
|
+
const startIdx = findMarkerPosition(existingContent, SECTION_START);
|
|
149
|
+
const endIdx = findMarkerPosition(existingContent, SECTION_END);
|
|
150
|
+
|
|
151
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
152
|
+
// Replace existing section
|
|
153
|
+
const before = existingContent.slice(0, startIdx);
|
|
154
|
+
const after = existingContent.slice(endIdx + SECTION_END.length);
|
|
155
|
+
return before + newSection + after;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Append section at end
|
|
159
|
+
if (existingContent.length > 0 && !existingContent.endsWith("\n")) {
|
|
160
|
+
return existingContent + "\n\n" + newSection + "\n";
|
|
161
|
+
}
|
|
162
|
+
if (existingContent.length > 0) {
|
|
163
|
+
return existingContent + "\n" + newSection + "\n";
|
|
164
|
+
}
|
|
165
|
+
return newSection + "\n";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if a file exists
|
|
170
|
+
*/
|
|
171
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
172
|
+
try {
|
|
173
|
+
await access(path);
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Export to CLAUDE.md
|
|
182
|
+
*/
|
|
183
|
+
export async function exportClaude(
|
|
184
|
+
projectPath: string,
|
|
185
|
+
options: ExportOptions = {}
|
|
186
|
+
): Promise<ExportResult> {
|
|
187
|
+
const aidePath = options.aidePath || join(projectPath, "bantay.aide");
|
|
188
|
+
const outputPath = options.outputPath || join(projectPath, "CLAUDE.md");
|
|
189
|
+
|
|
190
|
+
// Read the aide tree
|
|
191
|
+
const tree = await readAide(aidePath);
|
|
192
|
+
|
|
193
|
+
// Extract entities
|
|
194
|
+
const constraints = extractConstraints(tree);
|
|
195
|
+
const foundations = extractFoundations(tree);
|
|
196
|
+
const invariants = extractInvariants(tree);
|
|
197
|
+
|
|
198
|
+
// Generate section content
|
|
199
|
+
const section = generateClaudeSection(constraints, foundations, invariants);
|
|
200
|
+
|
|
201
|
+
// Read existing file if it exists
|
|
202
|
+
let existingContent = "";
|
|
203
|
+
if (await fileExists(outputPath)) {
|
|
204
|
+
existingContent = await readFile(outputPath, "utf-8");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Insert or replace section
|
|
208
|
+
const content = insertSection(existingContent, section);
|
|
209
|
+
|
|
210
|
+
// Write unless dry run
|
|
211
|
+
if (!options.dryRun) {
|
|
212
|
+
await writeFile(outputPath, content, "utf-8");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
target: "claude",
|
|
217
|
+
outputPath,
|
|
218
|
+
content,
|
|
219
|
+
bytesWritten: Buffer.byteLength(content, "utf-8"),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export to .cursorrules with section markers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile, writeFile, access } from "fs/promises";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { read as readAide } from "../aide";
|
|
8
|
+
import {
|
|
9
|
+
extractConstraints,
|
|
10
|
+
extractFoundations,
|
|
11
|
+
extractInvariants,
|
|
12
|
+
} from "./aide-reader";
|
|
13
|
+
import { generateClaudeSection, insertSection } from "./claude";
|
|
14
|
+
import type { ExportOptions, ExportResult } from "./types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if a file exists
|
|
18
|
+
*/
|
|
19
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
20
|
+
try {
|
|
21
|
+
await access(path);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Export to .cursorrules
|
|
30
|
+
*/
|
|
31
|
+
export async function exportCursor(
|
|
32
|
+
projectPath: string,
|
|
33
|
+
options: ExportOptions = {}
|
|
34
|
+
): Promise<ExportResult> {
|
|
35
|
+
const aidePath = options.aidePath || join(projectPath, "bantay.aide");
|
|
36
|
+
const outputPath = options.outputPath || join(projectPath, ".cursorrules");
|
|
37
|
+
|
|
38
|
+
// Read the aide tree
|
|
39
|
+
const tree = await readAide(aidePath);
|
|
40
|
+
|
|
41
|
+
// Extract entities
|
|
42
|
+
const constraints = extractConstraints(tree);
|
|
43
|
+
const foundations = extractFoundations(tree);
|
|
44
|
+
const invariants = extractInvariants(tree);
|
|
45
|
+
|
|
46
|
+
// Generate section content (same format as claude)
|
|
47
|
+
const section = generateClaudeSection(constraints, foundations, invariants);
|
|
48
|
+
|
|
49
|
+
// Read existing file if it exists
|
|
50
|
+
let existingContent = "";
|
|
51
|
+
if (await fileExists(outputPath)) {
|
|
52
|
+
existingContent = await readFile(outputPath, "utf-8");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Insert or replace section
|
|
56
|
+
const content = insertSection(existingContent, section);
|
|
57
|
+
|
|
58
|
+
// Write unless dry run
|
|
59
|
+
if (!options.dryRun) {
|
|
60
|
+
await writeFile(outputPath, content, "utf-8");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
target: "cursor",
|
|
65
|
+
outputPath,
|
|
66
|
+
content,
|
|
67
|
+
bytesWritten: Buffer.byteLength(content, "utf-8"),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export module - generates output files from bantay.aide
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { exportInvariants, generateInvariantsMd } from "./invariants";
|
|
6
|
+
export { exportClaude, generateClaudeSection, insertSection } from "./claude";
|
|
7
|
+
export { exportCursor } from "./cursor";
|
|
8
|
+
export { exportAll } from "./all";
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
ExportOptions,
|
|
12
|
+
ExportResult,
|
|
13
|
+
ExportTarget,
|
|
14
|
+
ExtractedInvariant,
|
|
15
|
+
ExtractedConstraint,
|
|
16
|
+
ExtractedFoundation,
|
|
17
|
+
ExtractedWisdom,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
export { SECTION_START, SECTION_END } from "./types";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
extractInvariants,
|
|
24
|
+
extractConstraints,
|
|
25
|
+
extractFoundations,
|
|
26
|
+
extractWisdom,
|
|
27
|
+
groupBy,
|
|
28
|
+
} from "./aide-reader";
|