@git-stunts/git-warp 10.8.0 → 11.3.3

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 (136) hide show
  1. package/README.md +53 -32
  2. package/SECURITY.md +64 -0
  3. package/bin/cli/commands/check.js +168 -0
  4. package/bin/cli/commands/doctor/checks.js +422 -0
  5. package/bin/cli/commands/doctor/codes.js +46 -0
  6. package/bin/cli/commands/doctor/index.js +239 -0
  7. package/bin/cli/commands/doctor/types.js +89 -0
  8. package/bin/cli/commands/history.js +80 -0
  9. package/bin/cli/commands/info.js +139 -0
  10. package/bin/cli/commands/install-hooks.js +128 -0
  11. package/bin/cli/commands/materialize.js +99 -0
  12. package/bin/cli/commands/patch.js +142 -0
  13. package/bin/cli/commands/path.js +88 -0
  14. package/bin/cli/commands/query.js +235 -0
  15. package/bin/cli/commands/registry.js +32 -0
  16. package/bin/cli/commands/seek.js +598 -0
  17. package/bin/cli/commands/tree.js +230 -0
  18. package/bin/cli/commands/trust.js +154 -0
  19. package/bin/cli/commands/verify-audit.js +114 -0
  20. package/bin/cli/commands/view.js +46 -0
  21. package/bin/cli/infrastructure.js +350 -0
  22. package/bin/cli/schemas.js +177 -0
  23. package/bin/cli/shared.js +244 -0
  24. package/bin/cli/types.js +96 -0
  25. package/bin/presenters/index.js +41 -9
  26. package/bin/presenters/json.js +14 -12
  27. package/bin/presenters/text.js +286 -28
  28. package/bin/warp-graph.js +5 -2346
  29. package/index.d.ts +111 -21
  30. package/index.js +2 -0
  31. package/package.json +10 -8
  32. package/src/domain/WarpGraph.js +109 -3252
  33. package/src/domain/crdt/ORSet.js +8 -8
  34. package/src/domain/errors/EmptyMessageError.js +2 -2
  35. package/src/domain/errors/ForkError.js +1 -1
  36. package/src/domain/errors/IndexError.js +1 -1
  37. package/src/domain/errors/OperationAbortedError.js +1 -1
  38. package/src/domain/errors/QueryError.js +3 -3
  39. package/src/domain/errors/SchemaUnsupportedError.js +1 -1
  40. package/src/domain/errors/ShardCorruptionError.js +2 -2
  41. package/src/domain/errors/ShardLoadError.js +2 -2
  42. package/src/domain/errors/ShardValidationError.js +4 -4
  43. package/src/domain/errors/StorageError.js +2 -2
  44. package/src/domain/errors/SyncError.js +1 -1
  45. package/src/domain/errors/TraversalError.js +1 -1
  46. package/src/domain/errors/TrustError.js +29 -0
  47. package/src/domain/errors/WarpError.js +2 -2
  48. package/src/domain/errors/WormholeError.js +1 -1
  49. package/src/domain/errors/index.js +1 -0
  50. package/src/domain/services/AuditMessageCodec.js +137 -0
  51. package/src/domain/services/AuditReceiptService.js +471 -0
  52. package/src/domain/services/AuditVerifierService.js +707 -0
  53. package/src/domain/services/BitmapIndexBuilder.js +3 -3
  54. package/src/domain/services/BitmapIndexReader.js +28 -19
  55. package/src/domain/services/BoundaryTransitionRecord.js +18 -17
  56. package/src/domain/services/CheckpointSerializerV5.js +17 -16
  57. package/src/domain/services/CheckpointService.js +2 -2
  58. package/src/domain/services/CommitDagTraversalService.js +13 -13
  59. package/src/domain/services/DagPathFinding.js +7 -7
  60. package/src/domain/services/DagTopology.js +1 -1
  61. package/src/domain/services/DagTraversal.js +1 -1
  62. package/src/domain/services/HealthCheckService.js +1 -1
  63. package/src/domain/services/HookInstaller.js +1 -1
  64. package/src/domain/services/HttpSyncServer.js +120 -55
  65. package/src/domain/services/IndexRebuildService.js +7 -7
  66. package/src/domain/services/IndexStalenessChecker.js +4 -3
  67. package/src/domain/services/JoinReducer.js +11 -11
  68. package/src/domain/services/LogicalTraversal.js +1 -1
  69. package/src/domain/services/MessageCodecInternal.js +4 -1
  70. package/src/domain/services/MessageSchemaDetector.js +2 -2
  71. package/src/domain/services/MigrationService.js +1 -1
  72. package/src/domain/services/ObserverView.js +8 -8
  73. package/src/domain/services/PatchBuilderV2.js +42 -26
  74. package/src/domain/services/ProvenanceIndex.js +1 -1
  75. package/src/domain/services/ProvenancePayload.js +1 -1
  76. package/src/domain/services/QueryBuilder.js +3 -3
  77. package/src/domain/services/StateDiff.js +14 -11
  78. package/src/domain/services/StateSerializerV5.js +2 -2
  79. package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
  80. package/src/domain/services/SyncAuthService.js +71 -4
  81. package/src/domain/services/SyncProtocol.js +25 -11
  82. package/src/domain/services/TemporalQuery.js +9 -6
  83. package/src/domain/services/TranslationCost.js +7 -5
  84. package/src/domain/services/WarpMessageCodec.js +4 -1
  85. package/src/domain/services/WormholeService.js +16 -7
  86. package/src/domain/trust/TrustCanonical.js +42 -0
  87. package/src/domain/trust/TrustCrypto.js +111 -0
  88. package/src/domain/trust/TrustEvaluator.js +195 -0
  89. package/src/domain/trust/TrustRecordService.js +281 -0
  90. package/src/domain/trust/TrustStateBuilder.js +222 -0
  91. package/src/domain/trust/canonical.js +68 -0
  92. package/src/domain/trust/reasonCodes.js +64 -0
  93. package/src/domain/trust/schemas.js +160 -0
  94. package/src/domain/trust/verdict.js +42 -0
  95. package/src/domain/types/TickReceipt.js +1 -1
  96. package/src/domain/types/WarpErrors.js +45 -0
  97. package/src/domain/types/WarpOptions.js +29 -0
  98. package/src/domain/types/WarpPersistence.js +41 -0
  99. package/src/domain/types/WarpTypes.js +2 -2
  100. package/src/domain/types/WarpTypesV2.js +2 -2
  101. package/src/domain/types/git-cas.d.ts +20 -0
  102. package/src/domain/utils/MinHeap.js +6 -5
  103. package/src/domain/utils/RefLayout.js +59 -0
  104. package/src/domain/utils/canonicalStringify.js +5 -4
  105. package/src/domain/utils/roaring.js +31 -5
  106. package/src/domain/warp/PatchSession.js +26 -17
  107. package/src/domain/warp/Writer.js +18 -3
  108. package/src/domain/warp/_internal.js +26 -0
  109. package/src/domain/warp/_wire.js +58 -0
  110. package/src/domain/warp/_wiredMethods.d.ts +254 -0
  111. package/src/domain/warp/checkpoint.methods.js +401 -0
  112. package/src/domain/warp/fork.methods.js +323 -0
  113. package/src/domain/warp/materialize.methods.js +238 -0
  114. package/src/domain/warp/materializeAdvanced.methods.js +350 -0
  115. package/src/domain/warp/patch.methods.js +554 -0
  116. package/src/domain/warp/provenance.methods.js +286 -0
  117. package/src/domain/warp/query.methods.js +280 -0
  118. package/src/domain/warp/subscribe.methods.js +272 -0
  119. package/src/domain/warp/sync.methods.js +554 -0
  120. package/src/globals.d.ts +64 -0
  121. package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
  122. package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
  123. package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
  124. package/src/infrastructure/adapters/GitGraphAdapter.js +79 -11
  125. package/src/infrastructure/adapters/InMemoryGraphAdapter.js +36 -0
  126. package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
  127. package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
  128. package/src/ports/CommitPort.js +10 -0
  129. package/src/ports/RefPort.js +17 -0
  130. package/src/visualization/layouts/converters.js +2 -2
  131. package/src/visualization/layouts/elkAdapter.js +1 -1
  132. package/src/visualization/layouts/elkLayout.js +10 -7
  133. package/src/visualization/layouts/index.js +1 -1
  134. package/src/visualization/renderers/ascii/seek.js +16 -6
  135. package/src/visualization/renderers/svg/index.js +1 -1
  136. package/src/hooks/post-merge.sh +0 -60
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Type definitions for the `doctor` command.
3
+ *
4
+ * @module cli/commands/doctor/types
5
+ */
6
+
7
+ // ── JSON-safe recursive value type ──────────────────────────────────────────
8
+
9
+ /** @typedef {null | boolean | number | string | Array<unknown> | {[k:string]: unknown}} JsonValue */
10
+
11
+ /** @typedef {{[k:string]: JsonValue}} FindingEvidence */
12
+
13
+ // ── Finding ─────────────────────────────────────────────────────────────────
14
+
15
+ /**
16
+ * @typedef {Object} DoctorFinding
17
+ * @property {string} id - Check identifier (e.g. 'repo-accessible')
18
+ * @property {'ok'|'warn'|'fail'} status
19
+ * @property {string} code - Machine-readable code from CODES registry
20
+ * @property {'data_integrity'|'security'|'operability'|'hygiene'} impact
21
+ * @property {string} message - Human-readable summary
22
+ * @property {string} [fix] - Suggested remediation command or instruction
23
+ * @property {string} [helpUrl] - Stable documentation anchor
24
+ * @property {FindingEvidence} [evidence] - JSON-safe supporting data
25
+ * @property {number} [durationMs] - Time spent on this check
26
+ */
27
+
28
+ // ── Policy ──────────────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * @typedef {Object} DoctorPolicy
32
+ * @property {boolean} strict
33
+ * @property {number} clockSkewMs
34
+ * @property {number} checkpointMaxAgeHours
35
+ * @property {number} globalDeadlineMs
36
+ * @property {{[checkId:string]: number}} checkTimeouts
37
+ */
38
+
39
+ // ── Payload ─────────────────────────────────────────────────────────────────
40
+
41
+ /**
42
+ * @typedef {Object} DoctorPayload
43
+ * @property {1} doctorVersion
44
+ * @property {string} repo
45
+ * @property {string} graph
46
+ * @property {string} checkedAt - ISO 8601 timestamp
47
+ * @property {'ok'|'degraded'|'failed'} health
48
+ * @property {DoctorPolicy} policy
49
+ * @property {DoctorSummary} summary
50
+ * @property {DoctorFinding[]} findings
51
+ * @property {number} durationMs
52
+ */
53
+
54
+ /**
55
+ * @typedef {Object} DoctorSummary
56
+ * @property {number} checksRun
57
+ * @property {number} findingsTotal
58
+ * @property {number} ok
59
+ * @property {number} warn
60
+ * @property {number} fail
61
+ * @property {string[]} priorityActions
62
+ */
63
+
64
+ // ── Context passed to each check ────────────────────────────────────────────
65
+
66
+ /**
67
+ * @typedef {Object} DoctorContext
68
+ * @property {import('../../types.js').Persistence} persistence
69
+ * @property {string} graphName
70
+ * @property {Array<{writerId: string, sha: string|null, ref: string}>} writerHeads
71
+ * @property {DoctorPolicy} policy
72
+ * @property {string} repoPath
73
+ */
74
+
75
+ /**
76
+ * @callback DoctorCheck
77
+ * @param {DoctorContext} ctx
78
+ * @returns {Promise<DoctorFinding|DoctorFinding[]|null>}
79
+ */
80
+
81
+ // ── Exit codes ──────────────────────────────────────────────────────────────
82
+
83
+ export const DOCTOR_EXIT_CODES = {
84
+ OK: 0,
85
+ FINDINGS: 3,
86
+ STRICT_FINDINGS: 4,
87
+ };
88
+
89
+ export {};
@@ -0,0 +1,80 @@
1
+ import { summarizeOps } from '../../../src/visualization/renderers/ascii/history.js';
2
+ import { EXIT_CODES, notFoundError, parseCommandArgs } from '../infrastructure.js';
3
+ import { historySchema } from '../schemas.js';
4
+ import { openGraph, applyCursorCeiling, emitCursorWarning } from '../shared.js';
5
+
6
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
7
+ /** @typedef {{patch: {schema?: number, lamport: number, ops?: Array<{type: string, node?: string, from?: string, to?: string}>}, sha: string}} PatchEntry */
8
+
9
+ const HISTORY_OPTIONS = {
10
+ node: { type: 'string' },
11
+ };
12
+
13
+ /** @param {string[]} args */
14
+ function parseHistoryArgs(args) {
15
+ const { values } = parseCommandArgs(args, HISTORY_OPTIONS, historySchema);
16
+ return { node: values.node ?? null };
17
+ }
18
+
19
+ /**
20
+ * @param {{ops?: Array<{node?: string, from?: string, to?: string}>}} patch
21
+ * @param {string} nodeId
22
+ */
23
+ function patchTouchesNode(patch, nodeId) {
24
+ const ops = Array.isArray(patch?.ops) ? patch.ops : [];
25
+ for (const op of ops) {
26
+ if (op.node === nodeId) {
27
+ return true;
28
+ }
29
+ if (op.from === nodeId || op.to === nodeId) {
30
+ return true;
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+
36
+ /**
37
+ * Handles the `history` command: shows patch history for a writer.
38
+ * @param {{options: CliOptions, args: string[]}} params
39
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
40
+ */
41
+ export default async function handleHistory({ options, args }) {
42
+ const historyOptions = parseHistoryArgs(args);
43
+ const { graph, graphName, persistence } = await openGraph(options);
44
+ const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
45
+ emitCursorWarning(cursorInfo, null);
46
+
47
+ const writerId = options.writer;
48
+ let patches = await graph.getWriterPatches(writerId);
49
+ if (cursorInfo.active) {
50
+ patches = patches.filter((/** @type {PatchEntry} */ { patch }) => patch.lamport <= /** @type {number} */ (cursorInfo.tick));
51
+ }
52
+ if (patches.length === 0) {
53
+ const knownWriters = await graph.discoverWriters();
54
+ if (knownWriters.length > 0) {
55
+ throw notFoundError(
56
+ `No patches found for writer: ${writerId}\nKnown writers: ${knownWriters.join(', ')}\nUse: warp-graph history --writer <id>`
57
+ );
58
+ }
59
+ throw notFoundError(`No patches found for writer: ${writerId}`);
60
+ }
61
+
62
+ const entries = patches
63
+ .filter((/** @type {PatchEntry} */ { patch }) => !historyOptions.node || patchTouchesNode(patch, historyOptions.node))
64
+ .map((/** @type {PatchEntry} */ { patch, sha }) => ({
65
+ sha,
66
+ schema: patch.schema,
67
+ lamport: patch.lamport,
68
+ opCount: Array.isArray(patch.ops) ? patch.ops.length : 0,
69
+ opSummary: Array.isArray(patch.ops) ? summarizeOps(patch.ops) : undefined,
70
+ }));
71
+
72
+ const payload = {
73
+ graph: graphName,
74
+ writer: writerId,
75
+ nodeFilter: historyOptions.node,
76
+ entries,
77
+ };
78
+
79
+ return { payload, exitCode: EXIT_CODES.OK };
80
+ }
@@ -0,0 +1,139 @@
1
+ import WebCryptoAdapter from '../../../src/infrastructure/adapters/WebCryptoAdapter.js';
2
+ import WarpGraph from '../../../src/domain/WarpGraph.js';
3
+ import {
4
+ buildCheckpointRef,
5
+ buildCoverageRef,
6
+ buildWritersPrefix,
7
+ parseWriterIdFromRef,
8
+ } from '../../../src/domain/utils/RefLayout.js';
9
+ import { notFoundError } from '../infrastructure.js';
10
+ import { createPersistence, listGraphNames, readActiveCursor, readCheckpointDate } from '../shared.js';
11
+
12
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
13
+ /** @typedef {import('../types.js').Persistence} Persistence */
14
+ /** @typedef {import('../types.js').GraphInfoResult} GraphInfoResult */
15
+
16
+ /**
17
+ * Collects metadata about a single graph (writer count, refs, patches, checkpoint).
18
+ * @param {Persistence} persistence
19
+ * @param {string} graphName
20
+ * @param {Object} [options]
21
+ * @param {boolean} [options.includeWriterIds=false]
22
+ * @param {boolean} [options.includeRefs=false]
23
+ * @param {boolean} [options.includeWriterPatches=false]
24
+ * @param {boolean} [options.includeCheckpointDate=false]
25
+ * @returns {Promise<GraphInfoResult>}
26
+ */
27
+ async function getGraphInfo(persistence, graphName, {
28
+ includeWriterIds = false,
29
+ includeRefs = false,
30
+ includeWriterPatches = false,
31
+ includeCheckpointDate = false,
32
+ } = {}) {
33
+ const writersPrefix = buildWritersPrefix(graphName);
34
+ const writerRefs = typeof persistence.listRefs === 'function'
35
+ ? await persistence.listRefs(writersPrefix)
36
+ : [];
37
+ const writerIds = /** @type {string[]} */ (writerRefs
38
+ .map((ref) => parseWriterIdFromRef(ref))
39
+ .filter(Boolean)
40
+ .sort());
41
+
42
+ /** @type {GraphInfoResult} */
43
+ const info = {
44
+ name: graphName,
45
+ writers: {
46
+ count: writerIds.length,
47
+ },
48
+ };
49
+
50
+ if (includeWriterIds) {
51
+ info.writers.ids = writerIds;
52
+ }
53
+
54
+ if (includeRefs || includeCheckpointDate) {
55
+ const checkpointRef = buildCheckpointRef(graphName);
56
+ const checkpointSha = await persistence.readRef(checkpointRef);
57
+
58
+ /** @type {{ref: string, sha: string|null, date?: string|null}} */
59
+ const checkpoint = { ref: checkpointRef, sha: checkpointSha || null };
60
+
61
+ if (includeCheckpointDate && checkpointSha) {
62
+ const checkpointDate = await readCheckpointDate(persistence, checkpointSha);
63
+ checkpoint.date = checkpointDate;
64
+ }
65
+
66
+ info.checkpoint = checkpoint;
67
+
68
+ if (includeRefs) {
69
+ const coverageRef = buildCoverageRef(graphName);
70
+ const coverageSha = await persistence.readRef(coverageRef);
71
+ info.coverage = { ref: coverageRef, sha: coverageSha || null };
72
+ }
73
+ }
74
+
75
+ if (includeWriterPatches && writerIds.length > 0) {
76
+ const graph = await WarpGraph.open({
77
+ persistence,
78
+ graphName,
79
+ writerId: 'cli',
80
+ crypto: new WebCryptoAdapter(),
81
+ });
82
+ /** @type {Record<string, number>} */
83
+ const writerPatches = {};
84
+ for (const writerId of writerIds) {
85
+ const patches = await graph.getWriterPatches(writerId);
86
+ writerPatches[/** @type {string} */ (writerId)] = patches.length;
87
+ }
88
+ info.writerPatches = writerPatches;
89
+ }
90
+
91
+ return info;
92
+ }
93
+
94
+ /**
95
+ * Handles the `info` command: summarizes graphs in the repository.
96
+ * @param {{options: CliOptions}} params
97
+ * @returns {Promise<{repo: string, graphs: GraphInfoResult[]}>}
98
+ */
99
+ export default async function handleInfo({ options }) {
100
+ const { persistence } = await createPersistence(options.repo);
101
+ const graphNames = await listGraphNames(persistence);
102
+
103
+ if (options.graph && !graphNames.includes(options.graph)) {
104
+ throw notFoundError(`Graph not found: ${options.graph}`);
105
+ }
106
+
107
+ const detailGraphs = new Set();
108
+ if (options.graph) {
109
+ detailGraphs.add(options.graph);
110
+ } else if (graphNames.length === 1) {
111
+ detailGraphs.add(graphNames[0]);
112
+ }
113
+
114
+ // In view mode, include extra data for visualization
115
+ const isViewMode = Boolean(options.view);
116
+
117
+ const graphs = [];
118
+ for (const name of graphNames) {
119
+ const includeDetails = detailGraphs.has(name);
120
+ const info = await getGraphInfo(persistence, name, {
121
+ includeWriterIds: includeDetails || isViewMode,
122
+ includeRefs: includeDetails || isViewMode,
123
+ includeWriterPatches: isViewMode,
124
+ includeCheckpointDate: isViewMode,
125
+ });
126
+ const activeCursor = await readActiveCursor(persistence, name);
127
+ if (activeCursor) {
128
+ info.cursor = { active: true, tick: activeCursor.tick, mode: activeCursor.mode };
129
+ } else {
130
+ info.cursor = { active: false };
131
+ }
132
+ graphs.push(info);
133
+ }
134
+
135
+ return {
136
+ repo: options.repo,
137
+ graphs,
138
+ };
139
+ }
@@ -0,0 +1,128 @@
1
+ import fs from 'node:fs';
2
+ import process from 'node:process';
3
+ import { classifyExistingHook } from '../../../src/domain/services/HookInstaller.js';
4
+ import { EXIT_CODES, usageError, parseCommandArgs } from '../infrastructure.js';
5
+ import { installHooksSchema } from '../schemas.js';
6
+ import { createHookInstaller, isInteractive, promptUser } from '../shared.js';
7
+
8
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
9
+
10
+ const INSTALL_HOOKS_OPTIONS = {
11
+ force: { type: 'boolean', default: false },
12
+ };
13
+
14
+ /** @param {string[]} args */
15
+ function parseInstallHooksArgs(args) {
16
+ const { values } = parseCommandArgs(args, INSTALL_HOOKS_OPTIONS, installHooksSchema);
17
+ return values;
18
+ }
19
+
20
+ /**
21
+ * @param {{kind: string, version?: string, appended?: boolean}} classification
22
+ * @param {{force: boolean}} hookOptions
23
+ */
24
+ async function resolveStrategy(classification, hookOptions) {
25
+ if (hookOptions.force) {
26
+ return 'replace';
27
+ }
28
+
29
+ if (classification.kind === 'none') {
30
+ return 'install';
31
+ }
32
+
33
+ if (classification.kind === 'ours') {
34
+ return await promptForOursStrategy(classification);
35
+ }
36
+
37
+ return await promptForForeignStrategy();
38
+ }
39
+
40
+ /** @param {{kind: string, version?: string, appended?: boolean}} classification */
41
+ async function promptForOursStrategy(classification) {
42
+ const installer = createHookInstaller();
43
+ if (classification.version === installer._version) {
44
+ return 'up-to-date';
45
+ }
46
+
47
+ if (!isInteractive()) {
48
+ throw usageError('Existing hook found. Use --force or run interactively.');
49
+ }
50
+
51
+ const answer = await promptUser(
52
+ `Upgrade hook from v${classification.version} to v${installer._version}? [Y/n] `,
53
+ );
54
+ if (answer === '' || answer.toLowerCase() === 'y') {
55
+ return 'upgrade';
56
+ }
57
+ return 'skip';
58
+ }
59
+
60
+ async function promptForForeignStrategy() {
61
+ if (!isInteractive()) {
62
+ throw usageError('Existing hook found. Use --force or run interactively.');
63
+ }
64
+
65
+ process.stderr.write('Existing post-merge hook found.\n');
66
+ process.stderr.write(' 1) Append (keep existing hook, add warp section)\n');
67
+ process.stderr.write(' 2) Replace (back up existing, install fresh)\n');
68
+ process.stderr.write(' 3) Skip\n');
69
+ const answer = await promptUser('Choose [1-3]: ');
70
+
71
+ if (answer === '1') {
72
+ return 'append';
73
+ }
74
+ if (answer === '2') {
75
+ return 'replace';
76
+ }
77
+ return 'skip';
78
+ }
79
+
80
+ /** @param {string} hookPath */
81
+ function readHookContent(hookPath) {
82
+ try {
83
+ return fs.readFileSync(hookPath, 'utf8');
84
+ } catch (err) {
85
+ if (err instanceof Error && /** @type {{code?: string}} */ (err).code === 'ENOENT') {
86
+ return null;
87
+ }
88
+ throw err;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Handles the `install-hooks` command.
94
+ * @param {{options: CliOptions, args: string[]}} params
95
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
96
+ */
97
+ export default async function handleInstallHooks({ options, args }) {
98
+ const hookOptions = parseInstallHooksArgs(args);
99
+ const installer = createHookInstaller();
100
+ const status = installer.getHookStatus(options.repo);
101
+ const content = readHookContent(status.hookPath);
102
+ const classification = classifyExistingHook(content);
103
+ const strategy = await resolveStrategy(classification, hookOptions);
104
+
105
+ if (strategy === 'up-to-date') {
106
+ return {
107
+ payload: {
108
+ action: 'up-to-date',
109
+ hookPath: status.hookPath,
110
+ version: installer._version,
111
+ },
112
+ exitCode: EXIT_CODES.OK,
113
+ };
114
+ }
115
+
116
+ if (strategy === 'skip') {
117
+ return {
118
+ payload: { action: 'skipped' },
119
+ exitCode: EXIT_CODES.OK,
120
+ };
121
+ }
122
+
123
+ const result = installer.install(options.repo, { strategy });
124
+ return {
125
+ payload: result,
126
+ exitCode: EXIT_CODES.OK,
127
+ };
128
+ }
@@ -0,0 +1,99 @@
1
+ import WebCryptoAdapter from '../../../src/infrastructure/adapters/WebCryptoAdapter.js';
2
+ import WarpGraph from '../../../src/domain/WarpGraph.js';
3
+ import { EXIT_CODES, notFoundError } from '../infrastructure.js';
4
+ import { createPersistence, listGraphNames, readActiveCursor, emitCursorWarning } from '../shared.js';
5
+
6
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
7
+ /** @typedef {import('../types.js').Persistence} Persistence */
8
+
9
+ /**
10
+ * Materializes a single graph, creates a checkpoint, and returns summary stats.
11
+ * @param {{persistence: Persistence, graphName: string, writerId: string, ceiling?: number}} params
12
+ * @returns {Promise<{graph: string, nodes: number, edges: number, properties: number, checkpoint: string|null, writers: Record<string, number>, patchCount: number}>}
13
+ */
14
+ async function materializeOneGraph({ persistence, graphName, writerId, ceiling }) {
15
+ const graph = await WarpGraph.open({ persistence, graphName, writerId, crypto: new WebCryptoAdapter() });
16
+ await graph.materialize(ceiling !== undefined ? { ceiling } : undefined);
17
+ const nodes = await graph.getNodes();
18
+ const edges = await graph.getEdges();
19
+ const checkpoint = ceiling !== undefined ? null : await graph.createCheckpoint();
20
+ const status = await graph.status();
21
+
22
+ // Build per-writer patch counts for the view renderer
23
+ /** @type {Record<string, number>} */
24
+ const writers = {};
25
+ let totalPatchCount = 0;
26
+ for (const wId of Object.keys(status.frontier)) {
27
+ const patches = await graph.getWriterPatches(wId);
28
+ writers[wId] = patches.length;
29
+ totalPatchCount += patches.length;
30
+ }
31
+
32
+ const properties = await graph.getPropertyCount();
33
+
34
+ return {
35
+ graph: graphName,
36
+ nodes: nodes.length,
37
+ edges: edges.length,
38
+ properties,
39
+ checkpoint,
40
+ writers,
41
+ patchCount: totalPatchCount,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Handles the `materialize` command: materializes and checkpoints all graphs.
47
+ * @param {{options: CliOptions}} params
48
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
49
+ */
50
+ export default async function handleMaterialize({ options }) {
51
+ const { persistence } = await createPersistence(options.repo);
52
+ const graphNames = await listGraphNames(persistence);
53
+
54
+ if (graphNames.length === 0) {
55
+ return {
56
+ payload: { graphs: [] },
57
+ exitCode: EXIT_CODES.OK,
58
+ };
59
+ }
60
+
61
+ const targets = options.graph
62
+ ? [options.graph]
63
+ : graphNames;
64
+
65
+ if (options.graph && !graphNames.includes(options.graph)) {
66
+ throw notFoundError(`Graph not found: ${options.graph}`);
67
+ }
68
+
69
+ const results = [];
70
+ let cursorWarningEmitted = false;
71
+ for (const name of targets) {
72
+ try {
73
+ const cursor = await readActiveCursor(persistence, name);
74
+ const ceiling = cursor ? cursor.tick : undefined;
75
+ if (cursor && !cursorWarningEmitted) {
76
+ emitCursorWarning({ active: true, tick: cursor.tick, maxTick: null }, null);
77
+ cursorWarningEmitted = true;
78
+ }
79
+ const result = await materializeOneGraph({
80
+ persistence,
81
+ graphName: name,
82
+ writerId: options.writer,
83
+ ceiling,
84
+ });
85
+ results.push(result);
86
+ } catch (error) {
87
+ results.push({
88
+ graph: name,
89
+ error: error instanceof Error ? error.message : String(error),
90
+ });
91
+ }
92
+ }
93
+
94
+ const allFailed = results.every((r) => 'error' in r);
95
+ return {
96
+ payload: { graphs: results },
97
+ exitCode: allFailed ? EXIT_CODES.INTERNAL : EXIT_CODES.OK,
98
+ };
99
+ }
@@ -0,0 +1,142 @@
1
+ import { EXIT_CODES, usageError, notFoundError, parseCommandArgs } from '../infrastructure.js';
2
+ import { openGraph } from '../shared.js';
3
+ import { z } from 'zod';
4
+
5
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
6
+
7
+ const PATCH_OPTIONS = {
8
+ writer: { type: 'string' },
9
+ limit: { type: 'string' },
10
+ };
11
+
12
+ const patchSchema = z.object({
13
+ writer: z.string().optional(),
14
+ limit: z.coerce.number().int().positive().optional(),
15
+ }).strict();
16
+
17
+ /**
18
+ * Collects all patches across all writers (or a single writer).
19
+ * @param {import('../types.js').WarpGraphInstance} graph
20
+ * @param {string|null} writerFilter
21
+ * @returns {Promise<Array<{sha: string, writer: string, patch: {schema?: number, lamport: number, ops?: Array<Record<string, unknown>>, context?: Record<string, unknown>}}>>}
22
+ */
23
+ async function collectPatches(graph, writerFilter) {
24
+ const writers = writerFilter ? [writerFilter] : await graph.discoverWriters();
25
+ const all = [];
26
+ for (const writerId of writers) {
27
+ const patches = await graph.getWriterPatches(writerId);
28
+ for (const { patch, sha } of patches) {
29
+ all.push({ sha, writer: writerId, patch });
30
+ }
31
+ }
32
+ // Sort by lamport ascending
33
+ all.sort((a, b) => (a.patch.lamport ?? 0) - (b.patch.lamport ?? 0));
34
+ return all;
35
+ }
36
+
37
+ /**
38
+ * Handles the `patch` command: show or list decoded patches.
39
+ * @param {{options: CliOptions, args: string[]}} params
40
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
41
+ */
42
+ export default async function handlePatch({ options, args }) {
43
+ // First positional is the subaction: show or list
44
+ const subaction = args[0];
45
+ const rest = args.slice(1);
46
+
47
+ if (subaction === 'show') {
48
+ return await handlePatchShow({ options, args: rest });
49
+ }
50
+ if (subaction === 'list') {
51
+ return await handlePatchList({ options, args: rest });
52
+ }
53
+ if (!subaction) {
54
+ throw usageError('Usage: warp-graph patch <show|list> [options]\n show <sha> Decode and display a single patch\n list List all patches');
55
+ }
56
+ throw usageError(`Unknown patch subaction: ${subaction}. Use: show, list`);
57
+ }
58
+
59
+ /**
60
+ * @param {{options: CliOptions, args: string[]}} params
61
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
62
+ */
63
+ async function handlePatchShow({ options, args }) {
64
+ if (!args[0]) {
65
+ throw usageError('Usage: warp-graph patch show <sha>');
66
+ }
67
+ const targetSha = args[0];
68
+ const { graph, graphName } = await openGraph(options);
69
+ const allPatches = await collectPatches(graph, null);
70
+
71
+ const match = allPatches.find((p) => p.sha === targetSha || p.sha.startsWith(targetSha));
72
+ if (!match) {
73
+ throw notFoundError(`Patch not found: ${targetSha}`);
74
+ }
75
+
76
+ const payload = {
77
+ graph: graphName,
78
+ sha: match.sha,
79
+ writer: match.writer,
80
+ lamport: match.patch.lamport,
81
+ schema: match.patch.schema,
82
+ ops: match.patch.ops,
83
+ context: match.patch.context,
84
+ };
85
+
86
+ return { payload, exitCode: EXIT_CODES.OK };
87
+ }
88
+
89
+ /**
90
+ * @param {{options: CliOptions, args: string[]}} params
91
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
92
+ */
93
+ async function handlePatchList({ options, args }) {
94
+ const { values } = parseCommandArgs(args, PATCH_OPTIONS, patchSchema);
95
+ const { graph, graphName } = await openGraph(options);
96
+ const writerFilter = values.writer || null;
97
+ const allPatches = await collectPatches(graph, writerFilter);
98
+
99
+ const limit = values.limit ?? allPatches.length;
100
+ const entries = allPatches.slice(0, limit).map((p) => ({
101
+ sha: p.sha.slice(0, 7),
102
+ fullSha: p.sha,
103
+ writer: p.writer,
104
+ lamport: p.patch.lamport,
105
+ opCount: Array.isArray(p.patch.ops) ? p.patch.ops.length : 0,
106
+ nodeIds: extractNodeIds(p.patch.ops || []),
107
+ }));
108
+
109
+ const payload = {
110
+ graph: graphName,
111
+ total: allPatches.length,
112
+ showing: entries.length,
113
+ writerFilter,
114
+ entries,
115
+ };
116
+
117
+ return { payload, exitCode: EXIT_CODES.OK };
118
+ }
119
+
120
+ /**
121
+ * Extracts unique node IDs touched by a patch's operations.
122
+ * @param {Array<Record<string, unknown>>} ops
123
+ * @returns {string[]}
124
+ */
125
+ function extractNodeIds(ops) {
126
+ if (!Array.isArray(ops)) {
127
+ return [];
128
+ }
129
+ const ids = new Set();
130
+ for (const op of ops) {
131
+ if (op.node) {
132
+ ids.add(op.node);
133
+ }
134
+ if (op.from) {
135
+ ids.add(op.from);
136
+ }
137
+ if (op.to) {
138
+ ids.add(op.to);
139
+ }
140
+ }
141
+ return [...ids].sort();
142
+ }