routstrd 0.3.10 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/IMPLEMENTATION.md +253 -0
- package/README.md +45 -0
- package/SECURITY.md +23 -0
- package/SKILL.md +13 -3
- package/bun.lock +68 -41
- package/dist/daemon/index.js +47850 -11349
- package/dist/index.js +4730 -448
- package/package.json +8 -3
- package/src/cli.test.ts +55 -0
- package/src/cli.ts +502 -229
- package/src/daemon/args.ts +8 -1
- package/src/daemon/models.ts +119 -39
- package/src/daemon/wallet/coco-client.npc.test.ts +252 -0
- package/src/daemon/wallet/coco-client.test.ts +508 -0
- package/src/daemon/wallet/coco-client.ts +835 -0
- package/src/daemon/wallet/cocod-client.ts +86 -2
- package/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz +0 -0
- package/src/daemon/wallet/migration.test.ts +147 -0
- package/src/daemon/wallet/migration.ts +209 -0
- package/src/daemon/wallet/paths.ts +33 -0
- package/src/integrations/claudecode.ts +2 -1
- package/src/integrations/hermes.ts +46 -6
- package/src/integrations/openclaw.ts +3 -2
- package/src/integrations/opencode.ts +3 -2
- package/src/integrations/pi.ts +3 -2
- package/src/integrations/registry.ts +7 -5
- package/src/start-daemon.ts +168 -22
- package/src/tui/usage/app.ts +41 -2
- package/src/tui/usage/render.ts +10 -3
- package/src/tui/usage/types.ts +6 -0
- package/src/utils/clients.ts +21 -0
- package/src/utils/config.ts +11 -0
- package/src/utils/daemon-client.ts +104 -25
- package/src/utils/logger.ts +42 -28
- package/src/utils/update-checker.ts +114 -0
- package/tests/utils/daemon-client.test.ts +61 -0
- package/tests/wallet/short-keyset-token.test.ts +106 -0
package/src/cli.ts
CHANGED
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
deleteClientAction,
|
|
16
16
|
addClientAction,
|
|
17
17
|
} from "./utils/clients";
|
|
18
|
-
import { existsSync, mkdirSync } from "fs";
|
|
18
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
19
19
|
import { execSync } from "child_process";
|
|
20
|
+
import { dirname, join } from "path";
|
|
20
21
|
import {
|
|
21
22
|
CONFIG_DIR,
|
|
22
23
|
DB_PATH,
|
|
@@ -25,17 +26,32 @@ import {
|
|
|
25
26
|
LOGS_DIR,
|
|
26
27
|
type RoutstrdConfig,
|
|
27
28
|
} from "./utils/config";
|
|
28
|
-
import { logger } from "./utils/logger";
|
|
29
|
+
import { COCO_LOGS_DIR, logger } from "./utils/logger";
|
|
29
30
|
import { setupIntegration, runIntegrationsForClients } from "./integrations";
|
|
31
|
+
import {
|
|
32
|
+
assertLegacyCocodNotRunning,
|
|
33
|
+
claimLegacyCocodPidFile,
|
|
34
|
+
stopLegacyCocod,
|
|
35
|
+
} from "./daemon/wallet/coco-client";
|
|
36
|
+
import { migrateLegacyWallet } from "./daemon/wallet/migration";
|
|
37
|
+
import {
|
|
38
|
+
legacyCocodPidPath,
|
|
39
|
+
legacyCocodSocketPath,
|
|
40
|
+
walletDir as defaultWalletDir,
|
|
41
|
+
walletPidPath,
|
|
42
|
+
} from "./daemon/wallet/paths";
|
|
30
43
|
import { getClientsList } from "./utils/clients";
|
|
31
44
|
import * as QRCode from "qrcode";
|
|
32
45
|
import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98";
|
|
33
46
|
import { generateSecretKey, nip19 } from "nostr-tools";
|
|
34
|
-
import {
|
|
35
|
-
|
|
36
|
-
resolveCocodExecutable,
|
|
37
|
-
} from "./daemon/wallet/cocod-client";
|
|
47
|
+
import { generateMnemonic } from "@scure/bip39";
|
|
48
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
38
49
|
import packageJson from "../package.json" with { type: "json" };
|
|
50
|
+
import {
|
|
51
|
+
compareVersions,
|
|
52
|
+
getGlobalPackageVersion,
|
|
53
|
+
getLatestNpmVersion,
|
|
54
|
+
} from "./utils/update-checker.ts";
|
|
39
55
|
|
|
40
56
|
type RoutstrModel = {
|
|
41
57
|
id: string;
|
|
@@ -81,32 +97,42 @@ async function printLightningInvoice(invoice: string): Promise<void> {
|
|
|
81
97
|
console.log(`${qr}\nInvoice:\n${invoice}`);
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
export function initializeWallet(walletDir = defaultWalletDir()): void {
|
|
101
|
+
const walletConfig = join(walletDir, "config.json");
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
stderr: "inherit",
|
|
92
|
-
},
|
|
93
|
-
);
|
|
103
|
+
// The wallet directory and config contain the plaintext seed phrase. Correct
|
|
104
|
+
// permissions on existing installations as well as newly created ones.
|
|
105
|
+
mkdirSync(walletDir, { recursive: true, mode: 0o700 });
|
|
106
|
+
chmodSync(walletDir, 0o700);
|
|
94
107
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
console.
|
|
98
|
-
|
|
99
|
-
);
|
|
100
|
-
throw new Error("cocod installation failed");
|
|
108
|
+
if (existsSync(walletConfig)) {
|
|
109
|
+
chmodSync(walletConfig, 0o600);
|
|
110
|
+
console.log("Wallet already initialized.");
|
|
111
|
+
return;
|
|
101
112
|
}
|
|
102
113
|
|
|
103
|
-
|
|
114
|
+
const mnemonic = generateMnemonic(wordlist);
|
|
115
|
+
const config = {
|
|
116
|
+
version: 1,
|
|
117
|
+
mnemonic,
|
|
118
|
+
encrypted: false,
|
|
119
|
+
createdAt: new Date().toISOString(),
|
|
120
|
+
};
|
|
121
|
+
writeFileSync(walletConfig, JSON.stringify(config, null, 2), {
|
|
122
|
+
mode: 0o600,
|
|
123
|
+
flag: "wx",
|
|
124
|
+
});
|
|
125
|
+
console.log("Initialized. Mnemonic:", mnemonic);
|
|
126
|
+
console.log("IMPORTANT: Write down this mnemonic and keep it safe!");
|
|
104
127
|
}
|
|
105
128
|
|
|
106
129
|
/**
|
|
107
|
-
* Restart the routstrd
|
|
108
|
-
*
|
|
109
|
-
*
|
|
130
|
+
* Restart the routstrd daemon after an update so the new binary takes
|
|
131
|
+
* effect immediately. Failures are collected and reported but never
|
|
132
|
+
* roll back the update itself.
|
|
133
|
+
*
|
|
134
|
+
* Note: cocod is no longer a separate process — the wallet now runs
|
|
135
|
+
* in-process via coco-core, so there is nothing else to restart.
|
|
110
136
|
*/
|
|
111
137
|
async function restartDaemonsAfterUpdate(): Promise<void> {
|
|
112
138
|
const config = await loadConfig();
|
|
@@ -124,23 +150,28 @@ async function restartDaemonsAfterUpdate(): Promise<void> {
|
|
|
124
150
|
} else {
|
|
125
151
|
console.log("\nRestarting routstrd daemon...");
|
|
126
152
|
|
|
127
|
-
// Graceful stop — the /stop endpoint closes the HTTP server
|
|
128
|
-
// (draining active connections) then exits.
|
|
129
153
|
await callDaemon("/stop", { method: "POST" });
|
|
130
154
|
|
|
131
|
-
for
|
|
155
|
+
// Wait for HTTP health check to fail AND wallet lock to be released.
|
|
156
|
+
const pidFilePath = walletPidPath();
|
|
157
|
+
for (let i = 0; i < 100; i++) {
|
|
132
158
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
133
|
-
|
|
159
|
+
const healthDown = !(await isDaemonRunning());
|
|
160
|
+
const pidFileReleased = !existsSync(pidFilePath);
|
|
161
|
+
if (healthDown && pidFileReleased) break;
|
|
134
162
|
}
|
|
135
163
|
|
|
136
164
|
if (await isDaemonRunning()) {
|
|
137
|
-
throw new Error("routstrd did not stop within
|
|
165
|
+
throw new Error("routstrd did not stop within 10 seconds");
|
|
138
166
|
}
|
|
139
167
|
console.log("routstrd daemon stopped.");
|
|
140
168
|
|
|
169
|
+
await stopLegacyCocod();
|
|
170
|
+
|
|
141
171
|
console.log("Starting routstrd daemon...");
|
|
142
172
|
await startDaemon({
|
|
143
173
|
port: String(config.port || 8008),
|
|
174
|
+
host: config.host || undefined,
|
|
144
175
|
provider: config.provider || undefined,
|
|
145
176
|
});
|
|
146
177
|
console.log("routstrd daemon restarted.");
|
|
@@ -151,80 +182,19 @@ async function restartDaemonsAfterUpdate(): Promise<void> {
|
|
|
151
182
|
}
|
|
152
183
|
}
|
|
153
184
|
|
|
154
|
-
// --- cocod daemon ---
|
|
155
|
-
try {
|
|
156
|
-
const cocodExecutable = resolveCocodExecutable(config.cocodPath);
|
|
157
|
-
|
|
158
|
-
// Check whether cocod is currently running.
|
|
159
|
-
const pingProc = Bun.spawn([cocodExecutable, "ping"], {
|
|
160
|
-
stdout: "ignore",
|
|
161
|
-
stderr: "ignore",
|
|
162
|
-
});
|
|
163
|
-
const cocodWasRunning = (await pingProc.exited) === 0;
|
|
164
|
-
|
|
165
|
-
if (!cocodWasRunning) {
|
|
166
|
-
console.log("\ncocod daemon was not running — skipping restart.");
|
|
167
|
-
} else {
|
|
168
|
-
console.log("\nRestarting cocod daemon...");
|
|
169
|
-
|
|
170
|
-
const stopProc = Bun.spawn([cocodExecutable, "stop"], {
|
|
171
|
-
stdout: "inherit",
|
|
172
|
-
stderr: "inherit",
|
|
173
|
-
});
|
|
174
|
-
const stopCode = await stopProc.exited;
|
|
175
|
-
if (stopCode !== 0) {
|
|
176
|
-
throw new Error(`cocod stop exited with code ${stopCode}`);
|
|
177
|
-
}
|
|
178
|
-
console.log("cocod daemon stopped.");
|
|
179
|
-
|
|
180
|
-
// Start cocod in the background (detached, like the wallet client does).
|
|
181
|
-
const env = { ...process.env };
|
|
182
|
-
const startProc = Bun.spawn([cocodExecutable, "daemon"], {
|
|
183
|
-
stdout: "ignore",
|
|
184
|
-
stderr: "ignore",
|
|
185
|
-
stdin: "ignore",
|
|
186
|
-
detached: true,
|
|
187
|
-
env,
|
|
188
|
-
});
|
|
189
|
-
startProc.unref?.();
|
|
190
|
-
|
|
191
|
-
// Poll until cocod responds to ping (max ~10 s).
|
|
192
|
-
let started = false;
|
|
193
|
-
for (let i = 0; i < 100; i++) {
|
|
194
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
195
|
-
const poll = Bun.spawn([cocodExecutable, "ping"], {
|
|
196
|
-
stdout: "ignore",
|
|
197
|
-
stderr: "ignore",
|
|
198
|
-
});
|
|
199
|
-
if ((await poll.exited) === 0) {
|
|
200
|
-
started = true;
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (!started) {
|
|
206
|
-
throw new Error("cocod did not come back up within 10 seconds");
|
|
207
|
-
}
|
|
208
|
-
console.log("cocod daemon restarted.");
|
|
209
|
-
}
|
|
210
|
-
} catch (error) {
|
|
211
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
212
|
-
failures.push(`cocod daemon: ${msg}`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
185
|
// --- report ---
|
|
216
186
|
if (failures.length > 0) {
|
|
217
|
-
console.error("\n⚠
|
|
187
|
+
console.error("\n⚠ Failed to restart daemon:");
|
|
218
188
|
for (const f of failures) {
|
|
219
189
|
console.error(` - ${f}`);
|
|
220
190
|
}
|
|
221
191
|
console.error(
|
|
222
|
-
"The update was applied but may not take effect until
|
|
192
|
+
"The update was applied but may not take effect until the daemon is manually restarted.",
|
|
223
193
|
);
|
|
224
194
|
process.exit(1);
|
|
225
195
|
}
|
|
226
196
|
|
|
227
|
-
console.log("\n✓
|
|
197
|
+
console.log("\n✓ Daemon restarted successfully.");
|
|
228
198
|
}
|
|
229
199
|
|
|
230
200
|
async function requireLocalDaemon(): Promise<void> {
|
|
@@ -269,76 +239,39 @@ async function initDaemon(): Promise<void> {
|
|
|
269
239
|
console.log(`You can view it in the config file at: ${CONFIG_FILE}\n`);
|
|
270
240
|
}
|
|
271
241
|
|
|
272
|
-
if (!(await isCocodInstalled(config.cocodPath))) {
|
|
273
|
-
if (config.cocodPath) {
|
|
274
|
-
logger.error(
|
|
275
|
-
`Configured cocod executable was not found: ${config.cocodPath}`,
|
|
276
|
-
);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
await installCocodOrExit();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const cocodExecutable = resolveCocodExecutable(config.cocodPath);
|
|
284
|
-
|
|
285
242
|
console.log(`Database will be stored at: ${DB_PATH}`);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
243
|
+
await stopLegacyCocod();
|
|
244
|
+
|
|
245
|
+
let migrationLockOwner: number | undefined;
|
|
246
|
+
const migration = await migrateLegacyWallet({
|
|
247
|
+
assertLegacyStopped: () =>
|
|
248
|
+
assertLegacyCocodNotRunning({
|
|
249
|
+
socketPath: legacyCocodSocketPath(),
|
|
250
|
+
pidFilePath: legacyCocodPidPath(),
|
|
251
|
+
ignorePid: migrationLockOwner,
|
|
252
|
+
}),
|
|
253
|
+
acquireLegacyLock: () => {
|
|
254
|
+
mkdirSync(dirname(legacyCocodPidPath()), {
|
|
255
|
+
recursive: true,
|
|
256
|
+
mode: 0o700,
|
|
257
|
+
});
|
|
258
|
+
const release = claimLegacyCocodPidFile({
|
|
259
|
+
pidFilePath: legacyCocodPidPath(),
|
|
260
|
+
});
|
|
261
|
+
migrationLockOwner = process.pid;
|
|
262
|
+
return () => {
|
|
263
|
+
migrationLockOwner = undefined;
|
|
264
|
+
release();
|
|
265
|
+
};
|
|
266
|
+
},
|
|
291
267
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const stdoutDone = initProc.stdout
|
|
297
|
-
? initProc.stdout.pipeTo(
|
|
298
|
-
new WritableStream<Uint8Array>({
|
|
299
|
-
write(chunk) {
|
|
300
|
-
const text = new TextDecoder().decode(chunk);
|
|
301
|
-
initStdout += text;
|
|
302
|
-
process.stdout.write(text);
|
|
303
|
-
},
|
|
304
|
-
}),
|
|
305
|
-
)
|
|
306
|
-
: Promise.resolve();
|
|
307
|
-
|
|
308
|
-
const stderrDone = initProc.stderr
|
|
309
|
-
? initProc.stderr.pipeTo(
|
|
310
|
-
new WritableStream<Uint8Array>({
|
|
311
|
-
write(chunk) {
|
|
312
|
-
const text = new TextDecoder().decode(chunk);
|
|
313
|
-
initStderr += text;
|
|
314
|
-
process.stderr.write(text);
|
|
315
|
-
},
|
|
316
|
-
}),
|
|
317
|
-
)
|
|
318
|
-
: Promise.resolve();
|
|
319
|
-
|
|
320
|
-
const [initCode] = await Promise.all([
|
|
321
|
-
initProc.exited,
|
|
322
|
-
stdoutDone,
|
|
323
|
-
stderrDone,
|
|
324
|
-
]);
|
|
325
|
-
const combinedOutput = `${initStdout}\n${initStderr}`.toLowerCase();
|
|
326
|
-
const alreadyInitialized = combinedOutput.includes("already initialized");
|
|
327
|
-
|
|
328
|
-
if (initCode !== 0 && !alreadyInitialized) {
|
|
329
|
-
console.error(
|
|
330
|
-
"Failed to initialize cocod. Please run 'cocod init' manually.",
|
|
331
|
-
);
|
|
332
|
-
return;
|
|
268
|
+
if (migration.status === "migrated") {
|
|
269
|
+
console.log(`Migrated wallet from ${migration.from} to ${migration.to}.`);
|
|
270
|
+
for (const warning of migration.cleanupWarnings) console.warn(warning);
|
|
333
271
|
}
|
|
272
|
+
initializeWallet();
|
|
334
273
|
|
|
335
|
-
|
|
336
|
-
console.log("cocod is already initialized.");
|
|
337
|
-
} else {
|
|
338
|
-
console.log("cocod initialized successfully.");
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
await startDaemon({ port: String(config.port || 8008) });
|
|
274
|
+
await startDaemon({ port: String(config.port || 8008), host: config.host || undefined });
|
|
342
275
|
|
|
343
276
|
await setupIntegration(config);
|
|
344
277
|
|
|
@@ -361,36 +294,54 @@ program
|
|
|
361
294
|
|
|
362
295
|
program
|
|
363
296
|
.command("update")
|
|
364
|
-
.description("Update routstrd
|
|
297
|
+
.description("Update routstrd to the latest version")
|
|
365
298
|
.action(async () => {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
});
|
|
371
|
-
const routstrdCode = await routstrdProc.exited;
|
|
372
|
-
if (routstrdCode !== 0) {
|
|
373
|
-
console.error("Failed to update routstrd.");
|
|
374
|
-
process.exit(1);
|
|
375
|
-
}
|
|
376
|
-
console.log("routstrd updated successfully.\n");
|
|
299
|
+
const packages = [
|
|
300
|
+
{ name: "routstrd", label: "routstrd" },
|
|
301
|
+
{ name: "@routstr/cocod", label: "cocod" },
|
|
302
|
+
];
|
|
377
303
|
|
|
378
|
-
|
|
379
|
-
const cocodProc = Bun.spawn(["bun", "install", "-g", "@routstr/cocod"], {
|
|
380
|
-
stdout: "inherit",
|
|
381
|
-
stderr: "inherit",
|
|
382
|
-
});
|
|
383
|
-
const cocodCode = await cocodProc.exited;
|
|
384
|
-
if (cocodCode !== 0) {
|
|
385
|
-
console.error("Failed to update cocod.");
|
|
386
|
-
process.exit(1);
|
|
387
|
-
}
|
|
388
|
-
console.log("cocod updated successfully.\n");
|
|
304
|
+
let updatedAny = false;
|
|
389
305
|
|
|
390
|
-
|
|
306
|
+
for (const { name, label } of packages) {
|
|
307
|
+
const installed = await getGlobalPackageVersion(name);
|
|
308
|
+
const latest = await getLatestNpmVersion(name);
|
|
391
309
|
|
|
392
|
-
|
|
393
|
-
|
|
310
|
+
// Only skip when we're confident the installed version is current.
|
|
311
|
+
// If we can't determine either version we fall through to installing.
|
|
312
|
+
if (
|
|
313
|
+
installed &&
|
|
314
|
+
latest &&
|
|
315
|
+
(compareVersions(installed, latest) ?? -1) >= 0
|
|
316
|
+
) {
|
|
317
|
+
console.log(`${label} is already up to date (v${installed}).`);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const fromPart = installed ? ` from v${installed}` : "";
|
|
322
|
+
const toPart = latest ? ` to v${latest}` : "";
|
|
323
|
+
console.log(`Updating ${label}${fromPart}${toPart}...`);
|
|
324
|
+
|
|
325
|
+
const proc = Bun.spawn(["bun", "install", "-g", name], {
|
|
326
|
+
stdout: "inherit",
|
|
327
|
+
stderr: "inherit",
|
|
328
|
+
});
|
|
329
|
+
const code = await proc.exited;
|
|
330
|
+
if (code !== 0) {
|
|
331
|
+
console.error(`Failed to update ${label}.`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
console.log(`${label} updated successfully.\n`);
|
|
335
|
+
updatedAny = true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (updatedAny) {
|
|
339
|
+
console.log("All requested updates have been applied!");
|
|
340
|
+
// Restart daemons so the new binaries take effect immediately.
|
|
341
|
+
await restartDaemonsAfterUpdate();
|
|
342
|
+
} else {
|
|
343
|
+
console.log("\nAll packages are already up to date — nothing to do.");
|
|
344
|
+
}
|
|
394
345
|
});
|
|
395
346
|
|
|
396
347
|
program
|
|
@@ -500,12 +451,36 @@ program
|
|
|
500
451
|
}
|
|
501
452
|
});
|
|
502
453
|
|
|
503
|
-
// Remote - configure a remote daemon URL
|
|
454
|
+
// Remote - show or configure a remote daemon URL
|
|
504
455
|
program
|
|
505
|
-
.command("remote
|
|
506
|
-
.description(
|
|
456
|
+
.command("remote [url]")
|
|
457
|
+
.description(
|
|
458
|
+
"Show the configured remote daemon, or configure one. With no URL, prints the current remote node; pass a URL to set one up.",
|
|
459
|
+
)
|
|
507
460
|
.option("--auth-url <authUrl>", "URL of the auth proxy for management commands (npubs, clients, usage)")
|
|
508
|
-
.action(async (url: string, options: { authUrl?: string }) => {
|
|
461
|
+
.action(async (url: string | undefined, options: { authUrl?: string }) => {
|
|
462
|
+
const config = await loadConfig();
|
|
463
|
+
|
|
464
|
+
// No URL provided -> show the current remote node, or tell the user to set one up.
|
|
465
|
+
if (!url) {
|
|
466
|
+
if (config.daemonUrl) {
|
|
467
|
+
console.log(`Remote daemon URL: ${config.daemonUrl}`);
|
|
468
|
+
if (config.authUrl) {
|
|
469
|
+
console.log(`Auth proxy URL: ${config.authUrl}`);
|
|
470
|
+
}
|
|
471
|
+
const npub = getUserNpub(config);
|
|
472
|
+
if (npub) {
|
|
473
|
+
console.log(`Nostr identity: ${npub}`);
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
console.error("No remote node is set up.");
|
|
478
|
+
console.error(
|
|
479
|
+
"Pass a URL to set one up, e.g. 'routstrd remote https://<host>:<port>'.",
|
|
480
|
+
);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
|
|
509
484
|
try {
|
|
510
485
|
new URL(url);
|
|
511
486
|
} catch {
|
|
@@ -526,7 +501,6 @@ program
|
|
|
526
501
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
527
502
|
}
|
|
528
503
|
|
|
529
|
-
const config = await loadConfig();
|
|
530
504
|
const updates: Partial<RoutstrdConfig> = { daemonUrl: url };
|
|
531
505
|
if (options.authUrl) {
|
|
532
506
|
updates.authUrl = options.authUrl;
|
|
@@ -563,11 +537,44 @@ program
|
|
|
563
537
|
}
|
|
564
538
|
});
|
|
565
539
|
|
|
540
|
+
// Local - switch back to local daemon mode (removes daemonUrl/authUrl)
|
|
541
|
+
program
|
|
542
|
+
.command("local")
|
|
543
|
+
.description("Switch back to local daemon mode (removes remote daemon URL)")
|
|
544
|
+
.action(async () => {
|
|
545
|
+
const config = await loadConfig();
|
|
546
|
+
|
|
547
|
+
if (!config.daemonUrl) {
|
|
548
|
+
console.log("Already in local mode — no remote daemon URL is configured.");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const previousDaemonUrl = config.daemonUrl;
|
|
553
|
+
const previousAuthUrl = config.authUrl;
|
|
554
|
+
|
|
555
|
+
const { daemonUrl: _daemonUrl, authUrl: _authUrl, ...rest } = config;
|
|
556
|
+
const updatedConfig: RoutstrdConfig = rest;
|
|
557
|
+
|
|
558
|
+
await Bun.write(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2));
|
|
559
|
+
|
|
560
|
+
console.log("Switched back to local daemon mode.");
|
|
561
|
+
if (previousDaemonUrl) {
|
|
562
|
+
console.log(` Removed remote daemon URL: ${previousDaemonUrl}`);
|
|
563
|
+
}
|
|
564
|
+
if (previousAuthUrl) {
|
|
565
|
+
console.log(` Removed auth proxy URL: ${previousAuthUrl}`);
|
|
566
|
+
}
|
|
567
|
+
console.log(
|
|
568
|
+
`\nUse 'routstrd onboard' for first time using local mode or 'routstrd start' to start the local daemon if you already are using it.`,
|
|
569
|
+
);
|
|
570
|
+
console.log(`You can view the config file at: ${CONFIG_FILE}`);
|
|
571
|
+
});
|
|
572
|
+
|
|
566
573
|
// Onboard - initialize the daemon
|
|
567
574
|
program
|
|
568
575
|
.command("onboard")
|
|
569
576
|
.description(
|
|
570
|
-
"Initialize routstrd (creates config directory and initializes
|
|
577
|
+
"Initialize routstrd (creates config directory and initializes wallet)",
|
|
571
578
|
)
|
|
572
579
|
.action(async () => {
|
|
573
580
|
await requireLocalDaemon();
|
|
@@ -579,19 +586,15 @@ program
|
|
|
579
586
|
.command("start")
|
|
580
587
|
.description("Start the background daemon")
|
|
581
588
|
.option("--port <port>", "Port to listen on")
|
|
589
|
+
.option("--host <host>", "Bind address (default: 127.0.0.1)")
|
|
582
590
|
.option("-p, --provider <provider>", "Default provider to use")
|
|
583
|
-
.action(async (options: { port?: string; provider?: string }) => {
|
|
591
|
+
.action(async (options: { port?: string; host?: string; provider?: string }) => {
|
|
584
592
|
await requireLocalDaemon();
|
|
585
593
|
const config = await loadConfig();
|
|
586
|
-
|
|
587
|
-
const installHint = config.cocodPath
|
|
588
|
-
? `Configured cocod executable was not found: ${config.cocodPath}`
|
|
589
|
-
: "cocod is not installed. Run 'routstrd onboard' first to install cocod.";
|
|
590
|
-
logger.error(installHint);
|
|
591
|
-
process.exit(1);
|
|
592
|
-
}
|
|
594
|
+
await stopLegacyCocod();
|
|
593
595
|
await startDaemon({
|
|
594
596
|
port: options.port || String(config.port || 8008),
|
|
597
|
+
host: options.host || config.host || undefined,
|
|
595
598
|
provider: options.provider,
|
|
596
599
|
});
|
|
597
600
|
});
|
|
@@ -631,9 +634,93 @@ program
|
|
|
631
634
|
program
|
|
632
635
|
.command("balance")
|
|
633
636
|
.description("Get wallet and API key balances")
|
|
634
|
-
.
|
|
637
|
+
.option("--api-keys", "List all stored API keys (baseUrl + key + balance)", false)
|
|
638
|
+
.option(
|
|
639
|
+
"--delete-api-keys <baseUrl>",
|
|
640
|
+
"Delete the API key stored for the given provider base URL (refunds balance first)",
|
|
641
|
+
)
|
|
642
|
+
.option(
|
|
643
|
+
"--mint-url <url>",
|
|
644
|
+
"Mint URL to refund the deleted API key balance to (defaults to first mint in wallet)",
|
|
645
|
+
)
|
|
646
|
+
.action(async (options: { apiKeys: boolean; deleteApiKeys?: string; mintUrl?: string }) => {
|
|
635
647
|
await ensureDaemonRunning();
|
|
636
648
|
|
|
649
|
+
if (options.deleteApiKeys) {
|
|
650
|
+
const baseUrl = options.deleteApiKeys;
|
|
651
|
+
const queryParts = [`baseUrl=${encodeURIComponent(baseUrl)}`];
|
|
652
|
+
if (options.mintUrl) {
|
|
653
|
+
queryParts.push(`mintUrl=${encodeURIComponent(options.mintUrl)}`);
|
|
654
|
+
}
|
|
655
|
+
const result = await callDaemon(
|
|
656
|
+
`/keys/api/delete?${queryParts.join("&")}`,
|
|
657
|
+
{ method: "DELETE" },
|
|
658
|
+
);
|
|
659
|
+
if (result.error) {
|
|
660
|
+
console.log(result.error);
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
const out = result.output as
|
|
664
|
+
| {
|
|
665
|
+
baseUrl?: string;
|
|
666
|
+
removed?: boolean;
|
|
667
|
+
refunded?: boolean;
|
|
668
|
+
refundedAmount?: number;
|
|
669
|
+
refundMessage?: string;
|
|
670
|
+
message?: string;
|
|
671
|
+
}
|
|
672
|
+
| undefined;
|
|
673
|
+
if (out) {
|
|
674
|
+
console.log(out.message ?? `Removed API key for ${baseUrl}`);
|
|
675
|
+
if (out.refunded && out.refundedAmount !== undefined) {
|
|
676
|
+
console.log(` Refunded: ${out.refundedAmount} sats`);
|
|
677
|
+
} else if (out.refundMessage) {
|
|
678
|
+
console.log(` Refund: ${out.refundMessage}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (options.apiKeys) {
|
|
685
|
+
const result = await callDaemon("/keys/api");
|
|
686
|
+
if (result.error) {
|
|
687
|
+
console.log(result.error);
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const data = result.output as
|
|
692
|
+
| {
|
|
693
|
+
apiKeys: Array<{
|
|
694
|
+
baseUrl: string;
|
|
695
|
+
key: string;
|
|
696
|
+
balance: number;
|
|
697
|
+
lastUsed: number | null;
|
|
698
|
+
}>;
|
|
699
|
+
count: number;
|
|
700
|
+
total: number;
|
|
701
|
+
unit: string;
|
|
702
|
+
}
|
|
703
|
+
| undefined;
|
|
704
|
+
|
|
705
|
+
console.log("=== API Keys ===\n");
|
|
706
|
+
if (!data || data.apiKeys.length === 0) {
|
|
707
|
+
console.log(" No API keys stored.");
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
for (const k of data.apiKeys) {
|
|
711
|
+
const lastUsed = k.lastUsed
|
|
712
|
+
? new Date(k.lastUsed).toISOString()
|
|
713
|
+
: "never";
|
|
714
|
+
console.log(` ${k.baseUrl}`);
|
|
715
|
+
console.log(` key: ${k.key}`);
|
|
716
|
+
console.log(` balance: ${k.balance} ${data.unit}`);
|
|
717
|
+
console.log(` lastUsed: ${lastUsed}`);
|
|
718
|
+
console.log("");
|
|
719
|
+
}
|
|
720
|
+
console.log(` Total: ${data.apiKeys.length} key(s), ${data.total} ${data.unit}`);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
637
724
|
const [walletResult, keysResult] = await Promise.all([
|
|
638
725
|
callDaemon("/balance"),
|
|
639
726
|
callDaemon("/keys/balance"),
|
|
@@ -757,6 +844,7 @@ program
|
|
|
757
844
|
context_length?: number;
|
|
758
845
|
providers: Array<{
|
|
759
846
|
baseUrl: string;
|
|
847
|
+
disabled?: boolean;
|
|
760
848
|
pricing: {
|
|
761
849
|
prompt: number;
|
|
762
850
|
completion: number;
|
|
@@ -783,7 +871,9 @@ program
|
|
|
783
871
|
}
|
|
784
872
|
console.log(`\n Providers (${modelData.providers.length}):`);
|
|
785
873
|
for (const provider of modelData.providers) {
|
|
786
|
-
console.log(
|
|
874
|
+
console.log(
|
|
875
|
+
`\n ${provider.baseUrl}${provider.disabled ? " [Disabled]" : ""}`,
|
|
876
|
+
);
|
|
787
877
|
console.log(
|
|
788
878
|
` Prompt: ${(provider.pricing.prompt * 1000000).toFixed(2)} sats/M tokens`,
|
|
789
879
|
);
|
|
@@ -1247,6 +1337,103 @@ npubsCmd
|
|
|
1247
1337
|
);
|
|
1248
1338
|
});
|
|
1249
1339
|
|
|
1340
|
+
program
|
|
1341
|
+
.command("history")
|
|
1342
|
+
.description("Show wallet transaction history")
|
|
1343
|
+
.option("-n, --limit <number>", "Number of entries to show", "50")
|
|
1344
|
+
.option("--offset <number>", "Number of entries to skip", "0")
|
|
1345
|
+
.option("-v, --verbose", "Show full details including encoded Cashu tokens")
|
|
1346
|
+
.option("--json", "Output raw JSON with token objects (no encoding)")
|
|
1347
|
+
.action(async (options: { limit: string; offset: string; verbose: boolean; json: boolean }) => {
|
|
1348
|
+
await ensureDaemonRunning();
|
|
1349
|
+
|
|
1350
|
+
const limit = Math.min(parseInt(options.limit, 10) || 50, 1000);
|
|
1351
|
+
const offset = parseInt(options.offset, 10) || 0;
|
|
1352
|
+
|
|
1353
|
+
const result = await callDaemon(
|
|
1354
|
+
`/wallet/history?offset=${offset}&limit=${limit}`,
|
|
1355
|
+
);
|
|
1356
|
+
|
|
1357
|
+
if (result.error) {
|
|
1358
|
+
console.log(result.error);
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const data = result.output as
|
|
1363
|
+
| { entries?: Record<string, unknown>[]; offset?: number; limit?: number }
|
|
1364
|
+
| undefined;
|
|
1365
|
+
const entries = data?.entries || [];
|
|
1366
|
+
|
|
1367
|
+
if (entries.length === 0) {
|
|
1368
|
+
console.log("No transaction history yet.");
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (options.json) {
|
|
1373
|
+
const jsonOutput = entries.map((e: Record<string, unknown>) => {
|
|
1374
|
+
const entry = { ...e };
|
|
1375
|
+
delete entry.encodedToken;
|
|
1376
|
+
return entry;
|
|
1377
|
+
});
|
|
1378
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (options.verbose) {
|
|
1383
|
+
for (const entry of entries) {
|
|
1384
|
+
const e = entry as Record<string, unknown>;
|
|
1385
|
+
const display: Record<string, unknown> = {};
|
|
1386
|
+
for (const key of Object.keys(e).sort()) {
|
|
1387
|
+
display[key] = e[key];
|
|
1388
|
+
}
|
|
1389
|
+
if (e.encodedToken && e.token) {
|
|
1390
|
+
display.token = e.encodedToken;
|
|
1391
|
+
delete display.encodedToken;
|
|
1392
|
+
}
|
|
1393
|
+
console.log(JSON.stringify(display, null, 2));
|
|
1394
|
+
}
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const idCol = "ID";
|
|
1399
|
+
const timeCol = "Date/Time";
|
|
1400
|
+
const typeCol = "Type";
|
|
1401
|
+
const mintCol = "Mint";
|
|
1402
|
+
const amtCol = "Amount";
|
|
1403
|
+
|
|
1404
|
+
const rows = entries.map((entry: Record<string, unknown>) => {
|
|
1405
|
+
const id = String(entry.id ?? "");
|
|
1406
|
+
const time = new Date(Number(entry.createdAt)).toISOString().replace("T", " ").slice(0, 19);
|
|
1407
|
+
const type = String(entry.type ?? "").toUpperCase();
|
|
1408
|
+
const mint = String(entry.mintUrl ?? "");
|
|
1409
|
+
const unit = String(entry.unit ?? "sat");
|
|
1410
|
+
const amount = `${entry.amount} ${unit}`;
|
|
1411
|
+
return { id, time, type, mint, amount };
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
const widths = {
|
|
1415
|
+
id: Math.max(idCol.length, ...rows.map((r) => r.id.length)),
|
|
1416
|
+
time: Math.max(timeCol.length, ...rows.map((r) => r.time.length)),
|
|
1417
|
+
type: Math.max(typeCol.length, ...rows.map((r) => r.type.length)),
|
|
1418
|
+
mint: Math.max(mintCol.length, ...rows.map((r) => r.mint.length)),
|
|
1419
|
+
amount: Math.max(amtCol.length, ...rows.map((r) => r.amount.length)),
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
const pad = (s: string, w: number) => s.padEnd(w);
|
|
1423
|
+
const sep = Object.values(widths).map((w) => "-".repeat(w)).join(" | ");
|
|
1424
|
+
|
|
1425
|
+
console.log(
|
|
1426
|
+
`${pad(idCol, widths.id)} | ${pad(timeCol, widths.time)} | ${pad(typeCol, widths.type)} | ${pad(mintCol, widths.mint)} | ${pad(amtCol, widths.amount)}`,
|
|
1427
|
+
);
|
|
1428
|
+
console.log(sep);
|
|
1429
|
+
|
|
1430
|
+
for (const row of rows) {
|
|
1431
|
+
console.log(
|
|
1432
|
+
`${pad(row.id, widths.id)} | ${pad(row.time, widths.time)} | ${pad(row.type, widths.type)} | ${pad(row.mint, widths.mint)} | ${pad(row.amount, widths.amount)}`,
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1250
1437
|
// Monitor - interactive TUI
|
|
1251
1438
|
program
|
|
1252
1439
|
.command("monitor")
|
|
@@ -1472,6 +1659,47 @@ walletMintsCmd
|
|
|
1472
1659
|
});
|
|
1473
1660
|
});
|
|
1474
1661
|
|
|
1662
|
+
walletMintsCmd
|
|
1663
|
+
.command("set-default <url>")
|
|
1664
|
+
.description("Set the default mint for wallet operations")
|
|
1665
|
+
.action(async (url: string) => {
|
|
1666
|
+
await handleDaemonCommand("/wallet/mints/default", {
|
|
1667
|
+
method: "POST",
|
|
1668
|
+
body: { url },
|
|
1669
|
+
});
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
const walletNpcCmd = walletCmd
|
|
1673
|
+
.command("npc")
|
|
1674
|
+
.description("NPC (npubx.cash) Lightning address operations");
|
|
1675
|
+
|
|
1676
|
+
walletNpcCmd
|
|
1677
|
+
.command("address")
|
|
1678
|
+
.description("Show this wallet's NPC Lightning address")
|
|
1679
|
+
.action(async () => {
|
|
1680
|
+
await handleDaemonCommand("/wallet/npc/address");
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
walletNpcCmd
|
|
1684
|
+
.command("username <name>")
|
|
1685
|
+
.description(
|
|
1686
|
+
"Claim an NPC username (pays the claim fee from the wallet with --confirm)",
|
|
1687
|
+
)
|
|
1688
|
+
.option("--confirm", "Confirm payment of the username claim fee")
|
|
1689
|
+
.action(async (name: string, options: { confirm?: boolean }) => {
|
|
1690
|
+
await handleDaemonCommand("/wallet/npc/username", {
|
|
1691
|
+
method: "POST",
|
|
1692
|
+
body: { username: name, confirm: options.confirm === true },
|
|
1693
|
+
});
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
walletNpcCmd
|
|
1697
|
+
.command("sync")
|
|
1698
|
+
.description("Manually sync paid NPC quotes into the wallet")
|
|
1699
|
+
.action(async () => {
|
|
1700
|
+
await handleDaemonCommand("/wallet/npc/sync", { method: "POST" });
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1475
1703
|
// ── NWC (Nostr Wallet Connect) commands ─────────────────────────
|
|
1476
1704
|
|
|
1477
1705
|
const nwcCmd = program
|
|
@@ -1638,6 +1866,8 @@ serviceCmd
|
|
|
1638
1866
|
|
|
1639
1867
|
console.log("Starting routstrd via PM2...");
|
|
1640
1868
|
try {
|
|
1869
|
+
await stopLegacyCocod();
|
|
1870
|
+
|
|
1641
1871
|
// Use --interpreter bun to ensure it runs with bun
|
|
1642
1872
|
execSync(`pm2 start "${daemonPath}" --name routstrd --interpreter bun`, {
|
|
1643
1873
|
stdio: "inherit",
|
|
@@ -1685,8 +1915,9 @@ program
|
|
|
1685
1915
|
.command("restart")
|
|
1686
1916
|
.description("Restart the background daemon")
|
|
1687
1917
|
.option("--port <port>", "Port to listen on")
|
|
1918
|
+
.option("--host <host>", "Bind address (default: 127.0.0.1)")
|
|
1688
1919
|
.option("-p, --provider <provider>", "Default provider to use")
|
|
1689
|
-
.action(async (options: { port?: string; provider?: string }) => {
|
|
1920
|
+
.action(async (options: { port?: string; host?: string; provider?: string }) => {
|
|
1690
1921
|
await requireLocalDaemon();
|
|
1691
1922
|
const config = await loadConfig();
|
|
1692
1923
|
const wasRunning = await isDaemonRunning();
|
|
@@ -1695,16 +1926,19 @@ program
|
|
|
1695
1926
|
console.log("Stopping daemon...");
|
|
1696
1927
|
await callDaemon("/stop", { method: "POST" });
|
|
1697
1928
|
|
|
1698
|
-
// Wait for
|
|
1699
|
-
|
|
1929
|
+
// Wait for HTTP health check to fail AND wallet lock to be released.
|
|
1930
|
+
const pidFilePath = walletPidPath();
|
|
1931
|
+
for (let i = 0; i < 100; i++) {
|
|
1700
1932
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1701
|
-
|
|
1933
|
+
const healthDown = !(await isDaemonRunning());
|
|
1934
|
+
const pidFileReleased = !existsSync(pidFilePath);
|
|
1935
|
+
if (healthDown && pidFileReleased) {
|
|
1702
1936
|
break;
|
|
1703
1937
|
}
|
|
1704
1938
|
}
|
|
1705
1939
|
|
|
1706
1940
|
if (await isDaemonRunning()) {
|
|
1707
|
-
logger.error("Daemon failed to stop within
|
|
1941
|
+
logger.error("Daemon failed to stop within 10 seconds");
|
|
1708
1942
|
process.exit(1);
|
|
1709
1943
|
}
|
|
1710
1944
|
console.log("Daemon stopped.");
|
|
@@ -1712,9 +1946,12 @@ program
|
|
|
1712
1946
|
console.log("Daemon was not running.");
|
|
1713
1947
|
}
|
|
1714
1948
|
|
|
1949
|
+
await stopLegacyCocod();
|
|
1950
|
+
|
|
1715
1951
|
console.log("Starting daemon...");
|
|
1716
1952
|
await startDaemon({
|
|
1717
1953
|
port: options.port || String(config.port || 8008),
|
|
1954
|
+
host: options.host || config.host || undefined,
|
|
1718
1955
|
provider: options.provider,
|
|
1719
1956
|
});
|
|
1720
1957
|
console.log("Daemon restarted.");
|
|
@@ -1797,34 +2034,71 @@ program
|
|
|
1797
2034
|
console.log("Starting daemon...");
|
|
1798
2035
|
await startDaemon({
|
|
1799
2036
|
port: String(config.port || 8008),
|
|
2037
|
+
host: config.host || undefined,
|
|
1800
2038
|
provider: config.provider || undefined,
|
|
1801
2039
|
});
|
|
1802
2040
|
console.log(`Daemon restarted with mode '${selectedMode}'.`);
|
|
1803
2041
|
});
|
|
1804
2042
|
|
|
1805
2043
|
// Logs
|
|
1806
|
-
function getLogFileForDate(date: Date = new Date()): string {
|
|
2044
|
+
function getLogFileForDate(logsDir: string, date: Date = new Date()): string {
|
|
1807
2045
|
const year = date.getFullYear();
|
|
1808
2046
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1809
2047
|
const day = String(date.getDate()).padStart(2, "0");
|
|
1810
|
-
return `${
|
|
2048
|
+
return `${logsDir}/${year}-${month}-${day}.log`;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
function readLastLines(file: string, lines: number): string {
|
|
2052
|
+
const content = readFileSync(file, "utf8");
|
|
2053
|
+
const allLines = content.replace(/\r\n/g, "\n").split("\n");
|
|
2054
|
+
if (allLines.at(-1) === "") allLines.pop();
|
|
2055
|
+
return allLines.slice(-lines).join("\n");
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
async function followLogFile(file: string, lines: number): Promise<void> {
|
|
2059
|
+
const initial = readLastLines(file, lines);
|
|
2060
|
+
if (initial) {
|
|
2061
|
+
console.log(initial);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
let position = statSync(file).size;
|
|
2065
|
+
while (true) {
|
|
2066
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2067
|
+
if (!existsSync(file)) {
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
const size = statSync(file).size;
|
|
2072
|
+
if (size < position) {
|
|
2073
|
+
position = 0;
|
|
2074
|
+
}
|
|
2075
|
+
if (size === position) {
|
|
2076
|
+
continue;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const text = await Bun.file(file).slice(position, size).text();
|
|
2080
|
+
process.stdout.write(text);
|
|
2081
|
+
position = size;
|
|
2082
|
+
}
|
|
1811
2083
|
}
|
|
1812
2084
|
|
|
1813
2085
|
program
|
|
1814
2086
|
.command("logs")
|
|
1815
2087
|
.description("View daemon logs")
|
|
1816
2088
|
.option("-f, --follow", "Follow log output", false)
|
|
2089
|
+
.option("-c, --coco", "Show Cashu wallet-engine (coco) logs instead of daemon logs", false)
|
|
1817
2090
|
.option("-n, --lines <number>", "Number of lines to show", "50")
|
|
1818
|
-
.action(async (options: { follow: boolean; lines: string }) => {
|
|
2091
|
+
.action(async (options: { follow: boolean; lines: string; coco: boolean }) => {
|
|
1819
2092
|
await requireLocalDaemon();
|
|
1820
|
-
const
|
|
2093
|
+
const logsDir = options.coco ? COCO_LOGS_DIR : LOGS_DIR;
|
|
2094
|
+
const todayFile = getLogFileForDate(logsDir);
|
|
1821
2095
|
const yesterday = new Date();
|
|
1822
2096
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
1823
|
-
const yesterdayFile = getLogFileForDate(yesterday);
|
|
2097
|
+
const yesterdayFile = getLogFileForDate(logsDir, yesterday);
|
|
1824
2098
|
|
|
1825
2099
|
if (!existsSync(todayFile) && !existsSync(yesterdayFile)) {
|
|
1826
2100
|
console.log("No log files found. Daemon may not have started yet.");
|
|
1827
|
-
console.log(`Logs directory: ${
|
|
2101
|
+
console.log(`Logs directory: ${logsDir}`);
|
|
1828
2102
|
process.exit(1);
|
|
1829
2103
|
}
|
|
1830
2104
|
|
|
@@ -1835,24 +2109,23 @@ program
|
|
|
1835
2109
|
});
|
|
1836
2110
|
|
|
1837
2111
|
if (options.follow) {
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
const exitCode = await proc.exited;
|
|
1845
|
-
process.exit(exitCode);
|
|
2112
|
+
if (existsSync(todayFile)) {
|
|
2113
|
+
await followLogFile(todayFile, lines);
|
|
2114
|
+
} else {
|
|
2115
|
+
console.log("No log file for today to follow.");
|
|
2116
|
+
}
|
|
2117
|
+
return;
|
|
1846
2118
|
}
|
|
1847
2119
|
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2120
|
+
for (const file of logFiles) {
|
|
2121
|
+
if (logFiles.length > 1) {
|
|
2122
|
+
console.log(`==> ${file} <==`);
|
|
2123
|
+
}
|
|
2124
|
+
const output = readLastLines(file, lines);
|
|
2125
|
+
if (output) {
|
|
2126
|
+
console.log(output);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
1856
2129
|
});
|
|
1857
2130
|
|
|
1858
2131
|
export function cli(args: string[]) {
|