filegrc 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/src/build.js ADDED
@@ -0,0 +1,27 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { FAVICON_PNG } from "./favicon.js";
4
+ import { resolveWorkspacePath, resolveWorkspaceRoot } from "./paths.js";
5
+ import { createAppState } from "./state.js";
6
+ import { APP_SCRIPT, APP_STYLES, renderIndex } from "./web.js";
7
+
8
+ export async function buildWorkspace(input = process.cwd(), options = {}) {
9
+ const root = resolveWorkspaceRoot(input);
10
+ const outputOption = options.output ?? ".filegrc/site";
11
+ const output = resolveWorkspacePath(root, outputOption);
12
+ const paths = {
13
+ html: resolveWorkspacePath(root, join(outputOption, "index.html")),
14
+ favicon: resolveWorkspacePath(root, join(outputOption, "favicon.png")),
15
+ script: resolveWorkspacePath(root, join(outputOption, "filegrc-app.js")),
16
+ styles: resolveWorkspacePath(root, join(outputOption, "filegrc.css"))
17
+ };
18
+ const state = await createAppState(root, { readOnly: true });
19
+ await mkdir(output, { recursive: true });
20
+ await Promise.all([
21
+ writeFile(paths.html, renderIndex(state), "utf8"),
22
+ writeFile(paths.favicon, FAVICON_PNG),
23
+ writeFile(paths.script, APP_SCRIPT, "utf8"),
24
+ writeFile(paths.styles, APP_STYLES, "utf8")
25
+ ]);
26
+ return { output, state };
27
+ }
package/src/cli.js ADDED
@@ -0,0 +1,623 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { loadModel } from "../model/index.js";
4
+ import { buildAgentGuide, findResourceReferences, listResourceTypes, scaffoldResourceMutation } from "./agent.js";
5
+ import { assessAuditPreparation, prepareAuditWorkspace } from "./audit-preparation.js";
6
+ import { buildWorkspace } from "./build.js";
7
+ import { generateEvidencePacket, prepareEvidencePacket } from "./evidence-packet.js";
8
+ import {
9
+ addEvidenceAttachment,
10
+ createResource,
11
+ deleteResource,
12
+ removeEvidenceAttachment,
13
+ updateResource
14
+ } from "./files.js";
15
+ import { generateModelDocumentation } from "./model-docs.js";
16
+ import {
17
+ completeObligationAction,
18
+ completeObligationEvent,
19
+ completeObligationOccurrence,
20
+ createObligationEvent,
21
+ planObligations
22
+ } from "./obligations.js";
23
+ import { relativeToWorkspace, resolveDataPath } from "./paths.js";
24
+ import { markdownEntries } from "./resource-markdown.js";
25
+ import { searchResources } from "./search.js";
26
+ import { serveWorkspace } from "./server.js";
27
+ import { createAppState } from "./state.js";
28
+ import { currentCalendarDate } from "./time.js";
29
+ import { validateWorkspace } from "./validate.js";
30
+ import { loadWorkspace } from "./workspace.js";
31
+
32
+ const BOOLEAN_FLAGS = new Set([
33
+ "check-docs",
34
+ "complete",
35
+ "json",
36
+ "mutation",
37
+ "preview",
38
+ "require-ready",
39
+ "write-docs",
40
+ "yes"
41
+ ]);
42
+
43
+ export async function runCli(argv = process.argv.slice(2)) {
44
+ const [command = "help", ...args] = argv;
45
+ const { positionals, flags } = parseArgs(args);
46
+ const root = flags.root ?? process.cwd();
47
+
48
+ if (["help", "--help", "-h"].includes(command)) return printHelp();
49
+ if (["version", "--version", "-v"].includes(command)) return printVersion();
50
+
51
+ if (command === "serve") {
52
+ const result = await serveWorkspace(positionals[0] ?? root, { host: flags.host, port: flags.port });
53
+ console.log(`FileGRC workspace: ${result.url}`);
54
+ console.log(`Data: ${result.root}/data`);
55
+ return await new Promise((resolvePromise) => {
56
+ const stop = () => result.server.close(resolvePromise);
57
+ process.once("SIGINT", stop);
58
+ process.once("SIGTERM", stop);
59
+ });
60
+ }
61
+ if (command === "build") {
62
+ const result = await buildWorkspace(positionals[0] ?? root, { output: flags.output });
63
+ console.log(`Built read-only site at ${result.output}`);
64
+ return;
65
+ }
66
+ if (command === "validate") {
67
+ const result = await validateWorkspace(positionals[0] ?? root);
68
+ if (flags.json) console.log(JSON.stringify({ ok: result.ok, counts: result.counts, diagnostics: result.diagnostics }, null, 2));
69
+ else printValidation(result);
70
+ if (!result.ok) process.exitCode = 1;
71
+ return result;
72
+ }
73
+ if (command === "model") {
74
+ const model = loadModel();
75
+ const source = generateModelDocumentation(model);
76
+ const path = resolve(String(flags.docs ?? "docs/data-model.md"));
77
+ if (flags["write-docs"]) {
78
+ await writeFile(path, source, "utf8");
79
+ console.log(`Wrote ${path}`);
80
+ } else if (flags["check-docs"]) {
81
+ let existing = "";
82
+ try { existing = await readFile(path, "utf8"); } catch {}
83
+ if (existing !== source) {
84
+ console.error(`${path} is not generated from model v${model.modelVersion}. Run filegrc model --write-docs.`);
85
+ process.exitCode = 1;
86
+ } else console.log(`${path} matches model v${model.modelVersion}.`);
87
+ } else if (flags.json) console.log(JSON.stringify(model, null, 2));
88
+ else console.log(source);
89
+ return;
90
+ }
91
+ if (command === "describe") {
92
+ const loaded = await loadWorkspace(root);
93
+ const type = positionals[0];
94
+ const definition = loaded.model.resources[type];
95
+ if (!definition) throw new Error(`Unknown resource type "${type}".`);
96
+ console.log(JSON.stringify({ type, ...definition, commonFields: loaded.model.commonFields }, null, 2));
97
+ return;
98
+ }
99
+ if (command === "types") {
100
+ const loaded = await loadWorkspace(root);
101
+ const types = listResourceTypes(loaded.model);
102
+ if (flags.json) console.log(JSON.stringify(types, null, 2));
103
+ else for (const item of types) console.log(`${item.type}\t${item.title}\t${item.purpose}`);
104
+ return types;
105
+ }
106
+ if (command === "guide") {
107
+ const loaded = await loadWorkspace(root);
108
+ const type = positionals[0];
109
+ if (!type) {
110
+ const result = agentOverview(loaded.model);
111
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
112
+ else printAgentOverview(result);
113
+ return result;
114
+ }
115
+ const result = buildAgentGuide(loaded, type, { id: flags.id });
116
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
117
+ else printAgentGuide(result);
118
+ return result;
119
+ }
120
+ if (command === "scaffold") {
121
+ const loaded = await loadWorkspace(root);
122
+ const type = positionals[0];
123
+ const result = scaffoldResourceMutation(loaded, type, flags.title, { id: flags.id });
124
+ console.log(JSON.stringify(result, null, 2));
125
+ return result;
126
+ }
127
+ if (command === "list") {
128
+ const loaded = await loadWorkspace(root);
129
+ const type = positionals[0];
130
+ if (type && !loaded.model.resources[type]) throw new Error(`Unknown resource type "${type}".`);
131
+ const records = loaded.resources
132
+ .filter((record) => !type || record.type === type)
133
+ .sort((left, right) => `${left.type}:${left.title}:${left.id}`.localeCompare(`${right.type}:${right.title}:${right.id}`));
134
+ if (flags.json) console.log(JSON.stringify(records, null, 2));
135
+ else for (const record of records) console.log(`${record.id}\t${record.type}\t${record.status ?? ""}\t${record.title}`);
136
+ return records;
137
+ }
138
+ if (command === "search") {
139
+ const loaded = await loadWorkspace(root);
140
+ const query = positionals.join(" ");
141
+ const results = searchResources(loaded.resources, loaded.model, { query, type: flags.type });
142
+ if (flags.json) console.log(JSON.stringify(results, null, 2));
143
+ else for (const resource of results) console.log(`${resource.id}\t${resource.type}\t${resource.title}`);
144
+ return results;
145
+ }
146
+ if (command === "obligations") {
147
+ const loaded = await loadWorkspace(root);
148
+ const result = planObligations(loaded.resources, {
149
+ asOf: flags["as-of"] ?? currentCalendarDate(loaded.workspace.timezone),
150
+ from: flags.from,
151
+ through: flags.through,
152
+ now: flags.now,
153
+ includeComplete: Boolean(flags.complete)
154
+ });
155
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
156
+ else {
157
+ console.log(`${result.counts.overdue} overdue, ${result.counts.due} due, ${result.counts.upcoming} upcoming`);
158
+ for (const item of result.items) {
159
+ const deadline = item.dueWindowEndAt || item.dueWindowEnd;
160
+ if (!deadline) throw new Error(`Planned work "${item.title}" is missing a deadline.`);
161
+ console.log([
162
+ item.status.toUpperCase(),
163
+ item.dueWindowStartAt || item.dueWindowStart,
164
+ deadline,
165
+ item.title,
166
+ item.actionItemId || item.obligationId
167
+ ].join("\t"));
168
+ }
169
+ if (result.triggers.length) {
170
+ console.log("\nEvent reminders:");
171
+ for (const trigger of result.triggers) console.log(`${trigger.eventType}\t${trigger.prompt}\t${trigger.steps.length} actions`);
172
+ }
173
+ }
174
+ return result;
175
+ }
176
+ if (command === "audit-readiness") {
177
+ const loaded = await loadWorkspace(root);
178
+ const result = await assessAuditPreparation(loaded, { auditId: positionals[0] || flags.audit });
179
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
180
+ else {
181
+ console.log(`${result.status.toUpperCase()}: ${result.progress.complete} of ${result.progress.total} management items complete`);
182
+ for (const stage of result.stages) {
183
+ console.log(`\n${stage.title}`);
184
+ for (const item of stage.items) {
185
+ console.log(`${item.status.toUpperCase()}\t${item.title}\t${item.message}`);
186
+ }
187
+ }
188
+ }
189
+ if (flags["require-ready"] && result.status !== "management-ready") process.exitCode = 2;
190
+ return result;
191
+ }
192
+ if (command === "prepare-audit") {
193
+ const auditId = positionals[0] || flags.audit;
194
+ if (!auditId) throw new Error("An audit ID is required.");
195
+ const result = await prepareAuditWorkspace(root, { auditId });
196
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
197
+ else console.log(`Prepared ${result.auditId}: linked ${result.linkedDocumentIds.length} management documents and created ${result.createdPopulationIds.length} population records.`);
198
+ return result;
199
+ }
200
+ if (command === "trigger") {
201
+ const result = await createObligationEvent(root, {
202
+ eventType: positionals[0],
203
+ occurredOn: flags["occurred-on"],
204
+ occurredAt: flags["occurred-at"],
205
+ subjectResourceIds: String(flags.subject || "").split(",").map((value) => value.trim()).filter(Boolean),
206
+ title: flags.title
207
+ });
208
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
209
+ else console.log(`Created ${result.event.id} with ${result.actions.length} action items.`);
210
+ return result;
211
+ }
212
+ if (command === "evidence-packet") {
213
+ const options = {
214
+ start: flags.start,
215
+ end: flags.end,
216
+ auditId: flags.audit,
217
+ output: flags.output
218
+ };
219
+ const generated = flags.preview
220
+ ? { packet: await prepareEvidencePacket(root, options), output: null, files: [] }
221
+ : await generateEvidencePacket(root, options);
222
+ const packet = generated.packet;
223
+ let output = null;
224
+ let files = generated.files;
225
+ if (!flags.preview) {
226
+ output = relativeToWorkspace(root, generated.output);
227
+ }
228
+ const result = { packet, output, files };
229
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
230
+ else {
231
+ console.log(`${packet.readiness.status.toUpperCase()}: ${packet.summary.datedRecords} dated records, ${packet.summary.obligationOccurrences} obligation occurrences, ${packet.summary.evidence} evidence records, ${packet.summary.errors} errors, ${packet.summary.warnings} warnings`);
232
+ console.log(flags.preview ? "Preview only; no files written." : `Wrote evidence packet to ${output}`);
233
+ }
234
+ if (flags["require-ready"] && packet.readiness.status !== "delivery-ready") process.exitCode = 2;
235
+ return result;
236
+ }
237
+ if (command === "get") {
238
+ const loaded = await loadWorkspace(root);
239
+ const [first, second] = positionals;
240
+ const type = second ? first : null;
241
+ const id = second ?? first;
242
+ if (!id) throw new Error("A resource ID is required.");
243
+ const record = loaded.resources.find((item) => item.id === id && (!type || item.type === type));
244
+ if (!record) throw new Error(`Resource "${type ? `${type}/` : ""}${id}" was not found.`);
245
+ if (flags.mutation) {
246
+ const state = await createAppState(root);
247
+ const entry = state.resources.find((item) => item.record.id === record.id);
248
+ const contentEntries = Object.entries(entry.content ?? {}).filter(([, content]) => content.source !== null);
249
+ const mutation = {
250
+ record,
251
+ ...(contentEntries.length ? {
252
+ content: Object.fromEntries(contentEntries.map(([name, content]) => [name, content.source])),
253
+ contentRevisions: Object.fromEntries(contentEntries.map(([, content]) => [content.path, content.revision]))
254
+ } : {}),
255
+ revision: entry.revision
256
+ };
257
+ console.log(JSON.stringify(mutation, null, 2));
258
+ return mutation;
259
+ }
260
+ console.log(JSON.stringify(record, null, 2));
261
+ return record;
262
+ }
263
+ if (command === "references") {
264
+ const loaded = await loadWorkspace(root);
265
+ const result = findResourceReferences(loaded, positionals[0]);
266
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
267
+ else {
268
+ console.log(`${result.resource.type}/${result.resource.id} has ${result.references.length} inbound reference${result.references.length === 1 ? "" : "s"}.`);
269
+ for (const reference of result.references) {
270
+ console.log(`${reference.type}/${reference.id}\t${reference.field}\t${reference.title}`);
271
+ }
272
+ }
273
+ return result;
274
+ }
275
+ if (command === "create") {
276
+ const mutation = await readMutation(positionals[0]);
277
+ const result = await createResource(root, mutation.record, { content: mutation.content });
278
+ if (flags.json) console.log(JSON.stringify({ record: result.record }, null, 2));
279
+ else console.log(`Created ${result.record.type}/${result.record.id}`);
280
+ return result;
281
+ }
282
+ if (command === "complete") {
283
+ const [obligationId, file] = positionals;
284
+ const mutation = await readMutation(file);
285
+ const result = await completeObligationOccurrence(root, {
286
+ obligationId,
287
+ record: mutation.record,
288
+ content: mutation.content,
289
+ expectedRevision: flags["expected-revision"]
290
+ });
291
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
292
+ else console.log(`Created ${result.created.type}/${result.created.id} and linked it to obligation/${obligationId}`);
293
+ return result;
294
+ }
295
+ if (command === "complete-action") {
296
+ const [actionItemId, file] = positionals;
297
+ const mutation = await readMutation(file);
298
+ const result = await completeObligationAction(root, {
299
+ actionItemId,
300
+ completedOn: flags["completed-on"],
301
+ record: mutation.record,
302
+ content: mutation.content,
303
+ expectedRevision: flags["expected-revision"]
304
+ });
305
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
306
+ else console.log(`Created ${result.created.type}/${result.created.id}, linked it to action-item/${actionItemId}, and marked the action done.`);
307
+ return result;
308
+ }
309
+ if (command === "complete-event") {
310
+ const eventId = positionals[0];
311
+ if (!eventId) throw new Error("An obligation event ID is required.");
312
+ const result = await completeObligationEvent(root, {
313
+ eventId,
314
+ completedOn: flags["completed-on"],
315
+ expectedRevision: flags["expected-revision"]
316
+ });
317
+ if (flags.json) console.log(JSON.stringify({ record: result.record }, null, 2));
318
+ else console.log(`Marked obligation-event/${eventId} complete.`);
319
+ return result;
320
+ }
321
+ if (command === "update") {
322
+ const [type, id, file] = positionals;
323
+ const mutation = await readMutation(file);
324
+ const result = await updateResource(root, type, id, mutation.record, {
325
+ content: mutation.content,
326
+ expectedRevision: mutation.revision,
327
+ expectedContentRevisions: mutation.contentRevisions
328
+ });
329
+ if (flags.json) console.log(JSON.stringify({ record: result.record }, null, 2));
330
+ else console.log(`Updated ${result.record.type}/${result.record.id}`);
331
+ return result;
332
+ }
333
+ if (command === "content") {
334
+ const [type, id, requestedSlot] = positionals;
335
+ if (!type || !id) throw new Error("A resource type and ID are required.");
336
+ const loaded = await loadWorkspace(root);
337
+ const record = loaded.resources.find((item) => item.type === type && item.id === id);
338
+ if (!record) throw new Error(`Resource "${type}/${id}" was not found.`);
339
+ const entries = markdownEntries(loaded.model, record);
340
+ const slot = requestedSlot
341
+ ? entries.find((item) => item.name === requestedSlot)
342
+ : entries.find((item) => item.primary) ?? entries[0];
343
+ if (!slot) throw new Error(`Markdown slot "${requestedSlot ?? ""}" was not found for ${type}/${id}.`);
344
+ if (flags.write !== undefined) {
345
+ const source = await readTextInput(flags.write);
346
+ const state = await createAppState(root);
347
+ const stateEntry = state.resources.find((item) => item.record.type === type && item.record.id === id);
348
+ const existingContentRevision = stateEntry.content?.[slot.name]?.revision;
349
+ await updateResource(root, type, id, record, {
350
+ content: { [slot.name]: source },
351
+ expectedRevision: stateEntry.revision,
352
+ expectedContentRevisions: existingContentRevision
353
+ ? { [slot.path]: flags["expected-revision"] ?? existingContentRevision }
354
+ : undefined
355
+ });
356
+ const result = { type, id, slot: slot.name, path: `data/${slot.path}`, written: true };
357
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
358
+ else console.log(`Updated ${result.path}`);
359
+ return result;
360
+ }
361
+ let source = null;
362
+ try {
363
+ source = await readFile(resolveDataPath(root, slot.path), "utf8");
364
+ } catch (error) {
365
+ if (error.code !== "ENOENT") throw error;
366
+ }
367
+ const result = { type, id, slot: slot.name, path: `data/${slot.path}`, exists: source !== null, source };
368
+ if (flags.json) console.log(JSON.stringify(result, null, 2));
369
+ else if (source !== null) process.stdout.write(source);
370
+ else console.log(`No Markdown exists at ${result.path}.`);
371
+ return result;
372
+ }
373
+ if (command === "attach") {
374
+ const [evidenceId, sourcePath] = positionals;
375
+ if (!evidenceId || !sourcePath) throw new Error("An evidence ID and source file are required.");
376
+ const result = await addEvidenceAttachment(root, evidenceId, sourcePath, {
377
+ name: flags.name,
378
+ expectedRevision: flags["expected-revision"]
379
+ });
380
+ const output = {
381
+ evidenceId,
382
+ path: `data/${result.dataRelativePath}`,
383
+ filePaths: result.record.filePaths
384
+ };
385
+ if (flags.json) console.log(JSON.stringify(output, null, 2));
386
+ else console.log(`Attached ${output.path} to evidence/${evidenceId}`);
387
+ return output;
388
+ }
389
+ if (command === "detach") {
390
+ const [evidenceId, attachment] = positionals;
391
+ if (!evidenceId || !attachment) throw new Error("An evidence ID and attachment name are required.");
392
+ if (!flags.yes) throw new Error("Pass --yes to confirm attachment removal.");
393
+ const result = await removeEvidenceAttachment(root, evidenceId, attachment, {
394
+ expectedRevision: flags["expected-revision"]
395
+ });
396
+ const output = {
397
+ evidenceId,
398
+ removed: `data/${result.dataRelativePath}`,
399
+ filePaths: result.record.filePaths ?? []
400
+ };
401
+ if (flags.json) console.log(JSON.stringify(output, null, 2));
402
+ else console.log(`Detached and removed ${output.removed} from evidence/${evidenceId}`);
403
+ return output;
404
+ }
405
+ if (command === "delete") {
406
+ const [type, id] = positionals;
407
+ if (!flags.yes) throw new Error("Pass --yes to confirm deletion. Preserve historical records unless this is a mistake or uncommitted draft.");
408
+ await deleteResource(root, type, id, { expectedRevision: flags["expected-revision"] });
409
+ console.log(`Deleted ${type}/${id}`);
410
+ return;
411
+ }
412
+ throw new Error(`Unknown command "${command}". Run filegrc help.`);
413
+ }
414
+
415
+ function parseArgs(args) {
416
+ const positionals = [];
417
+ const flags = {};
418
+ for (let index = 0; index < args.length; index += 1) {
419
+ const value = args[index];
420
+ if (!value.startsWith("--")) {
421
+ positionals.push(value);
422
+ continue;
423
+ }
424
+ const source = value.slice(2);
425
+ const separator = source.indexOf("=");
426
+ const name = separator === -1 ? source : source.slice(0, separator);
427
+ const inline = separator === -1 ? undefined : source.slice(separator + 1);
428
+ if (inline !== undefined) flags[name] = inline;
429
+ else if (BOOLEAN_FLAGS.has(name)) flags[name] = true;
430
+ else if (args[index + 1] && !args[index + 1].startsWith("--")) flags[name] = args[++index];
431
+ else flags[name] = true;
432
+ }
433
+ return { positionals, flags };
434
+ }
435
+
436
+ async function readMutation(path) {
437
+ if (!path) throw new Error("A JSON file path or - is required.");
438
+ const source = path === "-" ? await readStdin() : await readFile(resolve(path), "utf8");
439
+ const parsed = JSON.parse(source);
440
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
441
+ throw new Error("A resource record or { record, content } mutation object is required.");
442
+ }
443
+ if (!Object.hasOwn(parsed, "record")) {
444
+ return { record: parsed, content: undefined, revision: undefined, contentRevisions: undefined };
445
+ }
446
+ if (!parsed.record || Array.isArray(parsed.record) || typeof parsed.record !== "object") {
447
+ throw new Error("Mutation record must be a JSON object.");
448
+ }
449
+ if (parsed.content !== undefined && (Array.isArray(parsed.content) || typeof parsed.content !== "object" || parsed.content === null)) {
450
+ throw new Error("Mutation content must be an object keyed by Markdown slot.");
451
+ }
452
+ if (parsed.revision !== undefined && typeof parsed.revision !== "string") {
453
+ throw new Error("Mutation revision must be a string.");
454
+ }
455
+ if (
456
+ parsed.contentRevisions !== undefined
457
+ && (Array.isArray(parsed.contentRevisions) || typeof parsed.contentRevisions !== "object" || parsed.contentRevisions === null)
458
+ ) {
459
+ throw new Error("Mutation contentRevisions must be an object keyed by data-relative Markdown path.");
460
+ }
461
+ return {
462
+ record: parsed.record,
463
+ content: parsed.content,
464
+ revision: parsed.revision,
465
+ contentRevisions: parsed.contentRevisions
466
+ };
467
+ }
468
+
469
+ async function readTextInput(path) {
470
+ if (path === true || !path) throw new Error("Pass --write <markdown-file|->.");
471
+ return path === "-" ? readStdin() : readFile(resolve(String(path)), "utf8");
472
+ }
473
+
474
+ async function readStdin() {
475
+ const chunks = [];
476
+ for await (const chunk of process.stdin) chunks.push(chunk);
477
+ return Buffer.concat(chunks).toString("utf8");
478
+ }
479
+
480
+ function printValidation(result) {
481
+ for (const item of result.diagnostics) console.log(`${item.severity.toUpperCase()} ${item.path}: ${item.message}`);
482
+ console.log(`${result.counts.resources} resources, ${result.counts.errors} errors, ${result.counts.warnings} warnings`);
483
+ }
484
+
485
+ async function printVersion() {
486
+ const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
487
+ console.log(packageJson.version);
488
+ }
489
+
490
+ function printHelp() {
491
+ console.log(`FileGRC - Git-native GRC workspace
492
+
493
+ Usage:
494
+ filegrc serve [root] [--host 127.0.0.1] [--port 8787]
495
+ filegrc build [root] [--output .filegrc/site]
496
+ filegrc validate [root] [--json]
497
+ filegrc model [--json|--write-docs|--check-docs]
498
+ filegrc describe <resource-type>
499
+ filegrc types [--json]
500
+ filegrc guide [resource-type] [--id resource-id] [--json]
501
+ filegrc scaffold <resource-type> --title text [--id resource-id]
502
+ filegrc list [resource-type] [--json]
503
+ filegrc search <query> [--type resource-type] [--json]
504
+ filegrc obligations [--as-of YYYY-MM-DD] [--from YYYY-MM-DD] [--through YYYY-MM-DD] [--now RFC3339] [--complete] [--json]
505
+ filegrc audit-readiness [audit-id] [--require-ready] [--json]
506
+ filegrc prepare-audit <audit-id> [--json]
507
+ filegrc trigger <event-type> (--occurred-on YYYY-MM-DD | --occurred-at RFC3339) [--subject resource-id[,resource-id]] [--title text] [--json]
508
+ filegrc evidence-packet [--audit audit-id] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--output .filegrc/path] [--preview] [--require-ready] [--json]
509
+ filegrc get [resource-type] <id> [--mutation]
510
+ filegrc references <id> [--json]
511
+ filegrc create <record-or-mutation.json|-> [--json]
512
+ filegrc complete <obligation-id> <completion-record.json|-> [--expected-revision hash] [--json]
513
+ filegrc complete-action <action-item-id> <completion-record.json|-> --completed-on YYYY-MM-DD [--expected-revision hash] [--json]
514
+ filegrc complete-event <obligation-event-id> --completed-on YYYY-MM-DD [--expected-revision hash] [--json]
515
+ filegrc update <resource-type> <id> <record-or-mutation.json|-> [--json]
516
+ filegrc content <resource-type> <id> [slot] [--write markdown-file|-] [--expected-revision hash] [--json]
517
+ filegrc attach <evidence-id> <source-file> [--name file-name] [--expected-revision hash] [--json]
518
+ filegrc detach <evidence-id> <attachment-name> --yes [--expected-revision hash] [--json]
519
+ filegrc delete <resource-type> <id> --yes [--expected-revision hash]
520
+
521
+ All commands accept --root <workspace>. Writes never create Git commits.`);
522
+ }
523
+
524
+ function agentOverview(model) {
525
+ return {
526
+ rule: "Treat data/ as the source of truth. Run guide before creating an unfamiliar type, validate after every write, review the Git diff, then commit a focused change.",
527
+ actions: {
528
+ help: "filegrc help",
529
+ version: "filegrc version",
530
+ serve: "filegrc serve [root]",
531
+ build: "filegrc build [root]",
532
+ validate: "filegrc validate [root] --json",
533
+ model: "filegrc model --json",
534
+ describe: "filegrc describe <resource-type>",
535
+ types: "filegrc types --json",
536
+ guide: "filegrc guide [resource-type] --json",
537
+ scaffold: "filegrc scaffold <resource-type> --title <name>",
538
+ list: "filegrc list [resource-type] --json",
539
+ search: "filegrc search <query> --json",
540
+ obligations: "filegrc obligations --json",
541
+ auditReadiness: "filegrc audit-readiness <audit-id> --json",
542
+ prepareAudit: "filegrc prepare-audit <audit-id>",
543
+ trigger: "filegrc trigger <event-type> <date-or-time-and-subject-flags>",
544
+ evidencePacket: "filegrc evidence-packet --audit <audit-id> --preview --json",
545
+ get: "filegrc get <resource-id> [--mutation]",
546
+ references: "filegrc references <resource-id> --json",
547
+ create: "filegrc create <record-or-mutation.json>",
548
+ complete: "filegrc complete <obligation-id> <completion-mutation.json>",
549
+ completeAction: "filegrc complete-action <action-item-id> <completion-mutation.json> --completed-on <date>",
550
+ completeEvent: "filegrc complete-event <obligation-event-id> --completed-on <date>",
551
+ update: "filegrc update <resource-type> <id> <record-or-mutation.json>",
552
+ content: "filegrc content <resource-type> <id> [slot] [--write <markdown-file|->]",
553
+ attach: "filegrc attach <evidence-id> <source-file> [--name <file-name>]",
554
+ detach: "filegrc detach <evidence-id> <attachment-name> --yes",
555
+ delete: "filegrc delete <resource-type> <id> --yes",
556
+ commit: "git diff --check && git diff && git add <reviewed-paths> && git commit -m <reason>"
557
+ },
558
+ resourceTypes: listResourceTypes(model).map(({ type, title, group }) => ({ type, title, group }))
559
+ };
560
+ }
561
+
562
+ function printAgentOverview(result) {
563
+ console.log(result.rule);
564
+ console.log("\nActions:");
565
+ for (const [name, command] of Object.entries(result.actions)) console.log(`${name}\t${command}`);
566
+ console.log("\nResource types:");
567
+ for (const item of result.resourceTypes) console.log(`${item.type}\t${item.title}\t${item.group ?? ""}`);
568
+ }
569
+
570
+ function printAgentGuide(result) {
571
+ console.log(`${result.title} (${result.type})`);
572
+ console.log(`Purpose: ${result.purpose}`);
573
+ console.log(`Policy basis: ${result.policyBasis}`);
574
+ console.log(`Timing: ${result.cadence}`);
575
+ console.log(`JSON: ${result.location}`);
576
+ console.log("\nRequired fields:");
577
+ for (const field of result.requiredAtCreation) console.log(formatGuideField(field));
578
+ if (result.conditionalRequirements.length) {
579
+ console.log("\nConditional fields:");
580
+ for (const field of result.conditionalRequirements) console.log(formatGuideField(field));
581
+ }
582
+ if (result.optionalFields.length) {
583
+ console.log("\nOptional fields:");
584
+ for (const field of result.optionalFields) console.log(formatGuideField(field));
585
+ }
586
+ if (result.markdown.length) {
587
+ console.log("\nMarkdown:");
588
+ for (const slot of result.markdown) {
589
+ console.log(`${slot.name}\t${slot.required ? "required" : slot.recommended ? "recommended" : "optional"}\t${slot.path}`);
590
+ }
591
+ }
592
+ const relationshipFields = [
593
+ ...result.requiredAtCreation,
594
+ ...result.conditionalRequirements,
595
+ ...result.optionalFields
596
+ ].filter(({ relation }) => relation);
597
+ if (relationshipFields.length) {
598
+ console.log("\nRelationship candidates:");
599
+ for (const field of relationshipFields) {
600
+ const hasCandidates = field.relation.candidates.length > 0;
601
+ const suffix = field.relation.truncated && hasCandidates
602
+ ? `, … (${field.relation.candidateCount} total; use filegrc list)`
603
+ : "";
604
+ const candidates = hasCandidates
605
+ ? field.relation.candidates.join(", ") + suffix
606
+ : field.relation.candidateCount
607
+ ? `use filegrc list (${field.relation.candidateCount} possible)`
608
+ : "none";
609
+ console.log(`${field.name}\t${field.relation.types.join("|")}\t${candidates}`);
610
+ }
611
+ }
612
+ console.log("\nWorkflow:");
613
+ result.workflow.forEach((step, index) => console.log(`${index + 1}. ${step}`));
614
+ }
615
+
616
+ function formatGuideField(field) {
617
+ const details = [
618
+ field.values?.length ? `one of ${field.values.join("|")}` : field.type,
619
+ field.requiredWhen ? `required when ${JSON.stringify(field.requiredWhen)}` : null,
620
+ field.disjointFrom ? `must not overlap ${field.disjointFrom}` : null
621
+ ].filter(Boolean).join("; ");
622
+ return `${field.name}\t${details}`;
623
+ }