@git-stunts/git-warp 10.4.2 → 10.7.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/warp-graph.js +205 -69
- package/index.d.ts +24 -0
- package/package.json +1 -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
|
@@ -17,7 +17,26 @@ import { formatOpSummary } from './opSummary.js';
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @typedef {{ ticks: number[], tipSha?: string, tickShas?: Record<number, string> }} WriterInfo
|
|
20
|
-
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} SeekPayload
|
|
24
|
+
* @property {string} graph
|
|
25
|
+
* @property {number} tick
|
|
26
|
+
* @property {number} maxTick
|
|
27
|
+
* @property {number[]} ticks
|
|
28
|
+
* @property {number} nodes
|
|
29
|
+
* @property {number} edges
|
|
30
|
+
* @property {number} patchCount
|
|
31
|
+
* @property {Map<string, WriterInfo> | Record<string, WriterInfo>} perWriter
|
|
32
|
+
* @property {{ nodes?: number, edges?: number }} [diff]
|
|
33
|
+
* @property {Record<string, any>} [tickReceipt]
|
|
34
|
+
* @property {import('../../../domain/services/StateDiff.js').StateDiffResult | null} [structuralDiff]
|
|
35
|
+
* @property {string} [diffBaseline]
|
|
36
|
+
* @property {number | null} [baselineTick]
|
|
37
|
+
* @property {boolean} [truncated]
|
|
38
|
+
* @property {number} [totalChanges]
|
|
39
|
+
* @property {number} [shownChanges]
|
|
21
40
|
*/
|
|
22
41
|
|
|
23
42
|
/** Maximum number of tick columns shown in the windowed view. */
|
|
@@ -265,14 +284,45 @@ function buildTickPoints(ticks, tick) {
|
|
|
265
284
|
return { allPoints, currentIdx };
|
|
266
285
|
}
|
|
267
286
|
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// Structural Diff
|
|
289
|
+
// ============================================================================
|
|
290
|
+
|
|
291
|
+
/** Maximum structural diff lines shown in ASCII view. */
|
|
292
|
+
const MAX_DIFF_LINES = 20;
|
|
293
|
+
|
|
268
294
|
/**
|
|
269
|
-
* Builds the
|
|
270
|
-
*
|
|
271
|
-
* @
|
|
272
|
-
* @returns {string[]} Lines for the box body
|
|
295
|
+
* Builds the state summary, receipt, and structural diff footer lines.
|
|
296
|
+
* @param {SeekPayload} payload
|
|
297
|
+
* @returns {string[]}
|
|
273
298
|
*/
|
|
299
|
+
function buildFooterLines(payload) {
|
|
300
|
+
const { tick, nodes, edges, patchCount, diff, tickReceipt } = payload;
|
|
301
|
+
const lines = [];
|
|
302
|
+
lines.push('');
|
|
303
|
+
const nodesStr = `${nodes} ${pluralize(nodes, 'node', 'nodes')}${formatDelta(diff?.nodes ?? 0)}`;
|
|
304
|
+
const edgesStr = `${edges} ${pluralize(edges, 'edge', 'edges')}${formatDelta(diff?.edges ?? 0)}`;
|
|
305
|
+
lines.push(` ${colors.bold('State:')} ${nodesStr}, ${edgesStr}, ${patchCount} ${pluralize(patchCount, 'patch', 'patches')}`);
|
|
306
|
+
|
|
307
|
+
const receiptLines = buildReceiptLines(tickReceipt);
|
|
308
|
+
if (receiptLines.length > 0) {
|
|
309
|
+
lines.push('');
|
|
310
|
+
lines.push(` ${colors.bold(`Tick ${tick}:`)}`);
|
|
311
|
+
lines.push(...receiptLines);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const sdLines = buildStructuralDiffLines(payload, MAX_DIFF_LINES);
|
|
315
|
+
if (sdLines.length > 0) {
|
|
316
|
+
lines.push('');
|
|
317
|
+
lines.push(...sdLines);
|
|
318
|
+
}
|
|
319
|
+
lines.push('');
|
|
320
|
+
return lines;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** @param {SeekPayload} payload @returns {string[]} */
|
|
274
324
|
function buildSeekBodyLines(payload) {
|
|
275
|
-
const { graph, tick, maxTick, ticks,
|
|
325
|
+
const { graph, tick, maxTick, ticks, perWriter } = payload;
|
|
276
326
|
const lines = [];
|
|
277
327
|
|
|
278
328
|
lines.push('');
|
|
@@ -285,11 +335,8 @@ function buildSeekBodyLines(payload) {
|
|
|
285
335
|
} else {
|
|
286
336
|
const { allPoints, currentIdx } = buildTickPoints(ticks, tick);
|
|
287
337
|
const win = computeWindow(allPoints, currentIdx);
|
|
288
|
-
|
|
289
|
-
// Column headers with relative offsets
|
|
290
338
|
lines.push(buildHeaderRow(win));
|
|
291
339
|
|
|
292
|
-
// Per-writer swimlanes
|
|
293
340
|
/** @type {Array<[string, WriterInfo]>} */
|
|
294
341
|
const writerEntries = perWriter instanceof Map
|
|
295
342
|
? [...perWriter.entries()]
|
|
@@ -300,30 +347,133 @@ function buildSeekBodyLines(payload) {
|
|
|
300
347
|
}
|
|
301
348
|
}
|
|
302
349
|
|
|
303
|
-
lines.push(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const patchLabel = pluralize(patchCount, 'patch', 'patches');
|
|
350
|
+
lines.push(...buildFooterLines(payload));
|
|
351
|
+
return lines;
|
|
352
|
+
}
|
|
307
353
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Builds a truncation hint line when entries exceed the display or data limit.
|
|
356
|
+
* @param {{totalEntries: number, shown: number, maxLines: number, truncated?: boolean, totalChanges?: number, shownChanges?: number}} opts
|
|
357
|
+
* @returns {string|null}
|
|
358
|
+
*/
|
|
359
|
+
function buildTruncationHint(opts) {
|
|
360
|
+
const { totalEntries, shown, maxLines, truncated, totalChanges, shownChanges } = opts;
|
|
361
|
+
if (totalEntries > maxLines && truncated) {
|
|
362
|
+
const remaining = Math.max(0, (totalChanges || 0) - shown);
|
|
363
|
+
return `... and ${remaining} more changes (${totalChanges} total, use --diff-limit to increase)`;
|
|
364
|
+
}
|
|
365
|
+
if (totalEntries > maxLines) {
|
|
366
|
+
return `... and ${Math.max(0, totalEntries - maxLines)} more changes`;
|
|
367
|
+
}
|
|
368
|
+
if (truncated) {
|
|
369
|
+
return `... and ${Math.max(0, (totalChanges || 0) - (shownChanges || 0))} more changes (use --diff-limit to increase)`;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
311
373
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
374
|
+
/**
|
|
375
|
+
* @param {SeekPayload} payload
|
|
376
|
+
* @param {number} maxLines
|
|
377
|
+
* @returns {string[]}
|
|
378
|
+
*/
|
|
379
|
+
function buildStructuralDiffLines(payload, maxLines) {
|
|
380
|
+
const { structuralDiff, diffBaseline, baselineTick, truncated, totalChanges, shownChanges } = payload;
|
|
381
|
+
if (!structuralDiff) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const lines = [];
|
|
386
|
+
const baselineLabel = diffBaseline === 'tick'
|
|
387
|
+
? `baseline: tick ${baselineTick}`
|
|
388
|
+
: 'baseline: empty';
|
|
389
|
+
|
|
390
|
+
lines.push(` ${colors.bold(`Changes (${baselineLabel}):`)}`);
|
|
391
|
+
|
|
392
|
+
let shown = 0;
|
|
393
|
+
const entries = collectDiffEntries(structuralDiff);
|
|
394
|
+
|
|
395
|
+
for (const entry of entries) {
|
|
396
|
+
if (shown >= maxLines) {
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
lines.push(` ${entry}`);
|
|
400
|
+
shown++;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const hint = buildTruncationHint({ totalEntries: entries.length, shown, maxLines, truncated, totalChanges, shownChanges });
|
|
404
|
+
if (hint) {
|
|
405
|
+
lines.push(` ${colors.muted(hint)}`);
|
|
317
406
|
}
|
|
318
|
-
lines.push('');
|
|
319
407
|
|
|
320
408
|
return lines;
|
|
321
409
|
}
|
|
322
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Collects formatted diff entries from a structural diff result.
|
|
413
|
+
*
|
|
414
|
+
* @param {import('../../../domain/services/StateDiff.js').StateDiffResult} diff
|
|
415
|
+
* @returns {string[]} Formatted entries with +/-/~ prefixes
|
|
416
|
+
*/
|
|
417
|
+
function collectDiffEntries(diff) {
|
|
418
|
+
const entries = [];
|
|
419
|
+
|
|
420
|
+
for (const nodeId of diff.nodes.added) {
|
|
421
|
+
entries.push(colors.success(`+ node ${nodeId}`));
|
|
422
|
+
}
|
|
423
|
+
for (const nodeId of diff.nodes.removed) {
|
|
424
|
+
entries.push(colors.error(`- node ${nodeId}`));
|
|
425
|
+
}
|
|
426
|
+
for (const edge of diff.edges.added) {
|
|
427
|
+
entries.push(colors.success(`+ edge ${edge.from} -[${edge.label}]-> ${edge.to}`));
|
|
428
|
+
}
|
|
429
|
+
for (const edge of diff.edges.removed) {
|
|
430
|
+
entries.push(colors.error(`- edge ${edge.from} -[${edge.label}]-> ${edge.to}`));
|
|
431
|
+
}
|
|
432
|
+
for (const prop of diff.props.set) {
|
|
433
|
+
const old = prop.oldValue !== undefined ? formatPropValue(prop.oldValue) : null;
|
|
434
|
+
const arrow = old !== null ? `${old} -> ${formatPropValue(prop.newValue)}` : formatPropValue(prop.newValue);
|
|
435
|
+
entries.push(colors.warning(`~ ${prop.nodeId}.${prop.propKey}: ${arrow}`));
|
|
436
|
+
}
|
|
437
|
+
for (const prop of diff.props.removed) {
|
|
438
|
+
entries.push(colors.error(`- ${prop.nodeId}.${prop.propKey}: ${formatPropValue(prop.oldValue)}`));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return entries;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Formats a property value for display (truncated if too long).
|
|
446
|
+
* @param {*} value
|
|
447
|
+
* @returns {string}
|
|
448
|
+
*/
|
|
449
|
+
function formatPropValue(value) {
|
|
450
|
+
if (value === undefined) {
|
|
451
|
+
return 'undefined';
|
|
452
|
+
}
|
|
453
|
+
const s = typeof value === 'string' ? `"${value}"` : String(value);
|
|
454
|
+
return s.length > 40 ? `${s.slice(0, 37)}...` : s;
|
|
455
|
+
}
|
|
456
|
+
|
|
323
457
|
// ============================================================================
|
|
324
458
|
// Public API
|
|
325
459
|
// ============================================================================
|
|
326
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Formats a structural diff as a plain-text string (no boxen).
|
|
463
|
+
*
|
|
464
|
+
* Used by the non-view renderSeek() path in the CLI.
|
|
465
|
+
*
|
|
466
|
+
* @param {SeekPayload} payload - Seek payload containing structuralDiff
|
|
467
|
+
* @returns {string} Formatted diff section, or empty string if no diff
|
|
468
|
+
*/
|
|
469
|
+
export function formatStructuralDiff(payload) {
|
|
470
|
+
const lines = buildStructuralDiffLines(payload, MAX_DIFF_LINES);
|
|
471
|
+
if (lines.length === 0) {
|
|
472
|
+
return '';
|
|
473
|
+
}
|
|
474
|
+
return `${lines.join('\n')}\n`;
|
|
475
|
+
}
|
|
476
|
+
|
|
327
477
|
/**
|
|
328
478
|
* Renders the seek view dashboard inside a double-bordered box.
|
|
329
479
|
*
|