@izagood/avcs 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.
Files changed (111) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +221 -0
  3. package/dist/api/repo.d.ts +616 -0
  4. package/dist/api/repo.d.ts.map +1 -0
  5. package/dist/api/repo.js +1772 -0
  6. package/dist/api/repo.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +660 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/concurrency/lease.d.ts +27 -0
  12. package/dist/concurrency/lease.d.ts.map +1 -0
  13. package/dist/concurrency/lease.js +63 -0
  14. package/dist/concurrency/lease.js.map +1 -0
  15. package/dist/core/canonical.d.ts +16 -0
  16. package/dist/core/canonical.d.ts.map +1 -0
  17. package/dist/core/canonical.js +66 -0
  18. package/dist/core/canonical.js.map +1 -0
  19. package/dist/core/cbor.d.ts +9 -0
  20. package/dist/core/cbor.d.ts.map +1 -0
  21. package/dist/core/cbor.js +213 -0
  22. package/dist/core/cbor.js.map +1 -0
  23. package/dist/core/clock.d.ts +18 -0
  24. package/dist/core/clock.d.ts.map +1 -0
  25. package/dist/core/clock.js +32 -0
  26. package/dist/core/clock.js.map +1 -0
  27. package/dist/core/identity.d.ts +34 -0
  28. package/dist/core/identity.d.ts.map +1 -0
  29. package/dist/core/identity.js +62 -0
  30. package/dist/core/identity.js.map +1 -0
  31. package/dist/hub/hubClient.d.ts +38 -0
  32. package/dist/hub/hubClient.d.ts.map +1 -0
  33. package/dist/hub/hubClient.js +157 -0
  34. package/dist/hub/hubClient.js.map +1 -0
  35. package/dist/hub/hubServer.d.ts +28 -0
  36. package/dist/hub/hubServer.d.ts.map +1 -0
  37. package/dist/hub/hubServer.js +365 -0
  38. package/dist/hub/hubServer.js.map +1 -0
  39. package/dist/index.d.ts +13 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +19 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/objects/types.d.ts +352 -0
  44. package/dist/objects/types.d.ts.map +1 -0
  45. package/dist/objects/types.js +17 -0
  46. package/dist/objects/types.js.map +1 -0
  47. package/dist/observe/logger.d.ts +27 -0
  48. package/dist/observe/logger.d.ts.map +1 -0
  49. package/dist/observe/logger.js +37 -0
  50. package/dist/observe/logger.js.map +1 -0
  51. package/dist/observe/metrics.d.ts +20 -0
  52. package/dist/observe/metrics.d.ts.map +1 -0
  53. package/dist/observe/metrics.js +42 -0
  54. package/dist/observe/metrics.js.map +1 -0
  55. package/dist/policy/owners.d.ts +5 -0
  56. package/dist/policy/owners.d.ts.map +1 -0
  57. package/dist/policy/owners.js +38 -0
  58. package/dist/policy/owners.js.map +1 -0
  59. package/dist/policy/reliability.d.ts +7 -0
  60. package/dist/policy/reliability.d.ts.map +1 -0
  61. package/dist/policy/reliability.js +41 -0
  62. package/dist/policy/reliability.js.map +1 -0
  63. package/dist/query/diff.d.ts +16 -0
  64. package/dist/query/diff.d.ts.map +1 -0
  65. package/dist/query/diff.js +32 -0
  66. package/dist/query/diff.js.map +1 -0
  67. package/dist/reducer/incremental.d.ts +2 -0
  68. package/dist/reducer/incremental.d.ts.map +1 -0
  69. package/dist/reducer/incremental.js +14 -0
  70. package/dist/reducer/incremental.js.map +1 -0
  71. package/dist/reducer/policy.d.ts +17 -0
  72. package/dist/reducer/policy.d.ts.map +1 -0
  73. package/dist/reducer/policy.js +143 -0
  74. package/dist/reducer/policy.js.map +1 -0
  75. package/dist/reducer/reducer.d.ts +155 -0
  76. package/dist/reducer/reducer.d.ts.map +1 -0
  77. package/dist/reducer/reducer.js +849 -0
  78. package/dist/reducer/reducer.js.map +1 -0
  79. package/dist/release/sbom.d.ts +7 -0
  80. package/dist/release/sbom.d.ts.map +1 -0
  81. package/dist/release/sbom.js +26 -0
  82. package/dist/release/sbom.js.map +1 -0
  83. package/dist/semantic/contract.d.ts +32 -0
  84. package/dist/semantic/contract.d.ts.map +1 -0
  85. package/dist/semantic/contract.js +150 -0
  86. package/dist/semantic/contract.js.map +1 -0
  87. package/dist/semantic/symbols.d.ts +45 -0
  88. package/dist/semantic/symbols.d.ts.map +1 -0
  89. package/dist/semantic/symbols.js +323 -0
  90. package/dist/semantic/symbols.js.map +1 -0
  91. package/dist/store/applyRedactions.d.ts +16 -0
  92. package/dist/store/applyRedactions.d.ts.map +1 -0
  93. package/dist/store/applyRedactions.js +60 -0
  94. package/dist/store/applyRedactions.js.map +1 -0
  95. package/dist/store/lock.d.ts +12 -0
  96. package/dist/store/lock.d.ts.map +1 -0
  97. package/dist/store/lock.js +63 -0
  98. package/dist/store/lock.js.map +1 -0
  99. package/dist/store/objectStore.d.ts +127 -0
  100. package/dist/store/objectStore.d.ts.map +1 -0
  101. package/dist/store/objectStore.js +549 -0
  102. package/dist/store/objectStore.js.map +1 -0
  103. package/dist/validation/repair.d.ts +31 -0
  104. package/dist/validation/repair.d.ts.map +1 -0
  105. package/dist/validation/repair.js +37 -0
  106. package/dist/validation/repair.js.map +1 -0
  107. package/dist/validation/runner.d.ts +20 -0
  108. package/dist/validation/runner.d.ts.map +1 -0
  109. package/dist/validation/runner.js +90 -0
  110. package/dist/validation/runner.js.map +1 -0
  111. package/package.json +83 -0
package/dist/cli.js ADDED
@@ -0,0 +1,660 @@
1
+ #!/usr/bin/env node
2
+ // AVCS CLI — inspection & materialization.
3
+ //
4
+ // In an agent-native VCS the primary interface is the MCP server (src/mcp/server.ts);
5
+ // agents author intents/sessions/operations through it. This CLI is the human's
6
+ // read-and-decide surface: see what the agents did, why, and what still needs a call.
7
+ //
8
+ // avcs init [dir]
9
+ // avcs status [view]
10
+ // avcs conflicts [view]
11
+ // avcs log
12
+ // avcs materialize [view] [--out <dir>]
13
+ // avcs checkpoint <view> [-m <summary>]
14
+ // avcs show <oid>
15
+ import { readFileSync } from "node:fs";
16
+ import { fileURLToPath } from "node:url";
17
+ import { dirname, join } from "node:path";
18
+ import { Repo } from "./api/repo.js";
19
+ import { ObjectStore } from "./store/objectStore.js";
20
+ const args = process.argv.slice(2);
21
+ let cmd = args[0];
22
+ const cwd = process.cwd();
23
+ // Normalize the version/help flags so `avcs --version` / `avcs -h` work like
24
+ // every other CLI, instead of falling through to the usage exit-1 path.
25
+ if (cmd === "--version" || cmd === "-v")
26
+ cmd = "version";
27
+ if (cmd === "--help" || cmd === "-h")
28
+ cmd = "help";
29
+ function flag(name) {
30
+ const i = args.indexOf(name);
31
+ return i >= 0 ? args[i + 1] : undefined;
32
+ }
33
+ function pkgVersion() {
34
+ try {
35
+ const here = dirname(fileURLToPath(import.meta.url));
36
+ const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
37
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
38
+ }
39
+ catch {
40
+ return "0.0.0";
41
+ }
42
+ }
43
+ // The exact command that re-invokes this CLI (node + strip-types + this script path), so
44
+ // installed hooks call the same AVCS the user is running now — no global install assumed.
45
+ function avcsInvocation() {
46
+ return `${JSON.stringify(process.execPath)} --experimental-strip-types ${JSON.stringify(process.argv[1])}`;
47
+ }
48
+ const HOOK_PHASES = ["pre-commit", "prepare-commit-msg", "post-commit", "post-merge"];
49
+ const HOOK_MARKER = "# avcs-git-bridge-hook";
50
+ function hookScript(phase, avcsCmd) {
51
+ return `#!/bin/sh
52
+ ${HOOK_MARKER} ${phase}
53
+ # Managed by \`avcs install-hooks\` (docs/14). Delete this file to disable.
54
+ exec ${avcsCmd} git-hook ${phase} "$@"
55
+ `;
56
+ }
57
+ /** Install the git-bridge hook scripts into `hooksDir`, preserving any non-AVCS hooks. */
58
+ async function installHooks(hooksDir, avcsCmd, force) {
59
+ const { writeFile, mkdir, readFile, chmod } = await import("node:fs/promises");
60
+ const { existsSync } = await import("node:fs");
61
+ const { join } = await import("node:path");
62
+ await mkdir(hooksDir, { recursive: true });
63
+ const installed = [];
64
+ const skipped = [];
65
+ for (const phase of HOOK_PHASES) {
66
+ const p = join(hooksDir, phase);
67
+ if (existsSync(p)) {
68
+ const existing = await readFile(p, "utf8");
69
+ if (!existing.includes(HOOK_MARKER) && !force) {
70
+ skipped.push(phase);
71
+ continue;
72
+ } // a foreign hook — don't clobber
73
+ }
74
+ await writeFile(p, hookScript(phase, avcsCmd), "utf8");
75
+ await chmod(p, 0o755);
76
+ installed.push(phase);
77
+ }
78
+ return { installed, skipped };
79
+ }
80
+ async function main() {
81
+ switch (cmd) {
82
+ case "version": {
83
+ console.log(`avcs ${pkgVersion()}`);
84
+ break;
85
+ }
86
+ case "init": {
87
+ const dir = args[1] && !args[1].startsWith("--") ? args[1] : cwd;
88
+ const repo = await Repo.init(dir);
89
+ const want = flag("--mode");
90
+ const mode = want === "committed" ? "committed" : "sidecar";
91
+ await repo.setGitMode(mode);
92
+ console.log(`initialized AVCS repo at ${dir}/.avcs [git mode: ${mode}]`);
93
+ if (mode === "sidecar")
94
+ console.log(` .avcs/ is git-ignored — git tracks only the projection (run \`avcs git-mode committed\` to share history via git)`);
95
+ // If this is a git repo, offer to install the bridge hooks so `git commit` just works.
96
+ if (!args.includes("--no-hooks")) {
97
+ const { execFileSync } = await import("node:child_process");
98
+ try {
99
+ const gp = execFileSync("git", ["rev-parse", "--git-path", "hooks"], { cwd: dir }).toString().trim();
100
+ const { isAbsolute, join } = await import("node:path");
101
+ const hooksDir = isAbsolute(gp) ? gp : join(dir, gp);
102
+ const cmd = `${JSON.stringify(process.execPath)} --experimental-strip-types ${JSON.stringify(process.argv[1])}`;
103
+ const { installed } = await installHooks(hooksDir, cmd, false);
104
+ if (installed.length)
105
+ console.log(` installed git hooks (${installed.join(", ")}) — \`git commit\` now auto-syncs AVCS (--no-hooks to skip)`);
106
+ }
107
+ catch { /* not a git repo — fine; user can `git init` then `avcs install-hooks` */ }
108
+ }
109
+ break;
110
+ }
111
+ case "status": {
112
+ const repo = await Repo.open(cwd);
113
+ const view = args[1] ?? "main";
114
+ const res = await repo.materialize(view);
115
+ const counts = {};
116
+ for (const s of res.statuses.values())
117
+ counts[s] = (counts[s] ?? 0) + 1;
118
+ console.log(`view: ${view}`);
119
+ console.log(`operations: ${JSON.stringify(counts)}`);
120
+ console.log(`files: ${res.tree.size} conflicts: ${res.conflicts.length} auto-merged: ${res.autoDecisions.length}`);
121
+ console.log(`treeHash: ${res.treeHash}`);
122
+ for (const a of res.autoDecisions)
123
+ console.log(` ✓ auto @ ${a.key}: chose ${a.chosenOp.slice(0, 16)} (policy ${a.policyVersion})`);
124
+ if (res.conflicts.length)
125
+ console.log(`\nrun \`avcs conflicts ${view}\` to review`);
126
+ break;
127
+ }
128
+ case "conflicts": {
129
+ const repo = await Repo.open(cwd);
130
+ const view = args[1] ?? "main";
131
+ const res = await repo.materialize(view);
132
+ if (!res.conflicts.length) {
133
+ console.log("no open conflicts — nothing needs a human.");
134
+ break;
135
+ }
136
+ for (const c of res.conflicts) {
137
+ console.log(`\n● ${c.id} [${c.kind}] @ ${c.key}`);
138
+ console.log(` ${c.reason}`);
139
+ for (const o of c.options) {
140
+ const tags = [o.blocked && "blocked", o.requiresHuman && "needs-human"]
141
+ .filter(Boolean)
142
+ .join(",");
143
+ console.log(` - ${o.opOid}`);
144
+ console.log(` ${o.actor} :: ${o.purpose} (score ${o.score}${tags ? ", " + tags : ""})`);
145
+ }
146
+ if (c.recommendedOp)
147
+ console.log(` → recommended: ${c.recommendedOp}`);
148
+ console.log(` decide via MCP avcs.decision.record or the API`);
149
+ }
150
+ break;
151
+ }
152
+ case "metrics": {
153
+ const repo = await Repo.open(cwd);
154
+ await repo.materialize(args[1] ?? "main"); // do some work so there's something to show
155
+ console.log(JSON.stringify(repo.metrics.snapshot(), null, 2));
156
+ break;
157
+ }
158
+ case "blame": {
159
+ const repo = await Repo.open(cwd);
160
+ const key = args[1];
161
+ if (!key)
162
+ throw new Error("usage: avcs blame <file:path | symbol:path#name>");
163
+ const b = await repo.blame(key, flag("--line") ?? "main");
164
+ if (!b)
165
+ console.log("no owner (entity not present)");
166
+ else
167
+ console.log(`${b.actor.id} ${b.op.slice(0, 16)}\n why: ${b.purpose}${b.intentTitle ? ` [intent: ${b.intentTitle}]` : ""}\n at: ${b.at}`);
168
+ break;
169
+ }
170
+ case "diff": {
171
+ const repo = await Repo.open(cwd);
172
+ const a = args[1] ?? "main";
173
+ const b = args[2] ?? "main";
174
+ const d = await repo.diff(a, b);
175
+ for (const p of d.added)
176
+ console.log(`+ ${p}`);
177
+ for (const p of d.removed)
178
+ console.log(`- ${p}`);
179
+ for (const p of d.modified)
180
+ console.log(`~ ${p}`);
181
+ if (!d.added.length && !d.removed.length && !d.modified.length)
182
+ console.log("(no differences)");
183
+ break;
184
+ }
185
+ case "pull": {
186
+ const repo = await Repo.open(cwd);
187
+ const from = args[1];
188
+ if (!from)
189
+ throw new Error("usage: avcs pull <hub-url | other-repo-dir>");
190
+ if (/^https?:\/\//.test(from)) {
191
+ const r = await repo.pullHub(from);
192
+ console.log(`pulled ${r.pulled} object(s) from hub ${from}`);
193
+ }
194
+ else {
195
+ const r = await repo.pull(from);
196
+ console.log(`pulled ${r.copied} object(s)${r.rejected ? `, rejected ${r.rejected}` : ""}`);
197
+ }
198
+ break;
199
+ }
200
+ case "push": {
201
+ const repo = await Repo.open(cwd);
202
+ const url = args[1];
203
+ if (!url || !/^https?:\/\//.test(url))
204
+ throw new Error("usage: avcs push <hub-url>");
205
+ const r = await repo.pushHub(url);
206
+ console.log(`pushed ${r.pushed} object(s) to ${url}${r.rejected ? `, rejected ${r.rejected} (gated)` : ""}`);
207
+ break;
208
+ }
209
+ case "clone": {
210
+ const url = args[1];
211
+ const dir = args[2] ?? cwd;
212
+ if (!url || !/^https?:\/\//.test(url))
213
+ throw new Error("usage: avcs clone <hub-url> [dir]");
214
+ const repo = await Repo.init(dir);
215
+ const r = await repo.pullHub(url);
216
+ console.log(`cloned ${r.pulled} object(s) from ${url} into ${dir}`);
217
+ break;
218
+ }
219
+ case "serve": {
220
+ const { startHub } = await import("./hub/hubServer.js");
221
+ const { consoleLogger } = await import("./observe/logger.js");
222
+ const dir = args[1] && !args[1].startsWith("--") ? args[1] : cwd;
223
+ const port = Number(flag("--port") ?? 0);
224
+ const gated = args.includes("--gated");
225
+ const quiet = args.includes("--quiet");
226
+ const hub = await startHub({ repoDir: dir, port, gated, logger: quiet ? undefined : consoleLogger("info") });
227
+ console.log(`avcs hub serving ${dir} at ${hub.url}${gated ? " (gated: member-signed ops only)" : ""}`);
228
+ console.log("press Ctrl-C to stop");
229
+ const stop = async () => { await hub.close(); process.exit(0); };
230
+ process.on("SIGINT", stop);
231
+ process.on("SIGTERM", stop);
232
+ await new Promise(() => { }); // run until signalled
233
+ break;
234
+ }
235
+ case "head": {
236
+ const repo = await Repo.open(cwd);
237
+ const view = args[1] ?? "main";
238
+ const h = await repo.protectedHead(view);
239
+ console.log(h ? `${view}: ${h}` : `${view}: (not finalized)`);
240
+ break;
241
+ }
242
+ case "import": {
243
+ const repo = await Repo.open(cwd);
244
+ const src = args[1];
245
+ if (!src)
246
+ throw new Error("usage: avcs import <source-dir> [-m message] [--author id]");
247
+ const message = flag("-m") ?? flag("--message") ?? `import ${src}`;
248
+ const author = flag("--author") ?? "human:cli";
249
+ const r = await repo.commitWorkingTree(src, { message, actor: { kind: "human", id: author } });
250
+ console.log(`imported ${r.ops.length} file(s) from ${src} (${r.added.length} new)`);
251
+ break;
252
+ }
253
+ case "gc": {
254
+ const repo = await Repo.open(cwd);
255
+ const dryRun = args.includes("--dry-run");
256
+ const r = await repo.gc({ dryRun });
257
+ console.log(`${dryRun ? "would collect" : "collected"} ${r.blobs.length} orphan blob(s), ${r.quarantinedOps.length} expired quarantine op(s)`);
258
+ break;
259
+ }
260
+ case "pack": {
261
+ const repo = await Repo.open(cwd);
262
+ const r = await repo.pack();
263
+ console.log(`packed ${r.packed} loose object(s) into a packfile (blobs left loose)`);
264
+ break;
265
+ }
266
+ case "compact": {
267
+ const repo = await Repo.open(cwd);
268
+ const view = args[1] && !args[1].startsWith("--") ? args[1] : "main";
269
+ const r = await repo.compact(view);
270
+ console.log(`compacted ${view}: persisted a base snapshot over ${r.baseOps} op(s) (run with AVCS_COMPACT=1 to use it)`);
271
+ break;
272
+ }
273
+ case "bundle": {
274
+ const repo = await Repo.open(cwd);
275
+ const out = args[1];
276
+ if (!out)
277
+ throw new Error("usage: avcs bundle <out-file>");
278
+ const { writeFile } = await import("node:fs/promises");
279
+ const b = await repo.exportBundle();
280
+ await writeFile(out, JSON.stringify(b), "utf8");
281
+ console.log(`bundled ${b.objects.length} object(s) + ${Object.keys(b.refs).length} ref(s) → ${out}`);
282
+ break;
283
+ }
284
+ case "unbundle": {
285
+ const repo = await Repo.open(cwd);
286
+ const file = args[1];
287
+ if (!file)
288
+ throw new Error("usage: avcs unbundle <bundle-file>");
289
+ const { readFile } = await import("node:fs/promises");
290
+ const b = JSON.parse(await readFile(file, "utf8"));
291
+ const r = await repo.importBundle(b);
292
+ console.log(`unbundled ${r.objects} object(s), ${r.refs} ref(s)`);
293
+ break;
294
+ }
295
+ case "checkout": {
296
+ const repo = await Repo.open(cwd);
297
+ const view = args[1] && !args[1].startsWith("--") ? args[1] : "main";
298
+ const written = await repo.checkoutInto(cwd, view);
299
+ console.log(`checked out ${written.length} file(s) from ${view}`);
300
+ break;
301
+ }
302
+ case "commit": {
303
+ const repo = await Repo.open(cwd);
304
+ const message = flag("-m") ?? flag("--message");
305
+ if (!message)
306
+ throw new Error("usage: avcs commit -m <message> [--author <id>] [--line <line>]");
307
+ const author = flag("--author") ?? "human:cli";
308
+ const line = flag("--line");
309
+ const r = await repo.commitWorkingTree(cwd, { message, actor: { kind: "human", id: author }, ...(line ? { line } : {}) });
310
+ if (!r.ops.length) {
311
+ console.log("nothing to commit (working tree matches the view)");
312
+ break;
313
+ }
314
+ for (const p of r.added)
315
+ console.log(` A ${p}`);
316
+ for (const p of r.modified)
317
+ console.log(` M ${p}`);
318
+ for (const p of r.removed)
319
+ console.log(` D ${p}`);
320
+ console.log(`committed ${r.ops.length} change(s) as "${message}"`);
321
+ break;
322
+ }
323
+ case "git-sync": {
324
+ const repo = await Repo.open(cwd);
325
+ const message = flag("-m") ?? flag("--message");
326
+ if (!message)
327
+ throw new Error("usage: avcs git-sync -m <message> [--commit] [--author <id>] [--line <line>] [--no-add]");
328
+ const author = flag("--author") ?? "human:cli";
329
+ const line = flag("--line");
330
+ const r = await repo.gitSync({ message, actor: { kind: "human", id: author }, ...(line ? { line } : {}) });
331
+ for (const p of r.captured.added)
332
+ console.log(` A ${p}`);
333
+ for (const p of r.captured.modified)
334
+ console.log(` M ${p}`);
335
+ for (const p of r.captured.removed)
336
+ console.log(` D ${p}`);
337
+ if (r.conflicts.length) {
338
+ console.error(`\n✗ ${r.conflicts.length} open conflict(s) need a human — refusing to stage a conflicted tree.`);
339
+ console.error(` run \`avcs conflicts ${line ?? "main"}\` to review; resolve, then re-run git-sync.`);
340
+ process.exitCode = 1;
341
+ break;
342
+ }
343
+ console.log(`captured ${r.captured.ops.length} op(s) · checkpoint ${r.checkpoint.slice(0, 16)}… · treeHash ${r.treeHash.slice(0, 12)}…`);
344
+ console.log(`reprojected ${r.reprojected} file(s) [git mode: ${r.mode}]`);
345
+ const wantCommit = args.includes("--commit");
346
+ if (!args.includes("--no-add") || wantCommit) {
347
+ const { execFileSync } = await import("node:child_process");
348
+ try {
349
+ execFileSync("git", ["add", "-A"], { cwd, stdio: "inherit" });
350
+ if (wantCommit) {
351
+ // Inject the provenance trailer (git→avcs) then record the SHA back-link (avcs→git).
352
+ const trailerOn = await repo.gitTrailerEnabled();
353
+ const body = trailerOn
354
+ ? `${message}\n\n${repo.gitTrailer({ checkpoint: r.checkpoint, treeHash: r.treeHash, ...(r.captured.intent ? { intent: r.captured.intent } : {}) })}`
355
+ : message;
356
+ execFileSync("git", ["commit", "-m", body], { cwd, stdio: "inherit" });
357
+ const sha = execFileSync("git", ["rev-parse", "HEAD"], { cwd }).toString().trim();
358
+ await repo.recordGitCommit(sha, r.checkpoint);
359
+ console.log(`committed ${sha.slice(0, 12)} ↔ checkpoint ${r.checkpoint.slice(0, 16)}… (\`avcs verify-git\` to check)`);
360
+ }
361
+ else {
362
+ console.log(`staged working tree (git add -A) — now run \`git commit\` (or re-run with --commit)`);
363
+ }
364
+ }
365
+ catch {
366
+ console.error(`(git step failed — not a git repo? stage/commit manually, or pass --no-add)`);
367
+ process.exitCode = 1;
368
+ }
369
+ }
370
+ break;
371
+ }
372
+ case "verify-git": {
373
+ const repo = await Repo.open(cwd);
374
+ const { execFileSync } = await import("node:child_process");
375
+ const git = (a) => execFileSync("git", a, { cwd }).toString();
376
+ let sha;
377
+ try {
378
+ sha = args[1] && !args[1].startsWith("--") ? git(["rev-parse", args[1]]).trim() : git(["rev-parse", "HEAD"]).trim();
379
+ }
380
+ catch {
381
+ console.error(`not a git repo (or bad ref) — verify-git needs a git commit`);
382
+ process.exitCode = 1;
383
+ break;
384
+ }
385
+ // Find the checkpoint: prefer the local back-link ref, fall back to the commit trailer.
386
+ let cp = await repo.gitCheckpoint(sha);
387
+ if (!cp) {
388
+ const m = git(["log", "-1", "--format=%B", sha]).match(/^AVCS-Checkpoint:\s*(\S+)/m);
389
+ cp = m?.[1] ?? null;
390
+ }
391
+ if (!cp) {
392
+ console.error(`✗ no AVCS checkpoint linked to ${sha.slice(0, 12)} (no back-link ref, no trailer)`);
393
+ process.exitCode = 1;
394
+ break;
395
+ }
396
+ const { treeHashOk, files } = await repo.checkpointFiles(cp);
397
+ const avcs = new Map(files.map((f) => [f.path, f.content]));
398
+ // Compare against git's committed tree, excluding the .avcs/ history itself (committed mode).
399
+ const gitPaths = git(["ls-tree", "-r", "--name-only", sha]).split("\n").filter((p) => p && !p.startsWith(".avcs/"));
400
+ const gitSet = new Set(gitPaths);
401
+ const diffs = [];
402
+ for (const p of gitSet)
403
+ if (!avcs.has(p))
404
+ diffs.push(` +git only: ${p}`);
405
+ for (const p of avcs.keys())
406
+ if (!gitSet.has(p))
407
+ diffs.push(` -avcs only: ${p}`);
408
+ for (const p of gitSet)
409
+ if (avcs.has(p) && git(["show", `${sha}:${p}`]) !== avcs.get(p))
410
+ diffs.push(` ≠ content: ${p}`);
411
+ if (!treeHashOk)
412
+ diffs.push(` ! checkpoint treeHash no longer reproduces from its frontier`);
413
+ if (diffs.length) {
414
+ console.error(`✗ ${sha.slice(0, 12)} does NOT match checkpoint ${cp.slice(0, 16)}… (${diffs.length} difference(s)):`);
415
+ for (const d of diffs.slice(0, 20))
416
+ console.error(d);
417
+ process.exitCode = 1;
418
+ }
419
+ else {
420
+ console.log(`✓ ${sha.slice(0, 12)} is a faithful projection of checkpoint ${cp.slice(0, 16)}… (${avcs.size} file(s) match)`);
421
+ }
422
+ break;
423
+ }
424
+ case "git-mode": {
425
+ const repo = await Repo.open(cwd);
426
+ const want = args[1];
427
+ if (!want) {
428
+ console.log(`git mode: ${await repo.getGitMode()}`);
429
+ break;
430
+ }
431
+ if (want !== "sidecar" && want !== "committed")
432
+ throw new Error("usage: avcs git-mode [sidecar|committed]");
433
+ await repo.setGitMode(want);
434
+ console.log(`git mode set to: ${want} (rewrote .avcs/.gitignore)`);
435
+ if (want === "committed")
436
+ console.log(` next: \`git add .avcs\` to start tracking AVCS history, then commit`);
437
+ else
438
+ console.log(` .avcs/ is now git-ignored; \`git rm -r --cached .avcs\` to stop tracking already-committed history`);
439
+ break;
440
+ }
441
+ case "reindex": {
442
+ const repo = await Repo.open(cwd);
443
+ const r = await repo.reindex();
444
+ console.log(`reindexed ${r.ops} operation(s) into the entity index`);
445
+ break;
446
+ }
447
+ case "install-hooks": {
448
+ await Repo.open(cwd); // validate this is an AVCS repo
449
+ const { execFileSync } = await import("node:child_process");
450
+ let hooksDir;
451
+ try {
452
+ const gp = execFileSync("git", ["rev-parse", "--git-path", "hooks"], { cwd }).toString().trim();
453
+ const { isAbsolute, join } = await import("node:path");
454
+ hooksDir = isAbsolute(gp) ? gp : join(cwd, gp);
455
+ }
456
+ catch {
457
+ console.error(`not a git repo — run \`git init\` first`);
458
+ process.exitCode = 1;
459
+ break;
460
+ }
461
+ const { installed, skipped } = await installHooks(hooksDir, avcsInvocation(), args.includes("--force"));
462
+ if (installed.length)
463
+ console.log(`installed git hooks: ${installed.join(", ")}`);
464
+ if (skipped.length)
465
+ console.log(`skipped (foreign hook present, use --force): ${skipped.join(", ")}`);
466
+ console.log(`now \`git commit\` auto-runs avcs sync; \`git pull\`/\`merge\` auto-reprojects`);
467
+ break;
468
+ }
469
+ case "git-hook": {
470
+ // Internal dispatch target for the installed hook scripts (docs/14). Each phase is
471
+ // designed to be safe to run by hand, and a no-op when there is nothing to do.
472
+ const phase = args[1];
473
+ const repo = await Repo.open(cwd);
474
+ const { execFileSync } = await import("node:child_process");
475
+ const author = process.env.AVCS_AUTHOR ?? "human:cli";
476
+ switch (phase) {
477
+ case "pre-commit": {
478
+ // Capture working-tree edits as ops, gate on conflicts, checkpoint, reproject,
479
+ // re-stage the canonical projection, and stash the provenance for the next hooks.
480
+ const message = process.env.AVCS_COMMIT_MESSAGE ?? "git commit";
481
+ const r = await repo.gitSync({ message, actor: { kind: "human", id: author } });
482
+ if (r.conflicts.length) {
483
+ console.error(`avcs: ${r.conflicts.length} open conflict(s) — resolve via \`avcs conflicts\` before committing.`);
484
+ process.exit(1); // abort the commit
485
+ }
486
+ execFileSync("git", ["add", "-A"], { cwd, stdio: "inherit" });
487
+ await repo.writeGitPending({ checkpoint: r.checkpoint, treeHash: r.treeHash, ...(r.captured.intent ? { intent: r.captured.intent } : {}) });
488
+ break;
489
+ }
490
+ case "prepare-commit-msg": {
491
+ // args: [2]=msgFile [3]=source [4]=sha. Append the trailer if enabled & absent.
492
+ const msgFile = args[2];
493
+ if (!msgFile)
494
+ break;
495
+ const pending = await repo.readGitPending();
496
+ if (!pending || !(await repo.gitTrailerEnabled()))
497
+ break;
498
+ const { readFile, writeFile } = await import("node:fs/promises");
499
+ const cur = await readFile(msgFile, "utf8");
500
+ if (cur.includes("AVCS-Checkpoint:"))
501
+ break;
502
+ const trailer = repo.gitTrailer({ checkpoint: pending.checkpoint, treeHash: pending.treeHash, ...(pending.intent ? { intent: pending.intent } : {}) });
503
+ await writeFile(msgFile, `${cur.replace(/\n*$/, "")}\n\n${trailer}\n`, "utf8");
504
+ break;
505
+ }
506
+ case "post-commit": {
507
+ const pending = await repo.readGitPending();
508
+ if (!pending)
509
+ break;
510
+ const sha = execFileSync("git", ["rev-parse", "HEAD"], { cwd }).toString().trim();
511
+ await repo.recordGitCommit(sha, pending.checkpoint);
512
+ await repo.clearGitPending();
513
+ break;
514
+ }
515
+ case "post-merge": {
516
+ // A git pull/merge unioned new objects (committed mode) straight onto disk:
517
+ // rebuild the index and re-project deterministically from the merged op graph.
518
+ await repo.reindex();
519
+ await repo.checkoutInto(cwd, "main");
520
+ break;
521
+ }
522
+ default:
523
+ console.error(`unknown git-hook phase: ${phase}`);
524
+ process.exitCode = 1;
525
+ }
526
+ break;
527
+ }
528
+ case "lines": {
529
+ const repo = await Repo.open(cwd);
530
+ const lines = await repo.listLines();
531
+ console.log("main (root)");
532
+ for (const l of lines)
533
+ console.log(`${l.name} ← forked from ${l.baseLine} @ ${l.forkCheckpointOid?.slice(0, 16)}`);
534
+ break;
535
+ }
536
+ case "log": {
537
+ const store = new ObjectStore(cwd);
538
+ const ops = await store.collect("operation");
539
+ ops.sort((a, b) => a.lamport - b.lamport);
540
+ for (const op of ops) {
541
+ const tgt = `${op.target.entityKind}:${op.target.entityId}`;
542
+ console.log(`[${String(op.lamport).padStart(3, "0")}] ${op.actor.id} ${op.body.kind} ${tgt}` +
543
+ `\n ${op.declaredPurpose}`);
544
+ }
545
+ break;
546
+ }
547
+ case "materialize": {
548
+ const repo = await Repo.open(cwd);
549
+ const view = args[1] && !args[1].startsWith("--") ? args[1] : "main";
550
+ const res = await repo.materialize(view);
551
+ const out = flag("--out");
552
+ if (out) {
553
+ await repo.writeWorkspace(res, out);
554
+ console.log(`wrote ${res.tree.size} files to ${out}`);
555
+ }
556
+ else {
557
+ for (const p of [...res.tree.keys()].sort())
558
+ console.log(p);
559
+ }
560
+ break;
561
+ }
562
+ case "checkpoint": {
563
+ const repo = await Repo.open(cwd);
564
+ const view = args[1] ?? "main";
565
+ const oid = await repo.createCheckpoint(view, flag("-m") ?? "checkpoint");
566
+ console.log(oid);
567
+ break;
568
+ }
569
+ case "release": {
570
+ const repo = await Repo.open(cwd);
571
+ const view = args[1] && !args[1].startsWith("--") ? args[1] : "main";
572
+ const res = await repo.cutRelease(view, { summary: flag("-m") ?? `release of ${view}` });
573
+ if (!res.released) {
574
+ console.error(`cannot release: ${res.reason}`);
575
+ process.exitCode = 1;
576
+ break;
577
+ }
578
+ const rel = await repo.store.get(res.releaseOid);
579
+ console.log(`released ${res.releaseOid}`);
580
+ console.log(` treeHash : ${rel.treeHash}`);
581
+ console.log(` sbom : ${rel.sbom.components.length} components`);
582
+ console.log(` evidence : ${JSON.stringify(rel.evidence)}`);
583
+ break;
584
+ }
585
+ case "fsck": {
586
+ const store = new ObjectStore(cwd);
587
+ if (!ObjectStore.isRepo(cwd))
588
+ throw new Error("not an AVCS repo (no .avcs here)");
589
+ const rebuild = args.includes("--rebuild");
590
+ const r = await store.fsck({ rebuild });
591
+ console.log(`checked ${r.objectsChecked} object(s)`);
592
+ if (r.corrupt.length) {
593
+ console.log(`\n✗ ${r.corrupt.length} corrupt object(s):`);
594
+ for (const c of r.corrupt)
595
+ console.log(` ${c.oid} — ${c.reason}`);
596
+ }
597
+ const d = r.oplogDrift;
598
+ if (d.opsMissingFromLog.length)
599
+ console.log(`\n${rebuild ? "↻ repaired" : "✗"} op-log drift: ${d.opsMissingFromLog.length} operation(s) missing from the log${rebuild ? "" : " (run `avcs fsck --rebuild`)"}`);
600
+ if (d.logEntriesMissingObject.length)
601
+ console.log(`\nℹ ${d.logEntriesMissingObject.length} op-log entr(y/ies) without an object (GC'd or lost)`);
602
+ if (r.repaired)
603
+ console.log(` op-log rebuilt → ${r.repaired.oplogEntries} entr(y/ies)`);
604
+ console.log(r.ok ? "\n✓ healthy" : rebuild && r.corrupt.length === 0 ? "\n✓ repaired" : "\n✗ problems found");
605
+ if (!r.ok && !(rebuild && r.corrupt.length === 0))
606
+ process.exitCode = 1;
607
+ break;
608
+ }
609
+ case "show": {
610
+ const store = new ObjectStore(cwd);
611
+ const oid = args[1];
612
+ if (!oid)
613
+ throw new Error("usage: avcs show <oid>");
614
+ console.log(JSON.stringify(await store.get(oid), null, 2));
615
+ break;
616
+ }
617
+ case "help":
618
+ default:
619
+ console.log("avcs <command>\n\n" +
620
+ " init [dir] [--mode m] create a repo (--mode sidecar|committed, default sidecar)\n" +
621
+ " status [view] operation/conflict summary\n" +
622
+ " conflicts [view] list decisions a human owes\n" +
623
+ " import <dir> [-m msg] import an existing tree (e.g. a git repo) as ops\n" +
624
+ " gc [--dry-run] reclaim orphan blobs + expired quarantine ops\n" +
625
+ " pack fold loose objects into a packfile (blobs stay loose)\n" +
626
+ " compact [view] persist a base snapshot (cold materialize folds history)\n" +
627
+ " fsck [--rebuild] verify object integrity + op-log; --rebuild repairs the log\n" +
628
+ " bundle <file> export the whole repo to a portable file\n" +
629
+ " unbundle <file> import a bundle into this repo\n" +
630
+ " checkout [view] write the view's files into the working dir\n" +
631
+ " commit -m <msg> [--author id] author ops for working-tree changes\n" +
632
+ " git-sync -m <msg> [--commit] capture edits → checkpoint → reproject → git add (--commit: also commit w/ trailer)\n" +
633
+ " git-mode [sidecar|committed] show/set how AVCS history relates to git\n" +
634
+ " verify-git [<commit>] check a git commit is a faithful projection of its AVCS checkpoint\n" +
635
+ " install-hooks [--force] install git hooks so `git commit`/`pull` auto-sync AVCS\n" +
636
+ " reindex rebuild the entity index (after a git pull of .avcs objects)\n" +
637
+ " serve [dir] [--port N] [--gated] run a hub (HTTP) over a repo\n" +
638
+ " clone <hub-url> [dir] create a repo from a hub\n" +
639
+ " push <hub-url> push objects to a hub\n" +
640
+ " pull <hub-url | dir> sync objects from a hub or local repo\n" +
641
+ " head [view] show the protected head\n" +
642
+ " lines list lineage lines (Phase 8)\n" +
643
+ " blame <entityKey> [--line l] who owns an entity and why\n" +
644
+ " diff <viewA> <viewB> added/removed/modified paths\n" +
645
+ " log operation history\n" +
646
+ " materialize [view] [--out d] project the code tree\n" +
647
+ " checkpoint <view> [-m msg] freeze a verified state\n" +
648
+ " release [view] [-m msg] cut a verified release + SBOM\n" +
649
+ " show <oid> dump an object\n" +
650
+ " version | --version | -v print the avcs version\n" +
651
+ " help | --help | -h show this help\n");
652
+ if (cmd && cmd !== "help")
653
+ process.exitCode = 1;
654
+ }
655
+ }
656
+ main().catch((e) => {
657
+ console.error(`error: ${e instanceof Error ? e.message : String(e)}`);
658
+ process.exit(1);
659
+ });
660
+ //# sourceMappingURL=cli.js.map