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.
- package/launcher/index.mjs +60 -2
- 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);
|
|
@@ -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