@decentnetwork/beagle 0.1.90 → 0.1.92
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/cli.js +18 -1
- package/dist/desktop/app.js +4 -4
- package/dist/relaunch.d.ts +8 -0
- package/dist/relaunch.js +85 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +68 -21
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { dirname, join, resolve } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import { startBeagleServer } from "./server.js";
|
|
11
|
+
import { startBeagleServer, installMode } from "./server.js";
|
|
12
|
+
import { isSupervised, relaunchSelf, waitForPredecessor } from "./relaunch.js";
|
|
12
13
|
import { openPeerHost, defaultConfigDir, decentlanCarrierDir, makeDaemonHost } from "./peer-host.js";
|
|
13
14
|
import { SwitchablePeerHost } from "./lan-handoff.js";
|
|
14
15
|
import { loadNodeConfig, saveAutoAccept } from "./node-config.js";
|
|
@@ -97,6 +98,9 @@ const resolveVer = (pkg) => {
|
|
|
97
98
|
};
|
|
98
99
|
async function main() {
|
|
99
100
|
const args = parseArgs(process.argv.slice(2));
|
|
101
|
+
// Spawned by a predecessor restarting itself after an update: let it finish
|
|
102
|
+
// exiting before touching the identity lock or the port. No-op otherwise.
|
|
103
|
+
await waitForPredecessor();
|
|
100
104
|
// Bootstrap list: prefer the user's decentlan config so both stacks agree on
|
|
101
105
|
// the fleet, else fall back to the shipped defaults. A wrong list here is the
|
|
102
106
|
// difference between "joins the network" and "silently alone".
|
|
@@ -189,6 +193,19 @@ async function main() {
|
|
|
189
193
|
},
|
|
190
194
|
listenHost: args.host,
|
|
191
195
|
listenPort: args.port,
|
|
196
|
+
// After an update: stop the peer cleanly, then either let the supervisor
|
|
197
|
+
// relaunch us or, with none, spawn our own successor. The successor waits
|
|
198
|
+
// for this pid to be gone (waitForPredecessor above), so the identity
|
|
199
|
+
// lock and the port are free by the time it needs them.
|
|
200
|
+
onRestart: async () => {
|
|
201
|
+
await Promise.race([
|
|
202
|
+
peerHost.stop().catch(() => undefined),
|
|
203
|
+
new Promise((r) => setTimeout(r, 5000)),
|
|
204
|
+
]);
|
|
205
|
+
if (!isSupervised())
|
|
206
|
+
relaunchSelf(installMode());
|
|
207
|
+
process.exit(0);
|
|
208
|
+
},
|
|
192
209
|
});
|
|
193
210
|
const shutdown = async () => {
|
|
194
211
|
await peerHost.stop().catch(() => undefined);
|
package/dist/desktop/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
window.__DK_UI_VERSION="0.1.
|
|
1
|
+
window.__DK_UI_VERSION="0.1.92";
|
|
2
2
|
"use strict";
|
|
3
3
|
(() => {
|
|
4
4
|
// packages/beagle-ui/dist/beagle-ui.js
|
|
@@ -2443,7 +2443,7 @@ ${peer.address}`
|
|
|
2443
2443
|
const register = () => {
|
|
2444
2444
|
const l = label.trim();
|
|
2445
2445
|
if (l)
|
|
2446
|
-
post("/api/ens-register", { name: l, displayName: me.name });
|
|
2446
|
+
post("/api/ens-register", { name: l, displayName: me.name, description: me.description || "" });
|
|
2447
2447
|
};
|
|
2448
2448
|
const bind = (chain, address) => post("/api/ens-bind-wallet", { chain, address });
|
|
2449
2449
|
const bindEth = async () => {
|
|
@@ -4755,7 +4755,7 @@ ${peer.address}`
|
|
|
4755
4755
|
updRestart: "Restart",
|
|
4756
4756
|
updBusy: "Updating\u2026 (about a minute)",
|
|
4757
4757
|
updDone: "Updated \u2014 restart to apply.",
|
|
4758
|
-
updRestarting: "Restarting\u2026 this page reloads when it\u2019s back. (If
|
|
4758
|
+
updRestarting: "Restarting\u2026 this page reloads when it\u2019s back. (If it has not come back after a minute, run beagle again.)",
|
|
4759
4759
|
updNpx: "Running via npx \u2014 restarting picks up the latest version automatically.",
|
|
4760
4760
|
updDev: "Running from a source checkout \u2014 update with:",
|
|
4761
4761
|
updPeerDaemon: "The peer version belongs to the running agentnet daemon, not to this app. Update it with:",
|
|
@@ -4937,7 +4937,7 @@ ${peer.address}`
|
|
|
4937
4937
|
updRestart: "\u91CD\u542F",
|
|
4938
4938
|
updBusy: "\u66F4\u65B0\u4E2D\u2026(\u7EA6\u4E00\u5206\u949F)",
|
|
4939
4939
|
updDone: "\u66F4\u65B0\u5B8C\u6210\u2014\u2014\u91CD\u542F\u540E\u751F\u6548\u3002",
|
|
4940
|
-
updRestarting: "\u6B63\u5728\u91CD\u542F\u2026\u5B8C\u6210\u540E\u672C\u9875\u4F1A\u81EA\u52A8\u5237\u65B0\u3002(\u5982\u679C
|
|
4940
|
+
updRestarting: "\u6B63\u5728\u91CD\u542F\u2026\u5B8C\u6210\u540E\u672C\u9875\u4F1A\u81EA\u52A8\u5237\u65B0\u3002(\u5982\u679C\u4E00\u5206\u949F\u540E\u8FD8\u6CA1\u56DE\u6765,\u8BF7\u91CD\u65B0\u8FD0\u884C\u4E00\u6B21 beagle\u3002)",
|
|
4941
4941
|
updNpx: "\u4F60\u662F\u7528 npx \u8FD0\u884C\u7684\u2014\u2014\u91CD\u542F\u5373\u81EA\u52A8\u83B7\u53D6\u6700\u65B0\u7248\u3002",
|
|
4942
4942
|
updDev: "\u5F53\u524D\u4ECE\u6E90\u7801\u76EE\u5F55\u8FD0\u884C,\u8BF7\u624B\u52A8\u66F4\u65B0:",
|
|
4943
4943
|
updPeerDaemon: "peer \u7684\u7248\u672C\u5C5E\u4E8E\u6B63\u5728\u8FD0\u884C\u7684 agentnet daemon,\u4E0D\u5C5E\u4E8E\u672C\u5E94\u7528\u3002\u8BF7\u7528\u8FD9\u6761\u547D\u4EE4\u66F4\u65B0:",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Something outside this process will restart it when it exits. */
|
|
2
|
+
export declare function isSupervised(): boolean;
|
|
3
|
+
/** Start the successor. Caller exits right after. */
|
|
4
|
+
export declare function relaunchSelf(mode: "global" | "npx" | "dev" | "unknown"): void;
|
|
5
|
+
/** In the successor: block until the process that spawned us has exited.
|
|
6
|
+
* No-op when we were not spawned by relaunchSelf. Gives up after 20s rather
|
|
7
|
+
* than hang forever behind a predecessor that will not die. */
|
|
8
|
+
export declare function waitForPredecessor(): Promise<void>;
|
package/dist/relaunch.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// relaunch.ts — bring Beagle back after a self-update when nobody else will.
|
|
2
|
+
//
|
|
3
|
+
// "Restart" after an update used to be process.exit(0) and a hope: pm2,
|
|
4
|
+
// launchd and systemd relaunch a process that exits, so under any of them
|
|
5
|
+
// that was a restart. A `beagle` started by hand in a terminal has no such
|
|
6
|
+
// minder — it exited and stayed exited, the page sat on "Restarting…"
|
|
7
|
+
// forever, and the person had to notice and run it again (air-wli,
|
|
8
|
+
// 2026-09-12). The dialog even admitted as much in parentheses.
|
|
9
|
+
//
|
|
10
|
+
// When no supervisor is detected, spawn our own successor before exiting:
|
|
11
|
+
// the same node, the same script path (which after `npm i -g` now holds the
|
|
12
|
+
// new version), the same arguments, detached so our exit does not take it
|
|
13
|
+
// down. The successor is told our pid and waits for us to be gone before it
|
|
14
|
+
// opens the peer or binds the port, so it never races us for the identity
|
|
15
|
+
// lock or gets EADDRINUSE from a parent still shutting down.
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
const WAIT_ENV = "BEAGLE_RELAUNCH_WAIT_PID";
|
|
18
|
+
/** Set on a successor we spawned, and inherited by its successors in turn.
|
|
19
|
+
* A detached child is reparented to pid 1, which looks exactly like a
|
|
20
|
+
* launchd job — without this mark the SECOND update would have exited
|
|
21
|
+
* and stayed exited, which is the bug this file exists to fix. */
|
|
22
|
+
const SELF_ENV = "BEAGLE_SELF_RELAUNCHED";
|
|
23
|
+
/** Something outside this process will restart it when it exits. */
|
|
24
|
+
export function isSupervised() {
|
|
25
|
+
const env = process.env;
|
|
26
|
+
if (env[SELF_ENV])
|
|
27
|
+
return false; // we are our own minder
|
|
28
|
+
if (env.pm_id !== undefined)
|
|
29
|
+
return true; // pm2
|
|
30
|
+
if (env.INVOCATION_ID || env.JOURNAL_STREAM)
|
|
31
|
+
return true; // systemd
|
|
32
|
+
if (process.pid === 1)
|
|
33
|
+
return true; // a container with a restart policy
|
|
34
|
+
// launchd is the parent of every job it runs. A hand-started process in a
|
|
35
|
+
// terminal has the shell as its parent; only when that shell is gone does
|
|
36
|
+
// it get reparented to 1 — and then nobody was restarting it anyway, but a
|
|
37
|
+
// process that outlived its terminal is not one a person is watching, so
|
|
38
|
+
// exiting quietly is the lesser surprise.
|
|
39
|
+
if (process.platform === "darwin" && process.ppid === 1)
|
|
40
|
+
return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/** Start the successor. Caller exits right after. */
|
|
44
|
+
export function relaunchSelf(mode) {
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
let cmd;
|
|
47
|
+
let cmdArgs;
|
|
48
|
+
if (mode === "npx") {
|
|
49
|
+
// argv[1] points into npx's cache of the OLD version. `@latest`
|
|
50
|
+
// re-resolves, which is the whole update for an npx user.
|
|
51
|
+
cmd = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
52
|
+
cmdArgs = ["-y", "@decentnetwork/beagle@latest", ...args];
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
cmd = process.execPath;
|
|
56
|
+
cmdArgs = [process.argv[1], ...args];
|
|
57
|
+
}
|
|
58
|
+
const child = spawn(cmd, cmdArgs, {
|
|
59
|
+
detached: true,
|
|
60
|
+
// Keep the terminal this was started from as the log, exactly as before.
|
|
61
|
+
stdio: "inherit",
|
|
62
|
+
env: { ...process.env, [WAIT_ENV]: String(process.pid), [SELF_ENV]: "1" },
|
|
63
|
+
shell: process.platform === "win32" && mode === "npx",
|
|
64
|
+
});
|
|
65
|
+
child.unref();
|
|
66
|
+
}
|
|
67
|
+
/** In the successor: block until the process that spawned us has exited.
|
|
68
|
+
* No-op when we were not spawned by relaunchSelf. Gives up after 20s rather
|
|
69
|
+
* than hang forever behind a predecessor that will not die. */
|
|
70
|
+
export async function waitForPredecessor() {
|
|
71
|
+
const pid = Number(process.env[WAIT_ENV]);
|
|
72
|
+
delete process.env[WAIT_ENV];
|
|
73
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
74
|
+
return;
|
|
75
|
+
const deadline = Date.now() + 20_000;
|
|
76
|
+
while (Date.now() < deadline) {
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, 0);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return; // ESRCH: gone. (EPERM cannot happen — same user, our own parent.)
|
|
82
|
+
}
|
|
83
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
84
|
+
}
|
|
85
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { IpcRequest, IpcResponse } from "./ipc.js";
|
|
2
|
+
/** How this copy of beagle got onto disk — decides what "update" can do.
|
|
3
|
+
* npx cache paths contain _npx AND node_modules, so test _npx first. */
|
|
4
|
+
export declare function installMode(): "global" | "npx" | "dev" | "unknown";
|
|
2
5
|
export interface BeagleServerOptions {
|
|
3
6
|
/** Call the running daemon over IPC (injected so this module stays
|
|
4
7
|
* decoupled from the CLI's ipc client). */
|
|
@@ -35,6 +38,10 @@ export interface BeagleServerOptions {
|
|
|
35
38
|
}>;
|
|
36
39
|
cancelArm: () => Promise<string>;
|
|
37
40
|
};
|
|
41
|
+
/** Called instead of process.exit(0) on /api/update-restart, so the
|
|
42
|
+
* process that owns the peer can shut it down and relaunch itself when
|
|
43
|
+
* no supervisor will (see relaunch.ts). */
|
|
44
|
+
onRestart?: () => void | Promise<void>;
|
|
38
45
|
listenHost?: string;
|
|
39
46
|
listenPort?: number;
|
|
40
47
|
log?: (msg: string) => void;
|
package/dist/server.js
CHANGED
|
@@ -22,6 +22,7 @@ import yaml from "js-yaml";
|
|
|
22
22
|
import { DEFAULT_EXITS } from "./exits.js";
|
|
23
23
|
import * as peerAddr from "@decentnetwork/peer";
|
|
24
24
|
import { bridgeRoster } from "./bridge-roster.js";
|
|
25
|
+
import { isSupervised } from "./relaunch.js";
|
|
25
26
|
// Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
|
|
26
27
|
// scripts/build-ui.mjs emits it next to this compiled module at dist/ui/desktop/.
|
|
27
28
|
const DESKTOP_DIR = join(dirname(fileURLToPath(import.meta.url)), "desktop");
|
|
@@ -74,6 +75,29 @@ async function ensFetchJson(path) {
|
|
|
74
75
|
clearTimeout(timer);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
/** The record beagle writes for its own name: what the profile page shows
|
|
79
|
+
* (displayName, description) on top of whatever the record already held —
|
|
80
|
+
* wallet binds, socials, avatar, referee — so a profile edit never strips a
|
|
81
|
+
* bind. Shared by /api/ens-register and /api/set-profile; the two used to
|
|
82
|
+
* build it separately, and only register did, which is why a status message
|
|
83
|
+
* edited in Profile never reached https://app.beagle.chat/<name>. */
|
|
84
|
+
function ensProfileRecord(identity, name, carried, displayName, description) {
|
|
85
|
+
return {
|
|
86
|
+
name,
|
|
87
|
+
owner: identity.userid,
|
|
88
|
+
addresses: carried?.addresses ?? {},
|
|
89
|
+
texts: {
|
|
90
|
+
...(carried?.texts ?? {}),
|
|
91
|
+
description,
|
|
92
|
+
displayName,
|
|
93
|
+
carrierAddress: identity.address,
|
|
94
|
+
carrierUserId: identity.userid,
|
|
95
|
+
},
|
|
96
|
+
referee: carried?.referee ?? "",
|
|
97
|
+
nft: carried?.nft ?? "",
|
|
98
|
+
nftid: carried?.nftid ?? 0,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
77
101
|
/** Sign a name record with the Carrier identity and POST it to the gateway.
|
|
78
102
|
* The record itself (as a JSON string) is what gets signed; the gateway
|
|
79
103
|
* re-parses `signature.message` and verifies against `owner` inside it. */
|
|
@@ -268,7 +292,7 @@ async function runningVersions(call) {
|
|
|
268
292
|
}
|
|
269
293
|
/** How this copy of beagle got onto disk — decides what "update" can do.
|
|
270
294
|
* npx cache paths contain _npx AND node_modules, so test _npx first. */
|
|
271
|
-
function installMode() {
|
|
295
|
+
export function installMode() {
|
|
272
296
|
if (BEAGLE_ROOT.includes("_npx"))
|
|
273
297
|
return "npx";
|
|
274
298
|
if (BEAGLE_ROOT.includes(join("node_modules", "@decentnetwork", "beagle")))
|
|
@@ -1218,24 +1242,15 @@ export function startBeagleServer(opts) {
|
|
|
1218
1242
|
const mine = existing ?? (await ensFetchJson(`/getAddress/${encodeURIComponent(identity.userid)}`));
|
|
1219
1243
|
const carried = mine && mine.owner === identity.userid ? mine : null;
|
|
1220
1244
|
const displayName = (typeof body.displayName === "string" && body.displayName.trim().slice(0, 64)) || label;
|
|
1245
|
+
// The description IS the Profile status message. It used to fall
|
|
1246
|
+
// through to "<name>'s Beagle Chat Profile", a placeholder that then
|
|
1247
|
+
// sat on the public page as if the person had written it.
|
|
1248
|
+
const node = (diag.ok ? diag.data?.node : null) ?? {};
|
|
1221
1249
|
const description = (typeof body.description === "string" && body.description.trim().slice(0, 200)) ||
|
|
1250
|
+
(typeof node.statusMessage === "string" && node.statusMessage.trim().slice(0, 200)) ||
|
|
1222
1251
|
carried?.texts?.description ||
|
|
1223
|
-
|
|
1224
|
-
const r = await ensSignAndSet(opts.call, {
|
|
1225
|
-
name: fullName,
|
|
1226
|
-
owner: identity.userid,
|
|
1227
|
-
addresses: carried?.addresses ?? {},
|
|
1228
|
-
texts: {
|
|
1229
|
-
...(carried?.texts ?? {}),
|
|
1230
|
-
description,
|
|
1231
|
-
displayName,
|
|
1232
|
-
carrierAddress: identity.address,
|
|
1233
|
-
carrierUserId: identity.userid,
|
|
1234
|
-
},
|
|
1235
|
-
referee: carried?.referee ?? "",
|
|
1236
|
-
nft: carried?.nft ?? "",
|
|
1237
|
-
nftid: carried?.nftid ?? 0,
|
|
1238
|
-
});
|
|
1252
|
+
"";
|
|
1253
|
+
const r = await ensSignAndSet(opts.call, ensProfileRecord({ userid: identity.userid, address: identity.address }, fullName, carried, displayName, description));
|
|
1239
1254
|
sendJson(res, r.ok ? 200 : 502, r.ok ? { ok: true, name: fullName } : { ok: false, error: r.error });
|
|
1240
1255
|
return;
|
|
1241
1256
|
}
|
|
@@ -1796,10 +1811,17 @@ export function startBeagleServer(opts) {
|
|
|
1796
1811
|
sendJson(res, 403, { ok: false, error: "restart only from this machine (localhost)" });
|
|
1797
1812
|
return;
|
|
1798
1813
|
}
|
|
1799
|
-
sendJson(res, 200, { ok: true });
|
|
1800
|
-
// Let the response flush, then
|
|
1801
|
-
//
|
|
1802
|
-
|
|
1814
|
+
sendJson(res, 200, { ok: true, supervised: isSupervised() });
|
|
1815
|
+
// Let the response flush, then hand over. Under pm2/launchd/systemd an
|
|
1816
|
+
// exit IS the restart; a bare `beagle` in a terminal relaunches itself
|
|
1817
|
+
// (cli.ts onRestart) — it used to just exit and stay exited.
|
|
1818
|
+
setTimeout(() => {
|
|
1819
|
+
if (!opts.onRestart) {
|
|
1820
|
+
process.exit(0);
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
void Promise.resolve(opts.onRestart()).catch(() => process.exit(0));
|
|
1824
|
+
}, 400);
|
|
1803
1825
|
return;
|
|
1804
1826
|
}
|
|
1805
1827
|
// Public profile card for any userid (friend or directory row): what
|
|
@@ -2249,6 +2271,31 @@ export function startBeagleServer(opts) {
|
|
|
2249
2271
|
const { name, description, punkId } = await readBody(req);
|
|
2250
2272
|
const r = await opts.call({ op: "set-profile", name, description, punkId });
|
|
2251
2273
|
sendJson(res, r.ok ? 200 : 400, r);
|
|
2274
|
+
// Also the public record, when this identity registered a beagles.eth
|
|
2275
|
+
// name here: displayName and description on the record ARE the
|
|
2276
|
+
// profile name and status message, and editing them in Profile used
|
|
2277
|
+
// to change only what Carrier advertises, never the page. Best
|
|
2278
|
+
// effort, after the reply: the gateway being down must not turn a
|
|
2279
|
+
// profile save into a failure. A punk-only call (the avatar advertise
|
|
2280
|
+
// on poll) carries neither field and leaves the record alone.
|
|
2281
|
+
if (r.ok && (typeof name === "string" || typeof description === "string")) {
|
|
2282
|
+
void (async () => {
|
|
2283
|
+
const diag = await opts.call({ op: "diag" });
|
|
2284
|
+
const identity = (diag.ok ? diag.data?.identity : null) ?? {};
|
|
2285
|
+
if (!identity.userid || !identity.address)
|
|
2286
|
+
return;
|
|
2287
|
+
const mine = await ensFetchJson(`/getAddress/${encodeURIComponent(identity.userid)}`);
|
|
2288
|
+
if (!mine || mine.owner !== identity.userid)
|
|
2289
|
+
return; // not registered here — nothing to sign
|
|
2290
|
+
const displayName = (typeof name === "string" && name.trim().slice(0, 64)) ||
|
|
2291
|
+
mine.texts?.displayName ||
|
|
2292
|
+
mine.name.replace(/\.beagles\.eth$/, "");
|
|
2293
|
+
const desc = typeof description === "string" ? description.trim().slice(0, 200) : (mine.texts?.description ?? "");
|
|
2294
|
+
const w = await ensSignAndSet(opts.call, ensProfileRecord({ userid: identity.userid, address: identity.address }, mine.name, mine, displayName, desc));
|
|
2295
|
+
if (!w.ok)
|
|
2296
|
+
console.warn(`[ens] profile saved, but the ${mine.name} record was not updated: ${w.error}`);
|
|
2297
|
+
})().catch((e) => console.warn(`[ens] record update after profile save failed: ${e.message}`));
|
|
2298
|
+
}
|
|
2252
2299
|
return;
|
|
2253
2300
|
}
|
|
2254
2301
|
if (req.method === "GET" && url === "/api/routes") {
|
package/package.json
CHANGED