@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,571 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { readFile, writeFile } from "fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
read,
|
|
5
|
+
write,
|
|
6
|
+
addEntity,
|
|
7
|
+
removeEntity,
|
|
8
|
+
addRelationship,
|
|
9
|
+
validate,
|
|
10
|
+
type AideTree,
|
|
11
|
+
} from "../aide";
|
|
12
|
+
import type { RelationshipType, Cardinality } from "../aide/types";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_AIDE_PATH = "bantay.aide";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse command-line arguments for the aide commands
|
|
18
|
+
*/
|
|
19
|
+
interface AideCommandOptions {
|
|
20
|
+
aidePath?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseOptions(args: string[]): { options: AideCommandOptions; rest: string[] } {
|
|
24
|
+
const options: AideCommandOptions = {};
|
|
25
|
+
const rest: string[] = [];
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
if (args[i] === "--aide" || args[i] === "-a") {
|
|
29
|
+
options.aidePath = args[++i];
|
|
30
|
+
} else {
|
|
31
|
+
rest.push(args[i]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { options, rest };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the aide file path, defaulting to bantay.aide in cwd
|
|
40
|
+
*/
|
|
41
|
+
function getAidePath(options: AideCommandOptions): string {
|
|
42
|
+
return options.aidePath || `${process.cwd()}/${DEFAULT_AIDE_PATH}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handle bantay aide add
|
|
47
|
+
* Usage: bantay aide add <id> [--parent <parent>] [--display <display>] [--prop key=value...]
|
|
48
|
+
*/
|
|
49
|
+
export async function handleAideAdd(args: string[]): Promise<void> {
|
|
50
|
+
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
|
+
}
|
|
58
|
+
|
|
59
|
+
// Parse add-specific options
|
|
60
|
+
let id: string | undefined;
|
|
61
|
+
let parent: string | undefined;
|
|
62
|
+
let display: string | undefined;
|
|
63
|
+
const props: Record<string, unknown> = {};
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < rest.length; i++) {
|
|
66
|
+
const arg = rest[i];
|
|
67
|
+
if (arg === "--parent" || arg === "-p") {
|
|
68
|
+
parent = rest[++i];
|
|
69
|
+
} else if (arg === "--display" || arg === "-d") {
|
|
70
|
+
display = rest[++i];
|
|
71
|
+
} else if (arg === "--prop") {
|
|
72
|
+
const propStr = rest[++i];
|
|
73
|
+
const eqIdx = propStr.indexOf("=");
|
|
74
|
+
if (eqIdx > 0) {
|
|
75
|
+
const key = propStr.slice(0, eqIdx);
|
|
76
|
+
const value = propStr.slice(eqIdx + 1);
|
|
77
|
+
props[key] = parsePropertyValue(value);
|
|
78
|
+
}
|
|
79
|
+
} else if (!arg.startsWith("-") && !id) {
|
|
80
|
+
id = arg;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const tree = await read(aidePath);
|
|
86
|
+
const newTree = addEntity(tree, { id, parent, display, props });
|
|
87
|
+
await write(aidePath, newTree);
|
|
88
|
+
|
|
89
|
+
// Find the new ID (might be auto-generated)
|
|
90
|
+
const newId = id || findNewId(tree, newTree);
|
|
91
|
+
console.log(`Added entity: ${newId}`);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(`Error adding entity: ${error instanceof Error ? error.message : error}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handle bantay aide remove
|
|
100
|
+
* Usage: bantay aide remove <id> [--force]
|
|
101
|
+
*/
|
|
102
|
+
export async function handleAideRemove(args: string[]): Promise<void> {
|
|
103
|
+
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
|
+
}
|
|
110
|
+
|
|
111
|
+
const id = rest.find((arg) => !arg.startsWith("-"));
|
|
112
|
+
const force = rest.includes("--force") || rest.includes("-f");
|
|
113
|
+
|
|
114
|
+
if (!id) {
|
|
115
|
+
console.error("Error: Entity ID required");
|
|
116
|
+
console.error("Usage: bantay aide remove <id> [--force]");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const tree = await read(aidePath);
|
|
122
|
+
const newTree = removeEntity(tree, id, { force });
|
|
123
|
+
await write(aidePath, newTree);
|
|
124
|
+
|
|
125
|
+
console.log(`Removed entity: ${id}`);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`Error removing entity: ${error instanceof Error ? error.message : error}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Handle bantay aide link
|
|
134
|
+
* Usage: bantay aide link <from> <to> --type <type> [--cardinality <cardinality>]
|
|
135
|
+
*/
|
|
136
|
+
export async function handleAideLink(args: string[]): Promise<void> {
|
|
137
|
+
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
|
+
}
|
|
144
|
+
|
|
145
|
+
let from: string | undefined;
|
|
146
|
+
let to: string | undefined;
|
|
147
|
+
let type: RelationshipType | undefined;
|
|
148
|
+
let cardinality: Cardinality = "many_to_many";
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < rest.length; i++) {
|
|
151
|
+
const arg = rest[i];
|
|
152
|
+
if (arg === "--type" || arg === "-t") {
|
|
153
|
+
type = rest[++i] as RelationshipType;
|
|
154
|
+
} else if (arg === "--cardinality" || arg === "-c") {
|
|
155
|
+
cardinality = rest[++i] as Cardinality;
|
|
156
|
+
} else if (!arg.startsWith("-")) {
|
|
157
|
+
if (!from) {
|
|
158
|
+
from = arg;
|
|
159
|
+
} else if (!to) {
|
|
160
|
+
to = arg;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!from || !to) {
|
|
166
|
+
console.error("Error: Both 'from' and 'to' entity IDs required");
|
|
167
|
+
console.error("Usage: bantay aide link <from> <to> --type <type>");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!type) {
|
|
172
|
+
console.error("Error: Relationship type required (--type)");
|
|
173
|
+
console.error("Valid types: protected_by, depends_on, implements, delegates_to, weakens");
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const tree = await read(aidePath);
|
|
179
|
+
const newTree = addRelationship(tree, { from, to, type, cardinality });
|
|
180
|
+
await write(aidePath, newTree);
|
|
181
|
+
|
|
182
|
+
console.log(`Added relationship: ${from} --[${type}]--> ${to}`);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error(`Error adding relationship: ${error instanceof Error ? error.message : error}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Handle bantay aide show
|
|
191
|
+
* Usage: bantay aide show [id] [--format json|tree]
|
|
192
|
+
*/
|
|
193
|
+
export async function handleAideShow(args: string[]): Promise<void> {
|
|
194
|
+
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
|
+
}
|
|
201
|
+
|
|
202
|
+
let entityId: string | undefined;
|
|
203
|
+
let format = "tree";
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i < rest.length; i++) {
|
|
206
|
+
const arg = rest[i];
|
|
207
|
+
if (arg === "--format" || arg === "-f") {
|
|
208
|
+
format = rest[++i];
|
|
209
|
+
} else if (!arg.startsWith("-")) {
|
|
210
|
+
entityId = arg;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const tree = await read(aidePath);
|
|
216
|
+
|
|
217
|
+
if (format === "json") {
|
|
218
|
+
if (entityId) {
|
|
219
|
+
const entity = tree.entities[entityId];
|
|
220
|
+
if (!entity) {
|
|
221
|
+
console.error(`Entity not found: ${entityId}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
console.log(JSON.stringify({ id: entityId, ...entity }, null, 2));
|
|
225
|
+
} else {
|
|
226
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
// Tree format
|
|
230
|
+
if (entityId) {
|
|
231
|
+
showEntity(tree, entityId, 0);
|
|
232
|
+
} else {
|
|
233
|
+
showTree(tree);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(`Error reading aide file: ${error instanceof Error ? error.message : error}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Handle bantay aide validate
|
|
244
|
+
* Usage: bantay aide validate
|
|
245
|
+
*/
|
|
246
|
+
export async function handleAideValidate(args: string[]): Promise<void> {
|
|
247
|
+
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
|
+
}
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const tree = await read(aidePath);
|
|
257
|
+
const errors = validate(tree);
|
|
258
|
+
|
|
259
|
+
if (errors.length === 0) {
|
|
260
|
+
console.log("Aide file is valid.");
|
|
261
|
+
const entityCount = Object.keys(tree.entities).length;
|
|
262
|
+
const relCount = tree.relationships.length;
|
|
263
|
+
console.log(` Entities: ${entityCount}`);
|
|
264
|
+
console.log(` Relationships: ${relCount}`);
|
|
265
|
+
} else {
|
|
266
|
+
console.error("Validation errors:");
|
|
267
|
+
for (const error of errors) {
|
|
268
|
+
console.error(` - ${error}`);
|
|
269
|
+
}
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error(`Error validating aide file: ${error instanceof Error ? error.message : error}`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Handle bantay aide lock
|
|
280
|
+
* Usage: bantay aide lock
|
|
281
|
+
* Generates bantay.aide.lock with a hash of the current state
|
|
282
|
+
*/
|
|
283
|
+
export async function handleAideLock(args: string[]): Promise<void> {
|
|
284
|
+
const { options } = parseOptions(args);
|
|
285
|
+
const aidePath = getAidePath(options);
|
|
286
|
+
const lockPath = `${aidePath}.lock`;
|
|
287
|
+
|
|
288
|
+
if (!existsSync(aidePath)) {
|
|
289
|
+
console.error(`Error: Aide file not found: ${aidePath}`);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const tree = await read(aidePath);
|
|
295
|
+
const errors = validate(tree);
|
|
296
|
+
|
|
297
|
+
if (errors.length > 0) {
|
|
298
|
+
console.error("Cannot lock: aide file has validation errors");
|
|
299
|
+
for (const error of errors) {
|
|
300
|
+
console.error(` - ${error}`);
|
|
301
|
+
}
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Generate lock file content
|
|
306
|
+
const lockContent = generateLockFile(tree);
|
|
307
|
+
await writeFile(lockPath, lockContent, "utf-8");
|
|
308
|
+
|
|
309
|
+
console.log(`Lock file generated: ${lockPath}`);
|
|
310
|
+
console.log(` Entities: ${Object.keys(tree.entities).length}`);
|
|
311
|
+
console.log(` Relationships: ${tree.relationships.length}`);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error(`Error generating lock file: ${error instanceof Error ? error.message : error}`);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Handle bantay aide update
|
|
320
|
+
* Usage: bantay aide update <id> [--prop key=value...]
|
|
321
|
+
*/
|
|
322
|
+
export async function handleAideUpdate(args: string[]): Promise<void> {
|
|
323
|
+
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
|
+
}
|
|
330
|
+
|
|
331
|
+
let id: string | undefined;
|
|
332
|
+
const propsToSet: Record<string, unknown> = {};
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < rest.length; i++) {
|
|
335
|
+
const arg = rest[i];
|
|
336
|
+
if (arg === "--prop") {
|
|
337
|
+
const propStr = rest[++i];
|
|
338
|
+
const eqIdx = propStr.indexOf("=");
|
|
339
|
+
if (eqIdx > 0) {
|
|
340
|
+
const key = propStr.slice(0, eqIdx);
|
|
341
|
+
const value = propStr.slice(eqIdx + 1);
|
|
342
|
+
propsToSet[key] = parsePropertyValue(value);
|
|
343
|
+
}
|
|
344
|
+
} else if (arg.startsWith("--")) {
|
|
345
|
+
// Handle --checker, --statement, etc. as shorthand for --prop
|
|
346
|
+
const key = arg.slice(2);
|
|
347
|
+
const value = rest[++i];
|
|
348
|
+
if (value) {
|
|
349
|
+
propsToSet[key] = parsePropertyValue(value);
|
|
350
|
+
}
|
|
351
|
+
} else if (!arg.startsWith("-") && !id) {
|
|
352
|
+
id = arg;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!id) {
|
|
357
|
+
console.error("Error: Entity ID required");
|
|
358
|
+
console.error("Usage: bantay aide update <id> --prop key=value");
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (Object.keys(propsToSet).length === 0) {
|
|
363
|
+
console.error("Error: No properties to update");
|
|
364
|
+
console.error("Usage: bantay aide update <id> --prop key=value");
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
const tree = await read(aidePath);
|
|
370
|
+
|
|
371
|
+
if (!tree.entities[id]) {
|
|
372
|
+
console.error(`Error: Entity not found: ${id}`);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Update the entity's props
|
|
377
|
+
const entity = tree.entities[id];
|
|
378
|
+
entity.props = entity.props || {};
|
|
379
|
+
for (const [key, value] of Object.entries(propsToSet)) {
|
|
380
|
+
entity.props[key] = value;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
await write(aidePath, tree);
|
|
384
|
+
|
|
385
|
+
const updatedKeys = Object.keys(propsToSet).join(", ");
|
|
386
|
+
console.log(`Updated entity ${id}: ${updatedKeys}`);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error(`Error updating entity: ${error instanceof Error ? error.message : error}`);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// --- Helper Functions ---
|
|
394
|
+
|
|
395
|
+
function parsePropertyValue(value: string): unknown {
|
|
396
|
+
// Try to parse as JSON
|
|
397
|
+
try {
|
|
398
|
+
return JSON.parse(value);
|
|
399
|
+
} catch {
|
|
400
|
+
// Return as string
|
|
401
|
+
return value;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function findNewId(oldTree: AideTree, newTree: AideTree): string {
|
|
406
|
+
const oldIds = new Set(Object.keys(oldTree.entities));
|
|
407
|
+
for (const id of Object.keys(newTree.entities)) {
|
|
408
|
+
if (!oldIds.has(id)) {
|
|
409
|
+
return id;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return "unknown";
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function showTree(tree: AideTree): void {
|
|
416
|
+
console.log("Entities:");
|
|
417
|
+
|
|
418
|
+
// Find root entities (no parent)
|
|
419
|
+
const roots = Object.entries(tree.entities)
|
|
420
|
+
.filter(([_, entity]) => !entity.parent)
|
|
421
|
+
.map(([id]) => id);
|
|
422
|
+
|
|
423
|
+
for (const rootId of roots) {
|
|
424
|
+
showEntity(tree, rootId, 1);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (tree.relationships.length > 0) {
|
|
428
|
+
console.log("\nRelationships:");
|
|
429
|
+
for (const rel of tree.relationships) {
|
|
430
|
+
console.log(` ${rel.from} --[${rel.type}]--> ${rel.to}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function showEntity(tree: AideTree, id: string, indent: number): void {
|
|
436
|
+
const entity = tree.entities[id];
|
|
437
|
+
if (!entity) {
|
|
438
|
+
console.log(`${" ".repeat(indent * 2)}${id} (not found)`);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const prefix = " ".repeat(indent * 2);
|
|
443
|
+
const displayStr = entity.display ? ` [${entity.display}]` : "";
|
|
444
|
+
console.log(`${prefix}${id}${displayStr}`);
|
|
445
|
+
|
|
446
|
+
// Show props summary
|
|
447
|
+
if (entity.props) {
|
|
448
|
+
const propsKeys = Object.keys(entity.props);
|
|
449
|
+
if (propsKeys.length > 0) {
|
|
450
|
+
const summary = propsKeys.slice(0, 3).join(", ");
|
|
451
|
+
const more = propsKeys.length > 3 ? `, +${propsKeys.length - 3} more` : "";
|
|
452
|
+
console.log(`${prefix} props: {${summary}${more}}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Find and show children
|
|
457
|
+
const children = Object.entries(tree.entities)
|
|
458
|
+
.filter(([_, e]) => e.parent === id)
|
|
459
|
+
.map(([childId]) => childId);
|
|
460
|
+
|
|
461
|
+
for (const childId of children) {
|
|
462
|
+
showEntity(tree, childId, indent + 1);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function generateLockFile(tree: AideTree): string {
|
|
467
|
+
const lines: string[] = [];
|
|
468
|
+
lines.push("# bantay.aide.lock");
|
|
469
|
+
lines.push("# Auto-generated. Do not edit manually.");
|
|
470
|
+
lines.push(`# Generated: ${new Date().toISOString()}`);
|
|
471
|
+
lines.push("");
|
|
472
|
+
|
|
473
|
+
// Hash computation placeholder - for now just list entities
|
|
474
|
+
lines.push("entities:");
|
|
475
|
+
const sortedIds = Object.keys(tree.entities).sort();
|
|
476
|
+
for (const id of sortedIds) {
|
|
477
|
+
const entity = tree.entities[id];
|
|
478
|
+
const hash = computeEntityHash(id, entity);
|
|
479
|
+
lines.push(` ${id}: ${hash}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
lines.push("");
|
|
483
|
+
lines.push("relationships:");
|
|
484
|
+
for (const rel of tree.relationships) {
|
|
485
|
+
const hash = computeRelationshipHash(rel);
|
|
486
|
+
lines.push(` - ${rel.from}:${rel.to}:${rel.type}: ${hash}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return lines.join("\n") + "\n";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function computeEntityHash(id: string, entity: { display?: string; parent?: string; props?: Record<string, unknown> }): string {
|
|
493
|
+
// Simple hash for now - could use crypto.subtle for real hash
|
|
494
|
+
const str = JSON.stringify({ id, ...entity });
|
|
495
|
+
let hash = 0;
|
|
496
|
+
for (let i = 0; i < str.length; i++) {
|
|
497
|
+
const char = str.charCodeAt(i);
|
|
498
|
+
hash = ((hash << 5) - hash) + char;
|
|
499
|
+
hash = hash & hash;
|
|
500
|
+
}
|
|
501
|
+
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function computeRelationshipHash(rel: { from: string; to: string; type: string; cardinality: string }): string {
|
|
505
|
+
const str = JSON.stringify(rel);
|
|
506
|
+
let hash = 0;
|
|
507
|
+
for (let i = 0; i < str.length; i++) {
|
|
508
|
+
const char = str.charCodeAt(i);
|
|
509
|
+
hash = ((hash << 5) - hash) + char;
|
|
510
|
+
hash = hash & hash;
|
|
511
|
+
}
|
|
512
|
+
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Print help for aide subcommands
|
|
517
|
+
*/
|
|
518
|
+
export function printAideHelp(): void {
|
|
519
|
+
console.log(`
|
|
520
|
+
bantay aide - Manage the aide entity tree
|
|
521
|
+
|
|
522
|
+
Usage: bantay aide <subcommand> [options]
|
|
523
|
+
|
|
524
|
+
Subcommands:
|
|
525
|
+
add Add an entity to the tree
|
|
526
|
+
update Update an entity's properties
|
|
527
|
+
remove Remove an entity from the tree
|
|
528
|
+
link Add a relationship between entities
|
|
529
|
+
show Display the entity tree or a specific entity
|
|
530
|
+
validate Validate the aide file
|
|
531
|
+
lock Generate a lock file
|
|
532
|
+
|
|
533
|
+
Add Options:
|
|
534
|
+
<id> Entity ID (optional, auto-generated if parent provided)
|
|
535
|
+
--parent, -p Parent entity ID
|
|
536
|
+
--display, -d Display type (page, table, list, checklist)
|
|
537
|
+
--prop key=value Set a property (can be used multiple times)
|
|
538
|
+
|
|
539
|
+
Update Options:
|
|
540
|
+
<id> Entity ID (required)
|
|
541
|
+
--prop key=value Set a property (can be used multiple times)
|
|
542
|
+
--<key> <value> Shorthand for --prop key=value (e.g., --checker ./no-eval)
|
|
543
|
+
|
|
544
|
+
Remove Options:
|
|
545
|
+
<id> Entity ID (required)
|
|
546
|
+
--force, -f Force removal even if relationships exist
|
|
547
|
+
|
|
548
|
+
Link Options:
|
|
549
|
+
<from> <to> Source and target entity IDs
|
|
550
|
+
--type, -t Relationship type (required)
|
|
551
|
+
Valid: protected_by, depends_on, implements, delegates_to, weakens
|
|
552
|
+
--cardinality, -c Cardinality (default: many_to_many)
|
|
553
|
+
Valid: one_to_one, one_to_many, many_to_one, many_to_many
|
|
554
|
+
|
|
555
|
+
Show Options:
|
|
556
|
+
[id] Specific entity to show (optional)
|
|
557
|
+
--format, -f Output format: tree (default) or json
|
|
558
|
+
|
|
559
|
+
Global Options:
|
|
560
|
+
--aide, -a Path to aide file (default: bantay.aide)
|
|
561
|
+
|
|
562
|
+
Examples:
|
|
563
|
+
bantay aide add inv_new_check --parent invariants --prop "statement=My new check"
|
|
564
|
+
bantay aide update inv_no_network --checker ./no-network
|
|
565
|
+
bantay aide remove inv_old_check --force
|
|
566
|
+
bantay aide link sc_init_new inv_new_check --type protected_by
|
|
567
|
+
bantay aide show invariants
|
|
568
|
+
bantay aide validate
|
|
569
|
+
bantay aide lock
|
|
570
|
+
`);
|
|
571
|
+
}
|