@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,230 @@
1
+ import { EXIT_CODES, usageError, parseCommandArgs } from '../infrastructure.js';
2
+ import { openGraph, applyCursorCeiling, emitCursorWarning } from '../shared.js';
3
+ import { z } from 'zod';
4
+
5
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
6
+
7
+ const TREE_OPTIONS = {
8
+ edge: { type: 'string' },
9
+ prop: { type: 'string', multiple: true },
10
+ 'max-depth': { type: 'string' },
11
+ };
12
+
13
+ const treeSchema = z.object({
14
+ edge: z.string().optional(),
15
+ prop: z.union([z.string(), z.array(z.string())]).optional(),
16
+ 'max-depth': z.coerce.number().int().nonnegative().optional(),
17
+ }).strict().transform((val) => ({
18
+ edgeLabel: val.edge ?? null,
19
+ props: Array.isArray(val.prop) ? val.prop : val.prop ? [val.prop] : [],
20
+ maxDepth: val['max-depth'],
21
+ }));
22
+
23
+ /**
24
+ * Builds a parent-to-children adjacency map from edges.
25
+ * @param {Array<{from: string, to: string, label?: string}>} edges
26
+ * @param {string|null} labelFilter
27
+ * @returns {Map<string, Array<{id: string, label: string}>>}
28
+ */
29
+ function buildChildMap(edges, labelFilter) {
30
+ /** @type {Map<string, Array<{id: string, label: string}>>} */
31
+ const children = new Map();
32
+ /** @type {Set<string>} */
33
+ const hasParent = new Set();
34
+
35
+ for (const edge of edges) {
36
+ if (labelFilter && edge.label !== labelFilter) {
37
+ continue;
38
+ }
39
+ if (!children.has(edge.from)) {
40
+ children.set(edge.from, []);
41
+ }
42
+ const fromChildren = children.get(edge.from);
43
+ if (fromChildren) {
44
+ fromChildren.push({ id: edge.to, label: edge.label || '' });
45
+ }
46
+ hasParent.add(edge.to);
47
+ }
48
+
49
+ return children;
50
+ }
51
+
52
+ /**
53
+ * Finds root nodes (nodes with outgoing edges but no incoming edges in the filtered set).
54
+ * @param {string[]} nodeIds
55
+ * @param {Array<{from: string, to: string, label?: string}>} edges
56
+ * @param {string|null} labelFilter
57
+ * @returns {string[]}
58
+ */
59
+ function findRoots(nodeIds, edges, labelFilter) {
60
+ const hasParent = new Set();
61
+ const hasChild = new Set();
62
+
63
+ for (const edge of edges) {
64
+ if (labelFilter && edge.label !== labelFilter) {
65
+ continue;
66
+ }
67
+ hasParent.add(edge.to);
68
+ hasChild.add(edge.from);
69
+ }
70
+
71
+ // Roots: nodes that have children but no parents in the filtered edge set
72
+ const roots = nodeIds.filter((id) => !hasParent.has(id) && hasChild.has(id));
73
+ if (roots.length > 0) {
74
+ return roots.sort();
75
+ }
76
+
77
+ // Fallback: nodes with no incoming edges at all
78
+ return nodeIds.filter((id) => !hasParent.has(id)).sort();
79
+ }
80
+
81
+ /**
82
+ * Formats annotation string for a node based on requested props.
83
+ * @param {Record<string, unknown>} nodeProps
84
+ * @param {string[]} propKeys
85
+ * @returns {string}
86
+ */
87
+ function formatAnnotation(nodeProps, propKeys) {
88
+ if (propKeys.length === 0 || !nodeProps) {
89
+ return '';
90
+ }
91
+ const parts = [];
92
+ for (const key of propKeys) {
93
+ if (Object.prototype.hasOwnProperty.call(nodeProps, key)) {
94
+ parts.push(`${key}: ${nodeProps[key]}`);
95
+ }
96
+ }
97
+ return parts.length > 0 ? ` [${parts.join(', ')}]` : '';
98
+ }
99
+
100
+ /**
101
+ * Renders a tree structure as lines with box-drawing characters.
102
+ * @param {object} params
103
+ * @param {string} params.nodeId
104
+ * @param {Map<string, Array<{id: string, label: string}>>} params.childMap
105
+ * @param {Map<string, Record<string, unknown>>} params.propsMap
106
+ * @param {string[]} params.propKeys
107
+ * @param {string} params.prefix
108
+ * @param {boolean} params.isLast
109
+ * @param {Set<string>} params.visited
110
+ * @param {number} params.depth
111
+ * @param {number|undefined} params.maxDepth
112
+ * @param {string[]} params.lines
113
+ */
114
+ function renderTreeNode({ nodeId, childMap, propsMap, propKeys, prefix, isLast, visited, depth, maxDepth, lines }) {
115
+ const connector = depth === 0 ? '' : (isLast ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ');
116
+ const annotation = formatAnnotation(propsMap.get(nodeId) || {}, propKeys);
117
+ lines.push(`${prefix}${connector}${nodeId}${annotation}`);
118
+
119
+ if (visited.has(nodeId)) {
120
+ lines.push(`${prefix}${isLast ? ' ' : '\u2502 '} (cycle)`);
121
+ return;
122
+ }
123
+ visited.add(nodeId);
124
+
125
+ if (maxDepth !== undefined && depth >= maxDepth) {
126
+ const kids = childMap.get(nodeId);
127
+ if (kids && kids.length > 0) {
128
+ lines.push(`${prefix}${isLast ? ' ' : '\u2502 '} ... (${kids.length} children)`);
129
+ }
130
+ return;
131
+ }
132
+
133
+ const kids = childMap.get(nodeId) || [];
134
+ const childPrefix = depth === 0 ? '' : `${prefix}${isLast ? ' ' : '\u2502 '}`;
135
+ for (let i = 0; i < kids.length; i++) {
136
+ renderTreeNode({
137
+ nodeId: kids[i].id,
138
+ childMap,
139
+ propsMap,
140
+ propKeys,
141
+ prefix: childPrefix,
142
+ isLast: i === kids.length - 1,
143
+ visited,
144
+ depth: depth + 1,
145
+ maxDepth,
146
+ lines,
147
+ });
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Handles the `tree` command: ASCII tree output from graph edges.
153
+ * @param {{options: CliOptions, args: string[]}} params
154
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
155
+ */
156
+ export default async function handleTree({ options, args }) {
157
+ const { values, positionals } = parseCommandArgs(
158
+ args, TREE_OPTIONS, treeSchema, { allowPositionals: true },
159
+ );
160
+ const { graph, graphName, persistence } = await openGraph(options);
161
+ const cursorInfo = await applyCursorCeiling(graph, persistence, graphName);
162
+ emitCursorWarning(cursorInfo, null);
163
+
164
+ const queryResult = await graph.query().run();
165
+ const edges = await graph.getEdges();
166
+ const rootArg = positionals[0] || null;
167
+
168
+ const nodeIds = queryResult.nodes.map((/** @type {{id: string}} */ n) => n.id);
169
+ const propsMap = new Map(queryResult.nodes.map((/** @type {{id: string, props?: Record<string, unknown>}} */ n) => [n.id, n.props || {}]));
170
+ const childMap = buildChildMap(edges, values.edgeLabel);
171
+
172
+ const roots = rootArg ? [rootArg] : findRoots(nodeIds, edges, values.edgeLabel);
173
+
174
+ if (rootArg && !nodeIds.includes(rootArg)) {
175
+ throw usageError(`Node not found: ${rootArg}`);
176
+ }
177
+
178
+ /** @type {string[]} */
179
+ const lines = [];
180
+ for (const root of roots) {
181
+ renderTreeNode({
182
+ nodeId: root,
183
+ childMap,
184
+ propsMap,
185
+ propKeys: values.props,
186
+ prefix: '',
187
+ isLast: true,
188
+ visited: new Set(),
189
+ depth: 0,
190
+ maxDepth: values.maxDepth,
191
+ lines,
192
+ });
193
+ }
194
+
195
+ // Collect orphans (nodes not reachable from any root)
196
+ const reachable = new Set();
197
+ collectReachable(roots, childMap, reachable);
198
+ const orphans = nodeIds.filter((/** @type {string} */ id) => !reachable.has(id));
199
+
200
+ const payload = {
201
+ graph: graphName,
202
+ roots,
203
+ tree: lines.join('\n'),
204
+ orphanCount: orphans.length,
205
+ orphans: orphans.length > 0 ? orphans : undefined,
206
+ };
207
+
208
+ return { payload, exitCode: EXIT_CODES.OK };
209
+ }
210
+
211
+ /**
212
+ * Collects all reachable node IDs via DFS.
213
+ * @param {string[]} roots
214
+ * @param {Map<string, Array<{id: string, label: string}>>} childMap
215
+ * @param {Set<string>} reachable
216
+ */
217
+ function collectReachable(roots, childMap, reachable) {
218
+ const stack = [...roots];
219
+ while (stack.length > 0) {
220
+ const id = /** @type {string} */ (stack.pop());
221
+ if (reachable.has(id)) {
222
+ continue;
223
+ }
224
+ reachable.add(id);
225
+ const kids = childMap.get(id) || [];
226
+ for (const kid of kids) {
227
+ stack.push(kid.id);
228
+ }
229
+ }
230
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * CLI handler for `git warp trust`.
3
+ *
4
+ * Evaluates writer trust status against signed evidence in the trust
5
+ * record chain. Returns a TrustAssessment payload.
6
+ *
7
+ * @module cli/commands/trust
8
+ */
9
+
10
+ import { EXIT_CODES, parseCommandArgs, getEnvVar } from '../infrastructure.js';
11
+ import { trustSchema } from '../schemas.js';
12
+ import { createPersistence, resolveGraphName } from '../shared.js';
13
+ import defaultCodec from '../../../src/domain/utils/defaultCodec.js';
14
+ import { TrustRecordService } from '../../../src/domain/trust/TrustRecordService.js';
15
+ import { buildState } from '../../../src/domain/trust/TrustStateBuilder.js';
16
+ import { evaluateWriters } from '../../../src/domain/trust/TrustEvaluator.js';
17
+
18
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
19
+
20
+ const TRUST_OPTIONS = {
21
+ mode: { type: 'string' },
22
+ 'trust-pin': { type: 'string' },
23
+ };
24
+
25
+ /**
26
+ * @param {string[]} args
27
+ * @returns {{ mode: string|null, trustPin: string|null }}
28
+ */
29
+ export function parseTrustArgs(args) {
30
+ const { values } = parseCommandArgs(args, TRUST_OPTIONS, trustSchema);
31
+ return values;
32
+ }
33
+
34
+ /**
35
+ * Resolves the trust pin from CLI flag → env → live ref.
36
+ * @param {string|null} cliPin
37
+ * @returns {{pin: string|null, source: string, sourceDetail: string|null, status: string}}
38
+ */
39
+ function resolveTrustPin(cliPin) {
40
+ if (cliPin) {
41
+ return { pin: cliPin, source: 'cli_pin', sourceDetail: cliPin, status: 'pinned' };
42
+ }
43
+ const envPin = getEnvVar('WARP_TRUST_PIN');
44
+ if (envPin) {
45
+ return { pin: envPin, source: 'env_pin', sourceDetail: envPin, status: 'pinned' };
46
+ }
47
+ return { pin: null, source: 'ref', sourceDetail: null, status: 'configured' };
48
+ }
49
+
50
+ /**
51
+ * Discovers all writer IDs from the writers prefix refs.
52
+ * @param {import('../types.js').Persistence} persistence
53
+ * @param {string} graphName
54
+ * @returns {Promise<string[]>}
55
+ */
56
+ async function discoverWriterIds(persistence, graphName) {
57
+ const prefix = `refs/warp/${graphName}/writers/`;
58
+ const refs = await persistence.listRefs(prefix);
59
+ return refs
60
+ .map((/** @type {string} */ ref) => ref.slice(prefix.length))
61
+ .filter((/** @type {string} */ id) => id.length > 0)
62
+ .sort();
63
+ }
64
+
65
+ /**
66
+ * Builds a not_configured assessment when no trust records exist.
67
+ * @param {string} graphName
68
+ * @returns {{payload: *, exitCode: number}}
69
+ */
70
+ function buildNotConfiguredResult(graphName) {
71
+ return {
72
+ payload: {
73
+ graph: graphName,
74
+ trustSchemaVersion: 1,
75
+ mode: 'signed_evidence_v1',
76
+ trustVerdict: 'not_configured',
77
+ trust: {
78
+ status: 'not_configured',
79
+ source: 'none',
80
+ sourceDetail: null,
81
+ evaluatedWriters: [],
82
+ untrustedWriters: [],
83
+ explanations: [],
84
+ evidenceSummary: {
85
+ recordsScanned: 0,
86
+ activeKeys: 0,
87
+ revokedKeys: 0,
88
+ activeBindings: 0,
89
+ revokedBindings: 0,
90
+ },
91
+ },
92
+ },
93
+ exitCode: EXIT_CODES.OK,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * @param {{options: CliOptions, args: string[]}} params
99
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
100
+ */
101
+ export default async function handleTrust({ options, args }) {
102
+ const { mode, trustPin } = parseTrustArgs(args);
103
+ const { persistence } = await createPersistence(options.repo);
104
+ const graphName = await resolveGraphName(persistence, options.graph);
105
+
106
+ const recordService = new TrustRecordService({
107
+ persistence: /** @type {import('../../../src/domain/types/WarpPersistence.js').CorePersistence} */ (/** @type {unknown} */ (persistence)),
108
+ codec: defaultCodec,
109
+ });
110
+
111
+ // Resolve pin (determines source + status)
112
+ const { pin, source, sourceDetail, status } = resolveTrustPin(trustPin);
113
+
114
+ // Read trust records
115
+ const records = await recordService.readRecords(graphName, pin ? { tip: pin } : {});
116
+
117
+ if (records.length === 0) {
118
+ return buildNotConfiguredResult(graphName);
119
+ }
120
+
121
+ // Build trust state
122
+ const trustState = buildState(records);
123
+
124
+ // Discover writers
125
+ const writerIds = await discoverWriterIds(persistence, graphName);
126
+
127
+ // Build policy
128
+ const policy = {
129
+ schemaVersion: 1,
130
+ mode: mode ?? 'warn',
131
+ writerPolicy: 'all_writers_must_be_trusted',
132
+ };
133
+
134
+ // Evaluate
135
+ const assessment = evaluateWriters(writerIds, trustState, policy);
136
+
137
+ // Override source/status from pin resolution (evaluator sets defaults)
138
+ const payload = {
139
+ graph: graphName,
140
+ ...assessment,
141
+ trust: {
142
+ ...assessment.trust,
143
+ status,
144
+ source,
145
+ sourceDetail,
146
+ },
147
+ };
148
+
149
+ const exitCode = assessment.trustVerdict === 'fail' && (mode === 'enforce')
150
+ ? EXIT_CODES.TRUST_FAIL
151
+ : EXIT_CODES.OK;
152
+
153
+ return { payload, exitCode };
154
+ }
@@ -0,0 +1,114 @@
1
+ import { AuditVerifierService } from '../../../src/domain/services/AuditVerifierService.js';
2
+ import defaultCodec from '../../../src/domain/utils/defaultCodec.js';
3
+ import { EXIT_CODES, parseCommandArgs, getEnvVar } from '../infrastructure.js';
4
+ import { verifyAuditSchema } from '../schemas.js';
5
+ import { createPersistence, resolveGraphName } from '../shared.js';
6
+
7
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
8
+
9
+ /**
10
+ * Detects trust configuration from environment and returns a structured warning.
11
+ * Domain services never read process.env — detection happens at the CLI boundary.
12
+ * @returns {{ code: string, message: string, sources: string[] } | null}
13
+ */
14
+ function detectTrustWarning() {
15
+ const sources = [];
16
+ if (getEnvVar('WARP_TRUSTED_ROOT')) {
17
+ sources.push('env');
18
+ }
19
+ if (sources.length === 0) {
20
+ return null;
21
+ }
22
+ return {
23
+ code: 'TRUST_CONFIG_PRESENT_UNENFORCED',
24
+ message: 'Trust root configured but signature verification is not implemented in v1',
25
+ sources,
26
+ };
27
+ }
28
+
29
+ const VERIFY_AUDIT_OPTIONS = {
30
+ since: { type: 'string' },
31
+ writer: { type: 'string' },
32
+ 'trust-mode': { type: 'string' },
33
+ 'trust-pin': { type: 'string' },
34
+ };
35
+
36
+ /** @param {string[]} args */
37
+ export function parseVerifyAuditArgs(args) {
38
+ const { values } = parseCommandArgs(args, VERIFY_AUDIT_OPTIONS, verifyAuditSchema);
39
+ return {
40
+ since: values.since,
41
+ writerFilter: values.writer,
42
+ trustMode: values['trust-mode'],
43
+ trustPin: values['trust-pin'],
44
+ };
45
+ }
46
+
47
+ /**
48
+ * @param {{options: CliOptions, args: string[]}} params
49
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
50
+ */
51
+ export default async function handleVerifyAudit({ options, args }) {
52
+ const { since, writerFilter, trustMode, trustPin } = parseVerifyAuditArgs(args);
53
+ const { persistence } = await createPersistence(options.repo);
54
+ const graphName = await resolveGraphName(persistence, options.graph);
55
+ const verifier = new AuditVerifierService({
56
+ persistence: /** @type {import('../../../src/domain/types/WarpPersistence.js').CorePersistence} */ (/** @type {unknown} */ (persistence)),
57
+ codec: defaultCodec,
58
+ });
59
+
60
+ const trustWarning = detectTrustWarning();
61
+
62
+ /** @type {Record<string, unknown>} */
63
+ let payload;
64
+ if (writerFilter !== undefined) {
65
+ const chain = await verifier.verifyChain(graphName, writerFilter, { since });
66
+ const invalid = chain.status !== 'VALID' && chain.status !== 'PARTIAL' ? 1 : 0;
67
+ payload = {
68
+ graph: graphName,
69
+ verifiedAt: new Date().toISOString(),
70
+ summary: {
71
+ total: 1,
72
+ valid: chain.status === 'VALID' ? 1 : 0,
73
+ partial: chain.status === 'PARTIAL' ? 1 : 0,
74
+ invalid,
75
+ },
76
+ chains: [chain],
77
+ trustWarning,
78
+ };
79
+ } else {
80
+ payload = await verifier.verifyAll(graphName, { since, trustWarning });
81
+ }
82
+
83
+ // Attach trust assessment only when explicitly requested via --trust-mode
84
+ if (trustMode) {
85
+ try {
86
+ const trustAssessment = await verifier.evaluateTrust(graphName, {
87
+ pin: trustPin,
88
+ mode: trustMode,
89
+ });
90
+ payload.trustAssessment = trustAssessment;
91
+ } catch (err) {
92
+ if (trustMode === 'enforce') {
93
+ throw err;
94
+ }
95
+ payload.trustAssessment = {
96
+ trustSchemaVersion: 1,
97
+ mode: 'signed_evidence_v1',
98
+ trustVerdict: 'error',
99
+ error: err instanceof Error ? err.message : 'Trust evaluation failed',
100
+ };
101
+ }
102
+ }
103
+
104
+ const { summary, trustAssessment } = /** @type {{summary: {invalid: number}, trustAssessment?: {trustVerdict?: string}}} */ (payload);
105
+ const hasInvalid = summary.invalid > 0;
106
+ const trustFailed = trustMode === 'enforce' &&
107
+ trustAssessment?.trustVerdict === 'fail';
108
+ return {
109
+ payload,
110
+ exitCode: trustFailed ? EXIT_CODES.TRUST_FAIL
111
+ : hasInvalid ? EXIT_CODES.INTERNAL
112
+ : EXIT_CODES.OK,
113
+ };
114
+ }
@@ -0,0 +1,46 @@
1
+ import process from 'node:process';
2
+ import { parseCommandArgs, usageError } from '../infrastructure.js';
3
+ import { viewSchema } from '../schemas.js';
4
+
5
+ /** @typedef {import('../types.js').CliOptions} CliOptions */
6
+
7
+ const VIEW_OPTIONS = {
8
+ list: { type: 'boolean', default: false },
9
+ log: { type: 'boolean', default: false },
10
+ };
11
+
12
+ /**
13
+ * @param {{options: CliOptions, args: string[]}} params
14
+ * @returns {Promise<{payload: unknown, exitCode: number}>}
15
+ */
16
+ export default async function handleView({ options, args }) {
17
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
18
+ throw usageError('view command requires an interactive terminal (TTY)');
19
+ }
20
+
21
+ const { values, positionals } = parseCommandArgs(args, VIEW_OPTIONS, viewSchema, { allowPositionals: true });
22
+ const viewMode = values.log || positionals[0] === 'log' ? 'log' : 'list';
23
+
24
+ try {
25
+ // @ts-expect-error — optional peer dependency, may not be installed
26
+ const { startTui } = await import('@git-stunts/git-warp-tui');
27
+ await startTui({
28
+ repo: options.repo || '.',
29
+ graph: options.graph || 'default',
30
+ mode: viewMode,
31
+ });
32
+ } catch (err) {
33
+ const errObj = /** @type {{code?: string, message?: string, specifier?: string}} */ (typeof err === 'object' && err !== null ? err : {});
34
+ const isMissing = errObj.code === 'ERR_MODULE_NOT_FOUND' || (errObj.message && errObj.message.includes('Cannot find module'));
35
+ const isTui = errObj.specifier?.includes('git-warp-tui') ||
36
+ /cannot find (?:package|module) ['"]@git-stunts\/git-warp-tui/i.test(errObj.message || '');
37
+ if (isMissing && isTui) {
38
+ throw usageError(
39
+ 'Interactive TUI requires @git-stunts/git-warp-tui.\n' +
40
+ ' Install with: npm install -g @git-stunts/git-warp-tui',
41
+ );
42
+ }
43
+ throw err;
44
+ }
45
+ return { payload: undefined, exitCode: 0 };
46
+ }