@decentnetwork/beagle 0.1.24 → 0.1.25

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.
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.23";
1
+ window.__DK_UI_VERSION="0.1.24";
2
2
  const ICON_PATHS = {
3
3
  // ---- tab bar (the four must feel like one set) ----
4
4
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
@@ -4214,6 +4214,16 @@ const STR = {
4214
4214
  linksCard: "Links",
4215
4215
  linkPlaceholder: "username or profile URL",
4216
4216
  save: "save",
4217
+ updTitle: "Update available",
4218
+ updLater: "Later",
4219
+ updNow: "Update now",
4220
+ updRestart: "Restart",
4221
+ updBusy: "Updating\u2026 (about a minute)",
4222
+ updDone: "Updated \u2014 restart to apply.",
4223
+ updRestarting: "Restarting\u2026 this page reloads when it\u2019s back. (If beagle was started by hand in a terminal, run it again.)",
4224
+ updNpx: "Running via npx \u2014 restarting picks up the latest version automatically.",
4225
+ updDev: "Running from a source checkout \u2014 update with:",
4226
+ updLanNote: "agentnet (lan) updates separately; the daemon restart is manual:",
4217
4227
  pubOpen: "View public profile",
4218
4228
  pubNone: "No public profile \u2014 this user has not registered a beagles.eth name.",
4219
4229
  ensNoEthWallet: "no Ethereum wallet extension found (MetaMask\u2026)",
@@ -4360,6 +4370,16 @@ const STR = {
4360
4370
  linksCard: "\u793E\u4EA4\u8D26\u53F7",
4361
4371
  linkPlaceholder: "\u7528\u6237\u540D\u6216\u4E3B\u9875\u94FE\u63A5",
4362
4372
  save: "\u4FDD\u5B58",
4373
+ updTitle: "\u53D1\u73B0\u65B0\u7248\u672C",
4374
+ updLater: "\u7A0D\u540E",
4375
+ updNow: "\u7ACB\u5373\u66F4\u65B0",
4376
+ updRestart: "\u91CD\u542F",
4377
+ updBusy: "\u66F4\u65B0\u4E2D\u2026(\u7EA6\u4E00\u5206\u949F)",
4378
+ updDone: "\u66F4\u65B0\u5B8C\u6210\u2014\u2014\u91CD\u542F\u540E\u751F\u6548\u3002",
4379
+ updRestarting: "\u6B63\u5728\u91CD\u542F\u2026\u5B8C\u6210\u540E\u672C\u9875\u4F1A\u81EA\u52A8\u5237\u65B0\u3002(\u5982\u679C beagle \u662F\u5728\u7EC8\u7AEF\u91CC\u624B\u52A8\u542F\u52A8\u7684,\u8BF7\u91CD\u65B0\u8FD0\u884C\u4E00\u6B21\u3002)",
4380
+ updNpx: "\u4F60\u662F\u7528 npx \u8FD0\u884C\u7684\u2014\u2014\u91CD\u542F\u5373\u81EA\u52A8\u83B7\u53D6\u6700\u65B0\u7248\u3002",
4381
+ updDev: "\u5F53\u524D\u4ECE\u6E90\u7801\u76EE\u5F55\u8FD0\u884C,\u8BF7\u624B\u52A8\u66F4\u65B0:",
4382
+ updLanNote: "agentnet(lan)\u9700\u5355\u72EC\u5347\u7EA7,daemon \u91CD\u542F\u9700\u624B\u52A8\u6267\u884C:",
4363
4383
  pubOpen: "\u67E5\u770B\u516C\u5F00\u8D44\u6599",
4364
4384
  pubNone: "\u6682\u65E0\u516C\u5F00\u8D44\u6599\u2014\u2014\u5BF9\u65B9\u8FD8\u6CA1\u6709\u6CE8\u518C beagles.eth \u540D\u5B57\u3002",
4365
4385
  ensNoEthWallet: "\u672A\u68C0\u6D4B\u5230\u4EE5\u592A\u574A\u94B1\u5305\u63D2\u4EF6(MetaMask \u7B49)",
@@ -4409,12 +4429,82 @@ function RailBtn({ icon, label, active, soon, onClick }) {
4409
4429
  gap: 3
4410
4430
  } }, /* @__PURE__ */ React.createElement(Icon, { name: icon, size: 20, stroke: active ? 2.1 : 1.8, color: active ? "var(--accent)" : "var(--dim)" }), /* @__PURE__ */ React.createElement("span", { style: { fontSize: 9, fontWeight: 600, letterSpacing: 0.2, color: active ? "var(--accent)" : "var(--faint)" } }, label), soon && /* @__PURE__ */ React.createElement("span", { style: { position: "absolute", top: 5, right: 6, width: 6, height: 6, borderRadius: 999, background: "var(--warn)" } }));
4411
4431
  }
4432
+ function DkUpdateModal({ T, info, onClose }) {
4433
+ const [phase, setPhase] = React.useState("idle");
4434
+ const [err, setErr] = React.useState(null);
4435
+ const behind = (info.updates || []).filter((u) => u.behind);
4436
+ const appBehind = behind.some((u) => u.pkg !== "@decentnetwork/lan");
4437
+ const lanBehind = behind.find((u) => u.pkg === "@decentnetwork/lan");
4438
+ const short = (pkg) => pkg.replace("@decentnetwork/", "");
4439
+ const doUpdate = () => {
4440
+ setPhase("running");
4441
+ setErr(null);
4442
+ fetch("/api/update-run", { method: "POST" }).then((r) => r.json()).then((d) => {
4443
+ if (d.ok)
4444
+ setPhase("done");
4445
+ else {
4446
+ setPhase("error");
4447
+ setErr(d.error || "update failed");
4448
+ }
4449
+ }).catch((e) => {
4450
+ setPhase("error");
4451
+ setErr(String(e && e.message || e));
4452
+ });
4453
+ };
4454
+ const doRestart = () => {
4455
+ setPhase("restarting");
4456
+ fetch("/api/update-restart", { method: "POST" }).catch(() => {
4457
+ });
4458
+ const poll = setInterval(() => {
4459
+ fetch("/api/update-check").then((r) => {
4460
+ if (r.ok) {
4461
+ clearInterval(poll);
4462
+ window.location.reload();
4463
+ }
4464
+ }).catch(() => {
4465
+ });
4466
+ }, 1500);
4467
+ };
4468
+ const btn = (label, onClick, tone) => /* @__PURE__ */ React.createElement("button", { onClick, style: {
4469
+ padding: "7px 14px",
4470
+ borderRadius: 9,
4471
+ cursor: "pointer",
4472
+ fontFamily: "var(--ui)",
4473
+ fontSize: 12.5,
4474
+ border: tone ? "none" : "1px solid var(--line)",
4475
+ background: tone ? "var(--accent)" : "transparent",
4476
+ color: tone ? "#fff" : "var(--text)"
4477
+ } }, label);
4478
+ return /* @__PURE__ */ React.createElement("div", { style: { position: "fixed", inset: 0, zIndex: 95, background: "color-mix(in oklab, #000, transparent 38%)", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 } }, /* @__PURE__ */ React.createElement("div", { style: { width: 430, maxWidth: "94vw", background: "var(--panel)", border: "1px solid var(--line)", borderRadius: 16, padding: "20px 22px", display: "flex", flexDirection: "column", gap: 14 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 13.5, fontWeight: 700, color: "var(--text)" } }, T.updTitle || "Update available"), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 6 } }, behind.map((u) => /* @__PURE__ */ React.createElement("div", { key: u.pkg, style: { display: "flex", alignItems: "center", gap: 10, fontFamily: "var(--mono)", fontSize: 12.5 } }, /* @__PURE__ */ React.createElement("span", { style: { width: 70, color: "var(--faint)" } }, short(u.pkg)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--dim)" } }, u.current), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--faint)" } }, "\u2192"), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--accent)", fontWeight: 700 } }, u.latest)))), phase === "idle" && appBehind && info.mode === "dev" && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--dim)", lineHeight: 1.5 } }, T.updDev || "Running from a source checkout \u2014 update with:", /* @__PURE__ */ React.createElement("br", null), /* @__PURE__ */ React.createElement("code", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--text)" } }, "git pull && npm run build")), phase === "idle" && appBehind && info.mode === "npx" && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--dim)", lineHeight: 1.5 } }, T.updNpx || "Running via npx \u2014 restarting picks up the latest version automatically."), lanBehind && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--dim)", lineHeight: 1.5 } }, T.updLanNote || "agentnet (lan) updates separately; the daemon restart is manual:", /* @__PURE__ */ React.createElement("br", null), /* @__PURE__ */ React.createElement("code", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--text)", userSelect: "all" } }, "npm i -g @decentnetwork/lan@latest && sudo agentnet down && sudo agentnet up --real-tun")), phase === "running" && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--dim)" } }, T.updBusy || "Updating\u2026 (about a minute)"), phase === "done" && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--online)" } }, T.updDone || "Updated \u2014 restart to apply."), phase === "restarting" && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--dim)" } }, T.updRestarting || "Restarting\u2026 this page reloads when it\u2019s back. (If beagle was started by hand in a terminal, run it again.)"), phase === "error" && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--danger)", wordBreak: "break-word" } }, err), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 8 } }, (phase === "idle" || phase === "error") && btn(T.updLater || "Later", onClose), phase === "idle" && appBehind && info.mode === "global" && btn(T.updNow || "Update now", doUpdate, true), phase === "idle" && appBehind && info.mode === "npx" && btn(T.updRestart || "Restart", doRestart, true), phase === "done" && btn(T.updRestart || "Restart", doRestart, true))));
4479
+ }
4412
4480
  function DkApp() {
4413
4481
  const [t, setTweak] = useTweaks(DK_DEFAULTS);
4414
4482
  const [tab, setTab] = React.useState(t.startTab || "chat");
4415
4483
  const [activeId, setActiveId] = React.useState(null);
4416
4484
  const data = useDaemonData();
4417
4485
  const me = data.me;
4486
+ const [upd, setUpd] = React.useState(null);
4487
+ React.useEffect(() => {
4488
+ let dead = false;
4489
+ const check = () => fetch("/api/update-check").then((r) => r.json()).then((d) => {
4490
+ if (dead || !d.ok)
4491
+ return;
4492
+ const behind = (d.updates || []).filter((u) => u.behind);
4493
+ if (!behind.length)
4494
+ return;
4495
+ const key = behind.map((u) => u.pkg + "@" + u.latest).join(",");
4496
+ if (localStorage.getItem("dk-upd-dismissed") === key)
4497
+ return;
4498
+ setUpd({ ...d, key });
4499
+ }).catch(() => {
4500
+ });
4501
+ check();
4502
+ const timer = setInterval(check, 6 * 3600 * 1e3);
4503
+ return () => {
4504
+ dead = true;
4505
+ clearInterval(timer);
4506
+ };
4507
+ }, []);
4418
4508
  const [backend, setBackend] = React.useState(null);
4419
4509
  const refreshBackend = React.useCallback(() => {
4420
4510
  dkGet("/api/backend").then((b) => {
@@ -4631,6 +4721,16 @@ function DkApp() {
4631
4721
  color: "#fff",
4632
4722
  fontSize: 12,
4633
4723
  fontWeight: 600
4634
- } }, n.label)))), /* @__PURE__ */ React.createElement(IncomingCallModal, { T, ctl: callCtl, peers }), /* @__PURE__ */ React.createElement(CallOverlay, { T, ctl: callCtl, peers }), /* @__PURE__ */ React.createElement(RtcFileInbox, { T, peers, ctl: rtcFileCtl }));
4724
+ } }, n.label)))), /* @__PURE__ */ React.createElement(IncomingCallModal, { T, ctl: callCtl, peers }), /* @__PURE__ */ React.createElement(CallOverlay, { T, ctl: callCtl, peers }), /* @__PURE__ */ React.createElement(RtcFileInbox, { T, peers, ctl: rtcFileCtl }), upd && /* @__PURE__ */ React.createElement(
4725
+ DkUpdateModal,
4726
+ {
4727
+ T,
4728
+ info: upd,
4729
+ onClose: () => {
4730
+ localStorage.setItem("dk-upd-dismissed", upd.key);
4731
+ setUpd(null);
4732
+ }
4733
+ }
4734
+ ));
4635
4735
  }
4636
4736
  ReactDOM.createRoot(document.getElementById("root")).render(/* @__PURE__ */ React.createElement(DkApp, null));
package/dist/server.js CHANGED
@@ -187,6 +187,93 @@ async function ensPublicByUserid(maxWaitMs = 300) {
187
187
  }
188
188
  return map;
189
189
  }
190
+ // ── self-update ─────────────────────────────────────────────────────────────
191
+ // npm registry is consulted at most once per TTL (and only while a UI is
192
+ // open); the UI pops a confirm dialog when something is behind. "Update"
193
+ // reinstalls the global package in place and restarts this process —
194
+ // pm2/launchd/systemd bring it back; a bare terminal run exits with a hint.
195
+ const UPDATE_TTL_MS = 6 * 3600_000;
196
+ const BEAGLE_PKG = "@decentnetwork/beagle";
197
+ let updateCache = null;
198
+ const BEAGLE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
199
+ function beagleVersion() {
200
+ try {
201
+ return JSON.parse(readFileSync(join(BEAGLE_ROOT, "package.json"), "utf-8")).version ?? "";
202
+ }
203
+ catch {
204
+ return "";
205
+ }
206
+ }
207
+ /** How this copy of beagle got onto disk — decides what "update" can do.
208
+ * npx cache paths contain _npx AND node_modules, so test _npx first. */
209
+ function installMode() {
210
+ if (BEAGLE_ROOT.includes("_npx"))
211
+ return "npx";
212
+ if (BEAGLE_ROOT.includes(join("node_modules", "@decentnetwork", "beagle")))
213
+ return "global";
214
+ return "dev";
215
+ }
216
+ async function npmLatest(pkg) {
217
+ const ctl = new AbortController();
218
+ const t = setTimeout(() => ctl.abort(), 8000);
219
+ try {
220
+ const r = await fetch(`https://registry.npmjs.org/${pkg.replace("/", "%2F")}/latest`, { signal: ctl.signal });
221
+ if (!r.ok)
222
+ return null;
223
+ return (await r.json()).version ?? null;
224
+ }
225
+ catch {
226
+ return null;
227
+ }
228
+ finally {
229
+ clearTimeout(t);
230
+ }
231
+ }
232
+ function verLt(a, b) {
233
+ const pa = a.split(".").map((x) => parseInt(x, 10));
234
+ const pb = b.split(".").map((x) => parseInt(x, 10));
235
+ if (pa.some(Number.isNaN) || pb.some(Number.isNaN))
236
+ return false;
237
+ for (let i = 0; i < 3; i++) {
238
+ const d = (pb[i] ?? 0) - (pa[i] ?? 0);
239
+ if (d !== 0)
240
+ return d > 0;
241
+ }
242
+ return false;
243
+ }
244
+ /** Run `npm i -g @decentnetwork/beagle@latest`. npm is found on PATH, else via
245
+ * npm-cli.js next to the running node (covers pm2/cron environments whose
246
+ * PATH lacks the nvm shims). */
247
+ async function npmSelfUpdate() {
248
+ const { spawn } = await import("node:child_process");
249
+ const args = ["i", "-g", `${BEAGLE_PKG}@latest`];
250
+ const npmCli = join(dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
251
+ const attempts = [
252
+ { cmd: "npm", args },
253
+ ...(existsSync(npmCli) ? [{ cmd: process.execPath, args: [npmCli, ...args] }] : []),
254
+ ];
255
+ for (const a of attempts) {
256
+ const r = await new Promise((resolveRun) => {
257
+ const ch = spawn(a.cmd, a.args, { shell: process.platform === "win32" });
258
+ let out = "";
259
+ const timer = setTimeout(() => ch.kill("SIGKILL"), 5 * 60_000);
260
+ ch.stdout?.on("data", (c) => (out += c));
261
+ ch.stderr?.on("data", (c) => (out += c));
262
+ ch.on("error", (e) => { clearTimeout(timer); resolveRun({ code: null, out, spawnErr: e.message }); });
263
+ ch.on("close", (code) => { clearTimeout(timer); resolveRun({ code, out }); });
264
+ });
265
+ if (r.spawnErr)
266
+ continue; // npm not found this way — try the next resolver
267
+ const tail = r.out.slice(-2000);
268
+ if (r.code === 0)
269
+ return { ok: true, log: tail };
270
+ const hint = /EACCES|permission denied/i.test(r.out)
271
+ ? " — no write access to the global npm dir; run `sudo npm i -g @decentnetwork/beagle@latest` manually"
272
+ : "";
273
+ return { ok: false, error: `npm exited ${r.code}${hint}`, log: tail };
274
+ }
275
+ return { ok: false, error: "npm not found — update manually: npm i -g @decentnetwork/beagle@latest" };
276
+ }
190
277
  /** True only when the request came from the local machine. Used to gate the
191
278
  * "Sign in with Decent" routes so binding the UI to a LAN IP can't expose
192
279
  * identity signing to other hosts. The popup always runs in the local user's
@@ -1125,6 +1212,71 @@ export function startBeagleServer(opts) {
1125
1212
  // One bootstrap call for the desktop UI: composes the design's DK_*
1126
1213
  // shapes (me / peers / requests / exits) from diag + friends-list +
1127
1214
  // ipam + routes, so the client data layer is a single poll.
1215
+ // ── self-update: check (cached), run (localhost), restart (localhost) ──
1216
+ if (req.method === "GET" && url === "/api/update-check") {
1217
+ const force = new URL(req.url || "", "http://localhost").searchParams.get("force") === "1";
1218
+ if (!force && updateCache && Date.now() - updateCache.at < UPDATE_TTL_MS) {
1219
+ sendJson(res, 200, updateCache.body);
1220
+ return;
1221
+ }
1222
+ const [beagleLatest, peerLatest, lanLatest] = await Promise.all([
1223
+ npmLatest(BEAGLE_PKG),
1224
+ npmLatest("@decentnetwork/peer"),
1225
+ npmLatest("@decentnetwork/lan"),
1226
+ ]);
1227
+ const curLan = opts.meExtra?.lanVer ?? "";
1228
+ const row = (pkg, current, latest) => ({
1229
+ pkg,
1230
+ current,
1231
+ latest,
1232
+ behind: !!(current && latest && verLt(current, latest)),
1233
+ });
1234
+ const body = {
1235
+ ok: true,
1236
+ checkedAt: Date.now(),
1237
+ mode: installMode(),
1238
+ updates: [
1239
+ row(BEAGLE_PKG, beagleVersion(), beagleLatest),
1240
+ // peer ships inside beagle: reinstalling beagle refreshes it too.
1241
+ row("@decentnetwork/peer", opts.meExtra?.peerVer ?? "", peerLatest),
1242
+ ...(curLan && curLan !== "(none)" ? [row("@decentnetwork/lan", curLan, lanLatest)] : []),
1243
+ ],
1244
+ };
1245
+ updateCache = { at: Date.now(), body };
1246
+ sendJson(res, 200, body);
1247
+ return;
1248
+ }
1249
+ if (req.method === "POST" && url === "/api/update-run") {
1250
+ if (!isLocalRequest(req)) {
1251
+ sendJson(res, 403, { ok: false, error: "update runs only from this machine (localhost)" });
1252
+ return;
1253
+ }
1254
+ const mode = installMode();
1255
+ if (mode === "dev") {
1256
+ sendJson(res, 400, { ok: false, error: "running from a source checkout — update with: git pull && npm run build" });
1257
+ return;
1258
+ }
1259
+ if (mode === "npx") {
1260
+ // npx with @latest re-resolves on every launch: restarting IS the update.
1261
+ sendJson(res, 200, { ok: true, npx: true });
1262
+ return;
1263
+ }
1264
+ const r = await npmSelfUpdate();
1265
+ updateCache = null; // next check reflects the new install
1266
+ sendJson(res, r.ok ? 200 : 502, r);
1267
+ return;
1268
+ }
1269
+ if (req.method === "POST" && url === "/api/update-restart") {
1270
+ if (!isLocalRequest(req)) {
1271
+ sendJson(res, 403, { ok: false, error: "restart only from this machine (localhost)" });
1272
+ return;
1273
+ }
1274
+ sendJson(res, 200, { ok: true });
1275
+ // Let the response flush, then exit 0: pm2/launchd/systemd restart the
1276
+ // process; a bare `beagle` in a terminal just exits (the UI says so).
1277
+ setTimeout(() => process.exit(0), 400);
1278
+ return;
1279
+ }
1128
1280
  // Public profile card for any userid (friend or directory row): what
1129
1281
  // they published on their beagles.eth record. Read-only, no signing.
1130
1282
  if (req.method === "GET" && url === "/api/ens-profile") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",