@git-stunts/git-warp 10.4.2 → 10.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SECURITY.md +89 -1
- package/bin/presenters/index.js +208 -0
- package/bin/presenters/json.js +66 -0
- package/bin/presenters/text.js +407 -0
- package/bin/warp-graph.js +206 -534
- package/index.d.ts +24 -0
- package/package.json +2 -2
- package/src/domain/WarpGraph.js +72 -15
- package/src/domain/services/HttpSyncServer.js +74 -6
- package/src/domain/services/SyncAuthService.js +396 -0
- package/src/infrastructure/adapters/GitGraphAdapter.js +9 -56
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +488 -0
- package/src/infrastructure/adapters/adapterValidation.js +90 -0
- package/src/visualization/renderers/ascii/seek.js +172 -22
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-text renderers for CLI output.
|
|
3
|
+
*
|
|
4
|
+
* Each function accepts a command payload and returns a formatted string
|
|
5
|
+
* (with trailing newline) suitable for process.stdout.write().
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { formatStructuralDiff } from '../../src/visualization/renderers/ascii/seek.js';
|
|
9
|
+
|
|
10
|
+
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const ANSI_GREEN = '\x1b[32m';
|
|
13
|
+
const ANSI_YELLOW = '\x1b[33m';
|
|
14
|
+
const ANSI_RED = '\x1b[31m';
|
|
15
|
+
const ANSI_DIM = '\x1b[2m';
|
|
16
|
+
const ANSI_RESET = '\x1b[0m';
|
|
17
|
+
|
|
18
|
+
/** @param {string} state */
|
|
19
|
+
function colorCachedState(state) {
|
|
20
|
+
if (state === 'fresh') {
|
|
21
|
+
return `${ANSI_GREEN}${state}${ANSI_RESET}`;
|
|
22
|
+
}
|
|
23
|
+
if (state === 'stale') {
|
|
24
|
+
return `${ANSI_YELLOW}${state}${ANSI_RESET}`;
|
|
25
|
+
}
|
|
26
|
+
return `${ANSI_RED}${ANSI_DIM}${state}${ANSI_RESET}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @param {*} hook */
|
|
30
|
+
function formatHookStatusLine(hook) {
|
|
31
|
+
if (!hook.installed && hook.foreign) {
|
|
32
|
+
return "Hook: foreign hook present — run 'git warp install-hooks'";
|
|
33
|
+
}
|
|
34
|
+
if (!hook.installed) {
|
|
35
|
+
return "Hook: not installed — run 'git warp install-hooks'";
|
|
36
|
+
}
|
|
37
|
+
if (hook.current) {
|
|
38
|
+
return `Hook: installed (v${hook.version}) — up to date`;
|
|
39
|
+
}
|
|
40
|
+
return `Hook: installed (v${hook.version}) — upgrade available, run 'git warp install-hooks'`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Simple renderers ─────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/** @param {*} payload */
|
|
46
|
+
export function renderInfo(payload) {
|
|
47
|
+
const lines = [`Repo: ${payload.repo}`];
|
|
48
|
+
lines.push(`Graphs: ${payload.graphs.length}`);
|
|
49
|
+
for (const graph of payload.graphs) {
|
|
50
|
+
const writers = graph.writers ? ` writers=${graph.writers.count}` : '';
|
|
51
|
+
lines.push(`- ${graph.name}${writers}`);
|
|
52
|
+
if (graph.checkpoint?.sha) {
|
|
53
|
+
lines.push(` checkpoint: ${graph.checkpoint.sha}`);
|
|
54
|
+
}
|
|
55
|
+
if (graph.coverage?.sha) {
|
|
56
|
+
lines.push(` coverage: ${graph.coverage.sha}`);
|
|
57
|
+
}
|
|
58
|
+
if (graph.cursor?.active) {
|
|
59
|
+
lines.push(` cursor: tick ${graph.cursor.tick} (${graph.cursor.mode})`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return `${lines.join('\n')}\n`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @param {*} payload */
|
|
66
|
+
export function renderQuery(payload) {
|
|
67
|
+
const lines = [
|
|
68
|
+
`Graph: ${payload.graph}`,
|
|
69
|
+
`State: ${payload.stateHash}`,
|
|
70
|
+
`Nodes: ${payload.nodes.length}`,
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const node of payload.nodes) {
|
|
74
|
+
const id = node.id ?? '(unknown)';
|
|
75
|
+
lines.push(`- ${id}`);
|
|
76
|
+
if (node.props && Object.keys(node.props).length > 0) {
|
|
77
|
+
lines.push(` props: ${JSON.stringify(node.props)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return `${lines.join('\n')}\n`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @param {*} payload */
|
|
85
|
+
export function renderPath(payload) {
|
|
86
|
+
const lines = [
|
|
87
|
+
`Graph: ${payload.graph}`,
|
|
88
|
+
`From: ${payload.from}`,
|
|
89
|
+
`To: ${payload.to}`,
|
|
90
|
+
`Found: ${payload.found ? 'yes' : 'no'}`,
|
|
91
|
+
`Length: ${payload.length}`,
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
if (payload.path && payload.path.length > 0) {
|
|
95
|
+
lines.push(`Path: ${payload.path.join(' -> ')}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return `${lines.join('\n')}\n`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Appends checkpoint and writer lines to check output.
|
|
103
|
+
* @param {string[]} lines
|
|
104
|
+
* @param {*} payload
|
|
105
|
+
*/
|
|
106
|
+
function appendCheckpointAndWriters(lines, payload) {
|
|
107
|
+
if (payload.checkpoint?.sha) {
|
|
108
|
+
lines.push(`Checkpoint: ${payload.checkpoint.sha}`);
|
|
109
|
+
if (payload.checkpoint.ageSeconds !== null) {
|
|
110
|
+
lines.push(`Checkpoint Age: ${payload.checkpoint.ageSeconds}s`);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
lines.push('Checkpoint: none');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!payload.status) {
|
|
117
|
+
lines.push(`Writers: ${payload.writers.count}`);
|
|
118
|
+
}
|
|
119
|
+
for (const head of payload.writers.heads) {
|
|
120
|
+
lines.push(`- ${head.writerId}: ${head.sha}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Appends coverage, gc, and hook lines to check output.
|
|
126
|
+
* @param {string[]} lines
|
|
127
|
+
* @param {*} payload
|
|
128
|
+
*/
|
|
129
|
+
function appendCoverageAndExtras(lines, payload) {
|
|
130
|
+
if (payload.coverage?.sha) {
|
|
131
|
+
lines.push(`Coverage: ${payload.coverage.sha}`);
|
|
132
|
+
lines.push(`Coverage Missing: ${payload.coverage.missingWriters.length}`);
|
|
133
|
+
} else {
|
|
134
|
+
lines.push('Coverage: none');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (payload.gc) {
|
|
138
|
+
lines.push(`Tombstones: ${payload.gc.totalTombstones}`);
|
|
139
|
+
if (!payload.status) {
|
|
140
|
+
lines.push(`Tombstone Ratio: ${payload.gc.tombstoneRatio}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (payload.hook) {
|
|
145
|
+
lines.push(formatHookStatusLine(payload.hook));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @param {*} payload */
|
|
150
|
+
export function renderCheck(payload) {
|
|
151
|
+
const lines = [
|
|
152
|
+
`Graph: ${payload.graph}`,
|
|
153
|
+
`Health: ${payload.health.status}`,
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
if (payload.status) {
|
|
157
|
+
lines.push(`Cached State: ${colorCachedState(payload.status.cachedState)}`);
|
|
158
|
+
lines.push(`Patches Since Checkpoint: ${payload.status.patchesSinceCheckpoint}`);
|
|
159
|
+
lines.push(`Tombstone Ratio: ${payload.status.tombstoneRatio.toFixed(2)}`);
|
|
160
|
+
lines.push(`Writers: ${payload.status.writers}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
appendCheckpointAndWriters(lines, payload);
|
|
164
|
+
appendCoverageAndExtras(lines, payload);
|
|
165
|
+
return `${lines.join('\n')}\n`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @param {*} payload */
|
|
169
|
+
export function renderHistory(payload) {
|
|
170
|
+
const lines = [
|
|
171
|
+
`Graph: ${payload.graph}`,
|
|
172
|
+
`Writer: ${payload.writer}`,
|
|
173
|
+
`Entries: ${payload.entries.length}`,
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
if (payload.nodeFilter) {
|
|
177
|
+
lines.push(`Node Filter: ${payload.nodeFilter}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const entry of payload.entries) {
|
|
181
|
+
lines.push(`- ${entry.sha} (lamport: ${entry.lamport}, ops: ${entry.opCount})`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return `${lines.join('\n')}\n`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @param {*} payload */
|
|
188
|
+
export function renderError(payload) {
|
|
189
|
+
return `Error: ${payload.error.message}\n`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** @param {*} payload */
|
|
193
|
+
export function renderMaterialize(payload) {
|
|
194
|
+
if (payload.graphs.length === 0) {
|
|
195
|
+
return 'No graphs found in repo.\n';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const lines = [];
|
|
199
|
+
for (const entry of payload.graphs) {
|
|
200
|
+
if (entry.error) {
|
|
201
|
+
lines.push(`${entry.graph}: error — ${entry.error}`);
|
|
202
|
+
} else {
|
|
203
|
+
lines.push(`${entry.graph}: ${entry.nodes} nodes, ${entry.edges} edges, checkpoint ${entry.checkpoint}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return `${lines.join('\n')}\n`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** @param {*} payload */
|
|
210
|
+
export function renderInstallHooks(payload) {
|
|
211
|
+
if (payload.action === 'up-to-date') {
|
|
212
|
+
return `Hook: already up to date (v${payload.version}) at ${payload.hookPath}\n`;
|
|
213
|
+
}
|
|
214
|
+
if (payload.action === 'skipped') {
|
|
215
|
+
return 'Hook: installation skipped\n';
|
|
216
|
+
}
|
|
217
|
+
const lines = [`Hook: ${payload.action} (v${payload.version})`, `Path: ${payload.hookPath}`];
|
|
218
|
+
if (payload.backupPath) {
|
|
219
|
+
lines.push(`Backup: ${payload.backupPath}`);
|
|
220
|
+
}
|
|
221
|
+
return `${lines.join('\n')}\n`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Seek helpers (extracted for ESLint 50-line limit) ────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Formats a numeric delta as " (+N)" or " (-N)", or empty string for zero/non-finite.
|
|
228
|
+
* @param {*} n
|
|
229
|
+
* @returns {string}
|
|
230
|
+
*/
|
|
231
|
+
function formatDelta(n) { // TODO(ts-cleanup): type CLI payload
|
|
232
|
+
if (typeof n !== 'number' || !Number.isFinite(n) || n === 0) {
|
|
233
|
+
return '';
|
|
234
|
+
}
|
|
235
|
+
const sign = n > 0 ? '+' : '';
|
|
236
|
+
return ` (${sign}${n})`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Formats an operation summary object as a compact plain-text string.
|
|
241
|
+
* @param {*} summary
|
|
242
|
+
* @returns {string}
|
|
243
|
+
*/
|
|
244
|
+
function formatOpSummaryPlain(summary) { // TODO(ts-cleanup): type CLI payload
|
|
245
|
+
const order = [
|
|
246
|
+
['NodeAdd', '+', 'node'],
|
|
247
|
+
['EdgeAdd', '+', 'edge'],
|
|
248
|
+
['PropSet', '~', 'prop'],
|
|
249
|
+
['NodeTombstone', '-', 'node'],
|
|
250
|
+
['EdgeTombstone', '-', 'edge'],
|
|
251
|
+
['BlobValue', '+', 'blob'],
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
const parts = [];
|
|
255
|
+
for (const [opType, symbol, label] of order) {
|
|
256
|
+
const n = summary?.[opType];
|
|
257
|
+
if (typeof n === 'number' && Number.isFinite(n) && n > 0) {
|
|
258
|
+
parts.push(`${symbol}${n}${label}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return parts.length > 0 ? parts.join(' ') : '(empty)';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Appends a per-writer tick receipt summary below a base line.
|
|
266
|
+
* @param {string} baseLine
|
|
267
|
+
* @param {*} payload
|
|
268
|
+
* @returns {string}
|
|
269
|
+
*/
|
|
270
|
+
function appendReceiptSummary(baseLine, payload) {
|
|
271
|
+
const tickReceipt = payload?.tickReceipt;
|
|
272
|
+
if (!tickReceipt || typeof tickReceipt !== 'object') {
|
|
273
|
+
return `${baseLine}\n`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const entries = Object.entries(tickReceipt)
|
|
277
|
+
.filter(([writerId, entry]) => writerId && entry && typeof entry === 'object')
|
|
278
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
279
|
+
|
|
280
|
+
if (entries.length === 0) {
|
|
281
|
+
return `${baseLine}\n`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const maxWriterLen = Math.max(5, ...entries.map(([writerId]) => writerId.length));
|
|
285
|
+
const receiptLines = [` Tick ${payload.tick}:`];
|
|
286
|
+
for (const [writerId, entry] of entries) {
|
|
287
|
+
const sha = typeof entry.sha === 'string' ? entry.sha.slice(0, 7) : '';
|
|
288
|
+
const opSummary = entry.opSummary && typeof entry.opSummary === 'object' ? entry.opSummary : entry;
|
|
289
|
+
receiptLines.push(` ${writerId.padEnd(maxWriterLen)} ${sha.padEnd(7)} ${formatOpSummaryPlain(opSummary)}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return `${baseLine}\n${receiptLines.join('\n')}\n`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Builds human-readable state count strings from a seek payload.
|
|
297
|
+
* @param {*} payload
|
|
298
|
+
* @returns {{nodesStr: string, edgesStr: string, patchesStr: string}}
|
|
299
|
+
*/
|
|
300
|
+
function buildStateStrings(payload) {
|
|
301
|
+
const nodeLabel = payload.nodes === 1 ? 'node' : 'nodes';
|
|
302
|
+
const edgeLabel = payload.edges === 1 ? 'edge' : 'edges';
|
|
303
|
+
const patchLabel = payload.patchCount === 1 ? 'patch' : 'patches';
|
|
304
|
+
return {
|
|
305
|
+
nodesStr: `${payload.nodes} ${nodeLabel}${formatDelta(payload.diff?.nodes)}`,
|
|
306
|
+
edgesStr: `${payload.edges} ${edgeLabel}${formatDelta(payload.diff?.edges)}`,
|
|
307
|
+
patchesStr: `${payload.patchCount} ${patchLabel}`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Renders the "tick" / "latest" / "load" seek action with receipt + structural diff.
|
|
313
|
+
* @param {*} payload
|
|
314
|
+
* @param {string} headerLine
|
|
315
|
+
* @returns {string}
|
|
316
|
+
*/
|
|
317
|
+
function renderSeekWithDiff(payload, headerLine) {
|
|
318
|
+
const base = appendReceiptSummary(headerLine, payload);
|
|
319
|
+
return base + formatStructuralDiff(payload);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Seek simple-action renderers ─────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Renders seek actions that don't involve state counts: clear-cache, list, drop, save.
|
|
326
|
+
* @param {*} payload
|
|
327
|
+
* @returns {string|null} Rendered string, or null if action is not simple
|
|
328
|
+
*/
|
|
329
|
+
function renderSeekSimple(payload) {
|
|
330
|
+
if (payload.action === 'clear-cache') {
|
|
331
|
+
return `${payload.message}\n`;
|
|
332
|
+
}
|
|
333
|
+
if (payload.action === 'drop') {
|
|
334
|
+
return `Dropped cursor "${payload.name}" (was at tick ${payload.tick}).\n`;
|
|
335
|
+
}
|
|
336
|
+
if (payload.action === 'save') {
|
|
337
|
+
return `Saved cursor "${payload.name}" at tick ${payload.tick}.\n`;
|
|
338
|
+
}
|
|
339
|
+
if (payload.action === 'list') {
|
|
340
|
+
return renderSeekList(payload);
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Renders the cursor list action.
|
|
347
|
+
* @param {*} payload
|
|
348
|
+
* @returns {string}
|
|
349
|
+
*/
|
|
350
|
+
function renderSeekList(payload) {
|
|
351
|
+
if (payload.cursors.length === 0) {
|
|
352
|
+
return 'No saved cursors.\n';
|
|
353
|
+
}
|
|
354
|
+
const lines = [];
|
|
355
|
+
for (const c of payload.cursors) {
|
|
356
|
+
const active = c.tick === payload.activeTick ? ' (active)' : '';
|
|
357
|
+
lines.push(` ${c.name}: tick ${c.tick}${active}`);
|
|
358
|
+
}
|
|
359
|
+
return `${lines.join('\n')}\n`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── Seek state-action renderer ───────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Renders seek actions that show state: latest, load, tick, status.
|
|
366
|
+
* @param {*} payload
|
|
367
|
+
* @returns {string}
|
|
368
|
+
*/
|
|
369
|
+
function renderSeekState(payload) {
|
|
370
|
+
if (payload.action === 'latest') {
|
|
371
|
+
const { nodesStr, edgesStr } = buildStateStrings(payload);
|
|
372
|
+
return renderSeekWithDiff(
|
|
373
|
+
payload,
|
|
374
|
+
`${payload.graph}: returned to present (tick ${payload.maxTick}, ${nodesStr}, ${edgesStr})`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
if (payload.action === 'load') {
|
|
378
|
+
const { nodesStr, edgesStr } = buildStateStrings(payload);
|
|
379
|
+
return renderSeekWithDiff(
|
|
380
|
+
payload,
|
|
381
|
+
`${payload.graph}: loaded cursor "${payload.name}" at tick ${payload.tick} of ${payload.maxTick} (${nodesStr}, ${edgesStr})`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (payload.action === 'tick') {
|
|
385
|
+
const { nodesStr, edgesStr, patchesStr } = buildStateStrings(payload);
|
|
386
|
+
return renderSeekWithDiff(
|
|
387
|
+
payload,
|
|
388
|
+
`${payload.graph}: tick ${payload.tick} of ${payload.maxTick} (${nodesStr}, ${edgesStr}, ${patchesStr})`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
// status (structuralDiff is never populated here; no formatStructuralDiff call)
|
|
392
|
+
if (payload.cursor && payload.cursor.active) {
|
|
393
|
+
const { nodesStr, edgesStr, patchesStr } = buildStateStrings(payload);
|
|
394
|
+
return appendReceiptSummary(
|
|
395
|
+
`${payload.graph}: tick ${payload.tick} of ${payload.maxTick} (${nodesStr}, ${edgesStr}, ${patchesStr})`,
|
|
396
|
+
payload,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
return `${payload.graph}: no cursor active, ${payload.ticks.length} ticks available\n`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── Seek main renderer ──────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
/** @param {*} payload */
|
|
405
|
+
export function renderSeek(payload) {
|
|
406
|
+
return renderSeekSimple(payload) ?? renderSeekState(payload);
|
|
407
|
+
}
|