@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,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize command ASCII visualization renderer.
|
|
3
|
+
*
|
|
4
|
+
* Renders a visual dashboard showing materialization results with
|
|
5
|
+
* progress indicators, statistics bar charts, and checkpoint info.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createBox } from './box.js';
|
|
9
|
+
import { progressBar } from './progress.js';
|
|
10
|
+
import { colors } from './colors.js';
|
|
11
|
+
import { padRight } from '../../utils/unicode.js';
|
|
12
|
+
import { truncate } from '../../utils/truncate.js';
|
|
13
|
+
import { formatNumber, formatSha } from './formatters.js';
|
|
14
|
+
|
|
15
|
+
// Bar chart settings
|
|
16
|
+
const BAR_WIDTH = 20;
|
|
17
|
+
const STAT_LABEL_WIDTH = 12;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a scaled bar for statistics display.
|
|
21
|
+
* @param {number} value - The value to display
|
|
22
|
+
* @param {number} maxValue - Maximum value for scaling (0 = no scaling)
|
|
23
|
+
* @param {number} width - Bar width in characters
|
|
24
|
+
* @returns {string} Formatted bar string
|
|
25
|
+
*/
|
|
26
|
+
function statBar(value, maxValue, width = BAR_WIDTH) {
|
|
27
|
+
if (maxValue === 0 || value === 0) {
|
|
28
|
+
return colors.muted('\u2591'.repeat(width));
|
|
29
|
+
}
|
|
30
|
+
const percent = Math.min(100, (value / maxValue) * 100);
|
|
31
|
+
const filledCount = Math.round((percent / 100) * width);
|
|
32
|
+
const emptyCount = width - filledCount;
|
|
33
|
+
const bar = '\u2588'.repeat(filledCount) + '\u2591'.repeat(emptyCount);
|
|
34
|
+
return colors.primary(bar);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render graph header with icon and name.
|
|
39
|
+
* @param {string} graphName - Name of the graph
|
|
40
|
+
* @returns {string[]} Header lines
|
|
41
|
+
*/
|
|
42
|
+
function renderGraphHeader(graphName) {
|
|
43
|
+
const graphIcon = '\uD83D\uDCCA'; // 📊
|
|
44
|
+
return [` ${graphIcon} ${colors.bold(graphName)}`, ''];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Render error state for a graph.
|
|
49
|
+
* @param {string} errorMessage - Error message
|
|
50
|
+
* @returns {string[]} Error lines
|
|
51
|
+
*/
|
|
52
|
+
function renderErrorState(errorMessage) {
|
|
53
|
+
return [` ${colors.error('\u2717')} Error: ${errorMessage}`];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Render no-op state (already materialized).
|
|
58
|
+
* @param {Object} graph - Graph data
|
|
59
|
+
* @returns {string[]} No-op state lines
|
|
60
|
+
*/
|
|
61
|
+
function renderNoOpState(graph) {
|
|
62
|
+
const lines = [
|
|
63
|
+
` ${colors.success('\u2713')} Already materialized (no new patches)`,
|
|
64
|
+
'',
|
|
65
|
+
` ${padRight('Nodes:', STAT_LABEL_WIDTH)} ${formatNumber(graph.nodes)}`,
|
|
66
|
+
` ${padRight('Edges:', STAT_LABEL_WIDTH)} ${formatNumber(graph.edges)}`,
|
|
67
|
+
];
|
|
68
|
+
if (typeof graph.properties === 'number') {
|
|
69
|
+
lines.push(` ${padRight('Properties:', STAT_LABEL_WIDTH)} ${formatNumber(graph.properties)}`);
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Render empty graph state (0 patches).
|
|
76
|
+
* @param {Object} graph - Graph data
|
|
77
|
+
* @returns {string[]} Empty state lines
|
|
78
|
+
*/
|
|
79
|
+
function renderEmptyState(graph) {
|
|
80
|
+
const lines = [` ${colors.muted('Empty graph (0 patches)')}`, ''];
|
|
81
|
+
if (graph.checkpoint) {
|
|
82
|
+
lines.push(` Checkpoint: ${formatSha(graph.checkpoint)} ${colors.success('\u2713')}`);
|
|
83
|
+
}
|
|
84
|
+
return lines;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render writer progress section.
|
|
89
|
+
* @param {Object} writers - Writer patch counts
|
|
90
|
+
* @returns {string[]} Writer lines
|
|
91
|
+
*/
|
|
92
|
+
function renderWriterSection(writers) {
|
|
93
|
+
if (!writers || Object.keys(writers).length === 0) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const lines = [` ${colors.dim('Writers:')}`];
|
|
97
|
+
const writerEntries = Object.entries(writers);
|
|
98
|
+
const maxPatches = Math.max(...writerEntries.map(([, p]) => p), 1);
|
|
99
|
+
const maxWriterWidth = Math.min(Math.max(...writerEntries.map(([id]) => id.length), 6), 16);
|
|
100
|
+
for (const [writerId, patchCount] of writerEntries) {
|
|
101
|
+
const bar = progressBar(Math.round((patchCount / maxPatches) * 100), 15, { showPercent: false });
|
|
102
|
+
const displayId = truncate(writerId, maxWriterWidth);
|
|
103
|
+
lines.push(` ${padRight(displayId, maxWriterWidth)} ${bar} ${patchCount} patches`);
|
|
104
|
+
}
|
|
105
|
+
lines.push('');
|
|
106
|
+
return lines;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Render statistics section with bar charts.
|
|
111
|
+
* @param {Object} graph - Graph data
|
|
112
|
+
* @param {Object} maxValues - Max values for scaling
|
|
113
|
+
* @returns {string[]} Statistics lines
|
|
114
|
+
*/
|
|
115
|
+
function renderStatsSection(graph, { maxNodes, maxEdges, maxProps }) {
|
|
116
|
+
const lines = [
|
|
117
|
+
` ${colors.dim('Statistics:')}`,
|
|
118
|
+
` ${padRight('Nodes:', STAT_LABEL_WIDTH)} ${statBar(graph.nodes, maxNodes)} ${formatNumber(graph.nodes)}`,
|
|
119
|
+
` ${padRight('Edges:', STAT_LABEL_WIDTH)} ${statBar(graph.edges, maxEdges)} ${formatNumber(graph.edges)}`,
|
|
120
|
+
];
|
|
121
|
+
if (typeof graph.properties === 'number') {
|
|
122
|
+
lines.push(` ${padRight('Properties:', STAT_LABEL_WIDTH)} ${statBar(graph.properties, maxProps)} ${formatNumber(graph.properties)}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push('');
|
|
125
|
+
return lines;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Render checkpoint info line.
|
|
130
|
+
* @param {string|null} checkpoint - Checkpoint SHA or null
|
|
131
|
+
* @returns {string[]} Checkpoint lines
|
|
132
|
+
*/
|
|
133
|
+
function renderCheckpointInfo(checkpoint) {
|
|
134
|
+
if (checkpoint) {
|
|
135
|
+
return [` Checkpoint: ${formatSha(checkpoint)} ${colors.success('\u2713 created')}`];
|
|
136
|
+
}
|
|
137
|
+
return [` Checkpoint: ${colors.warning('none')}`];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Render a single graph's materialization result.
|
|
142
|
+
* @param {Object} graph - Graph result from materialize
|
|
143
|
+
* @param {Object} maxValues - Max values for scaling bars
|
|
144
|
+
* @returns {string[]} Array of lines for this graph
|
|
145
|
+
*/
|
|
146
|
+
function renderGraphResult(graph, maxValues) {
|
|
147
|
+
const lines = [...renderGraphHeader(graph.graph)];
|
|
148
|
+
|
|
149
|
+
if (graph.error) {
|
|
150
|
+
return [...lines, ...renderErrorState(graph.error)];
|
|
151
|
+
}
|
|
152
|
+
if (graph.noOp) {
|
|
153
|
+
return [...lines, ...renderNoOpState(graph)];
|
|
154
|
+
}
|
|
155
|
+
if (graph.patchCount === 0) {
|
|
156
|
+
return [...lines, ...renderEmptyState(graph)];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
lines.push(...renderWriterSection(graph.writers));
|
|
160
|
+
lines.push(...renderStatsSection(graph, maxValues));
|
|
161
|
+
lines.push(...renderCheckpointInfo(graph.checkpoint));
|
|
162
|
+
return lines;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Calculate max values for scaling bar charts.
|
|
167
|
+
* @param {Object[]} graphs - Array of graph results
|
|
168
|
+
* @returns {Object} Max values object
|
|
169
|
+
*/
|
|
170
|
+
function calculateMaxValues(graphs) {
|
|
171
|
+
const successfulGraphs = graphs.filter((g) => !g.error);
|
|
172
|
+
return {
|
|
173
|
+
maxNodes: Math.max(...successfulGraphs.map((g) => g.nodes || 0), 1),
|
|
174
|
+
maxEdges: Math.max(...successfulGraphs.map((g) => g.edges || 0), 1),
|
|
175
|
+
maxProps: Math.max(...successfulGraphs.map((g) => g.properties || 0), 1),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Build summary line based on success/failure counts.
|
|
181
|
+
* @param {number} successCount - Number of successful graphs
|
|
182
|
+
* @param {number} errorCount - Number of failed graphs
|
|
183
|
+
* @returns {string} Summary line
|
|
184
|
+
*/
|
|
185
|
+
function buildSummaryLine(successCount, errorCount) {
|
|
186
|
+
if (errorCount === 0) {
|
|
187
|
+
const plural = successCount !== 1 ? 's' : '';
|
|
188
|
+
return ` ${colors.success('\u2713')} ${successCount} graph${plural} materialized successfully`;
|
|
189
|
+
}
|
|
190
|
+
if (successCount === 0) {
|
|
191
|
+
const plural = errorCount !== 1 ? 's' : '';
|
|
192
|
+
return ` ${colors.error('\u2717')} ${errorCount} graph${plural} failed`;
|
|
193
|
+
}
|
|
194
|
+
return ` ${colors.warning('\u26A0')} ${successCount} succeeded, ${errorCount} failed`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Determine border color based on success/failure counts.
|
|
199
|
+
* @param {number} successCount - Number of successful graphs
|
|
200
|
+
* @param {number} errorCount - Number of failed graphs
|
|
201
|
+
* @returns {string} Border color name
|
|
202
|
+
*/
|
|
203
|
+
function getBorderColor(successCount, errorCount) {
|
|
204
|
+
if (errorCount > 0 && successCount === 0) {
|
|
205
|
+
return 'red';
|
|
206
|
+
}
|
|
207
|
+
if (errorCount > 0) {
|
|
208
|
+
return 'yellow';
|
|
209
|
+
}
|
|
210
|
+
return 'green';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Render the materialize view dashboard.
|
|
215
|
+
* @param {Object} payload - The materialize command payload
|
|
216
|
+
* @param {Object[]} payload.graphs - Array of graph results
|
|
217
|
+
* @returns {string} Formatted dashboard string
|
|
218
|
+
*/
|
|
219
|
+
export function renderMaterializeView(payload) {
|
|
220
|
+
if (!payload || !payload.graphs) {
|
|
221
|
+
return `${colors.error('No data available')}\n`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const { graphs } = payload;
|
|
225
|
+
|
|
226
|
+
if (graphs.length === 0) {
|
|
227
|
+
const content = colors.muted('No WARP graphs found in this repository.');
|
|
228
|
+
return `${createBox(content, { title: 'MATERIALIZE', titleAlignment: 'center', borderColor: 'cyan' })}\n`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const maxValues = calculateMaxValues(graphs);
|
|
232
|
+
const lines = [];
|
|
233
|
+
const separator = colors.muted(` ${'\u2500'.repeat(50)}`);
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < graphs.length; i++) {
|
|
236
|
+
if (i > 0) {
|
|
237
|
+
lines.push('', separator, '');
|
|
238
|
+
}
|
|
239
|
+
lines.push(...renderGraphResult(graphs[i], maxValues));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const successCount = graphs.filter((g) => !g.error).length;
|
|
243
|
+
const errorCount = graphs.length - successCount;
|
|
244
|
+
lines.push('', buildSummaryLine(successCount, errorCount));
|
|
245
|
+
|
|
246
|
+
const box = createBox(lines.join('\n'), {
|
|
247
|
+
title: 'MATERIALIZE',
|
|
248
|
+
titleAlignment: 'center',
|
|
249
|
+
borderColor: getBorderColor(successCount, errorCount),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return `${box}\n`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export default { renderMaterializeView };
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII renderer for the `path --view` command.
|
|
3
|
+
* Displays the shortest path between two nodes as a connected chain.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import stringWidth from 'string-width';
|
|
7
|
+
import { createBox } from './box.js';
|
|
8
|
+
import { colors } from './colors.js';
|
|
9
|
+
import { ARROW } from './symbols.js';
|
|
10
|
+
|
|
11
|
+
// Default terminal width for wrapping
|
|
12
|
+
const DEFAULT_TERMINAL_WIDTH = 80;
|
|
13
|
+
|
|
14
|
+
// Box content padding (for inner width calculation)
|
|
15
|
+
const BOX_PADDING = 4;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Formats a node ID for display, truncating if necessary.
|
|
19
|
+
* @param {string} nodeId - The node ID to format
|
|
20
|
+
* @param {number} [maxLen=20] - Maximum length before truncation
|
|
21
|
+
* @returns {string} Formatted node ID with brackets
|
|
22
|
+
*/
|
|
23
|
+
function formatNode(nodeId, maxLen = 20) {
|
|
24
|
+
if (!nodeId || typeof nodeId !== 'string') {
|
|
25
|
+
return '[?]';
|
|
26
|
+
}
|
|
27
|
+
const truncated = nodeId.length > maxLen
|
|
28
|
+
? `${nodeId.slice(0, maxLen - 1)}\u2026`
|
|
29
|
+
: nodeId;
|
|
30
|
+
return `[${truncated}]`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates an arrow string for connecting nodes.
|
|
35
|
+
* @param {string} [label] - Optional edge label to display on the arrow
|
|
36
|
+
* @returns {string} Arrow string like " ---> " or " --label--> "
|
|
37
|
+
*/
|
|
38
|
+
function createArrow(label) {
|
|
39
|
+
if (label && typeof label === 'string') {
|
|
40
|
+
return ` ${ARROW.line}${ARROW.line}${label}${ARROW.line}${ARROW.line}${ARROW.right} `;
|
|
41
|
+
}
|
|
42
|
+
return ` ${ARROW.line}${ARROW.line}${ARROW.line}${ARROW.right} `;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Checks if a segment fits on the current line.
|
|
47
|
+
* @param {number} currentWidth - Current line width
|
|
48
|
+
* @param {number} segmentWidth - Width of segment to add
|
|
49
|
+
* @param {number} maxWidth - Maximum line width
|
|
50
|
+
* @returns {boolean} Whether the segment fits
|
|
51
|
+
*/
|
|
52
|
+
function segmentFits(currentWidth, segmentWidth, maxWidth) {
|
|
53
|
+
return currentWidth === 0 || currentWidth + segmentWidth <= maxWidth;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates a path segment with node and optional arrow.
|
|
58
|
+
* @param {Object} opts - Segment options
|
|
59
|
+
* @param {string} opts.nodeId - Node ID
|
|
60
|
+
* @param {number} opts.index - Position in path
|
|
61
|
+
* @param {number} opts.pathLength - Total path length
|
|
62
|
+
* @param {string[]} [opts.edges] - Optional edge labels
|
|
63
|
+
* @returns {{segment: string, width: number}} Segment string and its width
|
|
64
|
+
*/
|
|
65
|
+
function createPathSegment({ nodeId, index, pathLength, edges }) {
|
|
66
|
+
const node = formatNode(nodeId);
|
|
67
|
+
const isEndpoint = index === 0 || index === pathLength - 1;
|
|
68
|
+
const coloredNode = isEndpoint ? colors.primary(node) : node;
|
|
69
|
+
const arrow = index < pathLength - 1 ? createArrow(edges?.[index]) : '';
|
|
70
|
+
const segment = coloredNode + arrow;
|
|
71
|
+
return { segment, width: stringWidth(segment) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Builds path segments that fit within the terminal width.
|
|
76
|
+
* Wraps long paths to multiple lines.
|
|
77
|
+
* @param {string[]} path - Array of node IDs
|
|
78
|
+
* @param {string[]} [edges] - Optional array of edge labels (one fewer than nodes)
|
|
79
|
+
* @param {number} maxWidth - Maximum line width
|
|
80
|
+
* @returns {string[]} Array of line strings
|
|
81
|
+
*/
|
|
82
|
+
function buildPathLines(path, edges, maxWidth) {
|
|
83
|
+
if (!path || path.length === 0) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (path.length === 1) {
|
|
88
|
+
return [colors.primary(formatNode(path[0]))];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lines = [];
|
|
92
|
+
let currentLine = '';
|
|
93
|
+
let currentWidth = 0;
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < path.length; i++) {
|
|
96
|
+
const { segment, width } = createPathSegment({
|
|
97
|
+
nodeId: path[i],
|
|
98
|
+
index: i,
|
|
99
|
+
pathLength: path.length,
|
|
100
|
+
edges,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!segmentFits(currentWidth, width, maxWidth)) {
|
|
104
|
+
lines.push(currentLine);
|
|
105
|
+
currentLine = ` ${segment}`;
|
|
106
|
+
currentWidth = 2 + width;
|
|
107
|
+
} else {
|
|
108
|
+
currentLine += segment;
|
|
109
|
+
currentWidth += width;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (currentLine) {
|
|
114
|
+
lines.push(currentLine);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Renders the "no path found" case.
|
|
122
|
+
* @param {string} from - Source node ID
|
|
123
|
+
* @param {string} to - Target node ID
|
|
124
|
+
* @returns {string} Formatted ASCII output
|
|
125
|
+
*/
|
|
126
|
+
function renderNoPath(from, to) {
|
|
127
|
+
const lines = [
|
|
128
|
+
` ${colors.error('No path found')}`,
|
|
129
|
+
'',
|
|
130
|
+
` From: ${colors.primary(from || '?')}`,
|
|
131
|
+
` To: ${colors.primary(to || '?')}`,
|
|
132
|
+
'',
|
|
133
|
+
` ${colors.muted('The nodes may be disconnected or unreachable')}`,
|
|
134
|
+
` ${colors.muted('with the given traversal direction.')}`,
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
return createBox(lines.join('\n'), {
|
|
138
|
+
title: 'PATH',
|
|
139
|
+
titleAlignment: 'center',
|
|
140
|
+
borderColor: 'red',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Renders the "already at destination" case (from === to).
|
|
146
|
+
* @param {string} nodeId - The node ID
|
|
147
|
+
* @returns {string} Formatted ASCII output
|
|
148
|
+
*/
|
|
149
|
+
function renderSameNode(nodeId) {
|
|
150
|
+
const lines = [
|
|
151
|
+
` ${colors.success('Already at destination')}`,
|
|
152
|
+
'',
|
|
153
|
+
` ${colors.primary(formatNode(nodeId))}`,
|
|
154
|
+
'',
|
|
155
|
+
` ${colors.muted('Start and end are the same node.')}`,
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
return createBox(lines.join('\n'), {
|
|
159
|
+
title: 'PATH',
|
|
160
|
+
titleAlignment: 'center',
|
|
161
|
+
borderColor: 'green',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Renders a found path.
|
|
167
|
+
* @param {Object} payload - Path payload
|
|
168
|
+
* @param {string} payload.graph - Graph name
|
|
169
|
+
* @param {string} payload.from - Source node ID
|
|
170
|
+
* @param {string} payload.to - Target node ID
|
|
171
|
+
* @param {string[]} payload.path - Array of node IDs in the path
|
|
172
|
+
* @param {number} payload.length - Path length (number of edges)
|
|
173
|
+
* @param {string[]} [payload.edges] - Optional array of edge labels
|
|
174
|
+
* @param {number} [terminalWidth] - Terminal width for wrapping
|
|
175
|
+
* @returns {string} Formatted ASCII output
|
|
176
|
+
*/
|
|
177
|
+
function renderFoundPath(payload, terminalWidth = DEFAULT_TERMINAL_WIDTH) {
|
|
178
|
+
const { graph, path, length, edges } = payload;
|
|
179
|
+
|
|
180
|
+
// Calculate available width for path (account for box borders and padding)
|
|
181
|
+
const maxWidth = Math.max(40, terminalWidth - BOX_PADDING - 4);
|
|
182
|
+
|
|
183
|
+
const hopLabel = length === 1 ? 'hop' : 'hops';
|
|
184
|
+
const pathLines = buildPathLines(path, edges, maxWidth);
|
|
185
|
+
|
|
186
|
+
const lines = [
|
|
187
|
+
` Graph: ${colors.muted(graph || 'unknown')}`,
|
|
188
|
+
` Length: ${colors.success(String(length))} ${hopLabel}`,
|
|
189
|
+
'',
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
// Add path visualization
|
|
193
|
+
for (const line of pathLines) {
|
|
194
|
+
lines.push(` ${line}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return createBox(lines.join('\n'), {
|
|
198
|
+
title: `PATH: ${path[0] || '?'} ${ARROW.right} ${path[path.length - 1] || '?'}`,
|
|
199
|
+
titleAlignment: 'center',
|
|
200
|
+
borderColor: 'green',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Renders the path view.
|
|
206
|
+
* @param {Object} payload - The path command payload
|
|
207
|
+
* @param {string} payload.graph - Graph name
|
|
208
|
+
* @param {string} payload.from - Source node ID
|
|
209
|
+
* @param {string} payload.to - Target node ID
|
|
210
|
+
* @param {boolean} payload.found - Whether a path was found
|
|
211
|
+
* @param {string[]} payload.path - Array of node IDs in the path
|
|
212
|
+
* @param {number} payload.length - Path length (number of edges)
|
|
213
|
+
* @param {string[]} [payload.edges] - Optional array of edge labels
|
|
214
|
+
* @param {Object} [options] - Rendering options
|
|
215
|
+
* @param {number} [options.terminalWidth] - Terminal width for wrapping
|
|
216
|
+
* @returns {string} Formatted ASCII output
|
|
217
|
+
*/
|
|
218
|
+
export function renderPathView(payload, options = {}) {
|
|
219
|
+
if (!payload) {
|
|
220
|
+
return `${colors.error('No data available')}\n`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const { from, to, found, path, length } = payload;
|
|
224
|
+
const terminalWidth = options.terminalWidth || process.stdout.columns || DEFAULT_TERMINAL_WIDTH;
|
|
225
|
+
|
|
226
|
+
// Handle "no path found" case
|
|
227
|
+
if (!found) {
|
|
228
|
+
return `${renderNoPath(from, to)}\n`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Handle "already at destination" case (from === to, length === 0)
|
|
232
|
+
if (length === 0 && path && path.length === 1) {
|
|
233
|
+
return `${renderSameNode(path[0])}\n`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Render the found path
|
|
237
|
+
return `${renderFoundPath(payload, terminalWidth)}\n`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export default { renderPathView };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders a colored progress bar string.
|
|
5
|
+
*
|
|
6
|
+
* Color thresholds: green >= 80%, yellow >= 50%, red < 50%.
|
|
7
|
+
*
|
|
8
|
+
* @param {number} percent - Percentage value (clamped to 0-100)
|
|
9
|
+
* @param {number} [width=20] - Character width of the bar
|
|
10
|
+
* @param {Object} [options] - Display options
|
|
11
|
+
* @param {string} [options.filled='█'] - Character for filled segments
|
|
12
|
+
* @param {string} [options.empty='░'] - Character for empty segments
|
|
13
|
+
* @param {boolean} [options.showPercent=true] - Whether to append the percentage value
|
|
14
|
+
* @returns {string} The rendered progress bar with ANSI colors
|
|
15
|
+
*/
|
|
16
|
+
export function progressBar(percent, width = 20, options = {}) {
|
|
17
|
+
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
18
|
+
const { filled = '█', empty = '░', showPercent = true } = options;
|
|
19
|
+
const filledCount = Math.round((clampedPercent / 100) * width);
|
|
20
|
+
const emptyCount = width - filledCount;
|
|
21
|
+
|
|
22
|
+
let bar = filled.repeat(filledCount) + empty.repeat(emptyCount);
|
|
23
|
+
|
|
24
|
+
// Color based on value
|
|
25
|
+
if (clampedPercent >= 80) {bar = chalk.green(bar);}
|
|
26
|
+
else if (clampedPercent >= 50) {bar = chalk.yellow(bar);}
|
|
27
|
+
else {bar = chalk.red(bar);}
|
|
28
|
+
|
|
29
|
+
return showPercent ? `${bar} ${clampedPercent}%` : bar;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default { progressBar };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Unicode symbol constants for ASCII renderers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from history.js, info.js, path.js, and graph.js to eliminate
|
|
5
|
+
* duplicate character constant definitions across renderers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Timeline characters for patch history and writer timelines. */
|
|
9
|
+
export const TIMELINE = {
|
|
10
|
+
vertical: '\u2502', // │
|
|
11
|
+
dot: '\u25CF', // ●
|
|
12
|
+
connector: '\u251C', // ├
|
|
13
|
+
end: '\u2514', // └
|
|
14
|
+
top: '\u250C', // ┌
|
|
15
|
+
line: '\u2500', // ─
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Arrow characters for path and graph visualization. */
|
|
19
|
+
export const ARROW = {
|
|
20
|
+
line: '\u2500', // ─
|
|
21
|
+
right: '\u25B6', // ▶
|
|
22
|
+
left: '\u25C0', // ◀
|
|
23
|
+
down: '\u25BC', // ▼
|
|
24
|
+
up: '\u25B2', // ▲
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Tree characters for hierarchical displays. */
|
|
28
|
+
export const TREE = {
|
|
29
|
+
branch: '\u251C', // ├
|
|
30
|
+
last: '\u2514', // └
|
|
31
|
+
vertical: '\u2502', // │
|
|
32
|
+
space: ' ',
|
|
33
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Table from 'cli-table3';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a cli-table3 instance with default WARP styling.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} [options] - Options forwarded to cli-table3 constructor
|
|
7
|
+
* @param {string[]} [options.head] - Column headers
|
|
8
|
+
* @param {Object} [options.style] - Style overrides (defaults: head=cyan, border=gray)
|
|
9
|
+
* @returns {import('cli-table3')} A cli-table3 instance
|
|
10
|
+
*/
|
|
11
|
+
export function createTable(options = {}) {
|
|
12
|
+
const defaultStyle = { head: ['cyan'], border: ['gray'] };
|
|
13
|
+
return new Table({
|
|
14
|
+
...options,
|
|
15
|
+
style: { ...defaultStyle, ...options.style },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default createTable;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Placeholder for M5
|