@git-stunts/git-warp 10.1.1
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/LICENSE +201 -0
- package/NOTICE +16 -0
- package/README.md +480 -0
- package/SECURITY.md +30 -0
- package/bin/git-warp +24 -0
- package/bin/warp-graph.js +1574 -0
- package/index.d.ts +2366 -0
- package/index.js +180 -0
- package/package.json +129 -0
- package/scripts/install-git-warp.sh +258 -0
- package/scripts/uninstall-git-warp.sh +139 -0
- package/src/domain/WarpGraph.js +3157 -0
- package/src/domain/crdt/Dot.js +160 -0
- package/src/domain/crdt/LWW.js +154 -0
- package/src/domain/crdt/ORSet.js +371 -0
- package/src/domain/crdt/VersionVector.js +222 -0
- package/src/domain/entities/GraphNode.js +60 -0
- package/src/domain/errors/EmptyMessageError.js +47 -0
- package/src/domain/errors/ForkError.js +30 -0
- package/src/domain/errors/IndexError.js +23 -0
- package/src/domain/errors/OperationAbortedError.js +22 -0
- package/src/domain/errors/QueryError.js +39 -0
- package/src/domain/errors/SchemaUnsupportedError.js +17 -0
- package/src/domain/errors/ShardCorruptionError.js +56 -0
- package/src/domain/errors/ShardLoadError.js +57 -0
- package/src/domain/errors/ShardValidationError.js +61 -0
- package/src/domain/errors/StorageError.js +57 -0
- package/src/domain/errors/SyncError.js +30 -0
- package/src/domain/errors/TraversalError.js +23 -0
- package/src/domain/errors/WarpError.js +31 -0
- package/src/domain/errors/WormholeError.js +28 -0
- package/src/domain/errors/WriterError.js +39 -0
- package/src/domain/errors/index.js +21 -0
- package/src/domain/services/AnchorMessageCodec.js +99 -0
- package/src/domain/services/BitmapIndexBuilder.js +225 -0
- package/src/domain/services/BitmapIndexReader.js +435 -0
- package/src/domain/services/BoundaryTransitionRecord.js +463 -0
- package/src/domain/services/CheckpointMessageCodec.js +147 -0
- package/src/domain/services/CheckpointSerializerV5.js +281 -0
- package/src/domain/services/CheckpointService.js +384 -0
- package/src/domain/services/CommitDagTraversalService.js +156 -0
- package/src/domain/services/DagPathFinding.js +712 -0
- package/src/domain/services/DagTopology.js +239 -0
- package/src/domain/services/DagTraversal.js +245 -0
- package/src/domain/services/Frontier.js +108 -0
- package/src/domain/services/GCMetrics.js +101 -0
- package/src/domain/services/GCPolicy.js +122 -0
- package/src/domain/services/GitLogParser.js +205 -0
- package/src/domain/services/HealthCheckService.js +246 -0
- package/src/domain/services/HookInstaller.js +326 -0
- package/src/domain/services/HttpSyncServer.js +262 -0
- package/src/domain/services/IndexRebuildService.js +426 -0
- package/src/domain/services/IndexStalenessChecker.js +103 -0
- package/src/domain/services/JoinReducer.js +582 -0
- package/src/domain/services/KeyCodec.js +113 -0
- package/src/domain/services/LegacyAnchorDetector.js +67 -0
- package/src/domain/services/LogicalTraversal.js +351 -0
- package/src/domain/services/MessageCodecInternal.js +132 -0
- package/src/domain/services/MessageSchemaDetector.js +145 -0
- package/src/domain/services/MigrationService.js +55 -0
- package/src/domain/services/ObserverView.js +265 -0
- package/src/domain/services/PatchBuilderV2.js +669 -0
- package/src/domain/services/PatchMessageCodec.js +140 -0
- package/src/domain/services/ProvenanceIndex.js +337 -0
- package/src/domain/services/ProvenancePayload.js +242 -0
- package/src/domain/services/QueryBuilder.js +835 -0
- package/src/domain/services/StateDiff.js +300 -0
- package/src/domain/services/StateSerializerV5.js +156 -0
- package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
- package/src/domain/services/SyncProtocol.js +593 -0
- package/src/domain/services/TemporalQuery.js +201 -0
- package/src/domain/services/TranslationCost.js +221 -0
- package/src/domain/services/TraversalService.js +8 -0
- package/src/domain/services/WarpMessageCodec.js +29 -0
- package/src/domain/services/WarpStateIndexBuilder.js +127 -0
- package/src/domain/services/WormholeService.js +353 -0
- package/src/domain/types/TickReceipt.js +285 -0
- package/src/domain/types/WarpTypes.js +209 -0
- package/src/domain/types/WarpTypesV2.js +200 -0
- package/src/domain/utils/CachedValue.js +140 -0
- package/src/domain/utils/EventId.js +89 -0
- package/src/domain/utils/LRUCache.js +112 -0
- package/src/domain/utils/MinHeap.js +114 -0
- package/src/domain/utils/RefLayout.js +280 -0
- package/src/domain/utils/WriterId.js +205 -0
- package/src/domain/utils/cancellation.js +33 -0
- package/src/domain/utils/canonicalStringify.js +42 -0
- package/src/domain/utils/defaultClock.js +20 -0
- package/src/domain/utils/defaultCodec.js +51 -0
- package/src/domain/utils/nullLogger.js +21 -0
- package/src/domain/utils/roaring.js +181 -0
- package/src/domain/utils/shardVersion.js +9 -0
- package/src/domain/warp/PatchSession.js +217 -0
- package/src/domain/warp/Writer.js +181 -0
- package/src/hooks/post-merge.sh +60 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
- package/src/infrastructure/adapters/ClockAdapter.js +57 -0
- package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
- package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
- package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
- package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
- package/src/infrastructure/adapters/NoOpLogger.js +62 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
- package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
- package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
- package/src/infrastructure/codecs/CborCodec.js +384 -0
- package/src/ports/BlobPort.js +30 -0
- package/src/ports/ClockPort.js +25 -0
- package/src/ports/CodecPort.js +25 -0
- package/src/ports/CommitPort.js +114 -0
- package/src/ports/ConfigPort.js +31 -0
- package/src/ports/CryptoPort.js +38 -0
- package/src/ports/GraphPersistencePort.js +57 -0
- package/src/ports/HttpServerPort.js +25 -0
- package/src/ports/IndexStoragePort.js +39 -0
- package/src/ports/LoggerPort.js +68 -0
- package/src/ports/RefPort.js +51 -0
- package/src/ports/TreePort.js +51 -0
- package/src/visualization/index.js +26 -0
- package/src/visualization/layouts/converters.js +75 -0
- package/src/visualization/layouts/elkAdapter.js +86 -0
- package/src/visualization/layouts/elkLayout.js +95 -0
- package/src/visualization/layouts/index.js +29 -0
- package/src/visualization/renderers/ascii/box.js +16 -0
- package/src/visualization/renderers/ascii/check.js +271 -0
- package/src/visualization/renderers/ascii/colors.js +13 -0
- package/src/visualization/renderers/ascii/formatters.js +73 -0
- package/src/visualization/renderers/ascii/graph.js +344 -0
- package/src/visualization/renderers/ascii/history.js +335 -0
- package/src/visualization/renderers/ascii/index.js +14 -0
- package/src/visualization/renderers/ascii/info.js +245 -0
- package/src/visualization/renderers/ascii/materialize.js +255 -0
- package/src/visualization/renderers/ascii/path.js +240 -0
- package/src/visualization/renderers/ascii/progress.js +32 -0
- package/src/visualization/renderers/ascii/symbols.js +33 -0
- package/src/visualization/renderers/ascii/table.js +19 -0
- package/src/visualization/renderers/browser/index.js +1 -0
- package/src/visualization/renderers/svg/index.js +159 -0
- package/src/visualization/utils/ansi.js +14 -0
- package/src/visualization/utils/time.js +40 -0
- package/src/visualization/utils/truncate.js +40 -0
- package/src/visualization/utils/unicode.js +52 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII renderer for the `history --view` command.
|
|
3
|
+
* Displays a visual timeline of patches for one or more writers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { colors } from './colors.js';
|
|
7
|
+
import { createBox } from './box.js';
|
|
8
|
+
import { padRight, padLeft } from '../../utils/unicode.js';
|
|
9
|
+
import { truncate } from '../../utils/truncate.js';
|
|
10
|
+
import { TIMELINE } from './symbols.js';
|
|
11
|
+
|
|
12
|
+
// Default pagination settings
|
|
13
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
14
|
+
|
|
15
|
+
// Operation type to display info mapping
|
|
16
|
+
const OP_DISPLAY = {
|
|
17
|
+
NodeAdd: { symbol: '+', label: 'node', color: colors.success },
|
|
18
|
+
NodeTombstone: { symbol: '-', label: 'node', color: colors.error },
|
|
19
|
+
EdgeAdd: { symbol: '+', label: 'edge', color: colors.success },
|
|
20
|
+
EdgeTombstone: { symbol: '-', label: 'edge', color: colors.error },
|
|
21
|
+
PropSet: { symbol: '~', label: 'prop', color: colors.warning },
|
|
22
|
+
BlobValue: { symbol: '+', label: 'blob', color: colors.primary },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Default empty operation summary
|
|
26
|
+
const EMPTY_OP_SUMMARY = Object.freeze({
|
|
27
|
+
NodeAdd: 0,
|
|
28
|
+
EdgeAdd: 0,
|
|
29
|
+
PropSet: 0,
|
|
30
|
+
NodeTombstone: 0,
|
|
31
|
+
EdgeTombstone: 0,
|
|
32
|
+
BlobValue: 0,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Summarizes operations in a patch.
|
|
37
|
+
* @param {Object[]} ops - Array of patch operations
|
|
38
|
+
* @returns {Object} Summary with counts by operation type
|
|
39
|
+
*/
|
|
40
|
+
function summarizeOps(ops) {
|
|
41
|
+
const summary = { ...EMPTY_OP_SUMMARY };
|
|
42
|
+
for (const op of ops) {
|
|
43
|
+
if (op.type && summary[op.type] !== undefined) {
|
|
44
|
+
summary[op.type]++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return summary;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Formats operation summary as a colored string.
|
|
52
|
+
* @param {Object} summary - Operation counts by type
|
|
53
|
+
* @param {number} maxWidth - Maximum width for the summary string
|
|
54
|
+
* @returns {string} Formatted summary string
|
|
55
|
+
*/
|
|
56
|
+
function formatOpSummary(summary, maxWidth = 40) {
|
|
57
|
+
const order = ['NodeAdd', 'EdgeAdd', 'PropSet', 'NodeTombstone', 'EdgeTombstone', 'BlobValue'];
|
|
58
|
+
const parts = order
|
|
59
|
+
.filter((opType) => summary[opType] > 0)
|
|
60
|
+
.map((opType) => {
|
|
61
|
+
const display = OP_DISPLAY[opType];
|
|
62
|
+
return { text: `${display.symbol}${summary[opType]}${display.label}`, color: display.color };
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (parts.length === 0) {
|
|
66
|
+
return colors.muted('(empty)');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Truncate plain text first to avoid breaking ANSI escape sequences
|
|
70
|
+
const plain = parts.map((p) => p.text).join(' ');
|
|
71
|
+
const truncated = truncate(plain, maxWidth);
|
|
72
|
+
if (truncated === plain) {
|
|
73
|
+
return parts.map((p) => p.color(p.text)).join(' ');
|
|
74
|
+
}
|
|
75
|
+
return colors.muted(truncated);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Ensures entry has an opSummary, computing one if needed.
|
|
80
|
+
* @param {Object} entry - Patch entry
|
|
81
|
+
* @returns {Object} Operation summary
|
|
82
|
+
*/
|
|
83
|
+
function ensureOpSummary(entry) {
|
|
84
|
+
if (entry.opSummary) {
|
|
85
|
+
return entry.opSummary;
|
|
86
|
+
}
|
|
87
|
+
if (entry.ops) {
|
|
88
|
+
return summarizeOps(entry.ops);
|
|
89
|
+
}
|
|
90
|
+
return { ...EMPTY_OP_SUMMARY };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Paginates entries, returning display entries and truncation info.
|
|
95
|
+
* @param {Object[]} entries - All entries
|
|
96
|
+
* @param {number} pageSize - Page size
|
|
97
|
+
* @param {boolean} showAll - Whether to show all
|
|
98
|
+
* @returns {{displayEntries: Object[], truncated: boolean, hiddenCount: number}}
|
|
99
|
+
*/
|
|
100
|
+
function paginateEntries(entries, pageSize, showAll) {
|
|
101
|
+
if (showAll || entries.length <= pageSize) {
|
|
102
|
+
return { displayEntries: entries, truncated: false, hiddenCount: 0 };
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
displayEntries: entries.slice(-pageSize),
|
|
106
|
+
truncated: true,
|
|
107
|
+
hiddenCount: entries.length - pageSize,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Renders the truncation indicator at the top of the timeline.
|
|
113
|
+
* @param {boolean} truncated - Whether entries are truncated
|
|
114
|
+
* @param {number} hiddenCount - Number of hidden entries
|
|
115
|
+
* @returns {string[]} Lines to prepend
|
|
116
|
+
*/
|
|
117
|
+
function renderTruncationIndicator(truncated, hiddenCount) {
|
|
118
|
+
if (truncated) {
|
|
119
|
+
return [
|
|
120
|
+
colors.muted(` ${TIMELINE.top}${TIMELINE.vertical} ... ${hiddenCount} older patches hidden`),
|
|
121
|
+
` ${TIMELINE.vertical}`,
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
return [` ${TIMELINE.top}`];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Renders a single patch entry line.
|
|
129
|
+
* @param {Object} params - Entry parameters
|
|
130
|
+
* @returns {string} Formatted entry line
|
|
131
|
+
*/
|
|
132
|
+
function renderEntryLine({ entry, isLast, lamportWidth, writerStr, maxWriterIdLen }) {
|
|
133
|
+
const connector = isLast ? TIMELINE.end : TIMELINE.connector;
|
|
134
|
+
const shortSha = (entry.sha || '').slice(0, 7);
|
|
135
|
+
const lamportStr = padLeft(String(entry.lamport), lamportWidth);
|
|
136
|
+
const opSummary = ensureOpSummary(entry);
|
|
137
|
+
const opSummaryStr = formatOpSummary(opSummary, writerStr ? 30 : 40);
|
|
138
|
+
|
|
139
|
+
if (writerStr) {
|
|
140
|
+
const paddedWriter = padRight(writerStr, maxWriterIdLen);
|
|
141
|
+
return ` ${connector}${TIMELINE.dot} ${colors.muted(`L${lamportStr}`)} ${colors.primary(paddedWriter)}:${colors.muted(shortSha)} ${opSummaryStr}`;
|
|
142
|
+
}
|
|
143
|
+
return ` ${connector}${TIMELINE.dot} ${colors.muted(`L${lamportStr}`)} ${colors.primary(shortSha)} ${opSummaryStr}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Renders single-writer timeline header.
|
|
148
|
+
* @param {string} writer - Writer ID
|
|
149
|
+
* @returns {string[]} Header lines
|
|
150
|
+
*/
|
|
151
|
+
function renderSingleWriterHeader(writer) {
|
|
152
|
+
return [colors.bold(` WRITER: ${writer}`), ''];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Renders single-writer timeline footer.
|
|
157
|
+
* @param {number} totalCount - Total entry count
|
|
158
|
+
* @returns {string[]} Footer lines
|
|
159
|
+
*/
|
|
160
|
+
function renderSingleWriterFooter(totalCount) {
|
|
161
|
+
const label = totalCount === 1 ? 'patch' : 'patches';
|
|
162
|
+
return ['', colors.muted(` Total: ${totalCount} ${label}`)];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Renders single-writer timeline view.
|
|
167
|
+
* @param {Object} payload - History payload
|
|
168
|
+
* @param {Object} options - Rendering options
|
|
169
|
+
* @returns {string[]} Lines for the timeline
|
|
170
|
+
*/
|
|
171
|
+
function renderSingleWriterTimeline(payload, options) {
|
|
172
|
+
const { entries, writer } = payload;
|
|
173
|
+
const { pageSize = DEFAULT_PAGE_SIZE, showAll = false } = options;
|
|
174
|
+
|
|
175
|
+
const lines = renderSingleWriterHeader(writer);
|
|
176
|
+
|
|
177
|
+
if (entries.length === 0) {
|
|
178
|
+
lines.push(colors.muted(' (no patches)'));
|
|
179
|
+
return lines;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { displayEntries, truncated, hiddenCount } = paginateEntries(entries, pageSize, showAll);
|
|
183
|
+
if (displayEntries.length === 0) {
|
|
184
|
+
lines.push(colors.muted(' (no patches)'));
|
|
185
|
+
return lines;
|
|
186
|
+
}
|
|
187
|
+
const maxLamport = Math.max(...displayEntries.map((e) => e.lamport));
|
|
188
|
+
const lamportWidth = String(maxLamport).length;
|
|
189
|
+
|
|
190
|
+
lines.push(...renderTruncationIndicator(truncated, hiddenCount));
|
|
191
|
+
|
|
192
|
+
for (let i = 0; i < displayEntries.length; i++) {
|
|
193
|
+
const isLast = i === displayEntries.length - 1;
|
|
194
|
+
lines.push(renderEntryLine({ entry: displayEntries[i], isLast, lamportWidth }));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
lines.push(...renderSingleWriterFooter(entries.length));
|
|
198
|
+
return lines;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Merges and sorts entries from all writers by lamport timestamp.
|
|
203
|
+
* @param {Object} writers - Map of writerId to entries
|
|
204
|
+
* @returns {Object[]} Sorted entries with writerId attached
|
|
205
|
+
*/
|
|
206
|
+
function mergeWriterEntries(writers) {
|
|
207
|
+
const allEntries = [];
|
|
208
|
+
for (const [writerId, writerEntries] of Object.entries(writers)) {
|
|
209
|
+
for (const entry of writerEntries) {
|
|
210
|
+
allEntries.push({ ...entry, writerId });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
allEntries.sort((a, b) => a.lamport - b.lamport || a.writerId.localeCompare(b.writerId));
|
|
214
|
+
return allEntries;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Renders multi-writer timeline header.
|
|
219
|
+
* @param {string} graph - Graph name
|
|
220
|
+
* @param {number} writerCount - Number of writers
|
|
221
|
+
* @returns {string[]} Header lines
|
|
222
|
+
*/
|
|
223
|
+
function renderMultiWriterHeader(graph, writerCount) {
|
|
224
|
+
return [
|
|
225
|
+
colors.bold(` GRAPH: ${graph}`),
|
|
226
|
+
colors.muted(` Writers: ${writerCount}`),
|
|
227
|
+
'',
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Renders multi-writer timeline footer.
|
|
233
|
+
* @param {number} totalCount - Total entry count
|
|
234
|
+
* @param {number} writerCount - Number of writers
|
|
235
|
+
* @returns {string[]} Footer lines
|
|
236
|
+
*/
|
|
237
|
+
function renderMultiWriterFooter(totalCount, writerCount) {
|
|
238
|
+
const label = totalCount === 1 ? 'patch' : 'patches';
|
|
239
|
+
return ['', colors.muted(` Total: ${totalCount} ${label} across ${writerCount} writers`)];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Renders multi-writer timeline view with parallel columns.
|
|
244
|
+
* @param {Object} payload - History payload with allWriters data
|
|
245
|
+
* @param {Object} options - Rendering options
|
|
246
|
+
* @returns {string[]} Lines for the timeline
|
|
247
|
+
*/
|
|
248
|
+
function renderMultiWriterTimeline(payload, options) {
|
|
249
|
+
const { writers, graph } = payload;
|
|
250
|
+
const { pageSize = DEFAULT_PAGE_SIZE, showAll = false } = options;
|
|
251
|
+
const writerIds = Object.keys(writers);
|
|
252
|
+
|
|
253
|
+
const lines = renderMultiWriterHeader(graph, writerIds.length);
|
|
254
|
+
|
|
255
|
+
if (writerIds.length === 0) {
|
|
256
|
+
lines.push(colors.muted(' (no writers)'));
|
|
257
|
+
return lines;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const allEntries = mergeWriterEntries(writers);
|
|
261
|
+
|
|
262
|
+
if (allEntries.length === 0) {
|
|
263
|
+
lines.push(colors.muted(' (no patches)'));
|
|
264
|
+
return lines;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const { displayEntries, truncated, hiddenCount } = paginateEntries(allEntries, pageSize, showAll);
|
|
268
|
+
if (displayEntries.length === 0) {
|
|
269
|
+
lines.push(colors.muted(' (no patches)'));
|
|
270
|
+
return lines;
|
|
271
|
+
}
|
|
272
|
+
const maxLamport = Math.max(...displayEntries.map((e) => e.lamport));
|
|
273
|
+
const lamportWidth = String(maxLamport).length;
|
|
274
|
+
const maxWriterIdLen = Math.max(...writerIds.map((id) => id.length), 6);
|
|
275
|
+
|
|
276
|
+
lines.push(...renderTruncationIndicator(truncated, hiddenCount));
|
|
277
|
+
|
|
278
|
+
for (let i = 0; i < displayEntries.length; i++) {
|
|
279
|
+
const entry = displayEntries[i];
|
|
280
|
+
const isLast = i === displayEntries.length - 1;
|
|
281
|
+
lines.push(renderEntryLine({
|
|
282
|
+
entry,
|
|
283
|
+
isLast,
|
|
284
|
+
lamportWidth,
|
|
285
|
+
writerStr: entry.writerId,
|
|
286
|
+
maxWriterIdLen,
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
lines.push(...renderMultiWriterFooter(allEntries.length, writerIds.length));
|
|
291
|
+
return lines;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Renders the history view with ASCII timeline.
|
|
296
|
+
* @param {Object} payload - History payload from handleHistory
|
|
297
|
+
* @param {string} payload.graph - Graph name
|
|
298
|
+
* @param {string} [payload.writer] - Writer ID (single writer mode)
|
|
299
|
+
* @param {string|null} [payload.nodeFilter] - Node filter if applied
|
|
300
|
+
* @param {Object[]} [payload.entries] - Array of patch entries (single writer mode)
|
|
301
|
+
* @param {Object} [payload.writers] - Map of writerId to entries (multi-writer mode)
|
|
302
|
+
* @param {Object} [options] - Rendering options
|
|
303
|
+
* @param {number} [options.pageSize=20] - Number of patches to show per page
|
|
304
|
+
* @param {boolean} [options.showAll=false] - Show all patches (no pagination)
|
|
305
|
+
* @returns {string} Formatted ASCII output
|
|
306
|
+
*/
|
|
307
|
+
export function renderHistoryView(payload, options = {}) {
|
|
308
|
+
if (!payload) {
|
|
309
|
+
return `${colors.error('No data available')}\n`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const isMultiWriter = payload.writers && typeof payload.writers === 'object';
|
|
313
|
+
const contentLines = isMultiWriter
|
|
314
|
+
? renderMultiWriterTimeline(payload, options)
|
|
315
|
+
: renderSingleWriterTimeline(payload, options);
|
|
316
|
+
|
|
317
|
+
// Add node filter indicator if present
|
|
318
|
+
if (payload.nodeFilter) {
|
|
319
|
+
contentLines.splice(1, 0, colors.muted(` Filter: node=${payload.nodeFilter}`));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const content = contentLines.join('\n');
|
|
323
|
+
|
|
324
|
+
const box = createBox(content, {
|
|
325
|
+
title: 'PATCH HISTORY',
|
|
326
|
+
titleAlignment: 'center',
|
|
327
|
+
borderColor: 'cyan',
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return `${box}\n`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export { summarizeOps };
|
|
334
|
+
|
|
335
|
+
export default { renderHistoryView, summarizeOps };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII renderer exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { colors, default as colorsDefault } from './colors.js';
|
|
6
|
+
export { createBox } from './box.js';
|
|
7
|
+
export { createTable } from './table.js';
|
|
8
|
+
export { progressBar } from './progress.js';
|
|
9
|
+
export { renderInfoView } from './info.js';
|
|
10
|
+
export { renderCheckView } from './check.js';
|
|
11
|
+
export { renderMaterializeView } from './materialize.js';
|
|
12
|
+
export { renderHistoryView, summarizeOps } from './history.js';
|
|
13
|
+
export { renderPathView } from './path.js';
|
|
14
|
+
export { renderGraphView } from './graph.js';
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII renderer for the `info --view` command.
|
|
3
|
+
* Displays a beautiful box with graph summaries and writer timelines.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import boxen from 'boxen';
|
|
7
|
+
import stringWidth from 'string-width';
|
|
8
|
+
import { colors } from './colors.js';
|
|
9
|
+
import { padRight } from '../../utils/unicode.js';
|
|
10
|
+
import { timeAgo } from '../../utils/time.js';
|
|
11
|
+
import { TIMELINE } from './symbols.js';
|
|
12
|
+
|
|
13
|
+
// Box drawing characters (info.js uses verbose key names for card rendering)
|
|
14
|
+
const BOX = {
|
|
15
|
+
topLeft: '\u250C', // ┌
|
|
16
|
+
topRight: '\u2510', // ┐
|
|
17
|
+
bottomLeft: '\u2514', // └
|
|
18
|
+
bottomRight: '\u2518', // ┘
|
|
19
|
+
horizontal: '\u2500', // ─
|
|
20
|
+
vertical: '\u2502', // │
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Maximum timeline width for patches
|
|
24
|
+
const MAX_TIMELINE_WIDTH = 40;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Builds a timeline string for a writer based on patch count.
|
|
28
|
+
* @param {number} patchCount - Number of patches
|
|
29
|
+
* @param {number} maxPatches - Maximum patches across all writers (for scaling)
|
|
30
|
+
* @returns {string} Timeline string like "────●────●────●────● (12 patches)"
|
|
31
|
+
*/
|
|
32
|
+
function buildTimeline(patchCount, maxPatches) {
|
|
33
|
+
if (patchCount === 0) {
|
|
34
|
+
return colors.muted('(no patches)');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Scale timeline width based on patch count relative to max
|
|
38
|
+
const scaledWidth = Math.max(
|
|
39
|
+
4,
|
|
40
|
+
Math.floor((patchCount / Math.max(maxPatches, 1)) * MAX_TIMELINE_WIDTH)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Determine number of dots (max 8 dots for visual clarity)
|
|
44
|
+
const dotCount = Math.min(patchCount, 8);
|
|
45
|
+
const segmentWidth = Math.floor(scaledWidth / dotCount);
|
|
46
|
+
|
|
47
|
+
let timeline = '';
|
|
48
|
+
for (let i = 0; i < dotCount; i++) {
|
|
49
|
+
if (i > 0) {
|
|
50
|
+
timeline += colors.muted(TIMELINE.line.repeat(segmentWidth));
|
|
51
|
+
}
|
|
52
|
+
timeline += colors.primary(TIMELINE.dot);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add trailing line
|
|
56
|
+
const remaining = scaledWidth - (dotCount * segmentWidth);
|
|
57
|
+
if (remaining > 0) {
|
|
58
|
+
timeline += colors.muted(TIMELINE.line.repeat(remaining));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const patchLabel = patchCount === 1 ? 'patch' : 'patches';
|
|
62
|
+
return `${timeline} ${colors.muted(`(${patchCount} ${patchLabel})`)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Builds the writer names display string.
|
|
67
|
+
* @param {string[]} writerIds - Array of writer IDs
|
|
68
|
+
* @returns {string} Formatted writer names
|
|
69
|
+
*/
|
|
70
|
+
function formatWriterNames(writerIds) {
|
|
71
|
+
if (writerIds.length === 0) {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
const displayed = writerIds.slice(0, 5).join(', ');
|
|
75
|
+
return writerIds.length > 5 ? `${displayed}...` : displayed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Renders the header lines for a graph card.
|
|
80
|
+
* @param {Object} graph - Graph info object
|
|
81
|
+
* @param {number} contentWidth - Available content width
|
|
82
|
+
* @returns {string[]} Header lines
|
|
83
|
+
*/
|
|
84
|
+
function renderCardHeader(graph, contentWidth) {
|
|
85
|
+
const lines = [];
|
|
86
|
+
const graphIcon = '\uD83D\uDCCA'; // 📊
|
|
87
|
+
const graphName = `${graphIcon} ${colors.bold(graph.name)}`;
|
|
88
|
+
lines.push(`${BOX.topLeft}${BOX.horizontal.repeat(contentWidth + 2)}${BOX.topRight}`);
|
|
89
|
+
lines.push(`${BOX.vertical} ${padRight(graphName, contentWidth)} ${BOX.vertical}`);
|
|
90
|
+
|
|
91
|
+
// Writers summary
|
|
92
|
+
const writerCount = graph.writers?.count ?? 0;
|
|
93
|
+
const writerIds = graph.writers?.ids ?? [];
|
|
94
|
+
const writerNames = formatWriterNames(writerIds);
|
|
95
|
+
const writerLine = writerNames
|
|
96
|
+
? `Writers: ${writerCount} (${writerNames})`
|
|
97
|
+
: `Writers: ${writerCount}`;
|
|
98
|
+
lines.push(`${BOX.vertical} ${padRight(writerLine, contentWidth)} ${BOX.vertical}`);
|
|
99
|
+
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Renders writer timeline lines for a graph card.
|
|
105
|
+
* @param {Object} writerPatches - Map of writerId to patch count
|
|
106
|
+
* @param {number} contentWidth - Available content width
|
|
107
|
+
* @returns {string[]} Timeline lines
|
|
108
|
+
*/
|
|
109
|
+
function renderWriterTimelines(writerPatches, contentWidth) {
|
|
110
|
+
if (!writerPatches || Object.keys(writerPatches).length === 0) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const lines = [];
|
|
115
|
+
const patchCounts = Object.values(writerPatches);
|
|
116
|
+
const maxPatches = Math.max(...patchCounts, 1);
|
|
117
|
+
|
|
118
|
+
// Find the longest writer ID for alignment
|
|
119
|
+
const maxWriterIdLen = Math.max(
|
|
120
|
+
...Object.keys(writerPatches).map((id) => stringWidth(id)),
|
|
121
|
+
6
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
for (const [writerId, patchCount] of Object.entries(writerPatches)) {
|
|
125
|
+
const paddedId = padRight(writerId, maxWriterIdLen);
|
|
126
|
+
const timeline = buildTimeline(patchCount, maxPatches);
|
|
127
|
+
const writerTimeline = ` ${colors.muted(paddedId)} ${timeline}`;
|
|
128
|
+
lines.push(`${BOX.vertical} ${padRight(writerTimeline, contentWidth)} ${BOX.vertical}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lines;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Renders checkpoint and coverage lines for a graph card.
|
|
136
|
+
* @param {Object} graph - Graph info object
|
|
137
|
+
* @param {number} contentWidth - Available content width
|
|
138
|
+
* @returns {string[]} Status lines
|
|
139
|
+
*/
|
|
140
|
+
function renderCardStatus(graph, contentWidth) {
|
|
141
|
+
const lines = [];
|
|
142
|
+
|
|
143
|
+
// Checkpoint info
|
|
144
|
+
if (graph.checkpoint) {
|
|
145
|
+
const { sha, date } = graph.checkpoint;
|
|
146
|
+
if (sha) {
|
|
147
|
+
const shortSha = sha.slice(0, 7);
|
|
148
|
+
const timeStr = date ? timeAgo(date) : '';
|
|
149
|
+
const checkIcon = colors.success('\u2713'); // ✓
|
|
150
|
+
const timePart = timeStr ? ` (${timeStr})` : '';
|
|
151
|
+
const checkpointLine = `Checkpoint: ${shortSha}${timePart} ${checkIcon}`;
|
|
152
|
+
lines.push(`${BOX.vertical} ${padRight(checkpointLine, contentWidth)} ${BOX.vertical}`);
|
|
153
|
+
} else {
|
|
154
|
+
const warnIcon = colors.warning('\u26A0'); // ⚠
|
|
155
|
+
const noCheckpointLine = `Checkpoint: none ${warnIcon}`;
|
|
156
|
+
lines.push(`${BOX.vertical} ${padRight(noCheckpointLine, contentWidth)} ${BOX.vertical}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Coverage info (if present)
|
|
161
|
+
if (graph.coverage?.sha) {
|
|
162
|
+
const shortSha = graph.coverage.sha.slice(0, 7);
|
|
163
|
+
const coverageLine = `Coverage: ${shortSha}`;
|
|
164
|
+
lines.push(`${BOX.vertical} ${padRight(colors.muted(coverageLine), contentWidth)} ${BOX.vertical}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return lines;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Renders a single graph card.
|
|
172
|
+
* @param {Object} graph - Graph info object
|
|
173
|
+
* @param {number} innerWidth - Available width inside the card
|
|
174
|
+
* @returns {string[]} Array of lines for this graph card
|
|
175
|
+
*/
|
|
176
|
+
function renderGraphCard(graph, innerWidth) {
|
|
177
|
+
const contentWidth = innerWidth - 4; // Account for │ padding on each side
|
|
178
|
+
|
|
179
|
+
const headerLines = renderCardHeader(graph, contentWidth);
|
|
180
|
+
const timelineLines = renderWriterTimelines(graph.writerPatches, contentWidth);
|
|
181
|
+
const statusLines = renderCardStatus(graph, contentWidth);
|
|
182
|
+
const bottomBorder = `${BOX.bottomLeft}${BOX.horizontal.repeat(contentWidth + 2)}${BOX.bottomRight}`;
|
|
183
|
+
|
|
184
|
+
return [...headerLines, ...timelineLines, ...statusLines, bottomBorder];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Renders the info view with ASCII box art.
|
|
189
|
+
* @param {Object} data - Info payload from handleInfo
|
|
190
|
+
* @param {string} data.repo - Repository path
|
|
191
|
+
* @param {Object[]} data.graphs - Array of graph info objects
|
|
192
|
+
* @returns {string} Formatted ASCII output
|
|
193
|
+
*/
|
|
194
|
+
export function renderInfoView(data) {
|
|
195
|
+
if (!data || !data.graphs) {
|
|
196
|
+
return `${colors.error('No data available')}\n`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { graphs } = data;
|
|
200
|
+
|
|
201
|
+
if (graphs.length === 0) {
|
|
202
|
+
const content = colors.muted('No WARP graphs found in this repository.');
|
|
203
|
+
const box = boxen(content, {
|
|
204
|
+
title: 'WARP GRAPHS',
|
|
205
|
+
titleAlignment: 'center',
|
|
206
|
+
padding: 1,
|
|
207
|
+
borderStyle: 'double',
|
|
208
|
+
borderColor: 'cyan',
|
|
209
|
+
});
|
|
210
|
+
return `${box}\n`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Calculate inner width (for consistent card sizing)
|
|
214
|
+
const innerWidth = 60;
|
|
215
|
+
|
|
216
|
+
// Build content
|
|
217
|
+
const contentLines = [];
|
|
218
|
+
|
|
219
|
+
for (let i = 0; i < graphs.length; i++) {
|
|
220
|
+
const graph = graphs[i];
|
|
221
|
+
const cardLines = renderGraphCard(graph, innerWidth);
|
|
222
|
+
|
|
223
|
+
// Add spacing between cards
|
|
224
|
+
if (i > 0) {
|
|
225
|
+
contentLines.push('');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
contentLines.push(...cardLines);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const content = contentLines.join('\n');
|
|
232
|
+
|
|
233
|
+
// Wrap in outer box
|
|
234
|
+
const output = boxen(content, {
|
|
235
|
+
title: 'WARP GRAPHS IN REPOSITORY',
|
|
236
|
+
titleAlignment: 'center',
|
|
237
|
+
padding: 1,
|
|
238
|
+
borderStyle: 'double',
|
|
239
|
+
borderColor: 'cyan',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return `${output}\n`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export default { renderInfoView };
|