@wolfstar/cli 2.2.0 → 2.3.0-next-20261004142621

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 (90) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +465 -304
  3. package/dist/{_shared-DJrpalNQ.js → _shared-BIQK0Bhm.js} +2 -2
  4. package/dist/_shared-BIQK0Bhm.js.map +1 -0
  5. package/dist/bridge-BvkmlAOZ.js +410 -0
  6. package/dist/bridge-BvkmlAOZ.js.map +1 -0
  7. package/dist/{build-CluOR8dJ.js → build-DJcc95Yq.js} +5 -5
  8. package/dist/build-DJcc95Yq.js.map +1 -0
  9. package/dist/cli.js +1 -1
  10. package/dist/cli.js.map +1 -1
  11. package/dist/{codegen-D6V1i_fa.js → codegen-aJSyPz-J.js} +4 -4
  12. package/dist/codegen-aJSyPz-J.js.map +1 -0
  13. package/dist/command-diff-BK912Hjc.js +78 -0
  14. package/dist/command-diff-BK912Hjc.js.map +1 -0
  15. package/dist/commands-CIPWQtF0.js +3 -0
  16. package/dist/commands-Clpbqq3K.js +15 -0
  17. package/dist/commands-Clpbqq3K.js.map +1 -0
  18. package/dist/{commands-BJ589_AC.js → commands-DVC7ky6f.js} +259 -9
  19. package/dist/commands-DVC7ky6f.js.map +1 -0
  20. package/dist/completions-D2KYtrrv.js +161 -0
  21. package/dist/completions-D2KYtrrv.js.map +1 -0
  22. package/dist/{dev-DJJZN4NJ.js → dev-oPPVR7d_.js} +434 -28
  23. package/dist/dev-oPPVR7d_.js.map +1 -0
  24. package/dist/{diagnostics-3JRAy8Xj.js → diagnostics-BysbNnPD.js} +22 -1
  25. package/dist/diagnostics-BysbNnPD.js.map +1 -0
  26. package/dist/doctor-DkVUwgDd.js +334 -0
  27. package/dist/doctor-DkVUwgDd.js.map +1 -0
  28. package/dist/errors-COFWKKKw.js.map +1 -1
  29. package/dist/external-CxFKxwi3.js.map +1 -1
  30. package/dist/framework-auto-imports-Gu_PXSyr.js.map +1 -1
  31. package/dist/{hooks-BfDFM38k.js → hooks-Cy6F2bhH.js} +20 -4
  32. package/dist/hooks-Cy6F2bhH.js.map +1 -0
  33. package/dist/index.d.ts +53 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +3 -3
  36. package/dist/{info-C1Kx_4Og.js → info-CKJVMWIT.js} +15 -4
  37. package/dist/info-CKJVMWIT.js.map +1 -0
  38. package/dist/{locales-BzOLRBVg.js → locales-BJAP4HYf.js} +5 -5
  39. package/dist/locales-BJAP4HYf.js.map +1 -0
  40. package/dist/log-view-B7uJEnMR.js +342 -0
  41. package/dist/log-view-B7uJEnMR.js.map +1 -0
  42. package/dist/{nitro-yjqZBh5L.js → nitro-B8u1iVgf.js} +2 -2
  43. package/dist/nitro-B8u1iVgf.js.map +1 -0
  44. package/dist/none-uC_wi_l4.js.map +1 -1
  45. package/dist/output-mode-Bp7Da8vJ.js.map +1 -1
  46. package/dist/{plain-B6z85uQP.js → plain-DG7cy0Kk.js} +7 -2
  47. package/dist/plain-DG7cy0Kk.js.map +1 -0
  48. package/dist/{prepare-Cb45Z4re.js → prepare-BM93i3Rm.js} +4 -4
  49. package/dist/prepare-BM93i3Rm.js.map +1 -0
  50. package/dist/{project-D-8VIcyF.js → project-zlQStwsf.js} +2 -2
  51. package/dist/project-zlQStwsf.js.map +1 -0
  52. package/dist/{run-BiLZqib3.js → run-DMM2YlP3.js} +2 -12
  53. package/dist/run-DMM2YlP3.js.map +1 -0
  54. package/dist/{theme-3Fc_LI-2.js → theme-yS2h5dd_.js} +25 -1
  55. package/dist/theme-yS2h5dd_.js.map +1 -0
  56. package/dist/tsc-C5l5TQGC.js +3 -0
  57. package/dist/{tsc-DEBcwIO0.js → tsc-puVAcwmo.js} +24 -5
  58. package/dist/tsc-puVAcwmo.js.map +1 -0
  59. package/dist/{tsdown-DrWXld54.js → tsdown-Dwt3JhPw.js} +2 -2
  60. package/dist/tsdown-Dwt3JhPw.js.map +1 -0
  61. package/dist/{tui-BRddrLQP.js → tui-nldGSD4c.js} +913 -141
  62. package/dist/tui-nldGSD4c.js.map +1 -0
  63. package/dist/version-BzclCE19.js.map +1 -1
  64. package/dist/{vite-Bf7nrJx7.js → vite-YZUtv2Hk.js} +2 -2
  65. package/dist/vite-YZUtv2Hk.js.map +1 -0
  66. package/package.json +5 -5
  67. package/dist/_shared-DJrpalNQ.js.map +0 -1
  68. package/dist/build-CluOR8dJ.js.map +0 -1
  69. package/dist/codegen-D6V1i_fa.js.map +0 -1
  70. package/dist/commands-BJ589_AC.js.map +0 -1
  71. package/dist/dev-DJJZN4NJ.js.map +0 -1
  72. package/dist/diagnostics-3JRAy8Xj.js.map +0 -1
  73. package/dist/hooks-BfDFM38k.js.map +0 -1
  74. package/dist/info-C1Kx_4Og.js.map +0 -1
  75. package/dist/locales-BzOLRBVg.js.map +0 -1
  76. package/dist/log-buffer-CY82aqnb.js +0 -72
  77. package/dist/log-buffer-CY82aqnb.js.map +0 -1
  78. package/dist/nitro-yjqZBh5L.js.map +0 -1
  79. package/dist/plain-B6z85uQP.js.map +0 -1
  80. package/dist/prepare-Cb45Z4re.js.map +0 -1
  81. package/dist/project-D-8VIcyF.js.map +0 -1
  82. package/dist/run-BiLZqib3.js.map +0 -1
  83. package/dist/theme-3Fc_LI-2.js.map +0 -1
  84. package/dist/tsc-B931Ezh7.js +0 -3
  85. package/dist/tsc-DEBcwIO0.js.map +0 -1
  86. package/dist/tsdown-DrWXld54.js.map +0 -1
  87. package/dist/tui-BRddrLQP.js.map +0 -1
  88. package/dist/tunnel-DHSxOZKr.js +0 -175
  89. package/dist/tunnel-DHSxOZKr.js.map +0 -1
  90. package/dist/vite-Bf7nrJx7.js.map +0 -1
@@ -1,13 +1,34 @@
1
1
  import { t as readOwnPackageJson } from "./version-BzclCE19.js";
2
- import { n as THEME_SETTINGS, o as resolveTheme, r as detectTerminalBackground, t as THEMES } from "./theme-3Fc_LI-2.js";
3
- import { n as classifyAppLine, r as isErrorDetail } from "./log-buffer-CY82aqnb.js";
2
+ import { n as THEME_SETTINGS, o as resolveTheme, r as detectTerminalBackground, t as THEMES } from "./theme-yS2h5dd_.js";
3
+ import { t as findInstalledVersion } from "./project-zlQStwsf.js";
4
+ import { t as describeCommand } from "./command-diff-BK912Hjc.js";
5
+ import { S as isErrorDetail, _ as windowAround, a as initialLogView, c as levelBadge, d as pinAt, f as pinnedEnd, g as toggleLevel, h as toggleChannel, i as highlight, l as matchesView, m as soloLevel, n as buildRows, o as isChannelVisible, p as soloChannel, r as formatClock, t as VIEW_LEVELS, u as newestErrorRow, v as wrapLabels, x as classifyAppLine } from "./log-view-B7uJEnMR.js";
6
+ import { displayPath } from "@wolfstar/schema";
4
7
  import { execFile } from "node:child_process";
5
8
  import { stripVTControlCharacters } from "node:util";
6
9
  import { Box, Text, render, useInput, useWindowSize } from "ink";
7
- import { createContext, useContext, useEffect, useMemo, useState } from "react";
10
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
8
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
12
  import { StringDecoder } from "node:string_decoder";
10
13
 
14
+ //#region src/dev/tui/ink/hooks/useLogVersion.ts
15
+ /** Bumps whenever the buffer changes, so a consumer can re-derive a filtered view without re-filtering on every render. */
16
+ function useLogVersion(logs) {
17
+ const [version, setVersion] = useState(0);
18
+ useEffect(() => {
19
+ const bump = () => setVersion((current) => current + 1);
20
+ logs.on("entry", bump);
21
+ logs.on("clear", bump);
22
+ bump();
23
+ return () => {
24
+ logs.off("entry", bump);
25
+ logs.off("clear", bump);
26
+ };
27
+ }, [logs]);
28
+ return version;
29
+ }
30
+
31
+ //#endregion
11
32
  //#region src/dev/tui/ink/theme.tsx
12
33
  /**
13
34
  * `stars dev` decides once whether the UI may use colour (`NO_COLOR`, `FORCE_COLOR`, TTY detection) and which theme
@@ -37,19 +58,808 @@ function useColorEnabled() {
37
58
  return useContext(ThemeContext).color;
38
59
  }
39
60
 
61
+ //#endregion
62
+ //#region src/dev/tui/ink/components/LogPane.tsx
63
+ const LEVEL_TOKENS$1 = {
64
+ trace: "trace",
65
+ debug: "debug",
66
+ info: "info",
67
+ success: "success",
68
+ warn: "warning",
69
+ error: "error"
70
+ };
71
+ const SEGMENT_TOKENS = {
72
+ url: "url",
73
+ path: null,
74
+ number: "accent",
75
+ name: "trace",
76
+ dim: null,
77
+ ok: "success",
78
+ warn: "warning",
79
+ error: "error"
80
+ };
81
+ /** The colours a channel's dot is picked from, by name, so a channel keeps its colour for the whole session. */
82
+ const CHANNEL_TOKENS = [
83
+ "info",
84
+ "debug",
85
+ "trace",
86
+ "accent",
87
+ "url",
88
+ "tunnel"
89
+ ];
90
+ function channelToken(channel) {
91
+ let hash = 0;
92
+ for (const character of channel) hash = hash * 31 + character.codePointAt(0) >>> 0;
93
+ return CHANNEL_TOKENS[hash % CHANNEL_TOKENS.length];
94
+ }
95
+ /** The log stream: one line per row, the newest at the bottom unless the view is scrolled. */
96
+ function LogPane({ rows, end, width, height, channelWidth, marked }) {
97
+ const paint = usePaint();
98
+ const color = useColorEnabled();
99
+ const visible = rows.slice(Math.max(0, end - height), end);
100
+ const gutter = " ".repeat(15 + channelWidth + 1);
101
+ return /* @__PURE__ */ jsxs(Box, {
102
+ flexDirection: "column",
103
+ width,
104
+ height,
105
+ overflow: "hidden",
106
+ justifyContent: "flex-end",
107
+ children: [visible.length === 0 && /* @__PURE__ */ jsx(Text, {
108
+ dimColor: true,
109
+ children: " no matching logs"
110
+ }), visible.map((row, index) => {
111
+ const key = `${end - visible.length + index}`;
112
+ if (row.kind === "rule") return /* @__PURE__ */ jsx(Text, {
113
+ dimColor: true,
114
+ wrap: "truncate-end",
115
+ children: "─".repeat(width)
116
+ }, key);
117
+ if (row.kind === "header") return /* @__PURE__ */ jsxs(Text, {
118
+ wrap: "truncate-end",
119
+ children: [/* @__PURE__ */ jsx(Text, {
120
+ bold: true,
121
+ color: paint(channelToken(row.channel)),
122
+ children: ` ${row.channel}`
123
+ }), /* @__PURE__ */ jsx(Text, {
124
+ dimColor: true,
125
+ children: ` · ${row.count}`
126
+ })]
127
+ }, key);
128
+ const { entry } = row;
129
+ const failed = entry.level === "error";
130
+ if (row.kind === "detail") return /* @__PURE__ */ jsxs(Text, {
131
+ wrap: "truncate-end",
132
+ children: [
133
+ gutter,
134
+ /* @__PURE__ */ jsx(Text, {
135
+ dimColor: true,
136
+ children: "│ "
137
+ }),
138
+ /* @__PURE__ */ jsx(Message, {
139
+ text: row.text,
140
+ channel: entry.channel,
141
+ color: failed ? paint("error") : void 0,
142
+ dim: !failed
143
+ })
144
+ ]
145
+ }, key);
146
+ const level = LEVEL_TOKENS$1[entry.level];
147
+ return /* @__PURE__ */ jsxs(Text, {
148
+ wrap: "truncate-end",
149
+ children: [
150
+ color ? /* @__PURE__ */ jsx(Text, {
151
+ bold: true,
152
+ backgroundColor: paint(level),
153
+ color: paint(failed ? "onError" : "onBright"),
154
+ children: ` ${levelBadge(entry.level)} `
155
+ }) : /* @__PURE__ */ jsx(Text, {
156
+ bold: true,
157
+ children: `[${levelBadge(entry.level)}]`
158
+ }),
159
+ /* @__PURE__ */ jsx(Text, {
160
+ color: paint("brand"),
161
+ children: entry.id === marked ? "▎" : " "
162
+ }),
163
+ " ",
164
+ /* @__PURE__ */ jsx(Text, {
165
+ dimColor: !failed,
166
+ color: failed ? paint("error") : void 0,
167
+ children: formatClock(entry.time)
168
+ }),
169
+ " ",
170
+ /* @__PURE__ */ jsx(Text, {
171
+ bold: failed,
172
+ color: failed ? paint("error") : void 0,
173
+ children: entry.channel.slice(0, channelWidth).padStart(channelWidth)
174
+ }),
175
+ " ",
176
+ /* @__PURE__ */ jsx(Text, {
177
+ color: paint(channelToken(entry.channel)),
178
+ children: "●"
179
+ }),
180
+ " ",
181
+ /* @__PURE__ */ jsx(Message, {
182
+ text: row.text,
183
+ channel: entry.channel,
184
+ color: entry.level === "warn" ? paint("warning") : void 0
185
+ })
186
+ ]
187
+ }, key);
188
+ })]
189
+ });
190
+ }
191
+ function Message({ text, channel, color, dim = false }) {
192
+ const paint = usePaint();
193
+ return /* @__PURE__ */ jsx(Text, {
194
+ color,
195
+ dimColor: dim,
196
+ children: highlight(text, channel).map((segment, index) => {
197
+ if (segment.token === void 0) return segment.text;
198
+ const token = SEGMENT_TOKENS[segment.token];
199
+ return /* @__PURE__ */ jsx(Text, {
200
+ color: token === null ? color : paint(token),
201
+ dimColor: token === null || dim,
202
+ bold: segment.token === "name",
203
+ children: segment.text
204
+ }, index);
205
+ })
206
+ });
207
+ }
208
+
209
+ //#endregion
210
+ //#region src/dev/tui/ink/components/PromptCard.tsx
211
+ /** The rows a prompt takes, border included, so the log pane above it can be sized. */
212
+ function promptHeight(prompt, available) {
213
+ return Math.min(Math.max(4, available), prompt.changes.length + 4);
214
+ }
215
+ /**
216
+ * The card under the log stream that asks before the bot's commands are deployed again: a redeploy changes what
217
+ * every user of the application sees, so it is never done behind the developer's back.
218
+ */
219
+ function PromptCard({ prompt, width, height }) {
220
+ const paint = usePaint();
221
+ const room = Math.max(0, height - 4);
222
+ const shown = prompt.changes.slice(0, prompt.changes.length > room ? Math.max(0, room - 1) : room);
223
+ const more = prompt.changes.length - shown.length;
224
+ return /* @__PURE__ */ jsxs(Box, {
225
+ flexDirection: "column",
226
+ width,
227
+ height,
228
+ borderStyle: "single",
229
+ borderColor: paint("accent"),
230
+ paddingX: 1,
231
+ children: [
232
+ /* @__PURE__ */ jsx(Text, {
233
+ bold: true,
234
+ color: paint("accent"),
235
+ wrap: "truncate-end",
236
+ children: "Commands updated:"
237
+ }),
238
+ shown.map((change) => /* @__PURE__ */ jsxs(Text, {
239
+ wrap: "truncate-end",
240
+ children: [
241
+ /* @__PURE__ */ jsx(Text, {
242
+ dimColor: true,
243
+ children: "- "
244
+ }),
245
+ /* @__PURE__ */ jsx(Text, {
246
+ color: paint(change.kind === "removed" ? "error" : change.kind === "added" ? "success" : "url"),
247
+ children: describeCommand(change)
248
+ }),
249
+ /* @__PURE__ */ jsx(Text, {
250
+ dimColor: true,
251
+ children: ` ${change.kind}`
252
+ })
253
+ ]
254
+ }, `${change.guild}:${change.type}:${change.name}`)),
255
+ more > 0 && /* @__PURE__ */ jsx(Text, {
256
+ dimColor: true,
257
+ children: ` and ${more} more`
258
+ }),
259
+ /* @__PURE__ */ jsxs(Text, {
260
+ wrap: "truncate-end",
261
+ children: ["Refresh commands? ", /* @__PURE__ */ jsx(Text, {
262
+ dimColor: true,
263
+ children: "(y/n)"
264
+ })]
265
+ })
266
+ ]
267
+ });
268
+ }
269
+
270
+ //#endregion
271
+ //#region src/dev/tui/panel-logic.ts
272
+ /** Whether the badge describes work in progress, as opposed to a state the session rests in. */
273
+ function isBusy(badge) {
274
+ return badge === "starting" || badge === "building" || badge === "restarting";
275
+ }
276
+ /**
277
+ * The badge and standing description for the panel's status row, mirroring the states Nuxt CLI v4's own dev panel
278
+ * cycles through (`STARTING`/`BUILDING`/`RESTART`/`READY`/`ERROR`).
279
+ */
280
+ function describeBadge(status) {
281
+ if (status.build === "building") return {
282
+ badge: status.startedAt === null ? "starting" : "building",
283
+ note: status.progress.message
284
+ };
285
+ if (status.process === "starting") return {
286
+ badge: "starting",
287
+ note: "starting the bot"
288
+ };
289
+ if (status.paused) return {
290
+ badge: "stopped",
291
+ note: "the bot is stopped, press r to start it"
292
+ };
293
+ if (status.process === "stopping") return {
294
+ badge: "restarting",
295
+ note: "restarting the bot"
296
+ };
297
+ if (status.build === "failed") return {
298
+ badge: "error",
299
+ note: "build failed, waiting for changes"
300
+ };
301
+ if (status.process === "crashed") return {
302
+ badge: "error",
303
+ note: "the bot crashed, press r to restart"
304
+ };
305
+ if (status.health === "down") return {
306
+ badge: "error",
307
+ note: "health check failed"
308
+ };
309
+ if (status.typecheck === "failed") return {
310
+ badge: "error",
311
+ note: "type check failed, press e to view it"
312
+ };
313
+ if (status.process === "stopped") return {
314
+ badge: "error",
315
+ note: "the bot stopped, press r to restart"
316
+ };
317
+ if (status.progress.fraction < 1) return {
318
+ badge: "starting",
319
+ note: status.progress.message
320
+ };
321
+ if (status.process === "running") return {
322
+ badge: "ready",
323
+ note: "watching for changes"
324
+ };
325
+ return {
326
+ badge: "starting",
327
+ note: "waiting for the first build"
328
+ };
329
+ }
330
+ /** Formats an uptime the way a status line reads it: `35s`, `3m 35s`, `2h 04m`. */
331
+ function formatUptime(milliseconds) {
332
+ const total = Math.max(0, Math.floor(milliseconds / 1e3));
333
+ const seconds = total % 60;
334
+ const minutes = Math.floor(total / 60) % 60;
335
+ const hours = Math.floor(total / 3600);
336
+ if (hours > 0) return `${hours}h ${minutes.toString().padStart(2, "0")}m`;
337
+ return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
338
+ }
339
+ /** Formats a duration the way a dev session reads it: `mm:ss` under an hour, `h:mm:ss` above. */
340
+ function formatDuration(milliseconds) {
341
+ const total = Math.max(0, Math.floor(milliseconds / 1e3));
342
+ const seconds = total % 60;
343
+ const minutes = Math.floor(total / 60) % 60;
344
+ const hours = Math.floor(total / 3600);
345
+ const pad = (value) => value.toString().padStart(2, "0");
346
+ return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
347
+ }
348
+
349
+ //#endregion
350
+ //#region src/dev/tui/ink/hooks/useUptime.ts
351
+ /**
352
+ * Re-renders once a second while the bot is up, so the uptime in the header keeps counting.
353
+ */
354
+ function useUptime(startedAt) {
355
+ const [now, setNow] = useState(() => Date.now());
356
+ useEffect(() => {
357
+ if (startedAt === null) return;
358
+ setNow(Date.now());
359
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
360
+ return () => clearInterval(timer);
361
+ }, [startedAt]);
362
+ return startedAt === null ? null : now - startedAt;
363
+ }
364
+
365
+ //#endregion
366
+ //#region src/dev/tui/ink/components/Sidebar.tsx
367
+ const LEVEL_TOKENS = {
368
+ error: "error",
369
+ warn: "warning",
370
+ info: "info",
371
+ debug: "debug",
372
+ trace: "trace"
373
+ };
374
+ const SPINNER = [
375
+ "⠋",
376
+ "⠙",
377
+ "⠹",
378
+ "⠸",
379
+ "⠼",
380
+ "⠴",
381
+ "⠦",
382
+ "⠧",
383
+ "⠇",
384
+ "⠏"
385
+ ];
386
+ const KEYS$1 = [[
387
+ ["←→", "select"],
388
+ ["tab", "group"],
389
+ ["space", "filter"],
390
+ ["s", "solo"]
391
+ ], [
392
+ ["q", "quit"],
393
+ ["r", "restart"],
394
+ ["d", "disconnect"],
395
+ ["c", "clear"],
396
+ ["↑↓", "scroll"],
397
+ ["g/G", "top/bottom"],
398
+ ["?", "help"]
399
+ ]];
400
+ let frameworkVersion = null;
401
+ /** Looked up once per project: it walks `node_modules`, and the sidebar repaints on every log entry. */
402
+ function readFrameworkVersion(root) {
403
+ if (frameworkVersion?.root !== root) frameworkVersion = {
404
+ root,
405
+ version: findInstalledVersion(root, "@wolfstar/http-framework")
406
+ };
407
+ return frameworkVersion.version;
408
+ }
409
+ /**
410
+ * The left column of the dashboard: what the session is doing, the filters of the log stream, and the keys. Under a
411
+ * short terminal the key help goes first, then the filters' own hints: the status and the filters are what it is for.
412
+ */
413
+ function Sidebar(props) {
414
+ const { status, config, counters, filter, channels, focus, collapsed, live, grouped, frame, elapsedMs, width, height, confirmQuit } = props;
415
+ const paint = usePaint();
416
+ const uptime = useUptime(status.startedAt);
417
+ const state = describeBadge(status);
418
+ const busy = isBusy(state.badge);
419
+ const failed = state.badge === "error";
420
+ const name = config.packageJson?.name ?? "stars project";
421
+ const inner = Math.max(1, width - 2);
422
+ const row = (label, value, key = label) => /* @__PURE__ */ jsxs(Text, {
423
+ wrap: "truncate-end",
424
+ children: [/* @__PURE__ */ jsx(Text, {
425
+ dimColor: true,
426
+ children: label.padEnd(8)
427
+ }), value]
428
+ }, key);
429
+ const stateColor = paint(failed ? "error" : busy || state.badge === "stopped" ? "busy" : "success");
430
+ const tunnel = status.tunnel === "up" ? /* @__PURE__ */ jsx(Text, {
431
+ color: paint("tunnel"),
432
+ children: "live"
433
+ }) : status.tunnel === "starting" ? /* @__PURE__ */ jsx(Text, {
434
+ color: paint("busy"),
435
+ children: "connecting"
436
+ }) : status.tunnel === "failed" ? /* @__PURE__ */ jsx(Text, {
437
+ color: paint("error"),
438
+ children: "failed"
439
+ }) : /* @__PURE__ */ jsx(Text, {
440
+ dimColor: true,
441
+ children: "off"
442
+ });
443
+ const port = status.port ?? (status.url ? new URL(status.url).port || null : null);
444
+ const framework = readFrameworkVersion(config.root);
445
+ const head = [
446
+ /* @__PURE__ */ jsxs(Text, {
447
+ wrap: "truncate-end",
448
+ children: [/* @__PURE__ */ jsx(Text, {
449
+ bold: true,
450
+ color: paint("brand"),
451
+ children: name
452
+ }), /* @__PURE__ */ jsx(Text, {
453
+ dimColor: true,
454
+ children: ` • stars v${readOwnPackageJson().version}`
455
+ })]
456
+ }, "title"),
457
+ /* @__PURE__ */ jsx(Text, { children: " " }, "gap-title"),
458
+ /* @__PURE__ */ jsxs(Text, {
459
+ wrap: "truncate-end",
460
+ children: [
461
+ /* @__PURE__ */ jsx(Text, {
462
+ color: stateColor,
463
+ children: busy ? SPINNER[frame % SPINNER.length] : failed ? "✖" : "●"
464
+ }),
465
+ " ",
466
+ /* @__PURE__ */ jsx(Text, {
467
+ bold: true,
468
+ color: stateColor,
469
+ children: confirmQuit ? "quit?" : state.badge === "ready" ? "running" : state.badge
470
+ }),
471
+ busy && /* @__PURE__ */ jsx(Text, {
472
+ dimColor: true,
473
+ children: ` ${Math.round(status.progress.fraction * 100)}% · ${(elapsedMs / 1e3).toFixed(1)}s`
474
+ })
475
+ ]
476
+ }, "state"),
477
+ /* @__PURE__ */ jsx(Text, {
478
+ wrap: "truncate-end",
479
+ children: confirmQuit ? /* @__PURE__ */ jsx(Text, {
480
+ dimColor: true,
481
+ children: "press y to confirm, esc to stay"
482
+ }) : state.badge === "ready" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Text, {
483
+ bold: true,
484
+ children: name
485
+ }), " is ready!"] }) : /* @__PURE__ */ jsx(Text, {
486
+ dimColor: true,
487
+ children: state.note
488
+ })
489
+ }, "note"),
490
+ row("http", framework ? `v${framework}` : /* @__PURE__ */ jsx(Text, {
491
+ dimColor: true,
492
+ children: "not installed"
493
+ })),
494
+ row("up", uptime === null ? /* @__PURE__ */ jsx(Text, {
495
+ dimColor: true,
496
+ children: "—"
497
+ }) : formatUptime(uptime)),
498
+ row("port", port ?? /* @__PURE__ */ jsx(Text, {
499
+ dimColor: true,
500
+ children: "—"
501
+ })),
502
+ row("tunnel", tunnel),
503
+ row("logs", config.dev.logFile ? displayPath(config.root, config.dev.logFile) : /* @__PURE__ */ jsx(Text, {
504
+ dimColor: true,
505
+ children: "off"
506
+ })),
507
+ ...config.dev.typecheck.enabled ? [row("types", status.typecheck === "failed" ? /* @__PURE__ */ jsx(Text, {
508
+ color: paint("error"),
509
+ children: `${status.typeErrors} ${status.typeErrors === 1 ? "error" : "errors"}`
510
+ }) : status.typecheck)] : [],
511
+ ...counters.errors > 0 || counters.warnings > 0 ? [/* @__PURE__ */ jsxs(Text, {
512
+ wrap: "truncate-end",
513
+ children: [counters.errors > 0 && /* @__PURE__ */ jsx(Text, {
514
+ bold: true,
515
+ color: paint("error"),
516
+ children: `✖ ${counters.errors} `
517
+ }), counters.warnings > 0 && /* @__PURE__ */ jsx(Text, {
518
+ color: paint("warning"),
519
+ children: `⚠ ${counters.warnings}`
520
+ })]
521
+ }, "counters")] : []
522
+ ];
523
+ const groupHeader = (group, suffix = "") => /* @__PURE__ */ jsxs(Text, {
524
+ wrap: "truncate-end",
525
+ children: [
526
+ /* @__PURE__ */ jsx(Text, {
527
+ color: focus.group === group ? paint("brand") : void 0,
528
+ children: collapsed[group] ? "▸" : "▾"
529
+ }),
530
+ " ",
531
+ /* @__PURE__ */ jsx(Text, {
532
+ bold: focus.group === group,
533
+ dimColor: focus.group !== group,
534
+ children: group
535
+ }),
536
+ group === "channels" && grouped && /* @__PURE__ */ jsx(Text, {
537
+ dimColor: true,
538
+ children: " · grouped"
539
+ }),
540
+ suffix && /* @__PURE__ */ jsx(Text, {
541
+ dimColor: true,
542
+ children: suffix
543
+ })
544
+ ]
545
+ }, `${group}-header`);
546
+ const item = (group, index, label, on, token) => {
547
+ const focused = focus.group === group && focus.index === index;
548
+ return /* @__PURE__ */ jsx(Text, {
549
+ color: on ? paint(token) : void 0,
550
+ dimColor: !on,
551
+ strikethrough: !on,
552
+ underline: focused,
553
+ bold: focused,
554
+ children: label
555
+ }, label);
556
+ };
557
+ const budget = Math.max(1, height - 1);
558
+ const labelWidth = Math.max(1, inner - 2);
559
+ const lines = (group, labels, nodes, range = [0, Infinity]) => wrapLabels(labels, labelWidth).slice(...range).map((line, index) => /* @__PURE__ */ jsxs(Text, {
560
+ wrap: "truncate-end",
561
+ children: [" ", line.flatMap((label, position) => position > 0 ? [" ", nodes[label]] : [nodes[label]])]
562
+ }, `${group}-${index}`));
563
+ const levelNodes = VIEW_LEVELS.map((level, index) => item("levels", index, level, filter.levels.has(level), LEVEL_TOKENS[level]));
564
+ const levelLines = collapsed.levels ? [] : lines("levels", VIEW_LEVELS, levelNodes);
565
+ const channelNodes = channels.map((channel, index) => item("channels", index, channel, isChannelVisible(filter, channel), channelToken(channel)));
566
+ const channelLayout = wrapLabels(channels, labelWidth);
567
+ const focusedLine = focus.group === "channels" ? Math.max(0, channelLayout.findIndex((line) => line.includes(focus.index))) : 0;
568
+ const range = windowAround(channelLayout.length, focusedLine, Math.max(1, budget - head.length - 3 - levelLines.length));
569
+ const hiddenChannels = channelLayout.length - (range[1] - range[0]);
570
+ const filters = [
571
+ /* @__PURE__ */ jsx(Text, { children: " " }, "gap-filters"),
572
+ groupHeader("channels", !collapsed.channels && hiddenChannels > 0 ? ` · ${range[0] + 1}-${range[1]}/${channelLayout.length}` : ""),
573
+ ...collapsed.channels ? [] : lines("channels", channels, channelNodes, range),
574
+ groupHeader("levels"),
575
+ ...levelLines
576
+ ];
577
+ const keys = KEYS$1.map((section, sectionIndex) => [/* @__PURE__ */ jsx(Text, { children: " " }, `gap-keys-${sectionIndex}`), ...section.map(([key, label]) => /* @__PURE__ */ jsxs(Text, {
578
+ wrap: "truncate-end",
579
+ children: [
580
+ /* @__PURE__ */ jsx(Text, {
581
+ bold: true,
582
+ color: paint("url"),
583
+ children: key
584
+ }),
585
+ " ",
586
+ /* @__PURE__ */ jsx(Text, {
587
+ dimColor: true,
588
+ children: label
589
+ })
590
+ ]
591
+ }, key))]);
592
+ let body = [
593
+ ...head,
594
+ ...filters,
595
+ ...keys.flat()
596
+ ];
597
+ if (body.length > budget) body = [
598
+ ...head,
599
+ ...filters,
600
+ ...keys[1]
601
+ ];
602
+ if (body.length > budget) body = [...head, ...filters];
603
+ body = body.slice(0, budget);
604
+ return /* @__PURE__ */ jsxs(Box, {
605
+ flexDirection: "column",
606
+ width,
607
+ height,
608
+ paddingX: 1,
609
+ children: [
610
+ body.map((node, index) => /* @__PURE__ */ jsx(Box, {
611
+ height: 1,
612
+ width: inner,
613
+ children: node
614
+ }, index)),
615
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
616
+ /* @__PURE__ */ jsx(Box, {
617
+ height: 1,
618
+ width: inner,
619
+ justifyContent: "flex-end",
620
+ children: /* @__PURE__ */ jsxs(Text, {
621
+ wrap: "truncate-end",
622
+ children: [
623
+ /* @__PURE__ */ jsx(Text, {
624
+ color: paint(live ? "error" : "busy"),
625
+ children: "●"
626
+ }),
627
+ " ",
628
+ /* @__PURE__ */ jsx(Text, {
629
+ dimColor: true,
630
+ children: live ? "live" : "paused"
631
+ })
632
+ ]
633
+ })
634
+ })
635
+ ]
636
+ });
637
+ }
638
+
639
+ //#endregion
640
+ //#region src/dev/tui/ink/components/Dashboard.tsx
641
+ /** The sidebar takes about a quarter of the terminal, within what its longest lines need and can spare. */
642
+ function sidebarWidth(columns) {
643
+ return Math.max(24, Math.min(34, Math.floor(columns * .26)));
644
+ }
645
+ /**
646
+ * The full-screen dev view: the session's state and the log filters on the left, the log stream on the right, and a
647
+ * card under the stream when `stars dev` has something to ask. Everything it shows comes from the service; the keys
648
+ * here only change what is looked at.
649
+ */
650
+ function Dashboard(props) {
651
+ const { service, status, counters, frame, elapsedMs, width, height, confirmQuit, initialFilter, active, onCapture } = props;
652
+ const paint = usePaint();
653
+ const [filter, setFilter] = useState(initialFilter);
654
+ const [focus, setFocus] = useState({
655
+ group: "channels",
656
+ index: 0
657
+ });
658
+ const [collapsed, setCollapsed] = useState({
659
+ channels: false,
660
+ levels: false
661
+ });
662
+ const [grouped, setGrouped] = useState(false);
663
+ const [searching, setSearching] = useState(false);
664
+ const [pinned, setPinned] = useState(null);
665
+ const [marked, setMarked] = useState(null);
666
+ const version = useLogVersion(service.logs);
667
+ const channels = useMemo(() => [.../* @__PURE__ */ new Set([...initialFilter.allow ?? [], ...service.logs.channels()])].sort(), [
668
+ service,
669
+ initialFilter,
670
+ version
671
+ ]);
672
+ const rows = useMemo(() => buildRows(service.logs.entries().filter((entry) => matchesView(entry, filter)), { group: grouped }), [
673
+ service,
674
+ filter,
675
+ grouped,
676
+ version
677
+ ]);
678
+ useEffect(() => {
679
+ onCapture(searching);
680
+ return () => onCapture(false);
681
+ }, [searching, onCapture]);
682
+ const side = sidebarWidth(width);
683
+ const mainWidth = Math.max(1, width - side - 1);
684
+ const searchRows = searching || filter.query ? 1 : 0;
685
+ const cardRows = status.prompt ? promptHeight(status.prompt, Math.max(4, height - searchRows - 3)) : 0;
686
+ const paneHeight = Math.max(1, height - searchRows - cardRows);
687
+ const end = pinnedEnd(rows, pinned, paneHeight);
688
+ const channelWidth = Math.min(12, Math.max(3, ...channels.map((channel) => channel.length)));
689
+ const items = focus.group === "channels" ? channels : VIEW_LEVELS;
690
+ const change = (next) => {
691
+ setFilter(next);
692
+ setPinned(null);
693
+ };
694
+ const scroll = (delta) => {
695
+ setPinned(pinAt(rows, Math.max(Math.min(paneHeight, rows.length), end + delta)));
696
+ };
697
+ useInput((input, key) => {
698
+ if (key.ctrl || confirmQuit) return;
699
+ if (searching) {
700
+ if (key.escape) {
701
+ setSearching(false);
702
+ change({
703
+ ...filter,
704
+ query: ""
705
+ });
706
+ } else if (key.return) setSearching(false);
707
+ else if (key.backspace || key.delete) change({
708
+ ...filter,
709
+ query: [...filter.query].slice(0, -1).join("")
710
+ });
711
+ else if (input && !key.upArrow && !key.downArrow && !key.leftArrow && !key.rightArrow && !key.tab) change({
712
+ ...filter,
713
+ query: filter.query + input
714
+ });
715
+ return;
716
+ }
717
+ if (key.escape) {
718
+ if (filter.query) change({
719
+ ...filter,
720
+ query: ""
721
+ });
722
+ else setPinned(null);
723
+ return setMarked(null);
724
+ }
725
+ if (input === "/") return setSearching(true);
726
+ if (key.tab) return setFocus((current) => ({
727
+ group: current.group === "channels" ? "levels" : "channels",
728
+ index: 0
729
+ }));
730
+ if (key.leftArrow || key.rightArrow) {
731
+ if (items.length === 0) return;
732
+ const delta = key.leftArrow ? -1 : 1;
733
+ return setFocus((current) => ({
734
+ ...current,
735
+ index: (current.index + delta + items.length) % items.length
736
+ }));
737
+ }
738
+ if (key.return) return setCollapsed((current) => ({
739
+ ...current,
740
+ [focus.group]: !current[focus.group]
741
+ }));
742
+ if (input === " " || input === "s") {
743
+ const target = items[Math.min(focus.index, items.length - 1)];
744
+ if (target === void 0) return;
745
+ if (focus.group === "channels") return change(input === " " ? toggleChannel(filter, target) : soloChannel(filter, target));
746
+ const level = target;
747
+ return change(input === " " ? toggleLevel(filter, level) : soloLevel(filter, level));
748
+ }
749
+ if (input === "a") return change({
750
+ ...filter,
751
+ allow: null,
752
+ hidden: /* @__PURE__ */ new Set(),
753
+ levels: new Set(VIEW_LEVELS),
754
+ query: ""
755
+ });
756
+ if (input === "b") {
757
+ setGrouped((current) => !current);
758
+ return setPinned(null);
759
+ }
760
+ if (key.upArrow || input === "k") return scroll(-1);
761
+ if (key.downArrow || input === "j") return scroll(1);
762
+ if (key.pageUp) return scroll(-Math.max(1, paneHeight - 1));
763
+ if (key.pageDown) return scroll(Math.max(1, paneHeight - 1));
764
+ if (key.home || input === "g") return setPinned(pinAt(rows, Math.min(paneHeight, rows.length)));
765
+ if (key.end || input === "G") return setPinned(null);
766
+ if (input === "e") {
767
+ const index = newestErrorRow(rows);
768
+ if (index < 0) return;
769
+ const row = rows[index];
770
+ setMarked(row.kind === "entry" ? row.entry.id : null);
771
+ setPinned(pinAt(rows, Math.max(paneHeight, index + Math.ceil(paneHeight / 2))));
772
+ }
773
+ }, { isActive: active });
774
+ return /* @__PURE__ */ jsxs(Box, {
775
+ width,
776
+ height,
777
+ children: [/* @__PURE__ */ jsx(Sidebar, {
778
+ status,
779
+ config: service.config,
780
+ counters,
781
+ filter,
782
+ channels,
783
+ focus,
784
+ collapsed,
785
+ live: pinned === null,
786
+ grouped,
787
+ frame,
788
+ elapsedMs,
789
+ width: side,
790
+ height,
791
+ confirmQuit
792
+ }), /* @__PURE__ */ jsxs(Box, {
793
+ flexDirection: "column",
794
+ width: mainWidth + 1,
795
+ height,
796
+ borderStyle: "single",
797
+ borderTop: false,
798
+ borderRight: false,
799
+ borderBottom: false,
800
+ borderDimColor: true,
801
+ children: [
802
+ /* @__PURE__ */ jsx(LogPane, {
803
+ rows,
804
+ end,
805
+ width: mainWidth,
806
+ height: paneHeight,
807
+ channelWidth,
808
+ marked
809
+ }),
810
+ status.prompt && /* @__PURE__ */ jsx(PromptCard, {
811
+ prompt: status.prompt,
812
+ width: mainWidth,
813
+ height: cardRows
814
+ }),
815
+ searchRows > 0 && /* @__PURE__ */ jsxs(Text, {
816
+ wrap: "truncate-end",
817
+ children: [
818
+ /* @__PURE__ */ jsx(Text, {
819
+ bold: true,
820
+ color: paint("url"),
821
+ children: " / "
822
+ }),
823
+ filter.query,
824
+ searching ? "▏" : /* @__PURE__ */ jsx(Text, {
825
+ dimColor: true,
826
+ children: " · esc clears"
827
+ })
828
+ ]
829
+ })
830
+ ]
831
+ })]
832
+ });
833
+ }
834
+
40
835
  //#endregion
41
836
  //#region src/dev/tui/ink/components/HelpOverlay.tsx
42
837
  const KEYS = [
43
838
  ["r", "restart the bot now"],
839
+ ["d", "disconnect: stop the bot until the next r"],
44
840
  ["o", "open the local URL in a browser"],
45
841
  ["t", "toggle the public tunnel"],
46
842
  ["i", "show versions, URLs and session info"],
47
843
  ["T", "pick a colour theme (dark, light, colour-blind friendly, ANSI)"],
48
- ["l", "open the logs (scroll, filter by source or level)"],
844
+ ["v", "switch between the dashboard and the compact panel"],
845
+ ["l", "open the logs (select, copy, filter by source or level)"],
49
846
  ["e", "jump to the last error, keeping its context"],
847
+ ["y / n", "answer a prompt (refresh the commands)"],
50
848
  ["c / Ctrl+L", "clear the log history"],
51
849
  ["q / Ctrl+C", "stop the bot and quit"],
52
- ["?", "toggle this help"]
850
+ ["?", "toggle this help"],
851
+ ["", ""],
852
+ ["dashboard", ""],
853
+ ["← / →", "select a channel or a level"],
854
+ ["tab", "switch between channels and levels"],
855
+ ["space", "show or hide the selected one"],
856
+ ["s", "solo: only the selected one, again for all"],
857
+ ["a", "show every channel and level"],
858
+ ["enter", "fold or unfold the selected group"],
859
+ ["b", "group the stream by channel"],
860
+ ["↑ / ↓", "scroll (PgUp/PgDn by page)"],
861
+ ["g / G", "top / back to live"],
862
+ ["/", "search (Esc clears)"]
53
863
  ];
54
864
  /** The static keys reference, opened with `?` from the pinned panel. */
55
865
  function HelpOverlay({ height, width, onClose }) {
@@ -73,7 +883,7 @@ function HelpOverlay({ height, width, onClose }) {
73
883
  dimColor: true,
74
884
  children: "─".repeat(width)
75
885
  }),
76
- KEYS.slice(0, Math.max(0, height - 3)).map(([key, description]) => /* @__PURE__ */ jsxs(Text, {
886
+ KEYS.slice(0, Math.max(0, height - 3)).map(([key, description], index) => /* @__PURE__ */ jsxs(Text, {
77
887
  wrap: "truncate-end",
78
888
  children: [
79
889
  " ",
@@ -87,7 +897,7 @@ function HelpOverlay({ height, width, onClose }) {
87
897
  children: description
88
898
  })
89
899
  ]
90
- }, key)),
900
+ }, index)),
91
901
  /* @__PURE__ */ jsx(Box, { flexGrow: 1 }),
92
902
  /* @__PURE__ */ jsx(Text, {
93
903
  dimColor: true,
@@ -97,24 +907,6 @@ function HelpOverlay({ height, width, onClose }) {
97
907
  });
98
908
  }
99
909
 
100
- //#endregion
101
- //#region src/dev/tui/ink/hooks/useLogVersion.ts
102
- /** Bumps whenever the buffer changes, so a consumer can re-derive a filtered view without re-filtering on every render. */
103
- function useLogVersion(logs) {
104
- const [version, setVersion] = useState(0);
105
- useEffect(() => {
106
- const bump = () => setVersion((current) => current + 1);
107
- logs.on("entry", bump);
108
- logs.on("clear", bump);
109
- bump();
110
- return () => {
111
- logs.off("entry", bump);
112
- logs.off("clear", bump);
113
- };
114
- }, [logs]);
115
- return version;
116
- }
117
-
118
910
  //#endregion
119
911
  //#region src/dev/tui/ink/components/Hints.tsx
120
912
  const PANEL_HINTS = [
@@ -350,84 +1142,6 @@ function useDevStatus(service) {
350
1142
  return status;
351
1143
  }
352
1144
 
353
- //#endregion
354
- //#region src/dev/tui/ink/hooks/useUptime.ts
355
- /**
356
- * Re-renders once a second while the bot is up, so the uptime in the header keeps counting.
357
- */
358
- function useUptime(startedAt) {
359
- const [now, setNow] = useState(() => Date.now());
360
- useEffect(() => {
361
- if (startedAt === null) return;
362
- setNow(Date.now());
363
- const timer = setInterval(() => setNow(Date.now()), 1e3);
364
- return () => clearInterval(timer);
365
- }, [startedAt]);
366
- return startedAt === null ? null : now - startedAt;
367
- }
368
-
369
- //#endregion
370
- //#region src/dev/tui/panel-logic.ts
371
- /**
372
- * The badge and standing description for the panel's status row, mirroring the states Nuxt CLI v4's own dev panel
373
- * cycles through (`STARTING`/`BUILDING`/`RESTART`/`READY`/`ERROR`).
374
- */
375
- function describeBadge(status) {
376
- if (status.build === "building") return {
377
- badge: status.startedAt === null ? "starting" : "building",
378
- note: status.progress.message
379
- };
380
- if (status.process === "starting") return {
381
- badge: "starting",
382
- note: "starting the bot"
383
- };
384
- if (status.process === "stopping") return {
385
- badge: "restarting",
386
- note: "restarting the bot"
387
- };
388
- if (status.build === "failed") return {
389
- badge: "error",
390
- note: "build failed, waiting for changes"
391
- };
392
- if (status.process === "crashed") return {
393
- badge: "error",
394
- note: "the bot crashed, press r to restart"
395
- };
396
- if (status.health === "down") return {
397
- badge: "error",
398
- note: "health check failed"
399
- };
400
- if (status.typecheck === "failed") return {
401
- badge: "error",
402
- note: "type check failed, press e to view it"
403
- };
404
- if (status.process === "stopped") return {
405
- badge: "error",
406
- note: "the bot stopped, press r to restart"
407
- };
408
- if (status.progress.fraction < 1) return {
409
- badge: "starting",
410
- note: status.progress.message
411
- };
412
- if (status.process === "running") return {
413
- badge: "ready",
414
- note: "watching for changes"
415
- };
416
- return {
417
- badge: "starting",
418
- note: "waiting for the first build"
419
- };
420
- }
421
- /** Formats a duration the way a dev session reads it: `mm:ss` under an hour, `h:mm:ss` above. */
422
- function formatDuration(milliseconds) {
423
- const total = Math.max(0, Math.floor(milliseconds / 1e3));
424
- const seconds = total % 60;
425
- const minutes = Math.floor(total / 60) % 60;
426
- const hours = Math.floor(total / 3600);
427
- const pad = (value) => value.toString().padStart(2, "0");
428
- return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
429
- }
430
-
431
1145
  //#endregion
432
1146
  //#region src/dev/tui/ink/components/InfoOverlay.tsx
433
1147
  function InfoOverlay({ service, height, width, onClose }) {
@@ -492,9 +1206,9 @@ function InfoOverlay({ service, height, width, onClose }) {
492
1206
  function Panel({ status, config, counters, frame, elapsedMs, width, height, confirmQuit }) {
493
1207
  const paint = usePaint();
494
1208
  const state = describeBadge(status);
495
- const busy = state.badge !== "ready" && state.badge !== "error";
1209
+ const busy = isBusy(state.badge);
496
1210
  const badge = !busy && counters.errors > 0 ? "error" : state.badge;
497
- const note = badge === "error" && state.badge === "ready" ? "an error was logged · press e to view it" : state.note;
1211
+ const note = status.prompt ? "commands updated · press y to refresh them, n to skip" : badge === "error" && state.badge === "ready" ? "an error was logged · press e to view it" : state.note;
498
1212
  const filled = Math.round(Math.min(1, Math.max(0, status.progress.fraction)) * 20);
499
1213
  let shown = [
500
1214
  ...(config.dev.banner === false ? [] : config.dev.banner?.flatMap((line) => line.split("\n")) ?? [null]).slice(0, 4).map((line, index) => ({
@@ -636,7 +1350,7 @@ function Panel({ status, config, counters, frame, elapsedMs, width, height, conf
636
1350
  " ",
637
1351
  /* @__PURE__ */ jsx(Text, {
638
1352
  bold: true,
639
- backgroundColor: paint(confirmQuit || busy ? "busy" : badge === "error" ? "error" : "success"),
1353
+ backgroundColor: paint(confirmQuit || busy || badge === "stopped" ? "busy" : badge === "error" ? "error" : "success"),
640
1354
  color: paint(badge === "error" && !confirmQuit ? "onError" : "onBright"),
641
1355
  children: ` ${confirmQuit ? "QUIT?" : badge === "restarting" ? "RESTART" : badge.toUpperCase()} `
642
1356
  }),
@@ -855,44 +1569,79 @@ function ThemeOverlay({ current, height, width, onPreview, onSelect, onClose })
855
1569
 
856
1570
  //#endregion
857
1571
  //#region src/dev/tui/ink/DevApp.tsx
1572
+ /** The dashboard needs room for its sidebar next to a readable log line, and for more than a handful of entries. */
1573
+ const DASHBOARD_MIN_COLUMNS = 90;
1574
+ const DASHBOARD_MIN_ROWS = 20;
1575
+ /** Below this even an explicit `dashboard` falls back to the panel: the sidebar alone would fill the terminal. */
1576
+ const DASHBOARD_HARD_MIN_COLUMNS = 60;
1577
+ const DASHBOARD_HARD_MIN_ROWS = 10;
1578
+ /** The layout to paint for a terminal size: what was asked for when it fits, the panel otherwise. */
1579
+ function resolveLayout(layout, columns, rows) {
1580
+ if (layout === "panel") return "panel";
1581
+ if (columns < DASHBOARD_HARD_MIN_COLUMNS || rows < DASHBOARD_HARD_MIN_ROWS) return "panel";
1582
+ if (layout === "dashboard") return "dashboard";
1583
+ return columns >= 90 && rows >= 20 ? "dashboard" : "panel";
1584
+ }
858
1585
  /**
859
- * A bottom-aligned panel in the normal buffer, with logs folded away. Only the browsable overlays use the
860
- * alternate screen, preserving the terminal's history and the panel when they close.
1586
+ * Two layouts over one service. The dashboard is a full-screen view in the alternate buffer; the panel is pinned to
1587
+ * the bottom of the normal buffer with the logs folded away, and is what a small terminal gets. The browsable
1588
+ * overlays use the alternate buffer from either, preserving the terminal's history and the panel when they close.
861
1589
  */
862
- function DevApp({ service, color, theme, reducedMotion, onQuit, onViewChange, onCopy, onThemeSave }) {
1590
+ function DevApp({ service, color, theme, reducedMotion, layout = "auto", filter, onQuit, onViewChange, onCopy, onThemeSave }) {
863
1591
  const { columns, rows } = useWindowSize();
864
- const [view, setView] = useState("panel");
1592
+ const [overlay, setOverlay] = useState(null);
1593
+ const [chosen, setChosen] = useState(layout);
865
1594
  const [confirmQuit, setConfirmQuit] = useState(false);
1595
+ const [captured, setCaptured] = useState(false);
1596
+ const [, repaint] = useState(0);
866
1597
  const [saved, setSaved] = useState(theme);
867
1598
  const [preview, setPreview] = useState(theme);
1599
+ const [initialFilter] = useState(() => filter ?? initialLogView(service.config));
868
1600
  const status = useDevStatus(service);
869
1601
  const counters = useLogCounters(service);
870
1602
  const badge = describeBadge(status).badge;
871
- const busy = badge !== "ready" && badge !== "error";
872
- const clock = useBuildClock(status.progress.startedAt, busy && view === "panel", reducedMotion);
1603
+ const busy = isBusy(badge);
1604
+ const base = resolveLayout(chosen, columns, rows);
1605
+ const clock = useBuildClock(status.progress.startedAt, busy && overlay === null, reducedMotion);
873
1606
  const width = Math.max(1, columns);
874
1607
  const height = Math.max(2, rows - 1);
875
- const changeView = (next) => {
876
- onViewChange(next !== "panel");
877
- setView(next);
1608
+ const fullScreen = overlay !== null || base === "dashboard";
1609
+ const shown = useRef(fullScreen);
1610
+ const show = (nextOverlay, nextChosen = chosen) => {
1611
+ const next = nextOverlay !== null || resolveLayout(nextChosen, columns, rows) === "dashboard";
1612
+ shown.current = next;
1613
+ onViewChange(next);
1614
+ setOverlay(nextOverlay);
1615
+ setChosen(nextChosen);
878
1616
  };
1617
+ useEffect(() => {
1618
+ if (shown.current === fullScreen) return;
1619
+ shown.current = fullScreen;
1620
+ onViewChange(fullScreen);
1621
+ repaint((count) => count + 1);
1622
+ }, [fullScreen, onViewChange]);
1623
+ const onCapture = useCallback((capturing) => setCaptured(capturing), []);
879
1624
  useInput((input, key) => {
880
1625
  if (key.ctrl && input === "c") return onQuit();
881
1626
  if (key.ctrl && input === "l") return service.clearLogs();
882
- if (view !== "panel") return;
1627
+ if (overlay !== null || captured) return;
883
1628
  if (confirmQuit) {
884
1629
  if (input === "y") return onQuit();
885
1630
  return setConfirmQuit(false);
886
1631
  }
887
1632
  if (key.ctrl && input === "r") return void service.restart("manual");
888
1633
  if (key.ctrl && input === "d") return busy ? setConfirmQuit(true) : onQuit();
1634
+ if (status.prompt && (input === "y" || input === "n")) return service.answerPrompt(input === "y");
889
1635
  switch (input) {
890
1636
  case "q": return busy ? setConfirmQuit(true) : onQuit();
891
1637
  case "r":
892
1638
  service.restart("manual");
893
1639
  return;
894
- case "l": return changeView("logs");
895
- case "e": return changeView("errors");
1640
+ case "d":
1641
+ service.disconnect();
1642
+ return;
1643
+ case "l": return show("logs");
1644
+ case "e": return base === "panel" ? show("errors") : void 0;
896
1645
  case "c": return service.clearLogs();
897
1646
  case "o":
898
1647
  openDevUrl(status.url).catch((error) => service.log("stars", "warn", error.message));
@@ -900,10 +1649,11 @@ function DevApp({ service, color, theme, reducedMotion, onQuit, onViewChange, on
900
1649
  case "t":
901
1650
  service.toggleTunnel();
902
1651
  return;
903
- case "i": return changeView("info");
904
- case "T": return changeView("theme");
1652
+ case "i": return show("info");
1653
+ case "T": return show("theme");
1654
+ case "v": return show(null, base === "dashboard" ? "panel" : "dashboard");
905
1655
  case "h":
906
- case "?": return changeView("help");
1656
+ case "?": return show("help");
907
1657
  }
908
1658
  });
909
1659
  return /* @__PURE__ */ jsx(ThemeProvider, {
@@ -913,28 +1663,28 @@ function DevApp({ service, color, theme, reducedMotion, onQuit, onViewChange, on
913
1663
  width,
914
1664
  height,
915
1665
  flexDirection: "column",
916
- justifyContent: view === "panel" ? "flex-end" : "flex-start",
1666
+ justifyContent: fullScreen ? "flex-start" : "flex-end",
917
1667
  children: [
918
- (view === "logs" || view === "errors") && /* @__PURE__ */ jsx(LogBrowser, {
1668
+ (overlay === "logs" || overlay === "errors") && /* @__PURE__ */ jsx(LogBrowser, {
919
1669
  service,
920
1670
  height,
921
1671
  width,
922
- lastError: view === "errors",
1672
+ lastError: overlay === "errors",
923
1673
  onCopy,
924
- onClose: () => changeView("panel")
1674
+ onClose: () => show(null)
925
1675
  }),
926
- view === "help" && /* @__PURE__ */ jsx(HelpOverlay, {
1676
+ overlay === "help" && /* @__PURE__ */ jsx(HelpOverlay, {
927
1677
  height,
928
1678
  width,
929
- onClose: () => changeView("panel")
1679
+ onClose: () => show(null)
930
1680
  }),
931
- view === "info" && /* @__PURE__ */ jsx(InfoOverlay, {
1681
+ overlay === "info" && /* @__PURE__ */ jsx(InfoOverlay, {
932
1682
  service,
933
1683
  height,
934
1684
  width,
935
- onClose: () => changeView("panel")
1685
+ onClose: () => show(null)
936
1686
  }),
937
- view === "theme" && /* @__PURE__ */ jsx(ThemeOverlay, {
1687
+ overlay === "theme" && /* @__PURE__ */ jsx(ThemeOverlay, {
938
1688
  current: saved,
939
1689
  height,
940
1690
  width,
@@ -943,14 +1693,29 @@ function DevApp({ service, color, theme, reducedMotion, onQuit, onViewChange, on
943
1693
  setSaved(setting);
944
1694
  setPreview(setting);
945
1695
  onThemeSave(setting);
946
- changeView("panel");
1696
+ show(null);
947
1697
  },
948
1698
  onClose: () => {
949
1699
  setPreview(saved);
950
- changeView("panel");
1700
+ show(null);
951
1701
  }
952
1702
  }),
953
- view === "panel" && /* @__PURE__ */ jsx(Panel, {
1703
+ base === "dashboard" && /* @__PURE__ */ jsx(Box, {
1704
+ display: overlay === null ? "flex" : "none",
1705
+ children: /* @__PURE__ */ jsx(Dashboard, {
1706
+ service,
1707
+ status,
1708
+ counters,
1709
+ ...clock,
1710
+ width,
1711
+ height,
1712
+ confirmQuit,
1713
+ initialFilter,
1714
+ active: overlay === null,
1715
+ onCapture
1716
+ })
1717
+ }),
1718
+ base === "panel" && overlay === null && /* @__PURE__ */ jsx(Panel, {
954
1719
  status,
955
1720
  config: service.config,
956
1721
  counters,
@@ -998,8 +1763,9 @@ function captureOutput(service, stream, fallback) {
998
1763
  //#endregion
999
1764
  //#region src/dev/tui/tui.tsx
1000
1765
  /**
1001
- * A normal-buffer, bottom-aligned panel and alternate-buffer overlays. External output is captured for the log
1002
- * browser; the renderer alone writes to the terminal. Switching views and teardown restore the original buffer.
1766
+ * The full-screen dashboard and the overlays in the alternate buffer, the bottom-aligned panel in the normal one.
1767
+ * External output is captured for the log views; the renderer alone writes to the terminal. Switching views and
1768
+ * teardown restore the original buffer.
1003
1769
  *
1004
1770
  * `start()` resolves when the user quits, which is what `stars dev` waits on before shutting the bot down.
1005
1771
  */
@@ -1013,14 +1779,17 @@ function createTuiRenderer(service, options = {}) {
1013
1779
  return typeof value === "function" ? value.bind(target) : value;
1014
1780
  } });
1015
1781
  const restoreOutput = stdout === process.stdout ? [captureOutput(service, process.stdout, "info"), captureOutput(service, process.stderr, "warn")] : [];
1016
- let alternate = false;
1782
+ const layout = options.layout ?? service.config.dev.layout ?? "auto";
1783
+ let alternate = resolveLayout(layout, stdout.columns ?? 80, stdout.rows ?? 24) === "dashboard";
1784
+ if (alternate) write("\x1B[?1049h\x1B[H");
1017
1785
  let stopped = false;
1018
- const switchView = (overlay) => {
1019
- if (overlay === alternate || stopped) return;
1786
+ const switchView = (fullScreen) => {
1787
+ if (fullScreen === alternate || stopped) return;
1020
1788
  instance.clear();
1021
- write(overlay ? "\x1B[?1049h\x1B[H" : "\x1B[?1049l");
1022
- alternate = overlay;
1789
+ write(fullScreen ? "\x1B[?1049h\x1B[H" : "\x1B[?1049l");
1790
+ alternate = fullScreen;
1023
1791
  };
1792
+ service.promptable = true;
1024
1793
  let quit;
1025
1794
  const quitting = new Promise((resolve) => {
1026
1795
  quit = resolve;
@@ -1031,6 +1800,8 @@ function createTuiRenderer(service, options = {}) {
1031
1800
  theme: options.theme ?? "auto",
1032
1801
  onThemeSave: options.onThemeSave ?? (() => {}),
1033
1802
  reducedMotion: options.reducedMotion ?? false,
1803
+ layout,
1804
+ filter: options.filter,
1034
1805
  onQuit: quit,
1035
1806
  onViewChange: switchView,
1036
1807
  onCopy: (text) => write(`\u001B]52;c;${Buffer.from(text.slice(0, 65536)).toString("base64")}\u0007`)
@@ -1049,6 +1820,7 @@ function createTuiRenderer(service, options = {}) {
1049
1820
  stop() {
1050
1821
  if (stopped) return;
1051
1822
  stopped = true;
1823
+ service.promptable = false;
1052
1824
  if (alternate) instance.clear();
1053
1825
  instance.unmount();
1054
1826
  if (alternate) write("\x1B[?1049l");
@@ -1065,4 +1837,4 @@ function createTuiRenderer(service, options = {}) {
1065
1837
 
1066
1838
  //#endregion
1067
1839
  export { createTuiRenderer };
1068
- //# sourceMappingURL=tui-BRddrLQP.js.map
1840
+ //# sourceMappingURL=tui-nldGSD4c.js.map