hlidskjalf 0.0.1

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 (3) hide show
  1. package/README.md +28 -0
  2. package/dist/index.js +493 -0
  3. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # hlidskjalf
2
+
3
+ A terminal dashboard for monitoring Turborepo dev processes, built with [Ink](https://npm.im/ink).
4
+
5
+ ## Usage
6
+
7
+ Add it to your root `package.json`:
8
+
9
+ ```json
10
+ {
11
+ "scripts": {
12
+ "dev": "hlidskjalf"
13
+ }
14
+ }
15
+ ```
16
+
17
+ Then run it:
18
+
19
+ ```sh
20
+ pnpm dev
21
+ ```
22
+
23
+ ## Options
24
+
25
+ | Option | Description |
26
+ | --- | --- |
27
+ | `--filter=<name>` | Only include matching workspaces. Can be passed multiple times. Append `...` to include transitive dependencies (e.g. `--filter=web...`). |
28
+ | `--order=<mode>` | `alphabetical` (default) or `run` (dependency order). |
package/dist/index.js ADDED
@@ -0,0 +1,493 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.tsx
4
+ import { render } from "ink";
5
+
6
+ // src/app.tsx
7
+ import { useApp, useInput } from "ink";
8
+ import { useCallback, useEffect, useRef, useState } from "react";
9
+
10
+ // src/processes.ts
11
+ import { spawn } from "child_process";
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 === "app");
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
+ };
242
+ var statusDisplay = {
243
+ pending: { color: "gray", label: "pending" },
244
+ building: { color: "yellow", label: "building" },
245
+ watching: { color: "green", label: "watching" },
246
+ ready: { color: "green", label: "ready" },
247
+ error: { color: "red", label: "error" },
248
+ stopped: { color: "gray", label: "stopped" }
249
+ };
250
+ var HINTS = "\u2191/\u2193 j/k select q quit";
251
+ function Dashboard({ processes, selectedIndex }) {
252
+ const { stdout } = useStdout();
253
+ const cols = stdout?.columns ?? 80;
254
+ const rows = stdout?.rows ?? 24;
255
+ const allReady = processes.length > 0 && processes.every((p) => p.status === "ready" || p.status === "watching");
256
+ const nameWidth = Math.max(14, ...processes.map((p) => p.workspace.name.length + 2));
257
+ const logHeight = Math.max(3, rows - processes.length - 5);
258
+ const safeIndex = Math.min(selectedIndex, Math.max(0, processes.length - 1));
259
+ const selected = processes[safeIndex];
260
+ const logLines = selected?.logs.slice(-logHeight) ?? [];
261
+ const showHints = cols >= 10 + HINTS.length + 4;
262
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
263
+ /* @__PURE__ */ jsxs(Box, { children: [
264
+ /* @__PURE__ */ jsxs(Box, { flexGrow: 1, children: [
265
+ /* @__PURE__ */ jsx(Text, { color: allReady ? "yellow" : "gray", children: allReady ? "\u{1F3D4}" : "\u25E6" }),
266
+ /* @__PURE__ */ jsx(Text, { bold: true, children: allReady ? " Hlidskjalf" : " Hlidskjalf" })
267
+ ] }),
268
+ showHints && /* @__PURE__ */ jsx(Text, { dimColor: true, children: HINTS })
269
+ ] }),
270
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(cols) }),
271
+ /* @__PURE__ */ jsxs(Box, { marginLeft: 1, children: [
272
+ /* @__PURE__ */ jsx(Box, { width: nameWidth, children: /* @__PURE__ */ jsx(Text, { dimColor: true, bold: true, children: "Name" }) }),
273
+ /* @__PURE__ */ jsx(Box, { width: 6, children: /* @__PURE__ */ jsx(Text, { dimColor: true, bold: true, children: "Kind" }) }),
274
+ /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, bold: true, children: "Status" }) }),
275
+ /* @__PURE__ */ jsx(Text, { dimColor: true, bold: true, children: "URL" })
276
+ ] }),
277
+ processes.map((proc, i) => {
278
+ const isSelected = i === safeIndex;
279
+ const { color, label } = statusDisplay[proc.status];
280
+ return /* @__PURE__ */ jsxs(Box, { children: [
281
+ /* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u25B8" : " " }),
282
+ /* @__PURE__ */ jsx(Box, { width: nameWidth, children: /* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : void 0, bold: isSelected, wrap: "truncate", children: proc.workspace.name }) }),
283
+ /* @__PURE__ */ jsx(Box, { width: 6, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: kindLabel[proc.workspace.kind] }) }),
284
+ /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsxs(Text, { color, children: [
285
+ "\u25CF ",
286
+ label
287
+ ] }) }),
288
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: proc.url ?? "" })
289
+ ] }, proc.workspace.name);
290
+ }),
291
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(cols) }),
292
+ selected && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
293
+ /* @__PURE__ */ jsx(Box, { marginLeft: 1, children: /* @__PURE__ */ jsxs(Text, { bold: true, children: [
294
+ "Logs: ",
295
+ selected.workspace.name
296
+ ] }) }),
297
+ logLines.map((line, i) => (
298
+ // biome-ignore lint/suspicious/noArrayIndexKey: log lines have no stable identity
299
+ /* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
300
+ " ",
301
+ line
302
+ ] }, i)
303
+ )),
304
+ Array.from({ length: logHeight - logLines.length }, (_, i) => (
305
+ // biome-ignore lint/suspicious/noArrayIndexKey: fill lines have no stable identity
306
+ /* @__PURE__ */ jsx(Text, { children: " " }, `fill-${i}`)
307
+ ))
308
+ ] })
309
+ ] });
310
+ }
311
+
312
+ // src/views/loading.tsx
313
+ import { Box as Box2, Text as Text2, useStdout as useStdout2 } from "ink";
314
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
315
+ function Loading() {
316
+ const { stdout } = useStdout2();
317
+ const cols = stdout?.columns ?? 80;
318
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
319
+ /* @__PURE__ */ jsxs2(Box2, { children: [
320
+ /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "\u25E6 " }),
321
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: "Hlidskjalf" })
322
+ ] }),
323
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(cols) }),
324
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, marginLeft: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Discovering workspaces..." }) })
325
+ ] });
326
+ }
327
+
328
+ // src/workspaces.ts
329
+ import { existsSync, readdirSync, readFileSync } from "fs";
330
+ import { join } from "path";
331
+ function readJson(path) {
332
+ try {
333
+ return JSON.parse(readFileSync(path, "utf-8"));
334
+ } catch {
335
+ return null;
336
+ }
337
+ }
338
+ function workspaceDeps(pkg) {
339
+ return Object.entries(pkg.dependencies ?? {}).filter(([, v]) => v.startsWith("workspace:")).map(([name]) => name);
340
+ }
341
+ var kindOrder = { package: 0, app: 1 };
342
+ function discover(root) {
343
+ const results = [];
344
+ for (const dir of ["packages", "apps"]) {
345
+ const base = join(root, dir);
346
+ const kind = dir === "apps" ? "app" : "package";
347
+ if (!existsSync(base)) continue;
348
+ for (const entry of readdirSync(base, { withFileTypes: true })) {
349
+ if (!entry.isDirectory()) continue;
350
+ const pkg = readJson(join(base, entry.name, "package.json"));
351
+ if (!pkg?.name) continue;
352
+ if (pkg.name === "hlidskjalf") continue;
353
+ if (!pkg.scripts?.dev) continue;
354
+ results.push({
355
+ name: pkg.name,
356
+ kind,
357
+ deps: workspaceDeps(pkg)
358
+ });
359
+ }
360
+ }
361
+ return results;
362
+ }
363
+ function sortByDeps(workspaces) {
364
+ const names = new Set(workspaces.map((w) => w.name));
365
+ return [...workspaces].sort((a, b) => {
366
+ if (a.kind !== b.kind) return kindOrder[a.kind] - kindOrder[b.kind];
367
+ const aDeps = a.deps.filter((d) => names.has(d)).length;
368
+ const bDeps = b.deps.filter((d) => names.has(d)).length;
369
+ return aDeps - bDeps;
370
+ });
371
+ }
372
+ function sortByName(workspaces) {
373
+ return [...workspaces].sort((a, b) => {
374
+ if (a.kind !== b.kind) return kindOrder[a.kind] - kindOrder[b.kind];
375
+ return a.name.localeCompare(b.name);
376
+ });
377
+ }
378
+ function filterWorkspaces(workspaces, patterns) {
379
+ const byName = new Map(workspaces.map((w) => [w.name, w]));
380
+ const matches = /* @__PURE__ */ new Set();
381
+ for (const pattern of patterns) {
382
+ const transitive = pattern.endsWith("...");
383
+ const name = transitive ? pattern.slice(0, -3) : pattern;
384
+ if (byName.has(name)) matches.add(name);
385
+ if (transitive) collectDeps(name, byName, matches);
386
+ }
387
+ return workspaces.filter((w) => matches.has(w.name));
388
+ }
389
+ function collectDeps(name, byName, collected) {
390
+ const workspace = byName.get(name);
391
+ if (!workspace) return;
392
+ for (const dep of workspace.deps) {
393
+ if (byName.has(dep) && !collected.has(dep)) {
394
+ collected.add(dep);
395
+ collectDeps(dep, byName, collected);
396
+ }
397
+ }
398
+ }
399
+
400
+ // src/app.tsx
401
+ import { jsx as jsx3 } from "react/jsx-runtime";
402
+ function App({ options }) {
403
+ const { exit } = useApp();
404
+ const [loading, setLoading] = useState(true);
405
+ const [processes, setProcesses] = useState([]);
406
+ const [cursor, setCursor] = useState(0);
407
+ const runnerRef = useRef(null);
408
+ const stoppingRef = useRef(false);
409
+ const stop = useCallback(() => {
410
+ if (stoppingRef.current) return;
411
+ stoppingRef.current = true;
412
+ const runner = runnerRef.current;
413
+ if (runner) {
414
+ void runner.shutdown().finally(() => exit());
415
+ } else {
416
+ exit();
417
+ }
418
+ }, [exit]);
419
+ useEffect(() => {
420
+ const run = async () => {
421
+ let workspaces = discover(options.root);
422
+ if (options.filter) {
423
+ workspaces = filterWorkspaces(workspaces, options.filter);
424
+ }
425
+ if (workspaces.length === 0) {
426
+ console.error("No matching workspaces found.");
427
+ exit();
428
+ return;
429
+ }
430
+ const startOrder = sortByDeps(workspaces);
431
+ const sorted = options.order === "run" ? startOrder : sortByName(workspaces);
432
+ const displayOrder = sorted.map((w) => w.name);
433
+ const runner = createRunner(options.root);
434
+ runnerRef.current = runner;
435
+ setProcesses(sorted.map((w) => ({ workspace: w, status: "pending", logs: [] })));
436
+ runner.on("change", () => {
437
+ setProcesses(
438
+ displayOrder.flatMap((name) => {
439
+ const p = runner.get(name);
440
+ return p ? [p] : [];
441
+ })
442
+ );
443
+ });
444
+ setLoading(false);
445
+ await runner.start(startOrder);
446
+ };
447
+ run().catch((err) => {
448
+ console.error("Fatal:", err);
449
+ exit();
450
+ });
451
+ process.on("SIGTERM", stop);
452
+ return () => {
453
+ process.off("SIGTERM", stop);
454
+ };
455
+ }, [exit, options.filter, options.order, options.root, stop]);
456
+ useInput((input, key) => {
457
+ if (loading) return;
458
+ if (input === "q" || key.ctrl && input === "c") {
459
+ stop();
460
+ return;
461
+ }
462
+ if (key.upArrow || input === "k") {
463
+ setCursor((i) => Math.max(0, i - 1));
464
+ } else if (key.downArrow || input === "j") {
465
+ setCursor((i) => Math.min(processes.length - 1, i + 1));
466
+ }
467
+ });
468
+ if (loading) return /* @__PURE__ */ jsx3(Loading, {});
469
+ return /* @__PURE__ */ jsx3(Dashboard, { processes, selectedIndex: cursor });
470
+ }
471
+
472
+ // src/index.tsx
473
+ import { jsx as jsx4 } from "react/jsx-runtime";
474
+ function parseArgs(argv) {
475
+ const root = process.cwd();
476
+ const filter = [];
477
+ let order = "alphabetical";
478
+ for (const arg of argv) {
479
+ if (arg.startsWith("--filter=")) {
480
+ const value = arg.slice("--filter=".length).replace(/^\{(.+)\}$/, "$1");
481
+ filter.push(value);
482
+ } else if (arg.startsWith("--order=")) {
483
+ const value = arg.slice("--order=".length);
484
+ if (value === "run" || value === "alphabetical") order = value;
485
+ }
486
+ }
487
+ return { root, order, filter: filter.length > 0 ? filter : void 0 };
488
+ }
489
+ var { waitUntilExit } = render(/* @__PURE__ */ jsx4(App, { options: parseArgs(process.argv.slice(2)) }), {
490
+ exitOnCtrlC: false
491
+ });
492
+ await waitUntilExit();
493
+ process.exit(0);
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "hlidskjalf",
3
+ "version": "0.0.1",
4
+ "description": "Terminal UI for monitoring Turborepo workspaces",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "turborepo",
9
+ "monorepo",
10
+ "terminal",
11
+ "tui",
12
+ "dev",
13
+ "pnpm"
14
+ ],
15
+ "engines": {
16
+ "node": ">=22"
17
+ },
18
+ "bin": {
19
+ "hlidskjalf": "dist/index.js"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "lint": "biome check --config-path=../.. .",
28
+ "lint:fix": "biome check --config-path=../.. --write .",
29
+ "format": "biome format --config-path=../.. --write ."
30
+ },
31
+ "dependencies": {
32
+ "ink": "^5.2.1",
33
+ "react": "^18.3.1"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.15.33",
37
+ "@types/react": "^18.3.23",
38
+ "tsup": "^8.5.1",
39
+ "typescript": "^5.9.3"
40
+ }
41
+ }