@appchy/jarvis 0.1.62 → 0.1.64

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/dist/bin.js CHANGED
@@ -197,19 +197,19 @@ function startScanner(options) {
197
197
  const trimmed = rel.trim();
198
198
  if (!trimmed) continue;
199
199
  const abs = path3.join(repo.path, trimmed);
200
- let stat3;
200
+ let stat4;
201
201
  try {
202
- stat3 = fs3.statSync(abs);
202
+ stat4 = fs3.statSync(abs);
203
203
  } catch {
204
204
  continue;
205
205
  }
206
- if (!stat3.isFile()) continue;
206
+ if (!stat4.isFile()) continue;
207
207
  const ext = path3.extname(trimmed).slice(1).toLowerCase() || null;
208
208
  items.push({
209
209
  repoPath: repo.path,
210
210
  path: trimmed,
211
211
  language: ext,
212
- size: stat3.size,
212
+ size: stat4.size,
213
213
  preview: readPreview(abs)
214
214
  });
215
215
  if (items.length >= BATCH_CAP) break;
@@ -401,20 +401,20 @@ function walkSessions(dir) {
401
401
  const out = [];
402
402
  for (const name of entries) {
403
403
  const p = path3.join(dir, name);
404
- let stat3;
404
+ let stat4;
405
405
  try {
406
- stat3 = fs3.statSync(p);
406
+ stat4 = fs3.statSync(p);
407
407
  } catch {
408
408
  continue;
409
409
  }
410
- if (stat3.isDirectory()) {
410
+ if (stat4.isDirectory()) {
411
411
  out.push(...walkSessions(p));
412
412
  continue;
413
413
  }
414
414
  if (!name.endsWith(".jsonl") && !name.endsWith(".json")) continue;
415
415
  out.push({
416
416
  filePath: p,
417
- mtimeMs: stat3.mtimeMs,
417
+ mtimeMs: stat4.mtimeMs,
418
418
  messageCount: countLines(p)
419
419
  });
420
420
  }
@@ -748,7 +748,7 @@ function startIpcServer(socketPath, userId) {
748
748
  };
749
749
  }
750
750
  async function callDaemonIpc(socketPath, method, params, timeoutMs = 5e3) {
751
- return new Promise((resolve10, reject) => {
751
+ return new Promise((resolve11, reject) => {
752
752
  const conn = net.connect(socketPath);
753
753
  let buf = "";
754
754
  let settled = false;
@@ -780,7 +780,7 @@ async function callDaemonIpc(socketPath, method, params, timeoutMs = 5e3) {
780
780
  if (parsed.error) {
781
781
  finish(() => reject(new Error(parsed.error?.message ?? "ipc error")));
782
782
  } else {
783
- finish(() => resolve10(parsed.result));
783
+ finish(() => resolve11(parsed.result));
784
784
  }
785
785
  } catch (err) {
786
786
  finish(() => reject(err instanceof Error ? err : new Error(String(err))));
@@ -1215,10 +1215,10 @@ function humanizeAgo(iso) {
1215
1215
  }
1216
1216
  function readlineQuestion(req) {
1217
1217
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1218
- return new Promise((resolve10) => {
1218
+ return new Promise((resolve11) => {
1219
1219
  rl.question(req.prompt, (answer) => {
1220
1220
  rl.close();
1221
- resolve10(answer);
1221
+ resolve11(answer);
1222
1222
  });
1223
1223
  });
1224
1224
  }
@@ -1242,8 +1242,8 @@ var TOOL_DIRS = [
1242
1242
  "/opt/homebrew/bin",
1243
1243
  "/usr/local/bin"
1244
1244
  ];
1245
- function runnable(bin, spawn3) {
1246
- const probe = spawn3(bin, ["--help"], { stdio: "ignore" });
1245
+ function runnable(bin, spawn4) {
1246
+ const probe = spawn4(bin, ["--help"], { stdio: "ignore" });
1247
1247
  return probe.error?.code !== "ENOENT";
1248
1248
  }
1249
1249
  function isExecutable(path22) {
@@ -1255,9 +1255,9 @@ function isExecutable(path22) {
1255
1255
  }
1256
1256
  }
1257
1257
  function locate(bin, options) {
1258
- const spawn3 = options.spawn ?? spawnSync;
1258
+ const spawn4 = options.spawn ?? spawnSync;
1259
1259
  const executable = options.executable ?? isExecutable;
1260
- if (runnable(bin, spawn3)) return { command: bin, strayDir: null };
1260
+ if (runnable(bin, spawn4)) return { command: bin, strayDir: null };
1261
1261
  for (const dir of TOOL_DIRS) {
1262
1262
  if (executable(join(dir, bin))) return { command: join(dir, bin), strayDir: dir };
1263
1263
  }
@@ -1267,10 +1267,10 @@ function reachInto(dir) {
1267
1267
  process.env.PATH = `${dir}${delimiter}${process.env.PATH ?? ""}`;
1268
1268
  }
1269
1269
  function resolveGraphify(options = {}) {
1270
- const tool = locate(BIN, options);
1270
+ const tool2 = locate(BIN, options);
1271
1271
  return {
1272
- onPath: tool.command !== null && tool.strayDir === null,
1273
- strayDir: tool.strayDir
1272
+ onPath: tool2.command !== null && tool2.strayDir === null,
1273
+ strayDir: tool2.strayDir
1274
1274
  };
1275
1275
  }
1276
1276
  function findUv(options = {}) {
@@ -1280,15 +1280,15 @@ async function confirmAtTerminal(req) {
1280
1280
  if (!process.stdin.isTTY) return false;
1281
1281
  const question = req.needsUv ? ` Install ${BIN}, and ${UV} (from ${UV_INSTALLER}) which installs it? [Y/n] ` : ` Install ${BIN} now with ${UV}? [Y/n] `;
1282
1282
  const rl = createInterface2({ input: process.stdin, output: process.stderr });
1283
- const answer = await new Promise((resolve10) => {
1284
- rl.question(question, (input) => resolve10(input.trim().toLowerCase()));
1283
+ const answer = await new Promise((resolve11) => {
1284
+ rl.question(question, (input) => resolve11(input.trim().toLowerCase()));
1285
1285
  });
1286
1286
  rl.close();
1287
1287
  return answer === "" || answer === "y" || answer === "yes";
1288
1288
  }
1289
1289
  function installGraphify(req, options = {}) {
1290
- const spawn3 = options.spawn ?? spawnSync;
1291
- const install = spawn3(req.uv, ["tool", "install", PACKAGE], { stdio: "inherit" });
1290
+ const spawn4 = options.spawn ?? spawnSync;
1291
+ const install = spawn4(req.uv, ["tool", "install", PACKAGE], { stdio: "inherit" });
1292
1292
  if (install.status !== 0) {
1293
1293
  return {
1294
1294
  installed: false,
@@ -1307,16 +1307,16 @@ function installGraphify(req, options = {}) {
1307
1307
  return { installed: true, strayDir: after.strayDir, problem: null };
1308
1308
  }
1309
1309
  function installUv(options = {}) {
1310
- const spawn3 = options.spawn ?? spawnSync;
1311
- if (!runnable("curl", spawn3)) {
1310
+ const spawn4 = options.spawn ?? spawnSync;
1311
+ if (!runnable("curl", spawn4)) {
1312
1312
  return { uv: null, problem: `curl is needed to fetch ${UV_INSTALLER} and is not installed.` };
1313
1313
  }
1314
1314
  const script = join(process.env.TMPDIR ?? "/tmp", `uv-install-${process.pid}.sh`);
1315
- const fetched = spawn3("curl", ["-LsSf", UV_INSTALLER, "-o", script], { stdio: "inherit" });
1315
+ const fetched = spawn4("curl", ["-LsSf", UV_INSTALLER, "-o", script], { stdio: "inherit" });
1316
1316
  if (fetched.status !== 0) {
1317
1317
  return { uv: null, problem: `could not download ${UV_INSTALLER}.` };
1318
1318
  }
1319
- const ran = spawn3("sh", [script], { stdio: "inherit" });
1319
+ const ran = spawn4("sh", [script], { stdio: "inherit" });
1320
1320
  if (ran.status !== 0) {
1321
1321
  return { uv: null, problem: `the uv installer did not succeed. See ${UV_INSTALLER}.` };
1322
1322
  }
@@ -2510,10 +2510,10 @@ function rootsEqual(a, b) {
2510
2510
  }
2511
2511
  async function readlineQuestion2(question) {
2512
2512
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
2513
- return new Promise((resolve10) => {
2513
+ return new Promise((resolve11) => {
2514
2514
  rl.question(question, (input) => {
2515
2515
  rl.close();
2516
- resolve10(input);
2516
+ resolve11(input);
2517
2517
  });
2518
2518
  });
2519
2519
  }
@@ -2593,8 +2593,8 @@ function defaultPrompts() {
2593
2593
  }
2594
2594
  async function promptList(question, fallback) {
2595
2595
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
2596
- const answer = await new Promise((resolve10) => {
2597
- rl.question(question, (input) => resolve10(input.trim()));
2596
+ const answer = await new Promise((resolve11) => {
2597
+ rl.question(question, (input) => resolve11(input.trim()));
2598
2598
  });
2599
2599
  rl.close();
2600
2600
  const raw = !answer ? fallback : answer.split(",").map((s) => s.trim()).filter(Boolean);
@@ -2748,8 +2748,8 @@ function register5(program) {
2748
2748
  }
2749
2749
  async function confirm(question) {
2750
2750
  const rl = createInterface4({ input: process.stdin, output: process.stdout });
2751
- const answer = await new Promise((resolve10) => {
2752
- rl.question(question, (input) => resolve10(input.trim()));
2751
+ const answer = await new Promise((resolve11) => {
2752
+ rl.question(question, (input) => resolve11(input.trim()));
2753
2753
  });
2754
2754
  rl.close();
2755
2755
  return /^y(es)?$/i.test(answer);
@@ -3020,7 +3020,7 @@ async function idOf(term) {
3020
3020
  } catch {
3021
3021
  }
3022
3022
  }
3023
- await new Promise((resolve10) => setTimeout(resolve10, 250));
3023
+ await new Promise((resolve11) => setTimeout(resolve11, 250));
3024
3024
  }
3025
3025
  throw new Error("Claude Code started but never registered a session id");
3026
3026
  }
@@ -3089,13 +3089,13 @@ async function live(req) {
3089
3089
  else options.signal.addEventListener("abort", () => term.kill(), { once: true });
3090
3090
  }
3091
3091
  rest();
3092
- await new Promise((resolve10) => {
3092
+ await new Promise((resolve11) => {
3093
3093
  term.onExit(({ exitCode, signal }) => {
3094
3094
  if (idle) clearTimeout(idle);
3095
3095
  if (closedForIdle) report2("stopped", "closed after sitting idle");
3096
3096
  else if (exitCode === 0) report2("done");
3097
3097
  else report2("failed", endedAs(exitCode, signal));
3098
- resolve10();
3098
+ resolve11();
3099
3099
  });
3100
3100
  });
3101
3101
  }
@@ -3608,13 +3608,13 @@ async function walkRoot(req) {
3608
3608
  for (const entry of entries) {
3609
3609
  if (entry.startsWith(".")) continue;
3610
3610
  const projectDir = path15.join(req.root.dir, entry);
3611
- let stat3;
3611
+ let stat4;
3612
3612
  try {
3613
- stat3 = await fs14.stat(projectDir);
3613
+ stat4 = await fs14.stat(projectDir);
3614
3614
  } catch {
3615
3615
  continue;
3616
3616
  }
3617
- if (!stat3.isDirectory()) continue;
3617
+ if (!stat4.isDirectory()) continue;
3618
3618
  let files2;
3619
3619
  try {
3620
3620
  files2 = await fs14.readdir(projectDir);
@@ -3655,18 +3655,18 @@ async function listLocalSessionIds(req = {}) {
3655
3655
  async function getSessionFile(req) {
3656
3656
  const provider2 = getSessionProvider({ path: req.path });
3657
3657
  if (!provider2) return null;
3658
- let stat3;
3658
+ let stat4;
3659
3659
  try {
3660
- stat3 = await fs14.stat(req.path);
3660
+ stat4 = await fs14.stat(req.path);
3661
3661
  } catch {
3662
3662
  return null;
3663
3663
  }
3664
- if (!stat3.isFile()) return null;
3664
+ if (!stat4.isFile()) return null;
3665
3665
  return {
3666
3666
  provider: provider2,
3667
3667
  filePath: req.path,
3668
3668
  projectDir: path15.dirname(req.path),
3669
- mtimeMs: stat3.mtimeMs
3669
+ mtimeMs: stat4.mtimeMs
3670
3670
  };
3671
3671
  }
3672
3672
  var defaultRepoCache = /* @__PURE__ */ new Map();
@@ -4498,45 +4498,10 @@ function describeAge(when) {
4498
4498
 
4499
4499
  // ../../packages/board/src/board.git.ts
4500
4500
  var run3 = promisify4(execFile4);
4501
- var FORMAT = "%x1e%H%x1f%aI%x1f%an%x1f%B";
4502
4501
  var SOURCED = "%x1e%H%x1f%S%x1f%aI%x1f%an%x1f%B";
4503
4502
  var TRAILER = "^Work-Item: ";
4504
4503
  var SEP = "";
4505
4504
  var REC = "";
4506
- async function boardCommits(root, id) {
4507
- const out = await git2(root, [
4508
- "log",
4509
- "--all",
4510
- "--reverse",
4511
- `--format=${FORMAT}`,
4512
- "-E",
4513
- `--grep=^Work-Item: ${escapeGrep(id)}$`
4514
- ]);
4515
- const commits = [];
4516
- for (const record of out.split(REC)) {
4517
- if (!record.trim()) continue;
4518
- const [sha, when, author, message] = record.split(SEP);
4519
- if (!message || !sha) continue;
4520
- const branch = await branchOf(root, sha);
4521
- commits.push({
4522
- sha,
4523
- when: when ?? "",
4524
- by: author ?? "",
4525
- message,
4526
- branch,
4527
- branchLastCommitAt: await lastCommitOn(root, branch)
4528
- });
4529
- }
4530
- return commits;
4531
- }
4532
- async function lastCommitOn(root, branch) {
4533
- return (await git2(root, ["log", "-1", "--format=%aI", branch || "HEAD"])).trim();
4534
- }
4535
- async function branchOf(root, sha) {
4536
- const out = await git2(root, ["branch", "--all", "--format=%(refname:short)", "--contains", sha]);
4537
- const first = out.split("\n").map((line) => line.trim()).filter(Boolean).filter((line) => !line.includes("HEAD ->"))[0];
4538
- return first ?? "";
4539
- }
4540
4505
  async function git2(root, argv) {
4541
4506
  try {
4542
4507
  const { stdout } = await run3("git", argv, { cwd: root, timeout: 2e4 });
@@ -4545,10 +4510,19 @@ async function git2(root, argv) {
4545
4510
  return "";
4546
4511
  }
4547
4512
  }
4548
- function escapeGrep(value) {
4549
- return value.replace(/[^A-Za-z0-9_-]/g, (c) => `\\${c}`);
4513
+ function boardHistory(root) {
4514
+ const running = walking.get(root);
4515
+ if (running) return running;
4516
+ const walk2 = readHistory(root);
4517
+ walking.set(root, walk2);
4518
+ const done = () => {
4519
+ if (walking.get(root) === walk2) walking.delete(root);
4520
+ };
4521
+ void walk2.then(done, done);
4522
+ return walk2;
4550
4523
  }
4551
- async function boardHistory(root) {
4524
+ var walking = /* @__PURE__ */ new Map();
4525
+ async function readHistory(root) {
4552
4526
  const [out, tips] = await Promise.all([
4553
4527
  git2(root, ["log", "--all", "--source", "--reverse", `--format=${SOURCED}`, "-E", `--grep=${TRAILER}`]),
4554
4528
  branchTips(root)
@@ -4591,7 +4565,8 @@ async function listHoldersByItem(root) {
4591
4565
  return toHoldersByItem(await boardHistory(root));
4592
4566
  }
4593
4567
  async function listHolders(root, id) {
4594
- return toHolders(await boardCommits(root, id));
4568
+ const byItem = await listHoldersByItem(root);
4569
+ return byItem.get(id) ?? [];
4595
4570
  }
4596
4571
 
4597
4572
  // ../../packages/board/src/board.tree.ts
@@ -4757,8 +4732,14 @@ async function listWorkItems(_ctx, req, options) {
4757
4732
  return toPage(await scanTree(options.root), req);
4758
4733
  }
4759
4734
  async function findWorkItem(_ctx, req, options) {
4760
- const all = await scanTree(options.root);
4761
- return all.find((item2) => item2.id === req.id) ?? null;
4735
+ const work = join4(options.root, "work");
4736
+ for (const base of ["versions", "backlog"]) {
4737
+ for (const file of await walk(join4(work, base))) {
4738
+ const path22 = pathIn(work, file);
4739
+ if (path22.split("/").at(-2) === req.id) return readItem(work, file);
4740
+ }
4741
+ }
4742
+ return null;
4762
4743
  }
4763
4744
  async function getWorkItem(ctx, req, options) {
4764
4745
  const item2 = await findWorkItem(ctx, req, options);
@@ -4887,20 +4868,37 @@ function harnessMessage(err) {
4887
4868
  const said = (e.stderr || e.stdout || "").trim();
4888
4869
  return said || e.message || "the work harness failed";
4889
4870
  }
4890
- async function scanTree(root) {
4871
+ function scanTree(root) {
4872
+ const running = scanning.get(root);
4873
+ if (running) return running;
4874
+ const scan2 = readTree(root);
4875
+ scanning.set(root, scan2);
4876
+ const done = () => {
4877
+ if (scanning.get(root) === scan2) scanning.delete(root);
4878
+ };
4879
+ void scan2.then(done, done);
4880
+ return scan2;
4881
+ }
4882
+ var scanning = /* @__PURE__ */ new Map();
4883
+ async function readTree(root) {
4891
4884
  const work = join4(root, "work");
4892
4885
  const items = [];
4893
4886
  for (const base of ["versions", "backlog"]) {
4894
4887
  for (const file of await walk(join4(work, base))) {
4895
- const text = await readFile(file, "utf-8");
4896
- const changedAt = (await stat(file)).mtime.toISOString().slice(0, 10);
4897
- const path22 = relative(work, file).split(sep).join("/");
4898
- const item2 = toWorkItem({ path: path22, text, changedAt });
4888
+ const item2 = await readItem(work, file);
4899
4889
  if (item2) items.push(item2);
4900
4890
  }
4901
4891
  }
4902
4892
  return items;
4903
4893
  }
4894
+ async function readItem(work, file) {
4895
+ const text = await readFile(file, "utf-8");
4896
+ const changedAt2 = (await stat(file)).mtime.toISOString().slice(0, 10);
4897
+ return toWorkItem({ path: pathIn(work, file), text, changedAt: changedAt2 });
4898
+ }
4899
+ function pathIn(work, file) {
4900
+ return relative(work, file).split(sep).join("/");
4901
+ }
4904
4902
  async function walk(dir, depth = 0) {
4905
4903
  if (depth > 6) return [];
4906
4904
  let entries;
@@ -7135,8 +7133,8 @@ function notReadyMs() {
7135
7133
  var PENDING = /* @__PURE__ */ Symbol("pending");
7136
7134
  async function readyWithin(pending) {
7137
7135
  let timer;
7138
- const timeout = new Promise((resolve10) => {
7139
- timer = setTimeout(() => resolve10(PENDING), notReadyMs());
7136
+ const timeout = new Promise((resolve11) => {
7137
+ timer = setTimeout(() => resolve11(PENDING), notReadyMs());
7140
7138
  timer.unref?.();
7141
7139
  });
7142
7140
  try {
@@ -9272,7 +9270,7 @@ function createServer(ctx, hosted = [], prompts = []) {
9272
9270
  {
9273
9271
  instructions: buildInstructions({
9274
9272
  ...typeof ctx === "function" ? {} : { ctx },
9275
- served: [...TOOLS.map((tool) => tool.name), ...hosted.map((tool) => tool.name)]
9273
+ served: [...TOOLS.map((tool2) => tool2.name), ...hosted.map((tool2) => tool2.name)]
9276
9274
  })
9277
9275
  }
9278
9276
  );
@@ -9328,7 +9326,7 @@ function createServer(ctx, hosted = [], prompts = []) {
9328
9326
  return server;
9329
9327
  }
9330
9328
  function buildInstructions(req = {}) {
9331
- const served = req.served ?? TOOLS.map((tool) => tool.name);
9329
+ const served = req.served ?? TOOLS.map((tool2) => tool2.name);
9332
9330
  const has = (prefix) => served.some((name) => name.startsWith(prefix));
9333
9331
  const lines = [
9334
9332
  "This repo, through one connection: the work it has to do, and a map of what it knows."
@@ -9636,13 +9634,13 @@ async function readFileTree(req) {
9636
9634
  if (entry && mtimeMs !== null && Date.now() - mtimeMs < TREE_FRESHNESS_MS) {
9637
9635
  return { tree: entry.tree, root: entry.root };
9638
9636
  }
9639
- let stat3;
9637
+ let stat4;
9640
9638
  try {
9641
- stat3 = await fs15.stat(req.workdir);
9639
+ stat4 = await fs15.stat(req.workdir);
9642
9640
  } catch {
9643
9641
  return { tree: [], root: null };
9644
9642
  }
9645
- if (!stat3.isDirectory()) return { tree: [], root: null };
9643
+ if (!stat4.isDirectory()) return { tree: [], root: null };
9646
9644
  const tree2 = await walkTree({ absPath: req.workdir, rootPath: req.workdir, depth: 0 });
9647
9645
  await writeTreeCacheEntry({
9648
9646
  workdir: req.workdir,
@@ -9683,15 +9681,15 @@ async function readFileContent(workdir, relPath) {
9683
9681
  if (!absPath.startsWith(resolvedRoot + path16.sep) && absPath !== resolvedRoot) {
9684
9682
  throw new FileReadError("path_outside_workdir", "path_outside_workdir");
9685
9683
  }
9686
- const stat3 = await fs15.stat(absPath);
9687
- if (!stat3.isFile()) {
9684
+ const stat4 = await fs15.stat(absPath);
9685
+ if (!stat4.isFile()) {
9688
9686
  throw new FileReadError("not_a_file", "not_a_file");
9689
9687
  }
9690
- if (stat3.size > MAX_FILE_BYTES) {
9691
- return { path: relPath, content: null, size: stat3.size, tooLarge: true };
9688
+ if (stat4.size > MAX_FILE_BYTES) {
9689
+ return { path: relPath, content: null, size: stat4.size, tooLarge: true };
9692
9690
  }
9693
9691
  const content = await fs15.readFile(absPath, "utf-8");
9694
- return { path: relPath, content, size: stat3.size };
9692
+ return { path: relPath, content, size: stat4.size };
9695
9693
  }
9696
9694
 
9697
9695
  // src/workspace.ts
@@ -10117,7 +10115,7 @@ import { createRequire as createRequire2 } from "module";
10117
10115
  var _require = createRequire2(import.meta.url);
10118
10116
  var VERSION2 = _require("../package.json").version ?? "0.0.0";
10119
10117
  var IS_DEV = String(_require("../package.json").name ?? "").endsWith("-dev");
10120
- var SHA = "f9de50c";
10118
+ var SHA = "96b2125";
10121
10119
  var BUILT = "2026-09-09";
10122
10120
  var BUILD = SHA ?? "source";
10123
10121
  var BUILD_LABEL = `${SHA ? `${VERSION2} (${SHA}${BUILT ? ` ${BUILT}` : ""})` : `${VERSION2} (source)`}${IS_DEV ? " \u2014 development build" : ""}`;
@@ -10572,7 +10570,7 @@ async function startAgent(options) {
10572
10570
  timestamp: Date.now()
10573
10571
  };
10574
10572
  publish(envCtx, "env.disconnected", { envId: options.envId, reason: "shutdown" });
10575
- await new Promise((resolve10) => setImmediate(resolve10));
10573
+ await new Promise((resolve11) => setImmediate(resolve11));
10576
10574
  }
10577
10575
  if (heartbeatTimer) clearInterval(heartbeatTimer);
10578
10576
  if (ipcServer) {
@@ -10849,10 +10847,10 @@ async function getHealth(req = {}) {
10849
10847
  async function probeSocket(url, probeMs) {
10850
10848
  const port = portOf(url);
10851
10849
  if (port === null) return { url, reachable: false };
10852
- const reachable = await new Promise((resolve10) => {
10850
+ const reachable = await new Promise((resolve11) => {
10853
10851
  const done = (answer) => {
10854
10852
  socket.destroy();
10855
- resolve10(answer);
10853
+ resolve11(answer);
10856
10854
  };
10857
10855
  const socket = net2.connect({ host: new URL(url).hostname, port }).once("connect", () => done(true)).once("error", () => done(false));
10858
10856
  socket.setTimeout(probeMs, () => done(false));
@@ -11025,10 +11023,10 @@ function removeDir(p) {
11025
11023
  }
11026
11024
  function confirm2(prompt) {
11027
11025
  const rl = createInterface5({ input: process.stdin, output: process.stdout });
11028
- return new Promise((resolve10) => {
11026
+ return new Promise((resolve11) => {
11029
11027
  rl.question(prompt, (answer) => {
11030
11028
  rl.close();
11031
- resolve10(/^y(es)?$/i.test(answer.trim()));
11029
+ resolve11(/^y(es)?$/i.test(answer.trim()));
11032
11030
  });
11033
11031
  });
11034
11032
  }
@@ -11868,16 +11866,16 @@ function describeState(verb, response) {
11868
11866
 
11869
11867
  // src/serve/hosted.ts
11870
11868
  import crypto6 from "crypto";
11871
- function toHostedTool(tool) {
11869
+ function toHostedTool(tool2) {
11872
11870
  return {
11873
- name: tool.name,
11874
- title: tool.title ?? tool.name,
11875
- description: tool.description,
11876
- inputSchema: toShape(tool.input),
11877
- annotations: { readOnlyHint: tool.metadata?.readOnly === true },
11871
+ name: tool2.name,
11872
+ title: tool2.title ?? tool2.name,
11873
+ description: tool2.description,
11874
+ inputSchema: toShape(tool2.input),
11875
+ annotations: { readOnlyHint: tool2.metadata?.readOnly === true },
11878
11876
  call: async (input) => {
11879
- const output = await tool.call(callerContext(), input);
11880
- return { ...output, summary: output.message ?? `${tool.name} ok` };
11877
+ const output = await tool2.call(callerContext(), input);
11878
+ return { ...output, summary: output.message ?? `${tool2.name} ok` };
11881
11879
  }
11882
11880
  };
11883
11881
  }
@@ -12181,7 +12179,7 @@ async function readMachine(req) {
12181
12179
  }
12182
12180
 
12183
12181
  // src/ui/server.ts
12184
- var ASSETS = resolve9(dirname7(fileURLToPath4(import.meta.url)), "ui");
12182
+ var SHIPPED = resolve9(dirname7(fileURLToPath4(import.meta.url)), "ui");
12185
12183
  var TYPES = {
12186
12184
  ".html": "text/html; charset=utf-8",
12187
12185
  ".js": "text/javascript; charset=utf-8",
@@ -12221,10 +12219,10 @@ async function item(repo, id, res) {
12221
12219
  const read3 = await getWorkItem2(context(), { id }, { root: repo, harness: harness2() });
12222
12220
  json(res, 200, { item: read3.item, brief: read3.brief });
12223
12221
  }
12224
- async function asset(repo, url, res) {
12222
+ async function asset(repo, assets, url, res) {
12225
12223
  const wanted = url.pathname === "/" || !extname(url.pathname) ? "/index.html" : url.pathname;
12226
- const path22 = join9(ASSETS, wanted);
12227
- if (!path22.startsWith(ASSETS)) {
12224
+ const path22 = join9(assets, wanted);
12225
+ if (!path22.startsWith(assets)) {
12228
12226
  res.writeHead(403).end("no");
12229
12227
  return;
12230
12228
  }
@@ -12249,7 +12247,9 @@ async function asset(repo, url, res) {
12249
12247
  );
12250
12248
  }
12251
12249
  }
12252
- function serveUi(repo, port) {
12250
+ function serveUi(repo, port, options = {}) {
12251
+ const assets = options.assets ?? SHIPPED;
12252
+ const watching = /* @__PURE__ */ new Set();
12253
12253
  const server = createServer2((req, res) => {
12254
12254
  const url = new URL(req.url ?? "/", "http://localhost");
12255
12255
  void (async () => {
@@ -12270,7 +12270,17 @@ function serveUi(repo, port) {
12270
12270
  json(res, 200, await readMachine({ root: repo }));
12271
12271
  return;
12272
12272
  }
12273
- await asset(repo, url, res);
12273
+ if (url.pathname === "/api/v1/ui/reload") {
12274
+ res.writeHead(200, {
12275
+ "content-type": "text/event-stream",
12276
+ "cache-control": "no-store",
12277
+ connection: "keep-alive"
12278
+ });
12279
+ watching.add(res);
12280
+ res.on("close", () => watching.delete(res));
12281
+ return;
12282
+ }
12283
+ await asset(repo, assets, url, res);
12274
12284
  } catch (err) {
12275
12285
  const status2 = err instanceof ServiceError ? err.statusCode : 500;
12276
12286
  json(res, status2, { error: err instanceof Error ? err.message : String(err) });
@@ -12282,21 +12292,167 @@ function serveUi(repo, port) {
12282
12292
  server.listen(port, "127.0.0.1", () => {
12283
12293
  ok({
12284
12294
  url: `http://localhost:${port}`,
12285
- close: () => new Promise((done) => server.close(() => done()))
12295
+ reload: () => {
12296
+ for (const page of watching) page.write("data: reload\n\n");
12297
+ },
12298
+ close: () => new Promise((done) => {
12299
+ for (const page of watching) page.end();
12300
+ watching.clear();
12301
+ server.close(() => done());
12302
+ })
12286
12303
  });
12287
12304
  });
12288
12305
  });
12289
12306
  }
12290
12307
 
12308
+ // src/ui/source.ts
12309
+ import { spawn as spawn3 } from "child_process";
12310
+ import { existsSync as existsSync6, readFileSync as readFileSync8, watch } from "fs";
12311
+ import { stat as stat3 } from "fs/promises";
12312
+ import { dirname as dirname8, join as join10, resolve as resolve10 } from "path";
12313
+ import { fileURLToPath as fileURLToPath5 } from "url";
12314
+ var PAGE = join10("apps", "cli", "dist", "ui");
12315
+ var BUNDLE = "app.js";
12316
+ var WATCHED = [join10("apps", "cli", "src", "ui"), join10("packages", "ui", "src")];
12317
+ function fromSource() {
12318
+ return fileURLToPath5(import.meta.url).endsWith(".ts");
12319
+ }
12320
+ function sourceCheckout() {
12321
+ const named2 = process.env.JARVIS_SOURCE;
12322
+ if (named2) return usable(resolve10(named2));
12323
+ if (fromSource()) return usable(resolve10(dirname8(fileURLToPath5(import.meta.url)), "../../../.."));
12324
+ const built = builtFrom();
12325
+ return built ? usable(built) : null;
12326
+ }
12327
+ function builtFrom() {
12328
+ try {
12329
+ const manifest = join10(dirname8(fileURLToPath5(import.meta.url)), "..", "package.json");
12330
+ const read3 = JSON.parse(readFileSync8(manifest, "utf-8"));
12331
+ return typeof read3.jarvis?.source === "string" ? read3.jarvis.source : null;
12332
+ } catch {
12333
+ return null;
12334
+ }
12335
+ }
12336
+ function usable(root) {
12337
+ const files2 = [
12338
+ join10(root, "apps", "cli", "src", "ui", "browser", "App.tsx"),
12339
+ join10(root, "apps", "cli", "tsup.ui.config.ts")
12340
+ ];
12341
+ const present = files2.every((path22) => existsSync6(path22)) && tool(root, "tsup") && tool(root, "tsx");
12342
+ return present ? root : null;
12343
+ }
12344
+ function tool(root, name) {
12345
+ const places = [
12346
+ join10(root, "apps", "cli", "node_modules", ".bin", name),
12347
+ join10(root, "node_modules", ".bin", name)
12348
+ ];
12349
+ return places.find((path22) => existsSync6(path22)) ?? null;
12350
+ }
12351
+ function runFromCheckout(req) {
12352
+ const child = spawn3(
12353
+ tool(req.root, "tsx") ?? "tsx",
12354
+ [
12355
+ "watch",
12356
+ "--clear-screen=false",
12357
+ join10(req.root, "apps", "cli", "src", "bin.ts"),
12358
+ "ui",
12359
+ "--repo",
12360
+ req.repo,
12361
+ "--port",
12362
+ String(req.port),
12363
+ // The supervisor opens the browser, once, when the port answers. Left to the child it
12364
+ // would open a fresh tab on every restart `tsx watch` makes.
12365
+ "--no-open"
12366
+ ],
12367
+ { stdio: "inherit", cwd: req.root }
12368
+ );
12369
+ const relay = (signal) => () => {
12370
+ child.kill(signal);
12371
+ };
12372
+ process.once("SIGINT", relay("SIGINT"));
12373
+ process.once("SIGTERM", relay("SIGTERM"));
12374
+ child.once("exit", (code) => {
12375
+ process.exitCode = code ?? 0;
12376
+ });
12377
+ return child;
12378
+ }
12379
+ async function watchPage(root) {
12380
+ const dir = join10(root, PAGE);
12381
+ const before = await changedAt(join10(dir, BUNDLE));
12382
+ const build2 = spawn3(
12383
+ tool(root, "tsup") ?? "tsup",
12384
+ ["--config", "tsup.ui.config.ts", ...WATCHED.flatMap((where) => ["--watch", join10(root, where)])],
12385
+ {
12386
+ stdio: ["ignore", "ignore", "inherit"],
12387
+ cwd: join10(root, "apps", "cli")
12388
+ }
12389
+ );
12390
+ const stop = () => {
12391
+ build2.kill("SIGTERM");
12392
+ };
12393
+ try {
12394
+ await landed(join10(dir, BUNDLE), before);
12395
+ } catch (err) {
12396
+ stop();
12397
+ throw err;
12398
+ }
12399
+ return { dir, stop };
12400
+ }
12401
+ var PATIENCE2 = 12e4;
12402
+ async function landed(file, before) {
12403
+ const until = Date.now() + PATIENCE2;
12404
+ while (Date.now() < until) {
12405
+ if (await changedAt(file) > before) return;
12406
+ await new Promise((done) => setTimeout(done, 100));
12407
+ }
12408
+ throw new Error(`the page did not build within ${PATIENCE2 / 1e3}s \u2014 run \`pnpm build\` there to see why`);
12409
+ }
12410
+ async function changedAt(file) {
12411
+ try {
12412
+ return (await stat3(file)).mtimeMs;
12413
+ } catch {
12414
+ return 0;
12415
+ }
12416
+ }
12417
+ function whenRebuilt(dir, reload) {
12418
+ let due = null;
12419
+ return watch(dir, (_event, name) => {
12420
+ if (name !== BUNDLE) return;
12421
+ if (due) clearTimeout(due);
12422
+ due = setTimeout(reload, 120);
12423
+ });
12424
+ }
12425
+
12291
12426
  // src/commands/ui.ts
12292
12427
  function register21(program) {
12293
- program.command("ui").description("Open this repo's board in a browser \u2014 nothing to configure, nothing to join").option("--repo <path>", "The repo to show. Default: the working directory").option("--port <number>", "The port to listen on. Default: 3402").option("--no-open", "Print the address instead of opening a browser").action(async (opts) => {
12428
+ program.command("ui").description("Open this repo's board in a browser \u2014 nothing to configure, nothing to join").option("--repo <path>", "The repo to show. Default: the working directory").option("--port <number>", "The port to listen on. Default: 3402").option("--no-open", "Print the address instead of opening a browser").option("--packaged", "Serve the page this build shipped with, never a checkout's").action(async (opts) => {
12294
12429
  const repo = resolveRepo(opts.repo);
12295
12430
  const port = opts.port ? Number.parseInt(opts.port, 10) : 3402;
12431
+ const source2 = opts.packaged ? null : sourceCheckout();
12432
+ if (source2 && !fromSource()) {
12433
+ console.log(`${basename3(repo)} \u2192 serving ${source2}, from source`);
12434
+ runFromCheckout({ root: source2, repo, port });
12435
+ if (opts.open !== false) void openWhenUp(port);
12436
+ return;
12437
+ }
12438
+ let assets;
12439
+ let stopBuilding;
12440
+ if (source2) {
12441
+ console.log("building the board's page from this checkout\u2026");
12442
+ try {
12443
+ const page = await watchPage(source2);
12444
+ assets = page.dir;
12445
+ stopBuilding = page.stop;
12446
+ } catch (err) {
12447
+ console.error(`could not build the page from ${source2}: ${err instanceof Error ? err.message : String(err)}`);
12448
+ console.error("serving the page this build shipped with instead.");
12449
+ }
12450
+ }
12296
12451
  let server;
12297
12452
  try {
12298
- server = await serveUi(repo, port);
12453
+ server = await serveUi(repo, port, assets ? { assets } : {});
12299
12454
  } catch (err) {
12455
+ stopBuilding?.();
12300
12456
  const busy = err.code === "EADDRINUSE";
12301
12457
  console.error(
12302
12458
  busy ? `port ${port} is already in use. Something else is on it \u2014 pass --port to pick another.` : `could not start: ${err instanceof Error ? err.message : String(err)}`
@@ -12304,16 +12460,37 @@ function register21(program) {
12304
12460
  process.exitCode = 1;
12305
12461
  return;
12306
12462
  }
12463
+ const watching = assets ? whenRebuilt(assets, server.reload) : null;
12307
12464
  console.log(`${basename3(repo)} \u2192 ${server.url}`);
12308
- console.log("Reading the board from this checkout. Ctrl-C to stop.");
12465
+ console.log(
12466
+ assets ? "Reading the board from this checkout, and rebuilding the page as you save it. Ctrl-C to stop." : "Reading the board from this checkout. Ctrl-C to stop."
12467
+ );
12309
12468
  if (opts.open !== false) openBrowser({ url: server.url });
12310
12469
  await new Promise((done) => {
12311
- const stop = () => void server.close().then(done);
12470
+ const stop = () => {
12471
+ watching?.close();
12472
+ stopBuilding?.();
12473
+ void server.close().then(done);
12474
+ };
12312
12475
  process.once("SIGINT", stop);
12313
12476
  process.once("SIGTERM", stop);
12314
12477
  });
12315
12478
  });
12316
12479
  }
12480
+ async function openWhenUp(port) {
12481
+ const until = Date.now() + 12e4;
12482
+ while (Date.now() < until) {
12483
+ try {
12484
+ const answered = await fetch(`http://localhost:${port}/api/v1/machine`);
12485
+ if (answered.ok) {
12486
+ openBrowser({ url: `http://localhost:${port}` });
12487
+ return;
12488
+ }
12489
+ } catch {
12490
+ }
12491
+ await new Promise((done) => setTimeout(done, 250));
12492
+ }
12493
+ }
12317
12494
 
12318
12495
  // src/commands/work.ts
12319
12496
  import { spawnSync as spawnSync3 } from "child_process";