routstrd 0.4.0 → 0.4.1
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/COCO-2.0.0-MIGRATION-PLAN.md +1191 -0
- package/README.md +5 -0
- package/dist/daemon/index.js +1096 -143
- package/dist/index.js +572 -124
- package/package.json +1 -1
- package/src/cli.ts +160 -26
- package/src/daemon/fatal-error.test.ts +104 -0
- package/src/daemon/fatal-error.ts +48 -0
- package/src/daemon/wallet/auto-refill.ts +4 -6
- package/src/daemon/wallet/cleanup.test.ts +168 -0
- package/src/daemon/wallet/cleanup.ts +104 -0
- package/src/daemon/wallet/coco-client.test.ts +191 -30
- package/src/daemon/wallet/coco-client.ts +867 -74
- package/src/daemon/wallet/cocod-client.ts +49 -1
- package/src/daemon/wallet/diagnostics.test.ts +378 -0
- package/src/daemon/wallet/diagnostics.ts +471 -0
- package/src/daemon/wallet/migration.test.ts +30 -6
- package/src/daemon/wallet/migration.ts +30 -85
- package/src/daemon/wallet/receive-dedup.test.ts +238 -0
- package/src/daemon/wallet/receive-dedup.ts +384 -0
- package/src/daemon/wallet/wallet-state.ts +75 -0
- package/src/start-daemon.ts +21 -1
- package/tests/integrations/hermes.test.ts +42 -5
package/dist/index.js
CHANGED
|
@@ -9914,6 +9914,17 @@ async function startDaemonUnlocked(options) {
|
|
|
9914
9914
|
detached: true
|
|
9915
9915
|
});
|
|
9916
9916
|
proc.unref();
|
|
9917
|
+
const killSpawnedDaemon = () => {
|
|
9918
|
+
try {
|
|
9919
|
+
process.kill(-proc.pid, "SIGTERM");
|
|
9920
|
+
} catch {}
|
|
9921
|
+
};
|
|
9922
|
+
const abandonOnSignal = () => {
|
|
9923
|
+
killSpawnedDaemon();
|
|
9924
|
+
process.exit(1);
|
|
9925
|
+
};
|
|
9926
|
+
process.once("SIGINT", abandonOnSignal);
|
|
9927
|
+
process.once("SIGTERM", abandonOnSignal);
|
|
9917
9928
|
let exitCode = null;
|
|
9918
9929
|
proc.exited.then((code) => {
|
|
9919
9930
|
exitCode = code;
|
|
@@ -9944,12 +9955,15 @@ Daemon output:
|
|
|
9944
9955
|
${daemonOutput}` : ` Check logs in ${LOGS_DIR2}`));
|
|
9945
9956
|
}
|
|
9946
9957
|
if (await healthyDaemonHost(port, host)) {
|
|
9958
|
+
process.off("SIGINT", abandonOnSignal);
|
|
9959
|
+
process.off("SIGTERM", abandonOnSignal);
|
|
9947
9960
|
printStartupProgress(progressLogOffset);
|
|
9948
9961
|
console.log(`Routstr daemon started (PID: ${proc.pid}, ${formatElapsed(Date.now() - startedAt)}).`);
|
|
9949
9962
|
return;
|
|
9950
9963
|
}
|
|
9951
9964
|
}
|
|
9952
|
-
|
|
9965
|
+
killSpawnedDaemon();
|
|
9966
|
+
throw new Error(`Daemon did not become healthy within ${Math.round(startupTimeoutMs / 1000)} seconds; ` + `the incomplete daemon (PID ${proc.pid}) was stopped. Check logs in ${LOGS_DIR2}`);
|
|
9953
9967
|
}
|
|
9954
9968
|
async function startDaemon(options = {}) {
|
|
9955
9969
|
const port = options.port || "8008";
|
|
@@ -21276,7 +21290,7 @@ function formatTime(timestamp) {
|
|
|
21276
21290
|
const p = (n) => String(n).padStart(2, "0");
|
|
21277
21291
|
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
21278
21292
|
}
|
|
21279
|
-
function
|
|
21293
|
+
function formatNumber2(n) {
|
|
21280
21294
|
if (n >= 1e6)
|
|
21281
21295
|
return (n / 1e6).toFixed(1) + "M";
|
|
21282
21296
|
if (n >= 1000)
|
|
@@ -21518,8 +21532,8 @@ function renderOverview(stats, balance, status, width) {
|
|
|
21518
21532
|
`${COLORS.bold}Avg Cost/Req:${COLORS.reset} ${formatCost(avgCost)} sats`
|
|
21519
21533
|
];
|
|
21520
21534
|
const rightBox = [
|
|
21521
|
-
`${COLORS.bold}Total Tokens:${COLORS.reset} ${
|
|
21522
|
-
`${COLORS.bold}Avg Tokens/Req:${COLORS.reset} ${
|
|
21535
|
+
`${COLORS.bold}Total Tokens:${COLORS.reset} ${formatNumber2(totals.totalTokens)}`,
|
|
21536
|
+
`${COLORS.bold}Avg Tokens/Req:${COLORS.reset} ${formatNumber2(Math.round(avgTokens))}`,
|
|
21523
21537
|
`${COLORS.bold}Prompt/Comp:${COLORS.reset} ${(totals.promptTokens / Math.max(1, totals.completionTokens)).toFixed(2)}x`
|
|
21524
21538
|
];
|
|
21525
21539
|
const halfWidth1 = Math.floor(width / 2);
|
|
@@ -21667,11 +21681,11 @@ function renderToday(stats, width) {
|
|
|
21667
21681
|
`${COLORS.bold}Date:${COLORS.reset} ${todayStats.date}`,
|
|
21668
21682
|
`${COLORS.bold}Requests:${COLORS.reset} ${formatReqs(todayStats.requests)}`,
|
|
21669
21683
|
`${COLORS.bold}Cost:${COLORS.reset} ${COLORS.green}${formatCost(todayStats.satsCost)} sats${COLORS.reset}`,
|
|
21670
|
-
`${COLORS.bold}Tokens:${COLORS.reset} ${
|
|
21684
|
+
`${COLORS.bold}Tokens:${COLORS.reset} ${formatNumber2(todayStats.totalTokens)} (p: ${formatNumber2(todayStats.promptTokens)} + c: ${formatNumber2(todayStats.completionTokens)})`
|
|
21671
21685
|
];
|
|
21672
21686
|
let output = renderBox(summaryLines, width, "Today");
|
|
21673
21687
|
if (recentDays.length > 0) {
|
|
21674
|
-
const dayLines = recentDays.map((d) => `${d.date}: ${formatReqs(d.requests)} req, ${formatCost(d.satsCost)} sats, ${
|
|
21688
|
+
const dayLines = recentDays.map((d) => `${d.date}: ${formatReqs(d.requests)} req, ${formatCost(d.satsCost)} sats, ${formatNumber2(d.totalTokens)} tokens`);
|
|
21675
21689
|
output += `
|
|
21676
21690
|
` + renderBox(dayLines, width, "Recent Days");
|
|
21677
21691
|
}
|
|
@@ -21713,7 +21727,7 @@ function renderModels(stats, width) {
|
|
|
21713
21727
|
lines.push(`${color}${COLORS.bold}${model.modelId}${COLORS.reset}`);
|
|
21714
21728
|
lines.push(` ${COLORS.dim}Cost:${COLORS.reset} ${formatCost(model.satsCost)} sats (${pct}%)`);
|
|
21715
21729
|
lines.push(` ${COLORS.dim}Requests:${COLORS.reset} ${formatReqs(model.requests)}`);
|
|
21716
|
-
lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${
|
|
21730
|
+
lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${formatNumber2(model.totalTokens)}`);
|
|
21717
21731
|
lines.push(` ${COLORS.dim}Avg:${COLORS.reset} ${formatCost(model.satsCost / model.requests)} sats/req`);
|
|
21718
21732
|
lines.push(` ${renderBarChart(" ", model.satsCost, maxCost, width - 6, color, Number(pct), "model-detail")}`);
|
|
21719
21733
|
lines.push("");
|
|
@@ -21731,7 +21745,7 @@ function renderProviders(stats, width) {
|
|
|
21731
21745
|
lines.push(`${COLORS.cyan}${COLORS.bold}${shortUrl}${COLORS.reset}`);
|
|
21732
21746
|
lines.push(` ${COLORS.dim}Requests:${COLORS.reset} ${formatReqs(provider.requests)}`);
|
|
21733
21747
|
lines.push(` ${COLORS.dim}Cost:${COLORS.reset} ${formatCost(provider.satsCost)} sats`);
|
|
21734
|
-
lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${
|
|
21748
|
+
lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${formatNumber2(provider.totalTokens)}`);
|
|
21735
21749
|
lines.push("");
|
|
21736
21750
|
}
|
|
21737
21751
|
return renderBox(lines, width, "Provider Breakdown");
|
|
@@ -21740,9 +21754,9 @@ function renderTokens(stats, width) {
|
|
|
21740
21754
|
const totals = stats.summary.totals;
|
|
21741
21755
|
const modelStats = stats.summary.models;
|
|
21742
21756
|
const summaryLines = [
|
|
21743
|
-
`${COLORS.bold}Total Prompt Tokens:${COLORS.reset} ${
|
|
21744
|
-
`${COLORS.bold}Total Completion Tokens:${COLORS.reset} ${
|
|
21745
|
-
`${COLORS.bold}Total Tokens:${COLORS.reset} ${
|
|
21757
|
+
`${COLORS.bold}Total Prompt Tokens:${COLORS.reset} ${formatNumber2(totals.promptTokens)}`,
|
|
21758
|
+
`${COLORS.bold}Total Completion Tokens:${COLORS.reset} ${formatNumber2(totals.completionTokens)}`,
|
|
21759
|
+
`${COLORS.bold}Total Tokens:${COLORS.reset} ${formatNumber2(totals.totalTokens)}`,
|
|
21746
21760
|
`${COLORS.bold}Prompt/Completion Ratio:${COLORS.reset} ${(totals.promptTokens / Math.max(1, totals.completionTokens)).toFixed(2)}x`,
|
|
21747
21761
|
`${COLORS.bold}Avg Tokens/Request:${COLORS.reset} ${(totals.totalTokens / Math.max(1, stats.totalEntries)).toFixed(0)}`
|
|
21748
21762
|
];
|
|
@@ -21751,7 +21765,7 @@ function renderTokens(stats, width) {
|
|
|
21751
21765
|
const tokenLines = modelStats.slice(0, 6).map((m) => {
|
|
21752
21766
|
const color = MODEL_COLORS[m.modelId] || MODEL_COLORS.default;
|
|
21753
21767
|
const promptPct = m.totalTokens > 0 ? (m.promptTokens / m.totalTokens * 100).toFixed(0) : "0";
|
|
21754
|
-
return `${color}${m.modelId.padEnd(15)}${COLORS.reset} ${
|
|
21768
|
+
return `${color}${m.modelId.padEnd(15)}${COLORS.reset} ${formatNumber2(m.totalTokens).padStart(8)} tokens (${promptPct}% prompt)`;
|
|
21755
21769
|
});
|
|
21756
21770
|
output += `
|
|
21757
21771
|
` + renderBox(tokenLines, width, "Tokens by Model");
|
|
@@ -21787,7 +21801,7 @@ function renderClients(stats, width) {
|
|
|
21787
21801
|
const dClient = client.client.slice(0, col1 - 1).padEnd(col1);
|
|
21788
21802
|
const dReqs = formatReqs(client.requests).padEnd(col2);
|
|
21789
21803
|
const dCost = `${formatCost(client.satsCost)} sats (${pct}%)`.padEnd(col3);
|
|
21790
|
-
const dTok =
|
|
21804
|
+
const dTok = formatNumber2(client.totalTokens).padEnd(col4);
|
|
21791
21805
|
const dAvg = `${avgCostFormatted} sats/req`;
|
|
21792
21806
|
lines.push(`${color}${COLORS.bold}${dClient}${COLORS.reset}` + `${dReqs}` + `${COLORS.green}${dCost}${COLORS.reset}` + `${COLORS.dim}${dTok}${dAvg}${COLORS.reset}`);
|
|
21793
21807
|
lines.push(` ${renderBarChart("", client.satsCost, maxCost, width - 6, color, Number(pct), "client-detail")}`);
|
|
@@ -21802,7 +21816,7 @@ function renderClients(stats, width) {
|
|
|
21802
21816
|
continue;
|
|
21803
21817
|
clientModelLines.push(`${COLORS.bold}${topClient.client}${COLORS.reset} (${formatReqs(topClient.requests)} reqs, ${formatCost(topClient.satsCost)} sats)`);
|
|
21804
21818
|
for (const m of topClient.topModels) {
|
|
21805
|
-
clientModelLines.push(` ${MODEL_COLORS[m.modelId] || MODEL_COLORS.default}${m.modelId.padEnd(18)}${COLORS.reset} ${
|
|
21819
|
+
clientModelLines.push(` ${MODEL_COLORS[m.modelId] || MODEL_COLORS.default}${m.modelId.padEnd(18)}${COLORS.reset} ${formatNumber2(m.totalTokens).padEnd(8)} tokens ${formatCost(m.satsCost)} sats`);
|
|
21806
21820
|
}
|
|
21807
21821
|
clientModelLines.push("");
|
|
21808
21822
|
}
|
|
@@ -21838,7 +21852,7 @@ function renderNpubs(stats, clients, width) {
|
|
|
21838
21852
|
const dNpub = shortNpub.padEnd(col1);
|
|
21839
21853
|
const dReqs = formatReqs(npub.requests).padEnd(col2);
|
|
21840
21854
|
const dCost = `${formatCost(npub.satsCost)} sats (${pct}%)`.padEnd(col3);
|
|
21841
|
-
const dTok =
|
|
21855
|
+
const dTok = formatNumber2(npub.totalTokens).padEnd(col4);
|
|
21842
21856
|
const dAvg = `${avgCostFormatted} sats/req`;
|
|
21843
21857
|
lines.push(`${COLORS.magenta}${COLORS.bold}${dNpub}${COLORS.reset}` + `${dReqs}` + `${COLORS.green}${dCost}${COLORS.reset}` + `${COLORS.dim}${dTok}${dAvg}${COLORS.reset}`);
|
|
21844
21858
|
lines.push(` ${COLORS.dim}${npub.npub}${COLORS.reset}`);
|
|
@@ -21854,7 +21868,7 @@ function renderNpubs(stats, clients, width) {
|
|
|
21854
21868
|
continue;
|
|
21855
21869
|
npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`);
|
|
21856
21870
|
for (const m of topNpub.topModels) {
|
|
21857
|
-
npubModelLines.push(` ${MODEL_COLORS[m.modelId] || MODEL_COLORS.default}${m.modelId.padEnd(18)}${COLORS.reset} ${
|
|
21871
|
+
npubModelLines.push(` ${MODEL_COLORS[m.modelId] || MODEL_COLORS.default}${m.modelId.padEnd(18)}${COLORS.reset} ${formatNumber2(m.totalTokens).padEnd(8)} tokens ${formatCost(m.satsCost)} sats`);
|
|
21858
21872
|
}
|
|
21859
21873
|
npubModelLines.push("");
|
|
21860
21874
|
}
|
|
@@ -21890,7 +21904,7 @@ function renderRecent(stats, width) {
|
|
|
21890
21904
|
entry.cacheReadInputTokens || 0,
|
|
21891
21905
|
entry.cacheCreationInputTokens || 0,
|
|
21892
21906
|
entry.completionTokens
|
|
21893
|
-
].map(
|
|
21907
|
+
].map(formatNumber2).join("/");
|
|
21894
21908
|
const totalSats = typeof entry.totalMsats === "number" ? entry.totalMsats / 1000 : entry.satsCost;
|
|
21895
21909
|
const cost = [
|
|
21896
21910
|
formatCost(msatsToSats(entry.inputMsats)),
|
|
@@ -22898,9 +22912,9 @@ async function addClientAction(options) {
|
|
|
22898
22912
|
// src/cli.ts
|
|
22899
22913
|
init_config();
|
|
22900
22914
|
init_logger();
|
|
22901
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
22915
|
+
import { chmodSync as chmodSync2, existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
22902
22916
|
import { execSync } from "child_process";
|
|
22903
|
-
import { dirname as dirname8, join as
|
|
22917
|
+
import { dirname as dirname8, join as join9 } from "path";
|
|
22904
22918
|
|
|
22905
22919
|
// src/integrations/index.ts
|
|
22906
22920
|
init_logger();
|
|
@@ -24255,6 +24269,11 @@ var STALE_SOCKET_ERROR_CODES = new Set([
|
|
|
24255
24269
|
"ENOENT",
|
|
24256
24270
|
"FailedToOpenSocket"
|
|
24257
24271
|
]);
|
|
24272
|
+
var RECOVERY_STALL_MARKERS = new Map([
|
|
24273
|
+
["SendOperationService\x00Could not reach mint for recovery, will retry later", "Send recovery: mint unreachable"],
|
|
24274
|
+
["ReceiveOperationService\x00Could not reach mint for receive recovery, will retry later", "Receive recovery: mint unreachable"],
|
|
24275
|
+
["MintOperationService\x00Failed to reconcile stale pending mint operation", "Mint recovery: pending operation check failed"]
|
|
24276
|
+
]);
|
|
24258
24277
|
var SAFE_COCO_LOG_FIELDS = new Set([
|
|
24259
24278
|
"module",
|
|
24260
24279
|
"mintUrl",
|
|
@@ -24271,13 +24290,22 @@ var SAFE_COCO_LOG_FIELDS = new Set([
|
|
|
24271
24290
|
"rollingBackOperations",
|
|
24272
24291
|
"orphanedReservations"
|
|
24273
24292
|
]);
|
|
24293
|
+
function isZombieProcess(pid, readFile7 = (path) => readFileSync(path, "utf-8")) {
|
|
24294
|
+
try {
|
|
24295
|
+
const stat2 = readFile7(`/proc/${pid}/stat`);
|
|
24296
|
+
const commandEnd = stat2.lastIndexOf(")");
|
|
24297
|
+
return commandEnd >= 0 && stat2.charAt(commandEnd + 2) === "Z";
|
|
24298
|
+
} catch {
|
|
24299
|
+
return false;
|
|
24300
|
+
}
|
|
24301
|
+
}
|
|
24274
24302
|
function defaultIsProcessRunning(pid) {
|
|
24275
24303
|
try {
|
|
24276
24304
|
process.kill(pid, 0);
|
|
24277
|
-
return true;
|
|
24278
24305
|
} catch (error) {
|
|
24279
24306
|
return error.code === "EPERM";
|
|
24280
24307
|
}
|
|
24308
|
+
return !isZombieProcess(pid);
|
|
24281
24309
|
}
|
|
24282
24310
|
function hasErrorCode(error, codes) {
|
|
24283
24311
|
let current = error;
|
|
@@ -24294,24 +24322,7 @@ function hasErrorCode(error, codes) {
|
|
|
24294
24322
|
}
|
|
24295
24323
|
async function assertLegacyCocodNotRunning(options = {}) {
|
|
24296
24324
|
const socketPath = options.socketPath || legacyCocodSocketPath();
|
|
24297
|
-
const pidFilePath = options.pidFilePath || legacyCocodPidPath();
|
|
24298
24325
|
const pathExists = options.pathExists || existsSync9;
|
|
24299
|
-
const readFile7 = options.readFile || ((path) => readFileSync(path, "utf-8"));
|
|
24300
|
-
const isProcessRunning2 = options.isProcessRunning || defaultIsProcessRunning;
|
|
24301
|
-
const getRunningLegacyPid = () => {
|
|
24302
|
-
if (!pathExists(pidFilePath))
|
|
24303
|
-
return null;
|
|
24304
|
-
try {
|
|
24305
|
-
const pid = Number.parseInt(readFile7(pidFilePath).trim(), 10);
|
|
24306
|
-
return Number.isInteger(pid) && pid > 0 && pid !== options.ignorePid && isProcessRunning2(pid) ? pid : null;
|
|
24307
|
-
} catch {
|
|
24308
|
-
return null;
|
|
24309
|
-
}
|
|
24310
|
-
};
|
|
24311
|
-
const runningPid = getRunningLegacyPid();
|
|
24312
|
-
if (runningPid !== null) {
|
|
24313
|
-
throw new Error(`Legacy cocod daemon is still running with PID ${runningPid}. ` + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + `Run 'cocod stop' or 'kill ${runningPid}' and try again.`);
|
|
24314
|
-
}
|
|
24315
24326
|
if (!pathExists(socketPath))
|
|
24316
24327
|
return;
|
|
24317
24328
|
const fetchImpl = options.fetchImpl || fetch;
|
|
@@ -24324,12 +24335,8 @@ async function assertLegacyCocodNotRunning(options = {}) {
|
|
|
24324
24335
|
await response.body?.cancel();
|
|
24325
24336
|
} catch (error) {
|
|
24326
24337
|
if (hasErrorCode(error, STALE_SOCKET_ERROR_CODES)) {
|
|
24327
|
-
|
|
24328
|
-
|
|
24329
|
-
logger.debug(`Ignoring stale legacy cocod socket at ${socketPath}`);
|
|
24330
|
-
return;
|
|
24331
|
-
}
|
|
24332
|
-
throw new Error(`Legacy cocod daemon is still running with PID ${newlyRunningPid}. ` + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + `Run 'cocod stop' or 'kill ${newlyRunningPid}' and try again.`, { cause: error });
|
|
24338
|
+
logger.debug(`Ignoring stale legacy cocod socket at ${socketPath}`);
|
|
24339
|
+
return;
|
|
24333
24340
|
}
|
|
24334
24341
|
throw new Error(`Cannot verify whether the legacy cocod daemon has stopped at ${socketPath}. ` + "Refusing to open the wallet database to prevent concurrent access. " + "Run 'cocod stop', verify the daemon has exited, and try again.", { cause: error });
|
|
24335
24342
|
}
|
|
@@ -24393,12 +24400,14 @@ async function stopLegacyCocod(options = {}) {
|
|
|
24393
24400
|
function claimLegacyCocodPidFile(options = {}) {
|
|
24394
24401
|
return claimPidFile({
|
|
24395
24402
|
...options,
|
|
24396
|
-
pidFilePath: options.pidFilePath || legacyCocodPidPath()
|
|
24403
|
+
pidFilePath: options.pidFilePath || legacyCocodPidPath(),
|
|
24404
|
+
label: options.label || "legacy cocod exclusion lock"
|
|
24397
24405
|
});
|
|
24398
24406
|
}
|
|
24399
24407
|
function claimPidFile(options) {
|
|
24400
24408
|
const pidFilePath = options.pidFilePath;
|
|
24401
24409
|
const pid = options.pid ?? process.pid;
|
|
24410
|
+
const label = options.label || "wallet process lock";
|
|
24402
24411
|
const openExclusive = options.openExclusive || ((path) => openSync2(path, "wx", 384));
|
|
24403
24412
|
const writePid = options.writePid || ((fd2, ownerPid) => writeFileSync(fd2, String(ownerPid)));
|
|
24404
24413
|
const closeFile = options.closeFile || closeSync2;
|
|
@@ -24415,16 +24424,17 @@ function claimPidFile(options) {
|
|
|
24415
24424
|
try {
|
|
24416
24425
|
stalePid = Number.parseInt(readFile7(pidFilePath).trim(), 10);
|
|
24417
24426
|
} catch {
|
|
24418
|
-
throw new Error(`Cannot claim the
|
|
24427
|
+
throw new Error(`Cannot claim the ${label} at ${pidFilePath}. ` + "Another cocod or routstrd process may be starting. Stop it and try again.", { cause: error });
|
|
24419
24428
|
}
|
|
24420
24429
|
if (!Number.isInteger(stalePid) || stalePid <= 0 || isProcessRunning2(stalePid)) {
|
|
24421
|
-
|
|
24430
|
+
const ownerMessage = Number.isInteger(stalePid) && stalePid > 0 ? `PID ${stalePid} is still running and holds it. ` + `Stop that process first ('routstrd stop' or 'kill ${stalePid}').` : "Another cocod or routstrd process may be starting. Stop it and try again.";
|
|
24431
|
+
throw new Error(`Cannot claim the ${label} at ${pidFilePath}: ${ownerMessage}`, { cause: error });
|
|
24422
24432
|
}
|
|
24423
24433
|
try {
|
|
24424
24434
|
removeFile(pidFilePath);
|
|
24425
24435
|
fd = openExclusive(pidFilePath);
|
|
24426
24436
|
} catch (retryError) {
|
|
24427
|
-
throw new Error(`Cannot claim the
|
|
24437
|
+
throw new Error(`Cannot claim the ${label} at ${pidFilePath}. ` + "Another cocod or routstrd process may be starting. Stop it and try again.", { cause: retryError });
|
|
24428
24438
|
}
|
|
24429
24439
|
}
|
|
24430
24440
|
try {
|
|
@@ -24438,36 +24448,48 @@ function claimPidFile(options) {
|
|
|
24438
24448
|
closeFile(fd);
|
|
24439
24449
|
}
|
|
24440
24450
|
let released = false;
|
|
24441
|
-
|
|
24451
|
+
const release = () => {
|
|
24442
24452
|
if (released)
|
|
24443
24453
|
return;
|
|
24444
24454
|
released = true;
|
|
24455
|
+
process.removeListener("exit", release);
|
|
24445
24456
|
try {
|
|
24446
24457
|
if (readFile7(pidFilePath).trim() === String(pid)) {
|
|
24447
24458
|
removeFile(pidFilePath);
|
|
24448
24459
|
}
|
|
24449
24460
|
} catch (error) {
|
|
24450
24461
|
if (error.code !== "ENOENT") {
|
|
24451
|
-
logger.warn(`Failed to release
|
|
24462
|
+
logger.warn(`Failed to release ${label} at ${pidFilePath}:`, error);
|
|
24452
24463
|
}
|
|
24453
24464
|
}
|
|
24454
24465
|
};
|
|
24466
|
+
process.once("exit", release);
|
|
24467
|
+
return release;
|
|
24455
24468
|
}
|
|
24456
24469
|
|
|
24457
24470
|
// src/daemon/wallet/migration.ts
|
|
24458
24471
|
import {
|
|
24459
24472
|
chmodSync,
|
|
24460
24473
|
copyFileSync,
|
|
24461
|
-
existsSync as
|
|
24474
|
+
existsSync as existsSync12,
|
|
24462
24475
|
mkdirSync as mkdirSync8,
|
|
24463
|
-
readFileSync as readFileSync2,
|
|
24464
24476
|
renameSync as renameSync2,
|
|
24465
24477
|
rmSync,
|
|
24466
|
-
statSync
|
|
24478
|
+
statSync as statSync3
|
|
24467
24479
|
} from "fs";
|
|
24480
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
24481
|
+
import { basename, dirname as dirname7, join as join8 } from "path";
|
|
24482
|
+
|
|
24483
|
+
// src/daemon/wallet/diagnostics.ts
|
|
24484
|
+
import { createHash } from "crypto";
|
|
24485
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
24468
24486
|
import { Database } from "bun:sqlite";
|
|
24469
|
-
import {
|
|
24470
|
-
|
|
24487
|
+
import { join as join7 } from "path";
|
|
24488
|
+
|
|
24489
|
+
// src/daemon/wallet/wallet-state.ts
|
|
24490
|
+
import { existsSync as existsSync10, readFileSync as readFileSync2, statSync } from "fs";
|
|
24491
|
+
import { join as join6 } from "path";
|
|
24492
|
+
function walletState(configPath, dbPath) {
|
|
24471
24493
|
const hasConfig = existsSync10(configPath);
|
|
24472
24494
|
const hasDb = existsSync10(dbPath);
|
|
24473
24495
|
if (hasConfig)
|
|
@@ -24483,9 +24505,36 @@ function filesEqual(left, right) {
|
|
|
24483
24505
|
return false;
|
|
24484
24506
|
return readFileSync2(left).equals(readFileSync2(right));
|
|
24485
24507
|
}
|
|
24486
|
-
function
|
|
24487
|
-
|
|
24508
|
+
function classifyWalletMigration(targetDir, sourceDir) {
|
|
24509
|
+
const targetConfig = join6(targetDir, "config.json");
|
|
24510
|
+
const targetDb = join6(targetDir, "coco.db");
|
|
24511
|
+
const sourceConfig = join6(sourceDir, "config.json");
|
|
24512
|
+
const sourceDb = join6(sourceDir, "coco.db");
|
|
24513
|
+
const sourceWal = `${sourceDb}-wal`;
|
|
24514
|
+
const sourceShm = `${sourceDb}-shm`;
|
|
24515
|
+
const targetState = walletState(targetConfig, targetDb);
|
|
24516
|
+
const sourceState = walletState(sourceConfig, sourceDb);
|
|
24517
|
+
if (targetState === "initialized" && sourceState === "initialized") {
|
|
24518
|
+
const configsMatch = filesEqual(targetConfig, sourceConfig);
|
|
24519
|
+
const databasesMatch = !existsSync10(targetDb) && !existsSync10(sourceDb) ? true : filesEqual(targetDb, sourceDb);
|
|
24520
|
+
if (configsMatch && databasesMatch)
|
|
24521
|
+
return { kind: "already-current" };
|
|
24522
|
+
return { kind: "conflict" };
|
|
24523
|
+
}
|
|
24524
|
+
if (targetState === "initialized")
|
|
24525
|
+
return { kind: "already-current" };
|
|
24526
|
+
if (!existsSync10(sourceDb) && (existsSync10(sourceWal) || existsSync10(sourceShm))) {
|
|
24527
|
+
return { kind: "orphaned-sidecars" };
|
|
24528
|
+
}
|
|
24529
|
+
if (targetState === "database-only" || sourceState === "database-only") {
|
|
24530
|
+
return { kind: "database-only", targetState, sourceState };
|
|
24531
|
+
}
|
|
24532
|
+
if (sourceState === "absent")
|
|
24533
|
+
return { kind: "fresh" };
|
|
24534
|
+
return { kind: "migrate" };
|
|
24488
24535
|
}
|
|
24536
|
+
|
|
24537
|
+
// src/daemon/wallet/diagnostics.ts
|
|
24489
24538
|
var SUMMARY_QUERIES = {
|
|
24490
24539
|
proofs: "SELECT mintUrl, state, COUNT(*) count, COALESCE(SUM(amount), 0) amount FROM coco_cashu_proofs GROUP BY mintUrl, state ORDER BY mintUrl, state",
|
|
24491
24540
|
counters: "SELECT mintUrl, keysetId, counter FROM coco_cashu_counters ORDER BY mintUrl, keysetId",
|
|
@@ -24507,8 +24556,339 @@ function verifyDatabase(database, label) {
|
|
|
24507
24556
|
}
|
|
24508
24557
|
return summary;
|
|
24509
24558
|
}
|
|
24559
|
+
function mnemonicFingerprint(mnemonic) {
|
|
24560
|
+
const normalized = mnemonic.trim().split(/\s+/).join(" ");
|
|
24561
|
+
const hex3 = createHash("sha256").update(normalized).digest("hex");
|
|
24562
|
+
return hex3.slice(0, 16).match(/.{1,4}/g)?.join("-") ?? hex3.slice(0, 16);
|
|
24563
|
+
}
|
|
24564
|
+
function summarizeDbReadonly(dbPath) {
|
|
24565
|
+
let database;
|
|
24566
|
+
try {
|
|
24567
|
+
database = new Database(dbPath, { readonly: true });
|
|
24568
|
+
const raw = verifyDatabase(database, "Wallet database");
|
|
24569
|
+
const proofs = raw.proofs ?? [];
|
|
24570
|
+
const mints = raw.mints ?? [];
|
|
24571
|
+
const amountByState = {};
|
|
24572
|
+
let totalProofs = 0;
|
|
24573
|
+
let totalAmount = 0;
|
|
24574
|
+
for (const row of proofs) {
|
|
24575
|
+
const count = typeof row.count === "number" ? row.count : 0;
|
|
24576
|
+
const amount = typeof row.amount === "number" ? row.amount : 0;
|
|
24577
|
+
totalProofs += count;
|
|
24578
|
+
totalAmount += amount;
|
|
24579
|
+
const state = row.state || "unknown";
|
|
24580
|
+
amountByState[state] = (amountByState[state] ?? 0) + amount;
|
|
24581
|
+
}
|
|
24582
|
+
return {
|
|
24583
|
+
summary: {
|
|
24584
|
+
proofs,
|
|
24585
|
+
mints,
|
|
24586
|
+
totalProofs,
|
|
24587
|
+
totalAmount,
|
|
24588
|
+
amountByState,
|
|
24589
|
+
distinctMints: new Set(proofs.map((row) => row.mintUrl).filter(Boolean)).size
|
|
24590
|
+
}
|
|
24591
|
+
};
|
|
24592
|
+
} catch (error) {
|
|
24593
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
24594
|
+
} finally {
|
|
24595
|
+
database?.close();
|
|
24596
|
+
}
|
|
24597
|
+
}
|
|
24598
|
+
function summarizeConfig(configPath) {
|
|
24599
|
+
const result = { exists: false, path: configPath };
|
|
24600
|
+
try {
|
|
24601
|
+
if (!existsSync11(configPath))
|
|
24602
|
+
return result;
|
|
24603
|
+
result.exists = true;
|
|
24604
|
+
result.mtimeMs = statSync2(configPath).mtimeMs;
|
|
24605
|
+
const parsed = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
24606
|
+
result.encrypted = parsed.encrypted === true;
|
|
24607
|
+
if (typeof parsed.createdAt === "string")
|
|
24608
|
+
result.createdAt = parsed.createdAt;
|
|
24609
|
+
if (typeof parsed.version === "number")
|
|
24610
|
+
result.version = parsed.version;
|
|
24611
|
+
if (typeof parsed.defaultMintUrl === "string") {
|
|
24612
|
+
result.defaultMintUrl = parsed.defaultMintUrl;
|
|
24613
|
+
}
|
|
24614
|
+
if (typeof parsed.mnemonic === "string") {
|
|
24615
|
+
result.hasMnemonic = true;
|
|
24616
|
+
if (!result.encrypted)
|
|
24617
|
+
result.fingerprint = mnemonicFingerprint(parsed.mnemonic);
|
|
24618
|
+
}
|
|
24619
|
+
} catch (error) {
|
|
24620
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
24621
|
+
}
|
|
24622
|
+
return result;
|
|
24623
|
+
}
|
|
24624
|
+
function summarizeDb(dbPath) {
|
|
24625
|
+
const result = { exists: false, path: dbPath };
|
|
24626
|
+
try {
|
|
24627
|
+
if (!existsSync11(dbPath))
|
|
24628
|
+
return result;
|
|
24629
|
+
result.exists = true;
|
|
24630
|
+
const stats = statSync2(dbPath);
|
|
24631
|
+
result.mtimeMs = stats.mtimeMs;
|
|
24632
|
+
result.sizeBytes = stats.size;
|
|
24633
|
+
const { summary, error } = summarizeDbReadonly(dbPath);
|
|
24634
|
+
if (summary)
|
|
24635
|
+
result.summary = summary;
|
|
24636
|
+
if (error)
|
|
24637
|
+
result.error = error;
|
|
24638
|
+
} catch (error) {
|
|
24639
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
24640
|
+
}
|
|
24641
|
+
return result;
|
|
24642
|
+
}
|
|
24643
|
+
function summarizeWalletDirectory(dir, role) {
|
|
24644
|
+
return {
|
|
24645
|
+
dir,
|
|
24646
|
+
role,
|
|
24647
|
+
config: summarizeConfig(join7(dir, "config.json")),
|
|
24648
|
+
db: summarizeDb(join7(dir, "coco.db"))
|
|
24649
|
+
};
|
|
24650
|
+
}
|
|
24651
|
+
|
|
24652
|
+
class WalletMigrationConflictError extends Error {
|
|
24653
|
+
target;
|
|
24654
|
+
source;
|
|
24655
|
+
constructor(target, source) {
|
|
24656
|
+
super(renderWalletConflict(target, source));
|
|
24657
|
+
this.name = "WalletMigrationConflictError";
|
|
24658
|
+
this.target = target;
|
|
24659
|
+
this.source = source;
|
|
24660
|
+
}
|
|
24661
|
+
}
|
|
24662
|
+
function timeAgo(ms) {
|
|
24663
|
+
if (ms === undefined || !Number.isFinite(ms))
|
|
24664
|
+
return;
|
|
24665
|
+
const elapsed = Date.now() - ms;
|
|
24666
|
+
if (elapsed < 0)
|
|
24667
|
+
return;
|
|
24668
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
24669
|
+
if (seconds < 5)
|
|
24670
|
+
return "just now";
|
|
24671
|
+
if (seconds < 60)
|
|
24672
|
+
return `${seconds}s ago`;
|
|
24673
|
+
const minutes = Math.floor(seconds / 60);
|
|
24674
|
+
if (minutes < 60)
|
|
24675
|
+
return `${minutes}m ago`;
|
|
24676
|
+
const hours = Math.floor(minutes / 60);
|
|
24677
|
+
if (hours < 48)
|
|
24678
|
+
return `${hours}h ago`;
|
|
24679
|
+
const days = Math.floor(hours / 24);
|
|
24680
|
+
if (days < 60)
|
|
24681
|
+
return `${days}d ago`;
|
|
24682
|
+
const months = Math.floor(days / 30);
|
|
24683
|
+
if (months < 24)
|
|
24684
|
+
return `${months}mo ago`;
|
|
24685
|
+
return `${Math.floor(days / 365)}y ago`;
|
|
24686
|
+
}
|
|
24687
|
+
function modifiedDescription(config) {
|
|
24688
|
+
if (config.createdAt)
|
|
24689
|
+
return `created ${config.createdAt}`;
|
|
24690
|
+
const ago = timeAgo(config.mtimeMs);
|
|
24691
|
+
if (ago)
|
|
24692
|
+
return `modified ${ago}`;
|
|
24693
|
+
return "modified time unknown";
|
|
24694
|
+
}
|
|
24695
|
+
function roleLabel(role) {
|
|
24696
|
+
return role === "canonical" ? "current routstrd wallet" : "legacy cocod wallet";
|
|
24697
|
+
}
|
|
24698
|
+
function configLine(config) {
|
|
24699
|
+
if (!config.exists)
|
|
24700
|
+
return "config.json absent";
|
|
24701
|
+
if (config.error)
|
|
24702
|
+
return `config.json present (unreadable: ${config.error})`;
|
|
24703
|
+
const parts = ["config.json present"];
|
|
24704
|
+
if (config.fingerprint)
|
|
24705
|
+
parts.push(`fingerprint ${config.fingerprint}`);
|
|
24706
|
+
else if (config.encrypted)
|
|
24707
|
+
parts.push("encrypted (mnemonic not readable)");
|
|
24708
|
+
else
|
|
24709
|
+
parts.push("no mnemonic found");
|
|
24710
|
+
parts.push(modifiedDescription(config));
|
|
24711
|
+
if (config.defaultMintUrl)
|
|
24712
|
+
parts.push(`default mint ${config.defaultMintUrl}`);
|
|
24713
|
+
return parts.join(" \xB7 ");
|
|
24714
|
+
}
|
|
24715
|
+
function formatNumber(value) {
|
|
24716
|
+
return value.toLocaleString("en-US");
|
|
24717
|
+
}
|
|
24718
|
+
function amountByStateLine(summary) {
|
|
24719
|
+
const parts = Object.entries(summary.amountByState).filter(([, amount]) => amount > 0);
|
|
24720
|
+
if (parts.length === 0)
|
|
24721
|
+
return "0 sats";
|
|
24722
|
+
return parts.map(([state, amount]) => `${state}: ${formatNumber(amount)} sats`).join(", ");
|
|
24723
|
+
}
|
|
24724
|
+
function dbLine(db) {
|
|
24725
|
+
if (!db.exists)
|
|
24726
|
+
return "coco.db absent";
|
|
24727
|
+
if (db.error && !db.summary)
|
|
24728
|
+
return `coco.db present (unreadable: ${db.error})`;
|
|
24729
|
+
const parts = ["coco.db present"];
|
|
24730
|
+
if (db.summary) {
|
|
24731
|
+
const { totalProofs, mints, distinctMints } = db.summary;
|
|
24732
|
+
const mintCount = mints.length > 0 ? mints.length : distinctMints;
|
|
24733
|
+
parts.push(`${formatNumber(totalProofs)} proof${totalProofs === 1 ? "" : "s"}`);
|
|
24734
|
+
parts.push(amountByStateLine(db.summary));
|
|
24735
|
+
parts.push(`${mintCount} mint${mintCount === 1 ? "" : "s"}`);
|
|
24736
|
+
}
|
|
24737
|
+
return parts.join(" \xB7 ");
|
|
24738
|
+
}
|
|
24739
|
+
function block(diag, letter) {
|
|
24740
|
+
return [
|
|
24741
|
+
` ${letter}) ${diag.dir} (${roleLabel(diag.role)})`,
|
|
24742
|
+
` ${configLine(diag.config)}`,
|
|
24743
|
+
` ${dbLine(diag.db)}`
|
|
24744
|
+
].join(`
|
|
24745
|
+
`);
|
|
24746
|
+
}
|
|
24747
|
+
function moveAsideCommands(target, source) {
|
|
24748
|
+
return [
|
|
24749
|
+
" \u2022 If B is your wallet (keep the legacy wallet):",
|
|
24750
|
+
` mv "${target.dir}" "${target.dir}.old"`,
|
|
24751
|
+
"",
|
|
24752
|
+
" \u2022 If A is your wallet (keep the normal routstrd wallet):",
|
|
24753
|
+
` mv "${source.dir}" "${source.dir}.old"`
|
|
24754
|
+
].join(`
|
|
24755
|
+
`);
|
|
24756
|
+
}
|
|
24757
|
+
function renderResolutionSteps(target, source) {
|
|
24758
|
+
return [
|
|
24759
|
+
"To resolve, decide which wallet is yours, move the other one aside, and run",
|
|
24760
|
+
"'routstrd onboard' (or 'routstrd start') again. Stop the daemon first:",
|
|
24761
|
+
"",
|
|
24762
|
+
" routstrd stop",
|
|
24763
|
+
"",
|
|
24764
|
+
"Then:",
|
|
24765
|
+
"",
|
|
24766
|
+
moveAsideCommands(target, source)
|
|
24767
|
+
].join(`
|
|
24768
|
+
`);
|
|
24769
|
+
}
|
|
24770
|
+
function renderWalletConflict(target, source) {
|
|
24771
|
+
return [
|
|
24772
|
+
"Cannot migrate wallet: two different wallets were found, and routstrd will",
|
|
24773
|
+
"not choose a mnemonic or merge databases automatically \u2014 the wrong choice",
|
|
24774
|
+
"could lose access to your funds.",
|
|
24775
|
+
"",
|
|
24776
|
+
block(target, "A"),
|
|
24777
|
+
block(source, "B"),
|
|
24778
|
+
"",
|
|
24779
|
+
"No files were changed.",
|
|
24780
|
+
"",
|
|
24781
|
+
renderResolutionSteps(target, source),
|
|
24782
|
+
"",
|
|
24783
|
+
"For a full comparison and safe cleanup, run: routstrd wallet doctor"
|
|
24784
|
+
].join(`
|
|
24785
|
+
`);
|
|
24786
|
+
}
|
|
24787
|
+
function conflictVerdict(target, source) {
|
|
24788
|
+
const unreadable = !!target.config.error || !!source.config.error || !!target.db.error || !!source.db.error;
|
|
24789
|
+
if (unreadable) {
|
|
24790
|
+
return {
|
|
24791
|
+
text: "Verdict: both wallets exist but one or both could not be fully read; startup will refuse to migrate. See the details above.",
|
|
24792
|
+
showResolution: true,
|
|
24793
|
+
conflict: true
|
|
24794
|
+
};
|
|
24795
|
+
}
|
|
24796
|
+
if (target.config.fingerprint && source.config.fingerprint) {
|
|
24797
|
+
if (target.config.fingerprint === source.config.fingerprint) {
|
|
24798
|
+
return {
|
|
24799
|
+
text: "Verdict: both wallets share the same mnemonic but the files differ, so startup still refuses. Keep the wallet with your funds and move the other aside.",
|
|
24800
|
+
showResolution: true,
|
|
24801
|
+
conflict: true
|
|
24802
|
+
};
|
|
24803
|
+
}
|
|
24804
|
+
return {
|
|
24805
|
+
text: "Verdict: the two wallets have DIFFERENT mnemonics. Startup will refuse to migrate.",
|
|
24806
|
+
showResolution: true,
|
|
24807
|
+
conflict: true
|
|
24808
|
+
};
|
|
24809
|
+
}
|
|
24810
|
+
return {
|
|
24811
|
+
text: "Verdict: both wallets exist but their mnemonics cannot be compared (encrypted or missing). Startup will refuse to migrate unless the files are identical.",
|
|
24812
|
+
showResolution: true,
|
|
24813
|
+
conflict: true
|
|
24814
|
+
};
|
|
24815
|
+
}
|
|
24816
|
+
function verdictFromClassification(target, source, classification) {
|
|
24817
|
+
switch (classification.kind) {
|
|
24818
|
+
case "fresh":
|
|
24819
|
+
return {
|
|
24820
|
+
text: "Verdict: no wallet found in either location (fresh install).",
|
|
24821
|
+
showResolution: false,
|
|
24822
|
+
conflict: false
|
|
24823
|
+
};
|
|
24824
|
+
case "already-current":
|
|
24825
|
+
return {
|
|
24826
|
+
text: "Verdict: the current routstrd wallet is authoritative; no migration needed.",
|
|
24827
|
+
showResolution: false,
|
|
24828
|
+
conflict: false
|
|
24829
|
+
};
|
|
24830
|
+
case "migrate":
|
|
24831
|
+
return {
|
|
24832
|
+
text: "Verdict: only the legacy cocod wallet exists; it will be migrated on next startup.",
|
|
24833
|
+
showResolution: false,
|
|
24834
|
+
conflict: false
|
|
24835
|
+
};
|
|
24836
|
+
case "conflict":
|
|
24837
|
+
return conflictVerdict(target, source);
|
|
24838
|
+
case "database-only":
|
|
24839
|
+
return {
|
|
24840
|
+
text: "Verdict: a wallet is incomplete (coco.db without config.json). Startup will refuse to migrate. See the details above.",
|
|
24841
|
+
showResolution: false,
|
|
24842
|
+
conflict: true
|
|
24843
|
+
};
|
|
24844
|
+
case "orphaned-sidecars":
|
|
24845
|
+
return {
|
|
24846
|
+
text: "Verdict: the legacy wallet has SQLite sidecar files without coco.db. Startup will refuse to migrate. Restore the matching database first.",
|
|
24847
|
+
showResolution: false,
|
|
24848
|
+
conflict: true
|
|
24849
|
+
};
|
|
24850
|
+
}
|
|
24851
|
+
}
|
|
24852
|
+
function diagnoseWallets(target, source) {
|
|
24853
|
+
let classification;
|
|
24854
|
+
try {
|
|
24855
|
+
classification = classifyWalletMigration(target.dir, source.dir);
|
|
24856
|
+
} catch {
|
|
24857
|
+
if (target.config.error || source.config.error || target.db.error || source.db.error) {
|
|
24858
|
+
return conflictVerdict(target, source);
|
|
24859
|
+
}
|
|
24860
|
+
return {
|
|
24861
|
+
text: "Verdict: the wallets could not be fully compared (read error); startup will refuse to migrate. See the details above.",
|
|
24862
|
+
showResolution: true,
|
|
24863
|
+
conflict: true
|
|
24864
|
+
};
|
|
24865
|
+
}
|
|
24866
|
+
return verdictFromClassification(target, source, classification);
|
|
24867
|
+
}
|
|
24868
|
+
function renderWalletDoctor(target, source) {
|
|
24869
|
+
const verdict = diagnoseWallets(target, source);
|
|
24870
|
+
const lines = [
|
|
24871
|
+
"Routstr wallet diagnostic",
|
|
24872
|
+
"========================",
|
|
24873
|
+
"",
|
|
24874
|
+
block(target, "A"),
|
|
24875
|
+
block(source, "B"),
|
|
24876
|
+
"",
|
|
24877
|
+
verdict.text
|
|
24878
|
+
];
|
|
24879
|
+
if (verdict.showResolution) {
|
|
24880
|
+
lines.push("", renderResolutionSteps(target, source));
|
|
24881
|
+
}
|
|
24882
|
+
return lines.join(`
|
|
24883
|
+
`);
|
|
24884
|
+
}
|
|
24885
|
+
|
|
24886
|
+
// src/daemon/wallet/migration.ts
|
|
24887
|
+
function sqlString(value) {
|
|
24888
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
24889
|
+
}
|
|
24510
24890
|
function snapshotDatabase(sourcePath, destinationPath) {
|
|
24511
|
-
const source = new
|
|
24891
|
+
const source = new Database2(sourcePath);
|
|
24512
24892
|
let sourceSummary;
|
|
24513
24893
|
try {
|
|
24514
24894
|
sourceSummary = verifyDatabase(source, "Legacy wallet database");
|
|
@@ -24516,7 +24896,7 @@ function snapshotDatabase(sourcePath, destinationPath) {
|
|
|
24516
24896
|
} finally {
|
|
24517
24897
|
source.close();
|
|
24518
24898
|
}
|
|
24519
|
-
const destination = new
|
|
24899
|
+
const destination = new Database2(destinationPath, { readonly: true });
|
|
24520
24900
|
try {
|
|
24521
24901
|
const destinationSummary = verifyDatabase(destination, "Staged wallet database");
|
|
24522
24902
|
if (JSON.stringify(sourceSummary) !== JSON.stringify(destinationSummary)) {
|
|
@@ -24529,48 +24909,42 @@ function snapshotDatabase(sourcePath, destinationPath) {
|
|
|
24529
24909
|
async function migrateLegacyWallet(options = {}) {
|
|
24530
24910
|
const targetDir = options.walletDir || walletDir();
|
|
24531
24911
|
const sourceDir = options.legacyDir || legacyCocodDir();
|
|
24532
|
-
const
|
|
24533
|
-
const
|
|
24534
|
-
const sourceConfig = join6(sourceDir, "config.json");
|
|
24535
|
-
const sourceDb = join6(sourceDir, "coco.db");
|
|
24912
|
+
const sourceConfig = join8(sourceDir, "config.json");
|
|
24913
|
+
const sourceDb = join8(sourceDir, "coco.db");
|
|
24536
24914
|
const sourceWal = `${sourceDb}-wal`;
|
|
24537
24915
|
const sourceShm = `${sourceDb}-shm`;
|
|
24538
|
-
const
|
|
24539
|
-
|
|
24540
|
-
|
|
24541
|
-
|
|
24542
|
-
|
|
24543
|
-
if (configsMatch && databasesMatch)
|
|
24916
|
+
const classification = classifyWalletMigration(targetDir, sourceDir);
|
|
24917
|
+
switch (classification.kind) {
|
|
24918
|
+
case "fresh":
|
|
24919
|
+
return { status: "fresh" };
|
|
24920
|
+
case "already-current":
|
|
24544
24921
|
return { status: "already-current" };
|
|
24545
|
-
|
|
24546
|
-
|
|
24547
|
-
|
|
24548
|
-
|
|
24549
|
-
|
|
24550
|
-
|
|
24551
|
-
|
|
24552
|
-
|
|
24553
|
-
throw new Error(`Cannot migrate wallet: ${targetDir} is ${targetState} and ${sourceDir} is ${sourceState}. ` + "A coco.db without config.json cannot be opened; restore the matching config first.");
|
|
24922
|
+
case "migrate":
|
|
24923
|
+
break;
|
|
24924
|
+
case "conflict":
|
|
24925
|
+
throw new WalletMigrationConflictError(summarizeWalletDirectory(targetDir, "canonical"), summarizeWalletDirectory(sourceDir, "legacy"));
|
|
24926
|
+
case "orphaned-sidecars":
|
|
24927
|
+
throw new Error(`Cannot migrate wallet: ${sourceDir} contains SQLite sidecar files without coco.db. ` + "Restore the matching main database before migration.");
|
|
24928
|
+
case "database-only":
|
|
24929
|
+
throw new Error(`Cannot migrate wallet: ${targetDir} is ${classification.targetState} and ${sourceDir} is ${classification.sourceState}. ` + "A coco.db without config.json cannot be opened; restore the matching config first.");
|
|
24554
24930
|
}
|
|
24555
|
-
if (sourceState === "absent")
|
|
24556
|
-
return { status: "fresh" };
|
|
24557
24931
|
await options.assertLegacyStopped?.();
|
|
24558
24932
|
const releaseLegacyLock = await options.acquireLegacyLock?.();
|
|
24559
|
-
const stagingDir =
|
|
24933
|
+
const stagingDir = join8(dirname7(targetDir), `.${targetDir.split(/[\\/]/).at(-1) || "wallet"}.staging-${process.pid}-${Date.now()}`);
|
|
24560
24934
|
try {
|
|
24561
24935
|
await options.assertLegacyStopped?.();
|
|
24562
24936
|
mkdirSync8(dirname7(targetDir), { recursive: true, mode: 448 });
|
|
24563
24937
|
mkdirSync8(stagingDir, { mode: 448 });
|
|
24564
|
-
const stagedConfig =
|
|
24565
|
-
const stagedDb =
|
|
24938
|
+
const stagedConfig = join8(stagingDir, "config.json");
|
|
24939
|
+
const stagedDb = join8(stagingDir, "coco.db");
|
|
24566
24940
|
copyFileSync(sourceConfig, stagedConfig);
|
|
24567
24941
|
chmodSync(stagedConfig, 384);
|
|
24568
|
-
const hasSourceDb =
|
|
24942
|
+
const hasSourceDb = existsSync12(sourceDb);
|
|
24569
24943
|
if (hasSourceDb) {
|
|
24570
24944
|
snapshotDatabase(sourceDb, stagedDb);
|
|
24571
24945
|
chmodSync(stagedDb, 384);
|
|
24572
24946
|
}
|
|
24573
|
-
if (
|
|
24947
|
+
if (statSync3(stagedConfig).size !== statSync3(sourceConfig).size) {
|
|
24574
24948
|
throw new Error("Cannot migrate wallet: staged config failed size validation.");
|
|
24575
24949
|
}
|
|
24576
24950
|
renameSync2(stagingDir, targetDir);
|
|
@@ -24581,13 +24955,13 @@ async function migrateLegacyWallet(options = {}) {
|
|
|
24581
24955
|
releaseLegacyLock?.();
|
|
24582
24956
|
}
|
|
24583
24957
|
const cleanupWarnings = [];
|
|
24584
|
-
const sourceFiles = [sourceConfig, sourceDb, sourceWal, sourceShm].filter(
|
|
24958
|
+
const sourceFiles = [sourceConfig, sourceDb, sourceWal, sourceShm].filter(existsSync12);
|
|
24585
24959
|
if (sourceFiles.length > 0) {
|
|
24586
|
-
const archiveDir =
|
|
24960
|
+
const archiveDir = join8(sourceDir, `wallet-migrated-${Date.now()}`);
|
|
24587
24961
|
try {
|
|
24588
24962
|
mkdirSync8(archiveDir, { mode: 448 });
|
|
24589
24963
|
for (const path of sourceFiles)
|
|
24590
|
-
renameSync2(path,
|
|
24964
|
+
renameSync2(path, join8(archiveDir, basename(path)));
|
|
24591
24965
|
} catch (error) {
|
|
24592
24966
|
const message = error instanceof Error ? error.message : String(error);
|
|
24593
24967
|
cleanupWarnings.push(`Could not archive legacy wallet files: ${message}`);
|
|
@@ -26721,7 +27095,7 @@ zoo`.split(`
|
|
|
26721
27095
|
// package.json
|
|
26722
27096
|
var package_default = {
|
|
26723
27097
|
name: "routstrd",
|
|
26724
|
-
version: "0.4.
|
|
27098
|
+
version: "0.4.1",
|
|
26725
27099
|
module: "src/index.ts",
|
|
26726
27100
|
type: "module",
|
|
26727
27101
|
private: false,
|
|
@@ -26787,10 +27161,10 @@ Invoice:
|
|
|
26787
27161
|
${invoice}`);
|
|
26788
27162
|
}
|
|
26789
27163
|
function initializeWallet(walletDir2 = walletDir()) {
|
|
26790
|
-
const walletConfig =
|
|
27164
|
+
const walletConfig = join9(walletDir2, "config.json");
|
|
26791
27165
|
mkdirSync9(walletDir2, { recursive: true, mode: 448 });
|
|
26792
27166
|
chmodSync2(walletDir2, 448);
|
|
26793
|
-
if (
|
|
27167
|
+
if (existsSync13(walletConfig)) {
|
|
26794
27168
|
chmodSync2(walletConfig, 384);
|
|
26795
27169
|
console.log("Wallet already initialized.");
|
|
26796
27170
|
return;
|
|
@@ -26830,7 +27204,7 @@ Restarting routstrd daemon...`);
|
|
|
26830
27204
|
for (let i2 = 0;i2 < 100; i2++) {
|
|
26831
27205
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
26832
27206
|
const healthDown = !await isDaemonRunning();
|
|
26833
|
-
const pidFileReleased = !
|
|
27207
|
+
const pidFileReleased = !existsSync13(pidFilePath);
|
|
26834
27208
|
if (healthDown && pidFileReleased)
|
|
26835
27209
|
break;
|
|
26836
27210
|
}
|
|
@@ -26873,11 +27247,11 @@ async function requireLocalDaemon() {
|
|
|
26873
27247
|
}
|
|
26874
27248
|
async function initDaemon() {
|
|
26875
27249
|
console.log("Initializing routstrd...");
|
|
26876
|
-
if (!
|
|
27250
|
+
if (!existsSync13(CONFIG_DIR)) {
|
|
26877
27251
|
mkdirSync9(CONFIG_DIR, { recursive: true });
|
|
26878
27252
|
console.log(`Created config directory: ${CONFIG_DIR}`);
|
|
26879
27253
|
}
|
|
26880
|
-
if (!
|
|
27254
|
+
if (!existsSync13(CONFIG_FILE)) {
|
|
26881
27255
|
const config2 = {
|
|
26882
27256
|
...DEFAULT_CONFIG,
|
|
26883
27257
|
cocodPath: null
|
|
@@ -26900,26 +27274,18 @@ A new Nostr identity has been generated for authentication.`);
|
|
|
26900
27274
|
}
|
|
26901
27275
|
console.log(`Database will be stored at: ${DB_PATH}`);
|
|
26902
27276
|
await stopLegacyCocod();
|
|
26903
|
-
let migrationLockOwner;
|
|
26904
27277
|
const migration = await migrateLegacyWallet({
|
|
26905
27278
|
assertLegacyStopped: () => assertLegacyCocodNotRunning({
|
|
26906
|
-
socketPath: legacyCocodSocketPath()
|
|
26907
|
-
pidFilePath: legacyCocodPidPath(),
|
|
26908
|
-
ignorePid: migrationLockOwner
|
|
27279
|
+
socketPath: legacyCocodSocketPath()
|
|
26909
27280
|
}),
|
|
26910
27281
|
acquireLegacyLock: () => {
|
|
26911
27282
|
mkdirSync9(dirname8(legacyCocodPidPath()), {
|
|
26912
27283
|
recursive: true,
|
|
26913
27284
|
mode: 448
|
|
26914
27285
|
});
|
|
26915
|
-
|
|
27286
|
+
return claimLegacyCocodPidFile({
|
|
26916
27287
|
pidFilePath: legacyCocodPidPath()
|
|
26917
27288
|
});
|
|
26918
|
-
migrationLockOwner = process.pid;
|
|
26919
|
-
return () => {
|
|
26920
|
-
migrationLockOwner = undefined;
|
|
26921
|
-
release();
|
|
26922
|
-
};
|
|
26923
27289
|
}
|
|
26924
27290
|
});
|
|
26925
27291
|
if (migration.status === "migrated") {
|
|
@@ -26977,7 +27343,7 @@ program.command("update").description("Update routstrd to the latest version").a
|
|
|
26977
27343
|
All packages are already up to date \u2014 nothing to do.`);
|
|
26978
27344
|
}
|
|
26979
27345
|
});
|
|
26980
|
-
program.command("refund").description("Refund pending tokens and API keys to a specified mint").option("-m, --mint-url <mintUrl>", "Mint URL to refund to (defaults to
|
|
27346
|
+
program.command("refund").description("Refund pending tokens and API keys to a specified mint").option("-m, --mint-url <mintUrl>", "Mint URL to refund to (defaults to active wallet mint)").option("-y, --yes", "Skip confirmation prompt", false).option("--xcashu", "Refund xcashu tokens only (uses refundXcashuTokens)", false).action(async (options) => {
|
|
26981
27347
|
await ensureDaemonRunning();
|
|
26982
27348
|
let mintUrl = options.mintUrl;
|
|
26983
27349
|
if (!mintUrl) {
|
|
@@ -26986,12 +27352,12 @@ program.command("refund").description("Refund pending tokens and API keys to a s
|
|
|
26986
27352
|
console.log(balanceResult.error);
|
|
26987
27353
|
process.exit(1);
|
|
26988
27354
|
}
|
|
26989
|
-
const
|
|
26990
|
-
|
|
27355
|
+
const output = balanceResult.output;
|
|
27356
|
+
mintUrl = output?.activeMint ?? (output?.balances ? Object.keys(output.balances)[0] : undefined);
|
|
27357
|
+
if (!mintUrl) {
|
|
26991
27358
|
console.log("No mint URLs found in wallet balance");
|
|
26992
27359
|
process.exit(1);
|
|
26993
27360
|
}
|
|
26994
|
-
mintUrl = Object.keys(balances)[0];
|
|
26995
27361
|
console.log(`Using mint URL: ${mintUrl}`);
|
|
26996
27362
|
}
|
|
26997
27363
|
try {
|
|
@@ -27078,7 +27444,7 @@ program.command("remote [url]").description("Show the configured remote daemon,
|
|
|
27078
27444
|
process.exit(1);
|
|
27079
27445
|
}
|
|
27080
27446
|
}
|
|
27081
|
-
if (!
|
|
27447
|
+
if (!existsSync13(CONFIG_DIR)) {
|
|
27082
27448
|
mkdirSync9(CONFIG_DIR, { recursive: true });
|
|
27083
27449
|
}
|
|
27084
27450
|
const updates = { daemonUrl: url };
|
|
@@ -27133,17 +27499,30 @@ Use 'routstrd onboard' for first time using local mode or 'routstrd start' to st
|
|
|
27133
27499
|
});
|
|
27134
27500
|
program.command("onboard").description("Initialize routstrd (creates config directory and initializes wallet)").action(async () => {
|
|
27135
27501
|
await requireLocalDaemon();
|
|
27136
|
-
|
|
27502
|
+
try {
|
|
27503
|
+
await initDaemon();
|
|
27504
|
+
} catch (error) {
|
|
27505
|
+
if (error instanceof WalletMigrationConflictError) {
|
|
27506
|
+
console.error(error.message);
|
|
27507
|
+
process.exit(1);
|
|
27508
|
+
}
|
|
27509
|
+
throw error;
|
|
27510
|
+
}
|
|
27137
27511
|
});
|
|
27138
27512
|
program.command("start").description("Start the background daemon").option("--port <port>", "Port to listen on").option("--host <host>", "Bind address (default: 127.0.0.1)").option("-p, --provider <provider>", "Default provider to use").action(async (options) => {
|
|
27139
27513
|
await requireLocalDaemon();
|
|
27140
27514
|
const config = await loadConfig();
|
|
27141
27515
|
await stopLegacyCocod();
|
|
27142
|
-
|
|
27143
|
-
|
|
27144
|
-
|
|
27145
|
-
|
|
27146
|
-
|
|
27516
|
+
try {
|
|
27517
|
+
await startDaemon({
|
|
27518
|
+
port: options.port || String(config.port || 8008),
|
|
27519
|
+
host: options.host || config.host || undefined,
|
|
27520
|
+
provider: options.provider
|
|
27521
|
+
});
|
|
27522
|
+
} catch (error) {
|
|
27523
|
+
console.error(error instanceof Error ? error.message : error);
|
|
27524
|
+
process.exit(1);
|
|
27525
|
+
}
|
|
27147
27526
|
});
|
|
27148
27527
|
program.command("status").description("Check daemon and wallet status").action(async () => {
|
|
27149
27528
|
const running = await isDaemonRunning();
|
|
@@ -27169,7 +27548,7 @@ program.command("status").description("Check daemon and wallet status").action(a
|
|
|
27169
27548
|
}
|
|
27170
27549
|
}
|
|
27171
27550
|
});
|
|
27172
|
-
program.command("balance").description("Get wallet and API key balances").option("--api-keys", "List all stored API keys (baseUrl + key + balance)", false).option("--delete-api-keys <baseUrl>", "Delete the API key stored for the given provider base URL (refunds balance first)").option("--mint-url <url>", "Mint URL to refund the deleted API key balance to (defaults to
|
|
27551
|
+
program.command("balance").description("Get wallet and API key balances").option("--api-keys", "List all stored API keys (baseUrl + key + balance)", false).option("--delete-api-keys <baseUrl>", "Delete the API key stored for the given provider base URL (refunds balance first)").option("--mint-url <url>", "Mint URL to refund the deleted API key balance to (defaults to active wallet mint)").action(async (options) => {
|
|
27173
27552
|
await ensureDaemonRunning();
|
|
27174
27553
|
if (options.deleteApiKeys) {
|
|
27175
27554
|
const baseUrl = options.deleteApiKeys;
|
|
@@ -27714,6 +28093,13 @@ var walletCmd = program.command("wallet").description("Wallet operations");
|
|
|
27714
28093
|
walletCmd.command("status").description("Check wallet status").action(async () => {
|
|
27715
28094
|
await handleDaemonCommand("/wallet/status");
|
|
27716
28095
|
});
|
|
28096
|
+
walletCmd.command("doctor").description("Diagnose conflicting wallets (current routstrd wallet vs legacy cocod)").action(async () => {
|
|
28097
|
+
const target = summarizeWalletDirectory(walletDir(), "canonical");
|
|
28098
|
+
const source = summarizeWalletDirectory(legacyCocodDir(), "legacy");
|
|
28099
|
+
console.log(renderWalletDoctor(target, source));
|
|
28100
|
+
if (diagnoseWallets(target, source).conflict)
|
|
28101
|
+
process.exit(1);
|
|
28102
|
+
});
|
|
27717
28103
|
walletCmd.command("unlock <passphrase>").description("Unlock the wallet").action(async (passphrase) => {
|
|
27718
28104
|
await handleDaemonCommand("/wallet/unlock", {
|
|
27719
28105
|
method: "POST",
|
|
@@ -27723,6 +28109,68 @@ walletCmd.command("unlock <passphrase>").description("Unlock the wallet").action
|
|
|
27723
28109
|
walletCmd.command("balance").description("Get wallet balance").action(async () => {
|
|
27724
28110
|
await handleDaemonCommand("/wallet/balance");
|
|
27725
28111
|
});
|
|
28112
|
+
walletCmd.command("cleanup").description("Clear stuck pending/in-flight wallet operations").option("--mint-url <url>", "Only clean up operations for this mint URL").option("--min-age <hours>", "Minimum age for reclaiming sends/cancelling melts, in hours (default: 168, one week; expired mint quotes are always failed)", "168").option("--dry-run", "Report what would be cleaned without applying changes", false).option("-y, --yes", "Skip confirmation prompt", false).action(async (options) => {
|
|
28113
|
+
const minAgeHours = Number.parseFloat(options.minAge);
|
|
28114
|
+
if (!Number.isFinite(minAgeHours) || minAgeHours < 0) {
|
|
28115
|
+
console.error(`Invalid --min-age value: ${options.minAge}`);
|
|
28116
|
+
process.exit(1);
|
|
28117
|
+
}
|
|
28118
|
+
if (!options.dryRun && !options.yes) {
|
|
28119
|
+
const rl = __require("readline").createInterface({
|
|
28120
|
+
input: process.stdin,
|
|
28121
|
+
output: process.stdout
|
|
28122
|
+
});
|
|
28123
|
+
const answer = await new Promise((resolve) => {
|
|
28124
|
+
rl.question("This will fail expired mint quotes, reclaim old pending sends, and cancel prepared melts. Continue? [y/N] ", (value) => {
|
|
28125
|
+
rl.close();
|
|
28126
|
+
resolve(value.trim().toLowerCase());
|
|
28127
|
+
});
|
|
28128
|
+
});
|
|
28129
|
+
if (answer !== "y" && answer !== "yes") {
|
|
28130
|
+
console.log("Aborted.");
|
|
28131
|
+
return;
|
|
28132
|
+
}
|
|
28133
|
+
}
|
|
28134
|
+
try {
|
|
28135
|
+
await ensureDaemonRunning();
|
|
28136
|
+
const result = await callDaemon("/wallet/cleanup", {
|
|
28137
|
+
method: "POST",
|
|
28138
|
+
body: {
|
|
28139
|
+
mintUrl: options.mintUrl,
|
|
28140
|
+
minAgeMs: Math.round(minAgeHours * 60 * 60 * 1000),
|
|
28141
|
+
dryRun: options.dryRun === true
|
|
28142
|
+
}
|
|
28143
|
+
});
|
|
28144
|
+
if (result.error) {
|
|
28145
|
+
console.log(result.error);
|
|
28146
|
+
process.exit(1);
|
|
28147
|
+
}
|
|
28148
|
+
const output = result.output;
|
|
28149
|
+
if (output) {
|
|
28150
|
+
const prefix = output.dryRun ? "Would clean up:" : "Cleaned up:";
|
|
28151
|
+
console.log(prefix);
|
|
28152
|
+
console.log(` Expired mint quotes failed: ${output.failedMintQuotes ?? 0}`);
|
|
28153
|
+
console.log(` Pending sends reclaimed: ${output.reclaimedSends ?? 0}`);
|
|
28154
|
+
console.log(` Prepared melts cancelled: ${output.cancelledMelts ?? 0}`);
|
|
28155
|
+
console.log(` Skipped (still recent or already terminal): ${output.skipped ?? 0}`);
|
|
28156
|
+
if (output.errors && output.errors.length > 0) {
|
|
28157
|
+
console.log(`
|
|
28158
|
+
Errors:`);
|
|
28159
|
+
for (const e of output.errors) {
|
|
28160
|
+
console.log(` - ${e.operationId}: ${e.error}`);
|
|
28161
|
+
}
|
|
28162
|
+
}
|
|
28163
|
+
}
|
|
28164
|
+
} catch (error) {
|
|
28165
|
+
const message = error.message;
|
|
28166
|
+
if (message?.includes("fetch failed") || message?.includes("Connection refused")) {
|
|
28167
|
+
console.error("Daemon is not running");
|
|
28168
|
+
process.exit(1);
|
|
28169
|
+
}
|
|
28170
|
+
console.error(message);
|
|
28171
|
+
process.exit(1);
|
|
28172
|
+
}
|
|
28173
|
+
});
|
|
27726
28174
|
var walletReceiveCmd = walletCmd.command("receive").description("Wallet receive operations");
|
|
27727
28175
|
walletReceiveCmd.command("cashu <token>").description("Receive a Cashu token").action(async (token) => {
|
|
27728
28176
|
await handleDaemonCommand("/wallet/receive/cashu", {
|
|
@@ -27890,7 +28338,7 @@ serviceCmd.command("install").description("Install and start routstrd using PM2
|
|
|
27890
28338
|
const path = __require("path");
|
|
27891
28339
|
daemonPath = path.join(path.dirname(import.meta.url).replace("file://", ""), "daemon", "index.js");
|
|
27892
28340
|
}
|
|
27893
|
-
if (!
|
|
28341
|
+
if (!existsSync13(daemonPath)) {
|
|
27894
28342
|
console.error(`Could not find daemon at ${daemonPath}. Did you run 'bun run build'?`);
|
|
27895
28343
|
process.exit(1);
|
|
27896
28344
|
}
|
|
@@ -27938,7 +28386,7 @@ program.command("restart").description("Restart the background daemon").option("
|
|
|
27938
28386
|
for (let i2 = 0;i2 < 100; i2++) {
|
|
27939
28387
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
27940
28388
|
const healthDown = !await isDaemonRunning();
|
|
27941
|
-
const pidFileReleased = !
|
|
28389
|
+
const pidFileReleased = !existsSync13(pidFilePath);
|
|
27942
28390
|
if (healthDown && pidFileReleased) {
|
|
27943
28391
|
break;
|
|
27944
28392
|
}
|
|
@@ -28029,7 +28477,7 @@ function getLogFileForDate2(logsDir, date = new Date) {
|
|
|
28029
28477
|
return `${logsDir}/${year}-${month}-${day}.log`;
|
|
28030
28478
|
}
|
|
28031
28479
|
function readLastLines(file, lines) {
|
|
28032
|
-
const content =
|
|
28480
|
+
const content = readFileSync4(file, "utf8");
|
|
28033
28481
|
const allLines = content.replace(/\r\n/g, `
|
|
28034
28482
|
`).split(`
|
|
28035
28483
|
`);
|
|
@@ -28043,13 +28491,13 @@ async function followLogFile(file, lines) {
|
|
|
28043
28491
|
if (initial) {
|
|
28044
28492
|
console.log(initial);
|
|
28045
28493
|
}
|
|
28046
|
-
let position =
|
|
28494
|
+
let position = statSync4(file).size;
|
|
28047
28495
|
while (true) {
|
|
28048
28496
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
28049
|
-
if (!
|
|
28497
|
+
if (!existsSync13(file)) {
|
|
28050
28498
|
continue;
|
|
28051
28499
|
}
|
|
28052
|
-
const size =
|
|
28500
|
+
const size = statSync4(file).size;
|
|
28053
28501
|
if (size < position) {
|
|
28054
28502
|
position = 0;
|
|
28055
28503
|
}
|
|
@@ -28068,17 +28516,17 @@ program.command("logs").description("View daemon logs").option("-f, --follow", "
|
|
|
28068
28516
|
const yesterday = new Date;
|
|
28069
28517
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
28070
28518
|
const yesterdayFile = getLogFileForDate2(logsDir, yesterday);
|
|
28071
|
-
if (!
|
|
28519
|
+
if (!existsSync13(todayFile) && !existsSync13(yesterdayFile)) {
|
|
28072
28520
|
console.log("No log files found. Daemon may not have started yet.");
|
|
28073
28521
|
console.log(`Logs directory: ${logsDir}`);
|
|
28074
28522
|
process.exit(1);
|
|
28075
28523
|
}
|
|
28076
28524
|
const lines = parseInt(options.lines, 10);
|
|
28077
28525
|
const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => {
|
|
28078
|
-
return
|
|
28526
|
+
return existsSync13(file) && files.indexOf(file) === index;
|
|
28079
28527
|
});
|
|
28080
28528
|
if (options.follow) {
|
|
28081
|
-
if (
|
|
28529
|
+
if (existsSync13(todayFile)) {
|
|
28082
28530
|
await followLogFile(todayFile, lines);
|
|
28083
28531
|
} else {
|
|
28084
28532
|
console.log("No log file for today to follow.");
|