demobite 1.18.5 → 1.19.1
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 +69 -3
- package/package.json +1 -1
package/launcher/index.mjs
CHANGED
|
@@ -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);
|
|
@@ -320,7 +323,15 @@ if (arg === "on") {
|
|
|
320
323
|
spawnSync("launchctl", ["bootout", `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: "ignore" });
|
|
321
324
|
killListener();
|
|
322
325
|
fs.writeFileSync(def.file, plist);
|
|
323
|
-
|
|
326
|
+
// launchd can refuse a bootstrap in the second after a bootout of the same label (I/O error while the
|
|
327
|
+
// old job is torn down; seen 2026-10-16 right after an update): a few tries, a second apart.
|
|
328
|
+
let r = null;
|
|
329
|
+
for (let i = 0; i < 5; i++) {
|
|
330
|
+
r = spawnSync("launchctl", ["bootstrap", `gui/${process.getuid()}`, def.file], { encoding: "utf8" });
|
|
331
|
+
if (r.status === 0) break;
|
|
332
|
+
spawnSync("launchctl", ["bootout", `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: "ignore" });
|
|
333
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
|
|
334
|
+
}
|
|
324
335
|
if (r.status !== 0) { warn(`launchctl refused the service: ${(r.stderr || r.stdout || "").trim()}`); process.exit(1); }
|
|
325
336
|
} else if (process.platform === "linux") {
|
|
326
337
|
const unit = `[Unit]\nDescription=DemoBites listener\nAfter=network-online.target\n\n[Service]\nExecStart=${args.map((a) => JSON.stringify(a)).join(" ")}\nWorkingDirectory=${cwd}\nEnvironment=PATH=${pathEnv}\n${process.env.DEMOBITE_HOME ? `Environment=DEMOBITE_HOME=${HOME}\n` : ""}Restart=always\nRestartSec=10\nStandardOutput=append:${LOG}\nStandardError=append:${LOG}\n\n[Install]\nWantedBy=default.target\n`;
|
|
@@ -463,6 +474,59 @@ if (arg === "listen") {
|
|
|
463
474
|
const event = (job_id, kind, payload) => api({ action: "event", job_id, kind, payload });
|
|
464
475
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
465
476
|
|
|
477
|
+
// ── SELF-UPDATE (1.19.0, founder 2026-10-16: "checks if there's an update and updates itself") ──
|
|
478
|
+
// The service copy under ~/.demobite/pkg asks the registry once an hour (and at start) for the latest
|
|
479
|
+
// version. Newer, and idle: it installs that version into pkg.next with the private Node's npm, checks
|
|
480
|
+
// the new launcher answers `--version`, swaps the folders (the old copy stays in pkg.prev), and exits;
|
|
481
|
+
// the login service brings it back on the new code. A new copy that fails to check in three boots in
|
|
482
|
+
// a row is swapped back. Never during a job; a machine asleep for a week updates within the hour it wakes.
|
|
483
|
+
const selfUpdating = SERVICE && launcherPath.startsWith(PKG_DIR + path.sep);
|
|
484
|
+
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; };
|
|
485
|
+
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) })); });
|
|
486
|
+
let updateCheckedAt = 0;
|
|
487
|
+
const selfUpdate = async () => {
|
|
488
|
+
if (!selfUpdating) return;
|
|
489
|
+
updateCheckedAt = Date.now();
|
|
490
|
+
let latest = null;
|
|
491
|
+
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 */ }
|
|
492
|
+
if (!latest || !newer(latest, pkg.version)) return;
|
|
493
|
+
const stamp = () => new Date().toISOString();
|
|
494
|
+
console.log(` ${stamp()} update: ${pkg.version} → ${latest}, installing…`);
|
|
495
|
+
const stage = PKG_DIR + ".next", prev = PKG_DIR + ".prev";
|
|
496
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
497
|
+
// check-ins keep flowing while npm works, so the app never calls this machine quiet
|
|
498
|
+
const beat = setInterval(() => { void api({ action: "checkin", machine_id: listen.machine_id, name, readiness: readiness() }); }, 5000);
|
|
499
|
+
const [cmd, args] = npmCmd(["install", "--prefix", stage, "--silent", "--no-audit", "--no-fund", `${pkg.name}@${latest}`]);
|
|
500
|
+
const r = await run(cmd, args);
|
|
501
|
+
clearInterval(beat);
|
|
502
|
+
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; }
|
|
503
|
+
const check = spawnSync(process.execPath, [path.join(stage, "node_modules", pkg.name, "launcher", "index.mjs"), "--version"], { encoding: "utf8", timeout: 20000 });
|
|
504
|
+
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; }
|
|
505
|
+
fs.rmSync(prev, { recursive: true, force: true });
|
|
506
|
+
fs.renameSync(PKG_DIR, prev);
|
|
507
|
+
fs.renameSync(stage, PKG_DIR);
|
|
508
|
+
writeJson(UPDATE, { from: pkg.version, to: latest, at: stamp(), boots: 0 });
|
|
509
|
+
console.log(` ${stamp()} update: ${latest} installed; restarting`);
|
|
510
|
+
if (process.platform === "win32") { const d = spawn("wscript.exe", [serviceDef().file], { detached: true, stdio: "ignore" }); d.unref(); }
|
|
511
|
+
bye("UPDATE");
|
|
512
|
+
};
|
|
513
|
+
// Three boots without a check-in on a fresh update: back to the copy that worked.
|
|
514
|
+
if (selfUpdating) {
|
|
515
|
+
const u = readJson(UPDATE);
|
|
516
|
+
if (u?.to === pkg.version) {
|
|
517
|
+
if ((u.boots ?? 0) >= 3 && fs.existsSync(PKG_DIR + ".prev")) {
|
|
518
|
+
console.log(` ${new Date().toISOString()} update: ${pkg.version} never checked in; back to ${u.from}`);
|
|
519
|
+
fs.rmSync(PKG_DIR + ".bad", { recursive: true, force: true });
|
|
520
|
+
fs.renameSync(PKG_DIR, PKG_DIR + ".bad"); fs.renameSync(PKG_DIR + ".prev", PKG_DIR);
|
|
521
|
+
fs.rmSync(UPDATE, { force: true });
|
|
522
|
+
if (process.platform === "win32") { const d = spawn("wscript.exe", [serviceDef().file], { detached: true, stdio: "ignore" }); d.unref(); }
|
|
523
|
+
bye("UPDATE");
|
|
524
|
+
}
|
|
525
|
+
writeJson(UPDATE, { ...u, boots: (u.boots ?? 0) + 1 });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
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`); } };
|
|
529
|
+
|
|
466
530
|
console.log(`\n Connected as "${name}" for ${cfg.workspace?.name ?? "your workspace"} on ${base.replace(/^https?:\/\//, "")}.`);
|
|
467
531
|
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
532
|
// The one thing the person must know, boxed (founder 2026-10-15): this
|
|
@@ -603,7 +667,9 @@ if (arg === "listen") {
|
|
|
603
667
|
|
|
604
668
|
for (;;) {
|
|
605
669
|
const r = await api({ action: "checkin", machine_id: listen.machine_id, name, readiness: readiness() });
|
|
670
|
+
if (r) settled();
|
|
606
671
|
if (r?.job) await runJob(r.job);
|
|
672
|
+
else if (r && Date.now() - updateCheckedAt > 60 * 60 * 1000) await selfUpdate();
|
|
607
673
|
else await sleep(5000);
|
|
608
674
|
}
|
|
609
675
|
}
|
package/package.json
CHANGED