@bantay/cli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/aide/discovery.ts +88 -0
- package/src/aide/index.ts +9 -0
- package/src/cli.ts +44 -2
- package/src/commands/aide.ts +432 -48
- package/src/commands/check.ts +9 -4
- package/src/commands/diff.ts +387 -0
- package/src/commands/init.ts +38 -1
- package/src/commands/status.ts +9 -7
- package/src/commands/tasks.ts +220 -0
- package/src/export/claude.ts +8 -5
- package/src/export/codex.ts +5 -3
- package/src/export/cursor.ts +5 -3
- package/src/export/invariants.ts +7 -5
- package/src/generators/claude-commands.ts +61 -0
- package/src/templates/commands/bantay-check.md +58 -0
- package/src/templates/commands/bantay-interview.md +155 -0
- package/src/templates/commands/bantay-orchestrate.md +164 -0
- package/src/templates/commands/bantay-status.md +39 -0
package/src/commands/aide.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
|
-
import { readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { readFile, writeFile, readdir } from "fs/promises";
|
|
3
|
+
import { basename } from "path";
|
|
3
4
|
import {
|
|
4
5
|
read,
|
|
5
6
|
write,
|
|
@@ -11,7 +12,37 @@ import {
|
|
|
11
12
|
} from "../aide";
|
|
12
13
|
import type { RelationshipType, Cardinality } from "../aide/types";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Entity type classification based on ID prefix
|
|
17
|
+
*/
|
|
18
|
+
const ENTITY_TYPE_PREFIXES: Record<string, string> = {
|
|
19
|
+
"cuj_": "cuj",
|
|
20
|
+
"sc_": "scenario",
|
|
21
|
+
"inv_": "invariant",
|
|
22
|
+
"con_": "constraint",
|
|
23
|
+
"found_": "foundation",
|
|
24
|
+
"wis_": "wisdom",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get entity type from ID prefix
|
|
29
|
+
*/
|
|
30
|
+
function getEntityType(id: string): string {
|
|
31
|
+
for (const [prefix, type] of Object.entries(ENTITY_TYPE_PREFIXES)) {
|
|
32
|
+
if (id.startsWith(prefix)) {
|
|
33
|
+
return type;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return "entity";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parsed lock file structure
|
|
41
|
+
*/
|
|
42
|
+
interface LockFile {
|
|
43
|
+
entities: Record<string, string>;
|
|
44
|
+
relationships: string[];
|
|
45
|
+
}
|
|
15
46
|
|
|
16
47
|
/**
|
|
17
48
|
* Parse command-line arguments for the aide commands
|
|
@@ -36,10 +67,161 @@ function parseOptions(args: string[]): { options: AideCommandOptions; rest: stri
|
|
|
36
67
|
}
|
|
37
68
|
|
|
38
69
|
/**
|
|
39
|
-
*
|
|
70
|
+
* Discover .aide files in the current directory
|
|
71
|
+
* Returns: { found: string[], error?: string }
|
|
72
|
+
*/
|
|
73
|
+
async function discoverAideFiles(cwd: string): Promise<{ found: string[]; error?: string }> {
|
|
74
|
+
try {
|
|
75
|
+
const files = await readdir(cwd);
|
|
76
|
+
const aideFiles = files.filter((f) => f.endsWith(".aide"));
|
|
77
|
+
return { found: aideFiles };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return { found: [], error: error instanceof Error ? error.message : String(error) };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the aide file path with auto-discovery
|
|
85
|
+
* - If --aide flag is provided, use that
|
|
86
|
+
* - Otherwise, glob for *.aide in cwd
|
|
87
|
+
* - If exactly one found, use it
|
|
88
|
+
* - If multiple found, error
|
|
89
|
+
* - If none found, error
|
|
90
|
+
*/
|
|
91
|
+
async function getAidePath(options: AideCommandOptions): Promise<string> {
|
|
92
|
+
const cwd = process.cwd();
|
|
93
|
+
|
|
94
|
+
// If explicit path provided, use it (resolve relative to cwd)
|
|
95
|
+
if (options.aidePath) {
|
|
96
|
+
if (options.aidePath.startsWith("/")) {
|
|
97
|
+
return options.aidePath;
|
|
98
|
+
}
|
|
99
|
+
return `${cwd}/${options.aidePath}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Auto-discover
|
|
103
|
+
const { found, error } = await discoverAideFiles(cwd);
|
|
104
|
+
|
|
105
|
+
if (error) {
|
|
106
|
+
console.error(`Error discovering aide files: ${error}`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (found.length === 0) {
|
|
111
|
+
console.error("No .aide file found. Run 'bantay aide init' to create one.");
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (found.length > 1) {
|
|
116
|
+
console.error("Multiple .aide files found. Specify one with --aide <path>");
|
|
117
|
+
console.error("Found:");
|
|
118
|
+
for (const f of found) {
|
|
119
|
+
console.error(` - ${f}`);
|
|
120
|
+
}
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return `${cwd}/${found[0]}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generate skeleton aide file content
|
|
40
129
|
*/
|
|
41
|
-
function
|
|
42
|
-
return
|
|
130
|
+
function generateAideSkeleton(projectName: string): string {
|
|
131
|
+
return `entities:
|
|
132
|
+
${projectName}:
|
|
133
|
+
display: page
|
|
134
|
+
props:
|
|
135
|
+
title: ${projectName}
|
|
136
|
+
description: Project description
|
|
137
|
+
version: "0.1"
|
|
138
|
+
cujs:
|
|
139
|
+
display: table
|
|
140
|
+
parent: ${projectName}
|
|
141
|
+
props:
|
|
142
|
+
title: Critical User Journeys
|
|
143
|
+
_pattern: roster
|
|
144
|
+
_group_by: area
|
|
145
|
+
_sort_by: tier
|
|
146
|
+
_sort_order: asc
|
|
147
|
+
invariants:
|
|
148
|
+
display: checklist
|
|
149
|
+
parent: ${projectName}
|
|
150
|
+
props:
|
|
151
|
+
title: Invariants
|
|
152
|
+
_pattern: roster
|
|
153
|
+
_group_by: category
|
|
154
|
+
constraints:
|
|
155
|
+
display: list
|
|
156
|
+
parent: ${projectName}
|
|
157
|
+
props:
|
|
158
|
+
title: Architectural Constraints
|
|
159
|
+
_pattern: flat_list
|
|
160
|
+
_group_by: domain
|
|
161
|
+
foundations:
|
|
162
|
+
display: list
|
|
163
|
+
parent: ${projectName}
|
|
164
|
+
props:
|
|
165
|
+
title: Design Foundations
|
|
166
|
+
_pattern: flat_list
|
|
167
|
+
wisdom:
|
|
168
|
+
display: list
|
|
169
|
+
parent: ${projectName}
|
|
170
|
+
props:
|
|
171
|
+
title: Project Wisdom
|
|
172
|
+
_pattern: flat_list
|
|
173
|
+
relationships: []
|
|
174
|
+
`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Handle bantay aide init
|
|
179
|
+
* Usage: bantay aide init [--name <name>]
|
|
180
|
+
* Creates a new .aide file with skeleton structure
|
|
181
|
+
*/
|
|
182
|
+
export async function handleAideInit(args: string[]): Promise<void> {
|
|
183
|
+
const cwd = process.cwd();
|
|
184
|
+
const dirName = basename(cwd);
|
|
185
|
+
|
|
186
|
+
// Parse --name option
|
|
187
|
+
let name: string | undefined;
|
|
188
|
+
for (let i = 0; i < args.length; i++) {
|
|
189
|
+
if (args[i] === "--name" || args[i] === "-n") {
|
|
190
|
+
name = args[++i];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Determine the aide filename
|
|
195
|
+
const aideName = name || dirName;
|
|
196
|
+
const aideFilename = `${aideName}.aide`;
|
|
197
|
+
const aidePath = `${cwd}/${aideFilename}`;
|
|
198
|
+
|
|
199
|
+
// Check if any .aide file already exists
|
|
200
|
+
const { found } = await discoverAideFiles(cwd);
|
|
201
|
+
|
|
202
|
+
if (found.length > 0) {
|
|
203
|
+
// Check if the specific file we want to create exists
|
|
204
|
+
if (found.includes(aideFilename)) {
|
|
205
|
+
console.error(`Error: ${aideFilename} already exists.`);
|
|
206
|
+
console.error("Use a different name with --name <name> or delete the existing file.");
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// If we're trying to create a new aide file but others exist, warn
|
|
211
|
+
console.error(`Error: .aide file already exists: ${found[0]}`);
|
|
212
|
+
console.error("Use --name <name> to create a differently named file, or use the existing one.");
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Generate and write the skeleton
|
|
217
|
+
const content = generateAideSkeleton(aideName);
|
|
218
|
+
await writeFile(aidePath, content, "utf-8");
|
|
219
|
+
|
|
220
|
+
console.log(`Created ${aideFilename}`);
|
|
221
|
+
console.log(`\nNext steps:`);
|
|
222
|
+
console.log(` 1. Edit ${aideFilename} to add your project's CUJs, invariants, and constraints`);
|
|
223
|
+
console.log(` 2. Run 'bantay aide validate' to check the file`);
|
|
224
|
+
console.log(` 3. Run 'bantay export all' to generate invariants.md and agent context files`);
|
|
43
225
|
}
|
|
44
226
|
|
|
45
227
|
/**
|
|
@@ -48,13 +230,7 @@ function getAidePath(options: AideCommandOptions): string {
|
|
|
48
230
|
*/
|
|
49
231
|
export async function handleAideAdd(args: string[]): Promise<void> {
|
|
50
232
|
const { options, rest } = parseOptions(args);
|
|
51
|
-
const aidePath = getAidePath(options);
|
|
52
|
-
|
|
53
|
-
if (!existsSync(aidePath)) {
|
|
54
|
-
console.error(`Error: Aide file not found: ${aidePath}`);
|
|
55
|
-
console.error("Run 'bantay init' to create a project first.");
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
233
|
+
const aidePath = await getAidePath(options);
|
|
58
234
|
|
|
59
235
|
// Parse add-specific options
|
|
60
236
|
let id: string | undefined;
|
|
@@ -101,12 +277,7 @@ export async function handleAideAdd(args: string[]): Promise<void> {
|
|
|
101
277
|
*/
|
|
102
278
|
export async function handleAideRemove(args: string[]): Promise<void> {
|
|
103
279
|
const { options, rest } = parseOptions(args);
|
|
104
|
-
const aidePath = getAidePath(options);
|
|
105
|
-
|
|
106
|
-
if (!existsSync(aidePath)) {
|
|
107
|
-
console.error(`Error: Aide file not found: ${aidePath}`);
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
280
|
+
const aidePath = await getAidePath(options);
|
|
110
281
|
|
|
111
282
|
const id = rest.find((arg) => !arg.startsWith("-"));
|
|
112
283
|
const force = rest.includes("--force") || rest.includes("-f");
|
|
@@ -135,12 +306,7 @@ export async function handleAideRemove(args: string[]): Promise<void> {
|
|
|
135
306
|
*/
|
|
136
307
|
export async function handleAideLink(args: string[]): Promise<void> {
|
|
137
308
|
const { options, rest } = parseOptions(args);
|
|
138
|
-
const aidePath = getAidePath(options);
|
|
139
|
-
|
|
140
|
-
if (!existsSync(aidePath)) {
|
|
141
|
-
console.error(`Error: Aide file not found: ${aidePath}`);
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
309
|
+
const aidePath = await getAidePath(options);
|
|
144
310
|
|
|
145
311
|
let from: string | undefined;
|
|
146
312
|
let to: string | undefined;
|
|
@@ -192,12 +358,7 @@ export async function handleAideLink(args: string[]): Promise<void> {
|
|
|
192
358
|
*/
|
|
193
359
|
export async function handleAideShow(args: string[]): Promise<void> {
|
|
194
360
|
const { options, rest } = parseOptions(args);
|
|
195
|
-
const aidePath = getAidePath(options);
|
|
196
|
-
|
|
197
|
-
if (!existsSync(aidePath)) {
|
|
198
|
-
console.error(`Error: Aide file not found: ${aidePath}`);
|
|
199
|
-
process.exit(1);
|
|
200
|
-
}
|
|
361
|
+
const aidePath = await getAidePath(options);
|
|
201
362
|
|
|
202
363
|
let entityId: string | undefined;
|
|
203
364
|
let format = "tree";
|
|
@@ -245,12 +406,7 @@ export async function handleAideShow(args: string[]): Promise<void> {
|
|
|
245
406
|
*/
|
|
246
407
|
export async function handleAideValidate(args: string[]): Promise<void> {
|
|
247
408
|
const { options } = parseOptions(args);
|
|
248
|
-
const aidePath = getAidePath(options);
|
|
249
|
-
|
|
250
|
-
if (!existsSync(aidePath)) {
|
|
251
|
-
console.error(`Error: Aide file not found: ${aidePath}`);
|
|
252
|
-
process.exit(1);
|
|
253
|
-
}
|
|
409
|
+
const aidePath = await getAidePath(options);
|
|
254
410
|
|
|
255
411
|
try {
|
|
256
412
|
const tree = await read(aidePath);
|
|
@@ -282,14 +438,9 @@ export async function handleAideValidate(args: string[]): Promise<void> {
|
|
|
282
438
|
*/
|
|
283
439
|
export async function handleAideLock(args: string[]): Promise<void> {
|
|
284
440
|
const { options } = parseOptions(args);
|
|
285
|
-
const aidePath = getAidePath(options);
|
|
441
|
+
const aidePath = await getAidePath(options);
|
|
286
442
|
const lockPath = `${aidePath}.lock`;
|
|
287
443
|
|
|
288
|
-
if (!existsSync(aidePath)) {
|
|
289
|
-
console.error(`Error: Aide file not found: ${aidePath}`);
|
|
290
|
-
process.exit(1);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
444
|
try {
|
|
294
445
|
const tree = await read(aidePath);
|
|
295
446
|
const errors = validate(tree);
|
|
@@ -321,12 +472,7 @@ export async function handleAideLock(args: string[]): Promise<void> {
|
|
|
321
472
|
*/
|
|
322
473
|
export async function handleAideUpdate(args: string[]): Promise<void> {
|
|
323
474
|
const { options, rest } = parseOptions(args);
|
|
324
|
-
const aidePath = getAidePath(options);
|
|
325
|
-
|
|
326
|
-
if (!existsSync(aidePath)) {
|
|
327
|
-
console.error(`Error: Aide file not found: ${aidePath}`);
|
|
328
|
-
process.exit(1);
|
|
329
|
-
}
|
|
475
|
+
const aidePath = await getAidePath(options);
|
|
330
476
|
|
|
331
477
|
let id: string | undefined;
|
|
332
478
|
const propsToSet: Record<string, unknown> = {};
|
|
@@ -512,6 +658,234 @@ function computeRelationshipHash(rel: { from: string; to: string; type: string;
|
|
|
512
658
|
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
513
659
|
}
|
|
514
660
|
|
|
661
|
+
/**
|
|
662
|
+
* Handle bantay aide diff
|
|
663
|
+
* Usage: bantay aide diff [--json]
|
|
664
|
+
* Compare bantay.aide against bantay.aide.lock and show changes
|
|
665
|
+
*/
|
|
666
|
+
export async function handleAideDiff(args: string[]): Promise<void> {
|
|
667
|
+
const { options } = parseOptions(args);
|
|
668
|
+
const aidePath = await getAidePath(options);
|
|
669
|
+
const lockPath = `${aidePath}.lock`;
|
|
670
|
+
const jsonOutput = args.includes("--json");
|
|
671
|
+
|
|
672
|
+
// Check that lock file exists
|
|
673
|
+
if (!existsSync(lockPath)) {
|
|
674
|
+
console.error(`Error: Lock file not found: ${lockPath}`);
|
|
675
|
+
console.error("Run 'bantay aide lock' to create a lock file first.");
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
// Read and parse current aide file
|
|
681
|
+
const tree = await read(aidePath);
|
|
682
|
+
|
|
683
|
+
// Read and parse lock file
|
|
684
|
+
const lockContent = await readFile(lockPath, "utf-8");
|
|
685
|
+
const lock = parseLockFile(lockContent);
|
|
686
|
+
|
|
687
|
+
// Compute current hashes
|
|
688
|
+
const currentHashes: Record<string, string> = {};
|
|
689
|
+
for (const [id, entity] of Object.entries(tree.entities)) {
|
|
690
|
+
currentHashes[id] = computeEntityHash(id, entity);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Compute current relationship keys
|
|
694
|
+
const currentRelationships = new Set<string>();
|
|
695
|
+
for (const rel of tree.relationships) {
|
|
696
|
+
currentRelationships.add(`${rel.from}:${rel.to}:${rel.type}`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Parse lock relationships into set
|
|
700
|
+
const lockRelationships = new Set<string>(lock.relationships);
|
|
701
|
+
|
|
702
|
+
// Find differences
|
|
703
|
+
const added: string[] = [];
|
|
704
|
+
const removed: string[] = [];
|
|
705
|
+
const modified: string[] = [];
|
|
706
|
+
|
|
707
|
+
// Find added and modified entities
|
|
708
|
+
for (const id of Object.keys(currentHashes)) {
|
|
709
|
+
if (!(id in lock.entities)) {
|
|
710
|
+
added.push(id);
|
|
711
|
+
} else if (lock.entities[id] !== currentHashes[id]) {
|
|
712
|
+
modified.push(id);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Find removed entities
|
|
717
|
+
for (const id of Object.keys(lock.entities)) {
|
|
718
|
+
if (!(id in currentHashes)) {
|
|
719
|
+
removed.push(id);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Find relationship changes
|
|
724
|
+
const addedRelationships: string[] = [];
|
|
725
|
+
const removedRelationships: string[] = [];
|
|
726
|
+
|
|
727
|
+
for (const rel of currentRelationships) {
|
|
728
|
+
if (!lockRelationships.has(rel)) {
|
|
729
|
+
addedRelationships.push(rel);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
for (const rel of lockRelationships) {
|
|
734
|
+
if (!currentRelationships.has(rel)) {
|
|
735
|
+
removedRelationships.push(rel);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Check if there are any changes
|
|
740
|
+
const hasChanges =
|
|
741
|
+
added.length > 0 ||
|
|
742
|
+
removed.length > 0 ||
|
|
743
|
+
modified.length > 0 ||
|
|
744
|
+
addedRelationships.length > 0 ||
|
|
745
|
+
removedRelationships.length > 0;
|
|
746
|
+
|
|
747
|
+
if (jsonOutput) {
|
|
748
|
+
// JSON output
|
|
749
|
+
const result = {
|
|
750
|
+
hasChanges,
|
|
751
|
+
added: added.map((id) => ({ id, type: getEntityType(id) })),
|
|
752
|
+
removed: removed.map((id) => ({ id, type: getEntityType(id) })),
|
|
753
|
+
modified: modified.map((id) => ({ id, type: getEntityType(id) })),
|
|
754
|
+
relationships: {
|
|
755
|
+
added: addedRelationships.map((r) => {
|
|
756
|
+
const [from, to, type] = r.split(":");
|
|
757
|
+
return { from, to, type };
|
|
758
|
+
}),
|
|
759
|
+
removed: removedRelationships.map((r) => {
|
|
760
|
+
const [from, to, type] = r.split(":");
|
|
761
|
+
return { from, to, type };
|
|
762
|
+
}),
|
|
763
|
+
},
|
|
764
|
+
summary: {
|
|
765
|
+
entitiesAdded: added.length,
|
|
766
|
+
entitiesRemoved: removed.length,
|
|
767
|
+
entitiesModified: modified.length,
|
|
768
|
+
relationshipsAdded: addedRelationships.length,
|
|
769
|
+
relationshipsRemoved: removedRelationships.length,
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
console.log(JSON.stringify(result, null, 2));
|
|
773
|
+
} else {
|
|
774
|
+
// Human-readable output
|
|
775
|
+
if (!hasChanges) {
|
|
776
|
+
console.log("No changes since last lock.");
|
|
777
|
+
process.exit(0);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
console.log("Changes since last lock:\n");
|
|
781
|
+
|
|
782
|
+
if (added.length > 0) {
|
|
783
|
+
console.log("ADDED");
|
|
784
|
+
for (const id of added.sort()) {
|
|
785
|
+
console.log(` + ${id} (${getEntityType(id)})`);
|
|
786
|
+
}
|
|
787
|
+
console.log("");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (modified.length > 0) {
|
|
791
|
+
console.log("MODIFIED");
|
|
792
|
+
for (const id of modified.sort()) {
|
|
793
|
+
console.log(` ~ ${id} (${getEntityType(id)})`);
|
|
794
|
+
}
|
|
795
|
+
console.log("");
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (removed.length > 0) {
|
|
799
|
+
console.log("REMOVED");
|
|
800
|
+
for (const id of removed.sort()) {
|
|
801
|
+
console.log(` - ${id} (${getEntityType(id)})`);
|
|
802
|
+
}
|
|
803
|
+
console.log("");
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (addedRelationships.length > 0 || removedRelationships.length > 0) {
|
|
807
|
+
console.log("RELATIONSHIPS");
|
|
808
|
+
for (const rel of addedRelationships) {
|
|
809
|
+
const [from, to, type] = rel.split(":");
|
|
810
|
+
console.log(` + ${from} --[${type}]--> ${to}`);
|
|
811
|
+
}
|
|
812
|
+
for (const rel of removedRelationships) {
|
|
813
|
+
const [from, to, type] = rel.split(":");
|
|
814
|
+
console.log(` - ${from} --[${type}]--> ${to}`);
|
|
815
|
+
}
|
|
816
|
+
console.log("");
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Summary
|
|
820
|
+
console.log("Summary:");
|
|
821
|
+
const parts: string[] = [];
|
|
822
|
+
if (added.length > 0) parts.push(`${added.length} added`);
|
|
823
|
+
if (modified.length > 0) parts.push(`${modified.length} modified`);
|
|
824
|
+
if (removed.length > 0) parts.push(`${removed.length} removed`);
|
|
825
|
+
if (addedRelationships.length > 0)
|
|
826
|
+
parts.push(`${addedRelationships.length} relationship(s) added`);
|
|
827
|
+
if (removedRelationships.length > 0)
|
|
828
|
+
parts.push(`${removedRelationships.length} relationship(s) removed`);
|
|
829
|
+
console.log(` ${parts.join(", ")}`);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Exit with code 1 if changes exist
|
|
833
|
+
process.exit(hasChanges ? 1 : 0);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
console.error(`Error comparing aide files: ${error instanceof Error ? error.message : error}`);
|
|
836
|
+
process.exit(1);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Parse lock file content into structured format
|
|
842
|
+
*/
|
|
843
|
+
function parseLockFile(content: string): LockFile {
|
|
844
|
+
const result: LockFile = {
|
|
845
|
+
entities: {},
|
|
846
|
+
relationships: [],
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
let section = "";
|
|
850
|
+
const lines = content.split("\n");
|
|
851
|
+
|
|
852
|
+
for (const line of lines) {
|
|
853
|
+
const trimmed = line.trim();
|
|
854
|
+
|
|
855
|
+
// Skip comments and empty lines
|
|
856
|
+
if (trimmed.startsWith("#") || trimmed === "") {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Section headers
|
|
861
|
+
if (trimmed === "entities:") {
|
|
862
|
+
section = "entities";
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (trimmed === "relationships:") {
|
|
866
|
+
section = "relationships";
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Parse content based on section
|
|
871
|
+
if (section === "entities") {
|
|
872
|
+
// Format: " entity_id: hash"
|
|
873
|
+
const match = trimmed.match(/^(\w+):\s*(\w+)$/);
|
|
874
|
+
if (match) {
|
|
875
|
+
result.entities[match[1]] = match[2];
|
|
876
|
+
}
|
|
877
|
+
} else if (section === "relationships") {
|
|
878
|
+
// Format: " - from:to:type: hash"
|
|
879
|
+
const match = trimmed.match(/^-\s*(\w+):(\w+):(\w+):\s*\w+$/);
|
|
880
|
+
if (match) {
|
|
881
|
+
result.relationships.push(`${match[1]}:${match[2]}:${match[3]}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return result;
|
|
887
|
+
}
|
|
888
|
+
|
|
515
889
|
/**
|
|
516
890
|
* Print help for aide subcommands
|
|
517
891
|
*/
|
|
@@ -522,6 +896,7 @@ bantay aide - Manage the aide entity tree
|
|
|
522
896
|
Usage: bantay aide <subcommand> [options]
|
|
523
897
|
|
|
524
898
|
Subcommands:
|
|
899
|
+
init Create a new .aide file with skeleton structure
|
|
525
900
|
add Add an entity to the tree
|
|
526
901
|
update Update an entity's properties
|
|
527
902
|
remove Remove an entity from the tree
|
|
@@ -529,6 +904,10 @@ Subcommands:
|
|
|
529
904
|
show Display the entity tree or a specific entity
|
|
530
905
|
validate Validate the aide file
|
|
531
906
|
lock Generate a lock file
|
|
907
|
+
diff Compare aide against lock file
|
|
908
|
+
|
|
909
|
+
Init Options:
|
|
910
|
+
--name, -n Name for the aide file (default: current directory name)
|
|
532
911
|
|
|
533
912
|
Add Options:
|
|
534
913
|
<id> Entity ID (optional, auto-generated if parent provided)
|
|
@@ -556,6 +935,9 @@ Show Options:
|
|
|
556
935
|
[id] Specific entity to show (optional)
|
|
557
936
|
--format, -f Output format: tree (default) or json
|
|
558
937
|
|
|
938
|
+
Diff Options:
|
|
939
|
+
--json Output as JSON
|
|
940
|
+
|
|
559
941
|
Global Options:
|
|
560
942
|
--aide, -a Path to aide file (default: bantay.aide)
|
|
561
943
|
|
|
@@ -567,5 +949,7 @@ Examples:
|
|
|
567
949
|
bantay aide show invariants
|
|
568
950
|
bantay aide validate
|
|
569
951
|
bantay aide lock
|
|
952
|
+
bantay aide diff
|
|
953
|
+
bantay aide diff --json
|
|
570
954
|
`);
|
|
571
955
|
}
|
package/src/commands/check.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { runChecker, hasChecker } from "../checkers/registry";
|
|
|
6
6
|
import { loadConfig } from "../config";
|
|
7
7
|
import { getGitDiff, shouldCheckInvariant } from "../diff";
|
|
8
8
|
import type { CheckResult, CheckerContext } from "../checkers/types";
|
|
9
|
-
import { read as readAide } from "../aide";
|
|
9
|
+
import { read as readAide, tryResolveAidePath } from "../aide";
|
|
10
10
|
|
|
11
11
|
export interface CheckOptions {
|
|
12
12
|
id?: string;
|
|
@@ -44,16 +44,21 @@ interface InvariantEnforcementInfo {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Load checker and test paths from
|
|
47
|
+
* Load checker and test paths from .aide file for each invariant
|
|
48
48
|
*/
|
|
49
49
|
async function getEnforcementInfo(
|
|
50
50
|
projectPath: string
|
|
51
51
|
): Promise<Map<string, InvariantEnforcementInfo>> {
|
|
52
52
|
const info = new Map<string, InvariantEnforcementInfo>();
|
|
53
|
-
|
|
53
|
+
|
|
54
|
+
// Try to discover the aide file
|
|
55
|
+
const resolved = await tryResolveAidePath(projectPath);
|
|
56
|
+
if (!resolved) {
|
|
57
|
+
return info; // No aide file found, return empty map
|
|
58
|
+
}
|
|
54
59
|
|
|
55
60
|
try {
|
|
56
|
-
const tree = await readAide(
|
|
61
|
+
const tree = await readAide(resolved.path);
|
|
57
62
|
for (const [id, entity] of Object.entries(tree.entities)) {
|
|
58
63
|
if (id.startsWith("inv_")) {
|
|
59
64
|
const enforcement: InvariantEnforcementInfo = {};
|