@leroylabs/cli 0.1.6 → 0.1.7
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/bin/leroy.mjs +16 -15
- package/package.json +1 -6
- package/src/live-ui.mjs +391 -433
package/bin/leroy.mjs
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { stdin as input, stderr, stdout as output } from "node:process";
|
|
5
|
-
import React from "react";
|
|
6
5
|
|
|
7
6
|
import { parseArgs } from "../src/args.mjs";
|
|
8
7
|
import { LeroyMcpClient } from "../src/client.mjs";
|
|
@@ -10,7 +9,7 @@ import { configPath, resolvedConfig, writeConfig } from "../src/config.mjs";
|
|
|
10
9
|
import { createDemoResponse } from "../src/demo.mjs";
|
|
11
10
|
import { usageError } from "../src/errors.mjs";
|
|
12
11
|
import { formatEvaluation, formatHelp } from "../src/format.mjs";
|
|
13
|
-
import {
|
|
12
|
+
import { renderLiveEvidence } from "../src/live-ui.mjs";
|
|
14
13
|
import { ansiForeground, LEROY_COLORS } from "../src/colors.mjs";
|
|
15
14
|
|
|
16
15
|
const ANSI = {
|
|
@@ -45,22 +44,20 @@ function printMissingApiKeyGuidance(options) {
|
|
|
45
44
|
printError(`${promptStyle("▲", ["green"], colorEnabled)} ${promptStyle("Connect Leroy", ["bold", "white"], colorEnabled)}`);
|
|
46
45
|
printError();
|
|
47
46
|
printError(` ${promptStyle("Live market evidence requires an MCP key.", ["white"], colorEnabled)}`);
|
|
48
|
-
printError(` ${promptStyle("Create or activate one at", ["neutral"], colorEnabled)}`);
|
|
49
|
-
printError(` ${promptStyle("https://getleroy.com/mcp", ["green"], colorEnabled)}`);
|
|
47
|
+
printError(` ${promptStyle("Create or activate one at", ["neutral"], colorEnabled)} ${promptStyle("https://getleroy.com/mcp", ["green"], colorEnabled)}`);
|
|
50
48
|
printError();
|
|
51
49
|
printError(` ${promptStyle("›", ["green"], colorEnabled)} Run ${promptStyle("leroy connect", ["bold", "white"], colorEnabled)} to continue.`);
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
async function connect(options) {
|
|
55
53
|
const config = await resolvedConfig(options);
|
|
54
|
+
const colorEnabled = output.isTTY && !options.noColor;
|
|
56
55
|
let apiKey = config.apiKey;
|
|
57
56
|
if (!apiKey && input.isTTY) {
|
|
58
|
-
const colorEnabled = output.isTTY && !options.noColor;
|
|
59
57
|
print();
|
|
60
58
|
print(`${promptStyle("▲", ["green"], colorEnabled)} ${promptStyle("Connect Leroy", ["bold", "white"], colorEnabled)}`);
|
|
61
59
|
print();
|
|
62
|
-
print(` ${promptStyle("Create or activate an MCP key at", ["neutral"], colorEnabled)}`);
|
|
63
|
-
print(` ${promptStyle("https://getleroy.com/mcp", ["green"], colorEnabled)}`);
|
|
60
|
+
print(` ${promptStyle("Create or activate an MCP key at", ["neutral"], colorEnabled)} ${promptStyle("https://getleroy.com/mcp", ["green"], colorEnabled)}`);
|
|
64
61
|
print();
|
|
65
62
|
const readline = createInterface({ input, output });
|
|
66
63
|
apiKey = (await readline.question(` ${promptStyle("›", ["green"], colorEnabled)} Paste your ${promptStyle("lr_live_", ["bold", "white"], colorEnabled)} key: `)).trim();
|
|
@@ -70,11 +67,16 @@ async function connect(options) {
|
|
|
70
67
|
print(`No API key supplied. Run: leroy connect --api-key lr_live_...`);
|
|
71
68
|
return 2;
|
|
72
69
|
}
|
|
73
|
-
print(
|
|
70
|
+
print();
|
|
71
|
+
print(` ${promptStyle("Checking Leroy connection...", ["white"], colorEnabled)}`);
|
|
74
72
|
const client = new LeroyMcpClient({ endpoint: config.endpoint, apiKey });
|
|
75
73
|
await client.initialize();
|
|
76
74
|
await writeConfig({ endpoint: config.endpoint, apiKey });
|
|
77
|
-
print(
|
|
75
|
+
print();
|
|
76
|
+
print(` ${promptStyle("Connected!", ["bold", "green"], colorEnabled)}`);
|
|
77
|
+
print();
|
|
78
|
+
print(` ${promptStyle(`Credentials saved to ${configPath()}`, ["white"], colorEnabled)}`);
|
|
79
|
+
print();
|
|
78
80
|
return 0;
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -99,13 +101,12 @@ async function evaluate(options) {
|
|
|
99
101
|
client = new LeroyMcpClient({ endpoint: config.endpoint, apiKey: config.apiKey });
|
|
100
102
|
}
|
|
101
103
|
if (input.isTTY && output.isTTY && !options.json && !options.setup && !options.verbose) {
|
|
102
|
-
|
|
104
|
+
await renderLiveEvidence({
|
|
103
105
|
client,
|
|
104
106
|
request,
|
|
105
107
|
colorEnabled: !options.noColor,
|
|
106
108
|
polling: false,
|
|
107
|
-
})
|
|
108
|
-
await app.waitUntilExit();
|
|
109
|
+
});
|
|
109
110
|
return 0;
|
|
110
111
|
}
|
|
111
112
|
const response = await client.evaluate(request);
|
|
@@ -143,13 +144,13 @@ async function watch(options) {
|
|
|
143
144
|
}
|
|
144
145
|
client = new LeroyMcpClient({ endpoint: config.endpoint, apiKey: config.apiKey });
|
|
145
146
|
}
|
|
146
|
-
|
|
147
|
+
await renderLiveEvidence({
|
|
147
148
|
client,
|
|
148
149
|
request,
|
|
149
150
|
colorEnabled: !options.noColor,
|
|
150
151
|
intervalMs: options.intervalSeconds * 1_000,
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
polling: true,
|
|
153
|
+
});
|
|
153
154
|
return 0;
|
|
154
155
|
}
|
|
155
156
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leroylabs/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Command-line access to Leroy market evidence.",
|
|
5
5
|
"homepage": "https://getleroy.com",
|
|
6
6
|
"type": "module",
|
|
@@ -15,11 +15,6 @@
|
|
|
15
15
|
"bin": {
|
|
16
16
|
"leroy": "bin/leroy.mjs"
|
|
17
17
|
},
|
|
18
|
-
"dependencies": {
|
|
19
|
-
"cli-table3": "^0.6.5",
|
|
20
|
-
"ink": "^7.1.1",
|
|
21
|
-
"react": "^19.2.8"
|
|
22
|
-
},
|
|
23
18
|
"engines": {
|
|
24
19
|
"node": ">=22"
|
|
25
20
|
}
|
package/src/live-ui.mjs
CHANGED
|
@@ -1,84 +1,86 @@
|
|
|
1
|
-
import
|
|
2
|
-
import Table from "cli-table3";
|
|
3
|
-
import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
|
|
1
|
+
import { stdin as processInput, stdout as processOutput } from "node:process";
|
|
4
2
|
|
|
5
3
|
import { ansiForeground, LEROY_COLORS } from "./colors.mjs";
|
|
6
4
|
import { buildEvaluationViewModel } from "./format.mjs";
|
|
7
5
|
|
|
8
|
-
const COLORS = {
|
|
9
|
-
green: LEROY_COLORS.green,
|
|
10
|
-
red: LEROY_COLORS.red,
|
|
11
|
-
white: "white",
|
|
12
|
-
muted: LEROY_COLORS.neutral,
|
|
13
|
-
content: LEROY_COLORS.neutral,
|
|
14
|
-
neutral: LEROY_COLORS.neutral,
|
|
15
|
-
inactive: LEROY_COLORS.inactive,
|
|
16
|
-
border: LEROY_COLORS.inactive,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const MONOCHROME = Object.fromEntries(Object.keys(COLORS).map((key) => [key, undefined]));
|
|
20
6
|
const MAX_CARD_WIDTH = 67;
|
|
21
7
|
const MIN_CARD_WIDTH = 64;
|
|
22
8
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
23
9
|
const LOADING_STEP_DELAY_MS = 110;
|
|
24
10
|
const STRATEGY_COUNT = 41;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
inactive: `38;2;41;51;43`,
|
|
11
|
+
const STRATEGY_CELL_COUNT = 20;
|
|
12
|
+
|
|
13
|
+
const ANSI = {
|
|
14
|
+
reset: "\u001b[0m",
|
|
15
|
+
bold: "\u001b[1m",
|
|
16
|
+
white: "\u001b[37m",
|
|
17
|
+
muted: "\u001b[90m",
|
|
18
|
+
green: ansiForeground(LEROY_COLORS.green),
|
|
19
|
+
red: ansiForeground(LEROY_COLORS.red),
|
|
20
|
+
neutral: ansiForeground(LEROY_COLORS.neutral),
|
|
21
|
+
inactive: ansiForeground(LEROY_COLORS.inactive),
|
|
22
|
+
yellow: "\u001b[33m",
|
|
23
|
+
hideCursor: "\u001b[?25l",
|
|
24
|
+
showCursor: "\u001b[?25h",
|
|
40
25
|
};
|
|
41
26
|
|
|
42
|
-
function
|
|
27
|
+
function record(value) {
|
|
28
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clamp(value, minimum, maximum) {
|
|
32
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function terminalColumns(output = processOutput) {
|
|
36
|
+
return Number.isFinite(output.columns) && output.columns > 0 ? output.columns : 80;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function layoutFor(columns) {
|
|
40
|
+
const width = clamp(columns - 4, MIN_CARD_WIDTH, MAX_CARD_WIDTH);
|
|
43
41
|
return {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
bottom: "",
|
|
49
|
-
"bottom-mid": "",
|
|
50
|
-
"bottom-left": "",
|
|
51
|
-
"bottom-right": "",
|
|
52
|
-
left: "",
|
|
53
|
-
"left-mid": "",
|
|
54
|
-
mid: "",
|
|
55
|
-
"mid-mid": "",
|
|
56
|
-
right: "",
|
|
57
|
-
"right-mid": "",
|
|
58
|
-
middle: "",
|
|
42
|
+
width,
|
|
43
|
+
innerWidth: width - 4,
|
|
44
|
+
sectionWidth: width - 6,
|
|
45
|
+
padding: " ",
|
|
59
46
|
};
|
|
60
47
|
}
|
|
61
48
|
|
|
62
|
-
function
|
|
63
|
-
|
|
49
|
+
function style(value, tones, colorEnabled) {
|
|
50
|
+
const text = String(value);
|
|
51
|
+
if (!colorEnabled || !tones?.length) return text;
|
|
52
|
+
const prefix = tones.map((tone) => ANSI[tone] ?? "").join("");
|
|
53
|
+
return prefix ? `${prefix}${text}${ANSI.reset}` : text;
|
|
64
54
|
}
|
|
65
55
|
|
|
66
|
-
function
|
|
67
|
-
return value
|
|
56
|
+
function tone(value, colorName, colorEnabled, bold = false) {
|
|
57
|
+
return style(value, [bold ? "bold" : null, colorName].filter(Boolean), colorEnabled);
|
|
68
58
|
}
|
|
69
59
|
|
|
70
|
-
function
|
|
71
|
-
return
|
|
60
|
+
function visibleLength(value) {
|
|
61
|
+
return String(value).replace(/\u001b\[[0-9;?]*[A-Za-z]/g, "").length;
|
|
72
62
|
}
|
|
73
63
|
|
|
74
|
-
function
|
|
75
|
-
const
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
64
|
+
function alignCell(cell, width) {
|
|
65
|
+
const padding = " ".repeat(Math.max(0, width - visibleLength(cell.content)));
|
|
66
|
+
return cell.hAlign === "right" ? `${padding}${cell.content}` : `${cell.content}${padding}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function tableValue(value, colorName, colorEnabled, { bold = false, hAlign = "left" } = {}) {
|
|
70
|
+
return { content: tone(value, colorName, colorEnabled, bold), hAlign };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function tableColumnWidths(rows) {
|
|
74
|
+
return rows[0].map((_, columnIndex) => Math.max(
|
|
75
|
+
...rows.map((row) => visibleLength(row[columnIndex]?.content ?? "")),
|
|
76
|
+
) + 2);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderTable(rows, colWidths, colorEnabled, { divider = true } = {}) {
|
|
80
|
+
const lines = rows.map((row) => row.map((cell, index) => alignCell(cell, colWidths[index])).join("").trimEnd());
|
|
81
|
+
if (!divider) return lines;
|
|
82
|
+
const dividerLine = tone("─".repeat(Math.max(1, colWidths.reduce((total, width) => total + width, 0))), "inactive", colorEnabled);
|
|
83
|
+
return [lines[0], dividerLine, ...lines.slice(1)];
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
function numberLabel(value, digits = 2) {
|
|
@@ -108,14 +110,11 @@ function signedPercent(value) {
|
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
function absolutePercent(value) {
|
|
111
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
112
|
-
? `${Math.abs(value).toFixed(2)}%`
|
|
113
|
-
: null;
|
|
113
|
+
return typeof value === "number" && Number.isFinite(value) ? `${Math.abs(value).toFixed(2)}%` : null;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
function marketVolumeLabel(value) {
|
|
117
|
-
|
|
118
|
-
return `${value.toFixed(2)}x`;
|
|
117
|
+
return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(2)}x` : "—";
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
function metricTone(value) {
|
|
@@ -123,47 +122,12 @@ function metricTone(value) {
|
|
|
123
122
|
return value > 0 ? "green" : "red";
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
function tableContent(value, tone, colorEnabled, bold = false) {
|
|
127
|
-
const text = String(value);
|
|
128
|
-
const code = ANSI_CODES[tone];
|
|
129
|
-
return colorEnabled && code
|
|
130
|
-
? `${bold ? "\u001b[1m" : ""}\u001b[${code}m${text}\u001b[39m${bold ? "\u001b[22m" : ""}`
|
|
131
|
-
: text;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function tableValue(value, tone, colorEnabled, { bold = false, hAlign = "left" } = {}) {
|
|
135
|
-
const content = tableContent(value, tone, colorEnabled, bold);
|
|
136
|
-
return { content, hAlign };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function visibleLength(value) {
|
|
140
|
-
return String(value).replace(/\u001b\[[0-9;?]*[A-Za-z]/g, "").length;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function renderTable(rows, colWidths, colorEnabled, { divider = true } = {}) {
|
|
144
|
-
const table = new Table({ chars: tableChars(), style: TABLE_STYLE, colWidths });
|
|
145
|
-
table.push(...rows);
|
|
146
|
-
const lines = table.toString().split("\n").map((line) => line.trimEnd());
|
|
147
|
-
if (!divider) return lines.join("\n");
|
|
148
|
-
const dividerLine = "─".repeat(Math.max(1, colWidths.reduce((total, width) => total + width, 0)));
|
|
149
|
-
const coloredDivider = colorEnabled
|
|
150
|
-
? `${ansiForeground(LEROY_COLORS.inactive)}${dividerLine}\u001b[39m`
|
|
151
|
-
: dividerLine;
|
|
152
|
-
return [lines[0], coloredDivider, ...lines.slice(1)].join("\n");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function tableColumnWidths(rows) {
|
|
156
|
-
return rows[0].map((_, columnIndex) => Math.max(
|
|
157
|
-
...rows.map((row) => visibleLength(row[columnIndex]?.content ?? "")),
|
|
158
|
-
) + 2);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
125
|
function hasMarketMetrics(setup) {
|
|
162
126
|
return [setup?.sessionReturnPct, setup?.volumeRatio, setup?.vwapDistancePct, setup?.atrPct]
|
|
163
127
|
.some((value) => typeof value === "number" && Number.isFinite(value));
|
|
164
128
|
}
|
|
165
129
|
|
|
166
|
-
function
|
|
130
|
+
function renderMarketSetupTable({ symbol, setup, market, colorEnabled }) {
|
|
167
131
|
const columns = [
|
|
168
132
|
{ symbol, setup },
|
|
169
133
|
...(hasMarketMetrics(market) ? [{ symbol: market.symbol, setup: market }] : []),
|
|
@@ -200,15 +164,7 @@ function MarketSetupTable({ symbol, setup, market, colorEnabled }) {
|
|
|
200
164
|
...columns.map((column) => tableValue(absolutePercent(column.setup.atrPct) ?? "—", "white", colorEnabled, { bold: true })),
|
|
201
165
|
],
|
|
202
166
|
];
|
|
203
|
-
return
|
|
204
|
-
React.createElement(Text, null, renderTable(rows, tableColumnWidths(rows), colorEnabled)),
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function strategyLabel(text, tone, alignment, width) {
|
|
209
|
-
return React.createElement(Box, { key: text, width: `${100 / width}%`, justifyContent: alignment },
|
|
210
|
-
React.createElement(Text, { color: tone }, text),
|
|
211
|
-
);
|
|
167
|
+
return renderTable(rows, tableColumnWidths(rows), colorEnabled);
|
|
212
168
|
}
|
|
213
169
|
|
|
214
170
|
function proportionalSignalCells(segments, total) {
|
|
@@ -232,59 +188,50 @@ function proportionalSignalCells(segments, total) {
|
|
|
232
188
|
return allocations.flatMap((segment) => Array.from({ length: segment.count }, () => segment.key));
|
|
233
189
|
}
|
|
234
190
|
|
|
235
|
-
function
|
|
236
|
-
const
|
|
191
|
+
function alignedLabel(text, alignment, width) {
|
|
192
|
+
const space = Math.max(0, width - visibleLength(text));
|
|
193
|
+
if (alignment === "right") return `${" ".repeat(space)}${text}`;
|
|
194
|
+
if (alignment === "center") {
|
|
195
|
+
const left = Math.floor(space / 2);
|
|
196
|
+
return `${" ".repeat(left)}${text}${" ".repeat(space - left)}`;
|
|
197
|
+
}
|
|
198
|
+
return `${text}${" ".repeat(space)}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function renderStrategyMix(strategy, colorEnabled) {
|
|
237
202
|
const neutralCount = strategy.counts.neutral + strategy.counts.warming;
|
|
238
203
|
const segments = [
|
|
239
|
-
{ key: "buy", count: strategy.counts.buy,
|
|
240
|
-
{ key: "neutral", count: neutralCount,
|
|
241
|
-
{ key: "sell", count: strategy.counts.sell,
|
|
204
|
+
{ key: "buy", count: strategy.counts.buy, colorName: "green" },
|
|
205
|
+
{ key: "neutral", count: neutralCount, colorName: "neutral" },
|
|
206
|
+
{ key: "sell", count: strategy.counts.sell, colorName: "red" },
|
|
242
207
|
];
|
|
243
208
|
const total = segments.reduce((sum, segment) => sum + segment.count, 0);
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
...
|
|
248
|
-
...Array.from({ length: STRATEGY_CELL_COUNT -
|
|
249
|
-
];
|
|
209
|
+
const toneByState = Object.fromEntries(segments.map((segment) => [segment.key, segment.colorName]));
|
|
210
|
+
const activeStates = proportionalSignalCells(segments, total);
|
|
211
|
+
const states = [
|
|
212
|
+
...activeStates,
|
|
213
|
+
...Array.from({ length: STRATEGY_CELL_COUNT - activeStates.length }, () => null),
|
|
214
|
+
].slice(0, STRATEGY_CELL_COUNT);
|
|
215
|
+
const cells = states.map((state, index) => `${tone(
|
|
216
|
+
strengthCellGlyph(Boolean(state), colorEnabled),
|
|
217
|
+
state ? toneByState[state] ?? "neutral" : "inactive",
|
|
218
|
+
colorEnabled,
|
|
219
|
+
)}${index < states.length - 1 ? " " : ""}`).join("");
|
|
250
220
|
const rowWidth = STRATEGY_CELL_COUNT * 2 + STRATEGY_CELL_COUNT - 1;
|
|
251
|
-
const strategyRow = React.createElement(Text, {
|
|
252
|
-
width: rowWidth,
|
|
253
|
-
}, paddedStates.map((state, cellIndex) => React.createElement(Text, {
|
|
254
|
-
key: `strategy-cell-${cellIndex}`,
|
|
255
|
-
color: state ? toneByState[state] ?? colors.neutral : colors.inactive,
|
|
256
|
-
}, `${strengthCellGlyph(Boolean(state), colorEnabled)}${cellIndex < paddedStates.length - 1 ? " " : ""}`)));
|
|
257
221
|
const labels = segments.map((segment) => `${segment.key[0].toUpperCase()}${segment.key.slice(1)} ${segment.count}`);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
index === 0 ? "flex-start" : index === labels.length - 1 ? "flex-end" : "center",
|
|
266
|
-
segments.length,
|
|
267
|
-
)),
|
|
268
|
-
),
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function StatusLine({ icon, iconColor, textColor, children, marginTop = 0 }) {
|
|
273
|
-
return React.createElement(Box, { flexDirection: "row", marginTop },
|
|
274
|
-
React.createElement(Box, { width: 3 },
|
|
275
|
-
React.createElement(Text, { color: iconColor, bold: true }, icon),
|
|
276
|
-
),
|
|
277
|
-
React.createElement(Text, { color: textColor }, children),
|
|
278
|
-
);
|
|
222
|
+
const labelWidth = (rowWidth + 1) / labels.length;
|
|
223
|
+
const labelLine = labels.map((label, index) => tone(
|
|
224
|
+
alignedLabel(label, index === 0 ? "left" : index === labels.length - 1 ? "right" : "center", labelWidth),
|
|
225
|
+
segments[index].colorName,
|
|
226
|
+
colorEnabled,
|
|
227
|
+
)).join("");
|
|
228
|
+
return [cells, "", labelLine];
|
|
279
229
|
}
|
|
280
230
|
|
|
281
|
-
function
|
|
282
|
-
const colors = palette(colorEnabled);
|
|
231
|
+
function renderOutcomeTable(outcomes, colorEnabled) {
|
|
283
232
|
const hasHistoricalOutcomes = outcomes.some((outcome) => [outcome.mean, outcome.winRate, outcome.liftBps]
|
|
284
233
|
.some((value) => typeof value === "number" && Number.isFinite(value)));
|
|
285
|
-
if (!hasHistoricalOutcomes)
|
|
286
|
-
return React.createElement(Text, { color: colors.content }, "No historical outcomes available.");
|
|
287
|
-
}
|
|
234
|
+
if (!hasHistoricalOutcomes) return [tone("No historical outcomes available.", "neutral", colorEnabled)];
|
|
288
235
|
const rows = [
|
|
289
236
|
[
|
|
290
237
|
tableValue("Timeframe", "muted", colorEnabled, { bold: true }),
|
|
@@ -303,44 +250,13 @@ function OutcomeTable({ outcomes, colorEnabled }) {
|
|
|
303
250
|
...outcomes.map((outcome) => tableValue(bpsLabel(outcome.liftBps), metricTone(outcome.liftBps), colorEnabled, { bold: true, hAlign: "right" })),
|
|
304
251
|
],
|
|
305
252
|
];
|
|
306
|
-
return
|
|
307
|
-
React.createElement(Text, null, renderTable(rows, tableColumnWidths(rows), colorEnabled)),
|
|
308
|
-
);
|
|
253
|
+
return renderTable(rows, tableColumnWidths(rows), colorEnabled);
|
|
309
254
|
}
|
|
310
255
|
|
|
311
|
-
function sideTone(side
|
|
312
|
-
if (side === "buy") return
|
|
313
|
-
if (side === "sell") return
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function SectionHeading({ label, meta, colors, width = "100%" }) {
|
|
318
|
-
return React.createElement(Box, {
|
|
319
|
-
flexDirection: "row",
|
|
320
|
-
width,
|
|
321
|
-
justifyContent: "space-between",
|
|
322
|
-
},
|
|
323
|
-
React.createElement(Box, { flexDirection: "row", flexGrow: 1 },
|
|
324
|
-
React.createElement(Box, { width: 2 },
|
|
325
|
-
React.createElement(Text, { color: colors.green, bold: true }, "▲"),
|
|
326
|
-
),
|
|
327
|
-
React.createElement(Text, { color: colors.white, bold: true }, label),
|
|
328
|
-
),
|
|
329
|
-
meta ? React.createElement(Text, { color: colors.white, bold: true }, meta) : null,
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function SectionBody({ children, colors, paddingLeft = 1 }) {
|
|
334
|
-
return React.createElement(Box, {
|
|
335
|
-
flexDirection: "column",
|
|
336
|
-
width: "100%",
|
|
337
|
-
borderStyle: "single",
|
|
338
|
-
borderColor: colors.border,
|
|
339
|
-
borderTop: false,
|
|
340
|
-
borderBottom: false,
|
|
341
|
-
borderRight: false,
|
|
342
|
-
paddingLeft,
|
|
343
|
-
}, children);
|
|
256
|
+
function sideTone(side) {
|
|
257
|
+
if (side === "buy") return "green";
|
|
258
|
+
if (side === "sell") return "red";
|
|
259
|
+
return "white";
|
|
344
260
|
}
|
|
345
261
|
|
|
346
262
|
function fallbackMarketSessionStatus(value = new Date()) {
|
|
@@ -363,293 +279,335 @@ function fallbackMarketSessionStatus(value = new Date()) {
|
|
|
363
279
|
: "closed";
|
|
364
280
|
}
|
|
365
281
|
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
width: "100%",
|
|
370
|
-
borderStyle: "single",
|
|
371
|
-
borderColor: colors.border,
|
|
372
|
-
borderTop: false,
|
|
373
|
-
borderBottom: false,
|
|
374
|
-
borderRight: false,
|
|
375
|
-
});
|
|
282
|
+
function fitColumns(left, right, width) {
|
|
283
|
+
if (!right) return left;
|
|
284
|
+
return `${left}${" ".repeat(Math.max(1, width - visibleLength(left) - visibleLength(right)))}${right}`;
|
|
376
285
|
}
|
|
377
286
|
|
|
378
|
-
function
|
|
287
|
+
function renderQueryHeader(view, colorEnabled, layout) {
|
|
379
288
|
const side = typeof view.side === "string" ? view.side.toUpperCase() : "MARKET";
|
|
380
289
|
const marketSessionStatus = view.marketSessionStatus ?? fallbackMarketSessionStatus();
|
|
381
290
|
const marketIsOpen = marketSessionStatus === "open";
|
|
382
|
-
const timestamp = [view.frameDate, view.frameTime]
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
return
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
function HistoricalEvidence({ view, colorEnabled }) {
|
|
291
|
+
const timestamp = [view.frameDate, view.frameTime].filter((value) => value && value !== "—").join(" · ") || "—";
|
|
292
|
+
const left = `${tone(side, sideTone(view.side), colorEnabled, true)} · ${tone(view.symbol, "white", colorEnabled, true)} @ ${tone(view.last, "white", colorEnabled)}`;
|
|
293
|
+
const status = `${tone("●", marketIsOpen ? "green" : "red", colorEnabled, true)}${tone(` Market ${marketIsOpen ? "Open" : "Closed"}`, "white", colorEnabled, true)}`;
|
|
294
|
+
return [
|
|
295
|
+
`${layout.padding}${fitColumns(left, tone(timestamp, "neutral", colorEnabled), layout.sectionWidth)}`,
|
|
296
|
+
`${layout.padding}${fitColumns("", status, layout.sectionWidth)}`,
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function renderSectionHeading(label, meta, colorEnabled, layout) {
|
|
301
|
+
const left = `${tone("▲", "green", colorEnabled, true)} ${tone(label, "white", colorEnabled, true)}`;
|
|
302
|
+
const right = meta ? tone(meta, "white", colorEnabled, true) : "";
|
|
303
|
+
return `${layout.padding}${fitColumns(left, right, layout.sectionWidth)}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function railLine(colorEnabled, layout) {
|
|
307
|
+
return `${layout.padding}${tone("│", "inactive", colorEnabled)}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function bodyLine(content, colorEnabled, layout) {
|
|
311
|
+
return `${layout.padding}${tone("│", "inactive", colorEnabled)} ${content}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function renderHistoricalEvidence(view, colorEnabled) {
|
|
408
315
|
const sampleLabel = view.match.score === 5
|
|
409
316
|
? " exact observations"
|
|
410
317
|
: view.match.score > 0
|
|
411
318
|
? " comparable observations"
|
|
412
319
|
: " observations";
|
|
413
320
|
const matchLabel = view.match.label === "No match" ? "No Match" : `${view.match.label} Match`;
|
|
414
|
-
const matchDetails = view.match.score > 0
|
|
415
|
-
? `${view.match.rowCount}${sampleLabel}`
|
|
416
|
-
: "No comparable observations.";
|
|
321
|
+
const matchDetails = view.match.score > 0 ? `${view.match.rowCount}${sampleLabel}` : "No comparable observations.";
|
|
417
322
|
const matchStrength = [
|
|
418
|
-
Array.from({ length: 5 }, (_, index) =>
|
|
323
|
+
Array.from({ length: 5 }, (_, index) => tone(
|
|
419
324
|
strengthCellGlyph(index < view.match.score, colorEnabled),
|
|
420
325
|
index < view.match.score ? "green" : "neutral",
|
|
421
326
|
colorEnabled,
|
|
422
327
|
)).join(" "),
|
|
423
|
-
|
|
328
|
+
tone(matchLabel, view.match.score > 0 ? "white" : "muted", colorEnabled, true),
|
|
424
329
|
].join(" ");
|
|
425
330
|
const rows = [
|
|
426
|
-
[
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
],
|
|
430
|
-
[
|
|
431
|
-
tableValue("Match Details", "muted", colorEnabled, { bold: true }),
|
|
432
|
-
tableValue(matchDetails, "content", colorEnabled, { bold: true }),
|
|
433
|
-
],
|
|
434
|
-
[
|
|
435
|
-
tableValue("Date Range", "muted", colorEnabled, { bold: true }),
|
|
436
|
-
tableValue(view.archiveWindow, "content", colorEnabled),
|
|
437
|
-
],
|
|
331
|
+
[tableValue("Match Strength", "muted", colorEnabled, { bold: true }), { content: matchStrength, hAlign: "left" }],
|
|
332
|
+
[tableValue("Match Details", "muted", colorEnabled, { bold: true }), tableValue(matchDetails, "neutral", colorEnabled, { bold: true })],
|
|
333
|
+
[tableValue("Date Range", "muted", colorEnabled, { bold: true }), tableValue(view.archiveWindow, "neutral", colorEnabled)],
|
|
438
334
|
];
|
|
439
|
-
return
|
|
335
|
+
return renderTable(rows, tableColumnWidths(rows), colorEnabled, { divider: false });
|
|
440
336
|
}
|
|
441
337
|
|
|
442
|
-
function
|
|
443
|
-
|
|
444
|
-
const width = terminalWidth ?? clamp(columns - 4, MIN_CARD_WIDTH, MAX_CARD_WIDTH);
|
|
445
|
-
const [spinnerIndex, setSpinnerIndex] = useState(0);
|
|
446
|
-
|
|
447
|
-
useEffect(() => {
|
|
448
|
-
if (progress.completed >= 4) return undefined;
|
|
449
|
-
const timer = setInterval(() => {
|
|
450
|
-
setSpinnerIndex((current) => (current + 1) % SPINNER_FRAMES.length);
|
|
451
|
-
}, 90);
|
|
452
|
-
return () => clearInterval(timer);
|
|
453
|
-
}, [colorEnabled, progress.completed]);
|
|
454
|
-
|
|
455
|
-
const steps = [
|
|
338
|
+
function loadingLabels(symbol, durationMs) {
|
|
339
|
+
return [
|
|
456
340
|
`Loading live market data for ${String(symbol).toUpperCase()}.`,
|
|
457
341
|
`Running ${STRATEGY_COUNT} strategy evaluations.`,
|
|
458
342
|
"Checking for historical matches.",
|
|
459
|
-
|
|
343
|
+
durationMs === null ? "Finishing…" : `Finished in ${durationMs}ms.`,
|
|
460
344
|
];
|
|
345
|
+
}
|
|
461
346
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}, steps.map((label, index) => {
|
|
347
|
+
export function renderLoadingSteps({ symbol, progress = { completed: 0, durationMs: null }, error = null, spinnerIndex = 0, colorEnabled = true, columns = 80 }) {
|
|
348
|
+
const layout = layoutFor(columns);
|
|
349
|
+
const labels = loadingLabels(symbol, progress.durationMs);
|
|
350
|
+
return ["", ...labels.map((label, index) => {
|
|
467
351
|
const isDone = index < progress.completed;
|
|
468
|
-
const isActive = index === progress.completed && progress.completed <
|
|
352
|
+
const isActive = index === progress.completed && progress.completed < labels.length;
|
|
469
353
|
const isFailed = Boolean(error) && isActive;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
function EvidenceView({ response, colorEnabled, columns, phase, updatedAt, error, progress }) {
|
|
482
|
-
const colors = palette(colorEnabled);
|
|
354
|
+
const icon = isFailed ? "!" : isDone ? "✓" : isActive ? SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length] : "·";
|
|
355
|
+
const iconColor = isFailed ? "red" : isDone ? "green" : isActive ? "white" : "muted";
|
|
356
|
+
const textColor = isDone || isFailed ? "white" : "muted";
|
|
357
|
+
const text = isFailed ? `${label} ${error}` : label;
|
|
358
|
+
return `${layout.padding}${tone(icon, iconColor, colorEnabled, true)} ${tone(text, textColor, colorEnabled)}`;
|
|
359
|
+
})].join("\n");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function renderEvidenceView(response, { colorEnabled = true, columns = 80, phase = "live", updatedAt = null, error = null, progress = { completed: 4, durationMs: null } } = {}) {
|
|
363
|
+
const layout = layoutFor(columns);
|
|
483
364
|
const view = buildEvaluationViewModel(response);
|
|
484
|
-
const width = clamp(columns - 4, MIN_CARD_WIDTH, MAX_CARD_WIDTH);
|
|
485
|
-
const sectionWidth = Math.max(1, width - 6);
|
|
486
365
|
const updatedText = updatedAt
|
|
487
366
|
? new Date(updatedAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })
|
|
488
367
|
: "—";
|
|
489
|
-
const liveTone = phase === "error" ?
|
|
368
|
+
const liveTone = phase === "error" ? "red" : phase === "checking" ? "yellow" : "green";
|
|
490
369
|
if (view.status === "artifact_unavailable") {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
370
|
+
const lines = ["", ""];
|
|
371
|
+
lines.push(`${layout.padding}${tone("!", liveTone, colorEnabled, true)} ${tone("Market Memory unavailable", "white", colorEnabled)}`);
|
|
372
|
+
lines.push("");
|
|
373
|
+
lines.push(`${layout.padding}${tone("The private evidence reader is unavailable. Retrying on the next tape check.", "neutral", colorEnabled)}`);
|
|
374
|
+
lines.push("");
|
|
375
|
+
lines.push(`${layout.padding}${tone(`updated ${updatedText} · q/esc to exit`, "neutral", colorEnabled)}`);
|
|
376
|
+
if (error) {
|
|
377
|
+
lines.push("");
|
|
378
|
+
lines.push(`${layout.padding}${tone("!", "red", colorEnabled, true)} ${tone(error, "white", colorEnabled)}`);
|
|
379
|
+
}
|
|
380
|
+
return lines.join("\n");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const lines = renderLoadingSteps({
|
|
384
|
+
symbol: view.symbol,
|
|
385
|
+
progress,
|
|
386
|
+
colorEnabled,
|
|
387
|
+
columns,
|
|
388
|
+
}).split("\n");
|
|
389
|
+
lines.push("");
|
|
390
|
+
lines.push(...renderQueryHeader(view, colorEnabled, layout));
|
|
391
|
+
lines.push("");
|
|
392
|
+
|
|
393
|
+
lines.push(renderSectionHeading("Strategy Signals", `${view.strategy.total} Evaluated`, colorEnabled, layout));
|
|
394
|
+
lines.push(railLine(colorEnabled, layout));
|
|
395
|
+
lines.push(...renderStrategyMix(view.strategy, colorEnabled).map((line) => bodyLine(line, colorEnabled, layout)));
|
|
396
|
+
lines.push(railLine(colorEnabled, layout));
|
|
397
|
+
|
|
398
|
+
lines.push(renderSectionHeading("Market Context", null, colorEnabled, layout));
|
|
399
|
+
lines.push(railLine(colorEnabled, layout));
|
|
400
|
+
lines.push(...renderMarketSetupTable({
|
|
401
|
+
symbol: view.symbol,
|
|
402
|
+
setup: view.marketSetup,
|
|
403
|
+
market: view.marketOverview,
|
|
404
|
+
colorEnabled,
|
|
405
|
+
}).map((line) => bodyLine(line, colorEnabled, layout)));
|
|
406
|
+
lines.push(railLine(colorEnabled, layout));
|
|
407
|
+
|
|
408
|
+
lines.push(renderSectionHeading("Historical Evidence", null, colorEnabled, layout));
|
|
409
|
+
lines.push(railLine(colorEnabled, layout));
|
|
410
|
+
lines.push(...renderHistoricalEvidence(view, colorEnabled).map((line) => bodyLine(line, colorEnabled, layout)));
|
|
411
|
+
lines.push(railLine(colorEnabled, layout));
|
|
412
|
+
|
|
413
|
+
lines.push(renderSectionHeading("Observed Outcomes", null, colorEnabled, layout));
|
|
414
|
+
lines.push(railLine(colorEnabled, layout));
|
|
415
|
+
lines.push(...renderOutcomeTable(view.outcomes, colorEnabled).map((line) => bodyLine(line, colorEnabled, layout)));
|
|
416
|
+
if (error) {
|
|
417
|
+
lines.push("");
|
|
418
|
+
lines.push(`${layout.padding}${tone("!", "red", colorEnabled, true)} ${tone(error, "white", colorEnabled)}`);
|
|
419
|
+
}
|
|
420
|
+
lines.push("", "");
|
|
421
|
+
return lines.join("\n");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function frameKey(response, view) {
|
|
425
|
+
const frame = record(response.current_market_frame);
|
|
426
|
+
return JSON.stringify({
|
|
427
|
+
sourceTime: view.frameSourceTime,
|
|
428
|
+
tapeId: frame.tape_id ?? frame.tapeId ?? null,
|
|
429
|
+
last: view.last,
|
|
430
|
+
status: view.status,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function createExitController() {
|
|
435
|
+
let stopped = false;
|
|
436
|
+
const listeners = new Set();
|
|
437
|
+
return {
|
|
438
|
+
get stopped() {
|
|
439
|
+
return stopped;
|
|
440
|
+
},
|
|
441
|
+
stop() {
|
|
442
|
+
if (stopped) return;
|
|
443
|
+
stopped = true;
|
|
444
|
+
for (const listener of listeners) listener();
|
|
445
|
+
listeners.clear();
|
|
446
|
+
},
|
|
447
|
+
wait(milliseconds) {
|
|
448
|
+
if (stopped) return Promise.resolve(true);
|
|
449
|
+
return new Promise((resolve) => {
|
|
450
|
+
const onStop = () => {
|
|
451
|
+
clearTimeout(timer);
|
|
452
|
+
resolve(true);
|
|
453
|
+
};
|
|
454
|
+
const timer = setTimeout(() => {
|
|
455
|
+
listeners.delete(onStop);
|
|
456
|
+
resolve(false);
|
|
457
|
+
}, milliseconds);
|
|
458
|
+
listeners.add(onStop);
|
|
459
|
+
});
|
|
495
460
|
},
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
class NativeTerminalRenderer {
|
|
465
|
+
constructor(output) {
|
|
466
|
+
this.output = output;
|
|
467
|
+
this.lineCount = 0;
|
|
468
|
+
this.started = false;
|
|
501
469
|
}
|
|
502
470
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}),
|
|
525
|
-
React.createElement(RailSpacer, { colors }),
|
|
526
|
-
React.createElement(SectionBody, { colors },
|
|
527
|
-
React.createElement(StrategyMix, {
|
|
528
|
-
strategy: view.strategy,
|
|
529
|
-
colorEnabled,
|
|
530
|
-
}),
|
|
531
|
-
),
|
|
532
|
-
),
|
|
533
|
-
React.createElement(RailSpacer, { colors }),
|
|
534
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
535
|
-
React.createElement(SectionHeading, { label: "Market Context", colors, width: sectionWidth }),
|
|
536
|
-
React.createElement(RailSpacer, { colors }),
|
|
537
|
-
React.createElement(SectionBody, { colors },
|
|
538
|
-
React.createElement(MarketSetupTable, {
|
|
539
|
-
symbol: view.symbol,
|
|
540
|
-
setup: view.marketSetup,
|
|
541
|
-
market: view.marketOverview,
|
|
542
|
-
colorEnabled,
|
|
543
|
-
}),
|
|
544
|
-
),
|
|
545
|
-
),
|
|
546
|
-
React.createElement(RailSpacer, { colors }),
|
|
547
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
548
|
-
React.createElement(SectionHeading, { label: "Historical Evidence", colors, width: sectionWidth }),
|
|
549
|
-
React.createElement(RailSpacer, { colors }),
|
|
550
|
-
React.createElement(SectionBody, { colors },
|
|
551
|
-
React.createElement(HistoricalEvidence, { view, colorEnabled }),
|
|
552
|
-
),
|
|
553
|
-
),
|
|
554
|
-
React.createElement(RailSpacer, { colors }),
|
|
555
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
556
|
-
React.createElement(SectionHeading, { label: "Observed Outcomes", colors, width: sectionWidth }),
|
|
557
|
-
React.createElement(RailSpacer, { colors }),
|
|
558
|
-
React.createElement(SectionBody, { colors },
|
|
559
|
-
React.createElement(OutcomeTable, {
|
|
560
|
-
outcomes: view.outcomes,
|
|
561
|
-
colorEnabled,
|
|
562
|
-
}),
|
|
563
|
-
),
|
|
564
|
-
),
|
|
565
|
-
error ? React.createElement(StatusLine, { icon: "!", iconColor: colors.red, textColor: colors.white, marginTop: 1 }, error) : null,
|
|
566
|
-
React.createElement(Box, { height: 2 }),
|
|
567
|
-
);
|
|
471
|
+
draw(frame) {
|
|
472
|
+
if (!this.output.isTTY) {
|
|
473
|
+
this.output.write(`${frame}\n`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (!this.started) {
|
|
477
|
+
this.output.write(ANSI.hideCursor);
|
|
478
|
+
this.started = true;
|
|
479
|
+
}
|
|
480
|
+
if (this.lineCount > 0) this.output.write(`\u001b[${this.lineCount}F`);
|
|
481
|
+
this.output.write("\u001b[J");
|
|
482
|
+
this.output.write(`${frame}\n`);
|
|
483
|
+
const columns = terminalColumns(this.output);
|
|
484
|
+
this.lineCount = frame.split("\n").reduce((total, line) => (
|
|
485
|
+
total + Math.max(1, Math.ceil(visibleLength(line) / columns))
|
|
486
|
+
), 0);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
finish() {
|
|
490
|
+
if (this.started) this.output.write(ANSI.showCursor);
|
|
491
|
+
}
|
|
568
492
|
}
|
|
569
493
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const
|
|
573
|
-
const
|
|
494
|
+
function attachExitKeys(input, controller) {
|
|
495
|
+
if (!input.isTTY || typeof input.on !== "function") return () => {};
|
|
496
|
+
const canSetRawMode = typeof input.setRawMode === "function";
|
|
497
|
+
const wasRaw = Boolean(input.isRaw);
|
|
498
|
+
const onData = (chunk) => {
|
|
499
|
+
const key = String(chunk);
|
|
500
|
+
if (key === "\u0003" || key === "\u001b" || key.toLowerCase() === "q") controller.stop();
|
|
501
|
+
};
|
|
502
|
+
if (canSetRawMode) input.setRawMode(true);
|
|
503
|
+
input.resume();
|
|
504
|
+
input.on("data", onData);
|
|
505
|
+
return () => {
|
|
506
|
+
input.off("data", onData);
|
|
507
|
+
if (canSetRawMode) input.setRawMode(wasRaw);
|
|
508
|
+
if (typeof input.pause === "function") input.pause();
|
|
509
|
+
};
|
|
510
|
+
}
|
|
574
511
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
512
|
+
async function loadEvidence({ client, request, response, renderer, colorEnabled, columns, controller }) {
|
|
513
|
+
const startedAt = Date.now();
|
|
514
|
+
let spinnerIndex = 0;
|
|
515
|
+
let progress = { completed: response ? 4 : 0, durationMs: response ? 0 : null };
|
|
516
|
+
if (!response) renderer.draw(renderLoadingSteps({ symbol: request.symbol, progress, spinnerIndex, colorEnabled, columns }));
|
|
517
|
+
|
|
518
|
+
const evaluation = Promise.resolve()
|
|
519
|
+
.then(() => client.evaluate(request))
|
|
520
|
+
.then((nextResponse) => ({ kind: "response", response: nextResponse }))
|
|
521
|
+
.catch((error) => ({ kind: "error", error }));
|
|
522
|
+
|
|
523
|
+
let result;
|
|
524
|
+
while (!controller.stopped && !result) {
|
|
525
|
+
result = await Promise.race([
|
|
526
|
+
evaluation,
|
|
527
|
+
controller.wait(90).then((stopped) => stopped ? { kind: "stopped" } : { kind: "tick" }),
|
|
528
|
+
]);
|
|
529
|
+
if (result.kind === "tick") {
|
|
530
|
+
spinnerIndex = (spinnerIndex + 1) % SPINNER_FRAMES.length;
|
|
531
|
+
if (!response) renderer.draw(renderLoadingSteps({ symbol: request.symbol, progress, spinnerIndex, colorEnabled, columns }));
|
|
532
|
+
result = null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (controller.stopped || result?.kind === "stopped") return { stopped: true };
|
|
536
|
+
if (result.kind === "error") {
|
|
537
|
+
const message = result.error instanceof Error ? result.error.message : String(result.error);
|
|
538
|
+
if (response) {
|
|
539
|
+
renderer.draw(renderEvidenceView(response, { colorEnabled, columns, phase: "error", error: message, progress }));
|
|
540
|
+
} else {
|
|
541
|
+
renderer.draw(renderLoadingSteps({ symbol: request.symbol, progress: { completed: 0, durationMs: null }, error: message, spinnerIndex, colorEnabled, columns }));
|
|
542
|
+
}
|
|
543
|
+
return { error: message };
|
|
544
|
+
}
|
|
578
545
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
546
|
+
const durationMs = Date.now() - startedAt;
|
|
547
|
+
for (let completed = 1; completed <= 4; completed += 1) {
|
|
548
|
+
progress = { completed, durationMs: completed === 4 ? durationMs : progress.durationMs };
|
|
549
|
+
if (response) {
|
|
550
|
+
renderer.draw(renderEvidenceView(response, { colorEnabled, columns, phase: "checking", progress }));
|
|
551
|
+
} else {
|
|
552
|
+
renderer.draw(renderLoadingSteps({ symbol: request.symbol, progress, spinnerIndex, colorEnabled, columns }));
|
|
553
|
+
}
|
|
554
|
+
if (completed < 4 && await controller.wait(LOADING_STEP_DELAY_MS)) return { stopped: true };
|
|
555
|
+
}
|
|
556
|
+
if (await controller.wait(LOADING_STEP_DELAY_MS)) return { stopped: true };
|
|
557
|
+
return { response: result.response, durationMs, progress };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export async function renderLiveEvidence({
|
|
561
|
+
client,
|
|
562
|
+
request,
|
|
563
|
+
colorEnabled = true,
|
|
564
|
+
intervalMs = 60_000,
|
|
565
|
+
polling = true,
|
|
566
|
+
input = processInput,
|
|
567
|
+
output = processOutput,
|
|
568
|
+
} = {}) {
|
|
569
|
+
const renderer = new NativeTerminalRenderer(output);
|
|
570
|
+
const controller = createExitController();
|
|
571
|
+
const detachKeys = attachExitKeys(input, controller);
|
|
572
|
+
let response = null;
|
|
573
|
+
let updatedAt = null;
|
|
574
|
+
let currentFrameKey = null;
|
|
575
|
+
let progress = { completed: 0, durationMs: null };
|
|
576
|
+
try {
|
|
577
|
+
do {
|
|
578
|
+
const loaded = await loadEvidence({
|
|
579
|
+
client,
|
|
580
|
+
request,
|
|
581
|
+
response,
|
|
582
|
+
renderer,
|
|
583
|
+
colorEnabled,
|
|
584
|
+
columns: terminalColumns(output),
|
|
585
|
+
controller,
|
|
586
|
+
});
|
|
587
|
+
if (loaded.stopped) break;
|
|
588
|
+
if (loaded.response) {
|
|
589
|
+
response = loaded.response;
|
|
590
|
+
progress = loaded.progress;
|
|
590
591
|
const view = buildEvaluationViewModel(response);
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
progress: { completed, durationMs: completed === 4 ? durationMs : current.progress.durationMs },
|
|
598
|
-
}));
|
|
599
|
-
if (completed < 4) await new Promise((resolve) => setTimeout(resolve, LOADING_STEP_DELAY_MS));
|
|
600
|
-
}
|
|
601
|
-
if (!active) return;
|
|
602
|
-
await new Promise((resolve) => setTimeout(resolve, LOADING_STEP_DELAY_MS));
|
|
603
|
-
if (!active) return;
|
|
604
|
-
setState((current) => ({
|
|
605
|
-
response,
|
|
592
|
+
const nextFrameKey = frameKey(response, view);
|
|
593
|
+
updatedAt = currentFrameKey === nextFrameKey ? updatedAt : Date.now();
|
|
594
|
+
currentFrameKey = nextFrameKey;
|
|
595
|
+
renderer.draw(renderEvidenceView(response, {
|
|
596
|
+
colorEnabled,
|
|
597
|
+
columns: terminalColumns(output),
|
|
606
598
|
phase: "live",
|
|
607
|
-
updatedAt
|
|
608
|
-
|
|
609
|
-
updateCount: current.frameKey === nextKey ? current.updateCount : current.updateCount + 1,
|
|
610
|
-
error: null,
|
|
611
|
-
progress: { completed: 4, durationMs },
|
|
599
|
+
updatedAt,
|
|
600
|
+
progress,
|
|
612
601
|
}));
|
|
613
|
-
} catch (error) {
|
|
614
|
-
if (!active) return;
|
|
615
|
-
setState((current) => ({ ...current, phase: "error", updatedAt: Date.now(), error: error instanceof Error ? error.message : String(error) }));
|
|
616
|
-
} finally {
|
|
617
|
-
busy = false;
|
|
618
602
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
useEffect(() => {
|
|
629
|
-
if (polling || !state.response) return undefined;
|
|
630
|
-
const timer = setTimeout(() => exit(), 120);
|
|
631
|
-
return () => clearTimeout(timer);
|
|
632
|
-
}, [exit, polling, state.response]);
|
|
633
|
-
|
|
634
|
-
if (!state.response) {
|
|
635
|
-
return React.createElement(LoadingSteps, {
|
|
636
|
-
columns,
|
|
637
|
-
colorEnabled,
|
|
638
|
-
symbol: request.symbol,
|
|
639
|
-
progress: state.progress,
|
|
640
|
-
error: state.error,
|
|
641
|
-
});
|
|
603
|
+
if (!polling) {
|
|
604
|
+
await controller.wait(120);
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
if (await controller.wait(intervalMs)) break;
|
|
608
|
+
} while (!controller.stopped);
|
|
609
|
+
} finally {
|
|
610
|
+
detachKeys();
|
|
611
|
+
renderer.finish();
|
|
642
612
|
}
|
|
643
|
-
|
|
644
|
-
return React.createElement(EvidenceView, {
|
|
645
|
-
response: state.response,
|
|
646
|
-
colorEnabled,
|
|
647
|
-
columns,
|
|
648
|
-
phase: state.phase,
|
|
649
|
-
updatedAt: state.updatedAt,
|
|
650
|
-
error: state.error,
|
|
651
|
-
progress: state.progress,
|
|
652
|
-
});
|
|
653
613
|
}
|
|
654
|
-
|
|
655
|
-
export { render };
|