@git-stunts/git-warp 11.2.1 → 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.
- package/bin/cli/commands/check.js +2 -2
- package/bin/cli/commands/doctor/checks.js +12 -12
- package/bin/cli/commands/doctor/index.js +2 -2
- package/bin/cli/commands/doctor/types.js +1 -1
- package/bin/cli/commands/history.js +12 -5
- package/bin/cli/commands/install-hooks.js +5 -5
- package/bin/cli/commands/materialize.js +2 -2
- package/bin/cli/commands/patch.js +142 -0
- package/bin/cli/commands/path.js +4 -4
- package/bin/cli/commands/query.js +54 -13
- package/bin/cli/commands/registry.js +4 -0
- package/bin/cli/commands/seek.js +17 -11
- package/bin/cli/commands/tree.js +230 -0
- package/bin/cli/commands/trust.js +3 -3
- package/bin/cli/commands/verify-audit.js +8 -7
- package/bin/cli/commands/view.js +6 -5
- package/bin/cli/infrastructure.js +26 -12
- package/bin/cli/shared.js +2 -2
- package/bin/cli/types.js +19 -8
- package/bin/presenters/index.js +35 -9
- package/bin/presenters/json.js +14 -12
- package/bin/presenters/text.js +155 -33
- package/index.d.ts +82 -22
- package/package.json +3 -2
- package/src/domain/WarpGraph.js +4 -1
- package/src/domain/crdt/ORSet.js +8 -8
- package/src/domain/errors/EmptyMessageError.js +2 -2
- package/src/domain/errors/ForkError.js +1 -1
- package/src/domain/errors/IndexError.js +1 -1
- package/src/domain/errors/OperationAbortedError.js +1 -1
- package/src/domain/errors/QueryError.js +1 -1
- package/src/domain/errors/SchemaUnsupportedError.js +1 -1
- package/src/domain/errors/ShardCorruptionError.js +2 -2
- package/src/domain/errors/ShardLoadError.js +2 -2
- package/src/domain/errors/ShardValidationError.js +4 -4
- package/src/domain/errors/StorageError.js +2 -2
- package/src/domain/errors/SyncError.js +1 -1
- package/src/domain/errors/TraversalError.js +1 -1
- package/src/domain/errors/TrustError.js +1 -1
- package/src/domain/errors/WarpError.js +2 -2
- package/src/domain/errors/WormholeError.js +1 -1
- package/src/domain/services/AuditReceiptService.js +6 -6
- package/src/domain/services/AuditVerifierService.js +52 -38
- package/src/domain/services/BitmapIndexBuilder.js +3 -3
- package/src/domain/services/BitmapIndexReader.js +28 -19
- package/src/domain/services/BoundaryTransitionRecord.js +18 -17
- package/src/domain/services/CheckpointSerializerV5.js +17 -16
- package/src/domain/services/CheckpointService.js +2 -2
- package/src/domain/services/CommitDagTraversalService.js +13 -13
- package/src/domain/services/DagPathFinding.js +7 -7
- package/src/domain/services/DagTopology.js +1 -1
- package/src/domain/services/DagTraversal.js +1 -1
- package/src/domain/services/HealthCheckService.js +1 -1
- package/src/domain/services/HookInstaller.js +1 -1
- package/src/domain/services/HttpSyncServer.js +92 -41
- package/src/domain/services/IndexRebuildService.js +7 -7
- package/src/domain/services/IndexStalenessChecker.js +4 -3
- package/src/domain/services/JoinReducer.js +11 -11
- package/src/domain/services/LogicalTraversal.js +1 -1
- package/src/domain/services/MessageCodecInternal.js +1 -1
- package/src/domain/services/MigrationService.js +1 -1
- package/src/domain/services/ObserverView.js +8 -8
- package/src/domain/services/PatchBuilderV2.js +42 -26
- package/src/domain/services/ProvenanceIndex.js +1 -1
- package/src/domain/services/ProvenancePayload.js +1 -1
- package/src/domain/services/QueryBuilder.js +3 -3
- package/src/domain/services/StateDiff.js +14 -11
- package/src/domain/services/StateSerializerV5.js +2 -2
- package/src/domain/services/StreamingBitmapIndexBuilder.js +26 -24
- package/src/domain/services/SyncAuthService.js +3 -2
- package/src/domain/services/SyncProtocol.js +25 -11
- package/src/domain/services/TemporalQuery.js +9 -6
- package/src/domain/services/TranslationCost.js +7 -5
- package/src/domain/services/WormholeService.js +16 -7
- package/src/domain/trust/TrustCanonical.js +3 -3
- package/src/domain/trust/TrustEvaluator.js +18 -3
- package/src/domain/trust/TrustRecordService.js +30 -23
- package/src/domain/trust/TrustStateBuilder.js +21 -8
- package/src/domain/trust/canonical.js +6 -6
- package/src/domain/types/TickReceipt.js +1 -1
- package/src/domain/types/WarpErrors.js +45 -0
- package/src/domain/types/WarpOptions.js +29 -0
- package/src/domain/types/WarpPersistence.js +41 -0
- package/src/domain/types/WarpTypes.js +2 -2
- package/src/domain/types/WarpTypesV2.js +2 -2
- package/src/domain/utils/MinHeap.js +6 -5
- package/src/domain/utils/canonicalStringify.js +5 -4
- package/src/domain/utils/roaring.js +31 -5
- package/src/domain/warp/PatchSession.js +9 -18
- package/src/domain/warp/_wiredMethods.d.ts +199 -45
- package/src/domain/warp/checkpoint.methods.js +5 -1
- package/src/domain/warp/fork.methods.js +2 -2
- package/src/domain/warp/materialize.methods.js +55 -5
- package/src/domain/warp/materializeAdvanced.methods.js +15 -4
- package/src/domain/warp/patch.methods.js +54 -29
- package/src/domain/warp/provenance.methods.js +5 -3
- package/src/domain/warp/query.methods.js +6 -5
- package/src/domain/warp/sync.methods.js +16 -11
- package/src/globals.d.ts +64 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +14 -9
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +9 -4
- package/src/infrastructure/adapters/DenoHttpAdapter.js +5 -6
- package/src/infrastructure/adapters/GitGraphAdapter.js +14 -12
- package/src/infrastructure/adapters/NodeHttpAdapter.js +2 -2
- package/src/infrastructure/adapters/WebCryptoAdapter.js +2 -2
- package/src/visualization/layouts/converters.js +2 -2
- package/src/visualization/layouts/elkAdapter.js +1 -1
- package/src/visualization/layouts/elkLayout.js +10 -7
- package/src/visualization/layouts/index.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +16 -6
- package/src/visualization/renderers/svg/index.js +1 -1
|
@@ -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
|
+
}
|
|
@@ -49,7 +49,7 @@ function resolveTrustPin(cliPin) {
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Discovers all writer IDs from the writers prefix refs.
|
|
52
|
-
* @param {
|
|
52
|
+
* @param {import('../types.js').Persistence} persistence
|
|
53
53
|
* @param {string} graphName
|
|
54
54
|
* @returns {Promise<string[]>}
|
|
55
55
|
*/
|
|
@@ -96,7 +96,7 @@ function buildNotConfiguredResult(graphName) {
|
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* @param {{options: CliOptions, args: string[]}} params
|
|
99
|
-
* @returns {Promise<{payload:
|
|
99
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
100
100
|
*/
|
|
101
101
|
export default async function handleTrust({ options, args }) {
|
|
102
102
|
const { mode, trustPin } = parseTrustArgs(args);
|
|
@@ -104,7 +104,7 @@ export default async function handleTrust({ options, args }) {
|
|
|
104
104
|
const graphName = await resolveGraphName(persistence, options.graph);
|
|
105
105
|
|
|
106
106
|
const recordService = new TrustRecordService({
|
|
107
|
-
persistence: /** @type {
|
|
107
|
+
persistence: /** @type {import('../../../src/domain/types/WarpPersistence.js').CorePersistence} */ (/** @type {unknown} */ (persistence)),
|
|
108
108
|
codec: defaultCodec,
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -46,20 +46,20 @@ export function parseVerifyAuditArgs(args) {
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* @param {{options: CliOptions, args: string[]}} params
|
|
49
|
-
* @returns {Promise<{payload:
|
|
49
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
50
50
|
*/
|
|
51
51
|
export default async function handleVerifyAudit({ options, args }) {
|
|
52
52
|
const { since, writerFilter, trustMode, trustPin } = parseVerifyAuditArgs(args);
|
|
53
53
|
const { persistence } = await createPersistence(options.repo);
|
|
54
54
|
const graphName = await resolveGraphName(persistence, options.graph);
|
|
55
55
|
const verifier = new AuditVerifierService({
|
|
56
|
-
persistence: /** @type {
|
|
56
|
+
persistence: /** @type {import('../../../src/domain/types/WarpPersistence.js').CorePersistence} */ (/** @type {unknown} */ (persistence)),
|
|
57
57
|
codec: defaultCodec,
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
const trustWarning = detectTrustWarning();
|
|
61
61
|
|
|
62
|
-
/** @type {
|
|
62
|
+
/** @type {Record<string, unknown>} */
|
|
63
63
|
let payload;
|
|
64
64
|
if (writerFilter !== undefined) {
|
|
65
65
|
const chain = await verifier.verifyChain(graphName, writerFilter, { since });
|
|
@@ -88,7 +88,7 @@ export default async function handleVerifyAudit({ options, args }) {
|
|
|
88
88
|
mode: trustMode,
|
|
89
89
|
});
|
|
90
90
|
payload.trustAssessment = trustAssessment;
|
|
91
|
-
} catch (
|
|
91
|
+
} catch (err) {
|
|
92
92
|
if (trustMode === 'enforce') {
|
|
93
93
|
throw err;
|
|
94
94
|
}
|
|
@@ -96,14 +96,15 @@ export default async function handleVerifyAudit({ options, args }) {
|
|
|
96
96
|
trustSchemaVersion: 1,
|
|
97
97
|
mode: 'signed_evidence_v1',
|
|
98
98
|
trustVerdict: 'error',
|
|
99
|
-
error: err
|
|
99
|
+
error: err instanceof Error ? err.message : 'Trust evaluation failed',
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const
|
|
104
|
+
const { summary, trustAssessment } = /** @type {{summary: {invalid: number}, trustAssessment?: {trustVerdict?: string}}} */ (payload);
|
|
105
|
+
const hasInvalid = summary.invalid > 0;
|
|
105
106
|
const trustFailed = trustMode === 'enforce' &&
|
|
106
|
-
|
|
107
|
+
trustAssessment?.trustVerdict === 'fail';
|
|
107
108
|
return {
|
|
108
109
|
payload,
|
|
109
110
|
exitCode: trustFailed ? EXIT_CODES.TRUST_FAIL
|
package/bin/cli/commands/view.js
CHANGED
|
@@ -11,7 +11,7 @@ const VIEW_OPTIONS = {
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @param {{options: CliOptions, args: string[]}} params
|
|
14
|
-
* @returns {Promise<{payload:
|
|
14
|
+
* @returns {Promise<{payload: unknown, exitCode: number}>}
|
|
15
15
|
*/
|
|
16
16
|
export default async function handleView({ options, args }) {
|
|
17
17
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -29,10 +29,11 @@ export default async function handleView({ options, args }) {
|
|
|
29
29
|
graph: options.graph || 'default',
|
|
30
30
|
mode: viewMode,
|
|
31
31
|
});
|
|
32
|
-
} catch (
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
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 || '');
|
|
36
37
|
if (isMissing && isTui) {
|
|
37
38
|
throw usageError(
|
|
38
39
|
'Interactive TUI requires @git-stunts/git-warp-tui.\n' +
|
|
@@ -11,6 +11,8 @@ export const EXIT_CODES = {
|
|
|
11
11
|
INTERNAL: 3,
|
|
12
12
|
/** Trust policy denial (enforce mode). */
|
|
13
13
|
TRUST_FAIL: 4,
|
|
14
|
+
/** Valid result but negative (e.g. no path found). Follows grep convention. */
|
|
15
|
+
NO_MATCH: 1,
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -22,9 +24,7 @@ export function getEnvVar(name) {
|
|
|
22
24
|
if (typeof process !== 'undefined' && process.env) {
|
|
23
25
|
return process.env[name];
|
|
24
26
|
}
|
|
25
|
-
// @ts-expect-error — Deno global is only present in Deno runtime
|
|
26
27
|
if (typeof Deno !== 'undefined') {
|
|
27
|
-
// @ts-expect-error — Deno global is only present in Deno runtime
|
|
28
28
|
// eslint-disable-next-line no-undef
|
|
29
29
|
try { return Deno.env.get(name); } catch { return undefined; }
|
|
30
30
|
}
|
|
@@ -45,6 +45,8 @@ Commands:
|
|
|
45
45
|
trust Evaluate writer trust from signed evidence
|
|
46
46
|
materialize Materialize and checkpoint all graphs
|
|
47
47
|
seek Time-travel: step through graph history by Lamport tick
|
|
48
|
+
patch Decode and inspect raw patches
|
|
49
|
+
tree ASCII tree traversal from root nodes
|
|
48
50
|
view Interactive TUI graph browser (requires @git-stunts/git-warp-tui)
|
|
49
51
|
install-hooks Install post-merge git hook
|
|
50
52
|
|
|
@@ -99,6 +101,18 @@ Seek options:
|
|
|
99
101
|
--drop <name> Delete a saved cursor
|
|
100
102
|
--diff Show structural diff (added/removed nodes, edges, props)
|
|
101
103
|
--diff-limit <N> Max diff entries (default 2000)
|
|
104
|
+
|
|
105
|
+
Patch options:
|
|
106
|
+
show <sha> Decode and display a single patch as JSON
|
|
107
|
+
list List all patches sorted by Lamport clock
|
|
108
|
+
--writer <id> Filter by writer (list only)
|
|
109
|
+
--limit <n> Max entries to show (list only)
|
|
110
|
+
|
|
111
|
+
Tree options:
|
|
112
|
+
[rootNode] Root node id (auto-detected if omitted)
|
|
113
|
+
--edge <label> Follow only this edge label
|
|
114
|
+
--prop <key> Annotate nodes with this property (repeatable)
|
|
115
|
+
--max-depth <n> Maximum traversal depth
|
|
102
116
|
`;
|
|
103
117
|
|
|
104
118
|
/**
|
|
@@ -130,7 +144,7 @@ export function notFoundError(message) {
|
|
|
130
144
|
return new CliError(message, { code: 'E_NOT_FOUND', exitCode: EXIT_CODES.NOT_FOUND });
|
|
131
145
|
}
|
|
132
146
|
|
|
133
|
-
export const KNOWN_COMMANDS = ['info', 'query', 'path', 'history', 'check', 'doctor', 'materialize', 'seek', 'verify-audit', 'trust', 'install-hooks', 'view'];
|
|
147
|
+
export const KNOWN_COMMANDS = ['info', 'query', 'path', 'history', 'check', 'doctor', 'materialize', 'seek', 'verify-audit', 'trust', 'patch', 'tree', 'install-hooks', 'view'];
|
|
134
148
|
|
|
135
149
|
const BASE_OPTIONS = {
|
|
136
150
|
repo: { type: 'string', short: 'r' },
|
|
@@ -272,17 +286,17 @@ export function parseArgs(argv) {
|
|
|
272
286
|
const { baseArgs, command, commandArgs } = extractBaseArgs(argv);
|
|
273
287
|
const processed = preprocessView(baseArgs);
|
|
274
288
|
|
|
275
|
-
/** @type {
|
|
289
|
+
/** @type {{ values: Record<string, string|boolean|string[]|boolean[]|undefined>, positionals: string[] }} */
|
|
276
290
|
let parsed;
|
|
277
291
|
try {
|
|
278
292
|
parsed = nodeParseArgs({
|
|
279
293
|
args: processed,
|
|
280
|
-
options: /** @type {
|
|
294
|
+
options: /** @type {import('node:util').ParseArgsConfig['options']} */ (BASE_OPTIONS),
|
|
281
295
|
strict: true,
|
|
282
296
|
allowPositionals: false,
|
|
283
297
|
});
|
|
284
|
-
} catch (
|
|
285
|
-
throw usageError(err.message);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
throw usageError(err instanceof Error ? err.message : String(err));
|
|
286
300
|
}
|
|
287
301
|
|
|
288
302
|
const { values } = parsed;
|
|
@@ -312,22 +326,22 @@ export function parseArgs(argv) {
|
|
|
312
326
|
* @returns {{values: *, positionals: string[]}}
|
|
313
327
|
*/
|
|
314
328
|
export function parseCommandArgs(args, config, schema, { allowPositionals = false } = {}) {
|
|
315
|
-
/** @type {
|
|
329
|
+
/** @type {{ values: Record<string, string|boolean|string[]|boolean[]|undefined>, positionals: string[] }} */
|
|
316
330
|
let parsed;
|
|
317
331
|
try {
|
|
318
332
|
parsed = nodeParseArgs({
|
|
319
333
|
args,
|
|
320
|
-
options: /** @type {
|
|
334
|
+
options: /** @type {import('node:util').ParseArgsConfig['options']} */ (config),
|
|
321
335
|
strict: true,
|
|
322
336
|
allowPositionals,
|
|
323
337
|
});
|
|
324
|
-
} catch (
|
|
325
|
-
throw usageError(err.message);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
throw usageError(err instanceof Error ? err.message : String(err));
|
|
326
340
|
}
|
|
327
341
|
|
|
328
342
|
const result = schema.safeParse(parsed.values);
|
|
329
343
|
if (!result.success) {
|
|
330
|
-
const msg = result.error.issues.map((/** @type {
|
|
344
|
+
const msg = result.error.issues.map((/** @type {{message: string}} */ issue) => issue.message).join('; ');
|
|
331
345
|
throw usageError(msg);
|
|
332
346
|
}
|
|
333
347
|
|
package/bin/cli/shared.js
CHANGED
|
@@ -92,7 +92,7 @@ export async function openGraph(options) {
|
|
|
92
92
|
throw notFoundError(`Graph not found: ${options.graph}`);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
const graph = /** @type {WarpGraphInstance} */ (/** @type {
|
|
95
|
+
const graph = /** @type {WarpGraphInstance} */ (/** @type {unknown} */ (await WarpGraph.open({
|
|
96
96
|
persistence,
|
|
97
97
|
graphName,
|
|
98
98
|
writerId: options.writer,
|
|
@@ -183,7 +183,7 @@ export function createHookInstaller() {
|
|
|
183
183
|
const templateDir = path.resolve(__dirname, '..', '..', 'scripts', 'hooks');
|
|
184
184
|
const { version } = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json'), 'utf8'));
|
|
185
185
|
return new HookInstaller({
|
|
186
|
-
fs: /** @type {
|
|
186
|
+
fs: /** @type {import('../../src/domain/services/HookInstaller.js').FsAdapter} */ (/** @type {unknown} */ (fs)),
|
|
187
187
|
execGitConfig: execGitConfigValue,
|
|
188
188
|
version,
|
|
189
189
|
templateDir,
|
package/bin/cli/types.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @property {(sha: string) => Promise<boolean>} nodeExists
|
|
11
11
|
* @property {(sha: string, coverageSha: string) => Promise<boolean>} isAncestor
|
|
12
12
|
* @property {() => Promise<{ok: boolean}>} ping
|
|
13
|
-
* @property {
|
|
13
|
+
* @property {unknown} plumbing
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -19,18 +19,19 @@
|
|
|
19
19
|
* @property {() => Promise<Array<{id: string}>>} getNodes
|
|
20
20
|
* @property {() => Promise<Array<{from: string, to: string, label?: string}>>} getEdges
|
|
21
21
|
* @property {() => Promise<string|null>} createCheckpoint
|
|
22
|
-
* @property {() =>
|
|
22
|
+
* @property {() => QueryBuilderLike} query
|
|
23
23
|
* @property {{ shortestPath: Function }} traverse
|
|
24
|
-
* @property {(writerId: string) => Promise<Array<{patch:
|
|
25
|
-
* @property {() => Promise<{frontier: Record<string,
|
|
26
|
-
* @property {() => Promise<
|
|
24
|
+
* @property {(writerId: string) => Promise<Array<{patch: {schema?: number, lamport: number, ops?: Array<{type: string, node?: string, from?: string, to?: string}>}, sha: string}>>} getWriterPatches
|
|
25
|
+
* @property {() => Promise<{frontier: Record<string, string>}>} status
|
|
26
|
+
* @property {() => Promise<string[]>} discoverWriters
|
|
27
|
+
* @property {() => Promise<Map<string, string>>} getFrontier
|
|
27
28
|
* @property {() => {totalTombstones: number, tombstoneRatio: number}} getGCMetrics
|
|
28
29
|
* @property {() => Promise<number>} getPropertyCount
|
|
29
30
|
* @property {() => Promise<import('../../src/domain/services/JoinReducer.js').WarpStateV5 | null>} getStateSnapshot
|
|
30
31
|
* @property {() => Promise<{ticks: number[], maxTick: number, perWriter: Map<string, WriterTickInfo>}>} discoverTicks
|
|
31
|
-
* @property {(sha: string) => Promise<{ops?:
|
|
32
|
-
* @property {(cache:
|
|
33
|
-
* @property {
|
|
32
|
+
* @property {(sha: string) => Promise<{ops?: Array<{type: string, node?: string, from?: string, to?: string}>}>} loadPatchBySha
|
|
33
|
+
* @property {(cache: import('../../src/ports/SeekCachePort.js').default) => void} setSeekCache
|
|
34
|
+
* @property {{clear: () => Promise<void>} | null} seekCache
|
|
34
35
|
* @property {number} [_seekCeiling]
|
|
35
36
|
* @property {boolean} [_provenanceDegraded]
|
|
36
37
|
*/
|
|
@@ -82,4 +83,14 @@
|
|
|
82
83
|
* @property {number} diffLimit
|
|
83
84
|
*/
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* @typedef {Object} QueryBuilderLike
|
|
88
|
+
* @property {(label?: string) => QueryBuilderLike} outgoing
|
|
89
|
+
* @property {(label?: string) => QueryBuilderLike} incoming
|
|
90
|
+
* @property {(fn: Function) => QueryBuilderLike} where
|
|
91
|
+
* @property {(pattern: string) => QueryBuilderLike} match
|
|
92
|
+
* @property {(fields: string[]) => QueryBuilderLike} select
|
|
93
|
+
* @property {() => Promise<{nodes: Array<{id: string, props?: Record<string, unknown>}>, stateHash?: string}>} run
|
|
94
|
+
*/
|
|
95
|
+
|
|
85
96
|
export {};
|
package/bin/presenters/index.js
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
renderSeek,
|
|
31
31
|
renderVerifyAudit,
|
|
32
32
|
renderTrust,
|
|
33
|
+
renderPatchShow,
|
|
34
|
+
renderPatchList,
|
|
33
35
|
} from './text.js';
|
|
34
36
|
|
|
35
37
|
// ── Color control ────────────────────────────────────────────────────────────
|
|
@@ -61,8 +63,29 @@ export function shouldStripColor() {
|
|
|
61
63
|
|
|
62
64
|
// ── Text renderer map ────────────────────────────────────────────────────────
|
|
63
65
|
|
|
64
|
-
/** @
|
|
65
|
-
|
|
66
|
+
/** @param {import('./text.js').PatchShowPayload & Partial<import('./text.js').PatchListPayload>} payload */
|
|
67
|
+
function renderPatch(payload) {
|
|
68
|
+
if (payload.ops) {
|
|
69
|
+
return renderPatchShow(payload);
|
|
70
|
+
}
|
|
71
|
+
return renderPatchList(/** @type {import('./text.js').PatchListPayload} */ (payload));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @param {{ graph: string, tree?: string, orphanCount?: number, orphans?: string[] }} payload */
|
|
75
|
+
function renderTree(payload) {
|
|
76
|
+
const lines = [`Graph: ${payload.graph}`];
|
|
77
|
+
if (payload.tree) {
|
|
78
|
+
lines.push(payload.tree);
|
|
79
|
+
}
|
|
80
|
+
if (payload.orphanCount && payload.orphanCount > 0 && payload.orphans) {
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(`Orphans (${payload.orphanCount}): ${payload.orphans.join(', ')}`);
|
|
83
|
+
}
|
|
84
|
+
return `${lines.join('\n')}\n`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @type {Map<string, function(unknown): string>} */
|
|
88
|
+
const TEXT_RENDERERS = new Map(/** @type {[string, function(unknown): string][]} */ ([
|
|
66
89
|
['info', renderInfo],
|
|
67
90
|
['query', renderQuery],
|
|
68
91
|
['path', renderPath],
|
|
@@ -73,11 +96,13 @@ const TEXT_RENDERERS = new Map(/** @type {[string, function(*): string][]} */ ([
|
|
|
73
96
|
['seek', renderSeek],
|
|
74
97
|
['verify-audit', renderVerifyAudit],
|
|
75
98
|
['trust', renderTrust],
|
|
99
|
+
['patch', renderPatch],
|
|
100
|
+
['tree', renderTree],
|
|
76
101
|
['install-hooks', renderInstallHooks],
|
|
77
102
|
]));
|
|
78
103
|
|
|
79
|
-
/** @type {Map<string, function(
|
|
80
|
-
const VIEW_RENDERERS = new Map(/** @type {[string, function(
|
|
104
|
+
/** @type {Map<string, function(unknown): string>} */
|
|
105
|
+
const VIEW_RENDERERS = new Map(/** @type {[string, function(unknown): string][]} */ ([
|
|
81
106
|
['info', renderInfoView],
|
|
82
107
|
['check', renderCheckView],
|
|
83
108
|
['history', renderHistoryView],
|
|
@@ -102,7 +127,7 @@ function writeHtmlExport(filePath, svgContent) {
|
|
|
102
127
|
|
|
103
128
|
/**
|
|
104
129
|
* Handles svg:PATH and html:PATH view modes for commands that carry _renderedSvg.
|
|
105
|
-
* @param {
|
|
130
|
+
* @param {{ _renderedSvg?: string }} payload
|
|
106
131
|
* @param {string} view
|
|
107
132
|
* @returns {boolean} true if handled
|
|
108
133
|
*/
|
|
@@ -146,13 +171,13 @@ function writeText(text, strip) {
|
|
|
146
171
|
/**
|
|
147
172
|
* Writes a command result to stdout/stderr in the requested format.
|
|
148
173
|
*
|
|
149
|
-
* @param {
|
|
174
|
+
* @param {Record<string, unknown>} payload - Command result payload
|
|
150
175
|
* @param {{format: string, command: string, view: string|null|boolean}} options
|
|
151
176
|
*/
|
|
152
177
|
export function present(payload, { format, command, view }) {
|
|
153
178
|
// Error payloads always go to stderr as plain text
|
|
154
179
|
if (payload?.error) {
|
|
155
|
-
process.stderr.write(renderError(payload));
|
|
180
|
+
process.stderr.write(renderError(/** @type {import('./text.js').ErrorPayload} */ (payload)));
|
|
156
181
|
return;
|
|
157
182
|
}
|
|
158
183
|
|
|
@@ -186,7 +211,7 @@ export function present(payload, { format, command, view }) {
|
|
|
186
211
|
|
|
187
212
|
/**
|
|
188
213
|
* Handles --view output dispatch (ASCII view, SVG file, HTML file).
|
|
189
|
-
* @param {
|
|
214
|
+
* @param {Record<string, unknown>} payload
|
|
190
215
|
* @param {string} command
|
|
191
216
|
* @param {string|boolean} view
|
|
192
217
|
*/
|
|
@@ -200,7 +225,8 @@ function presentView(payload, command, view) {
|
|
|
200
225
|
|
|
201
226
|
// query is special: uses pre-rendered _renderedAscii
|
|
202
227
|
if (command === 'query') {
|
|
203
|
-
|
|
228
|
+
const ascii = typeof payload._renderedAscii === 'string' ? payload._renderedAscii : '';
|
|
229
|
+
writeText(`${ascii}\n`, strip);
|
|
204
230
|
return;
|
|
205
231
|
}
|
|
206
232
|
|
package/bin/presenters/json.js
CHANGED
|
@@ -8,18 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Recursively sorts object keys for deterministic JSON output.
|
|
11
|
-
* @param {
|
|
12
|
-
* @returns {
|
|
11
|
+
* @param {unknown} input
|
|
12
|
+
* @returns {unknown}
|
|
13
13
|
*/
|
|
14
14
|
function normalize(input) {
|
|
15
15
|
if (Array.isArray(input)) {
|
|
16
16
|
return input.map(normalize);
|
|
17
17
|
}
|
|
18
18
|
if (input && typeof input === 'object') {
|
|
19
|
-
/** @type {Record<string,
|
|
19
|
+
const rec = /** @type {Record<string, unknown>} */ (input);
|
|
20
|
+
/** @type {Record<string, unknown>} */
|
|
20
21
|
const sorted = {};
|
|
21
|
-
for (const key of Object.keys(
|
|
22
|
-
sorted[key] = normalize(
|
|
22
|
+
for (const key of Object.keys(rec).sort()) {
|
|
23
|
+
sorted[key] = normalize(rec[key]);
|
|
23
24
|
}
|
|
24
25
|
return sorted;
|
|
25
26
|
}
|
|
@@ -28,7 +29,7 @@ function normalize(input) {
|
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Pretty-printed JSON with sorted keys (2-space indent).
|
|
31
|
-
* @param {
|
|
32
|
+
* @param {unknown} value
|
|
32
33
|
* @returns {string}
|
|
33
34
|
*/
|
|
34
35
|
export function stableStringify(value) {
|
|
@@ -37,7 +38,7 @@ export function stableStringify(value) {
|
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* Single-line JSON with sorted keys (no indent).
|
|
40
|
-
* @param {
|
|
41
|
+
* @param {unknown} value
|
|
41
42
|
* @returns {string}
|
|
42
43
|
*/
|
|
43
44
|
export function compactStringify(value) {
|
|
@@ -48,18 +49,19 @@ export function compactStringify(value) {
|
|
|
48
49
|
* Shallow-clones a payload, removing all top-level underscore-prefixed keys.
|
|
49
50
|
* These are internal rendering artifacts (e.g. _renderedSvg, _renderedAscii)
|
|
50
51
|
* that should not leak into JSON/NDJSON output.
|
|
51
|
-
* @param {
|
|
52
|
-
* @returns {
|
|
52
|
+
* @param {Record<string, unknown> | unknown} payload
|
|
53
|
+
* @returns {Record<string, unknown> | unknown}
|
|
53
54
|
*/
|
|
54
55
|
export function sanitizePayload(payload) {
|
|
55
56
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
56
57
|
return payload;
|
|
57
58
|
}
|
|
58
|
-
/** @type {Record<string,
|
|
59
|
+
const rec = /** @type {Record<string, unknown>} */ (payload);
|
|
60
|
+
/** @type {Record<string, unknown>} */
|
|
59
61
|
const clean = {};
|
|
60
|
-
for (const key of Object.keys(
|
|
62
|
+
for (const key of Object.keys(rec)) {
|
|
61
63
|
if (!key.startsWith('_')) {
|
|
62
|
-
clean[key] =
|
|
64
|
+
clean[key] = rec[key];
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
return clean;
|