@wolfstar/cli 0.2.0 → 0.4.0

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 (58) hide show
  1. package/README.md +53 -27
  2. package/dist/{build-B6xw00Ee.js → build-B1YfbqX3.js} +2 -2
  3. package/dist/{build-B6xw00Ee.js.map → build-B1YfbqX3.js.map} +1 -1
  4. package/dist/{build-CvSvbsYN.js → build-vpcF5bwc.js} +5 -4
  5. package/dist/build-vpcF5bwc.js.map +1 -0
  6. package/dist/cli.js +6 -6
  7. package/dist/{codegen-mYkL-GBf.js → codegen-1kW_OnrL.js} +2 -2
  8. package/dist/{codegen-mYkL-GBf.js.map → codegen-1kW_OnrL.js.map} +1 -1
  9. package/dist/{codegen-DjPOn7cp.js → codegen-DHQH83M3.js} +2 -2
  10. package/dist/{codegen-DjPOn7cp.js.map → codegen-DHQH83M3.js.map} +1 -1
  11. package/dist/{commands-Dw2LFWV-.js → commands-DPGDL7ft.js} +4 -4
  12. package/dist/commands-DPGDL7ft.js.map +1 -0
  13. package/dist/{commands-BkGKs_Bg.js → commands-hpnmaBM2.js} +4 -4
  14. package/dist/{commands-BkGKs_Bg.js.map → commands-hpnmaBM2.js.map} +1 -1
  15. package/dist/{dev-CWA_JVTb.js → dev-Dh1Euxk1.js} +111 -68
  16. package/dist/dev-Dh1Euxk1.js.map +1 -0
  17. package/dist/{dev-BCgaVd0m.js → dev-hT1WR1Kh.js} +2 -2
  18. package/dist/{dev-BCgaVd0m.js.map → dev-hT1WR1Kh.js.map} +1 -1
  19. package/dist/external-Bk4EESdT.js.map +1 -1
  20. package/dist/index.d.ts +14 -10
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/{info-Czh4LUsD.js → info-BrxKCcUW.js} +2 -2
  24. package/dist/{info-Czh4LUsD.js.map → info-BrxKCcUW.js.map} +1 -1
  25. package/dist/{info-BqxpktLG.js → info-rYRLITnv.js} +2 -2
  26. package/dist/{info-BqxpktLG.js.map → info-rYRLITnv.js.map} +1 -1
  27. package/dist/locales-B3mRenhY.js +104 -0
  28. package/dist/locales-B3mRenhY.js.map +1 -0
  29. package/dist/log-buffer-D-nj2ZEU.js +72 -0
  30. package/dist/log-buffer-D-nj2ZEU.js.map +1 -0
  31. package/dist/none-DEo7zEr9.js.map +1 -1
  32. package/dist/{output-mode-ChrOf0LO.js → output-mode-Cxes6YUW.js} +4 -2
  33. package/dist/output-mode-Cxes6YUW.js.map +1 -0
  34. package/dist/{prepare-BXcm2Ukp.js → prepare-90XQCoak.js} +1 -1
  35. package/dist/{prepare-D5NZ0w4t.js → prepare-CHLrneYU.js} +2 -2
  36. package/dist/{prepare-D5NZ0w4t.js.map → prepare-CHLrneYU.js.map} +1 -1
  37. package/dist/{prepare-CVf88_Or.js → prepare-hOwjyelr.js} +2 -2
  38. package/dist/{prepare-CVf88_Or.js.map → prepare-hOwjyelr.js.map} +1 -1
  39. package/dist/process-supervisor-CJOdmp0W.js.map +1 -1
  40. package/dist/tsc-BqZfDTD0.js.map +1 -1
  41. package/dist/{tsdown-BEFq2API.js → tsdown-DN3PQTJf.js} +30 -13
  42. package/dist/tsdown-DN3PQTJf.js.map +1 -0
  43. package/dist/tui-BCGQrMQo.js +947 -0
  44. package/dist/tui-BCGQrMQo.js.map +1 -0
  45. package/dist/{tunnel-B8Id4-lR.js → tunnel-mGJe2zuE.js} +20 -7
  46. package/dist/tunnel-mGJe2zuE.js.map +1 -0
  47. package/dist/vite-27ZbreDz.js.map +1 -1
  48. package/package.json +10 -9
  49. package/dist/build-CvSvbsYN.js.map +0 -1
  50. package/dist/builders-D97Uqjwg.js +0 -44
  51. package/dist/builders-D97Uqjwg.js.map +0 -1
  52. package/dist/commands-Dw2LFWV-.js.map +0 -1
  53. package/dist/dev-CWA_JVTb.js.map +0 -1
  54. package/dist/output-mode-ChrOf0LO.js.map +0 -1
  55. package/dist/tsdown-BEFq2API.js.map +0 -1
  56. package/dist/tui-BTRFsLQR.js +0 -770
  57. package/dist/tui-BTRFsLQR.js.map +0 -1
  58. package/dist/tunnel-B8Id4-lR.js.map +0 -1
@@ -0,0 +1,947 @@
1
+ import { t as readOwnPackageJson } from "./version-DN6Uho-Y.js";
2
+ import { n as classifyAppLine, r as isErrorDetail } from "./log-buffer-D-nj2ZEU.js";
3
+ import { execFile } from "node:child_process";
4
+ import { stripVTControlCharacters } from "node:util";
5
+ import { Box, Text, render, useInput, useWindowSize } from "ink";
6
+ import { createContext, useContext, useEffect, useMemo, useState } from "react";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { StringDecoder } from "node:string_decoder";
9
+
10
+ //#region src/renderers/ink/theme.tsx
11
+ /**
12
+ * Whether the UI may use colour. `stars dev` decides this once (`NO_COLOR`, `FORCE_COLOR`, TTY detection) and every
13
+ * component reads it from here instead of re-deriving it.
14
+ */
15
+ const ColorContext = createContext(true);
16
+ function ColorProvider({ color, children }) {
17
+ return /* @__PURE__ */ jsx(ColorContext, {
18
+ value: color,
19
+ children
20
+ });
21
+ }
22
+ /**
23
+ * Returns a colour name only when colour is enabled, so `<Text color={useColor()('green')}>` degrades to plain text.
24
+ */
25
+ function useColor() {
26
+ const enabled = useContext(ColorContext);
27
+ return (name) => enabled ? name : void 0;
28
+ }
29
+
30
+ //#endregion
31
+ //#region src/renderers/ink/components/HelpOverlay.tsx
32
+ const KEYS = [
33
+ ["r", "restart the bot now"],
34
+ ["o", "open the local URL in a browser"],
35
+ ["t", "toggle the public tunnel"],
36
+ ["i", "show versions, URLs and session info"],
37
+ ["l", "open the logs (scroll, filter by source or level)"],
38
+ ["e", "jump to the last error, keeping its context"],
39
+ ["c / Ctrl+L", "clear the log history"],
40
+ ["q / Ctrl+C", "stop the bot and quit"],
41
+ ["?", "toggle this help"]
42
+ ];
43
+ /** The static keys reference, opened with `?` from the pinned panel. */
44
+ function HelpOverlay({ height, width, onClose }) {
45
+ const paint = useColor();
46
+ useInput((input, key) => {
47
+ if (!key.ctrl && (key.escape || [
48
+ "q",
49
+ "h",
50
+ "?"
51
+ ].includes(input))) onClose();
52
+ });
53
+ return /* @__PURE__ */ jsxs(Box, {
54
+ flexDirection: "column",
55
+ height,
56
+ children: [
57
+ /* @__PURE__ */ jsx(Text, {
58
+ bold: true,
59
+ children: " keyboard shortcuts"
60
+ }),
61
+ /* @__PURE__ */ jsx(Text, {
62
+ dimColor: true,
63
+ children: "─".repeat(width)
64
+ }),
65
+ KEYS.slice(0, Math.max(0, height - 3)).map(([key, description]) => /* @__PURE__ */ jsxs(Text, {
66
+ wrap: "truncate-end",
67
+ children: [
68
+ " ",
69
+ /* @__PURE__ */ jsx(Text, {
70
+ bold: true,
71
+ color: paint("white"),
72
+ children: key.padEnd(14)
73
+ }),
74
+ /* @__PURE__ */ jsx(Text, {
75
+ dimColor: true,
76
+ children: description
77
+ })
78
+ ]
79
+ }, key)),
80
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
81
+ /* @__PURE__ */ jsx(Text, {
82
+ dimColor: true,
83
+ children: " q close"
84
+ })
85
+ ]
86
+ });
87
+ }
88
+
89
+ //#endregion
90
+ //#region src/renderers/ink/hooks/useLogVersion.ts
91
+ /** Bumps whenever the buffer changes, so a consumer can re-derive a filtered view without re-filtering on every render. */
92
+ function useLogVersion(logs) {
93
+ const [version, setVersion] = useState(0);
94
+ useEffect(() => {
95
+ const bump = () => setVersion((current) => current + 1);
96
+ logs.on("entry", bump);
97
+ logs.on("clear", bump);
98
+ bump();
99
+ return () => {
100
+ logs.off("entry", bump);
101
+ logs.off("clear", bump);
102
+ };
103
+ }, [logs]);
104
+ return version;
105
+ }
106
+
107
+ //#endregion
108
+ //#region src/renderers/ink/components/Hints.tsx
109
+ const PANEL_HINTS = [
110
+ {
111
+ key: "r",
112
+ label: "restart",
113
+ priority: 80
114
+ },
115
+ {
116
+ key: "o",
117
+ label: "open",
118
+ priority: 40
119
+ },
120
+ {
121
+ key: "t",
122
+ label: "tunnel",
123
+ priority: 60
124
+ },
125
+ {
126
+ key: "i",
127
+ label: "info",
128
+ priority: 50
129
+ },
130
+ {
131
+ key: "l",
132
+ label: "logs",
133
+ priority: 70
134
+ },
135
+ {
136
+ key: "?",
137
+ label: "help",
138
+ priority: 100
139
+ },
140
+ {
141
+ key: "q",
142
+ label: "quit",
143
+ priority: 90
144
+ }
145
+ ];
146
+ /** Drop the lowest-priority shortcuts first; help and the last error survive narrow panes. */
147
+ function Hints({ hints, width }) {
148
+ const shown = [...hints];
149
+ const length = () => shown.reduce((size, hint) => size + hint.key.length + hint.label.length + 4, -2);
150
+ while (shown.length > 1 && length() > width) {
151
+ const weakest = shown.reduce((a, b) => a.priority <= b.priority ? a : b);
152
+ shown.splice(shown.indexOf(weakest), 1);
153
+ }
154
+ return /* @__PURE__ */ jsxs(Text, {
155
+ wrap: "truncate-end",
156
+ children: [" ", shown.map(({ key, label }, i) => /* @__PURE__ */ jsxs(Text, { children: [
157
+ i > 0 && /* @__PURE__ */ jsx(Text, {
158
+ dimColor: true,
159
+ children: " · "
160
+ }),
161
+ /* @__PURE__ */ jsx(Text, {
162
+ bold: true,
163
+ children: key
164
+ }),
165
+ /* @__PURE__ */ jsx(Text, {
166
+ dimColor: true,
167
+ children: ` ${label}`
168
+ })
169
+ ] }, key))]
170
+ });
171
+ }
172
+
173
+ //#endregion
174
+ //#region src/renderers/ink/components/LogBrowser.tsx
175
+ const group = (entry) => entry.source === "app" ? "runtime" : entry.source === "build" || entry.source === "tsc" ? "build" : "cli";
176
+ /** Filterable history, using Nuxt's keys and selecting the last error without discarding its context. */
177
+ function LogBrowser({ service, height, width, lastError = false, onClose, onCopy }) {
178
+ const paint = useColor();
179
+ const [sources, setSources] = useState({
180
+ cli: true,
181
+ build: true,
182
+ runtime: true
183
+ });
184
+ const [level, setLevel] = useState(null);
185
+ const [query, setQuery] = useState("");
186
+ const [searching, setSearching] = useState(false);
187
+ const [selected, setSelected] = useState(() => lastError ? service.logs.entries().findLast((entry) => entry.level === "error" && !isErrorDetail(entry))?.id ?? null : null);
188
+ const version = useLogVersion(service.logs);
189
+ const entries = useMemo(() => service.logs.filter({ level }).filter((entry) => sources[group(entry)] && stripVTControlCharacters(entry.text).toLowerCase().includes(query.toLowerCase())), [
190
+ service,
191
+ sources,
192
+ level,
193
+ query,
194
+ version
195
+ ]);
196
+ const bodyHeight = Math.max(0, height - 3);
197
+ const index = entries.findIndex((entry) => entry.id === selected);
198
+ const rows = entries.flatMap((entry) => stripVTControlCharacters(entry.text).split("\n").map((line, i) => ({
199
+ entry,
200
+ text: i === 0 ? `${new Date(entry.time).toLocaleTimeString()} ${entry.source.padEnd(6)} ${line}` : ` ${line}`
201
+ })));
202
+ const selectedStart = rows.findIndex((row) => row.entry.id === selected);
203
+ const selectedEnd = rows.findLastIndex((row) => row.entry.id === selected) + 1;
204
+ const end = index < 0 ? rows.length : Math.min(rows.length, selectedStart + Math.max(bodyHeight, Math.min(selectedEnd - selectedStart, bodyHeight)));
205
+ const visible = rows.slice(Math.max(0, end - bodyHeight), end);
206
+ const move = (delta) => {
207
+ if (!entries.length) return;
208
+ const next = index < 0 ? delta < 0 ? entries.length - 1 : 0 : Math.max(0, Math.min(entries.length - 1, index + delta));
209
+ setSelected(entries[next].id);
210
+ };
211
+ useInput((input, key) => {
212
+ if (key.ctrl) return;
213
+ if (searching) {
214
+ if (key.escape) {
215
+ setSearching(false);
216
+ setQuery("");
217
+ } else if (key.return) setSearching(false);
218
+ else if (key.backspace || key.delete) setQuery((value) => [...value].slice(0, -1).join(""));
219
+ else if (input && !key.upArrow && !key.downArrow) setQuery((value) => value + input);
220
+ setSelected(null);
221
+ return;
222
+ }
223
+ if (key.escape || input === "l" || input === "q") return onClose();
224
+ if (input === "/") return setSearching(true);
225
+ if (input === "e" || input === "w" || input === "a") {
226
+ const next = input === "e" ? "error" : input === "w" ? "warn" : null;
227
+ setLevel((current) => current === next ? null : next);
228
+ return setSelected(null);
229
+ }
230
+ if (input === "c" || input === "b" || input === "r") {
231
+ const source = input === "c" ? "cli" : input === "b" ? "build" : "runtime";
232
+ setSources((current) => {
233
+ const next = {
234
+ ...current,
235
+ [source]: !current[source]
236
+ };
237
+ return Object.values(next).some(Boolean) ? next : current;
238
+ });
239
+ return setSelected(null);
240
+ }
241
+ if (input === "x") {
242
+ service.clearLogs();
243
+ return setSelected(null);
244
+ }
245
+ if (key.upArrow || input === "k") return move(-1);
246
+ if (key.downArrow || input === "j") return move(1);
247
+ if (key.pageUp) return move(-Math.max(1, bodyHeight));
248
+ if (key.pageDown) return move(Math.max(1, bodyHeight));
249
+ if (key.home || input === "g") return setSelected(entries[0]?.id ?? null);
250
+ if (key.end || input === "G") return setSelected(null);
251
+ if ((key.return || input === "y") && index >= 0) onCopy?.(stripVTControlCharacters(entries[index].text));
252
+ });
253
+ const hints = searching ? [["enter", "apply"], ["esc", "cancel"]] : [
254
+ ["↑/↓", "select"],
255
+ ["g/G", "top/bottom"],
256
+ ["e", "errors"],
257
+ ["w", "warnings"],
258
+ ["a", "all"],
259
+ ["c/b/r", "sources"],
260
+ ["/", "search"],
261
+ ["x", "clear"],
262
+ ["enter", "copy"],
263
+ ["q", "close"]
264
+ ];
265
+ return /* @__PURE__ */ jsxs(Box, {
266
+ flexDirection: "column",
267
+ height,
268
+ children: [
269
+ /* @__PURE__ */ jsxs(Text, {
270
+ wrap: "truncate-end",
271
+ children: [
272
+ " ",
273
+ /* @__PURE__ */ jsx(Text, {
274
+ bold: true,
275
+ children: "logs"
276
+ }),
277
+ /* @__PURE__ */ jsx(Text, {
278
+ dimColor: true,
279
+ children: ` · ${Object.entries(sources).filter(([, shown]) => shown).map(([source]) => source).join("+")} · ${level ? `${level}+ only` : "all levels"}${query || searching ? ` · search ${query}${searching ? "▏" : ""}` : ""}`
280
+ })
281
+ ]
282
+ }),
283
+ /* @__PURE__ */ jsx(Text, {
284
+ dimColor: true,
285
+ children: "─".repeat(width)
286
+ }),
287
+ /* @__PURE__ */ jsxs(Box, {
288
+ flexDirection: "column",
289
+ height: bodyHeight,
290
+ overflow: "hidden",
291
+ children: [visible.length === 0 && /* @__PURE__ */ jsx(Text, {
292
+ dimColor: true,
293
+ children: " no matching logs"
294
+ }), visible.map(({ entry, text }, i) => /* @__PURE__ */ jsxs(Text, {
295
+ wrap: "truncate-end",
296
+ children: [/* @__PURE__ */ jsx(Text, {
297
+ color: paint("green"),
298
+ children: entry.id === selected ? "▎ " : " "
299
+ }), /* @__PURE__ */ jsx(Text, {
300
+ color: paint(entry.level === "error" ? "red" : entry.level === "warn" ? "yellow" : entry.level === "success" ? "green" : "white"),
301
+ dimColor: isErrorDetail(entry),
302
+ children: text
303
+ })]
304
+ }, `${entry.id}-${i}`))]
305
+ }),
306
+ /* @__PURE__ */ jsx(Hints, {
307
+ width,
308
+ hints: hints.map(([key, label], i) => ({
309
+ key,
310
+ label,
311
+ priority: i === hints.length - 1 ? 100 : hints.length - i
312
+ }))
313
+ })
314
+ ]
315
+ });
316
+ }
317
+
318
+ //#endregion
319
+ //#region src/renderers/ink/hooks/useDevStatus.ts
320
+ /**
321
+ * Mirrors the service's status into React state. The service stays the single source of truth: the UI only
322
+ * subscribes to it, exactly like the plain renderer does.
323
+ */
324
+ function useDevStatus(service) {
325
+ const [status, setStatus] = useState(() => service.status);
326
+ useEffect(() => {
327
+ const onStatus = (next) => setStatus(next);
328
+ service.on("status", onStatus);
329
+ setStatus(service.status);
330
+ return () => {
331
+ service.off("status", onStatus);
332
+ };
333
+ }, [service]);
334
+ return status;
335
+ }
336
+
337
+ //#endregion
338
+ //#region src/renderers/ink/hooks/useUptime.ts
339
+ /**
340
+ * Re-renders once a second while the bot is up, so the uptime in the header keeps counting.
341
+ */
342
+ function useUptime(startedAt) {
343
+ const [now, setNow] = useState(() => Date.now());
344
+ useEffect(() => {
345
+ if (startedAt === null) return;
346
+ setNow(Date.now());
347
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
348
+ return () => clearInterval(timer);
349
+ }, [startedAt]);
350
+ return startedAt === null ? null : now - startedAt;
351
+ }
352
+
353
+ //#endregion
354
+ //#region src/renderers/panel-logic.ts
355
+ /**
356
+ * The badge and standing description for the panel's status row, mirroring the states Nuxt CLI v4's own dev panel
357
+ * cycles through (`STARTING`/`BUILDING`/`RESTART`/`READY`/`ERROR`).
358
+ */
359
+ function describeBadge(status) {
360
+ if (status.build === "building") return {
361
+ badge: status.startedAt === null ? "starting" : "building",
362
+ note: status.progress.message
363
+ };
364
+ if (status.process === "starting") return {
365
+ badge: "starting",
366
+ note: "starting the bot"
367
+ };
368
+ if (status.process === "stopping") return {
369
+ badge: "restarting",
370
+ note: "restarting the bot"
371
+ };
372
+ if (status.build === "failed") return {
373
+ badge: "error",
374
+ note: "build failed, waiting for changes"
375
+ };
376
+ if (status.process === "crashed") return {
377
+ badge: "error",
378
+ note: "the bot crashed, press r to restart"
379
+ };
380
+ if (status.health === "down") return {
381
+ badge: "error",
382
+ note: "health check failed"
383
+ };
384
+ if (status.typecheck === "failed") return {
385
+ badge: "error",
386
+ note: "type check failed, press e to view it"
387
+ };
388
+ if (status.process === "stopped") return {
389
+ badge: "error",
390
+ note: "the bot stopped, press r to restart"
391
+ };
392
+ if (status.progress.fraction < 1) return {
393
+ badge: "starting",
394
+ note: status.progress.message
395
+ };
396
+ if (status.process === "running") return {
397
+ badge: "ready",
398
+ note: "watching for changes"
399
+ };
400
+ return {
401
+ badge: "starting",
402
+ note: "waiting for the first build"
403
+ };
404
+ }
405
+ /** Formats a duration the way a dev session reads it: `mm:ss` under an hour, `h:mm:ss` above. */
406
+ function formatDuration(milliseconds) {
407
+ const total = Math.max(0, Math.floor(milliseconds / 1e3));
408
+ const seconds = total % 60;
409
+ const minutes = Math.floor(total / 60) % 60;
410
+ const hours = Math.floor(total / 3600);
411
+ const pad = (value) => value.toString().padStart(2, "0");
412
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
413
+ }
414
+
415
+ //#endregion
416
+ //#region src/renderers/ink/components/InfoOverlay.tsx
417
+ function InfoOverlay({ service, height, width, onClose }) {
418
+ const status = useDevStatus(service);
419
+ const uptime = useUptime(status.startedAt);
420
+ useInput((input, key) => {
421
+ if (!key.ctrl && (key.escape || ["q", "i"].includes(input))) onClose();
422
+ });
423
+ const lines = [
424
+ ["Project", service.config.packageJson?.name ?? "stars project"],
425
+ ["Root", service.config.root],
426
+ ["Node", process.version],
427
+ ["Builder", service.builder.tool],
428
+ ["Local", status.url ?? "—"],
429
+ ["Tunnel", status.tunnelUrl ?? status.tunnel],
430
+ ["Process", `${status.process}${status.pid ? ` (${status.pid})` : ""}`],
431
+ ["Uptime", uptime === null ? "—" : formatDuration(uptime)],
432
+ ["Restarts", String(status.restarts)],
433
+ ["Health", service.config.dev.health ? status.health : "not configured (READY reports process state)"],
434
+ ["Types", `${status.typecheck}${status.typeErrors ? ` (${status.typeErrors} errors)` : ""}`],
435
+ ["Log file", service.config.dev.logFile ?? "disabled"]
436
+ ];
437
+ return /* @__PURE__ */ jsxs(Box, {
438
+ flexDirection: "column",
439
+ height,
440
+ children: [
441
+ /* @__PURE__ */ jsxs(Text, {
442
+ bold: true,
443
+ wrap: "truncate-end",
444
+ children: [" ", "session info"]
445
+ }),
446
+ /* @__PURE__ */ jsx(Text, {
447
+ dimColor: true,
448
+ children: "─".repeat(width)
449
+ }),
450
+ lines.slice(0, Math.max(0, height - 3)).map(([label, value]) => /* @__PURE__ */ jsxs(Text, {
451
+ wrap: "truncate-end",
452
+ children: [
453
+ " ",
454
+ /* @__PURE__ */ jsx(Text, {
455
+ bold: true,
456
+ children: label.padEnd(10)
457
+ }),
458
+ /* @__PURE__ */ jsx(Text, {
459
+ dimColor: true,
460
+ children: value
461
+ })
462
+ ]
463
+ }, label)),
464
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
465
+ /* @__PURE__ */ jsx(Text, {
466
+ dimColor: true,
467
+ children: " q close"
468
+ })
469
+ ]
470
+ });
471
+ }
472
+
473
+ //#endregion
474
+ //#region src/renderers/ink/components/Panel.tsx
475
+ /** Nuxt's wordmark / URLs / progress / status / hints layout, with Stars branding. */
476
+ function Panel({ status, config, counters, frame, elapsedMs, width, height, confirmQuit }) {
477
+ const paint = useColor();
478
+ const state = describeBadge(status);
479
+ const busy = state.badge !== "ready" && state.badge !== "error";
480
+ const badge = !busy && counters.errors > 0 ? "error" : state.badge;
481
+ const note = badge === "error" && state.badge === "ready" ? "an error was logged · press e to view it" : state.note;
482
+ const filled = Math.round(Math.min(1, Math.max(0, status.progress.fraction)) * 20);
483
+ let shown = [
484
+ ...(config.dev.banner === false ? [] : config.dev.banner?.flatMap((line) => line.split("\n")) ?? [null]).slice(0, 4).map((line, index) => ({
485
+ name: "wordmark",
486
+ content: /* @__PURE__ */ jsxs(Box, {
487
+ justifyContent: "space-between",
488
+ children: [/* @__PURE__ */ jsxs(Text, {
489
+ wrap: "truncate-end",
490
+ children: [" ", line === null ? /* @__PURE__ */ jsxs(Fragment, { children: [
491
+ /* @__PURE__ */ jsx(Text, {
492
+ color: paint("green"),
493
+ children: [..."·✦★✦"].map((cell, i) => /* @__PURE__ */ jsx(Text, {
494
+ dimColor: busy && i !== frame % 4,
495
+ children: cell
496
+ }, i))
497
+ }),
498
+ " ",
499
+ /* @__PURE__ */ jsx(Text, {
500
+ bold: true,
501
+ color: paint("green"),
502
+ children: "Stars"
503
+ }),
504
+ /* @__PURE__ */ jsx(Text, {
505
+ dimColor: true,
506
+ children: ` ${readOwnPackageJson().version}`
507
+ })
508
+ ] }) : /* @__PURE__ */ jsx(Text, {
509
+ color: paint("green"),
510
+ children: line
511
+ })]
512
+ }), index === 0 && width >= 60 && badge === "ready" && status.progress.readyMs !== null && /* @__PURE__ */ jsx(Text, {
513
+ dimColor: true,
514
+ children: `ready in ${(status.progress.readyMs / 1e3).toFixed(2)}s `
515
+ })]
516
+ }, index)
517
+ })),
518
+ {
519
+ name: "space",
520
+ content: /* @__PURE__ */ jsx(Text, { children: " " })
521
+ },
522
+ ...status.url ? [{
523
+ name: "urls",
524
+ content: /* @__PURE__ */ jsxs(Text, {
525
+ wrap: "truncate-end",
526
+ children: [
527
+ " ",
528
+ /* @__PURE__ */ jsx(Text, {
529
+ dimColor: true,
530
+ children: "Local "
531
+ }),
532
+ /* @__PURE__ */ jsx(Text, {
533
+ color: paint("cyan"),
534
+ dimColor: busy,
535
+ children: status.url
536
+ }),
537
+ busy && /* @__PURE__ */ jsx(Text, {
538
+ color: paint("yellow"),
539
+ children: ` ${[
540
+ "⠋",
541
+ "⠙",
542
+ "⠹",
543
+ "⠸",
544
+ "⠼",
545
+ "⠴",
546
+ "⠦",
547
+ "⠧",
548
+ "⠇",
549
+ "⠏"
550
+ ][frame % 10]}`
551
+ })
552
+ ]
553
+ })
554
+ }] : [],
555
+ ...status.tunnelUrl ? [{
556
+ name: "urls",
557
+ content: /* @__PURE__ */ jsxs(Text, {
558
+ wrap: "truncate-end",
559
+ children: [
560
+ " ",
561
+ /* @__PURE__ */ jsx(Text, {
562
+ dimColor: true,
563
+ children: "Tunnel "
564
+ }),
565
+ /* @__PURE__ */ jsx(Text, {
566
+ color: paint("magenta"),
567
+ children: status.tunnelUrl
568
+ })
569
+ ]
570
+ })
571
+ }] : [],
572
+ {
573
+ name: "space",
574
+ content: /* @__PURE__ */ jsx(Text, { children: " " })
575
+ },
576
+ {
577
+ name: "summary",
578
+ content: busy ? /* @__PURE__ */ jsxs(Text, {
579
+ wrap: "truncate-end",
580
+ children: [
581
+ " ",
582
+ /* @__PURE__ */ jsx(Text, {
583
+ color: paint("green"),
584
+ children: "━".repeat(filled)
585
+ }),
586
+ /* @__PURE__ */ jsxs(Text, {
587
+ dimColor: true,
588
+ children: ["━".repeat(20 - filled), ` ${Math.round(status.progress.fraction * 100)}% · ${(elapsedMs / 1e3).toFixed(1)}s`]
589
+ })
590
+ ]
591
+ }) : /* @__PURE__ */ jsxs(Text, {
592
+ wrap: "truncate-end",
593
+ children: [
594
+ " ",
595
+ counters.warnings > 0 && /* @__PURE__ */ jsx(Text, {
596
+ color: paint("yellow"),
597
+ children: `⚠ ${counters.warnings} ${plural(counters.warnings, "warning")} `
598
+ }),
599
+ counters.errors > 0 && /* @__PURE__ */ jsx(Text, {
600
+ bold: true,
601
+ color: paint("red"),
602
+ children: `✖ ${counters.errors} ${plural(counters.errors, "error")} `
603
+ }),
604
+ counters.errors === 0 && counters.warnings === 0 && /* @__PURE__ */ jsx(Text, {
605
+ dimColor: true,
606
+ children: status.typecheck === "checking" ? "checking types" : status.tunnel === "failed" ? "tunnel failed · press l to view logs" : "logs folded away · press l to view"
607
+ })
608
+ ]
609
+ })
610
+ },
611
+ {
612
+ name: "space",
613
+ content: /* @__PURE__ */ jsx(Text, { children: " " })
614
+ },
615
+ {
616
+ name: "status",
617
+ content: /* @__PURE__ */ jsxs(Text, {
618
+ wrap: "truncate-end",
619
+ children: [
620
+ " ",
621
+ /* @__PURE__ */ jsx(Text, {
622
+ bold: true,
623
+ backgroundColor: paint(confirmQuit || busy ? "yellow" : badge === "error" ? "red" : "green"),
624
+ color: paint(badge === "error" && !confirmQuit ? "white" : "black"),
625
+ children: ` ${confirmQuit ? "QUIT?" : badge === "restarting" ? "RESTART" : badge.toUpperCase()} `
626
+ }),
627
+ " ",
628
+ /* @__PURE__ */ jsx(Text, {
629
+ dimColor: true,
630
+ children: confirmQuit ? "press y to confirm, esc to stay" : note
631
+ })
632
+ ]
633
+ })
634
+ },
635
+ {
636
+ name: "hints",
637
+ content: confirmQuit ? /* @__PURE__ */ jsx(Text, { children: " " }) : /* @__PURE__ */ jsx(Hints, {
638
+ width,
639
+ hints: [...counters.errors ? [{
640
+ key: "e",
641
+ label: "last error",
642
+ priority: Infinity
643
+ }] : [], ...PANEL_HINTS]
644
+ })
645
+ }
646
+ ];
647
+ const budget = Math.max(2, Math.min(height, 14));
648
+ for (const name of [
649
+ "space",
650
+ "summary",
651
+ "urls",
652
+ "wordmark"
653
+ ]) {
654
+ if (shown.length <= budget) break;
655
+ shown = shown.filter((block) => block.name !== name);
656
+ }
657
+ shown = shown.filter((block, i, all) => block.name !== "space" || i > 0 && i < all.length - 1 && all[i - 1]?.name !== "space");
658
+ return /* @__PURE__ */ jsx(Box, {
659
+ flexDirection: "column",
660
+ children: shown.map((block, index) => /* @__PURE__ */ jsx(Box, {
661
+ flexDirection: "column",
662
+ height: 1,
663
+ children: block.content
664
+ }, index))
665
+ });
666
+ }
667
+ function plural(count, word) {
668
+ return count === 1 ? word : `${word}s`;
669
+ }
670
+
671
+ //#endregion
672
+ //#region src/renderers/ink/hooks/useLogCounters.ts
673
+ /**
674
+ * Warnings and errors logged since the last successful build, the way Nuxt CLI v4's own panel counts them: the
675
+ * badge's job is the current state, this line is what happened getting there.
676
+ */
677
+ function useLogCounters(service) {
678
+ const count = () => service.logs.entries().reduce((total, entry) => ({
679
+ warnings: total.warnings + Number(entry.level === "warn"),
680
+ errors: total.errors + Number(entry.level === "error" && !isErrorDetail(entry))
681
+ }), {
682
+ warnings: 0,
683
+ errors: 0
684
+ });
685
+ const [counters, setCounters] = useState(count);
686
+ useEffect(() => {
687
+ let previousBuild = service.status.build;
688
+ let previousStart = service.status.startedAt;
689
+ const onEntry = (entry) => {
690
+ if (entry.level === "warn") setCounters((current) => ({
691
+ ...current,
692
+ warnings: current.warnings + 1
693
+ }));
694
+ else if (entry.level === "error" && !isErrorDetail(entry)) setCounters((current) => ({
695
+ ...current,
696
+ errors: current.errors + 1
697
+ }));
698
+ };
699
+ const onStatus = (status) => {
700
+ if (status.build === "building" && previousBuild !== "building" && previousBuild !== "idle" || status.lastRestartReason === "manual" && status.startedAt !== null && status.startedAt !== previousStart) setCounters({
701
+ warnings: 0,
702
+ errors: 0
703
+ });
704
+ previousBuild = status.build;
705
+ previousStart = status.startedAt;
706
+ };
707
+ service.logs.on("entry", onEntry);
708
+ const onClear = () => setCounters({
709
+ warnings: 0,
710
+ errors: 0
711
+ });
712
+ service.logs.on("clear", onClear);
713
+ service.on("status", onStatus);
714
+ setCounters(count());
715
+ return () => {
716
+ service.logs.off("entry", onEntry);
717
+ service.logs.off("clear", onClear);
718
+ service.off("status", onStatus);
719
+ };
720
+ }, [service]);
721
+ return counters;
722
+ }
723
+
724
+ //#endregion
725
+ //#region src/renderers/ink/hooks/useBuildClock.ts
726
+ /** The clock moves during a long compiler phase; the percentage only moves on real build events. */
727
+ function useBuildClock(startedAt, active, reducedMotion) {
728
+ const [now, setNow] = useState(Date.now);
729
+ useEffect(() => {
730
+ if (!active) return;
731
+ setNow(Date.now());
732
+ const timer = setInterval(() => setNow(Date.now()), reducedMotion ? 1e3 : 120);
733
+ return () => clearInterval(timer);
734
+ }, [
735
+ startedAt,
736
+ active,
737
+ reducedMotion
738
+ ]);
739
+ const elapsedMs = Math.max(0, now - startedAt);
740
+ return {
741
+ elapsedMs,
742
+ frame: reducedMotion ? 0 : Math.floor(elapsedMs / 120)
743
+ };
744
+ }
745
+
746
+ //#endregion
747
+ //#region src/lib/open-url.ts
748
+ /** Called only on an explicit `o` keypress, with no shell interpolation. */
749
+ function openDevUrl(value) {
750
+ if (!value) return Promise.resolve();
751
+ const url = new URL(value);
752
+ if (url.protocol !== "http:" && url.protocol !== "https:") return Promise.reject(/* @__PURE__ */ new Error("Only HTTP(S) URLs can be opened."));
753
+ const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url.href]] : process.platform === "darwin" ? ["open", [url.href]] : ["xdg-open", [url.href]];
754
+ return new Promise((resolve, reject) => execFile(command, [...args], (error) => error ? reject(/* @__PURE__ */ new Error(`Could not open the browser: ${error.message}`)) : resolve()));
755
+ }
756
+
757
+ //#endregion
758
+ //#region src/renderers/ink/DevApp.tsx
759
+ /**
760
+ * A bottom-aligned panel in the normal buffer, with logs folded away. Only the browsable overlays use the
761
+ * alternate screen, preserving the terminal's history and the panel when they close.
762
+ */
763
+ function DevApp({ service, color, reducedMotion, onQuit, onViewChange, onCopy }) {
764
+ const { columns, rows } = useWindowSize();
765
+ const [view, setView] = useState("panel");
766
+ const [confirmQuit, setConfirmQuit] = useState(false);
767
+ const status = useDevStatus(service);
768
+ const counters = useLogCounters(service);
769
+ const badge = describeBadge(status).badge;
770
+ const busy = badge !== "ready" && badge !== "error";
771
+ const clock = useBuildClock(status.progress.startedAt, busy && view === "panel", reducedMotion);
772
+ const width = Math.max(1, columns);
773
+ const height = Math.max(2, rows - 1);
774
+ const changeView = (next) => {
775
+ onViewChange(next !== "panel");
776
+ setView(next);
777
+ };
778
+ useInput((input, key) => {
779
+ if (key.ctrl && input === "c") return onQuit();
780
+ if (key.ctrl && input === "l") return service.clearLogs();
781
+ if (view !== "panel") return;
782
+ if (confirmQuit) {
783
+ if (input === "y") return onQuit();
784
+ return setConfirmQuit(false);
785
+ }
786
+ if (key.ctrl && input === "r") return void service.restart("manual");
787
+ if (key.ctrl && input === "d") return busy ? setConfirmQuit(true) : onQuit();
788
+ switch (input) {
789
+ case "q": return busy ? setConfirmQuit(true) : onQuit();
790
+ case "r":
791
+ service.restart("manual");
792
+ return;
793
+ case "l": return changeView("logs");
794
+ case "e": return changeView("errors");
795
+ case "c": return service.clearLogs();
796
+ case "o":
797
+ openDevUrl(status.url).catch((error) => service.log("stars", "warn", error.message));
798
+ return;
799
+ case "t":
800
+ service.toggleTunnel();
801
+ return;
802
+ case "i": return changeView("info");
803
+ case "h":
804
+ case "?": return changeView("help");
805
+ }
806
+ });
807
+ return /* @__PURE__ */ jsx(ColorProvider, {
808
+ color,
809
+ children: /* @__PURE__ */ jsxs(Box, {
810
+ width,
811
+ height,
812
+ flexDirection: "column",
813
+ justifyContent: view === "panel" ? "flex-end" : "flex-start",
814
+ children: [
815
+ (view === "logs" || view === "errors") && /* @__PURE__ */ jsx(LogBrowser, {
816
+ service,
817
+ height,
818
+ width,
819
+ lastError: view === "errors",
820
+ onCopy,
821
+ onClose: () => changeView("panel")
822
+ }),
823
+ view === "help" && /* @__PURE__ */ jsx(HelpOverlay, {
824
+ height,
825
+ width,
826
+ onClose: () => changeView("panel")
827
+ }),
828
+ view === "info" && /* @__PURE__ */ jsx(InfoOverlay, {
829
+ service,
830
+ height,
831
+ width,
832
+ onClose: () => changeView("panel")
833
+ }),
834
+ view === "panel" && /* @__PURE__ */ jsx(Panel, {
835
+ status,
836
+ config: service.config,
837
+ counters,
838
+ ...clock,
839
+ width,
840
+ height,
841
+ confirmQuit
842
+ })
843
+ ]
844
+ })
845
+ });
846
+ }
847
+
848
+ //#endregion
849
+ //#region src/renderers/capture-output.ts
850
+ /** Capture third-party writes before build plugins run. Ink writes through a separate, unpatched sink. */
851
+ function captureOutput(service, stream, fallback) {
852
+ const original = stream.write;
853
+ const decoder = new StringDecoder("utf8");
854
+ let pending = "";
855
+ const emit = (text) => {
856
+ if (text.trim()) service.log("build", classifyAppLine(text, fallback), text);
857
+ };
858
+ const write = (chunk, encoding, callback) => {
859
+ pending += typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? decoder.write(Buffer.from(chunk)) : String(chunk);
860
+ const lines = pending.split(/\r?\n/);
861
+ pending = lines.pop() ?? "";
862
+ for (const line of lines) emit(line);
863
+ if (pending.length > 65536) {
864
+ emit(pending);
865
+ pending = "";
866
+ }
867
+ const done = typeof encoding === "function" ? encoding : callback;
868
+ if (typeof done === "function") queueMicrotask(() => done());
869
+ return true;
870
+ };
871
+ stream.write = write;
872
+ return () => {
873
+ if (stream.write === write) stream.write = original;
874
+ emit(pending + decoder.end());
875
+ pending = "";
876
+ };
877
+ }
878
+
879
+ //#endregion
880
+ //#region src/renderers/tui.tsx
881
+ /**
882
+ * A normal-buffer, bottom-aligned panel and alternate-buffer overlays. External output is captured for the log
883
+ * browser; the renderer alone writes to the terminal. Switching views and teardown restore the original buffer.
884
+ *
885
+ * `start()` resolves when the user quits, which is what `stars dev` waits on before shutting the bot down.
886
+ */
887
+ function createTuiRenderer(service, options = {}) {
888
+ const stdout = options.stdout ?? process.stdout;
889
+ const stdin = options.stdin ?? process.stdin;
890
+ const write = stdout.write.bind(stdout);
891
+ const sink = new Proxy(stdout, { get(target, property) {
892
+ if (property === "write") return write;
893
+ const value = Reflect.get(target, property, target);
894
+ return typeof value === "function" ? value.bind(target) : value;
895
+ } });
896
+ const restoreOutput = stdout === process.stdout ? [captureOutput(service, process.stdout, "info"), captureOutput(service, process.stderr, "warn")] : [];
897
+ let alternate = false;
898
+ let stopped = false;
899
+ const switchView = (overlay) => {
900
+ if (overlay === alternate || stopped) return;
901
+ instance.clear();
902
+ write(overlay ? "\x1B[?1049h\x1B[H" : "\x1B[?1049l");
903
+ alternate = overlay;
904
+ };
905
+ let quit;
906
+ const quitting = new Promise((resolve) => {
907
+ quit = resolve;
908
+ });
909
+ const instance = render(/* @__PURE__ */ jsx(DevApp, {
910
+ service,
911
+ color: options.color ?? false,
912
+ reducedMotion: options.reducedMotion ?? false,
913
+ onQuit: quit,
914
+ onViewChange: switchView,
915
+ onCopy: (text) => write(`\u001B]52;c;${Buffer.from(text.slice(0, 65536)).toString("base64")}\u0007`)
916
+ }), {
917
+ stdout: sink,
918
+ stdin,
919
+ exitOnCtrlC: false,
920
+ patchConsole: false,
921
+ interactive: true,
922
+ ...options.fps === void 0 ? {} : { maxFps: options.fps }
923
+ });
924
+ return {
925
+ async start() {
926
+ await Promise.race([quitting, instance.waitUntilExit()]);
927
+ },
928
+ stop() {
929
+ if (stopped) return;
930
+ stopped = true;
931
+ if (alternate) instance.clear();
932
+ instance.unmount();
933
+ if (alternate) write("\x1B[?1049l");
934
+ write("\x1B[?25h");
935
+ for (const restore of restoreOutput) restore();
936
+ if (service.status.progress.readyMs === null) {
937
+ const error = service.logs.entries().findLast((entry) => entry.level === "error" && !isErrorDetail(entry));
938
+ if (error) write(`\n${error.text}\n`);
939
+ }
940
+ quit();
941
+ }
942
+ };
943
+ }
944
+
945
+ //#endregion
946
+ export { createTuiRenderer };
947
+ //# sourceMappingURL=tui-BCGQrMQo.js.map