@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.
Files changed (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +16 -0
  3. package/README.md +480 -0
  4. package/SECURITY.md +30 -0
  5. package/bin/git-warp +24 -0
  6. package/bin/warp-graph.js +1574 -0
  7. package/index.d.ts +2366 -0
  8. package/index.js +180 -0
  9. package/package.json +129 -0
  10. package/scripts/install-git-warp.sh +258 -0
  11. package/scripts/uninstall-git-warp.sh +139 -0
  12. package/src/domain/WarpGraph.js +3157 -0
  13. package/src/domain/crdt/Dot.js +160 -0
  14. package/src/domain/crdt/LWW.js +154 -0
  15. package/src/domain/crdt/ORSet.js +371 -0
  16. package/src/domain/crdt/VersionVector.js +222 -0
  17. package/src/domain/entities/GraphNode.js +60 -0
  18. package/src/domain/errors/EmptyMessageError.js +47 -0
  19. package/src/domain/errors/ForkError.js +30 -0
  20. package/src/domain/errors/IndexError.js +23 -0
  21. package/src/domain/errors/OperationAbortedError.js +22 -0
  22. package/src/domain/errors/QueryError.js +39 -0
  23. package/src/domain/errors/SchemaUnsupportedError.js +17 -0
  24. package/src/domain/errors/ShardCorruptionError.js +56 -0
  25. package/src/domain/errors/ShardLoadError.js +57 -0
  26. package/src/domain/errors/ShardValidationError.js +61 -0
  27. package/src/domain/errors/StorageError.js +57 -0
  28. package/src/domain/errors/SyncError.js +30 -0
  29. package/src/domain/errors/TraversalError.js +23 -0
  30. package/src/domain/errors/WarpError.js +31 -0
  31. package/src/domain/errors/WormholeError.js +28 -0
  32. package/src/domain/errors/WriterError.js +39 -0
  33. package/src/domain/errors/index.js +21 -0
  34. package/src/domain/services/AnchorMessageCodec.js +99 -0
  35. package/src/domain/services/BitmapIndexBuilder.js +225 -0
  36. package/src/domain/services/BitmapIndexReader.js +435 -0
  37. package/src/domain/services/BoundaryTransitionRecord.js +463 -0
  38. package/src/domain/services/CheckpointMessageCodec.js +147 -0
  39. package/src/domain/services/CheckpointSerializerV5.js +281 -0
  40. package/src/domain/services/CheckpointService.js +384 -0
  41. package/src/domain/services/CommitDagTraversalService.js +156 -0
  42. package/src/domain/services/DagPathFinding.js +712 -0
  43. package/src/domain/services/DagTopology.js +239 -0
  44. package/src/domain/services/DagTraversal.js +245 -0
  45. package/src/domain/services/Frontier.js +108 -0
  46. package/src/domain/services/GCMetrics.js +101 -0
  47. package/src/domain/services/GCPolicy.js +122 -0
  48. package/src/domain/services/GitLogParser.js +205 -0
  49. package/src/domain/services/HealthCheckService.js +246 -0
  50. package/src/domain/services/HookInstaller.js +326 -0
  51. package/src/domain/services/HttpSyncServer.js +262 -0
  52. package/src/domain/services/IndexRebuildService.js +426 -0
  53. package/src/domain/services/IndexStalenessChecker.js +103 -0
  54. package/src/domain/services/JoinReducer.js +582 -0
  55. package/src/domain/services/KeyCodec.js +113 -0
  56. package/src/domain/services/LegacyAnchorDetector.js +67 -0
  57. package/src/domain/services/LogicalTraversal.js +351 -0
  58. package/src/domain/services/MessageCodecInternal.js +132 -0
  59. package/src/domain/services/MessageSchemaDetector.js +145 -0
  60. package/src/domain/services/MigrationService.js +55 -0
  61. package/src/domain/services/ObserverView.js +265 -0
  62. package/src/domain/services/PatchBuilderV2.js +669 -0
  63. package/src/domain/services/PatchMessageCodec.js +140 -0
  64. package/src/domain/services/ProvenanceIndex.js +337 -0
  65. package/src/domain/services/ProvenancePayload.js +242 -0
  66. package/src/domain/services/QueryBuilder.js +835 -0
  67. package/src/domain/services/StateDiff.js +300 -0
  68. package/src/domain/services/StateSerializerV5.js +156 -0
  69. package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
  70. package/src/domain/services/SyncProtocol.js +593 -0
  71. package/src/domain/services/TemporalQuery.js +201 -0
  72. package/src/domain/services/TranslationCost.js +221 -0
  73. package/src/domain/services/TraversalService.js +8 -0
  74. package/src/domain/services/WarpMessageCodec.js +29 -0
  75. package/src/domain/services/WarpStateIndexBuilder.js +127 -0
  76. package/src/domain/services/WormholeService.js +353 -0
  77. package/src/domain/types/TickReceipt.js +285 -0
  78. package/src/domain/types/WarpTypes.js +209 -0
  79. package/src/domain/types/WarpTypesV2.js +200 -0
  80. package/src/domain/utils/CachedValue.js +140 -0
  81. package/src/domain/utils/EventId.js +89 -0
  82. package/src/domain/utils/LRUCache.js +112 -0
  83. package/src/domain/utils/MinHeap.js +114 -0
  84. package/src/domain/utils/RefLayout.js +280 -0
  85. package/src/domain/utils/WriterId.js +205 -0
  86. package/src/domain/utils/cancellation.js +33 -0
  87. package/src/domain/utils/canonicalStringify.js +42 -0
  88. package/src/domain/utils/defaultClock.js +20 -0
  89. package/src/domain/utils/defaultCodec.js +51 -0
  90. package/src/domain/utils/nullLogger.js +21 -0
  91. package/src/domain/utils/roaring.js +181 -0
  92. package/src/domain/utils/shardVersion.js +9 -0
  93. package/src/domain/warp/PatchSession.js +217 -0
  94. package/src/domain/warp/Writer.js +181 -0
  95. package/src/hooks/post-merge.sh +60 -0
  96. package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
  97. package/src/infrastructure/adapters/ClockAdapter.js +57 -0
  98. package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
  99. package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
  100. package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
  101. package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
  102. package/src/infrastructure/adapters/NoOpLogger.js +62 -0
  103. package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
  104. package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
  105. package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
  106. package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
  107. package/src/infrastructure/codecs/CborCodec.js +384 -0
  108. package/src/ports/BlobPort.js +30 -0
  109. package/src/ports/ClockPort.js +25 -0
  110. package/src/ports/CodecPort.js +25 -0
  111. package/src/ports/CommitPort.js +114 -0
  112. package/src/ports/ConfigPort.js +31 -0
  113. package/src/ports/CryptoPort.js +38 -0
  114. package/src/ports/GraphPersistencePort.js +57 -0
  115. package/src/ports/HttpServerPort.js +25 -0
  116. package/src/ports/IndexStoragePort.js +39 -0
  117. package/src/ports/LoggerPort.js +68 -0
  118. package/src/ports/RefPort.js +51 -0
  119. package/src/ports/TreePort.js +51 -0
  120. package/src/visualization/index.js +26 -0
  121. package/src/visualization/layouts/converters.js +75 -0
  122. package/src/visualization/layouts/elkAdapter.js +86 -0
  123. package/src/visualization/layouts/elkLayout.js +95 -0
  124. package/src/visualization/layouts/index.js +29 -0
  125. package/src/visualization/renderers/ascii/box.js +16 -0
  126. package/src/visualization/renderers/ascii/check.js +271 -0
  127. package/src/visualization/renderers/ascii/colors.js +13 -0
  128. package/src/visualization/renderers/ascii/formatters.js +73 -0
  129. package/src/visualization/renderers/ascii/graph.js +344 -0
  130. package/src/visualization/renderers/ascii/history.js +335 -0
  131. package/src/visualization/renderers/ascii/index.js +14 -0
  132. package/src/visualization/renderers/ascii/info.js +245 -0
  133. package/src/visualization/renderers/ascii/materialize.js +255 -0
  134. package/src/visualization/renderers/ascii/path.js +240 -0
  135. package/src/visualization/renderers/ascii/progress.js +32 -0
  136. package/src/visualization/renderers/ascii/symbols.js +33 -0
  137. package/src/visualization/renderers/ascii/table.js +19 -0
  138. package/src/visualization/renderers/browser/index.js +1 -0
  139. package/src/visualization/renderers/svg/index.js +159 -0
  140. package/src/visualization/utils/ansi.js +14 -0
  141. package/src/visualization/utils/time.js +40 -0
  142. package/src/visualization/utils/truncate.js +40 -0
  143. 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