@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.
- package/README.md +45 -15
- package/dist/{build-CvSvbsYN.js → build-DbGs9-s8.js} +4 -4
- package/dist/{build-CvSvbsYN.js.map → build-DbGs9-s8.js.map} +1 -1
- package/dist/{build-B6xw00Ee.js → build-DkMulM4f.js} +2 -2
- package/dist/{build-B6xw00Ee.js.map → build-DkMulM4f.js.map} +1 -1
- package/dist/{builders-D97Uqjwg.js → builders-NkAoPRdw.js} +2 -2
- package/dist/{builders-D97Uqjwg.js.map → builders-NkAoPRdw.js.map} +1 -1
- package/dist/cli.js +6 -6
- package/dist/{codegen-mYkL-GBf.js → codegen-1kW_OnrL.js} +2 -2
- package/dist/{codegen-mYkL-GBf.js.map → codegen-1kW_OnrL.js.map} +1 -1
- package/dist/{codegen-DjPOn7cp.js → codegen-DHQH83M3.js} +2 -2
- package/dist/{codegen-DjPOn7cp.js.map → codegen-DHQH83M3.js.map} +1 -1
- package/dist/{commands-Dw2LFWV-.js → commands-BkZNThm8.js} +2 -2
- package/dist/{commands-Dw2LFWV-.js.map → commands-BkZNThm8.js.map} +1 -1
- package/dist/{commands-BkGKs_Bg.js → commands-DwqViJ2V.js} +4 -4
- package/dist/{commands-BkGKs_Bg.js.map → commands-DwqViJ2V.js.map} +1 -1
- package/dist/{dev-BCgaVd0m.js → dev-BazJrPZ3.js} +2 -2
- package/dist/{dev-BCgaVd0m.js.map → dev-BazJrPZ3.js.map} +1 -1
- package/dist/{dev-CWA_JVTb.js → dev-C6HogNjR.js} +80 -65
- package/dist/dev-C6HogNjR.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{info-Czh4LUsD.js → info-BrxKCcUW.js} +2 -2
- package/dist/{info-Czh4LUsD.js.map → info-BrxKCcUW.js.map} +1 -1
- package/dist/{info-BqxpktLG.js → info-rYRLITnv.js} +2 -2
- package/dist/{info-BqxpktLG.js.map → info-rYRLITnv.js.map} +1 -1
- package/dist/log-buffer-D-nj2ZEU.js +72 -0
- package/dist/log-buffer-D-nj2ZEU.js.map +1 -0
- package/dist/{output-mode-ChrOf0LO.js → output-mode-Cxes6YUW.js} +4 -2
- package/dist/output-mode-Cxes6YUW.js.map +1 -0
- package/dist/{prepare-BXcm2Ukp.js → prepare-90XQCoak.js} +1 -1
- package/dist/{prepare-D5NZ0w4t.js → prepare-CHLrneYU.js} +2 -2
- package/dist/{prepare-D5NZ0w4t.js.map → prepare-CHLrneYU.js.map} +1 -1
- package/dist/{prepare-CVf88_Or.js → prepare-hOwjyelr.js} +2 -2
- package/dist/{prepare-CVf88_Or.js.map → prepare-hOwjyelr.js.map} +1 -1
- package/dist/{tsdown-BEFq2API.js → tsdown-Do8vjnIK.js} +23 -6
- package/dist/tsdown-Do8vjnIK.js.map +1 -0
- package/dist/tui-C2P91TgW.js +938 -0
- package/dist/tui-C2P91TgW.js.map +1 -0
- package/package.json +7 -6
- package/dist/dev-CWA_JVTb.js.map +0 -1
- package/dist/output-mode-ChrOf0LO.js.map +0 -1
- package/dist/tsdown-BEFq2API.js.map +0 -1
- package/dist/tui-BTRFsLQR.js +0 -770
- 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
|