@hs-x/cli 0.4.11 → 0.4.13
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/README.md +21 -7
- package/dist/cloudflare-workers-logs.d.ts +1 -0
- package/dist/cloudflare-workers-logs.d.ts.map +1 -1
- package/dist/cloudflare-workers-logs.js +14 -3
- package/dist/cloudflare-workers-logs.js.map +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +3 -0
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +2 -1
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/deploy.d.ts +16 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +95 -13
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +368 -16
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +22 -6
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/project-lifecycle.d.ts +10 -0
- package/dist/commands/project-lifecycle.d.ts.map +1 -1
- package/dist/commands/project-lifecycle.js +120 -33
- package/dist/commands/project-lifecycle.js.map +1 -1
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +2 -0
- package/dist/commands/project.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dev/account-selection.d.ts +55 -0
- package/dist/dev/account-selection.d.ts.map +1 -1
- package/dist/dev/account-selection.js +120 -0
- package/dist/dev/account-selection.js.map +1 -1
- package/dist/dev/browser-log-bridge.d.ts +13 -8
- package/dist/dev/browser-log-bridge.d.ts.map +1 -1
- package/dist/dev/browser-log-bridge.js +37 -17
- package/dist/dev/browser-log-bridge.js.map +1 -1
- package/dist/dev/clipboard.d.ts +9 -0
- package/dist/dev/clipboard.d.ts.map +1 -0
- package/dist/dev/clipboard.js +42 -0
- package/dist/dev/clipboard.js.map +1 -0
- package/dist/dev/compat-shim.d.ts +19 -4
- package/dist/dev/compat-shim.d.ts.map +1 -1
- package/dist/dev/compat-shim.js +31 -4
- package/dist/dev/compat-shim.js.map +1 -1
- package/dist/dev/component-selection.d.ts +11 -0
- package/dist/dev/component-selection.d.ts.map +1 -1
- package/dist/dev/component-selection.js +23 -0
- package/dist/dev/component-selection.js.map +1 -1
- package/dist/dev/frontend-log-relay.d.ts +12 -0
- package/dist/dev/frontend-log-relay.d.ts.map +1 -1
- package/dist/dev/frontend-log-relay.js +31 -20
- package/dist/dev/frontend-log-relay.js.map +1 -1
- package/dist/dev/hubspot-logs-poller.d.ts +7 -1
- package/dist/dev/hubspot-logs-poller.d.ts.map +1 -1
- package/dist/dev/hubspot-logs-poller.js +28 -1
- package/dist/dev/hubspot-logs-poller.js.map +1 -1
- package/dist/dev/keyboard.d.ts +21 -3
- package/dist/dev/keyboard.d.ts.map +1 -1
- package/dist/dev/keyboard.js +96 -4
- package/dist/dev/keyboard.js.map +1 -1
- package/dist/dev/local-proxy.d.ts +90 -0
- package/dist/dev/local-proxy.d.ts.map +1 -0
- package/dist/dev/local-proxy.js +186 -0
- package/dist/dev/local-proxy.js.map +1 -0
- package/dist/dev/log-server.d.ts +9 -0
- package/dist/dev/log-server.d.ts.map +1 -1
- package/dist/dev/log-server.js +46 -3
- package/dist/dev/log-server.js.map +1 -1
- package/dist/dev/production-app-check.d.ts +23 -0
- package/dist/dev/production-app-check.d.ts.map +1 -0
- package/dist/dev/production-app-check.js +28 -0
- package/dist/dev/production-app-check.js.map +1 -0
- package/dist/dev/session-manager.d.ts +49 -1
- package/dist/dev/session-manager.d.ts.map +1 -1
- package/dist/dev/session-manager.js +152 -12
- package/dist/dev/session-manager.js.map +1 -1
- package/dist/dev/stream-event-format.d.ts +10 -1
- package/dist/dev/stream-event-format.d.ts.map +1 -1
- package/dist/dev/stream-event-format.js +157 -5
- package/dist/dev/stream-event-format.js.map +1 -1
- package/dist/dev/stream-renderer.d.ts +34 -1
- package/dist/dev/stream-renderer.d.ts.map +1 -1
- package/dist/dev/stream-renderer.js +371 -60
- package/dist/dev/stream-renderer.js.map +1 -1
- package/dist/dev/trace-markdown.d.ts +25 -0
- package/dist/dev/trace-markdown.d.ts.map +1 -0
- package/dist/dev/trace-markdown.js +159 -0
- package/dist/dev/trace-markdown.js.map +1 -0
- package/dist/errors-registry.d.ts.map +1 -1
- package/dist/errors-registry.js +15 -0
- package/dist/errors-registry.js.map +1 -1
- package/dist/hubspot-app-logs.d.ts +9 -0
- package/dist/hubspot-app-logs.d.ts.map +1 -1
- package/dist/hubspot-app-logs.js +6 -4
- package/dist/hubspot-app-logs.js.map +1 -1
- package/dist/hubspot-auth.d.ts.map +1 -1
- package/dist/hubspot-auth.js +15 -4
- package/dist/hubspot-auth.js.map +1 -1
- package/dist/hubspot-developer-client.js +3 -0
- package/dist/hubspot-developer-client.js.map +1 -1
- package/dist/hubspot-project.d.ts +2 -0
- package/dist/hubspot-project.d.ts.map +1 -1
- package/dist/hubspot-project.js +13 -1
- package/dist/hubspot-project.js.map +1 -1
- package/dist/prompt.d.ts +6 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +20 -0
- package/dist/prompt.js.map +1 -1
- package/dist/reporter/human.d.ts.map +1 -1
- package/dist/reporter/human.js +6 -1
- package/dist/reporter/human.js.map +1 -1
- package/dist/reporter/style.d.ts +10 -0
- package/dist/reporter/style.d.ts.map +1 -1
- package/dist/reporter/style.js +39 -0
- package/dist/reporter/style.js.map +1 -1
- package/dist/services/hubspot-dev-client.js +3 -0
- package/dist/services/hubspot-dev-client.js.map +1 -1
- package/package.json +8 -8
|
@@ -13,14 +13,41 @@
|
|
|
13
13
|
* surface (`setFilter` / `pause` / `setPrompt`) that the keyboard controller
|
|
14
14
|
* in keyboard.ts drives. Non-TTY output is untouched: no status line, no
|
|
15
15
|
* escape codes, every event printed.
|
|
16
|
+
*
|
|
17
|
+
* Details mode (`d` / ↑) swaps the stream for a browser: a list of the
|
|
18
|
+
* retained matching events on one side and the untruncated details of the
|
|
19
|
+
* selected one on the other, in the spirit of a clipboard-history picker.
|
|
20
|
+
* Up/down move the selection, page keys scroll the detail pane, and new
|
|
21
|
+
* events keep arriving in the list while the selection stays pinned.
|
|
22
|
+
*
|
|
23
|
+
* The browser lives in the terminal's alternate screen buffer (the way
|
|
24
|
+
* `less` and `vim` do). Repaints happen in place there, so neither
|
|
25
|
+
* navigation nor arriving events grow the scrollback — some terminals
|
|
26
|
+
* (alacritty-based ones such as Zed's) push a cleared primary screen into
|
|
27
|
+
* history, which turned every keypress into a screenful to scroll past.
|
|
28
|
+
* Leaving restores the stream exactly as it was and appends whatever
|
|
29
|
+
* arrived meanwhile, so nothing is repainted on the way out.
|
|
16
30
|
*/
|
|
17
|
-
import { detectColor, detectUnicode, makeStyle, symbolsFor, wrapVisible, } from '../reporter/style.js';
|
|
18
|
-
import {
|
|
31
|
+
import { detectColor, detectUnicode, makeStyle, padVisible, symbolsFor, truncateVisible, wrapVisible, } from '../reporter/style.js';
|
|
32
|
+
import { copyTextToClipboard } from './clipboard.js';
|
|
33
|
+
import { devEventFingerprint, formatDevDetailsListRow, formatDevStreamEvent, renderDevEventDetails, } from './stream-event-format.js';
|
|
19
34
|
import { EMPTY_FILTER, describeFilter, isEmptyFilter, matchesFilter, } from './stream-filter.js';
|
|
35
|
+
import { renderDevTraceMarkdown, selectDevTrace } from './trace-markdown.js';
|
|
20
36
|
const ERASE_LINE = '\r\x1b[2K';
|
|
21
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Home the cursor and erase to the end of the screen. Unlike `ESC[2J` this
|
|
39
|
+
* never pushes the visible rows into scrollback, so filter repaints overwrite
|
|
40
|
+
* the view in place instead of stacking screenfuls of history.
|
|
41
|
+
*/
|
|
42
|
+
const REPAINT_SCREEN = '\x1b[H\x1b[0J';
|
|
43
|
+
const ALT_SCREEN_ENTER = '\x1b[?1049h\x1b[?25l';
|
|
44
|
+
const ALT_SCREEN_LEAVE = '\x1b[?25h\x1b[?1049l';
|
|
22
45
|
const HISTORY_LIMIT = 500;
|
|
23
46
|
const HISTORY_EVENT_MAX_LINES = 12;
|
|
47
|
+
/** Terminals at least this wide get the list beside the detail pane. */
|
|
48
|
+
const SIDE_BY_SIDE_MIN_COLUMNS = 90;
|
|
49
|
+
/** Stacked layout: the list keeps a few rows and the detail pane takes the rest. */
|
|
50
|
+
const STACKED_LIST_MAX_ROWS = 8;
|
|
24
51
|
export function startDevStreamRenderer(opts) {
|
|
25
52
|
const stderr = opts.stderr ?? process.stderr;
|
|
26
53
|
const env = opts.env ?? process.env;
|
|
@@ -28,6 +55,7 @@ export function startDevStreamRenderer(opts) {
|
|
|
28
55
|
const color = detectColor(env, isTTY);
|
|
29
56
|
const style = makeStyle(color, detectUnicode(env));
|
|
30
57
|
const sym = symbolsFor(style.unicode);
|
|
58
|
+
const copyText = opts.copyText ?? copyTextToClipboard;
|
|
31
59
|
// Cursor control is a terminal capability, independent of color. NO_COLOR
|
|
32
60
|
// must remove SGR styling without hiding prompts, filters, pause state, or
|
|
33
61
|
// the key map. Piped/non-TTY output remains plain append-only text.
|
|
@@ -40,8 +68,9 @@ export function startDevStreamRenderer(opts) {
|
|
|
40
68
|
let prompt;
|
|
41
69
|
let statusShown = false;
|
|
42
70
|
let lastStatusWidth = 0;
|
|
43
|
-
let
|
|
44
|
-
let
|
|
71
|
+
let details;
|
|
72
|
+
let detailsRepaintQueued = false;
|
|
73
|
+
let stopped = false;
|
|
45
74
|
let lastWritten;
|
|
46
75
|
const pauseBuffer = [];
|
|
47
76
|
const startupBuffer = [];
|
|
@@ -60,6 +89,15 @@ export function startDevStreamRenderer(opts) {
|
|
|
60
89
|
const terminalRows = stderr.rows;
|
|
61
90
|
return typeof terminalRows === 'number' && terminalRows > 0 ? terminalRows : 24;
|
|
62
91
|
};
|
|
92
|
+
const matchingEntries = () => history.filter((entry) => matchesFilter(filter, entry.event));
|
|
93
|
+
const detailEntries = () => {
|
|
94
|
+
const matching = matchingEntries();
|
|
95
|
+
if (!details?.related)
|
|
96
|
+
return matching;
|
|
97
|
+
const related = new Set(selectDevTrace(history, details.selected).entries);
|
|
98
|
+
return matching.filter((entry) => related.has(entry));
|
|
99
|
+
};
|
|
100
|
+
const separator = style.dim(` ${sym.bullet} `);
|
|
63
101
|
// Compose from plain segments and paint at the end, so the line can be
|
|
64
102
|
// fitted to the terminal width without ever cutting through an ANSI code —
|
|
65
103
|
// a wrapped status line is unerasable and smears on every redraw.
|
|
@@ -71,25 +109,40 @@ export function startDevStreamRenderer(opts) {
|
|
|
71
109
|
return line.length > width ? line.slice(0, Math.max(0, width)) : line;
|
|
72
110
|
}
|
|
73
111
|
const scope = describeFilter(filter);
|
|
74
|
-
const segments =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
const segments = [];
|
|
113
|
+
if (details) {
|
|
114
|
+
const matching = detailEntries();
|
|
115
|
+
const index = Math.max(0, matching.indexOf(details.selected));
|
|
116
|
+
const position = `${index + 1}/${matching.length}`;
|
|
117
|
+
segments.push({
|
|
118
|
+
text: `event ${position} ${details.selected.event.lane}`,
|
|
119
|
+
painted: `${style.dim('event')} ${position} ${style.orange(details.selected.event.lane)}`,
|
|
120
|
+
});
|
|
121
|
+
if (!isEmptyFilter(filter)) {
|
|
122
|
+
segments.push({
|
|
83
123
|
text: `view: ${scope}`,
|
|
84
|
-
painted: `${style.dim('view:')} ${
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
124
|
+
painted: `${style.dim('view:')} ${style.orange(scope)}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (details.pending > 0) {
|
|
128
|
+
const text = `${details.pending} new`;
|
|
129
|
+
segments.push({ text, painted: style.yellow(text) });
|
|
130
|
+
}
|
|
131
|
+
if (details.related) {
|
|
132
|
+
segments.push({ text: 'trace', painted: style.cyan('trace') });
|
|
133
|
+
}
|
|
134
|
+
if (details.notice) {
|
|
135
|
+
segments.push({ text: details.notice, painted: style.green(details.notice) });
|
|
136
|
+
}
|
|
90
137
|
}
|
|
91
|
-
else
|
|
92
|
-
segments.push({
|
|
138
|
+
else {
|
|
139
|
+
segments.push({
|
|
140
|
+
text: `view: ${scope}`,
|
|
141
|
+
painted: `${style.dim('view:')} ${isEmptyFilter(filter) ? style.dim(scope) : style.orange(scope)}`,
|
|
142
|
+
});
|
|
143
|
+
if (hidden > 0) {
|
|
144
|
+
segments.push({ text: `${hidden} hidden`, painted: style.dim(`${hidden} hidden`) });
|
|
145
|
+
}
|
|
93
146
|
}
|
|
94
147
|
if (paused) {
|
|
95
148
|
const bufferedCount = retainedCount(pauseBuffer);
|
|
@@ -109,11 +162,17 @@ export function startDevStreamRenderer(opts) {
|
|
|
109
162
|
}
|
|
110
163
|
// Keep the footer focused on the less-obvious view controls. Help and quit
|
|
111
164
|
// remain available from the keyboard without occupying persistent space.
|
|
112
|
-
const
|
|
113
|
-
|
|
165
|
+
const arrows = style.unicode ? '↑↓' : 'up/down';
|
|
166
|
+
const hintTiers = details
|
|
167
|
+
? [
|
|
168
|
+
`[${arrows}] move [y] copy trace [t] related [pgup/pgdn] scroll [esc] back`,
|
|
169
|
+
`[${arrows}] move [y] copy [t] related [esc] back`,
|
|
170
|
+
'[y] copy [t] related [esc] back',
|
|
171
|
+
'[esc] back',
|
|
172
|
+
]
|
|
114
173
|
: [
|
|
115
|
-
|
|
116
|
-
|
|
174
|
+
`[${arrows}] browse [f] lane [e] errors [/] search [c] reset [p] pause`,
|
|
175
|
+
`[${arrows}] [f] [e] [/] [c] [p]`,
|
|
117
176
|
];
|
|
118
177
|
for (const hints of hintTiers) {
|
|
119
178
|
const cost = sepWidth + hints.length;
|
|
@@ -131,7 +190,7 @@ export function startDevStreamRenderer(opts) {
|
|
|
131
190
|
used = prefixWidth + cut.length;
|
|
132
191
|
}
|
|
133
192
|
lastStatusWidth = used;
|
|
134
|
-
return ` ${style.orange(sym.rail)} ${kept.map((segment) => segment.painted).join(
|
|
193
|
+
return ` ${style.orange(sym.rail)} ${kept.map((segment) => segment.painted).join(separator)}`;
|
|
135
194
|
};
|
|
136
195
|
const drawStatus = () => {
|
|
137
196
|
if (!statusEnabled || statusHeld)
|
|
@@ -146,6 +205,11 @@ export function startDevStreamRenderer(opts) {
|
|
|
146
205
|
const onResize = () => {
|
|
147
206
|
if (!statusEnabled)
|
|
148
207
|
return;
|
|
208
|
+
if (details) {
|
|
209
|
+
// The primary screen is untouched underneath; only the browser repaints.
|
|
210
|
+
renderDetails();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
149
213
|
lastWritten = undefined;
|
|
150
214
|
if (statusShown) {
|
|
151
215
|
stderr.write(ERASE_LINE);
|
|
@@ -195,9 +259,16 @@ export function startDevStreamRenderer(opts) {
|
|
|
195
259
|
if (previous?.fingerprint === fingerprint) {
|
|
196
260
|
previous.event = event;
|
|
197
261
|
previous.count += 1;
|
|
262
|
+
previous.lastTs = event.ts;
|
|
198
263
|
return { entry: previous, repeated: true };
|
|
199
264
|
}
|
|
200
|
-
const entry = {
|
|
265
|
+
const entry = {
|
|
266
|
+
event,
|
|
267
|
+
fingerprint,
|
|
268
|
+
count: 1,
|
|
269
|
+
firstTs: event.ts,
|
|
270
|
+
lastTs: event.ts,
|
|
271
|
+
};
|
|
201
272
|
history.push(entry);
|
|
202
273
|
if (history.length <= HISTORY_LIMIT)
|
|
203
274
|
return { entry, repeated: false };
|
|
@@ -220,7 +291,7 @@ export function startDevStreamRenderer(opts) {
|
|
|
220
291
|
return;
|
|
221
292
|
}
|
|
222
293
|
eraseStatus();
|
|
223
|
-
const matching =
|
|
294
|
+
const matching = matchingEntries();
|
|
224
295
|
const rowBudget = Math.max(8, rows() - 4);
|
|
225
296
|
const selected = [];
|
|
226
297
|
let usedRows = 0;
|
|
@@ -245,8 +316,8 @@ export function startDevStreamRenderer(opts) {
|
|
|
245
316
|
const count = matching.length === selected.length
|
|
246
317
|
? `${matchingCount} recent event${matchingCount === 1 ? '' : 's'}`
|
|
247
318
|
: `latest ${selectedCount} of ${matchingCount} recent events`;
|
|
248
|
-
stderr.write(
|
|
249
|
-
stderr.write(` ${style.bold('hs-x dev logs')} ${
|
|
319
|
+
stderr.write(REPAINT_SCREEN);
|
|
320
|
+
stderr.write(` ${style.bold('hs-x dev logs')} ${separator}${isEmptyFilter(filter) ? style.dim(scope) : style.orange(scope)}${separator}${style.dim(count)}\n`);
|
|
250
321
|
if (selected.length === 0) {
|
|
251
322
|
const detail = filter.texts.length > 0
|
|
252
323
|
? `No events match ${filter.texts.map((text) => `“${text}”`).join(' or ')}. Search is case-insensitive.`
|
|
@@ -268,11 +339,169 @@ export function startDevStreamRenderer(opts) {
|
|
|
268
339
|
: undefined;
|
|
269
340
|
drawStatus();
|
|
270
341
|
};
|
|
342
|
+
/** One compact list row: the stream line's headline, no continuations. */
|
|
343
|
+
const listRow = (entry, selected, width) => {
|
|
344
|
+
const pointer = selected ? style.orange(style.unicode ? '▸' : '>') : ' ';
|
|
345
|
+
return truncateVisible(`${pointer} ${formatDevDetailsListRow(entry.event, entry.count, style)}`, width);
|
|
346
|
+
};
|
|
347
|
+
/**
|
|
348
|
+
* Paint the details browser: header, the list beside (or above) the detail
|
|
349
|
+
* pane, then the status line. Everything is derived from retained history,
|
|
350
|
+
* so it can be repainted after any change — selection, scroll, new event,
|
|
351
|
+
* filter, resize — without tracking what is on screen.
|
|
352
|
+
*/
|
|
353
|
+
const renderDetails = () => {
|
|
354
|
+
if (!details || !statusEnabled || statusHeld)
|
|
355
|
+
return;
|
|
356
|
+
const matching = detailEntries();
|
|
357
|
+
if (matching.length === 0) {
|
|
358
|
+
if (details.related) {
|
|
359
|
+
details.related = false;
|
|
360
|
+
details.notice = 'no related events match the active filter';
|
|
361
|
+
renderDetails();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
closeDetails();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
let index = matching.indexOf(details.selected);
|
|
368
|
+
if (index < 0) {
|
|
369
|
+
// The pinned entry was evicted from history or no longer matches.
|
|
370
|
+
index = matching.length - 1;
|
|
371
|
+
details.selected = matching[index];
|
|
372
|
+
details.scroll = 0;
|
|
373
|
+
}
|
|
374
|
+
const isNewest = index === matching.length - 1;
|
|
375
|
+
if (isNewest)
|
|
376
|
+
details.pending = 0;
|
|
377
|
+
const width = columns();
|
|
378
|
+
const height = rows();
|
|
379
|
+
// Header, blank rule, and the status line on the last row (written
|
|
380
|
+
// without a newline so the alternate screen never scrolls).
|
|
381
|
+
const bodyRows = Math.max(6, height - 3);
|
|
382
|
+
const sideBySide = width >= SIDE_BY_SIDE_MIN_COLUMNS;
|
|
383
|
+
const listWidth = sideBySide ? Math.min(56, Math.max(36, Math.floor(width * 0.38))) : width - 1;
|
|
384
|
+
const listRows = sideBySide
|
|
385
|
+
? bodyRows
|
|
386
|
+
: Math.min(matching.length, Math.max(3, Math.min(STACKED_LIST_MAX_ROWS, Math.floor(bodyRows * 0.35))));
|
|
387
|
+
const detailWidth = sideBySide ? width - listWidth - 3 : width;
|
|
388
|
+
const detailRows = sideBySide ? bodyRows : bodyRows - listRows - 1;
|
|
389
|
+
if (index < details.listTop)
|
|
390
|
+
details.listTop = index;
|
|
391
|
+
if (index >= details.listTop + listRows)
|
|
392
|
+
details.listTop = index - listRows + 1;
|
|
393
|
+
details.listTop = Math.max(0, Math.min(details.listTop, matching.length - listRows));
|
|
394
|
+
const listLines = [];
|
|
395
|
+
for (let row = 0; row < listRows; row += 1) {
|
|
396
|
+
const entry = matching[details.listTop + row];
|
|
397
|
+
listLines.push(entry ? listRow(entry, entry === details.selected, listWidth) : '');
|
|
398
|
+
}
|
|
399
|
+
const above = details.listTop;
|
|
400
|
+
const below = Math.max(0, matching.length - (details.listTop + listRows));
|
|
401
|
+
if (above > 0 && listLines.length > 0) {
|
|
402
|
+
listLines[0] = truncateVisible(` ${style.dim(`… ${above} older`)}`, listWidth);
|
|
403
|
+
}
|
|
404
|
+
if (below > 0 && listLines.length > 1) {
|
|
405
|
+
listLines[listLines.length - 1] = truncateVisible(` ${style.dim(`… ${below} newer`)}`, listWidth);
|
|
406
|
+
}
|
|
407
|
+
const detailLines = renderDevEventDetails(details.selected.event, details.selected.count, style, detailWidth, { firstTs: details.selected.firstTs, lastTs: details.selected.lastTs }).split('\n');
|
|
408
|
+
const maxScroll = Math.max(0, detailLines.length - detailRows);
|
|
409
|
+
details.scroll = Math.max(0, Math.min(details.scroll, maxScroll));
|
|
410
|
+
const pane = detailLines.slice(details.scroll, details.scroll + detailRows);
|
|
411
|
+
if (details.scroll > 0 && pane.length > 0) {
|
|
412
|
+
pane[0] = ` ${style.dim(`… ${plural(details.scroll, 'line')} above (pgup)`)}`;
|
|
413
|
+
}
|
|
414
|
+
const remaining = detailLines.length - (details.scroll + pane.length);
|
|
415
|
+
if (remaining > 0 && pane.length > 1) {
|
|
416
|
+
pane[pane.length - 1] = ` ${style.dim(`… ${plural(remaining, 'line')} more (pgdn)`)}`;
|
|
417
|
+
}
|
|
418
|
+
const paneLines = pane.map((line) => truncateVisible(line, detailWidth));
|
|
419
|
+
const position = `${index + 1} of ${matching.length}`;
|
|
420
|
+
const scope = details.related ? 'trace' : details.selected.event.lane;
|
|
421
|
+
const header = ` ${style.bold('hs-x dev event')}${separator}${style.orange(scope)}${separator}${style.dim(position)}`;
|
|
422
|
+
const out = [truncateVisible(header, width - 1), ''];
|
|
423
|
+
if (sideBySide) {
|
|
424
|
+
const rail = style.dim(sym.rail);
|
|
425
|
+
for (let row = 0; row < bodyRows; row += 1) {
|
|
426
|
+
const left = padVisible(listLines[row] ?? '', listWidth);
|
|
427
|
+
const right = paneLines[row] ?? '';
|
|
428
|
+
out.push(`${left} ${rail} ${right}`.trimEnd());
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
out.push(...listLines);
|
|
433
|
+
out.push(` ${style.dim((style.unicode ? '─' : '-').repeat(Math.max(8, width - 4)))}`);
|
|
434
|
+
out.push(...paneLines);
|
|
435
|
+
}
|
|
436
|
+
statusShown = false; // the repaint below erases the whole alternate screen
|
|
437
|
+
stderr.write(REPAINT_SCREEN);
|
|
438
|
+
stderr.write(`${out.join('\n')}\n`);
|
|
439
|
+
drawStatus();
|
|
440
|
+
};
|
|
441
|
+
// Bursts (a HubSpot poll batch, a stack trace) arrive as many synchronous
|
|
442
|
+
// emits; repaint the browser once after the burst instead of per event.
|
|
443
|
+
const queueDetailsRepaint = () => {
|
|
444
|
+
if (detailsRepaintQueued)
|
|
445
|
+
return;
|
|
446
|
+
detailsRepaintQueued = true;
|
|
447
|
+
queueMicrotask(() => {
|
|
448
|
+
detailsRepaintQueued = false;
|
|
449
|
+
if (!stopped && details)
|
|
450
|
+
renderDetails();
|
|
451
|
+
});
|
|
452
|
+
};
|
|
453
|
+
const openDetails = () => {
|
|
454
|
+
const latest = matchingEntries().at(-1);
|
|
455
|
+
if (!latest)
|
|
456
|
+
return false;
|
|
457
|
+
// Erase the primary status line first so it is not restored twice: the
|
|
458
|
+
// alternate screen switch saves the primary buffer and cursor as they are.
|
|
459
|
+
eraseStatus();
|
|
460
|
+
stderr.write(ALT_SCREEN_ENTER);
|
|
461
|
+
details = {
|
|
462
|
+
selected: latest,
|
|
463
|
+
listTop: 0,
|
|
464
|
+
scroll: 0,
|
|
465
|
+
pending: 0,
|
|
466
|
+
arrivals: [],
|
|
467
|
+
related: false,
|
|
468
|
+
notice: undefined,
|
|
469
|
+
};
|
|
470
|
+
renderDetails();
|
|
471
|
+
return true;
|
|
472
|
+
};
|
|
473
|
+
const closeDetails = () => {
|
|
474
|
+
if (!details)
|
|
475
|
+
return;
|
|
476
|
+
const { arrivals } = details;
|
|
477
|
+
details = undefined;
|
|
478
|
+
statusShown = false; // the alternate screen's status line vanishes with it
|
|
479
|
+
stderr.write(ALT_SCREEN_LEAVE);
|
|
480
|
+
hidden = retainedCount(history.filter((entry) => !matchesFilter(filter, entry.event)));
|
|
481
|
+
// The stream is back exactly as it was; append what arrived meanwhile.
|
|
482
|
+
for (const entry of arrivals) {
|
|
483
|
+
if (!matchesFilter(filter, entry.event))
|
|
484
|
+
continue;
|
|
485
|
+
if (paused)
|
|
486
|
+
pauseBuffer.push(entry);
|
|
487
|
+
else if (lastWritten?.entry === entry)
|
|
488
|
+
rewriteLastEvent(entry);
|
|
489
|
+
else
|
|
490
|
+
writeEvent(entry);
|
|
491
|
+
}
|
|
492
|
+
drawStatus();
|
|
493
|
+
};
|
|
271
494
|
const onEvent = (event) => {
|
|
272
495
|
const remembered = remember(event);
|
|
273
496
|
if (statusHeld) {
|
|
274
497
|
if (!statusEnabled) {
|
|
275
|
-
startupBuffer.push({
|
|
498
|
+
startupBuffer.push({
|
|
499
|
+
event,
|
|
500
|
+
fingerprint: remembered.entry.fingerprint,
|
|
501
|
+
count: 1,
|
|
502
|
+
firstTs: event.ts,
|
|
503
|
+
lastTs: event.ts,
|
|
504
|
+
});
|
|
276
505
|
}
|
|
277
506
|
else if (!remembered.repeated) {
|
|
278
507
|
startupBuffer.push(remembered.entry);
|
|
@@ -285,15 +514,22 @@ export function startDevStreamRenderer(opts) {
|
|
|
285
514
|
drawStatus();
|
|
286
515
|
return;
|
|
287
516
|
}
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
|
|
517
|
+
if (details) {
|
|
518
|
+
details.pending += 1;
|
|
519
|
+
if (!details.arrivals.includes(remembered.entry))
|
|
520
|
+
details.arrivals.push(remembered.entry);
|
|
521
|
+
queueDetailsRepaint();
|
|
292
522
|
return;
|
|
293
523
|
}
|
|
294
524
|
if (paused) {
|
|
295
525
|
if (!statusEnabled) {
|
|
296
|
-
pauseBuffer.push({
|
|
526
|
+
pauseBuffer.push({
|
|
527
|
+
event,
|
|
528
|
+
fingerprint: remembered.entry.fingerprint,
|
|
529
|
+
count: 1,
|
|
530
|
+
firstTs: event.ts,
|
|
531
|
+
lastTs: event.ts,
|
|
532
|
+
});
|
|
297
533
|
}
|
|
298
534
|
else if (!remembered.repeated) {
|
|
299
535
|
pauseBuffer.push(remembered.entry);
|
|
@@ -310,6 +546,18 @@ export function startDevStreamRenderer(opts) {
|
|
|
310
546
|
const unsubscribe = opts.bus.subscribe(onEvent);
|
|
311
547
|
if (statusEnabled)
|
|
312
548
|
drawStatus();
|
|
549
|
+
const applyFilter = (next) => {
|
|
550
|
+
filter = next;
|
|
551
|
+
if (details) {
|
|
552
|
+
// Stay in the browser: the list re-derives from the new filter and the
|
|
553
|
+
// selection snaps to the newest match if the pinned event dropped out.
|
|
554
|
+
details.listTop = 0;
|
|
555
|
+
hidden = retainedCount(history.filter((entry) => !matchesFilter(filter, entry.event)));
|
|
556
|
+
renderDetails();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
refreshHistoryView();
|
|
560
|
+
};
|
|
313
561
|
return {
|
|
314
562
|
ready() {
|
|
315
563
|
if (!statusHeld)
|
|
@@ -329,17 +577,9 @@ export function startDevStreamRenderer(opts) {
|
|
|
329
577
|
},
|
|
330
578
|
hasStatusLine: () => statusEnabled,
|
|
331
579
|
getFilter: () => filter,
|
|
332
|
-
setFilter
|
|
333
|
-
filter = next;
|
|
334
|
-
detailEntry = undefined;
|
|
335
|
-
detailPending = 0;
|
|
336
|
-
refreshHistoryView();
|
|
337
|
-
},
|
|
580
|
+
setFilter: applyFilter,
|
|
338
581
|
resetFilter() {
|
|
339
|
-
|
|
340
|
-
detailEntry = undefined;
|
|
341
|
-
detailPending = 0;
|
|
342
|
-
refreshHistoryView();
|
|
582
|
+
applyFilter(initialFilter);
|
|
343
583
|
},
|
|
344
584
|
setPaused(next) {
|
|
345
585
|
if (paused === next)
|
|
@@ -356,13 +596,11 @@ export function startDevStreamRenderer(opts) {
|
|
|
356
596
|
},
|
|
357
597
|
isPaused: () => paused,
|
|
358
598
|
toggleLatestDetails() {
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
detailPending = 0;
|
|
362
|
-
refreshHistoryView();
|
|
599
|
+
if (details) {
|
|
600
|
+
closeDetails();
|
|
363
601
|
return;
|
|
364
602
|
}
|
|
365
|
-
const latest =
|
|
603
|
+
const latest = matchingEntries().at(-1);
|
|
366
604
|
if (!latest) {
|
|
367
605
|
eraseStatus();
|
|
368
606
|
stderr.write(` ${style.dim('No matching event is available for details.')}\n`);
|
|
@@ -374,14 +612,76 @@ export function startDevStreamRenderer(opts) {
|
|
|
374
612
|
stderr.write(`${renderDevEventDetails(latest.event, latest.count, style, columns())}\n`);
|
|
375
613
|
return;
|
|
376
614
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
615
|
+
openDetails();
|
|
616
|
+
},
|
|
617
|
+
isShowingDetails: () => details !== undefined,
|
|
618
|
+
moveDetailsSelection(delta) {
|
|
619
|
+
if (!details) {
|
|
620
|
+
if (!statusEnabled || !openDetails())
|
|
621
|
+
return;
|
|
622
|
+
// Entering with ↑ lands on the newest event; the move itself is the
|
|
623
|
+
// gesture, so a single press does not additionally step older.
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const matching = detailEntries();
|
|
627
|
+
const index = matching.indexOf(details.selected);
|
|
628
|
+
const next = Math.max(0, Math.min(matching.length - 1, (index < 0 ? matching.length - 1 : index) + delta));
|
|
629
|
+
const target = matching[next];
|
|
630
|
+
if (!target)
|
|
631
|
+
return;
|
|
632
|
+
if (target !== details.selected) {
|
|
633
|
+
details.selected = target;
|
|
634
|
+
details.scroll = 0;
|
|
635
|
+
details.notice = undefined;
|
|
636
|
+
}
|
|
637
|
+
renderDetails();
|
|
638
|
+
},
|
|
639
|
+
jumpDetailsSelection(edge) {
|
|
640
|
+
if (!details) {
|
|
641
|
+
if (!statusEnabled || !openDetails())
|
|
642
|
+
return;
|
|
643
|
+
if (edge === 'last')
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (!details)
|
|
647
|
+
return;
|
|
648
|
+
const matching = detailEntries();
|
|
649
|
+
const target = edge === 'first' ? matching[0] : matching.at(-1);
|
|
650
|
+
if (!target)
|
|
651
|
+
return;
|
|
652
|
+
if (target !== details.selected) {
|
|
653
|
+
details.selected = target;
|
|
654
|
+
details.scroll = 0;
|
|
655
|
+
details.notice = undefined;
|
|
656
|
+
}
|
|
657
|
+
renderDetails();
|
|
658
|
+
},
|
|
659
|
+
scrollDetails(delta) {
|
|
660
|
+
if (!details)
|
|
661
|
+
return;
|
|
662
|
+
details.scroll = Math.max(0, details.scroll + delta);
|
|
663
|
+
details.notice = undefined;
|
|
664
|
+
renderDetails();
|
|
665
|
+
},
|
|
666
|
+
toggleRelatedDetails() {
|
|
667
|
+
if (!details)
|
|
668
|
+
return;
|
|
669
|
+
details.related = !details.related;
|
|
670
|
+
details.listTop = 0;
|
|
671
|
+
details.scroll = 0;
|
|
672
|
+
details.notice = details.related ? 'showing related events' : undefined;
|
|
673
|
+
renderDetails();
|
|
674
|
+
},
|
|
675
|
+
copyDetailsTrace() {
|
|
676
|
+
if (!details)
|
|
677
|
+
return;
|
|
678
|
+
const selection = selectDevTrace(history, details.selected);
|
|
679
|
+
const markdown = renderDevTraceMarkdown(selection, details.selected);
|
|
680
|
+
const result = copyText(markdown);
|
|
681
|
+
details.notice = result.ok
|
|
682
|
+
? `copied ${selection.entries.length}-event trace as Markdown`
|
|
683
|
+
: `copy failed: ${result.reason}`;
|
|
684
|
+
renderDetails();
|
|
385
685
|
},
|
|
386
686
|
setPrompt(next) {
|
|
387
687
|
prompt = next;
|
|
@@ -395,10 +695,18 @@ export function startDevStreamRenderer(opts) {
|
|
|
395
695
|
drawStatus();
|
|
396
696
|
},
|
|
397
697
|
stop() {
|
|
698
|
+
stopped = true;
|
|
398
699
|
unsubscribe();
|
|
399
700
|
if (statusEnabled && typeof stderr.off === 'function') {
|
|
400
701
|
stderr.off('resize', onResize);
|
|
401
702
|
}
|
|
703
|
+
if (details) {
|
|
704
|
+
// Never leave the terminal stranded in the alternate screen.
|
|
705
|
+
details = undefined;
|
|
706
|
+
statusShown = false;
|
|
707
|
+
stderr.write(ALT_SCREEN_LEAVE);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
402
710
|
eraseStatus();
|
|
403
711
|
},
|
|
404
712
|
};
|
|
@@ -412,6 +720,9 @@ function truncateRenderedEvent(rendered, maxLines, style) {
|
|
|
412
720
|
` ${style.dim(`… ${lines.length - maxLines + 1} more lines`)}`,
|
|
413
721
|
].join('\n');
|
|
414
722
|
}
|
|
723
|
+
function plural(count, noun) {
|
|
724
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
725
|
+
}
|
|
415
726
|
function retainedCount(entries) {
|
|
416
727
|
return entries.reduce((total, entry) => total + entry.count, 0);
|
|
417
728
|
}
|