demobite 1.18.3 → 1.18.5

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 +21 -15
  2. package/package.json +1 -1
@@ -25,6 +25,7 @@ import os from "node:os";
25
25
  import path from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
27
  import { execFileSync, spawnSync } from "node:child_process";
28
+ import { createHash } from "node:crypto";
28
29
 
29
30
  const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
30
31
  const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, "package.json"), "utf8"));
@@ -61,7 +62,12 @@ const LOCK = path.join(HOME, "listen.lock");
61
62
  const LOG = path.join(HOME, "listen.log");
62
63
  const STATE = path.join(HOME, "state.json");
63
64
  const CHILDREN = path.join(HOME, "children.json");
64
- const SERVICE_LABEL = "com.demobites.listen";
65
+ // A second home (DEMOBITE_HOME, used by tests and rigs) gets its own service name, so `off` on a rig can
66
+ // never unload the real one (2026-10-16: a scratch rig's `off` removed the founder's service).
67
+ const HOME_TAG = process.env.DEMOBITE_HOME ? "." + createHash("sha1").update(HOME).digest("hex").slice(0, 8) : "";
68
+ const SERVICE_LABEL = `com.demobites.listen${HOME_TAG}`;
69
+ const SERVICE_UNIT = `demobite${HOME_TAG}.service`;
70
+ const RUN_NAME = `DemoBites${HOME_TAG}`;
65
71
  const SERVICE = process.argv.includes("--service");
66
72
  const launcherPath = fileURLToPath(import.meta.url);
67
73
  const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } };
@@ -70,11 +76,11 @@ const pidAlive = (pid) => { if (!pid) return false; try { process.kill(pid, 0);
70
76
  const ago = (iso) => { const s = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 1000)); return s < 60 ? `${s} s ago` : s < 3600 ? `${Math.round(s / 60)} min ago` : `${Math.round(s / 3600)} h ago`; };
71
77
  const serviceDef = () => {
72
78
  if (process.platform === "darwin") return { file: path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) };
73
- if (process.platform === "linux") return { file: path.join(os.homedir(), ".config", "systemd", "user", "demobite.service") };
79
+ if (process.platform === "linux") return { file: path.join(os.homedir(), ".config", "systemd", "user", SERVICE_UNIT) };
74
80
  return { file: path.join(HOME, "service.vbs") };
75
81
  };
76
82
  const serviceInstalled = () => {
77
- if (process.platform === "win32") { const r = spawnSync("reg", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", "DemoBites"], { encoding: "utf8" }); return r.status === 0; }
83
+ if (process.platform === "win32") { const r = spawnSync("reg", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", RUN_NAME], { encoding: "utf8" }); return r.status === 0; }
78
84
  return fs.existsSync(serviceDef().file);
79
85
  };
80
86
  const killListener = () => {
@@ -89,11 +95,11 @@ const printStatus = () => {
89
95
  const cfgThere = lock?.cwd ? readJson(path.join(lock.cwd, ".recorder", "config.json")) : null;
90
96
  const st = { base: cfgThere?.base ?? null, workspace: cfgThere?.workspace?.name ?? null, ...(readJson(STATE) ?? {}) };
91
97
  if (live) {
92
- console.log(` ● Listening 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"})`);
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"})`);
93
99
  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}` : ""}`);
94
100
  console.log(` working in ${lock.cwd.replace(os.homedir(), "~")}${lock.repo ? ` · code: ${lock.repo}` : ""}`);
95
101
  } else {
96
- console.log(` ○ Not listening.${st?.at ? ` Last check-in ${ago(st.at)}.` : ""}`);
102
+ console.log(` ○ Not connected.${st?.at ? ` Last check-in ${ago(st.at)}.` : ""}`);
97
103
  }
98
104
  console.log(` login service: ${serviceInstalled() ? "installed (starts at login, restarts if it dies)" : "not installed"} · run: demobite ${live ? "off" : "on"}`);
99
105
  console.log(` log: ${LOG.replace(os.homedir(), "~")}`);
@@ -107,8 +113,8 @@ if (arg === "logs") {
107
113
  if (arg === "off") {
108
114
  const def = serviceDef();
109
115
  if (process.platform === "darwin") spawnSync("launchctl", ["bootout", `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: "ignore" });
110
- else if (process.platform === "linux") spawnSync("systemctl", ["--user", "disable", "--now", "demobite.service"], { stdio: "ignore" });
111
- else spawnSync("reg", ["delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", "DemoBites", "/f"], { stdio: "ignore" });
116
+ else if (process.platform === "linux") spawnSync("systemctl", ["--user", "disable", "--now", SERVICE_UNIT], { stdio: "ignore" });
117
+ else spawnSync("reg", ["delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", RUN_NAME, "/f"], { stdio: "ignore" });
112
118
  fs.rmSync(def.file, { force: true });
113
119
  const pid = killListener();
114
120
  // the browsers of an interrupted take go with it
@@ -322,14 +328,14 @@ if (arg === "on") {
322
328
  fs.writeFileSync(def.file, unit);
323
329
  killListener();
324
330
  spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
325
- const r = spawnSync("systemctl", ["--user", "enable", "--now", "demobite.service"], { encoding: "utf8" });
331
+ const r = spawnSync("systemctl", ["--user", "enable", "--now", SERVICE_UNIT], { encoding: "utf8" });
326
332
  if (r.status !== 0) { warn(`systemd refused the service: ${(r.stderr || "").trim()}`); process.exit(1); }
327
333
  } else {
328
334
  // Windows: a Run entry that starts the listener hidden at logon (no console window), via a tiny script.
329
335
  const q = (t) => `""${t}""`;
330
336
  const vbs = `Set sh = CreateObject("WScript.Shell")\nsh.CurrentDirectory = "${cwd}"\nsh.Run "${args.map((a) => q(a)).join(" ")} >> ${q(LOG)} 2>&1", 0, False\n`;
331
337
  fs.writeFileSync(def.file, vbs);
332
- spawnSync("reg", ["add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", "DemoBites", "/t", "REG_SZ", "/d", `wscript.exe "${def.file}"`, "/f"], { stdio: "ignore" });
338
+ spawnSync("reg", ["add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", RUN_NAME, "/t", "REG_SZ", "/d", `wscript.exe "${def.file}"`, "/f"], { stdio: "ignore" });
333
339
  killListener();
334
340
  spawnSync("wscript.exe", [def.file], { stdio: "ignore" });
335
341
  }
@@ -340,7 +346,7 @@ if (arg === "on") {
340
346
  console.log(`
341
347
  ┌──────────────────────────────────────────────────────────┐
342
348
  │ │
343
- │ ✓ Done. DemoBites is listening on this ${process.platform === "darwin" ? "Mac" : "machine"}.${process.platform === "darwin" ? " " : " "}│
349
+ │ ✓ Done. DemoBites is connected on this ${process.platform === "darwin" ? "Mac" : "machine"}.${process.platform === "darwin" ? " " : " "}│
344
350
  │ │
345
351
  │ You can close this window. The recorder keeps │
346
352
  │ running quietly in the background, and starts │
@@ -349,7 +355,7 @@ if (arg === "on") {
349
355
  │ Back in the app, the Recorder box says Connected. │
350
356
  │ │
351
357
  │ Any time, in a Terminal: │
352
- │ demobite status is it listening │
358
+ │ demobite status is it connected │
353
359
  │ demobite off stop it and remove it │
354
360
  │ demobite on bring it back │
355
361
  │ │
@@ -385,7 +391,7 @@ if (arg === "listen") {
385
391
  {
386
392
  const cur = readJson(LOCK);
387
393
  if (cur?.pid && cur.pid !== process.pid && pidAlive(cur.pid)) {
388
- console.log(` ● Already listening (pid ${cur.pid}, since ${new Date(cur.since).toLocaleTimeString()}${cur.service ? ", the login service" : ""}). Nothing to do.\n demobite status · demobite off`);
394
+ console.log(` ● Already connected (pid ${cur.pid}, since ${new Date(cur.since).toLocaleTimeString()}${cur.service ? ", the login service" : ""}). Nothing to do.\n demobite status · demobite off`);
389
395
  process.exit(0);
390
396
  }
391
397
  // the browsers of a listener that died hard
@@ -457,11 +463,11 @@ if (arg === "listen") {
457
463
  const event = (job_id, kind, payload) => api({ action: "event", job_id, kind, payload });
458
464
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
459
465
 
460
- console.log(`\n Listening as "${name}" for ${cfg.workspace?.name ?? "your workspace"} on ${base.replace(/^https?:\/\//, "")}.`);
466
+ console.log(`\n Connected as "${name}" for ${cfg.workspace?.name ?? "your workspace"} on ${base.replace(/^https?:\/\//, "")}.`);
461
467
  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" : ""}.`}`);
462
468
  // The one thing the person must know, boxed (founder 2026-10-15): this
463
469
  // window is the machine's side of every recording. The login service has no window: one line instead.
464
- if (SERVICE) console.log(` ${new Date().toISOString()} listening (login service, pid ${process.pid})`);
470
+ if (SERVICE) console.log(` ${new Date().toISOString()} connected (login service, pid ${process.pid})`);
465
471
  else console.log(`
466
472
  ┌──────────────────────────────────────────────────────────┐
467
473
  │ │
@@ -482,7 +488,7 @@ if (arg === "listen") {
482
488
  const s = Math.floor((Date.now() - startedAt) / 1000);
483
489
  const clock = `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
484
490
  const dot = s % 2 ? "●" : "○";
485
- process.stdout.write(`\r\x1b[2K ${dot} Listening · ${clock} · waiting for a demo from the app`);
491
+ process.stdout.write(`\r\x1b[2K ${dot} Connected · ${clock} · waiting for a demo from the app`);
486
492
  };
487
493
  const waitOn = () => { if (tty && !ticker) { tick(); ticker = setInterval(tick, 1000); } };
488
494
  const waitOff = () => { if (ticker) { clearInterval(ticker); ticker = null; process.stdout.write("\r\x1b[2K"); } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "demobite",
3
- "version": "1.18.3",
3
+ "version": "1.18.5",
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"