demobite 1.18.4 → 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.
- package/launcher/index.mjs +73 -9
- package/package.json +1 -1
package/launcher/index.mjs
CHANGED
|
@@ -24,7 +24,8 @@ 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
|
+
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,14 @@ 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
|
-
|
|
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 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
|
|
71
|
+
const SERVICE_UNIT = `demobite${HOME_TAG}.service`;
|
|
72
|
+
const RUN_NAME = `DemoBites${HOME_TAG}`;
|
|
65
73
|
const SERVICE = process.argv.includes("--service");
|
|
66
74
|
const launcherPath = fileURLToPath(import.meta.url);
|
|
67
75
|
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } };
|
|
@@ -70,11 +78,11 @@ const pidAlive = (pid) => { if (!pid) return false; try { process.kill(pid, 0);
|
|
|
70
78
|
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
79
|
const serviceDef = () => {
|
|
72
80
|
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",
|
|
81
|
+
if (process.platform === "linux") return { file: path.join(os.homedir(), ".config", "systemd", "user", SERVICE_UNIT) };
|
|
74
82
|
return { file: path.join(HOME, "service.vbs") };
|
|
75
83
|
};
|
|
76
84
|
const serviceInstalled = () => {
|
|
77
|
-
if (process.platform === "win32") { const r = spawnSync("reg", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v",
|
|
85
|
+
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
86
|
return fs.existsSync(serviceDef().file);
|
|
79
87
|
};
|
|
80
88
|
const killListener = () => {
|
|
@@ -89,7 +97,7 @@ const printStatus = () => {
|
|
|
89
97
|
const cfgThere = lock?.cwd ? readJson(path.join(lock.cwd, ".recorder", "config.json")) : null;
|
|
90
98
|
const st = { base: cfgThere?.base ?? null, workspace: cfgThere?.workspace?.name ?? null, ...(readJson(STATE) ?? {}) };
|
|
91
99
|
if (live) {
|
|
92
|
-
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}`);
|
|
93
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}` : ""}`);
|
|
94
102
|
console.log(` working in ${lock.cwd.replace(os.homedir(), "~")}${lock.repo ? ` · code: ${lock.repo}` : ""}`);
|
|
95
103
|
} else {
|
|
@@ -98,6 +106,7 @@ const printStatus = () => {
|
|
|
98
106
|
console.log(` login service: ${serviceInstalled() ? "installed (starts at login, restarts if it dies)" : "not installed"} · run: demobite ${live ? "off" : "on"}`);
|
|
99
107
|
console.log(` log: ${LOG.replace(os.homedir(), "~")}`);
|
|
100
108
|
};
|
|
109
|
+
if (arg === "--version" || arg === "-v" || arg === "version") { console.log(pkg.version); process.exit(0); }
|
|
101
110
|
if (arg === "status" && !process.argv[3]) { printStatus(); process.exit(0); }
|
|
102
111
|
if (arg === "logs") {
|
|
103
112
|
const n = Number(process.argv[3] || 40);
|
|
@@ -107,8 +116,8 @@ if (arg === "logs") {
|
|
|
107
116
|
if (arg === "off") {
|
|
108
117
|
const def = serviceDef();
|
|
109
118
|
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",
|
|
111
|
-
else spawnSync("reg", ["delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v",
|
|
119
|
+
else if (process.platform === "linux") spawnSync("systemctl", ["--user", "disable", "--now", SERVICE_UNIT], { stdio: "ignore" });
|
|
120
|
+
else spawnSync("reg", ["delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", RUN_NAME, "/f"], { stdio: "ignore" });
|
|
112
121
|
fs.rmSync(def.file, { force: true });
|
|
113
122
|
const pid = killListener();
|
|
114
123
|
// the browsers of an interrupted take go with it
|
|
@@ -322,14 +331,14 @@ if (arg === "on") {
|
|
|
322
331
|
fs.writeFileSync(def.file, unit);
|
|
323
332
|
killListener();
|
|
324
333
|
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
325
|
-
const r = spawnSync("systemctl", ["--user", "enable", "--now",
|
|
334
|
+
const r = spawnSync("systemctl", ["--user", "enable", "--now", SERVICE_UNIT], { encoding: "utf8" });
|
|
326
335
|
if (r.status !== 0) { warn(`systemd refused the service: ${(r.stderr || "").trim()}`); process.exit(1); }
|
|
327
336
|
} else {
|
|
328
337
|
// Windows: a Run entry that starts the listener hidden at logon (no console window), via a tiny script.
|
|
329
338
|
const q = (t) => `""${t}""`;
|
|
330
339
|
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
340
|
fs.writeFileSync(def.file, vbs);
|
|
332
|
-
spawnSync("reg", ["add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v",
|
|
341
|
+
spawnSync("reg", ["add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", RUN_NAME, "/t", "REG_SZ", "/d", `wscript.exe "${def.file}"`, "/f"], { stdio: "ignore" });
|
|
333
342
|
killListener();
|
|
334
343
|
spawnSync("wscript.exe", [def.file], { stdio: "ignore" });
|
|
335
344
|
}
|
|
@@ -457,6 +466,59 @@ if (arg === "listen") {
|
|
|
457
466
|
const event = (job_id, kind, payload) => api({ action: "event", job_id, kind, payload });
|
|
458
467
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
459
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
|
+
|
|
460
522
|
console.log(`\n Connected as "${name}" for ${cfg.workspace?.name ?? "your workspace"} on ${base.replace(/^https?:\/\//, "")}.`);
|
|
461
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" : ""}.`}`);
|
|
462
524
|
// The one thing the person must know, boxed (founder 2026-10-15): this
|
|
@@ -597,7 +659,9 @@ if (arg === "listen") {
|
|
|
597
659
|
|
|
598
660
|
for (;;) {
|
|
599
661
|
const r = await api({ action: "checkin", machine_id: listen.machine_id, name, readiness: readiness() });
|
|
662
|
+
if (r) settled();
|
|
600
663
|
if (r?.job) await runJob(r.job);
|
|
664
|
+
else if (r && Date.now() - updateCheckedAt > 60 * 60 * 1000) await selfUpdate();
|
|
601
665
|
else await sleep(5000);
|
|
602
666
|
}
|
|
603
667
|
}
|
package/package.json
CHANGED