hlidskjalf 0.0.4 → 0.0.6
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 +3 -3
- package/dist/index.js +11 -497
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# hlidskjalf
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A Terminal User Interface for visualizing Turborepo tasks, built with [Ink](https://npm.im/ink).
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
@@ -24,5 +24,5 @@ pnpm dev
|
|
|
24
24
|
|
|
25
25
|
| Option | Description |
|
|
26
26
|
| --- | --- |
|
|
27
|
-
|
|
|
28
|
-
|
|
|
27
|
+
| `filter` | Include specific workspaces (`--filter=web`). Repeatable. Append `...` for transitive deps. |
|
|
28
|
+
| `order` | Sort by `alphabetical` (default) or `run` (`--order=run`) dependency order. |
|
package/dist/index.js
CHANGED
|
@@ -1,498 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import { EventEmitter } from "events";
|
|
13
|
-
|
|
14
|
-
// src/parser.ts
|
|
15
|
-
var DTS = /\bDTS\b/;
|
|
16
|
-
var matchers = [
|
|
17
|
-
{ pattern: /running on (https?:\/\/\S+)/, status: "ready" },
|
|
18
|
-
{ pattern: /listening on (https?:\/\/\S+)/, status: "ready" },
|
|
19
|
-
{ pattern: /started.*(https?:\/\/localhost:\d+)/, status: "ready" },
|
|
20
|
-
{ pattern: /\bVITE\b.*\bready in\b/i, status: "ready" },
|
|
21
|
-
{ pattern: /\bLocal:\s+(https?:\/\/\S+)/, status: "ready" },
|
|
22
|
-
// ⚡ may include U+FE0F variation selector
|
|
23
|
-
{ pattern: /⚡\uFE0F?\s*Build success/, status: "watching" },
|
|
24
|
-
{ pattern: /Build start/, status: "building" },
|
|
25
|
-
{ pattern: /Watching for changes/, status: "watching" },
|
|
26
|
-
{ pattern: /[Ee]rror[\s:]/, status: "error" },
|
|
27
|
-
{ pattern: /process exit/, status: "error" }
|
|
28
|
-
];
|
|
29
|
-
function parseLine(line) {
|
|
30
|
-
if (DTS.test(line)) return {};
|
|
31
|
-
for (const { pattern, status } of matchers) {
|
|
32
|
-
const match = line.match(pattern);
|
|
33
|
-
if (match) return { status, url: match[1] };
|
|
34
|
-
}
|
|
35
|
-
return {};
|
|
36
|
-
}
|
|
37
|
-
function stripAnsi(text) {
|
|
38
|
-
return text.replace(/\x1b(?:\[[0-9;]*[A-Za-z]|\].*?(?:\x07|\x1b\\))/g, "");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// src/processes.ts
|
|
42
|
-
var MAX_LOGS = 500;
|
|
43
|
-
var ERROR_RECOVERY_MS = 5e3;
|
|
44
|
-
var MAX_CRASH_RETRIES = 3;
|
|
45
|
-
var ProcessRunner = class extends EventEmitter {
|
|
46
|
-
children = /* @__PURE__ */ new Map();
|
|
47
|
-
state = /* @__PURE__ */ new Map();
|
|
48
|
-
errorTimers = /* @__PURE__ */ new Map();
|
|
49
|
-
lastGoodStatus = /* @__PURE__ */ new Map();
|
|
50
|
-
crashRetries = /* @__PURE__ */ new Map();
|
|
51
|
-
pendingRebuilds = /* @__PURE__ */ new Set();
|
|
52
|
-
root;
|
|
53
|
-
stopping = false;
|
|
54
|
-
constructor(root) {
|
|
55
|
-
super();
|
|
56
|
-
this.root = root;
|
|
57
|
-
}
|
|
58
|
-
get(name) {
|
|
59
|
-
return this.state.get(name);
|
|
60
|
-
}
|
|
61
|
-
async start(workspaces) {
|
|
62
|
-
const packages = workspaces.filter((w) => w.kind === "package");
|
|
63
|
-
const apps = workspaces.filter((w) => w.kind !== "package");
|
|
64
|
-
for (const workspace of workspaces) {
|
|
65
|
-
this.state.set(workspace.name, { workspace, status: "pending", logs: [] });
|
|
66
|
-
}
|
|
67
|
-
for (const workspace of packages) {
|
|
68
|
-
this.spawn(workspace);
|
|
69
|
-
}
|
|
70
|
-
if (packages.length > 0) {
|
|
71
|
-
await this.waitForPackages(packages.map((p) => p.name));
|
|
72
|
-
}
|
|
73
|
-
for (const workspace of apps) {
|
|
74
|
-
this.spawn(workspace);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
async shutdown() {
|
|
78
|
-
this.stopping = true;
|
|
79
|
-
for (const timer of this.errorTimers.values()) clearTimeout(timer);
|
|
80
|
-
this.errorTimers.clear();
|
|
81
|
-
for (const child of this.pendingRebuilds) child.kill("SIGTERM");
|
|
82
|
-
const waiting = [];
|
|
83
|
-
for (const [, child] of this.children) {
|
|
84
|
-
if (child.exitCode !== null || child.signalCode !== null) continue;
|
|
85
|
-
waiting.push(
|
|
86
|
-
new Promise((resolve) => {
|
|
87
|
-
const escalate = setTimeout(() => {
|
|
88
|
-
if (child.exitCode === null) child.kill("SIGKILL");
|
|
89
|
-
}, 5e3);
|
|
90
|
-
child.on("close", () => {
|
|
91
|
-
clearTimeout(escalate);
|
|
92
|
-
resolve();
|
|
93
|
-
});
|
|
94
|
-
child.kill("SIGTERM");
|
|
95
|
-
})
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
await Promise.all(waiting);
|
|
99
|
-
}
|
|
100
|
-
waitForPackages(names) {
|
|
101
|
-
const remaining = new Set(names);
|
|
102
|
-
return new Promise((resolve) => {
|
|
103
|
-
const check = () => {
|
|
104
|
-
for (const name of [...remaining]) {
|
|
105
|
-
const s = this.state.get(name)?.status;
|
|
106
|
-
if (s === "watching" || s === "error" || s === "stopped") remaining.delete(name);
|
|
107
|
-
}
|
|
108
|
-
if (remaining.size === 0) {
|
|
109
|
-
this.off("change", check);
|
|
110
|
-
resolve();
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
this.on("change", check);
|
|
114
|
-
check();
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
spawn(workspace) {
|
|
118
|
-
const child = spawn("pnpm", ["--filter", workspace.name, "run", "dev"], {
|
|
119
|
-
cwd: this.root,
|
|
120
|
-
stdio: "pipe",
|
|
121
|
-
env: { ...process.env, FORCE_COLOR: "1" }
|
|
122
|
-
});
|
|
123
|
-
this.children.set(workspace.name, child);
|
|
124
|
-
this.setStatus(workspace.name, "building");
|
|
125
|
-
let buffer = "";
|
|
126
|
-
const onData = (data) => {
|
|
127
|
-
buffer += data.toString();
|
|
128
|
-
const lines = buffer.split("\n");
|
|
129
|
-
buffer = lines.pop();
|
|
130
|
-
for (const raw of lines) {
|
|
131
|
-
const line = raw.trimEnd();
|
|
132
|
-
if (line) this.handleLine(workspace.name, line);
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
child.stdout?.on("data", onData);
|
|
136
|
-
child.stderr?.on("data", onData);
|
|
137
|
-
child.on("close", (code, signal) => {
|
|
138
|
-
if (buffer.trim()) this.handleLine(workspace.name, buffer.trimEnd());
|
|
139
|
-
buffer = "";
|
|
140
|
-
if (this.stopping) return;
|
|
141
|
-
if (signal === "SIGABRT") {
|
|
142
|
-
this.handleCrash(workspace);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
this.setStatus(workspace.name, code === 0 ? "stopped" : "error");
|
|
146
|
-
});
|
|
147
|
-
child.on("error", () => this.setStatus(workspace.name, "error"));
|
|
148
|
-
}
|
|
149
|
-
handleLine(name, line) {
|
|
150
|
-
const proc = this.state.get(name);
|
|
151
|
-
if (!proc) return;
|
|
152
|
-
proc.logs.push(line);
|
|
153
|
-
if (proc.logs.length > MAX_LOGS) proc.logs.splice(0, proc.logs.length - MAX_LOGS);
|
|
154
|
-
const { status, url } = parseLine(stripAnsi(line));
|
|
155
|
-
if (status) {
|
|
156
|
-
if (status === "error") {
|
|
157
|
-
this.scheduleErrorRecovery(name);
|
|
158
|
-
} else {
|
|
159
|
-
this.lastGoodStatus.set(name, status);
|
|
160
|
-
this.clearErrorTimer(name);
|
|
161
|
-
}
|
|
162
|
-
proc.status = status;
|
|
163
|
-
}
|
|
164
|
-
if (url) proc.url = url;
|
|
165
|
-
this.emit("change");
|
|
166
|
-
}
|
|
167
|
-
handleCrash(workspace) {
|
|
168
|
-
const retries = (this.crashRetries.get(workspace.name) ?? 0) + 1;
|
|
169
|
-
this.crashRetries.set(workspace.name, retries);
|
|
170
|
-
const proc = this.state.get(workspace.name);
|
|
171
|
-
if (retries <= MAX_CRASH_RETRIES) {
|
|
172
|
-
if (proc) {
|
|
173
|
-
proc.logs.push(
|
|
174
|
-
`[hlidskjalf] fsevents crash detected (attempt ${retries}/${MAX_CRASH_RETRIES}) \u2014 rebuilding...`
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
this.emit("change");
|
|
178
|
-
this.rebuildFsevents().then(() => {
|
|
179
|
-
if (!this.stopping) this.spawn(workspace);
|
|
180
|
-
}).catch(() => this.setStatus(workspace.name, "error"));
|
|
181
|
-
} else {
|
|
182
|
-
if (proc) {
|
|
183
|
-
proc.logs.push(
|
|
184
|
-
`[hlidskjalf] fsevents still crashing after ${MAX_CRASH_RETRIES} attempts \u2014 giving up.`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
this.setStatus(workspace.name, "error");
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
rebuildFsevents() {
|
|
191
|
-
return new Promise((resolve) => {
|
|
192
|
-
const child = spawn("pnpm", ["rebuild", "fsevents"], {
|
|
193
|
-
cwd: this.root,
|
|
194
|
-
stdio: "pipe"
|
|
195
|
-
});
|
|
196
|
-
this.pendingRebuilds.add(child);
|
|
197
|
-
const done = () => {
|
|
198
|
-
this.pendingRebuilds.delete(child);
|
|
199
|
-
resolve();
|
|
200
|
-
};
|
|
201
|
-
child.on("close", done);
|
|
202
|
-
child.on("error", done);
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
scheduleErrorRecovery(name) {
|
|
206
|
-
this.clearErrorTimer(name);
|
|
207
|
-
const timer = setTimeout(() => {
|
|
208
|
-
this.errorTimers.delete(name);
|
|
209
|
-
const proc = this.state.get(name);
|
|
210
|
-
if (proc?.status === "error") {
|
|
211
|
-
this.setStatus(name, this.lastGoodStatus.get(name) ?? "ready");
|
|
212
|
-
}
|
|
213
|
-
}, ERROR_RECOVERY_MS);
|
|
214
|
-
timer.unref();
|
|
215
|
-
this.errorTimers.set(name, timer);
|
|
216
|
-
}
|
|
217
|
-
clearErrorTimer(name) {
|
|
218
|
-
const timer = this.errorTimers.get(name);
|
|
219
|
-
if (timer) {
|
|
220
|
-
clearTimeout(timer);
|
|
221
|
-
this.errorTimers.delete(name);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
setStatus(name, status) {
|
|
225
|
-
const proc = this.state.get(name);
|
|
226
|
-
if (!proc) return;
|
|
227
|
-
proc.status = status;
|
|
228
|
-
this.emit("change");
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
function createRunner(root) {
|
|
232
|
-
return new ProcessRunner(root);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// src/views/dashboard.tsx
|
|
236
|
-
import { Box, Text, useStdout } from "ink";
|
|
237
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
238
|
-
var kindLabel = {
|
|
239
|
-
package: "pkg",
|
|
240
|
-
app: "app",
|
|
241
|
-
service: "svc"
|
|
242
|
-
};
|
|
243
|
-
var statusDisplay = {
|
|
244
|
-
pending: { color: "gray", label: "pending" },
|
|
245
|
-
building: { color: "yellow", label: "building" },
|
|
246
|
-
watching: { color: "green", label: "watching" },
|
|
247
|
-
ready: { color: "green", label: "watching" },
|
|
248
|
-
error: { color: "red", label: "error" },
|
|
249
|
-
stopped: { color: "gray", label: "stopped" }
|
|
250
|
-
};
|
|
251
|
-
var HINTS = "\u2191/\u2193 j/k select q quit";
|
|
252
|
-
function Dashboard({ processes, selectedIndex }) {
|
|
253
|
-
const { stdout } = useStdout();
|
|
254
|
-
const cols = stdout?.columns ?? 80;
|
|
255
|
-
const rows = stdout?.rows ?? 24;
|
|
256
|
-
const allReady = processes.length > 0 && processes.every((p) => p.status === "ready" || p.status === "watching");
|
|
257
|
-
const nameWidth = Math.max(14, ...processes.map((p) => p.workspace.name.length + 2));
|
|
258
|
-
const logHeight = Math.max(3, rows - processes.length - 5);
|
|
259
|
-
const safeIndex = Math.min(selectedIndex, Math.max(0, processes.length - 1));
|
|
260
|
-
const selected = processes[safeIndex];
|
|
261
|
-
const logLines = selected?.logs.slice(-logHeight) ?? [];
|
|
262
|
-
const showHints = cols >= 10 + HINTS.length + 4;
|
|
263
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
264
|
-
/* @__PURE__ */ jsxs(Box, { children: [
|
|
265
|
-
/* @__PURE__ */ jsxs(Box, { flexGrow: 1, children: [
|
|
266
|
-
/* @__PURE__ */ jsx(Text, { color: allReady ? "green" : "gray", children: "\u25CF " }),
|
|
267
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: "Hlidskjalf" })
|
|
268
|
-
] }),
|
|
269
|
-
showHints && /* @__PURE__ */ jsx(Text, { dimColor: true, children: HINTS })
|
|
270
|
-
] }),
|
|
271
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(cols) }),
|
|
272
|
-
/* @__PURE__ */ jsxs(Box, { marginLeft: 1, children: [
|
|
273
|
-
/* @__PURE__ */ jsx(Box, { width: nameWidth, children: /* @__PURE__ */ jsx(Text, { dimColor: true, bold: true, children: "Name" }) }),
|
|
274
|
-
/* @__PURE__ */ jsx(Box, { width: 6, children: /* @__PURE__ */ jsx(Text, { dimColor: true, bold: true, children: "Kind" }) }),
|
|
275
|
-
/* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, bold: true, children: "Status" }) }),
|
|
276
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, bold: true, children: "URL" })
|
|
277
|
-
] }),
|
|
278
|
-
processes.map((proc, i) => {
|
|
279
|
-
const isSelected = i === safeIndex;
|
|
280
|
-
const { color, label } = statusDisplay[proc.status];
|
|
281
|
-
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
282
|
-
/* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8" : " " }),
|
|
283
|
-
/* @__PURE__ */ jsx(Box, { width: nameWidth, children: /* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : void 0, bold: isSelected, wrap: "truncate", children: proc.workspace.name }) }),
|
|
284
|
-
/* @__PURE__ */ jsx(Box, { width: 6, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: kindLabel[proc.workspace.kind] }) }),
|
|
285
|
-
/* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsxs(Text, { color, children: [
|
|
286
|
-
"\u25CF ",
|
|
287
|
-
label
|
|
288
|
-
] }) }),
|
|
289
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: proc.url ?? "" })
|
|
290
|
-
] }, proc.workspace.name);
|
|
291
|
-
}),
|
|
292
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(cols) }),
|
|
293
|
-
selected && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
294
|
-
/* @__PURE__ */ jsx(Box, { marginLeft: 1, children: /* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
295
|
-
"Logs: ",
|
|
296
|
-
selected.workspace.name
|
|
297
|
-
] }) }),
|
|
298
|
-
logLines.map((line, i) => (
|
|
299
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: log lines have no stable identity
|
|
300
|
-
/* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
|
|
301
|
-
" ",
|
|
302
|
-
line
|
|
303
|
-
] }, i)
|
|
304
|
-
)),
|
|
305
|
-
Array.from({ length: logHeight - logLines.length }, (_, i) => (
|
|
306
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: fill lines have no stable identity
|
|
307
|
-
/* @__PURE__ */ jsx(Text, { children: " " }, `fill-${i}`)
|
|
308
|
-
))
|
|
309
|
-
] })
|
|
310
|
-
] });
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// src/views/loading.tsx
|
|
314
|
-
import { Box as Box2, Text as Text2, useStdout as useStdout2 } from "ink";
|
|
315
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
316
|
-
function Loading() {
|
|
317
|
-
const { stdout } = useStdout2();
|
|
318
|
-
const cols = stdout?.columns ?? 80;
|
|
319
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
320
|
-
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
321
|
-
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: "\u25CF " }),
|
|
322
|
-
/* @__PURE__ */ jsx2(Text2, { bold: true, children: "Hlidskjalf" })
|
|
323
|
-
] }),
|
|
324
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(cols) }),
|
|
325
|
-
/* @__PURE__ */ jsx2(Box2, { marginTop: 1, marginLeft: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Discovering workspaces..." }) })
|
|
326
|
-
] });
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// src/workspaces.ts
|
|
330
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
331
|
-
import { join } from "path";
|
|
332
|
-
function readJson(path) {
|
|
333
|
-
try {
|
|
334
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
335
|
-
} catch {
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
function workspaceDeps(pkg) {
|
|
340
|
-
return Object.entries(pkg.dependencies ?? {}).filter(([, v]) => v.startsWith("workspace:")).map(([name]) => name);
|
|
341
|
-
}
|
|
342
|
-
var kindOrder = { package: 0, app: 1, service: 1 };
|
|
343
|
-
function discover(root) {
|
|
344
|
-
const results = [];
|
|
345
|
-
const dirs = [
|
|
346
|
-
["packages", "package"],
|
|
347
|
-
["apps", "app"],
|
|
348
|
-
["services", "service"]
|
|
349
|
-
];
|
|
350
|
-
for (const [dir, kind] of dirs) {
|
|
351
|
-
const base = join(root, dir);
|
|
352
|
-
if (!existsSync(base)) continue;
|
|
353
|
-
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
354
|
-
if (!entry.isDirectory()) continue;
|
|
355
|
-
const pkg = readJson(join(base, entry.name, "package.json"));
|
|
356
|
-
if (!pkg?.name) continue;
|
|
357
|
-
if (pkg.name === "hlidskjalf") continue;
|
|
358
|
-
if (!pkg.scripts?.dev) continue;
|
|
359
|
-
results.push({
|
|
360
|
-
name: pkg.name,
|
|
361
|
-
kind,
|
|
362
|
-
deps: workspaceDeps(pkg)
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return results;
|
|
367
|
-
}
|
|
368
|
-
function sortByDeps(workspaces) {
|
|
369
|
-
const names = new Set(workspaces.map((w) => w.name));
|
|
370
|
-
return [...workspaces].sort((a, b) => {
|
|
371
|
-
if (a.kind !== b.kind) return kindOrder[a.kind] - kindOrder[b.kind];
|
|
372
|
-
const aDeps = a.deps.filter((d) => names.has(d)).length;
|
|
373
|
-
const bDeps = b.deps.filter((d) => names.has(d)).length;
|
|
374
|
-
return aDeps - bDeps;
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
function sortByName(workspaces) {
|
|
378
|
-
return [...workspaces].sort((a, b) => {
|
|
379
|
-
if (a.kind !== b.kind) return kindOrder[a.kind] - kindOrder[b.kind];
|
|
380
|
-
return a.name.localeCompare(b.name);
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
function filterWorkspaces(workspaces, patterns) {
|
|
384
|
-
const byName = new Map(workspaces.map((w) => [w.name, w]));
|
|
385
|
-
const matches = /* @__PURE__ */ new Set();
|
|
386
|
-
for (const pattern of patterns) {
|
|
387
|
-
const transitive = pattern.endsWith("...");
|
|
388
|
-
const name = transitive ? pattern.slice(0, -3) : pattern;
|
|
389
|
-
if (byName.has(name)) matches.add(name);
|
|
390
|
-
if (transitive) collectDeps(name, byName, matches);
|
|
391
|
-
}
|
|
392
|
-
return workspaces.filter((w) => matches.has(w.name));
|
|
393
|
-
}
|
|
394
|
-
function collectDeps(name, byName, collected) {
|
|
395
|
-
const workspace = byName.get(name);
|
|
396
|
-
if (!workspace) return;
|
|
397
|
-
for (const dep of workspace.deps) {
|
|
398
|
-
if (byName.has(dep) && !collected.has(dep)) {
|
|
399
|
-
collected.add(dep);
|
|
400
|
-
collectDeps(dep, byName, collected);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// src/app.tsx
|
|
406
|
-
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
407
|
-
function App({ options }) {
|
|
408
|
-
const { exit } = useApp();
|
|
409
|
-
const [loading, setLoading] = useState(true);
|
|
410
|
-
const [processes, setProcesses] = useState([]);
|
|
411
|
-
const [cursor, setCursor] = useState(0);
|
|
412
|
-
const runnerRef = useRef(null);
|
|
413
|
-
const stoppingRef = useRef(false);
|
|
414
|
-
const stop = useCallback(() => {
|
|
415
|
-
if (stoppingRef.current) return;
|
|
416
|
-
stoppingRef.current = true;
|
|
417
|
-
const runner = runnerRef.current;
|
|
418
|
-
if (runner) {
|
|
419
|
-
void runner.shutdown().finally(() => exit());
|
|
420
|
-
} else {
|
|
421
|
-
exit();
|
|
422
|
-
}
|
|
423
|
-
}, [exit]);
|
|
424
|
-
useEffect(() => {
|
|
425
|
-
const run = async () => {
|
|
426
|
-
let workspaces = discover(options.root);
|
|
427
|
-
if (options.filter) {
|
|
428
|
-
workspaces = filterWorkspaces(workspaces, options.filter);
|
|
429
|
-
}
|
|
430
|
-
if (workspaces.length === 0) {
|
|
431
|
-
console.error("No matching workspaces found.");
|
|
432
|
-
exit();
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const startOrder = sortByDeps(workspaces);
|
|
436
|
-
const sorted = options.order === "run" ? startOrder : sortByName(workspaces);
|
|
437
|
-
const displayOrder = sorted.map((w) => w.name);
|
|
438
|
-
const runner = createRunner(options.root);
|
|
439
|
-
runnerRef.current = runner;
|
|
440
|
-
setProcesses(sorted.map((w) => ({ workspace: w, status: "pending", logs: [] })));
|
|
441
|
-
runner.on("change", () => {
|
|
442
|
-
setProcesses(
|
|
443
|
-
displayOrder.flatMap((name) => {
|
|
444
|
-
const p = runner.get(name);
|
|
445
|
-
return p ? [p] : [];
|
|
446
|
-
})
|
|
447
|
-
);
|
|
448
|
-
});
|
|
449
|
-
setLoading(false);
|
|
450
|
-
await runner.start(startOrder);
|
|
451
|
-
};
|
|
452
|
-
run().catch((err) => {
|
|
453
|
-
console.error("Fatal:", err);
|
|
454
|
-
exit();
|
|
455
|
-
});
|
|
456
|
-
process.on("SIGTERM", stop);
|
|
457
|
-
return () => {
|
|
458
|
-
process.off("SIGTERM", stop);
|
|
459
|
-
};
|
|
460
|
-
}, [exit, options.filter, options.order, options.root, stop]);
|
|
461
|
-
useInput((input, key) => {
|
|
462
|
-
if (loading) return;
|
|
463
|
-
if (input === "q" || key.ctrl && input === "c") {
|
|
464
|
-
stop();
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
if (key.upArrow || input === "k") {
|
|
468
|
-
setCursor((i) => Math.max(0, i - 1));
|
|
469
|
-
} else if (key.downArrow || input === "j") {
|
|
470
|
-
setCursor((i) => Math.min(processes.length - 1, i + 1));
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
if (loading) return /* @__PURE__ */ jsx3(Loading, {});
|
|
474
|
-
return /* @__PURE__ */ jsx3(Dashboard, { processes, selectedIndex: cursor });
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// src/index.tsx
|
|
478
|
-
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
479
|
-
function parseArgs(argv) {
|
|
480
|
-
const root = process.cwd();
|
|
481
|
-
const filter = [];
|
|
482
|
-
let order = "alphabetical";
|
|
483
|
-
for (const arg of argv) {
|
|
484
|
-
if (arg.startsWith("--filter=")) {
|
|
485
|
-
const value = arg.slice("--filter=".length).replace(/^\{(.+)\}$/, "$1");
|
|
486
|
-
filter.push(value);
|
|
487
|
-
} else if (arg.startsWith("--order=")) {
|
|
488
|
-
const value = arg.slice("--order=".length);
|
|
489
|
-
if (value === "run" || value === "alphabetical") order = value;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
return { root, order, filter: filter.length > 0 ? filter : void 0 };
|
|
493
|
-
}
|
|
494
|
-
var { waitUntilExit } = render(/* @__PURE__ */ jsx4(App, { options: parseArgs(process.argv.slice(2)) }), {
|
|
495
|
-
exitOnCtrlC: false
|
|
496
|
-
});
|
|
497
|
-
await waitUntilExit();
|
|
498
|
-
process.exit(0);
|
|
2
|
+
import { parseArgs } from 'util';
|
|
3
|
+
import { render, useInput, useApp, useStdout, Box, Text } from 'ink';
|
|
4
|
+
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
10
|
+
|
|
11
|
+
function useCursor(length,enabled){let[cursor,setCursor]=useState(0);return useInput((input,key)=>{key.upArrow||input==="k"?setCursor(i=>Math.max(0,i-1)):(key.downArrow||input==="j")&&setCursor(i=>Math.min(length-1,i+1));},{isActive:enabled}),cursor}var DTS=/\bDTS\b/,matchers=[{pattern:/running on (https?:\/\/\S+)/,status:"ready"},{pattern:/listening on (https?:\/\/\S+)/,status:"ready"},{pattern:/started.*(https?:\/\/localhost:\d+)/,status:"ready"},{pattern:/\bVITE\b.*\bready in\b/i,status:"ready"},{pattern:/\bLocal:\s+(https?:\/\/\S+)/,status:"ready"},{pattern:/⚡\uFE0F?\s*Build success/,status:"watching"},{pattern:/Build start/,status:"building"},{pattern:/Watching for changes/,status:"watching"},{pattern:/[Ee]rror[\s:]/,status:"error"},{pattern:/process exit/,status:"error"}];function parseLine(line){if(DTS.test(line))return {};for(let{pattern,status}of matchers){let match=line.match(pattern);if(match)return {status,url:match[1]}}return {}}function stripAnsi(text){return text.replace(/\x1b(?:\[[0-9;]*[A-Za-z]|\].*?(?:\x07|\x1b\\))/g,"")}var MAX_LOGS=500,ERROR_RECOVERY_MS=5e3,MAX_RESTART_RETRIES=3,RESTART_DELAY_MS=1e3,STARTUP_TIMEOUT_MS=12e4,HEARTBEAT_INTERVAL_MS=1e4,STALE_THRESHOLD_MS=6e4,ProcessRunner=class extends EventEmitter{entries=new Map;pendingRebuilds=new Set;heartbeatInterval=null;root;stopping=false;allWorkspaces=[];constructor(root){super(),this.root=root;}get(name){return this.entries.get(name)?.process}async start(workspaces){this.allWorkspaces=workspaces;let packages=workspaces.filter(w=>w.kind==="package"),apps=workspaces.filter(w=>w.kind!=="package");for(let workspace of workspaces)this.entries.set(workspace.name,{process:{workspace,status:"pending",logs:[]},child:null,errorTimer:null,restartTimer:null,startupTimer:null,lastGoodStatus:null,restartRetries:0,lastOutputAt:0});for(let workspace of packages)this.spawn(workspace);packages.length>0&&await this.waitForPackages(packages.map(p=>p.name));let failedPackages=new Set;for(let pkg of packages){let s=this.entries.get(pkg.name)?.process.status;(s==="error"||s==="stopped"||s==="timeout")&&failedPackages.add(pkg.name);}for(let workspace of apps){let failedDeps=workspace.deps.filter(d=>failedPackages.has(d));if(failedDeps.length>0){let entry=this.entries.get(workspace.name);entry&&(entry.process.logs.push(`[hlidskjalf] warning: dependency ${failedDeps.join(", ")} failed \u2014 starting anyway`),this.emit("change"));}this.spawn(workspace);}this.startHeartbeat();}async shutdown(){this.stopping=true,this.heartbeatInterval&&clearInterval(this.heartbeatInterval);for(let entry of this.entries.values())entry.errorTimer&&clearTimeout(entry.errorTimer),entry.restartTimer&&clearTimeout(entry.restartTimer),entry.startupTimer&&clearTimeout(entry.startupTimer);for(let child of this.pendingRebuilds)child.kill("SIGTERM");let waiting=[];for(let entry of this.entries.values()){let{child}=entry;!child||child.exitCode!==null||child.signalCode!==null||waiting.push(new Promise(resolve=>{let escalate=setTimeout(()=>{child.exitCode===null&&child.kill("SIGKILL");},5e3);child.on("close",()=>{clearTimeout(escalate),resolve();}),child.kill("SIGTERM");}));}await Promise.all(waiting);}entry(name){return this.entries.get(name)}waitForPackages(names){let remaining=new Set(names);return new Promise(resolve=>{let check=()=>{for(let name of [...remaining]){let s=this.entry(name)?.process.status;(s==="watching"||s==="error"||s==="stopped"||s==="timeout")&&remaining.delete(name);}remaining.size===0&&(this.off("change",check),resolve());};this.on("change",check),check();})}spawn(workspace){let child=spawn("pnpm",["--filter",workspace.name,"run","dev"],{cwd:this.root,stdio:"pipe",env:{...process.env,FORCE_COLOR:"1"}}),entry=this.entry(workspace.name);entry&&(entry.child=child),this.setStatus(workspace.name,"building");let startupTimer=setTimeout(()=>{let e=this.entry(workspace.name);e&&(e.startupTimer=null,e.process.status!=="watching"&&e.process.status!=="ready"&&(e.process.logs.push(`[hlidskjalf] startup timeout after ${STARTUP_TIMEOUT_MS/1e3}s`),this.setStatus(workspace.name,"timeout")));},STARTUP_TIMEOUT_MS);startupTimer.unref(),entry&&(entry.startupTimer=startupTimer);let buffer="",onData=data=>{buffer+=data.toString();let lines=buffer.split(`
|
|
12
|
+
`);buffer=lines.pop()??"";for(let raw of lines){let line=raw.trimEnd();line&&this.handleLine(workspace.name,line);}};child.stdout?.on("data",onData),child.stderr?.on("data",onData),child.on("close",(code,signal)=>{buffer.trim()&&this.handleLine(workspace.name,buffer.trimEnd()),buffer="",!this.stopping&&this.handleUnexpectedExit(workspace,code,signal);}),child.on("error",()=>{let e=this.entry(workspace.name);e?.startupTimer&&(clearTimeout(e.startupTimer),e.startupTimer=null),this.setStatus(workspace.name,"error");});}handleLine(name,line){if(this.stopping)return;let entry=this.entry(name);if(!entry)return;let{process:proc}=entry;proc.logs.push(line),proc.logs.length>MAX_LOGS&&proc.logs.splice(0,proc.logs.length-MAX_LOGS),entry.lastOutputAt=Date.now();let{status,url}=parseLine(stripAnsi(line));status&&(status==="error"?this.scheduleErrorRecovery(name):(entry.lastGoodStatus=status,this.clearErrorTimer(name),entry.restartRetries=0,(status==="watching"||status==="ready")&&entry.startupTimer&&(clearTimeout(entry.startupTimer),entry.startupTimer=null)),proc.status=status),url&&(proc.url=url),this.emit("change");}handleUnexpectedExit(workspace,code,signal){if(code===0){this.setStatus(workspace.name,"stopped");return}let entry=this.entry(workspace.name);if(!entry)return;entry.restartRetries+=1;let{restartRetries}=entry;if(restartRetries>MAX_RESTART_RETRIES){entry.process.logs.push(`[hlidskjalf] process exited ${MAX_RESTART_RETRIES} times \u2014 giving up.`),this.setStatus(workspace.name,"error");return}let delay=RESTART_DELAY_MS*2**(restartRetries-1);if(entry.process.logs.push(`[hlidskjalf] process exited unexpectedly (attempt ${restartRetries}/${MAX_RESTART_RETRIES}) \u2014 restarting in ${delay/1e3}s...`),this.setStatus(workspace.name,"error"),signal==="SIGABRT"){this.rebuildFsevents().then(()=>{this.stopping||this.spawn(workspace);}).catch(()=>this.setStatus(workspace.name,"error"));return}let timer=setTimeout(()=>{entry&&(entry.restartTimer=null),this.stopping||this.spawn(workspace);},delay);timer.unref(),entry.restartTimer=timer;}rebuildFsevents(){return new Promise(resolve=>{let child=spawn("pnpm",["rebuild","fsevents"],{cwd:this.root,stdio:"pipe"});this.pendingRebuilds.add(child);let done=()=>{this.pendingRebuilds.delete(child),resolve();};child.on("close",done),child.on("error",done);})}startHeartbeat(){this.heartbeatInterval=setInterval(()=>{let now=Date.now();for(let[name,entry]of this.entries)entry.process.status!=="watching"&&entry.process.status!=="ready"||entry.lastOutputAt&&now-entry.lastOutputAt>STALE_THRESHOLD_MS&&this.setStatus(name,"stale");},HEARTBEAT_INTERVAL_MS),this.heartbeatInterval.unref();}scheduleErrorRecovery(name){this.clearErrorTimer(name);let entry=this.entry(name);if(!entry)return;let timer=setTimeout(()=>{entry.errorTimer=null,entry.process.status==="error"&&this.setStatus(name,entry.lastGoodStatus??"ready");},ERROR_RECOVERY_MS);timer.unref(),entry.errorTimer=timer;}clearErrorTimer(name){let entry=this.entry(name);entry?.errorTimer&&(clearTimeout(entry.errorTimer),entry.errorTimer=null);}setStatus(name,status){let entry=this.entry(name);entry&&(entry.process.status=status,status==="error"&&entry.process.workspace.kind==="package"&&this.notifyDependents(name),this.emit("change"));}notifyDependents(failedName){for(let workspace of this.allWorkspaces){if(!workspace.deps.includes(failedName))continue;let entry=this.entry(workspace.name);entry&&entry.process.logs.push(`[hlidskjalf] warning: dependency ${failedName} entered error state`);}}};function createRunner(root){return new ProcessRunner(root)}function readJson(path){try{return JSON.parse(readFileSync(path,"utf-8"))}catch{return null}}function workspaceDeps(pkg){return Object.entries(pkg.dependencies??{}).filter(([,v])=>v.startsWith("workspace:")).map(([name])=>name)}var kindOrder={package:0,app:1,service:1};function discover(root){let results=[],dirs=[["packages","package"],["apps","app"],["services","service"]];for(let[dir,kind]of dirs){let base=join(root,dir);if(existsSync(base))for(let entry of readdirSync(base,{withFileTypes:true})){if(!entry.isDirectory())continue;let pkg=readJson(join(base,entry.name,"package.json"));pkg?.name&&pkg.name!=="hlidskjalf"&&pkg.scripts?.dev&&results.push({name:pkg.name,kind,deps:workspaceDeps(pkg)});}}return results}function sortByDeps(workspaces){let names=new Set(workspaces.map(w=>w.name));return [...workspaces].sort((a,b)=>{if(a.kind!==b.kind)return kindOrder[a.kind]-kindOrder[b.kind];let aDeps=a.deps.filter(d=>names.has(d)).length,bDeps=b.deps.filter(d=>names.has(d)).length;return aDeps-bDeps})}function sortByName(workspaces){return [...workspaces].sort((a,b)=>a.kind!==b.kind?kindOrder[a.kind]-kindOrder[b.kind]:a.name.localeCompare(b.name))}function filterWorkspaces(workspaces,patterns){let byName=new Map(workspaces.map(w=>[w.name,w])),matches=new Set;for(let pattern of patterns){let transitive=pattern.endsWith("..."),name=transitive?pattern.slice(0,-3):pattern;byName.has(name)&&matches.add(name),transitive&&collectDeps(name,byName,matches);}return workspaces.filter(w=>matches.has(w.name))}function collectDeps(name,byName,collected){let workspace=byName.get(name);if(workspace)for(let dep of workspace.deps)byName.has(dep)&&!collected.has(dep)&&(collected.add(dep),collectDeps(dep,byName,collected));}function useRunner(options2){let{exit}=useApp(),[loading,setLoading]=useState(true),[processes,setProcesses]=useState([]),runnerRef=useRef(null),stoppingRef=useRef(false),stop=useCallback(()=>{if(stoppingRef.current)return;stoppingRef.current=true;let runner=runnerRef.current;runner?runner.shutdown().finally(()=>exit()):exit();},[exit]);return useEffect(()=>((async()=>{let workspaces=discover(options2.root);if(options2.filter&&(workspaces=filterWorkspaces(workspaces,options2.filter)),workspaces.length===0){console.error("No matching workspaces found."),exit();return}let startOrder=sortByDeps(workspaces),sorted=options2.order==="run"?startOrder:sortByName(workspaces),displayOrder=sorted.map(w=>w.name),runner=createRunner(options2.root);runnerRef.current=runner,setProcesses(sorted.map(w=>({workspace:w,status:"pending",logs:[]}))),runner.on("change",()=>{setProcesses(displayOrder.flatMap(name=>{let p=runner.get(name);return p?[p]:[]}));}),setLoading(false),await runner.start(startOrder);})().catch(err=>{console.error("Fatal:",err),exit();}),process.on("SIGTERM",stop),()=>{process.off("SIGTERM",stop);}),[exit,options2.filter,options2.order,options2.root,stop]),{processes,loading,stop}}function Header({ready=false,columns,hints}){let showHints=hints&&columns>=10+hints.length+4;return jsxs(Fragment,{children:[jsxs(Box,{children:[jsxs(Box,{flexGrow:1,children:[jsx(Text,{color:ready?"green":"gray",children:"\u25CF "}),jsx(Text,{bold:true,children:"Hlidskjalf"})]}),showHints&&jsx(Text,{dimColor:true,children:hints})]}),jsx(Text,{dimColor:true,children:"\u2500".repeat(columns)})]})}var kindLabel={package:"pkg",app:"app",service:"svc"},statusDisplay={pending:{color:"gray",label:"pending"},building:{color:"yellow",label:"building"},watching:{color:"green",label:"watching"},ready:{color:"green",label:"watching"},error:{color:"red",label:"error"},stopped:{color:"gray",label:"stopped"},stale:{color:"yellow",label:"stale"},timeout:{color:"red",label:"timeout"}},HINTS="\u2191/\u2193 j/k select q quit";function ProcessRow({process:proc,selected,nameWidth}){let{color,label}=statusDisplay[proc.status];return jsxs(Box,{children:[jsx(Text,{color:selected?"cyan":void 0,children:selected?"\u25B8":" "}),jsx(Box,{width:nameWidth,children:jsx(Text,{color:selected?"cyan":void 0,bold:selected,wrap:"truncate",children:proc.workspace.name})}),jsx(Box,{width:6,children:jsx(Text,{dimColor:true,children:kindLabel[proc.workspace.kind]})}),jsx(Box,{width:14,children:jsxs(Text,{color,children:["\u25CF ",label]})}),jsx(Text,{dimColor:true,children:proc.url??""})]})}function LogPanel({process:proc,height}){let logLines=proc.logs.slice(-height),fillCount=height-logLines.length;return jsxs(Box,{flexDirection:"column",children:[jsx(Box,{marginLeft:1,children:jsxs(Text,{bold:true,children:["Logs: ",proc.workspace.name]})}),logLines.map((line,i)=>jsxs(Text,{wrap:"truncate",children:[" ",line]},i)),Array.from({length:fillCount},(_,i)=>jsx(Text,{children:" "},`fill-${i}`))]})}function Dashboard({processes,selectedIndex}){let{stdout}=useStdout(),cols=stdout?.columns??80,rows=stdout?.rows??24,allReady=useMemo(()=>processes.length>0&&processes.every(p=>p.status==="ready"||p.status==="watching"),[processes]),nameWidth=useMemo(()=>Math.max(14,...processes.map(p=>p.workspace.name.length+2)),[processes]),logHeight=Math.max(3,rows-processes.length-5),safeIndex=Math.min(selectedIndex,Math.max(0,processes.length-1)),selected=processes[safeIndex];return jsxs(Box,{flexDirection:"column",children:[jsx(Header,{ready:allReady,columns:cols,hints:HINTS}),jsxs(Box,{marginLeft:1,children:[jsx(Box,{width:nameWidth,children:jsx(Text,{dimColor:true,bold:true,children:"Name"})}),jsx(Box,{width:6,children:jsx(Text,{dimColor:true,bold:true,children:"Kind"})}),jsx(Box,{width:14,children:jsx(Text,{dimColor:true,bold:true,children:"Status"})}),jsx(Text,{dimColor:true,bold:true,children:"URL"})]}),processes.map((proc,i)=>jsx(ProcessRow,{process:proc,selected:i===safeIndex,nameWidth},proc.workspace.name)),jsx(Text,{dimColor:true,children:"\u2500".repeat(cols)}),selected&&jsx(LogPanel,{process:selected,height:logHeight})]})}function Loading(){let{stdout}=useStdout(),cols=stdout?.columns??80;return jsxs(Box,{flexDirection:"column",children:[jsx(Header,{columns:cols}),jsx(Box,{marginTop:1,marginLeft:1,children:jsx(Text,{dimColor:true,children:"Discovering workspaces..."})})]})}function App({options:options2}){let{processes,loading,stop}=useRunner(options2),cursor=useCursor(processes.length,!loading);return useInput((input,key)=>{(input==="q"||key.ctrl&&input==="c")&&stop();}),loading?jsx(Loading,{}):jsx(Dashboard,{processes,selectedIndex:cursor})}var{values}=parseArgs({args:process.argv.slice(2),options:{filter:{type:"string",multiple:true},order:{type:"string",default:"alphabetical"}}}),filter=values.filter?.map(v=>v.replace(/^\{(.+)\}$/,"$1")),order=values.order==="run"?"run":"alphabetical",options={root:process.cwd(),order,filter:filter?.length?filter:void 0},{waitUntilExit}=render(jsx(App,{options}),{exitOnCtrlC:false});await waitUntilExit();process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hlidskjalf",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Terminal
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "A Terminal User Interface for monitoring Turborepo tasks, built with Ink.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -24,17 +24,19 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup",
|
|
26
26
|
"dev": "tsup --watch",
|
|
27
|
-
"lint": "biome check
|
|
28
|
-
"lint:fix": "biome check --
|
|
29
|
-
"format": "biome format --
|
|
27
|
+
"lint": "biome check .",
|
|
28
|
+
"lint:fix": "biome check --write .",
|
|
29
|
+
"format": "biome format --write ."
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"ink": "^5.2.1",
|
|
33
33
|
"react": "^18.3.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
+
"@biomejs/biome": "^2.4.7",
|
|
36
37
|
"@types/node": "^22.15.33",
|
|
37
38
|
"@types/react": "^18.3.23",
|
|
39
|
+
"lefthook": "^2.1.4",
|
|
38
40
|
"tsup": "^8.5.1",
|
|
39
41
|
"typescript": "^5.9.3"
|
|
40
42
|
}
|