routstrd 0.4.7 → 0.4.9
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/README.md +96 -6
- package/SKILL.md +341 -0
- package/dist/daemon/index.js +31092 -31824
- package/dist/index.js +162637 -14155
- package/package.json +9 -4
- package/src/cli.ts +279 -110
- package/src/daemon/args.ts +4 -5
- package/src/daemon/http/index.ts +37 -2
- package/src/daemon/index.ts +94 -33
- package/src/daemon/wallet/auto-refill.ts +1 -1
- package/src/daemon/wallet/coco-client.ts +205 -3
- package/src/daemon/wallet/cocod-client.ts +19 -3
- package/src/daemon/wallet/index.ts +1 -1
- package/src/index.ts +9 -1
- package/src/runtime.ts +55 -0
- package/src/start-daemon.ts +4 -8
- package/src/utils/clients.ts +74 -1
- package/src/utils/config.ts +14 -0
- package/src/utils/daemon-stop.ts +125 -0
- package/src/utils/standalone-update.ts +214 -0
- package/src/utils/update-checker.ts +81 -18
- package/src/version.ts +3 -0
- package/src/cli.test.ts +0 -192
- package/src/daemon/fatal-error.test.ts +0 -104
- package/src/daemon/http/usage-summary.test.ts +0 -272
- package/src/daemon/models.test.ts +0 -81
- package/src/daemon/wallet/cleanup.test.ts +0 -168
- package/src/daemon/wallet/coco-client.npc.test.ts +0 -252
- package/src/daemon/wallet/coco-client.test.ts +0 -669
- package/src/daemon/wallet/diagnostics.test.ts +0 -378
- package/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz +0 -0
- package/src/daemon/wallet/migration.test.ts +0 -171
- package/src/daemon/wallet/receive-dedup.test.ts +0 -238
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
"routstrd": "./dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist",
|
|
11
|
+
"dist/index.js",
|
|
12
|
+
"dist/daemon/index.js",
|
|
12
13
|
"src",
|
|
14
|
+
"!src/**/*.test.ts",
|
|
15
|
+
"!src/daemon/wallet/fixtures",
|
|
13
16
|
"README.md",
|
|
14
|
-
"SECURITY.md"
|
|
17
|
+
"SECURITY.md",
|
|
18
|
+
"SKILL.md"
|
|
15
19
|
],
|
|
16
20
|
"scripts": {
|
|
17
21
|
"start": "bun src/index.ts start",
|
|
@@ -20,6 +24,7 @@
|
|
|
20
24
|
"lint": "tsc --noEmit",
|
|
21
25
|
"test": "bun test",
|
|
22
26
|
"build": "bun build src/index.ts --target=bun --outfile=dist/index.js --external better-sqlite3 && bun build src/daemon/index.ts --target=bun --outfile=dist/daemon/index.js --external better-sqlite3",
|
|
27
|
+
"build:binary": "bun build --compile --no-compile-autoload-dotenv --no-compile-autoload-bunfig src/index.ts --outfile=dist/routstrd",
|
|
23
28
|
"prepublishOnly": "bun run build"
|
|
24
29
|
},
|
|
25
30
|
"devDependencies": {
|
|
@@ -33,7 +38,7 @@
|
|
|
33
38
|
"@cashu/cashu-ts": "^4.3.0",
|
|
34
39
|
"@cashu/coco-core": "^1.0.1",
|
|
35
40
|
"@cashu/coco-sqlite-bun": "^1.0.1",
|
|
36
|
-
"@routstr/sdk": "^0.4.
|
|
41
|
+
"@routstr/sdk": "^0.4.4",
|
|
37
42
|
"@scure/bip39": "^2.2.0",
|
|
38
43
|
"applesauce-core": "^5.1.0",
|
|
39
44
|
"applesauce-relay": "^5.1.0",
|
package/src/cli.ts
CHANGED
|
@@ -11,10 +11,13 @@ import {
|
|
|
11
11
|
getDaemonBaseUrl,
|
|
12
12
|
getUserNpub,
|
|
13
13
|
} from "./utils/daemon-client";
|
|
14
|
+
import { waitForDaemonToExit } from "./utils/daemon-stop";
|
|
14
15
|
import {
|
|
15
16
|
listClientsAction,
|
|
16
17
|
deleteClientAction,
|
|
17
18
|
addClientAction,
|
|
19
|
+
refreshModelsAndClientsAction,
|
|
20
|
+
setAutomaticRefreshAction,
|
|
18
21
|
} from "./utils/clients";
|
|
19
22
|
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
20
23
|
import { execSync } from "child_process";
|
|
@@ -28,7 +31,7 @@ import {
|
|
|
28
31
|
type RoutstrdConfig,
|
|
29
32
|
} from "./utils/config";
|
|
30
33
|
import { COCO_LOGS_DIR, logger } from "./utils/logger";
|
|
31
|
-
import { setupIntegration,
|
|
34
|
+
import { setupIntegration, type IntegrationKey } from "./integrations";
|
|
32
35
|
import {
|
|
33
36
|
assertLegacyCocodNotRunning,
|
|
34
37
|
claimLegacyCocodPidFile,
|
|
@@ -48,18 +51,22 @@ import {
|
|
|
48
51
|
walletDir as defaultWalletDir,
|
|
49
52
|
walletPidPath,
|
|
50
53
|
} from "./daemon/wallet/paths";
|
|
51
|
-
import { getClientsList } from "./utils/clients";
|
|
52
54
|
import * as QRCode from "qrcode";
|
|
53
55
|
import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98";
|
|
54
56
|
import { generateSecretKey, nip19 } from "nostr-tools";
|
|
55
57
|
import { generateMnemonic } from "@scure/bip39";
|
|
56
58
|
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
57
|
-
import packageJson from "../package.json" with { type: "json" };
|
|
58
59
|
import {
|
|
59
60
|
compareVersions,
|
|
60
61
|
getGlobalPackageVersion,
|
|
61
62
|
getLatestNpmVersion,
|
|
62
63
|
} from "./utils/update-checker.ts";
|
|
64
|
+
import { isStandaloneExecutable, pm2DaemonArgs } from "./runtime";
|
|
65
|
+
import { VERSION } from "./version";
|
|
66
|
+
import {
|
|
67
|
+
getLatestStandaloneRelease,
|
|
68
|
+
installStandaloneRelease,
|
|
69
|
+
} from "./utils/standalone-update";
|
|
63
70
|
|
|
64
71
|
type RoutstrModel = {
|
|
65
72
|
id: string;
|
|
@@ -105,6 +112,40 @@ async function printLightningInvoice(invoice: string): Promise<void> {
|
|
|
105
112
|
console.log(`${qr}\nInvoice:\n${invoice}`);
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
/** Block until the daemon has minted the paid invoice, then show the credit. */
|
|
116
|
+
async function waitForMintQuote(operationId: string): Promise<void> {
|
|
117
|
+
console.log(
|
|
118
|
+
"\nWaiting for payment... (Ctrl+C stops waiting; the daemon keeps checking)",
|
|
119
|
+
);
|
|
120
|
+
for (;;) {
|
|
121
|
+
const result = await callDaemon(
|
|
122
|
+
`/wallet/receive/bolt11/${encodeURIComponent(operationId)}`,
|
|
123
|
+
);
|
|
124
|
+
const quote = result.output as
|
|
125
|
+
| { state?: string; amount?: number; error?: string }
|
|
126
|
+
| undefined;
|
|
127
|
+
if (quote?.state === "finalized" && quote.error) {
|
|
128
|
+
// The mint issued the quote but the wallet could not restore the proofs.
|
|
129
|
+
console.error(`Payment reached the mint but could not be credited: ${quote.error}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (quote?.state === "finalized") {
|
|
133
|
+
const balance = await callDaemon("/wallet/balance");
|
|
134
|
+
const balances =
|
|
135
|
+
(balance.output as { balances?: Record<string, number> } | undefined)
|
|
136
|
+
?.balances ?? {};
|
|
137
|
+
const total = Object.values(balances).reduce((sum, sats) => sum + sats, 0);
|
|
138
|
+
console.log(`Received ${quote.amount} sat. Balance: ${total} sat`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (quote?.state === "failed") {
|
|
142
|
+
console.error(`Invoice failed: ${quote.error ?? "unknown reason"}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
108
149
|
export function initializeWallet(walletDir = defaultWalletDir()): void {
|
|
109
150
|
const walletConfig = join(walletDir, "config.json");
|
|
110
151
|
|
|
@@ -134,9 +175,38 @@ export function initializeWallet(walletDir = defaultWalletDir()): void {
|
|
|
134
175
|
console.log("IMPORTANT: Write down this mnemonic and keep it safe!");
|
|
135
176
|
}
|
|
136
177
|
|
|
178
|
+
type PidFileDeps = {
|
|
179
|
+
readFile(path: string): string;
|
|
180
|
+
isProcessRunning(pid: number): boolean;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export function getLivePidFileOwner(
|
|
184
|
+
path: string,
|
|
185
|
+
deps: PidFileDeps = {
|
|
186
|
+
readFile: (pidPath) => readFileSync(pidPath, "utf8"),
|
|
187
|
+
isProcessRunning: (pid) => {
|
|
188
|
+
try {
|
|
189
|
+
process.kill(pid, 0);
|
|
190
|
+
return true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
): number | null {
|
|
197
|
+
try {
|
|
198
|
+
const contents = deps.readFile(path).trim();
|
|
199
|
+
if (!/^\d+$/.test(contents)) return null;
|
|
200
|
+
const pid = Number.parseInt(contents, 10);
|
|
201
|
+
return pid > 0 && deps.isProcessRunning(pid) ? pid : null;
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
137
207
|
/**
|
|
138
208
|
* Restart the routstrd daemon after an update so the new binary takes
|
|
139
|
-
* effect immediately.
|
|
209
|
+
* effect immediately. Failures are collected and reported but never
|
|
140
210
|
* roll back the update itself.
|
|
141
211
|
*
|
|
142
212
|
* Note: cocod is no longer a separate process — the wallet now runs
|
|
@@ -156,22 +226,22 @@ async function restartDaemonsAfterUpdate(): Promise<void> {
|
|
|
156
226
|
if (!wasRunning) {
|
|
157
227
|
console.log("\nroutstrd daemon was not running — skipping restart.");
|
|
158
228
|
} else {
|
|
159
|
-
console.log("\nRestarting routstrd daemon...");
|
|
160
|
-
|
|
161
|
-
await callDaemon("/stop", { method: "POST" });
|
|
162
|
-
|
|
163
|
-
// Wait for HTTP health check to fail AND wallet lock to be released.
|
|
164
229
|
const pidFilePath = walletPidPath();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
230
|
+
const ownerPid = getLivePidFileOwner(pidFilePath);
|
|
231
|
+
if (ownerPid === null) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Refusing to stop the process at ${getDaemonBaseUrl(config)} because ` +
|
|
234
|
+
`no live routstrd owner was found in ${pidFilePath}`,
|
|
235
|
+
);
|
|
170
236
|
}
|
|
171
237
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
238
|
+
console.log(`\nRestarting routstrd daemon (PID: ${ownerPid})...`);
|
|
239
|
+
await callDaemon("/stop", { method: "POST" });
|
|
240
|
+
|
|
241
|
+
// Wait for the old daemon to fully exit: it keeps serving ongoing
|
|
242
|
+
// requests before it releases the wallet lock and the new daemon can
|
|
243
|
+
// safely claim it.
|
|
244
|
+
await waitForDaemonToExit({ pidFilePath });
|
|
175
245
|
console.log("routstrd daemon stopped.");
|
|
176
246
|
|
|
177
247
|
await stopLegacyCocod();
|
|
@@ -290,12 +360,26 @@ async function initDaemon(integrationKey?: IntegrationKey): Promise<void> {
|
|
|
290
360
|
program
|
|
291
361
|
.name("routstrd")
|
|
292
362
|
.description("Routstr daemon - Manage routstr processes")
|
|
293
|
-
.version(
|
|
363
|
+
.version(VERSION, "--version", "output the version number");
|
|
294
364
|
|
|
295
365
|
program
|
|
296
366
|
.command("update")
|
|
297
367
|
.description("Update routstrd to the latest version")
|
|
298
368
|
.action(async () => {
|
|
369
|
+
if (isStandaloneExecutable()) {
|
|
370
|
+
const release = await getLatestStandaloneRelease();
|
|
371
|
+
if ((compareVersions(VERSION, release.version) ?? -1) >= 0) {
|
|
372
|
+
console.log(`routstrd is already up to date (v${VERSION}).`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
console.log(`Updating routstrd from v${VERSION} to v${release.version}...`);
|
|
377
|
+
await installStandaloneRelease(release);
|
|
378
|
+
console.log("routstrd updated successfully.\n");
|
|
379
|
+
await restartDaemonsAfterUpdate();
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
299
383
|
const packages = [{ name: "routstrd", label: "routstrd" }];
|
|
300
384
|
|
|
301
385
|
let updatedAny = false;
|
|
@@ -637,6 +721,22 @@ program
|
|
|
637
721
|
},
|
|
638
722
|
);
|
|
639
723
|
|
|
724
|
+
program
|
|
725
|
+
.command("daemon")
|
|
726
|
+
.description("Run the daemon in the foreground")
|
|
727
|
+
.option("--port <port>", "Port to listen on")
|
|
728
|
+
.option("--host <host>", "Bind address")
|
|
729
|
+
.option("-p, --provider <provider>", "Default provider to use")
|
|
730
|
+
.action(async (options: { port?: string; host?: string; provider?: string }) => {
|
|
731
|
+
await requireLocalDaemon();
|
|
732
|
+
const argv = ["routstrd", "daemon"];
|
|
733
|
+
if (options.port) argv.push("--port", options.port);
|
|
734
|
+
if (options.host) argv.push("--host", options.host);
|
|
735
|
+
if (options.provider) argv.push("--provider", options.provider);
|
|
736
|
+
const { runDaemon } = await import("./daemon/index");
|
|
737
|
+
await runDaemon(argv);
|
|
738
|
+
});
|
|
739
|
+
|
|
640
740
|
// Start - start the background daemon
|
|
641
741
|
program
|
|
642
742
|
.command("start")
|
|
@@ -858,26 +958,7 @@ program
|
|
|
858
958
|
.description("Refresh routstr21 models and client integrations")
|
|
859
959
|
.action(async () => {
|
|
860
960
|
await ensureDaemonRunning();
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
// Refresh models via daemon API
|
|
864
|
-
console.log("Refreshing routstr21 models...");
|
|
865
|
-
const result = await callDaemon("/v1/models?refresh=true");
|
|
866
|
-
if (result.error) {
|
|
867
|
-
console.log(`Model refresh failed: ${result.error}`);
|
|
868
|
-
process.exit(1);
|
|
869
|
-
}
|
|
870
|
-
console.log("Models refreshed.");
|
|
871
|
-
|
|
872
|
-
// Refresh integrations for all clients
|
|
873
|
-
const clients = await getClientsList();
|
|
874
|
-
if (clients.length > 0) {
|
|
875
|
-
console.log(`Refreshing ${clients.length} client integration(s)...`);
|
|
876
|
-
await runIntegrationsForClients(clients, config);
|
|
877
|
-
console.log("Client integrations refreshed.");
|
|
878
|
-
} else {
|
|
879
|
-
console.log("No clients to refresh.");
|
|
880
|
-
}
|
|
961
|
+
await refreshModelsAndClientsAction();
|
|
881
962
|
});
|
|
882
963
|
|
|
883
964
|
// Models - list routstr21 models
|
|
@@ -1256,7 +1337,54 @@ providersCmd
|
|
|
1256
1337
|
// Clients - list and manage clients
|
|
1257
1338
|
const clientsCmd = program
|
|
1258
1339
|
.command("clients")
|
|
1259
|
-
.description("List and manage clients")
|
|
1340
|
+
.description("List and manage clients")
|
|
1341
|
+
.option(
|
|
1342
|
+
"--manual-refresh",
|
|
1343
|
+
"Refresh routstr21 models and all client integrations now",
|
|
1344
|
+
false,
|
|
1345
|
+
)
|
|
1346
|
+
.option(
|
|
1347
|
+
"--disable-automatic-refresh",
|
|
1348
|
+
"Disable the daemon's scheduled refresh job",
|
|
1349
|
+
false,
|
|
1350
|
+
)
|
|
1351
|
+
.option(
|
|
1352
|
+
"--enable-automatic-refresh",
|
|
1353
|
+
"Re-enable the daemon's scheduled refresh job",
|
|
1354
|
+
false,
|
|
1355
|
+
)
|
|
1356
|
+
.action(
|
|
1357
|
+
async (options: {
|
|
1358
|
+
manualRefresh: boolean;
|
|
1359
|
+
disableAutomaticRefresh: boolean;
|
|
1360
|
+
enableAutomaticRefresh: boolean;
|
|
1361
|
+
}) => {
|
|
1362
|
+
if (options.disableAutomaticRefresh && options.enableAutomaticRefresh) {
|
|
1363
|
+
console.error(
|
|
1364
|
+
"error: --disable-automatic-refresh and --enable-automatic-refresh are mutually exclusive.",
|
|
1365
|
+
);
|
|
1366
|
+
process.exit(1);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (
|
|
1370
|
+
!options.manualRefresh &&
|
|
1371
|
+
!options.disableAutomaticRefresh &&
|
|
1372
|
+
!options.enableAutomaticRefresh
|
|
1373
|
+
) {
|
|
1374
|
+
clientsCmd.help({ error: true });
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (options.manualRefresh) {
|
|
1379
|
+
await ensureDaemonRunning();
|
|
1380
|
+
await refreshModelsAndClientsAction();
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (options.disableAutomaticRefresh || options.enableAutomaticRefresh) {
|
|
1384
|
+
await setAutomaticRefreshAction(options.enableAutomaticRefresh);
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
);
|
|
1260
1388
|
|
|
1261
1389
|
clientsCmd
|
|
1262
1390
|
.command("list")
|
|
@@ -1301,12 +1429,23 @@ const npubsCmd = program
|
|
|
1301
1429
|
|
|
1302
1430
|
type NpubEntry = {
|
|
1303
1431
|
npub: string;
|
|
1432
|
+
name: string | null;
|
|
1304
1433
|
role: string;
|
|
1305
1434
|
};
|
|
1306
1435
|
|
|
1436
|
+
/** Normalize a user-supplied npub display name: trim whitespace and cap at 64 chars. */
|
|
1437
|
+
function parseNpubName(raw: string | undefined): { name?: string | null; error?: string } {
|
|
1438
|
+
if (raw === undefined) return {};
|
|
1439
|
+
const trimmed = raw.trim();
|
|
1440
|
+
if ([...trimmed].length > 64) {
|
|
1441
|
+
return { error: "Invalid name. Maximum length is 64 characters." };
|
|
1442
|
+
}
|
|
1443
|
+
return { name: trimmed === "" ? null : trimmed };
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1307
1446
|
npubsCmd
|
|
1308
1447
|
.command("list")
|
|
1309
|
-
.description("List configured npubs with their roles")
|
|
1448
|
+
.description("List configured npubs with their roles and names")
|
|
1310
1449
|
.action(async () => {
|
|
1311
1450
|
await ensureDaemonRunning();
|
|
1312
1451
|
const config = await loadConfig();
|
|
@@ -1322,7 +1461,7 @@ npubsCmd
|
|
|
1322
1461
|
: result;
|
|
1323
1462
|
const npubs = (data as { npubs?: NpubEntry[] } | undefined)?.npubs ?? [];
|
|
1324
1463
|
if (npubs.length === 0) {
|
|
1325
|
-
console.log("No
|
|
1464
|
+
console.log("No npubs configured. Run 'routstrd npubs register' to register yourself as the first admin.");
|
|
1326
1465
|
return;
|
|
1327
1466
|
}
|
|
1328
1467
|
console.log(`Npubs (${npubs.length}):`);
|
|
@@ -1330,12 +1469,13 @@ npubsCmd
|
|
|
1330
1469
|
for (const entry of npubs) {
|
|
1331
1470
|
const marker = entry.npub === userNpub ? " → you" : "";
|
|
1332
1471
|
if (entry.npub === userNpub) found = true;
|
|
1333
|
-
|
|
1472
|
+
const name = entry.name ? ` "${entry.name}"` : "";
|
|
1473
|
+
console.log(`- ${entry.npub} [${entry.role}]${name}${marker}`);
|
|
1334
1474
|
}
|
|
1335
1475
|
if (userNpub && !found) {
|
|
1336
1476
|
console.log("");
|
|
1337
1477
|
console.log(
|
|
1338
|
-
"Your npub is not in the
|
|
1478
|
+
"Your npub is not in the npub list. Ask an admin to add your npub:",
|
|
1339
1479
|
);
|
|
1340
1480
|
console.log(` ${userNpub}`);
|
|
1341
1481
|
}
|
|
@@ -1344,7 +1484,8 @@ npubsCmd
|
|
|
1344
1484
|
npubsCmd
|
|
1345
1485
|
.command("register")
|
|
1346
1486
|
.description("Register yourself as the first admin (only when no admins exist)")
|
|
1347
|
-
.
|
|
1487
|
+
.option("-n, --name <name>", "Display name for this npub (optional)")
|
|
1488
|
+
.action(async (options: { name?: string }) => {
|
|
1348
1489
|
await ensureDaemonRunning();
|
|
1349
1490
|
const config = await loadConfig();
|
|
1350
1491
|
const userNpub = getUserNpub(config);
|
|
@@ -1372,19 +1513,27 @@ npubsCmd
|
|
|
1372
1513
|
console.error("Failed to normalize user npub.");
|
|
1373
1514
|
process.exit(1);
|
|
1374
1515
|
}
|
|
1516
|
+
const name = parseNpubName(options.name);
|
|
1517
|
+
if (name.error) {
|
|
1518
|
+
console.error(name.error);
|
|
1519
|
+
process.exit(1);
|
|
1520
|
+
}
|
|
1521
|
+
const body: Record<string, string | null> = { npub: npubFromPubkey(normalized) };
|
|
1522
|
+
if (name.name !== undefined) body.name = name.name;
|
|
1375
1523
|
const addResult = await callAuth("/npubs", {
|
|
1376
1524
|
method: "POST",
|
|
1377
|
-
body
|
|
1525
|
+
body,
|
|
1378
1526
|
});
|
|
1379
1527
|
if (addResult.error) {
|
|
1380
1528
|
console.log(addResult.error);
|
|
1381
1529
|
process.exit(1);
|
|
1382
1530
|
}
|
|
1383
1531
|
const output = addResult.output as
|
|
1384
|
-
| { npub?: string; added?: boolean; error?: string }
|
|
1532
|
+
| { npub?: string; name?: string | null; added?: boolean; error?: string }
|
|
1385
1533
|
| undefined;
|
|
1386
1534
|
if (output?.npub) {
|
|
1387
|
-
|
|
1535
|
+
const nameSuffix = output.name ? ` ("${output.name}")` : "";
|
|
1536
|
+
console.log(`Successfully registered as first admin npub: ${output.npub}${nameSuffix}`);
|
|
1388
1537
|
} else {
|
|
1389
1538
|
console.log(`Successfully registered as first admin npub: ${userNpub}`);
|
|
1390
1539
|
}
|
|
@@ -1394,7 +1543,8 @@ npubsCmd
|
|
|
1394
1543
|
.command("add <npub>")
|
|
1395
1544
|
.description("Add a npub (hex pubkey or npub1...). Defaults to 'user' role unless --role is specified.")
|
|
1396
1545
|
.option("-r, --role <role>", "Role for the npub: 'admin' or 'user' (default: 'user')", "user")
|
|
1397
|
-
.
|
|
1546
|
+
.option("-n, --name <name>", "Display name for the npub (optional)")
|
|
1547
|
+
.action(async (npubArg: string, options: { role: string; name?: string }) => {
|
|
1398
1548
|
await ensureDaemonRunning();
|
|
1399
1549
|
const normalized = normalizeNostrPubkey(npubArg);
|
|
1400
1550
|
if (!normalized) {
|
|
@@ -1405,7 +1555,16 @@ npubsCmd
|
|
|
1405
1555
|
console.error("Invalid role. Expected 'admin' or 'user'.");
|
|
1406
1556
|
process.exit(1);
|
|
1407
1557
|
}
|
|
1408
|
-
const
|
|
1558
|
+
const name = parseNpubName(options.name);
|
|
1559
|
+
if (name.error) {
|
|
1560
|
+
console.error(name.error);
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
const body: Record<string, string | null> = {
|
|
1564
|
+
npub: npubFromPubkey(normalized),
|
|
1565
|
+
role: options.role,
|
|
1566
|
+
};
|
|
1567
|
+
if (name.name !== undefined) body.name = name.name;
|
|
1409
1568
|
const result = await callAuth("/npubs", {
|
|
1410
1569
|
method: "POST",
|
|
1411
1570
|
body,
|
|
@@ -1415,44 +1574,63 @@ npubsCmd
|
|
|
1415
1574
|
process.exit(1);
|
|
1416
1575
|
}
|
|
1417
1576
|
const output = result.output as
|
|
1418
|
-
| { npub?: string; role?: string; added?: boolean; error?: string }
|
|
1577
|
+
| { npub?: string; name?: string | null; role?: string; added?: boolean; error?: string }
|
|
1419
1578
|
| undefined;
|
|
1420
1579
|
if (output?.npub) {
|
|
1580
|
+
const nameSuffix = output.name ? ` ("${output.name}")` : "";
|
|
1421
1581
|
console.log(
|
|
1422
|
-
`${output.added ? "Added" : "Already configured"} npub: ${output.npub} [${output.role ?? "user"}]`,
|
|
1582
|
+
`${output.added ? "Added" : "Already configured"} npub: ${output.npub} [${output.role ?? "user"}]${nameSuffix}`,
|
|
1423
1583
|
);
|
|
1424
1584
|
}
|
|
1425
1585
|
});
|
|
1426
1586
|
|
|
1427
1587
|
npubsCmd
|
|
1428
1588
|
.command("update <npub>")
|
|
1429
|
-
.description("Update the role of an existing npub (requires admin)")
|
|
1430
|
-
.
|
|
1431
|
-
.
|
|
1589
|
+
.description("Update the role and/or name of an existing npub (requires admin)")
|
|
1590
|
+
.option("-r, --role <role>", "New role: 'admin' or 'user'")
|
|
1591
|
+
.option("-n, --name <name>", "New display name (empty string clears it)")
|
|
1592
|
+
.action(async (npubArg: string, options: { role?: string; name?: string }) => {
|
|
1432
1593
|
await ensureDaemonRunning();
|
|
1433
1594
|
const normalized = normalizeNostrPubkey(npubArg);
|
|
1434
1595
|
if (!normalized) {
|
|
1435
1596
|
console.error("Invalid npub value. Use npub1... or 64-char hex pubkey.");
|
|
1436
1597
|
process.exit(1);
|
|
1437
1598
|
}
|
|
1438
|
-
if (options.role !== "admin" && options.role !== "user") {
|
|
1599
|
+
if (options.role !== undefined && options.role !== "admin" && options.role !== "user") {
|
|
1439
1600
|
console.error("Invalid role. Expected 'admin' or 'user'.");
|
|
1440
1601
|
process.exit(1);
|
|
1441
1602
|
}
|
|
1603
|
+
if (options.role === undefined && options.name === undefined) {
|
|
1604
|
+
console.error("Provide '--role' and/or '--name' to update.");
|
|
1605
|
+
process.exit(1);
|
|
1606
|
+
}
|
|
1607
|
+
const name = parseNpubName(options.name);
|
|
1608
|
+
if (name.error) {
|
|
1609
|
+
console.error(name.error);
|
|
1610
|
+
process.exit(1);
|
|
1611
|
+
}
|
|
1612
|
+
const body: Record<string, string | null> = {
|
|
1613
|
+
npub: npubFromPubkey(normalized),
|
|
1614
|
+
};
|
|
1615
|
+
if (options.role !== undefined) body.role = options.role;
|
|
1616
|
+
if (name.name !== undefined) body.name = name.name;
|
|
1442
1617
|
const result = await callAuth("/npubs", {
|
|
1443
1618
|
method: "PATCH",
|
|
1444
|
-
body
|
|
1619
|
+
body,
|
|
1445
1620
|
});
|
|
1446
1621
|
if (result.error) {
|
|
1447
1622
|
console.log(result.error);
|
|
1448
1623
|
process.exit(1);
|
|
1449
1624
|
}
|
|
1450
|
-
// PATCH /npubs returns { npub, pubkey, role } at the top level, not wrapped in { output }
|
|
1625
|
+
// PATCH /npubs returns { npub, pubkey, name, role } at the top level, not wrapped in { output }
|
|
1451
1626
|
const data = (result.output ?? result) as
|
|
1452
|
-
| { npub?: string; pubkey?: string; role?: string; error?: string }
|
|
1627
|
+
| { npub?: string; pubkey?: string; name?: string | null; role?: string; error?: string }
|
|
1453
1628
|
| undefined;
|
|
1454
1629
|
if (data?.npub) {
|
|
1455
|
-
|
|
1630
|
+
const parts: string[] = [];
|
|
1631
|
+
if (data.role) parts.push(`role='${data.role}'`);
|
|
1632
|
+
parts.push(`name=${data.name ? `"${data.name}"` : "(none)"}`);
|
|
1633
|
+
console.log(`Updated npub ${data.npub} (${parts.join(", ")}).`);
|
|
1456
1634
|
} else {
|
|
1457
1635
|
console.log("Npub not found or update failed.");
|
|
1458
1636
|
}
|
|
@@ -1649,11 +1827,14 @@ program
|
|
|
1649
1827
|
});
|
|
1650
1828
|
|
|
1651
1829
|
const output = result.output as
|
|
1652
|
-
| { invoice?: string; amount?: number; mintUrl?: string }
|
|
1830
|
+
| { invoice?: string; operationId?: string; amount?: number; mintUrl?: string }
|
|
1653
1831
|
| undefined;
|
|
1654
1832
|
|
|
1655
1833
|
if (typeof output?.invoice === "string" && output.invoice) {
|
|
1656
1834
|
await printLightningInvoice(output.invoice);
|
|
1835
|
+
if (typeof output.operationId === "string") {
|
|
1836
|
+
await waitForMintQuote(output.operationId);
|
|
1837
|
+
}
|
|
1657
1838
|
return;
|
|
1658
1839
|
}
|
|
1659
1840
|
|
|
@@ -2081,7 +2262,21 @@ program
|
|
|
2081
2262
|
.command("stop")
|
|
2082
2263
|
.description("Stop the background daemon")
|
|
2083
2264
|
.action(async () => {
|
|
2084
|
-
await
|
|
2265
|
+
if (!(await isDaemonRunning())) {
|
|
2266
|
+
console.log("Daemon was not running.");
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
await callDaemon("/stop", { method: "POST" });
|
|
2270
|
+
|
|
2271
|
+
// The daemon exits only after ongoing requests finish; wait for it so
|
|
2272
|
+
// the wallet lock is actually free when this command returns.
|
|
2273
|
+
try {
|
|
2274
|
+
await waitForDaemonToExit({ pidFilePath: walletPidPath() });
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
2277
|
+
process.exit(1);
|
|
2278
|
+
}
|
|
2279
|
+
console.log("Daemon stopped.");
|
|
2085
2280
|
});
|
|
2086
2281
|
|
|
2087
2282
|
// Service - PM2 management
|
|
@@ -2098,6 +2293,12 @@ serviceCmd
|
|
|
2098
2293
|
try {
|
|
2099
2294
|
execSync("pm2 -v", { stdio: "ignore" });
|
|
2100
2295
|
} catch (e) {
|
|
2296
|
+
if (isStandaloneExecutable()) {
|
|
2297
|
+
console.error(
|
|
2298
|
+
"PM2 is optional and is not bundled with routstrd. Install PM2 separately before using 'routstrd service install'.",
|
|
2299
|
+
);
|
|
2300
|
+
process.exit(1);
|
|
2301
|
+
}
|
|
2101
2302
|
console.log("PM2 not found. Installing PM2 globally with bun...");
|
|
2102
2303
|
try {
|
|
2103
2304
|
execSync("bun install -g pm2", { stdio: "inherit" });
|
|
@@ -2109,37 +2310,15 @@ serviceCmd
|
|
|
2109
2310
|
}
|
|
2110
2311
|
}
|
|
2111
2312
|
|
|
2112
|
-
// 2. Resolve the path to the daemon
|
|
2113
|
-
// In a global install, we want the bundled daemon in dist/daemon/index.js
|
|
2114
|
-
let daemonPath: string;
|
|
2115
|
-
try {
|
|
2116
|
-
// Try to resolve relative to this file first (works in dev and global)
|
|
2117
|
-
daemonPath = Bun.resolveSync("./daemon/index.js", import.meta.url);
|
|
2118
|
-
} catch (e) {
|
|
2119
|
-
// Fallback for some bundling scenarios
|
|
2120
|
-
const path = require("path");
|
|
2121
|
-
daemonPath = path.join(
|
|
2122
|
-
path.dirname(import.meta.url).replace("file://", ""),
|
|
2123
|
-
"daemon",
|
|
2124
|
-
"index.js",
|
|
2125
|
-
);
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
if (!existsSync(daemonPath)) {
|
|
2129
|
-
console.error(
|
|
2130
|
-
`Could not find daemon at ${daemonPath}. Did you run 'bun run build'?`,
|
|
2131
|
-
);
|
|
2132
|
-
process.exit(1);
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
2313
|
console.log("Starting routstrd via PM2...");
|
|
2136
2314
|
try {
|
|
2137
2315
|
await stopLegacyCocod();
|
|
2138
2316
|
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2317
|
+
const proc = Bun.spawn(["pm2", ...pm2DaemonArgs()], {
|
|
2318
|
+
stdout: "inherit",
|
|
2319
|
+
stderr: "inherit",
|
|
2142
2320
|
});
|
|
2321
|
+
if ((await proc.exited) !== 0) throw new Error("PM2 exited with an error");
|
|
2143
2322
|
|
|
2144
2323
|
console.log("\n✅ routstrd is now managed by PM2.");
|
|
2145
2324
|
console.log("\nTo ensure it starts on system reboot, run:");
|
|
@@ -2194,19 +2373,12 @@ program
|
|
|
2194
2373
|
console.log("Stopping daemon...");
|
|
2195
2374
|
await callDaemon("/stop", { method: "POST" });
|
|
2196
2375
|
|
|
2197
|
-
// Wait for
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
await
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
if (healthDown && pidFileReleased) {
|
|
2204
|
-
break;
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
if (await isDaemonRunning()) {
|
|
2209
|
-
logger.error("Daemon failed to stop within 10 seconds");
|
|
2376
|
+
// Wait for the old daemon to fully exit so the wallet lock is free
|
|
2377
|
+
// before a new daemon is spawned.
|
|
2378
|
+
try {
|
|
2379
|
+
await waitForDaemonToExit({ pidFilePath: walletPidPath() });
|
|
2380
|
+
} catch (error) {
|
|
2381
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
2210
2382
|
process.exit(1);
|
|
2211
2383
|
}
|
|
2212
2384
|
console.log("Daemon stopped.");
|
|
@@ -2285,15 +2457,12 @@ program
|
|
|
2285
2457
|
console.log("Stopping daemon...");
|
|
2286
2458
|
await callDaemon("/stop", { method: "POST" });
|
|
2287
2459
|
|
|
2288
|
-
for
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
if (await isDaemonRunning()) {
|
|
2296
|
-
logger.error("Daemon failed to stop within 5 seconds");
|
|
2460
|
+
// Wait for the old daemon to fully exit so the wallet lock is free
|
|
2461
|
+
// before a new daemon is spawned.
|
|
2462
|
+
try {
|
|
2463
|
+
await waitForDaemonToExit({ pidFilePath: walletPidPath() });
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
2297
2466
|
process.exit(1);
|
|
2298
2467
|
}
|
|
2299
2468
|
console.log("Daemon stopped.");
|
|
@@ -2589,5 +2758,5 @@ program
|
|
|
2589
2758
|
);
|
|
2590
2759
|
|
|
2591
2760
|
export function cli(args: string[]) {
|
|
2592
|
-
program.
|
|
2761
|
+
return program.parseAsync(args);
|
|
2593
2762
|
}
|