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