@decentnetwork/beagle 0.1.24 → 0.1.26
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/dist/desktop/app.js +122 -7
- package/dist/server.js +152 -0
- package/package.json +1 -1
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.25";
|
|
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"/>',
|
|
@@ -3191,7 +3191,7 @@ function DkDirAvatar({ p, size }) {
|
|
|
3191
3191
|
}
|
|
3192
3192
|
return /* @__PURE__ */ React.createElement(DkIdenticon, { seed: p.userid || p.name, size, radius: Math.round(size / 4) });
|
|
3193
3193
|
}
|
|
3194
|
-
function DkDirRow({ p, T, isFriend, isMe, onAdd, onOpenChat }) {
|
|
3194
|
+
function DkDirRow({ p, T, isFriend, isMe, onAdd, onOpenChat, onOpenProfile }) {
|
|
3195
3195
|
const [state, setState] = React.useState(null);
|
|
3196
3196
|
const add = () => {
|
|
3197
3197
|
setState("busy");
|
|
@@ -3200,7 +3200,10 @@ function DkDirRow({ p, T, isFriend, isMe, onAdd, onOpenChat }) {
|
|
|
3200
3200
|
const btn = (label, onClick, disabled) => /* @__PURE__ */ React.createElement(
|
|
3201
3201
|
"button",
|
|
3202
3202
|
{
|
|
3203
|
-
onClick
|
|
3203
|
+
onClick: (e) => {
|
|
3204
|
+
e.stopPropagation();
|
|
3205
|
+
onClick();
|
|
3206
|
+
},
|
|
3204
3207
|
disabled,
|
|
3205
3208
|
style: {
|
|
3206
3209
|
padding: "5px 11px",
|
|
@@ -3216,12 +3219,23 @@ function DkDirRow({ p, T, isFriend, isMe, onAdd, onOpenChat }) {
|
|
|
3216
3219
|
},
|
|
3217
3220
|
label
|
|
3218
3221
|
);
|
|
3219
|
-
return /* @__PURE__ */ React.createElement(
|
|
3222
|
+
return /* @__PURE__ */ React.createElement(
|
|
3223
|
+
"div",
|
|
3224
|
+
{
|
|
3225
|
+
onClick: () => onOpenProfile && onOpenProfile(p),
|
|
3226
|
+
title: T && T.pubOpen || "View public profile",
|
|
3227
|
+
style: { display: "flex", alignItems: "center", gap: 11, padding: "var(--row-pad)", borderBottom: "1px solid var(--line)", cursor: "pointer" }
|
|
3228
|
+
},
|
|
3229
|
+
/* @__PURE__ */ React.createElement(DkDirAvatar, { p, size: 34 }),
|
|
3230
|
+
/* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 2 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "baseline", gap: 8, minWidth: 0 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 13.5, fontWeight: 600, color: "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, p.name), p.points != null && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--accent)", flexShrink: 0 } }, p.points.toLocaleString(), " pts"), p.ens && p.ens !== p.name && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, p.ens)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 } }, /* @__PURE__ */ React.createElement(Mono, { size: 10.5, dim: true, copy: p.userid, title: p.userid }, shortKey(p.userid, 10, 6)), p.description && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--faint)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, p.description))),
|
|
3231
|
+
isMe ? /* @__PURE__ */ React.createElement(Tag, null, T.dirMe || "me") : isFriend ? btn(T.dirOpenChat || "chat", () => onOpenChat(p.userid), false) : state === "sent" ? /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, T.dirSent || "requested") : state && state !== "busy" ? /* @__PURE__ */ React.createElement("span", { title: state, style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--warn, #f59e0b)", flexShrink: 0 } }, T.addFailed || "failed") : btn(state === "busy" ? "\u2026" : T.dirAdd || "Add", add, state === "busy")
|
|
3232
|
+
);
|
|
3220
3233
|
}
|
|
3221
3234
|
function DiscoverTab({ T, kind, peers, meId, onAdd, onOpenChat }) {
|
|
3222
3235
|
const [list, setList] = React.useState(null);
|
|
3223
3236
|
const [err, setErr] = React.useState(null);
|
|
3224
3237
|
const [q, setQ] = React.useState("");
|
|
3238
|
+
const [prof, setProf] = React.useState(null);
|
|
3225
3239
|
React.useEffect(() => {
|
|
3226
3240
|
let dead = false;
|
|
3227
3241
|
setList(null);
|
|
@@ -3276,9 +3290,10 @@ function DiscoverTab({ T, kind, peers, meId, onAdd, onOpenChat }) {
|
|
|
3276
3290
|
isMe: !!meId && p.userid === meId,
|
|
3277
3291
|
isFriend: friendIds.has(p.userid),
|
|
3278
3292
|
onAdd,
|
|
3279
|
-
onOpenChat
|
|
3293
|
+
onOpenChat,
|
|
3294
|
+
onOpenProfile: (row) => setProf({ userid: row.userid, name: row.name })
|
|
3280
3295
|
}
|
|
3281
|
-
))));
|
|
3296
|
+
))), prof && /* @__PURE__ */ React.createElement(DkPublicProfile, { T, userid: prof.userid, fallbackName: prof.name, onClose: () => setProf(null) }));
|
|
3282
3297
|
}
|
|
3283
3298
|
const DK_FILE_RTC_KIND = "file";
|
|
3284
3299
|
const DK_FILE_RTC_CHUNK = 16 * 1024;
|
|
@@ -4214,6 +4229,16 @@ const STR = {
|
|
|
4214
4229
|
linksCard: "Links",
|
|
4215
4230
|
linkPlaceholder: "username or profile URL",
|
|
4216
4231
|
save: "save",
|
|
4232
|
+
updTitle: "Update available",
|
|
4233
|
+
updLater: "Later",
|
|
4234
|
+
updNow: "Update now",
|
|
4235
|
+
updRestart: "Restart",
|
|
4236
|
+
updBusy: "Updating\u2026 (about a minute)",
|
|
4237
|
+
updDone: "Updated \u2014 restart to apply.",
|
|
4238
|
+
updRestarting: "Restarting\u2026 this page reloads when it\u2019s back. (If beagle was started by hand in a terminal, run it again.)",
|
|
4239
|
+
updNpx: "Running via npx \u2014 restarting picks up the latest version automatically.",
|
|
4240
|
+
updDev: "Running from a source checkout \u2014 update with:",
|
|
4241
|
+
updLanNote: "agentnet (lan) updates separately; the daemon restart is manual:",
|
|
4217
4242
|
pubOpen: "View public profile",
|
|
4218
4243
|
pubNone: "No public profile \u2014 this user has not registered a beagles.eth name.",
|
|
4219
4244
|
ensNoEthWallet: "no Ethereum wallet extension found (MetaMask\u2026)",
|
|
@@ -4360,6 +4385,16 @@ const STR = {
|
|
|
4360
4385
|
linksCard: "\u793E\u4EA4\u8D26\u53F7",
|
|
4361
4386
|
linkPlaceholder: "\u7528\u6237\u540D\u6216\u4E3B\u9875\u94FE\u63A5",
|
|
4362
4387
|
save: "\u4FDD\u5B58",
|
|
4388
|
+
updTitle: "\u53D1\u73B0\u65B0\u7248\u672C",
|
|
4389
|
+
updLater: "\u7A0D\u540E",
|
|
4390
|
+
updNow: "\u7ACB\u5373\u66F4\u65B0",
|
|
4391
|
+
updRestart: "\u91CD\u542F",
|
|
4392
|
+
updBusy: "\u66F4\u65B0\u4E2D\u2026(\u7EA6\u4E00\u5206\u949F)",
|
|
4393
|
+
updDone: "\u66F4\u65B0\u5B8C\u6210\u2014\u2014\u91CD\u542F\u540E\u751F\u6548\u3002",
|
|
4394
|
+
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)",
|
|
4395
|
+
updNpx: "\u4F60\u662F\u7528 npx \u8FD0\u884C\u7684\u2014\u2014\u91CD\u542F\u5373\u81EA\u52A8\u83B7\u53D6\u6700\u65B0\u7248\u3002",
|
|
4396
|
+
updDev: "\u5F53\u524D\u4ECE\u6E90\u7801\u76EE\u5F55\u8FD0\u884C,\u8BF7\u624B\u52A8\u66F4\u65B0:",
|
|
4397
|
+
updLanNote: "agentnet(lan)\u9700\u5355\u72EC\u5347\u7EA7,daemon \u91CD\u542F\u9700\u624B\u52A8\u6267\u884C:",
|
|
4363
4398
|
pubOpen: "\u67E5\u770B\u516C\u5F00\u8D44\u6599",
|
|
4364
4399
|
pubNone: "\u6682\u65E0\u516C\u5F00\u8D44\u6599\u2014\u2014\u5BF9\u65B9\u8FD8\u6CA1\u6709\u6CE8\u518C beagles.eth \u540D\u5B57\u3002",
|
|
4365
4400
|
ensNoEthWallet: "\u672A\u68C0\u6D4B\u5230\u4EE5\u592A\u574A\u94B1\u5305\u63D2\u4EF6(MetaMask \u7B49)",
|
|
@@ -4409,12 +4444,82 @@ function RailBtn({ icon, label, active, soon, onClick }) {
|
|
|
4409
4444
|
gap: 3
|
|
4410
4445
|
} }, /* @__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
4446
|
}
|
|
4447
|
+
function DkUpdateModal({ T, info, onClose }) {
|
|
4448
|
+
const [phase, setPhase] = React.useState("idle");
|
|
4449
|
+
const [err, setErr] = React.useState(null);
|
|
4450
|
+
const behind = (info.updates || []).filter((u) => u.behind);
|
|
4451
|
+
const appBehind = behind.some((u) => u.pkg !== "@decentnetwork/lan");
|
|
4452
|
+
const lanBehind = behind.find((u) => u.pkg === "@decentnetwork/lan");
|
|
4453
|
+
const short = (pkg) => pkg.replace("@decentnetwork/", "");
|
|
4454
|
+
const doUpdate = () => {
|
|
4455
|
+
setPhase("running");
|
|
4456
|
+
setErr(null);
|
|
4457
|
+
fetch("/api/update-run", { method: "POST" }).then((r) => r.json()).then((d) => {
|
|
4458
|
+
if (d.ok)
|
|
4459
|
+
setPhase("done");
|
|
4460
|
+
else {
|
|
4461
|
+
setPhase("error");
|
|
4462
|
+
setErr(d.error || "update failed");
|
|
4463
|
+
}
|
|
4464
|
+
}).catch((e) => {
|
|
4465
|
+
setPhase("error");
|
|
4466
|
+
setErr(String(e && e.message || e));
|
|
4467
|
+
});
|
|
4468
|
+
};
|
|
4469
|
+
const doRestart = () => {
|
|
4470
|
+
setPhase("restarting");
|
|
4471
|
+
fetch("/api/update-restart", { method: "POST" }).catch(() => {
|
|
4472
|
+
});
|
|
4473
|
+
const poll = setInterval(() => {
|
|
4474
|
+
fetch("/api/update-check").then((r) => {
|
|
4475
|
+
if (r.ok) {
|
|
4476
|
+
clearInterval(poll);
|
|
4477
|
+
window.location.reload();
|
|
4478
|
+
}
|
|
4479
|
+
}).catch(() => {
|
|
4480
|
+
});
|
|
4481
|
+
}, 1500);
|
|
4482
|
+
};
|
|
4483
|
+
const btn = (label, onClick, tone) => /* @__PURE__ */ React.createElement("button", { onClick, style: {
|
|
4484
|
+
padding: "7px 14px",
|
|
4485
|
+
borderRadius: 9,
|
|
4486
|
+
cursor: "pointer",
|
|
4487
|
+
fontFamily: "var(--ui)",
|
|
4488
|
+
fontSize: 12.5,
|
|
4489
|
+
border: tone ? "none" : "1px solid var(--line)",
|
|
4490
|
+
background: tone ? "var(--accent)" : "transparent",
|
|
4491
|
+
color: tone ? "#fff" : "var(--text)"
|
|
4492
|
+
} }, label);
|
|
4493
|
+
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 && agentnet restart")), 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))));
|
|
4494
|
+
}
|
|
4412
4495
|
function DkApp() {
|
|
4413
4496
|
const [t, setTweak] = useTweaks(DK_DEFAULTS);
|
|
4414
4497
|
const [tab, setTab] = React.useState(t.startTab || "chat");
|
|
4415
4498
|
const [activeId, setActiveId] = React.useState(null);
|
|
4416
4499
|
const data = useDaemonData();
|
|
4417
4500
|
const me = data.me;
|
|
4501
|
+
const [upd, setUpd] = React.useState(null);
|
|
4502
|
+
React.useEffect(() => {
|
|
4503
|
+
let dead = false;
|
|
4504
|
+
const check = () => fetch("/api/update-check").then((r) => r.json()).then((d) => {
|
|
4505
|
+
if (dead || !d.ok)
|
|
4506
|
+
return;
|
|
4507
|
+
const behind = (d.updates || []).filter((u) => u.behind);
|
|
4508
|
+
if (!behind.length)
|
|
4509
|
+
return;
|
|
4510
|
+
const key = behind.map((u) => u.pkg + "@" + u.latest).join(",");
|
|
4511
|
+
if (localStorage.getItem("dk-upd-dismissed") === key)
|
|
4512
|
+
return;
|
|
4513
|
+
setUpd({ ...d, key });
|
|
4514
|
+
}).catch(() => {
|
|
4515
|
+
});
|
|
4516
|
+
check();
|
|
4517
|
+
const timer = setInterval(check, 6 * 3600 * 1e3);
|
|
4518
|
+
return () => {
|
|
4519
|
+
dead = true;
|
|
4520
|
+
clearInterval(timer);
|
|
4521
|
+
};
|
|
4522
|
+
}, []);
|
|
4418
4523
|
const [backend, setBackend] = React.useState(null);
|
|
4419
4524
|
const refreshBackend = React.useCallback(() => {
|
|
4420
4525
|
dkGet("/api/backend").then((b) => {
|
|
@@ -4631,6 +4736,16 @@ function DkApp() {
|
|
|
4631
4736
|
color: "#fff",
|
|
4632
4737
|
fontSize: 12,
|
|
4633
4738
|
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 })
|
|
4739
|
+
} }, 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(
|
|
4740
|
+
DkUpdateModal,
|
|
4741
|
+
{
|
|
4742
|
+
T,
|
|
4743
|
+
info: upd,
|
|
4744
|
+
onClose: () => {
|
|
4745
|
+
localStorage.setItem("dk-upd-dismissed", upd.key);
|
|
4746
|
+
setUpd(null);
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
));
|
|
4635
4750
|
}
|
|
4636
4751
|
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