accelr8 3.0.1 → 3.0.3

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/protocol.ts
4
- var VERSION = "3.0.0";
4
+ var VERSION = true ? "3.0.3" : "3.0.2";
5
5
  var PROTO = 3;
6
6
  var PORT = 8787;
7
7
  var DEFAULT_SERVER = "wss://accelr8-canvas.patonchain.workers.dev";
@@ -7,7 +7,7 @@ import {
7
7
  VERSION,
8
8
  decode,
9
9
  encode
10
- } from "./chunk-GXDRLI6N.js";
10
+ } from "./chunk-SY2ASAWX.js";
11
11
  import {
12
12
  DEFAULT_MODEL,
13
13
  addAccount,
@@ -1181,6 +1181,7 @@ var SAMPLE_RATE = 16e3;
1181
1181
  var CHUNK_BYTES = 1280;
1182
1182
  var KILL_GRACE_MS = 300;
1183
1183
  var SPEAKER_IDLE_MS = 2e3;
1184
+ var PREBUFFER_BYTES = 4800;
1184
1185
  var DRAIN_KILL_MS = 2e3;
1185
1186
  var MAX_PLAYERS = 8;
1186
1187
  var STDERR_TAIL = 400;
@@ -1190,7 +1191,9 @@ var PLAYBACK_ARGS = [
1190
1191
  "-loglevel",
1191
1192
  "error",
1192
1193
  "-nodisp",
1193
- "-autoexit",
1194
+ "-sync",
1195
+ "audio",
1196
+ "-infbuf",
1194
1197
  "-f",
1195
1198
  "s16le",
1196
1199
  "-ar",
@@ -1352,16 +1355,35 @@ var VoicePlayback = class {
1352
1355
  if (!spawned) return;
1353
1356
  p = spawned;
1354
1357
  }
1355
- if (!this.write(p, bytes)) {
1356
- this.reap(speakerId, p);
1357
- const fresh = this.players.size < MAX_PLAYERS ? this.spawnPlayer(speakerId) : null;
1358
- if (!fresh || !this.write(fresh, bytes)) return;
1359
- p = fresh;
1358
+ if (!p.flowing) {
1359
+ p.pre.push(bytes);
1360
+ if (p.pre.reduce((n, b) => n + b.length, 0) >= PREBUFFER_BYTES) {
1361
+ const cushion = Buffer.concat(p.pre);
1362
+ p.pre = [];
1363
+ p.flowing = true;
1364
+ const np2 = this.writeOrRespawn(speakerId, p, cushion);
1365
+ if (np2) this.touch(speakerId, np2);
1366
+ } else {
1367
+ this.touch(speakerId, p);
1368
+ }
1369
+ return;
1360
1370
  }
1361
- this.touch(speakerId, p);
1362
- }
1363
- /** Graceful close for one speaker (voiceStop): end stdin so ffplay
1364
- * drains buffered audio and -autoexit's; SIGKILL backstop after 2s. */
1371
+ const np = this.writeOrRespawn(speakerId, p, bytes);
1372
+ if (np) this.touch(speakerId, np);
1373
+ }
1374
+ /** Write to a player; on EPIPE (it died) respawn once and write there. Returns
1375
+ * the live player, or null if it couldn't be (re)spawned. */
1376
+ writeOrRespawn(speakerId, p, buf) {
1377
+ if (this.write(p, buf)) return p;
1378
+ this.reap(speakerId, p);
1379
+ const fresh = this.players.size < MAX_PLAYERS ? this.spawnPlayer(speakerId) : null;
1380
+ if (!fresh || !this.write(fresh, buf)) return null;
1381
+ fresh.flowing = true;
1382
+ return fresh;
1383
+ }
1384
+ /** Graceful close for one speaker (voiceStop): end stdin so ffplay drains
1385
+ * its buffered audio, then a SIGKILL backstop after 2s (we dropped -autoexit,
1386
+ * so the backstop is what actually reaps the idle player). */
1365
1387
  end(speakerId) {
1366
1388
  const p = this.players.get(speakerId);
1367
1389
  if (!p) return;
@@ -1404,7 +1426,7 @@ var VoicePlayback = class {
1404
1426
  } catch {
1405
1427
  return null;
1406
1428
  }
1407
- const p = { child, alive: true, idleTimer: null };
1429
+ const p = { child, alive: true, idleTimer: null, pre: [], flowing: false };
1408
1430
  child.stdin?.on("error", () => {
1409
1431
  p.alive = false;
1410
1432
  });
@@ -1508,24 +1530,74 @@ var overlay = null;
1508
1530
  var accessAgentId = null;
1509
1531
  var overlayFocus = 0;
1510
1532
  var updateInfo = null;
1511
- var updating = null;
1512
- var cancelUpdate = null;
1533
+ var task = null;
1534
+ var cancelTask = null;
1513
1535
  function startUpdate() {
1514
- if (updating && !updating.done) return;
1515
- updating = { line: "starting npm\u2026", message: "" };
1536
+ if (task && task.phase === "running") return;
1537
+ task = { title: "updating accelr8", phase: "running", line: "starting npm\u2026", okLabel: "quit now \xB7 enter", onOk: () => shutdown() };
1516
1538
  import("./update-UEPNGB6Y.js").then(({ runUpdate }) => {
1517
- cancelUpdate = runUpdate({
1539
+ cancelTask = runUpdate({
1518
1540
  onLine: (l) => {
1519
- if (updating) updating.line = l.slice(0, 120);
1541
+ if (task) task.line = l.slice(0, 120);
1520
1542
  },
1521
1543
  onDone: (ok, message) => {
1522
- updating = { line: "", done: ok ? "ok" : "fail", message };
1523
- cancelUpdate = null;
1544
+ if (task) {
1545
+ task.phase = "done";
1546
+ task.ok = ok;
1547
+ task.line = message;
1548
+ }
1549
+ cancelTask = null;
1524
1550
  if (ok) updateInfo = null;
1525
1551
  }
1526
1552
  });
1527
1553
  }).catch(() => {
1528
- updating = { line: "", done: "fail", message: "could not load the updater \xB7 npm i -g accelr8@latest" };
1554
+ if (task) {
1555
+ task.phase = "done";
1556
+ task.ok = false;
1557
+ task.line = "could not load the updater \xB7 npm i -g accelr8@latest";
1558
+ }
1559
+ });
1560
+ }
1561
+ function startVoiceInstall() {
1562
+ import("./install-DL5WLVXN.js").then(({ ffmpegInstallPlan, runCommand }) => {
1563
+ const plan = ffmpegInstallPlan();
1564
+ if (!plan.auto) {
1565
+ state.status = `voice needs ffmpeg \xB7 ${plan.manual}`;
1566
+ return;
1567
+ }
1568
+ const auto = plan.auto;
1569
+ task = {
1570
+ title: "enable voice",
1571
+ phase: "confirm",
1572
+ line: `voice needs ffmpeg \xB7 ${auto.label} \xB7 press enter`,
1573
+ run: () => {
1574
+ if (!task) return;
1575
+ task.phase = "running";
1576
+ task.line = "installing ffmpeg\u2026";
1577
+ cancelTask = runCommand(auto.cmd, auto.args, {
1578
+ onLine: (l) => {
1579
+ if (task) task.line = l.slice(0, 120);
1580
+ },
1581
+ onDone: (ok, message) => {
1582
+ if (task) {
1583
+ task.phase = "done";
1584
+ task.ok = ok;
1585
+ task.line = ok ? "ffmpeg ready \xB7 voice is on" : message + " \xB7 " + auto.label;
1586
+ task.okLabel = ok ? "start voice \xB7 enter" : void 0;
1587
+ }
1588
+ cancelTask = null;
1589
+ }
1590
+ });
1591
+ },
1592
+ onOk: () => {
1593
+ voiceTools = null;
1594
+ task = null;
1595
+ toggleVoice();
1596
+ }
1597
+ // re-detect + join
1598
+ };
1599
+ }).catch(() => {
1600
+ state.status = "could not load the installer";
1529
1601
  });
1530
1602
  }
1531
1603
  var voiceTools = null;
@@ -1541,7 +1613,7 @@ function toggleVoice() {
1541
1613
  }
1542
1614
  voiceTools = voiceTools ?? detectVoiceTools();
1543
1615
  if (!voiceTools.capture && !voiceTools.playback) {
1544
- state.status = "voice needs ffmpeg \xB7 install it, then /voice";
1616
+ startVoiceInstall();
1545
1617
  return;
1546
1618
  }
1547
1619
  voicePlayback = new VoicePlayback(voiceTools);
@@ -1549,7 +1621,7 @@ function toggleVoice() {
1549
1621
  voiceOn = true;
1550
1622
  voiceTransmit = false;
1551
1623
  sendMsg({ t: "voiceJoin" });
1552
- state.status = voiceTools.capture ? "voice on \xB7 ctrl-g to talk" : "voice on \xB7 listen only (no mic found)";
1624
+ state.status = voiceTools.capture ? "voice on \xB7 /talk (or ctrl-g) to speak" : "voice on \xB7 listen only (no mic found)";
1553
1625
  }
1554
1626
  function leaveVoice() {
1555
1627
  if (!voiceOn) return;
@@ -1572,10 +1644,10 @@ function toggleTalk() {
1572
1644
  if (voiceTransmit) {
1573
1645
  voiceCapture.stop();
1574
1646
  voiceTransmit = false;
1575
- state.status = "muted \xB7 ctrl-g to talk";
1647
+ state.status = "muted \xB7 /talk (or ctrl-g) to speak";
1576
1648
  } else {
1577
1649
  voiceTransmit = true;
1578
- state.status = "\u25CF live \xB7 ctrl-g to mute";
1650
+ state.status = "\u25CF live \xB7 /mute (or ctrl-g) to stop";
1579
1651
  voiceCapture.start({
1580
1652
  onChunk: (b64) => {
1581
1653
  if (voiceTransmit) sendMsg({ t: "voiceFrame", data: b64 });
@@ -2203,6 +2275,9 @@ function __curTheme() {
2203
2275
  function __setUpdate(latest, current = "3.0.0") {
2204
2276
  updateInfo = latest ? { latest, current } : null;
2205
2277
  }
2278
+ function __taskDemo(phase, ok = true) {
2279
+ task = { title: "enable voice", phase, ok, line: phase === "confirm" ? "voice needs ffmpeg \xB7 brew install ffmpeg \xB7 press enter" : phase === "running" ? "installing ffmpeg\u2026" : ok ? "ffmpeg ready \xB7 voice is on" : "brew exited 1 \xB7 brew install ffmpeg", okLabel: "start voice \xB7 enter" };
2280
+ }
2206
2281
  function __closeOverlay() {
2207
2282
  overlay = null;
2208
2283
  }
@@ -2288,28 +2363,36 @@ function render() {
2288
2363
  overlayFocus = clickables.length ? Math.max(0, Math.min(overlayFocus, clickables.length - 1)) : 0;
2289
2364
  }
2290
2365
  if (state.modal && bigEnough) renderModal();
2291
- if (updating && bigEnough) renderUpdating();
2366
+ if (task && bigEnough) renderTask();
2292
2367
  if (screen.dst) screen.draw({ delta: true });
2293
2368
  }
2294
- function renderUpdating() {
2369
+ function renderTask() {
2295
2370
  clickables = [];
2296
2371
  const w = W(), h = H();
2297
2372
  const bw = Math.min(66, w - 6), bh = 7;
2298
2373
  const x0 = Math.floor((w - bw) / 2), y0 = Math.floor((h - bh) / 2);
2299
2374
  for (let yy = y0; yy < y0 + bh; yy++) puts(x0, yy, " ".repeat(bw), { bgColor: C.panel });
2300
- box(x0, y0, x0 + bw - 1, y0 + bh - 1, { color: C.accent, bgColor: C.panel }, "updating accelr8");
2301
- const u = updating;
2302
- if (!u.done) {
2303
- puts(x0 + 2, y0 + 2, `${SPINNER[frame % SPINNER.length]} installing accelr8@latest\u2026`, { color: C.bright, bgColor: C.panel, bold: true });
2304
- puts(x0 + 2, y0 + 4, ("\xB7 " + u.line).slice(0, bw - 4), { color: C.dim, bgColor: C.panel });
2305
- } else if (u.done === "ok") {
2306
- puts(x0 + 2, y0 + 2, "\u2713 " + u.message, { color: C.good, bgColor: C.panel, bold: true });
2307
- button(x0 + 2, y0 + 4, " quit now \xB7 enter ", { bgColor: C.good, color: onFill(C.good), bold: true }, () => shutdown());
2308
- puts(x0 + 22, y0 + 4, "then run: accelr8", { color: C.dim, bgColor: C.panel });
2375
+ box(x0, y0, x0 + bw - 1, y0 + bh - 1, { color: C.accent, bgColor: C.panel }, task.title);
2376
+ const t = task;
2377
+ if (t.phase === "confirm") {
2378
+ puts(x0 + 2, y0 + 2, t.line.slice(0, bw - 4), { color: C.bright, bgColor: C.panel, bold: true });
2379
+ button(x0 + 2, y0 + 4, " install \xB7 enter ", { bgColor: C.accent, color: onFill(C.accent), bold: true }, () => t.run?.());
2380
+ button(x0 + 20, y0 + 4, " not now \xB7 esc ", { bgColor: C.tabOff, color: C.text }, () => {
2381
+ task = null;
2382
+ });
2383
+ } else if (t.phase === "running") {
2384
+ puts(x0 + 2, y0 + 2, `${SPINNER[frame % SPINNER.length]} ${t.title}\u2026`, { color: C.bright, bgColor: C.panel, bold: true });
2385
+ puts(x0 + 2, y0 + 4, ("\xB7 " + t.line).slice(0, bw - 4), { color: C.dim, bgColor: C.panel });
2386
+ } else if (t.ok) {
2387
+ puts(x0 + 2, y0 + 2, "\u2713 " + t.line.slice(0, bw - 6), { color: C.good, bgColor: C.panel, bold: true });
2388
+ if (t.okLabel) button(x0 + 2, y0 + 4, ` ${t.okLabel} `, { bgColor: C.good, color: onFill(C.good), bold: true }, () => t.onOk?.());
2389
+ else button(x0 + 2, y0 + 4, " close \xB7 enter ", { bgColor: C.good, color: onFill(C.good), bold: true }, () => {
2390
+ task = null;
2391
+ });
2309
2392
  } else {
2310
- puts(x0 + 2, y0 + 2, "\u2715 " + u.message.slice(0, bw - 6), { color: C.danger, bgColor: C.panel, bold: true });
2393
+ puts(x0 + 2, y0 + 2, "\u2715 " + t.line.slice(0, bw - 6), { color: C.danger, bgColor: C.panel, bold: true });
2311
2394
  button(x0 + 2, y0 + 4, " close \xB7 esc ", { bgColor: C.tabOff, color: C.text }, () => {
2312
- updating = null;
2395
+ task = null;
2313
2396
  });
2314
2397
  }
2315
2398
  }
@@ -2591,7 +2674,7 @@ function renderHome(rung) {
2591
2674
  }
2592
2675
  y += 1;
2593
2676
  }
2594
- if (updateInfo && !updating) {
2677
+ if (updateInfo && !task) {
2595
2678
  puts(2, y, "\u21BB", { color: C.warn, bold: true });
2596
2679
  const w2 = W();
2597
2680
  const label = ` update to v${updateInfo.latest} \xB7 press enter `;
@@ -2652,7 +2735,7 @@ function renderStatusBar(rung) {
2652
2735
  const drawn = left.slice(0, w - 24);
2653
2736
  puts(0, sy, drawn, { bgColor: C.panel, color: C.dim });
2654
2737
  if (voiceOn) {
2655
- const label = voiceTransmit ? " \u25CF live \xB7 ctrl-g mute" : ` \u25CB voice ${state.voicePeers.length} \xB7 ctrl-g talk`;
2738
+ const label = voiceTransmit ? " \u25CF live \xB7 /mute" : ` \u25CB voice ${state.voicePeers.length} \xB7 /talk`;
2656
2739
  if (drawn.length + label.length < w - 24) puts(drawn.length, sy, label, { bgColor: C.panel, color: voiceTransmit ? C.good : C.accent, bold: voiceTransmit });
2657
2740
  }
2658
2741
  const hint = state.modal ? "[enter] ok \xB7 [esc] cancel " : "[tab] tabs \xB7 [^C] quit ";
@@ -3333,7 +3416,7 @@ function onMouse(name, data) {
3333
3416
  return;
3334
3417
  }
3335
3418
  if (state.modal) return;
3336
- if (updating) {
3419
+ if (task) {
3337
3420
  if (name === "MOUSE_LEFT_BUTTON_PRESSED") {
3338
3421
  for (const r of clickables) if (mx >= r.x0 && mx <= r.x1 && my >= r.y0 && my <= r.y1) {
3339
3422
  r.action();
@@ -3479,25 +3562,27 @@ function sendChat() {
3479
3562
  }
3480
3563
  function onKey(name) {
3481
3564
  if (name === "CTRL_C") return shutdown();
3482
- if (name === "CTRL_G" && voiceOn) return toggleTalk();
3565
+ if ((name === "CTRL_G" || name === "CTRL_ALT_G") && voiceOn) return toggleTalk();
3483
3566
  if (boot) return skipBoot();
3484
3567
  const rung = rungFor(W(), H());
3485
3568
  if (rung === "micro" || rung === "orb") return;
3486
3569
  const paused = rung === "ticker";
3487
- if (updating && !paused) {
3488
- if (!updating.done) {
3489
- if (name === "ESCAPE") {
3490
- cancelUpdate?.();
3491
- updating = null;
3492
- }
3570
+ if (task && !paused) {
3571
+ const enter = name === "ENTER" || name === "KP_ENTER";
3572
+ if (task.phase === "confirm") {
3573
+ if (enter) task.run?.();
3574
+ else if (name === "ESCAPE") task = null;
3493
3575
  return;
3494
3576
  }
3495
- if (updating.done === "ok") {
3496
- if (name === "ENTER" || name === "KP_ENTER" || name === "q") return shutdown();
3497
- if (name === "ESCAPE") updating = null;
3577
+ if (task.phase === "running") {
3578
+ if (name === "ESCAPE") {
3579
+ cancelTask?.();
3580
+ task = null;
3581
+ }
3498
3582
  return;
3499
3583
  }
3500
- if (name === "ESCAPE" || name === "ENTER" || name === "KP_ENTER") updating = null;
3584
+ if (enter && task.ok && task.onOk) return task.onOk();
3585
+ if (enter || name === "ESCAPE") task = null;
3501
3586
  return;
3502
3587
  }
3503
3588
  if (state.modal && !paused) {
@@ -3807,6 +3892,7 @@ export {
3807
3892
  __setMySid,
3808
3893
  __setNav,
3809
3894
  __setUpdate,
3895
+ __taskDemo,
3810
3896
  chatLog,
3811
3897
  cursors,
3812
3898
  editor,
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/install.ts
4
+ import { spawn } from "node:child_process";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ function which(bin) {
8
+ const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
9
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
10
+ for (const ext of exts) {
11
+ const p = path.join(dir, bin + ext);
12
+ try {
13
+ fs.accessSync(p, fs.constants.X_OK);
14
+ if (fs.statSync(p).isFile()) return true;
15
+ } catch {
16
+ }
17
+ }
18
+ }
19
+ return false;
20
+ }
21
+ function ffmpegInstallPlan(platform = process.platform) {
22
+ if (platform === "darwin") {
23
+ if (which("brew")) return { auto: { cmd: "brew", args: ["install", "ffmpeg"], label: "brew install ffmpeg" }, manual: "brew install ffmpeg" };
24
+ return { manual: "install Homebrew from brew.sh, then: brew install ffmpeg", note: "Homebrew not found" };
25
+ }
26
+ if (platform === "win32") {
27
+ if (which("winget")) return { auto: { cmd: "winget", args: ["install", "--silent", "--accept-package-agreements", "--accept-source-agreements", "Gyan.FFmpeg"], label: "winget install ffmpeg" }, manual: "winget install Gyan.FFmpeg" };
28
+ if (which("choco")) return { auto: { cmd: "choco", args: ["install", "-y", "ffmpeg-full"], label: "choco install ffmpeg-full" }, manual: "choco install ffmpeg-full" };
29
+ return { manual: "install ffmpeg from ffmpeg.org and add it to PATH", note: "no winget/choco found" };
30
+ }
31
+ if (which("apt-get") || which("apt")) return { manual: "sudo apt install ffmpeg", note: "needs sudo \u2014 run it in your shell" };
32
+ if (which("dnf")) return { manual: "sudo dnf install ffmpeg", note: "needs sudo \u2014 run it in your shell" };
33
+ if (which("pacman")) return { manual: "sudo pacman -S ffmpeg", note: "needs sudo \u2014 run it in your shell" };
34
+ return { manual: "install ffmpeg with your package manager", note: "unknown package manager" };
35
+ }
36
+ function runCommand(cmd, args, cb) {
37
+ let child;
38
+ try {
39
+ child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
40
+ } catch (e) {
41
+ queueMicrotask(() => cb.onDone(false, `couldn't start ${cmd}`));
42
+ return () => {
43
+ };
44
+ }
45
+ let tail = "";
46
+ const feed = (chunk) => {
47
+ tail += chunk.toString();
48
+ let nl;
49
+ while ((nl = tail.indexOf("\n")) >= 0) {
50
+ const line = tail.slice(0, nl).trim();
51
+ tail = tail.slice(nl + 1);
52
+ if (line) cb.onLine(line);
53
+ }
54
+ };
55
+ child.stdout?.on("data", feed);
56
+ child.stderr?.on("data", feed);
57
+ child.on("error", (e) => cb.onDone(false, e.code === "ENOENT" ? `${cmd} not found` : e.message));
58
+ child.on("close", (code) => {
59
+ if (tail.trim()) cb.onLine(tail.trim());
60
+ cb.onDone(code === 0, code === 0 ? "installed" : `${cmd} exited ${code}`);
61
+ });
62
+ return () => {
63
+ try {
64
+ child.kill();
65
+ } catch {
66
+ }
67
+ };
68
+ }
69
+ export {
70
+ ffmpegInstallPlan,
71
+ runCommand
72
+ };
@@ -6,7 +6,7 @@ import {
6
6
  LIMITS,
7
7
  PORT,
8
8
  PROTO
9
- } from "./chunk-GXDRLI6N.js";
9
+ } from "./chunk-SY2ASAWX.js";
10
10
 
11
11
  // src/server.ts
12
12
  import { WebSocketServer, WebSocket } from "ws";
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  DEFAULT_SERVER,
4
4
  PORT,
5
5
  VERSION
6
- } from "./chunks/chunk-GXDRLI6N.js";
6
+ } from "./chunks/chunk-SY2ASAWX.js";
7
7
 
8
8
  // src/cli.ts
9
9
  var HELP = `
@@ -52,7 +52,7 @@ async function main() {
52
52
  console.error(`invalid port: ${argVal(args, "--port")}`);
53
53
  process.exit(1);
54
54
  }
55
- const { runServer } = await import("./chunks/server-NWTBQYCW.js");
55
+ const { runServer } = await import("./chunks/server-IVR5UHTM.js");
56
56
  runServer(port);
57
57
  return;
58
58
  }
@@ -76,7 +76,7 @@ async function main() {
76
76
  const { cachedUpdate, refreshUpdateCache } = await import("./chunks/update-UEPNGB6Y.js");
77
77
  refreshUpdateCache();
78
78
  const update = cachedUpdate(VERSION);
79
- const { runClient } = await import("./chunks/client-36OXOMLV.js");
79
+ const { runClient } = await import("./chunks/client-HLMFYYAQ.js");
80
80
  runClient({
81
81
  name: argVal(args, "--name"),
82
82
  server: argVal(args, "--server"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "accelr8",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "Collaborative ascii canvas in your terminal. Real multiplayer, live cursors, mouse-driven TUI.",
5
5
  "type": "module",
6
6
  "license": "ISC",
@@ -42,7 +42,7 @@
42
42
  "chattest": "tsx src/chattest.ts",
43
43
  "inputtest": "tsx src/inputtest.ts",
44
44
  "torturetest": "tsx src/torturetest.ts",
45
- "test": "for t in core chat input ladder boot orb crypto accounts executor agentcontext map voice glyph materialize update contrast; do tsx src/${t}test.ts || exit 1; done && tsx src/rendertest.ts > /dev/null && echo RENDER OK",
45
+ "test": "for t in core chat input ladder boot orb crypto accounts executor agentcontext map voice glyph materialize update install contrast; do tsx src/${t}test.ts || exit 1; done && tsx src/rendertest.ts > /dev/null && echo RENDER OK",
46
46
  "laddertest": "tsx src/laddertest.ts",
47
47
  "boottest": "tsx src/boottest.ts",
48
48
  "reconnecttest": "tsx src/reconnecttest.ts",