demobite 1.18.5 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/launcher/index.mjs +60 -2
  2. package/package.json +1 -1
@@ -24,7 +24,7 @@ import fs from "node:fs";
24
24
  import os from "node:os";
25
25
  import path from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
- import { execFileSync, spawnSync } from "node:child_process";
27
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
28
28
  import { createHash } from "node:crypto";
29
29
 
30
30
  const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -66,6 +66,8 @@ const CHILDREN = path.join(HOME, "children.json");
66
66
  // never unload the real one (2026-10-16: a scratch rig's `off` removed the founder's service).
67
67
  const HOME_TAG = process.env.DEMOBITE_HOME ? "." + createHash("sha1").update(HOME).digest("hex").slice(0, 8) : "";
68
68
  const SERVICE_LABEL = `com.demobites.listen${HOME_TAG}`;
69
+ const PKG_DIR = path.join(HOME, "pkg"); // the copy the service runs
70
+ const UPDATE = path.join(HOME, "update.json"); // { from, to, at, boots } while an update settles
69
71
  const SERVICE_UNIT = `demobite${HOME_TAG}.service`;
70
72
  const RUN_NAME = `DemoBites${HOME_TAG}`;
71
73
  const SERVICE = process.argv.includes("--service");
@@ -95,7 +97,7 @@ const printStatus = () => {
95
97
  const cfgThere = lock?.cwd ? readJson(path.join(lock.cwd, ".recorder", "config.json")) : null;
96
98
  const st = { base: cfgThere?.base ?? null, workspace: cfgThere?.workspace?.name ?? null, ...(readJson(STATE) ?? {}) };
97
99
  if (live) {
98
- console.log(` ● Connected as "${st?.name ?? os.hostname().replace(/\.local$/, "")}" since ${new Date(lock.since).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })} · for ${st?.workspace ?? "your workspace"} on ${(st?.base ?? "").replace(/^https?:\/\//, "") || "app.demobites.com"} (pid ${lock.pid}${lock.service ? ", login service" : ", a Terminal window"})`);
100
+ console.log(` ● Connected as "${st?.name ?? os.hostname().replace(/\.local$/, "")}" since ${new Date(lock.since).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })} · for ${st?.workspace ?? "your workspace"} on ${(st?.base ?? "").replace(/^https?:\/\//, "") || "app.demobites.com"} (pid ${lock.pid}${lock.service ? ", login service" : ", a Terminal window"}) · v${readJson(UPDATE)?.to ?? pkg.version}`);
99
101
  if (st?.at) console.log(` last check-in ${ago(st.at)} · ${st.busy ? `recording: ${st.job ?? ""}` : "idle"}${st.last_error ? ` · last error: ${st.last_error}` : ""}`);
100
102
  console.log(` working in ${lock.cwd.replace(os.homedir(), "~")}${lock.repo ? ` · code: ${lock.repo}` : ""}`);
101
103
  } else {
@@ -104,6 +106,7 @@ const printStatus = () => {
104
106
  console.log(` login service: ${serviceInstalled() ? "installed (starts at login, restarts if it dies)" : "not installed"} · run: demobite ${live ? "off" : "on"}`);
105
107
  console.log(` log: ${LOG.replace(os.homedir(), "~")}`);
106
108
  };
109
+ if (arg === "--version" || arg === "-v" || arg === "version") { console.log(pkg.version); process.exit(0); }
107
110
  if (arg === "status" && !process.argv[3]) { printStatus(); process.exit(0); }
108
111
  if (arg === "logs") {
109
112
  const n = Number(process.argv[3] || 40);
@@ -463,6 +466,59 @@ if (arg === "listen") {
463
466
  const event = (job_id, kind, payload) => api({ action: "event", job_id, kind, payload });
464
467
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
465
468
 
469
+ // ── SELF-UPDATE (1.19.0, founder 2026-10-16: "checks if there's an update and updates itself") ──
470
+ // The service copy under ~/.demobite/pkg asks the registry once an hour (and at start) for the latest
471
+ // version. Newer, and idle: it installs that version into pkg.next with the private Node's npm, checks
472
+ // the new launcher answers `--version`, swaps the folders (the old copy stays in pkg.prev), and exits;
473
+ // the login service brings it back on the new code. A new copy that fails to check in three boots in
474
+ // a row is swapped back. Never during a job; a machine asleep for a week updates within the hour it wakes.
475
+ const selfUpdating = SERVICE && launcherPath.startsWith(PKG_DIR + path.sep);
476
+ const newer = (a, b) => { const A = a.split(".").map(Number), B = b.split(".").map(Number); for (let i = 0; i < 3; i++) { if ((A[i] || 0) !== (B[i] || 0)) return (A[i] || 0) > (B[i] || 0); } return false; };
477
+ const run = (cmd, args) => new Promise((resolve) => { const c = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] }); let out = ""; c.stdout.on("data", (d) => { out += d; }); c.stderr.on("data", (d) => { out += d; }); c.on("close", (code) => resolve({ code, out })); c.on("error", (e) => resolve({ code: -1, out: String(e) })); });
478
+ let updateCheckedAt = 0;
479
+ const selfUpdate = async () => {
480
+ if (!selfUpdating) return;
481
+ updateCheckedAt = Date.now();
482
+ let latest = null;
483
+ try { const r = await fetch(`https://registry.npmjs.org/${pkg.name}/latest`, { signal: AbortSignal.timeout(8000) }); if (r.ok) latest = (await r.json()).version; } catch { /* offline: next hour */ }
484
+ if (!latest || !newer(latest, pkg.version)) return;
485
+ const stamp = () => new Date().toISOString();
486
+ console.log(` ${stamp()} update: ${pkg.version} → ${latest}, installing…`);
487
+ const stage = PKG_DIR + ".next", prev = PKG_DIR + ".prev";
488
+ fs.rmSync(stage, { recursive: true, force: true });
489
+ // check-ins keep flowing while npm works, so the app never calls this machine quiet
490
+ const beat = setInterval(() => { void api({ action: "checkin", machine_id: listen.machine_id, name, readiness: readiness() }); }, 5000);
491
+ const [cmd, args] = npmCmd(["install", "--prefix", stage, "--silent", "--no-audit", "--no-fund", `${pkg.name}@${latest}`]);
492
+ const r = await run(cmd, args);
493
+ clearInterval(beat);
494
+ if (r.code !== 0) { console.log(` ${stamp()} update: npm failed (${r.out.trim().split("\n").pop() || r.code}); staying on ${pkg.version}`); fs.rmSync(stage, { recursive: true, force: true }); return; }
495
+ const check = spawnSync(process.execPath, [path.join(stage, "node_modules", pkg.name, "launcher", "index.mjs"), "--version"], { encoding: "utf8", timeout: 20000 });
496
+ if (check.status !== 0 || check.stdout.trim() !== latest) { console.log(` ${stamp()} update: the new launcher did not answer; staying on ${pkg.version}`); fs.rmSync(stage, { recursive: true, force: true }); return; }
497
+ fs.rmSync(prev, { recursive: true, force: true });
498
+ fs.renameSync(PKG_DIR, prev);
499
+ fs.renameSync(stage, PKG_DIR);
500
+ writeJson(UPDATE, { from: pkg.version, to: latest, at: stamp(), boots: 0 });
501
+ console.log(` ${stamp()} update: ${latest} installed; restarting`);
502
+ if (process.platform === "win32") { const d = spawn("wscript.exe", [serviceDef().file], { detached: true, stdio: "ignore" }); d.unref(); }
503
+ bye("UPDATE");
504
+ };
505
+ // Three boots without a check-in on a fresh update: back to the copy that worked.
506
+ if (selfUpdating) {
507
+ const u = readJson(UPDATE);
508
+ if (u?.to === pkg.version) {
509
+ if ((u.boots ?? 0) >= 3 && fs.existsSync(PKG_DIR + ".prev")) {
510
+ console.log(` ${new Date().toISOString()} update: ${pkg.version} never checked in; back to ${u.from}`);
511
+ fs.rmSync(PKG_DIR + ".bad", { recursive: true, force: true });
512
+ fs.renameSync(PKG_DIR, PKG_DIR + ".bad"); fs.renameSync(PKG_DIR + ".prev", PKG_DIR);
513
+ fs.rmSync(UPDATE, { force: true });
514
+ if (process.platform === "win32") { const d = spawn("wscript.exe", [serviceDef().file], { detached: true, stdio: "ignore" }); d.unref(); }
515
+ bye("UPDATE");
516
+ }
517
+ writeJson(UPDATE, { ...u, boots: (u.boots ?? 0) + 1 });
518
+ }
519
+ }
520
+ const settled = () => { if (!selfUpdating) return; if (fs.existsSync(UPDATE)) { fs.rmSync(UPDATE, { force: true }); fs.rmSync(PKG_DIR + ".prev", { recursive: true, force: true }); console.log(` ${new Date().toISOString()} update: ${pkg.version} is in and checked in`); } };
521
+
466
522
  console.log(`\n Connected as "${name}" for ${cfg.workspace?.name ?? "your workspace"} on ${base.replace(/^https?:\/\//, "")}.`);
467
523
  console.log(` Ask for a demo in the app (Create → Agentic Capture). ${engine === "claude" ? (agentBin ? `Claude Code${agentInfo.agent_version ? " " + agentInfo.agent_version : ""} plans the demos, on your own plan; they film on ${JOB_MODEL} unless you pick another model in the app.` : "! --engine=claude, but Claude Code was not found on this machine.") : `Demos are planned by the DemoBites agent on ${JOB_MODEL} (pick another model in the app)${process.env.DEMOBITES_NO_CODE ? "; code sharing is off" : repo ? "; the files it reads from this project are logged in each take" : ""}.`}`);
468
524
  // The one thing the person must know, boxed (founder 2026-10-15): this
@@ -603,7 +659,9 @@ if (arg === "listen") {
603
659
 
604
660
  for (;;) {
605
661
  const r = await api({ action: "checkin", machine_id: listen.machine_id, name, readiness: readiness() });
662
+ if (r) settled();
606
663
  if (r?.job) await runJob(r.job);
664
+ else if (r && Date.now() - updateCheckedAt > 60 * 60 * 1000) await selfUpdate();
607
665
  else await sleep(5000);
608
666
  }
609
667
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demobite",
3
- "version": "1.18.5",
3
+ "version": "1.19.0",
4
4
  "description": "The DemoBites agentic recorder — you prompt, it films a real browser, and DemoBites turns the take into an editable demo bite.",
5
5
  "bin": {
6
6
  "demobite": "launcher/index.mjs"