routstrd 0.4.0 → 0.4.2
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/bun.lock +316 -2
- package/dist/daemon/index.js +70789 -54681
- package/dist/index.js +729 -183
- package/package.json +2 -2
- package/src/cli.ts +258 -38
- package/src/daemon/config-store.ts +79 -4
- package/src/daemon/fatal-error.test.ts +104 -0
- package/src/daemon/fatal-error.ts +48 -0
- package/src/daemon/models.test.ts +81 -0
- package/src/daemon/models.ts +18 -2
- 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/integrations/pi.ts +17 -1
- package/src/start-daemon.ts +21 -1
- package/src/tui/usage/data.ts +13 -2
- package/src/tui/usage/render.ts +8 -0
- package/src/utils/config.ts +3 -1
- package/tests/daemon/config-store.perms.test.ts +55 -0
- package/tests/daemon/config-store.scenario.ts +113 -0
- 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";
|
|
@@ -21220,7 +21234,10 @@ var init_constants = __esm(() => {
|
|
|
21220
21234
|
// src/tui/usage/data.ts
|
|
21221
21235
|
async function fetchStatus() {
|
|
21222
21236
|
try {
|
|
21223
|
-
const result = await
|
|
21237
|
+
const [result, config] = await Promise.all([
|
|
21238
|
+
callDaemon("/status"),
|
|
21239
|
+
loadConfig()
|
|
21240
|
+
]);
|
|
21224
21241
|
if (result.error)
|
|
21225
21242
|
return null;
|
|
21226
21243
|
const output = result.output;
|
|
@@ -21228,6 +21245,7 @@ async function fetchStatus() {
|
|
|
21228
21245
|
daemon: output?.daemon || "unknown",
|
|
21229
21246
|
wallet: output?.wallet || "unknown",
|
|
21230
21247
|
mode: output?.mode || "apikeys",
|
|
21248
|
+
url: getDaemonBaseUrl(config),
|
|
21231
21249
|
error: output?.error
|
|
21232
21250
|
};
|
|
21233
21251
|
} catch {
|
|
@@ -21276,7 +21294,7 @@ function formatTime(timestamp) {
|
|
|
21276
21294
|
const p = (n) => String(n).padStart(2, "0");
|
|
21277
21295
|
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
21278
21296
|
}
|
|
21279
|
-
function
|
|
21297
|
+
function formatNumber2(n) {
|
|
21280
21298
|
if (n >= 1e6)
|
|
21281
21299
|
return (n / 1e6).toFixed(1) + "M";
|
|
21282
21300
|
if (n >= 1000)
|
|
@@ -21518,8 +21536,8 @@ function renderOverview(stats, balance, status, width) {
|
|
|
21518
21536
|
`${COLORS.bold}Avg Cost/Req:${COLORS.reset} ${formatCost(avgCost)} sats`
|
|
21519
21537
|
];
|
|
21520
21538
|
const rightBox = [
|
|
21521
|
-
`${COLORS.bold}Total Tokens:${COLORS.reset} ${
|
|
21522
|
-
`${COLORS.bold}Avg Tokens/Req:${COLORS.reset} ${
|
|
21539
|
+
`${COLORS.bold}Total Tokens:${COLORS.reset} ${formatNumber2(totals.totalTokens)}`,
|
|
21540
|
+
`${COLORS.bold}Avg Tokens/Req:${COLORS.reset} ${formatNumber2(Math.round(avgTokens))}`,
|
|
21523
21541
|
`${COLORS.bold}Prompt/Comp:${COLORS.reset} ${(totals.promptTokens / Math.max(1, totals.completionTokens)).toFixed(2)}x`
|
|
21524
21542
|
];
|
|
21525
21543
|
const halfWidth1 = Math.floor(width / 2);
|
|
@@ -21545,6 +21563,12 @@ function renderOverview(stats, balance, status, width) {
|
|
|
21545
21563
|
const walletColor = status.wallet === "connected" ? COLORS.green : COLORS.red;
|
|
21546
21564
|
const modeColor = COLORS.cyan;
|
|
21547
21565
|
statusLines.push(`${COLORS.bold}Daemon:${COLORS.reset} ${daemonColor}${status.daemon}${COLORS.reset}`);
|
|
21566
|
+
if (status.url) {
|
|
21567
|
+
const label = "Running on: ";
|
|
21568
|
+
const maxUrlLen = Math.max(8, halfWidth2 - 4 - label.length);
|
|
21569
|
+
const url = status.url.length > maxUrlLen ? `${status.url.slice(0, Math.max(0, maxUrlLen - 1))}\u2026` : status.url;
|
|
21570
|
+
statusLines.push(`${COLORS.bold}${label}${COLORS.reset} ${COLORS.cyan}${url}${COLORS.reset}`);
|
|
21571
|
+
}
|
|
21548
21572
|
statusLines.push(`${COLORS.bold}Wallet:${COLORS.reset} ${walletColor}${status.wallet}${COLORS.reset}`);
|
|
21549
21573
|
statusLines.push(`${COLORS.bold}Mode:${COLORS.reset} ${modeColor}${status.mode}${COLORS.reset}`);
|
|
21550
21574
|
if (status.error) {
|
|
@@ -21667,11 +21691,11 @@ function renderToday(stats, width) {
|
|
|
21667
21691
|
`${COLORS.bold}Date:${COLORS.reset} ${todayStats.date}`,
|
|
21668
21692
|
`${COLORS.bold}Requests:${COLORS.reset} ${formatReqs(todayStats.requests)}`,
|
|
21669
21693
|
`${COLORS.bold}Cost:${COLORS.reset} ${COLORS.green}${formatCost(todayStats.satsCost)} sats${COLORS.reset}`,
|
|
21670
|
-
`${COLORS.bold}Tokens:${COLORS.reset} ${
|
|
21694
|
+
`${COLORS.bold}Tokens:${COLORS.reset} ${formatNumber2(todayStats.totalTokens)} (p: ${formatNumber2(todayStats.promptTokens)} + c: ${formatNumber2(todayStats.completionTokens)})`
|
|
21671
21695
|
];
|
|
21672
21696
|
let output = renderBox(summaryLines, width, "Today");
|
|
21673
21697
|
if (recentDays.length > 0) {
|
|
21674
|
-
const dayLines = recentDays.map((d) => `${d.date}: ${formatReqs(d.requests)} req, ${formatCost(d.satsCost)} sats, ${
|
|
21698
|
+
const dayLines = recentDays.map((d) => `${d.date}: ${formatReqs(d.requests)} req, ${formatCost(d.satsCost)} sats, ${formatNumber2(d.totalTokens)} tokens`);
|
|
21675
21699
|
output += `
|
|
21676
21700
|
` + renderBox(dayLines, width, "Recent Days");
|
|
21677
21701
|
}
|
|
@@ -21713,7 +21737,7 @@ function renderModels(stats, width) {
|
|
|
21713
21737
|
lines.push(`${color}${COLORS.bold}${model.modelId}${COLORS.reset}`);
|
|
21714
21738
|
lines.push(` ${COLORS.dim}Cost:${COLORS.reset} ${formatCost(model.satsCost)} sats (${pct}%)`);
|
|
21715
21739
|
lines.push(` ${COLORS.dim}Requests:${COLORS.reset} ${formatReqs(model.requests)}`);
|
|
21716
|
-
lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${
|
|
21740
|
+
lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${formatNumber2(model.totalTokens)}`);
|
|
21717
21741
|
lines.push(` ${COLORS.dim}Avg:${COLORS.reset} ${formatCost(model.satsCost / model.requests)} sats/req`);
|
|
21718
21742
|
lines.push(` ${renderBarChart(" ", model.satsCost, maxCost, width - 6, color, Number(pct), "model-detail")}`);
|
|
21719
21743
|
lines.push("");
|
|
@@ -21731,7 +21755,7 @@ function renderProviders(stats, width) {
|
|
|
21731
21755
|
lines.push(`${COLORS.cyan}${COLORS.bold}${shortUrl}${COLORS.reset}`);
|
|
21732
21756
|
lines.push(` ${COLORS.dim}Requests:${COLORS.reset} ${formatReqs(provider.requests)}`);
|
|
21733
21757
|
lines.push(` ${COLORS.dim}Cost:${COLORS.reset} ${formatCost(provider.satsCost)} sats`);
|
|
21734
|
-
lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${
|
|
21758
|
+
lines.push(` ${COLORS.dim}Tokens:${COLORS.reset} ${formatNumber2(provider.totalTokens)}`);
|
|
21735
21759
|
lines.push("");
|
|
21736
21760
|
}
|
|
21737
21761
|
return renderBox(lines, width, "Provider Breakdown");
|
|
@@ -21740,9 +21764,9 @@ function renderTokens(stats, width) {
|
|
|
21740
21764
|
const totals = stats.summary.totals;
|
|
21741
21765
|
const modelStats = stats.summary.models;
|
|
21742
21766
|
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} ${
|
|
21767
|
+
`${COLORS.bold}Total Prompt Tokens:${COLORS.reset} ${formatNumber2(totals.promptTokens)}`,
|
|
21768
|
+
`${COLORS.bold}Total Completion Tokens:${COLORS.reset} ${formatNumber2(totals.completionTokens)}`,
|
|
21769
|
+
`${COLORS.bold}Total Tokens:${COLORS.reset} ${formatNumber2(totals.totalTokens)}`,
|
|
21746
21770
|
`${COLORS.bold}Prompt/Completion Ratio:${COLORS.reset} ${(totals.promptTokens / Math.max(1, totals.completionTokens)).toFixed(2)}x`,
|
|
21747
21771
|
`${COLORS.bold}Avg Tokens/Request:${COLORS.reset} ${(totals.totalTokens / Math.max(1, stats.totalEntries)).toFixed(0)}`
|
|
21748
21772
|
];
|
|
@@ -21751,7 +21775,7 @@ function renderTokens(stats, width) {
|
|
|
21751
21775
|
const tokenLines = modelStats.slice(0, 6).map((m) => {
|
|
21752
21776
|
const color = MODEL_COLORS[m.modelId] || MODEL_COLORS.default;
|
|
21753
21777
|
const promptPct = m.totalTokens > 0 ? (m.promptTokens / m.totalTokens * 100).toFixed(0) : "0";
|
|
21754
|
-
return `${color}${m.modelId.padEnd(15)}${COLORS.reset} ${
|
|
21778
|
+
return `${color}${m.modelId.padEnd(15)}${COLORS.reset} ${formatNumber2(m.totalTokens).padStart(8)} tokens (${promptPct}% prompt)`;
|
|
21755
21779
|
});
|
|
21756
21780
|
output += `
|
|
21757
21781
|
` + renderBox(tokenLines, width, "Tokens by Model");
|
|
@@ -21787,7 +21811,7 @@ function renderClients(stats, width) {
|
|
|
21787
21811
|
const dClient = client.client.slice(0, col1 - 1).padEnd(col1);
|
|
21788
21812
|
const dReqs = formatReqs(client.requests).padEnd(col2);
|
|
21789
21813
|
const dCost = `${formatCost(client.satsCost)} sats (${pct}%)`.padEnd(col3);
|
|
21790
|
-
const dTok =
|
|
21814
|
+
const dTok = formatNumber2(client.totalTokens).padEnd(col4);
|
|
21791
21815
|
const dAvg = `${avgCostFormatted} sats/req`;
|
|
21792
21816
|
lines.push(`${color}${COLORS.bold}${dClient}${COLORS.reset}` + `${dReqs}` + `${COLORS.green}${dCost}${COLORS.reset}` + `${COLORS.dim}${dTok}${dAvg}${COLORS.reset}`);
|
|
21793
21817
|
lines.push(` ${renderBarChart("", client.satsCost, maxCost, width - 6, color, Number(pct), "client-detail")}`);
|
|
@@ -21802,7 +21826,7 @@ function renderClients(stats, width) {
|
|
|
21802
21826
|
continue;
|
|
21803
21827
|
clientModelLines.push(`${COLORS.bold}${topClient.client}${COLORS.reset} (${formatReqs(topClient.requests)} reqs, ${formatCost(topClient.satsCost)} sats)`);
|
|
21804
21828
|
for (const m of topClient.topModels) {
|
|
21805
|
-
clientModelLines.push(` ${MODEL_COLORS[m.modelId] || MODEL_COLORS.default}${m.modelId.padEnd(18)}${COLORS.reset} ${
|
|
21829
|
+
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
21830
|
}
|
|
21807
21831
|
clientModelLines.push("");
|
|
21808
21832
|
}
|
|
@@ -21838,7 +21862,7 @@ function renderNpubs(stats, clients, width) {
|
|
|
21838
21862
|
const dNpub = shortNpub.padEnd(col1);
|
|
21839
21863
|
const dReqs = formatReqs(npub.requests).padEnd(col2);
|
|
21840
21864
|
const dCost = `${formatCost(npub.satsCost)} sats (${pct}%)`.padEnd(col3);
|
|
21841
|
-
const dTok =
|
|
21865
|
+
const dTok = formatNumber2(npub.totalTokens).padEnd(col4);
|
|
21842
21866
|
const dAvg = `${avgCostFormatted} sats/req`;
|
|
21843
21867
|
lines.push(`${COLORS.magenta}${COLORS.bold}${dNpub}${COLORS.reset}` + `${dReqs}` + `${COLORS.green}${dCost}${COLORS.reset}` + `${COLORS.dim}${dTok}${dAvg}${COLORS.reset}`);
|
|
21844
21868
|
lines.push(` ${COLORS.dim}${npub.npub}${COLORS.reset}`);
|
|
@@ -21854,7 +21878,7 @@ function renderNpubs(stats, clients, width) {
|
|
|
21854
21878
|
continue;
|
|
21855
21879
|
npubModelLines.push(`${COLORS.bold}${truncateNpub(topNpub.npub)}${COLORS.reset} (${formatReqs(topNpub.requests)} reqs, ${formatCost(topNpub.satsCost)} sats)`);
|
|
21856
21880
|
for (const m of topNpub.topModels) {
|
|
21857
|
-
npubModelLines.push(` ${MODEL_COLORS[m.modelId] || MODEL_COLORS.default}${m.modelId.padEnd(18)}${COLORS.reset} ${
|
|
21881
|
+
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
21882
|
}
|
|
21859
21883
|
npubModelLines.push("");
|
|
21860
21884
|
}
|
|
@@ -21890,7 +21914,7 @@ function renderRecent(stats, width) {
|
|
|
21890
21914
|
entry.cacheReadInputTokens || 0,
|
|
21891
21915
|
entry.cacheCreationInputTokens || 0,
|
|
21892
21916
|
entry.completionTokens
|
|
21893
|
-
].map(
|
|
21917
|
+
].map(formatNumber2).join("/");
|
|
21894
21918
|
const totalSats = typeof entry.totalMsats === "number" ? entry.totalMsats / 1000 : entry.satsCost;
|
|
21895
21919
|
const cost = [
|
|
21896
21920
|
formatCost(msatsToSats(entry.inputMsats)),
|
|
@@ -22262,6 +22286,53 @@ var {
|
|
|
22262
22286
|
|
|
22263
22287
|
// src/cli.ts
|
|
22264
22288
|
init_start_daemon();
|
|
22289
|
+
|
|
22290
|
+
// src/daemon/config-store.ts
|
|
22291
|
+
init_config();
|
|
22292
|
+
init_logger();
|
|
22293
|
+
import {
|
|
22294
|
+
chmodSync,
|
|
22295
|
+
existsSync as existsSync4,
|
|
22296
|
+
mkdirSync as mkdirSync2,
|
|
22297
|
+
readFileSync,
|
|
22298
|
+
renameSync,
|
|
22299
|
+
unlinkSync,
|
|
22300
|
+
writeFileSync
|
|
22301
|
+
} from "fs";
|
|
22302
|
+
var REQUESTS_DIR = `${CONFIG_DIR}/requests`;
|
|
22303
|
+
function chmodIgnoreErrors(path, mode) {
|
|
22304
|
+
try {
|
|
22305
|
+
chmodSync(path, mode);
|
|
22306
|
+
} catch {}
|
|
22307
|
+
}
|
|
22308
|
+
function ensureDirsSync() {
|
|
22309
|
+
try {
|
|
22310
|
+
mkdirSync2(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
22311
|
+
} catch {}
|
|
22312
|
+
chmodSync(CONFIG_DIR, 448);
|
|
22313
|
+
try {
|
|
22314
|
+
mkdirSync2(REQUESTS_DIR, { recursive: true, mode: 448 });
|
|
22315
|
+
} catch {}
|
|
22316
|
+
chmodSync(REQUESTS_DIR, 448);
|
|
22317
|
+
}
|
|
22318
|
+
function saveDaemonConfig(config) {
|
|
22319
|
+
ensureDirsSync();
|
|
22320
|
+
const temporaryFile = `${CONFIG_FILE}.${process.pid}.tmp`;
|
|
22321
|
+
try {
|
|
22322
|
+
writeFileSync(temporaryFile, JSON.stringify(config, null, 2), {
|
|
22323
|
+
mode: 384
|
|
22324
|
+
});
|
|
22325
|
+
renameSync(temporaryFile, CONFIG_FILE);
|
|
22326
|
+
} catch (error) {
|
|
22327
|
+
try {
|
|
22328
|
+
unlinkSync(temporaryFile);
|
|
22329
|
+
} catch {}
|
|
22330
|
+
throw error;
|
|
22331
|
+
}
|
|
22332
|
+
chmodIgnoreErrors(CONFIG_FILE, 384);
|
|
22333
|
+
}
|
|
22334
|
+
|
|
22335
|
+
// src/cli.ts
|
|
22265
22336
|
init_daemon_client();
|
|
22266
22337
|
|
|
22267
22338
|
// src/utils/clients.ts
|
|
@@ -22273,7 +22344,7 @@ import { join as join3 } from "path";
|
|
|
22273
22344
|
|
|
22274
22345
|
// src/integrations/opencode.ts
|
|
22275
22346
|
init_daemon_client();
|
|
22276
|
-
import { existsSync as
|
|
22347
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
22277
22348
|
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
22278
22349
|
import { dirname as dirname2 } from "path";
|
|
22279
22350
|
var OPENCODE_SMALL_MODEL = "routstr/minimax-m2.5";
|
|
@@ -22285,7 +22356,7 @@ Installing routstr models in opencode.json...`);
|
|
|
22285
22356
|
const baseUrl = getDaemonBaseUrl(config);
|
|
22286
22357
|
let opencodeConfig;
|
|
22287
22358
|
try {
|
|
22288
|
-
if (
|
|
22359
|
+
if (existsSync5(configPath)) {
|
|
22289
22360
|
const content = await readFile2(configPath, "utf-8");
|
|
22290
22361
|
opencodeConfig = JSON.parse(content);
|
|
22291
22362
|
} else {
|
|
@@ -22299,7 +22370,7 @@ Installing routstr models in opencode.json...`);
|
|
|
22299
22370
|
opencodeConfig.provider = {};
|
|
22300
22371
|
}
|
|
22301
22372
|
try {
|
|
22302
|
-
|
|
22373
|
+
mkdirSync3(dirname2(configPath), { recursive: true });
|
|
22303
22374
|
const data = await callDaemon("/models");
|
|
22304
22375
|
const models = data.output?.models || [];
|
|
22305
22376
|
if (models.length === 0) {
|
|
@@ -22330,7 +22401,7 @@ Installing routstr models in opencode.json...`);
|
|
|
22330
22401
|
|
|
22331
22402
|
// src/integrations/pi.ts
|
|
22332
22403
|
init_daemon_client();
|
|
22333
|
-
import { existsSync as
|
|
22404
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
22334
22405
|
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
22335
22406
|
import { dirname as dirname3 } from "path";
|
|
22336
22407
|
async function installPiIntegration(config, apiKey, integrationConfig) {
|
|
@@ -22341,7 +22412,7 @@ Installing routstr models in pi models.json...`);
|
|
|
22341
22412
|
const baseUrl = `${getDaemonBaseUrl(config)}/v1`;
|
|
22342
22413
|
let piConfig = {};
|
|
22343
22414
|
try {
|
|
22344
|
-
if (
|
|
22415
|
+
if (existsSync6(configPath)) {
|
|
22345
22416
|
const content = await readFile3(configPath, "utf-8");
|
|
22346
22417
|
piConfig = JSON.parse(content);
|
|
22347
22418
|
}
|
|
@@ -22353,21 +22424,25 @@ Installing routstr models in pi models.json...`);
|
|
|
22353
22424
|
piConfig.providers = {};
|
|
22354
22425
|
}
|
|
22355
22426
|
try {
|
|
22356
|
-
|
|
22427
|
+
mkdirSync4(dirname3(configPath), { recursive: true });
|
|
22357
22428
|
const data = await callDaemon("/models");
|
|
22358
22429
|
const models = data.output?.models || [];
|
|
22359
22430
|
if (models.length === 0) {
|
|
22360
22431
|
console.log("No models found from routstr daemon.");
|
|
22361
22432
|
return;
|
|
22362
22433
|
}
|
|
22434
|
+
const existingModels = new Map((piConfig.providers["routstr"]?.models ?? []).map((m) => [m.id, m]));
|
|
22363
22435
|
const providerModels = models.map((model) => {
|
|
22364
|
-
const
|
|
22436
|
+
const previous = existingModels.get(model.id);
|
|
22437
|
+
const entry = previous ? { ...previous } : { id: model.id };
|
|
22438
|
+
entry.id = model.id;
|
|
22365
22439
|
if (model.context_length !== undefined && model.context_length > 0) {
|
|
22366
22440
|
entry.contextWindow = model.context_length;
|
|
22367
22441
|
}
|
|
22368
22442
|
return entry;
|
|
22369
22443
|
});
|
|
22370
22444
|
piConfig.providers["routstr"] = {
|
|
22445
|
+
...piConfig.providers["routstr"] ?? {},
|
|
22371
22446
|
baseUrl,
|
|
22372
22447
|
api: "openai-completions",
|
|
22373
22448
|
apiKey,
|
|
@@ -22382,7 +22457,7 @@ Installing routstr models in pi models.json...`);
|
|
|
22382
22457
|
|
|
22383
22458
|
// src/integrations/openclaw.ts
|
|
22384
22459
|
init_daemon_client();
|
|
22385
|
-
import { existsSync as
|
|
22460
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
22386
22461
|
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
22387
22462
|
import { dirname as dirname4 } from "path";
|
|
22388
22463
|
var OPENCLAW_PROVIDER_ID = "routstr";
|
|
@@ -22396,7 +22471,7 @@ Installing routstr models in openclaw.json...`);
|
|
|
22396
22471
|
const baseUrl = getDaemonBaseUrl(config);
|
|
22397
22472
|
let openclawConfig = {};
|
|
22398
22473
|
try {
|
|
22399
|
-
if (
|
|
22474
|
+
if (existsSync7(configPath)) {
|
|
22400
22475
|
const content = await readFile4(configPath, "utf-8");
|
|
22401
22476
|
openclawConfig = JSON.parse(content);
|
|
22402
22477
|
}
|
|
@@ -22417,7 +22492,7 @@ Installing routstr models in openclaw.json...`);
|
|
|
22417
22492
|
openclawConfig.agents.defaults = {};
|
|
22418
22493
|
}
|
|
22419
22494
|
try {
|
|
22420
|
-
|
|
22495
|
+
mkdirSync5(dirname4(configPath), { recursive: true });
|
|
22421
22496
|
const data = await callDaemon("/models");
|
|
22422
22497
|
const models = data.output?.models || [];
|
|
22423
22498
|
if (models.length === 0) {
|
|
@@ -22458,7 +22533,7 @@ Installing routstr models in openclaw.json...`);
|
|
|
22458
22533
|
|
|
22459
22534
|
// src/integrations/claudecode.ts
|
|
22460
22535
|
init_daemon_client();
|
|
22461
|
-
import { existsSync as
|
|
22536
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
|
|
22462
22537
|
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
22463
22538
|
import { dirname as dirname5 } from "path";
|
|
22464
22539
|
async function installClaudeCodeIntegration(config, apiKey, integrationConfig) {
|
|
@@ -22469,7 +22544,7 @@ Installing routstr configuration in ${configPath}...`);
|
|
|
22469
22544
|
const baseUrl = getDaemonBaseUrl(config);
|
|
22470
22545
|
let settings = {};
|
|
22471
22546
|
try {
|
|
22472
|
-
if (
|
|
22547
|
+
if (existsSync8(configPath)) {
|
|
22473
22548
|
const content = await readFile5(configPath, "utf-8");
|
|
22474
22549
|
settings = JSON.parse(content);
|
|
22475
22550
|
}
|
|
@@ -22506,7 +22581,7 @@ Installing routstr configuration in ${configPath}...`);
|
|
|
22506
22581
|
console.error("Failed to fetch models for Claude Code integration:", error);
|
|
22507
22582
|
}
|
|
22508
22583
|
try {
|
|
22509
|
-
|
|
22584
|
+
mkdirSync6(dirname5(configPath), { recursive: true });
|
|
22510
22585
|
await writeFile5(configPath, JSON.stringify(settings, null, 2));
|
|
22511
22586
|
console.log(`Successfully updated ${configPath} with routstr settings.`);
|
|
22512
22587
|
} catch (error) {
|
|
@@ -22515,7 +22590,7 @@ Installing routstr configuration in ${configPath}...`);
|
|
|
22515
22590
|
}
|
|
22516
22591
|
|
|
22517
22592
|
// src/integrations/hermes.ts
|
|
22518
|
-
import { existsSync as
|
|
22593
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7 } from "fs";
|
|
22519
22594
|
import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
22520
22595
|
import { dirname as dirname6 } from "path";
|
|
22521
22596
|
|
|
@@ -22638,7 +22713,7 @@ Installing routstr configuration in ${configPath}...`);
|
|
|
22638
22713
|
}
|
|
22639
22714
|
let content = "";
|
|
22640
22715
|
try {
|
|
22641
|
-
if (
|
|
22716
|
+
if (existsSync9(configPath)) {
|
|
22642
22717
|
content = await readFile6(configPath, "utf-8");
|
|
22643
22718
|
}
|
|
22644
22719
|
} catch (error) {
|
|
@@ -22661,7 +22736,7 @@ Installing routstr configuration in ${configPath}...`);
|
|
|
22661
22736
|
console.log(`${configPath} already contains current routstr settings.`);
|
|
22662
22737
|
return;
|
|
22663
22738
|
}
|
|
22664
|
-
|
|
22739
|
+
mkdirSync7(dirname6(configPath), { recursive: true });
|
|
22665
22740
|
await writeFile6(configPath, newContent);
|
|
22666
22741
|
console.log(`Successfully updated ${configPath} with routstr settings.`);
|
|
22667
22742
|
} catch (error) {
|
|
@@ -22898,9 +22973,9 @@ async function addClientAction(options) {
|
|
|
22898
22973
|
// src/cli.ts
|
|
22899
22974
|
init_config();
|
|
22900
22975
|
init_logger();
|
|
22901
|
-
import { chmodSync as
|
|
22976
|
+
import { chmodSync as chmodSync3, existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync5, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
22902
22977
|
import { execSync } from "child_process";
|
|
22903
|
-
import { dirname as dirname8, join as
|
|
22978
|
+
import { dirname as dirname8, join as join9 } from "path";
|
|
22904
22979
|
|
|
22905
22980
|
// src/integrations/index.ts
|
|
22906
22981
|
init_logger();
|
|
@@ -24180,13 +24255,13 @@ k("Cashu_P2BK_v1");
|
|
|
24180
24255
|
// src/daemon/wallet/coco-client.ts
|
|
24181
24256
|
import {
|
|
24182
24257
|
closeSync as closeSync2,
|
|
24183
|
-
existsSync as
|
|
24184
|
-
mkdirSync as
|
|
24258
|
+
existsSync as existsSync10,
|
|
24259
|
+
mkdirSync as mkdirSync8,
|
|
24185
24260
|
openSync as openSync2,
|
|
24186
|
-
readFileSync,
|
|
24187
|
-
renameSync,
|
|
24188
|
-
unlinkSync,
|
|
24189
|
-
writeFileSync
|
|
24261
|
+
readFileSync as readFileSync2,
|
|
24262
|
+
renameSync as renameSync2,
|
|
24263
|
+
unlinkSync as unlinkSync2,
|
|
24264
|
+
writeFileSync as writeFileSync2
|
|
24190
24265
|
} from "fs";
|
|
24191
24266
|
|
|
24192
24267
|
// node_modules/@scure/bip39/index.js
|
|
@@ -24255,6 +24330,11 @@ var STALE_SOCKET_ERROR_CODES = new Set([
|
|
|
24255
24330
|
"ENOENT",
|
|
24256
24331
|
"FailedToOpenSocket"
|
|
24257
24332
|
]);
|
|
24333
|
+
var RECOVERY_STALL_MARKERS = new Map([
|
|
24334
|
+
["SendOperationService\x00Could not reach mint for recovery, will retry later", "Send recovery: mint unreachable"],
|
|
24335
|
+
["ReceiveOperationService\x00Could not reach mint for receive recovery, will retry later", "Receive recovery: mint unreachable"],
|
|
24336
|
+
["MintOperationService\x00Failed to reconcile stale pending mint operation", "Mint recovery: pending operation check failed"]
|
|
24337
|
+
]);
|
|
24258
24338
|
var SAFE_COCO_LOG_FIELDS = new Set([
|
|
24259
24339
|
"module",
|
|
24260
24340
|
"mintUrl",
|
|
@@ -24271,13 +24351,22 @@ var SAFE_COCO_LOG_FIELDS = new Set([
|
|
|
24271
24351
|
"rollingBackOperations",
|
|
24272
24352
|
"orphanedReservations"
|
|
24273
24353
|
]);
|
|
24354
|
+
function isZombieProcess(pid, readFile7 = (path) => readFileSync2(path, "utf-8")) {
|
|
24355
|
+
try {
|
|
24356
|
+
const stat2 = readFile7(`/proc/${pid}/stat`);
|
|
24357
|
+
const commandEnd = stat2.lastIndexOf(")");
|
|
24358
|
+
return commandEnd >= 0 && stat2.charAt(commandEnd + 2) === "Z";
|
|
24359
|
+
} catch {
|
|
24360
|
+
return false;
|
|
24361
|
+
}
|
|
24362
|
+
}
|
|
24274
24363
|
function defaultIsProcessRunning(pid) {
|
|
24275
24364
|
try {
|
|
24276
24365
|
process.kill(pid, 0);
|
|
24277
|
-
return true;
|
|
24278
24366
|
} catch (error) {
|
|
24279
24367
|
return error.code === "EPERM";
|
|
24280
24368
|
}
|
|
24369
|
+
return !isZombieProcess(pid);
|
|
24281
24370
|
}
|
|
24282
24371
|
function hasErrorCode(error, codes) {
|
|
24283
24372
|
let current = error;
|
|
@@ -24294,24 +24383,7 @@ function hasErrorCode(error, codes) {
|
|
|
24294
24383
|
}
|
|
24295
24384
|
async function assertLegacyCocodNotRunning(options = {}) {
|
|
24296
24385
|
const socketPath = options.socketPath || legacyCocodSocketPath();
|
|
24297
|
-
const
|
|
24298
|
-
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
|
-
}
|
|
24386
|
+
const pathExists = options.pathExists || existsSync10;
|
|
24315
24387
|
if (!pathExists(socketPath))
|
|
24316
24388
|
return;
|
|
24317
24389
|
const fetchImpl = options.fetchImpl || fetch;
|
|
@@ -24324,12 +24396,8 @@ async function assertLegacyCocodNotRunning(options = {}) {
|
|
|
24324
24396
|
await response.body?.cancel();
|
|
24325
24397
|
} catch (error) {
|
|
24326
24398
|
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 });
|
|
24399
|
+
logger.debug(`Ignoring stale legacy cocod socket at ${socketPath}`);
|
|
24400
|
+
return;
|
|
24333
24401
|
}
|
|
24334
24402
|
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
24403
|
}
|
|
@@ -24338,8 +24406,8 @@ async function assertLegacyCocodNotRunning(options = {}) {
|
|
|
24338
24406
|
async function stopLegacyCocod(options = {}) {
|
|
24339
24407
|
const socketPath = options.socketPath || legacyCocodSocketPath();
|
|
24340
24408
|
const pidFilePath = options.pidFilePath || legacyCocodPidPath();
|
|
24341
|
-
const pathExists = options.pathExists ||
|
|
24342
|
-
const readFile7 = options.readFile || ((path) =>
|
|
24409
|
+
const pathExists = options.pathExists || existsSync10;
|
|
24410
|
+
const readFile7 = options.readFile || ((path) => readFileSync2(path, "utf-8"));
|
|
24343
24411
|
const isProcessRunning2 = options.isProcessRunning || defaultIsProcessRunning;
|
|
24344
24412
|
const fetchImpl = options.fetchImpl || fetch;
|
|
24345
24413
|
const killProcess = options.killProcess || ((pid2, signal) => process.kill(pid2, signal));
|
|
@@ -24393,17 +24461,19 @@ async function stopLegacyCocod(options = {}) {
|
|
|
24393
24461
|
function claimLegacyCocodPidFile(options = {}) {
|
|
24394
24462
|
return claimPidFile({
|
|
24395
24463
|
...options,
|
|
24396
|
-
pidFilePath: options.pidFilePath || legacyCocodPidPath()
|
|
24464
|
+
pidFilePath: options.pidFilePath || legacyCocodPidPath(),
|
|
24465
|
+
label: options.label || "legacy cocod exclusion lock"
|
|
24397
24466
|
});
|
|
24398
24467
|
}
|
|
24399
24468
|
function claimPidFile(options) {
|
|
24400
24469
|
const pidFilePath = options.pidFilePath;
|
|
24401
24470
|
const pid = options.pid ?? process.pid;
|
|
24471
|
+
const label = options.label || "wallet process lock";
|
|
24402
24472
|
const openExclusive = options.openExclusive || ((path) => openSync2(path, "wx", 384));
|
|
24403
|
-
const writePid = options.writePid || ((fd2, ownerPid) =>
|
|
24473
|
+
const writePid = options.writePid || ((fd2, ownerPid) => writeFileSync2(fd2, String(ownerPid)));
|
|
24404
24474
|
const closeFile = options.closeFile || closeSync2;
|
|
24405
|
-
const readFile7 = options.readFile || ((path) =>
|
|
24406
|
-
const removeFile = options.removeFile ||
|
|
24475
|
+
const readFile7 = options.readFile || ((path) => readFileSync2(path, "utf-8"));
|
|
24476
|
+
const removeFile = options.removeFile || unlinkSync2;
|
|
24407
24477
|
const isProcessRunning2 = options.isProcessRunning || defaultIsProcessRunning;
|
|
24408
24478
|
let fd;
|
|
24409
24479
|
try {
|
|
@@ -24415,16 +24485,17 @@ function claimPidFile(options) {
|
|
|
24415
24485
|
try {
|
|
24416
24486
|
stalePid = Number.parseInt(readFile7(pidFilePath).trim(), 10);
|
|
24417
24487
|
} catch {
|
|
24418
|
-
throw new Error(`Cannot claim the
|
|
24488
|
+
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
24489
|
}
|
|
24420
24490
|
if (!Number.isInteger(stalePid) || stalePid <= 0 || isProcessRunning2(stalePid)) {
|
|
24421
|
-
|
|
24491
|
+
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.";
|
|
24492
|
+
throw new Error(`Cannot claim the ${label} at ${pidFilePath}: ${ownerMessage}`, { cause: error });
|
|
24422
24493
|
}
|
|
24423
24494
|
try {
|
|
24424
24495
|
removeFile(pidFilePath);
|
|
24425
24496
|
fd = openExclusive(pidFilePath);
|
|
24426
24497
|
} catch (retryError) {
|
|
24427
|
-
throw new Error(`Cannot claim the
|
|
24498
|
+
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
24499
|
}
|
|
24429
24500
|
}
|
|
24430
24501
|
try {
|
|
@@ -24438,38 +24509,50 @@ function claimPidFile(options) {
|
|
|
24438
24509
|
closeFile(fd);
|
|
24439
24510
|
}
|
|
24440
24511
|
let released = false;
|
|
24441
|
-
|
|
24512
|
+
const release = () => {
|
|
24442
24513
|
if (released)
|
|
24443
24514
|
return;
|
|
24444
24515
|
released = true;
|
|
24516
|
+
process.removeListener("exit", release);
|
|
24445
24517
|
try {
|
|
24446
24518
|
if (readFile7(pidFilePath).trim() === String(pid)) {
|
|
24447
24519
|
removeFile(pidFilePath);
|
|
24448
24520
|
}
|
|
24449
24521
|
} catch (error) {
|
|
24450
24522
|
if (error.code !== "ENOENT") {
|
|
24451
|
-
logger.warn(`Failed to release
|
|
24523
|
+
logger.warn(`Failed to release ${label} at ${pidFilePath}:`, error);
|
|
24452
24524
|
}
|
|
24453
24525
|
}
|
|
24454
24526
|
};
|
|
24527
|
+
process.once("exit", release);
|
|
24528
|
+
return release;
|
|
24455
24529
|
}
|
|
24456
24530
|
|
|
24457
24531
|
// src/daemon/wallet/migration.ts
|
|
24458
24532
|
import {
|
|
24459
|
-
chmodSync,
|
|
24533
|
+
chmodSync as chmodSync2,
|
|
24460
24534
|
copyFileSync,
|
|
24461
|
-
existsSync as
|
|
24462
|
-
mkdirSync as
|
|
24463
|
-
|
|
24464
|
-
renameSync as renameSync2,
|
|
24535
|
+
existsSync as existsSync13,
|
|
24536
|
+
mkdirSync as mkdirSync9,
|
|
24537
|
+
renameSync as renameSync3,
|
|
24465
24538
|
rmSync,
|
|
24466
|
-
statSync
|
|
24539
|
+
statSync as statSync3
|
|
24467
24540
|
} from "fs";
|
|
24541
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
24542
|
+
import { basename, dirname as dirname7, join as join8 } from "path";
|
|
24543
|
+
|
|
24544
|
+
// src/daemon/wallet/diagnostics.ts
|
|
24545
|
+
import { createHash } from "crypto";
|
|
24546
|
+
import { existsSync as existsSync12, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
24468
24547
|
import { Database } from "bun:sqlite";
|
|
24469
|
-
import {
|
|
24470
|
-
|
|
24471
|
-
|
|
24472
|
-
|
|
24548
|
+
import { join as join7 } from "path";
|
|
24549
|
+
|
|
24550
|
+
// src/daemon/wallet/wallet-state.ts
|
|
24551
|
+
import { existsSync as existsSync11, readFileSync as readFileSync3, statSync } from "fs";
|
|
24552
|
+
import { join as join6 } from "path";
|
|
24553
|
+
function walletState(configPath, dbPath) {
|
|
24554
|
+
const hasConfig = existsSync11(configPath);
|
|
24555
|
+
const hasDb = existsSync11(dbPath);
|
|
24473
24556
|
if (hasConfig)
|
|
24474
24557
|
return "initialized";
|
|
24475
24558
|
if (hasDb)
|
|
@@ -24477,15 +24560,42 @@ function state(configPath, dbPath) {
|
|
|
24477
24560
|
return "absent";
|
|
24478
24561
|
}
|
|
24479
24562
|
function filesEqual(left, right) {
|
|
24480
|
-
if (!
|
|
24563
|
+
if (!existsSync11(left) || !existsSync11(right))
|
|
24481
24564
|
return false;
|
|
24482
24565
|
if (statSync(left).size !== statSync(right).size)
|
|
24483
24566
|
return false;
|
|
24484
|
-
return
|
|
24567
|
+
return readFileSync3(left).equals(readFileSync3(right));
|
|
24485
24568
|
}
|
|
24486
|
-
function
|
|
24487
|
-
|
|
24569
|
+
function classifyWalletMigration(targetDir, sourceDir) {
|
|
24570
|
+
const targetConfig = join6(targetDir, "config.json");
|
|
24571
|
+
const targetDb = join6(targetDir, "coco.db");
|
|
24572
|
+
const sourceConfig = join6(sourceDir, "config.json");
|
|
24573
|
+
const sourceDb = join6(sourceDir, "coco.db");
|
|
24574
|
+
const sourceWal = `${sourceDb}-wal`;
|
|
24575
|
+
const sourceShm = `${sourceDb}-shm`;
|
|
24576
|
+
const targetState = walletState(targetConfig, targetDb);
|
|
24577
|
+
const sourceState = walletState(sourceConfig, sourceDb);
|
|
24578
|
+
if (targetState === "initialized" && sourceState === "initialized") {
|
|
24579
|
+
const configsMatch = filesEqual(targetConfig, sourceConfig);
|
|
24580
|
+
const databasesMatch = !existsSync11(targetDb) && !existsSync11(sourceDb) ? true : filesEqual(targetDb, sourceDb);
|
|
24581
|
+
if (configsMatch && databasesMatch)
|
|
24582
|
+
return { kind: "already-current" };
|
|
24583
|
+
return { kind: "conflict" };
|
|
24584
|
+
}
|
|
24585
|
+
if (targetState === "initialized")
|
|
24586
|
+
return { kind: "already-current" };
|
|
24587
|
+
if (!existsSync11(sourceDb) && (existsSync11(sourceWal) || existsSync11(sourceShm))) {
|
|
24588
|
+
return { kind: "orphaned-sidecars" };
|
|
24589
|
+
}
|
|
24590
|
+
if (targetState === "database-only" || sourceState === "database-only") {
|
|
24591
|
+
return { kind: "database-only", targetState, sourceState };
|
|
24592
|
+
}
|
|
24593
|
+
if (sourceState === "absent")
|
|
24594
|
+
return { kind: "fresh" };
|
|
24595
|
+
return { kind: "migrate" };
|
|
24488
24596
|
}
|
|
24597
|
+
|
|
24598
|
+
// src/daemon/wallet/diagnostics.ts
|
|
24489
24599
|
var SUMMARY_QUERIES = {
|
|
24490
24600
|
proofs: "SELECT mintUrl, state, COUNT(*) count, COALESCE(SUM(amount), 0) amount FROM coco_cashu_proofs GROUP BY mintUrl, state ORDER BY mintUrl, state",
|
|
24491
24601
|
counters: "SELECT mintUrl, keysetId, counter FROM coco_cashu_counters ORDER BY mintUrl, keysetId",
|
|
@@ -24507,8 +24617,339 @@ function verifyDatabase(database, label) {
|
|
|
24507
24617
|
}
|
|
24508
24618
|
return summary;
|
|
24509
24619
|
}
|
|
24620
|
+
function mnemonicFingerprint(mnemonic) {
|
|
24621
|
+
const normalized = mnemonic.trim().split(/\s+/).join(" ");
|
|
24622
|
+
const hex3 = createHash("sha256").update(normalized).digest("hex");
|
|
24623
|
+
return hex3.slice(0, 16).match(/.{1,4}/g)?.join("-") ?? hex3.slice(0, 16);
|
|
24624
|
+
}
|
|
24625
|
+
function summarizeDbReadonly(dbPath) {
|
|
24626
|
+
let database;
|
|
24627
|
+
try {
|
|
24628
|
+
database = new Database(dbPath, { readonly: true });
|
|
24629
|
+
const raw = verifyDatabase(database, "Wallet database");
|
|
24630
|
+
const proofs = raw.proofs ?? [];
|
|
24631
|
+
const mints = raw.mints ?? [];
|
|
24632
|
+
const amountByState = {};
|
|
24633
|
+
let totalProofs = 0;
|
|
24634
|
+
let totalAmount = 0;
|
|
24635
|
+
for (const row of proofs) {
|
|
24636
|
+
const count = typeof row.count === "number" ? row.count : 0;
|
|
24637
|
+
const amount = typeof row.amount === "number" ? row.amount : 0;
|
|
24638
|
+
totalProofs += count;
|
|
24639
|
+
totalAmount += amount;
|
|
24640
|
+
const state = row.state || "unknown";
|
|
24641
|
+
amountByState[state] = (amountByState[state] ?? 0) + amount;
|
|
24642
|
+
}
|
|
24643
|
+
return {
|
|
24644
|
+
summary: {
|
|
24645
|
+
proofs,
|
|
24646
|
+
mints,
|
|
24647
|
+
totalProofs,
|
|
24648
|
+
totalAmount,
|
|
24649
|
+
amountByState,
|
|
24650
|
+
distinctMints: new Set(proofs.map((row) => row.mintUrl).filter(Boolean)).size
|
|
24651
|
+
}
|
|
24652
|
+
};
|
|
24653
|
+
} catch (error) {
|
|
24654
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
24655
|
+
} finally {
|
|
24656
|
+
database?.close();
|
|
24657
|
+
}
|
|
24658
|
+
}
|
|
24659
|
+
function summarizeConfig(configPath) {
|
|
24660
|
+
const result = { exists: false, path: configPath };
|
|
24661
|
+
try {
|
|
24662
|
+
if (!existsSync12(configPath))
|
|
24663
|
+
return result;
|
|
24664
|
+
result.exists = true;
|
|
24665
|
+
result.mtimeMs = statSync2(configPath).mtimeMs;
|
|
24666
|
+
const parsed = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
24667
|
+
result.encrypted = parsed.encrypted === true;
|
|
24668
|
+
if (typeof parsed.createdAt === "string")
|
|
24669
|
+
result.createdAt = parsed.createdAt;
|
|
24670
|
+
if (typeof parsed.version === "number")
|
|
24671
|
+
result.version = parsed.version;
|
|
24672
|
+
if (typeof parsed.defaultMintUrl === "string") {
|
|
24673
|
+
result.defaultMintUrl = parsed.defaultMintUrl;
|
|
24674
|
+
}
|
|
24675
|
+
if (typeof parsed.mnemonic === "string") {
|
|
24676
|
+
result.hasMnemonic = true;
|
|
24677
|
+
if (!result.encrypted)
|
|
24678
|
+
result.fingerprint = mnemonicFingerprint(parsed.mnemonic);
|
|
24679
|
+
}
|
|
24680
|
+
} catch (error) {
|
|
24681
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
24682
|
+
}
|
|
24683
|
+
return result;
|
|
24684
|
+
}
|
|
24685
|
+
function summarizeDb(dbPath) {
|
|
24686
|
+
const result = { exists: false, path: dbPath };
|
|
24687
|
+
try {
|
|
24688
|
+
if (!existsSync12(dbPath))
|
|
24689
|
+
return result;
|
|
24690
|
+
result.exists = true;
|
|
24691
|
+
const stats = statSync2(dbPath);
|
|
24692
|
+
result.mtimeMs = stats.mtimeMs;
|
|
24693
|
+
result.sizeBytes = stats.size;
|
|
24694
|
+
const { summary, error } = summarizeDbReadonly(dbPath);
|
|
24695
|
+
if (summary)
|
|
24696
|
+
result.summary = summary;
|
|
24697
|
+
if (error)
|
|
24698
|
+
result.error = error;
|
|
24699
|
+
} catch (error) {
|
|
24700
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
24701
|
+
}
|
|
24702
|
+
return result;
|
|
24703
|
+
}
|
|
24704
|
+
function summarizeWalletDirectory(dir, role) {
|
|
24705
|
+
return {
|
|
24706
|
+
dir,
|
|
24707
|
+
role,
|
|
24708
|
+
config: summarizeConfig(join7(dir, "config.json")),
|
|
24709
|
+
db: summarizeDb(join7(dir, "coco.db"))
|
|
24710
|
+
};
|
|
24711
|
+
}
|
|
24712
|
+
|
|
24713
|
+
class WalletMigrationConflictError extends Error {
|
|
24714
|
+
target;
|
|
24715
|
+
source;
|
|
24716
|
+
constructor(target, source) {
|
|
24717
|
+
super(renderWalletConflict(target, source));
|
|
24718
|
+
this.name = "WalletMigrationConflictError";
|
|
24719
|
+
this.target = target;
|
|
24720
|
+
this.source = source;
|
|
24721
|
+
}
|
|
24722
|
+
}
|
|
24723
|
+
function timeAgo(ms) {
|
|
24724
|
+
if (ms === undefined || !Number.isFinite(ms))
|
|
24725
|
+
return;
|
|
24726
|
+
const elapsed = Date.now() - ms;
|
|
24727
|
+
if (elapsed < 0)
|
|
24728
|
+
return;
|
|
24729
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
24730
|
+
if (seconds < 5)
|
|
24731
|
+
return "just now";
|
|
24732
|
+
if (seconds < 60)
|
|
24733
|
+
return `${seconds}s ago`;
|
|
24734
|
+
const minutes = Math.floor(seconds / 60);
|
|
24735
|
+
if (minutes < 60)
|
|
24736
|
+
return `${minutes}m ago`;
|
|
24737
|
+
const hours = Math.floor(minutes / 60);
|
|
24738
|
+
if (hours < 48)
|
|
24739
|
+
return `${hours}h ago`;
|
|
24740
|
+
const days = Math.floor(hours / 24);
|
|
24741
|
+
if (days < 60)
|
|
24742
|
+
return `${days}d ago`;
|
|
24743
|
+
const months = Math.floor(days / 30);
|
|
24744
|
+
if (months < 24)
|
|
24745
|
+
return `${months}mo ago`;
|
|
24746
|
+
return `${Math.floor(days / 365)}y ago`;
|
|
24747
|
+
}
|
|
24748
|
+
function modifiedDescription(config) {
|
|
24749
|
+
if (config.createdAt)
|
|
24750
|
+
return `created ${config.createdAt}`;
|
|
24751
|
+
const ago = timeAgo(config.mtimeMs);
|
|
24752
|
+
if (ago)
|
|
24753
|
+
return `modified ${ago}`;
|
|
24754
|
+
return "modified time unknown";
|
|
24755
|
+
}
|
|
24756
|
+
function roleLabel(role) {
|
|
24757
|
+
return role === "canonical" ? "current routstrd wallet" : "legacy cocod wallet";
|
|
24758
|
+
}
|
|
24759
|
+
function configLine(config) {
|
|
24760
|
+
if (!config.exists)
|
|
24761
|
+
return "config.json absent";
|
|
24762
|
+
if (config.error)
|
|
24763
|
+
return `config.json present (unreadable: ${config.error})`;
|
|
24764
|
+
const parts = ["config.json present"];
|
|
24765
|
+
if (config.fingerprint)
|
|
24766
|
+
parts.push(`fingerprint ${config.fingerprint}`);
|
|
24767
|
+
else if (config.encrypted)
|
|
24768
|
+
parts.push("encrypted (mnemonic not readable)");
|
|
24769
|
+
else
|
|
24770
|
+
parts.push("no mnemonic found");
|
|
24771
|
+
parts.push(modifiedDescription(config));
|
|
24772
|
+
if (config.defaultMintUrl)
|
|
24773
|
+
parts.push(`default mint ${config.defaultMintUrl}`);
|
|
24774
|
+
return parts.join(" \xB7 ");
|
|
24775
|
+
}
|
|
24776
|
+
function formatNumber(value) {
|
|
24777
|
+
return value.toLocaleString("en-US");
|
|
24778
|
+
}
|
|
24779
|
+
function amountByStateLine(summary) {
|
|
24780
|
+
const parts = Object.entries(summary.amountByState).filter(([, amount]) => amount > 0);
|
|
24781
|
+
if (parts.length === 0)
|
|
24782
|
+
return "0 sats";
|
|
24783
|
+
return parts.map(([state, amount]) => `${state}: ${formatNumber(amount)} sats`).join(", ");
|
|
24784
|
+
}
|
|
24785
|
+
function dbLine(db) {
|
|
24786
|
+
if (!db.exists)
|
|
24787
|
+
return "coco.db absent";
|
|
24788
|
+
if (db.error && !db.summary)
|
|
24789
|
+
return `coco.db present (unreadable: ${db.error})`;
|
|
24790
|
+
const parts = ["coco.db present"];
|
|
24791
|
+
if (db.summary) {
|
|
24792
|
+
const { totalProofs, mints, distinctMints } = db.summary;
|
|
24793
|
+
const mintCount = mints.length > 0 ? mints.length : distinctMints;
|
|
24794
|
+
parts.push(`${formatNumber(totalProofs)} proof${totalProofs === 1 ? "" : "s"}`);
|
|
24795
|
+
parts.push(amountByStateLine(db.summary));
|
|
24796
|
+
parts.push(`${mintCount} mint${mintCount === 1 ? "" : "s"}`);
|
|
24797
|
+
}
|
|
24798
|
+
return parts.join(" \xB7 ");
|
|
24799
|
+
}
|
|
24800
|
+
function block(diag, letter) {
|
|
24801
|
+
return [
|
|
24802
|
+
` ${letter}) ${diag.dir} (${roleLabel(diag.role)})`,
|
|
24803
|
+
` ${configLine(diag.config)}`,
|
|
24804
|
+
` ${dbLine(diag.db)}`
|
|
24805
|
+
].join(`
|
|
24806
|
+
`);
|
|
24807
|
+
}
|
|
24808
|
+
function moveAsideCommands(target, source) {
|
|
24809
|
+
return [
|
|
24810
|
+
" \u2022 If B is your wallet (keep the legacy wallet):",
|
|
24811
|
+
` mv "${target.dir}" "${target.dir}.old"`,
|
|
24812
|
+
"",
|
|
24813
|
+
" \u2022 If A is your wallet (keep the normal routstrd wallet):",
|
|
24814
|
+
` mv "${source.dir}" "${source.dir}.old"`
|
|
24815
|
+
].join(`
|
|
24816
|
+
`);
|
|
24817
|
+
}
|
|
24818
|
+
function renderResolutionSteps(target, source) {
|
|
24819
|
+
return [
|
|
24820
|
+
"To resolve, decide which wallet is yours, move the other one aside, and run",
|
|
24821
|
+
"'routstrd onboard' (or 'routstrd start') again. Stop the daemon first:",
|
|
24822
|
+
"",
|
|
24823
|
+
" routstrd stop",
|
|
24824
|
+
"",
|
|
24825
|
+
"Then:",
|
|
24826
|
+
"",
|
|
24827
|
+
moveAsideCommands(target, source)
|
|
24828
|
+
].join(`
|
|
24829
|
+
`);
|
|
24830
|
+
}
|
|
24831
|
+
function renderWalletConflict(target, source) {
|
|
24832
|
+
return [
|
|
24833
|
+
"Cannot migrate wallet: two different wallets were found, and routstrd will",
|
|
24834
|
+
"not choose a mnemonic or merge databases automatically \u2014 the wrong choice",
|
|
24835
|
+
"could lose access to your funds.",
|
|
24836
|
+
"",
|
|
24837
|
+
block(target, "A"),
|
|
24838
|
+
block(source, "B"),
|
|
24839
|
+
"",
|
|
24840
|
+
"No files were changed.",
|
|
24841
|
+
"",
|
|
24842
|
+
renderResolutionSteps(target, source),
|
|
24843
|
+
"",
|
|
24844
|
+
"For a full comparison and safe cleanup, run: routstrd wallet doctor"
|
|
24845
|
+
].join(`
|
|
24846
|
+
`);
|
|
24847
|
+
}
|
|
24848
|
+
function conflictVerdict(target, source) {
|
|
24849
|
+
const unreadable = !!target.config.error || !!source.config.error || !!target.db.error || !!source.db.error;
|
|
24850
|
+
if (unreadable) {
|
|
24851
|
+
return {
|
|
24852
|
+
text: "Verdict: both wallets exist but one or both could not be fully read; startup will refuse to migrate. See the details above.",
|
|
24853
|
+
showResolution: true,
|
|
24854
|
+
conflict: true
|
|
24855
|
+
};
|
|
24856
|
+
}
|
|
24857
|
+
if (target.config.fingerprint && source.config.fingerprint) {
|
|
24858
|
+
if (target.config.fingerprint === source.config.fingerprint) {
|
|
24859
|
+
return {
|
|
24860
|
+
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.",
|
|
24861
|
+
showResolution: true,
|
|
24862
|
+
conflict: true
|
|
24863
|
+
};
|
|
24864
|
+
}
|
|
24865
|
+
return {
|
|
24866
|
+
text: "Verdict: the two wallets have DIFFERENT mnemonics. Startup will refuse to migrate.",
|
|
24867
|
+
showResolution: true,
|
|
24868
|
+
conflict: true
|
|
24869
|
+
};
|
|
24870
|
+
}
|
|
24871
|
+
return {
|
|
24872
|
+
text: "Verdict: both wallets exist but their mnemonics cannot be compared (encrypted or missing). Startup will refuse to migrate unless the files are identical.",
|
|
24873
|
+
showResolution: true,
|
|
24874
|
+
conflict: true
|
|
24875
|
+
};
|
|
24876
|
+
}
|
|
24877
|
+
function verdictFromClassification(target, source, classification) {
|
|
24878
|
+
switch (classification.kind) {
|
|
24879
|
+
case "fresh":
|
|
24880
|
+
return {
|
|
24881
|
+
text: "Verdict: no wallet found in either location (fresh install).",
|
|
24882
|
+
showResolution: false,
|
|
24883
|
+
conflict: false
|
|
24884
|
+
};
|
|
24885
|
+
case "already-current":
|
|
24886
|
+
return {
|
|
24887
|
+
text: "Verdict: the current routstrd wallet is authoritative; no migration needed.",
|
|
24888
|
+
showResolution: false,
|
|
24889
|
+
conflict: false
|
|
24890
|
+
};
|
|
24891
|
+
case "migrate":
|
|
24892
|
+
return {
|
|
24893
|
+
text: "Verdict: only the legacy cocod wallet exists; it will be migrated on next startup.",
|
|
24894
|
+
showResolution: false,
|
|
24895
|
+
conflict: false
|
|
24896
|
+
};
|
|
24897
|
+
case "conflict":
|
|
24898
|
+
return conflictVerdict(target, source);
|
|
24899
|
+
case "database-only":
|
|
24900
|
+
return {
|
|
24901
|
+
text: "Verdict: a wallet is incomplete (coco.db without config.json). Startup will refuse to migrate. See the details above.",
|
|
24902
|
+
showResolution: false,
|
|
24903
|
+
conflict: true
|
|
24904
|
+
};
|
|
24905
|
+
case "orphaned-sidecars":
|
|
24906
|
+
return {
|
|
24907
|
+
text: "Verdict: the legacy wallet has SQLite sidecar files without coco.db. Startup will refuse to migrate. Restore the matching database first.",
|
|
24908
|
+
showResolution: false,
|
|
24909
|
+
conflict: true
|
|
24910
|
+
};
|
|
24911
|
+
}
|
|
24912
|
+
}
|
|
24913
|
+
function diagnoseWallets(target, source) {
|
|
24914
|
+
let classification;
|
|
24915
|
+
try {
|
|
24916
|
+
classification = classifyWalletMigration(target.dir, source.dir);
|
|
24917
|
+
} catch {
|
|
24918
|
+
if (target.config.error || source.config.error || target.db.error || source.db.error) {
|
|
24919
|
+
return conflictVerdict(target, source);
|
|
24920
|
+
}
|
|
24921
|
+
return {
|
|
24922
|
+
text: "Verdict: the wallets could not be fully compared (read error); startup will refuse to migrate. See the details above.",
|
|
24923
|
+
showResolution: true,
|
|
24924
|
+
conflict: true
|
|
24925
|
+
};
|
|
24926
|
+
}
|
|
24927
|
+
return verdictFromClassification(target, source, classification);
|
|
24928
|
+
}
|
|
24929
|
+
function renderWalletDoctor(target, source) {
|
|
24930
|
+
const verdict = diagnoseWallets(target, source);
|
|
24931
|
+
const lines = [
|
|
24932
|
+
"Routstr wallet diagnostic",
|
|
24933
|
+
"========================",
|
|
24934
|
+
"",
|
|
24935
|
+
block(target, "A"),
|
|
24936
|
+
block(source, "B"),
|
|
24937
|
+
"",
|
|
24938
|
+
verdict.text
|
|
24939
|
+
];
|
|
24940
|
+
if (verdict.showResolution) {
|
|
24941
|
+
lines.push("", renderResolutionSteps(target, source));
|
|
24942
|
+
}
|
|
24943
|
+
return lines.join(`
|
|
24944
|
+
`);
|
|
24945
|
+
}
|
|
24946
|
+
|
|
24947
|
+
// src/daemon/wallet/migration.ts
|
|
24948
|
+
function sqlString(value) {
|
|
24949
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
24950
|
+
}
|
|
24510
24951
|
function snapshotDatabase(sourcePath, destinationPath) {
|
|
24511
|
-
const source = new
|
|
24952
|
+
const source = new Database2(sourcePath);
|
|
24512
24953
|
let sourceSummary;
|
|
24513
24954
|
try {
|
|
24514
24955
|
sourceSummary = verifyDatabase(source, "Legacy wallet database");
|
|
@@ -24516,7 +24957,7 @@ function snapshotDatabase(sourcePath, destinationPath) {
|
|
|
24516
24957
|
} finally {
|
|
24517
24958
|
source.close();
|
|
24518
24959
|
}
|
|
24519
|
-
const destination = new
|
|
24960
|
+
const destination = new Database2(destinationPath, { readonly: true });
|
|
24520
24961
|
try {
|
|
24521
24962
|
const destinationSummary = verifyDatabase(destination, "Staged wallet database");
|
|
24522
24963
|
if (JSON.stringify(sourceSummary) !== JSON.stringify(destinationSummary)) {
|
|
@@ -24529,51 +24970,45 @@ function snapshotDatabase(sourcePath, destinationPath) {
|
|
|
24529
24970
|
async function migrateLegacyWallet(options = {}) {
|
|
24530
24971
|
const targetDir = options.walletDir || walletDir();
|
|
24531
24972
|
const sourceDir = options.legacyDir || legacyCocodDir();
|
|
24532
|
-
const
|
|
24533
|
-
const
|
|
24534
|
-
const sourceConfig = join6(sourceDir, "config.json");
|
|
24535
|
-
const sourceDb = join6(sourceDir, "coco.db");
|
|
24973
|
+
const sourceConfig = join8(sourceDir, "config.json");
|
|
24974
|
+
const sourceDb = join8(sourceDir, "coco.db");
|
|
24536
24975
|
const sourceWal = `${sourceDb}-wal`;
|
|
24537
24976
|
const sourceShm = `${sourceDb}-shm`;
|
|
24538
|
-
const
|
|
24539
|
-
|
|
24540
|
-
|
|
24541
|
-
|
|
24542
|
-
|
|
24543
|
-
if (configsMatch && databasesMatch)
|
|
24977
|
+
const classification = classifyWalletMigration(targetDir, sourceDir);
|
|
24978
|
+
switch (classification.kind) {
|
|
24979
|
+
case "fresh":
|
|
24980
|
+
return { status: "fresh" };
|
|
24981
|
+
case "already-current":
|
|
24544
24982
|
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.");
|
|
24983
|
+
case "migrate":
|
|
24984
|
+
break;
|
|
24985
|
+
case "conflict":
|
|
24986
|
+
throw new WalletMigrationConflictError(summarizeWalletDirectory(targetDir, "canonical"), summarizeWalletDirectory(sourceDir, "legacy"));
|
|
24987
|
+
case "orphaned-sidecars":
|
|
24988
|
+
throw new Error(`Cannot migrate wallet: ${sourceDir} contains SQLite sidecar files without coco.db. ` + "Restore the matching main database before migration.");
|
|
24989
|
+
case "database-only":
|
|
24990
|
+
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
24991
|
}
|
|
24555
|
-
if (sourceState === "absent")
|
|
24556
|
-
return { status: "fresh" };
|
|
24557
24992
|
await options.assertLegacyStopped?.();
|
|
24558
24993
|
const releaseLegacyLock = await options.acquireLegacyLock?.();
|
|
24559
|
-
const stagingDir =
|
|
24994
|
+
const stagingDir = join8(dirname7(targetDir), `.${targetDir.split(/[\\/]/).at(-1) || "wallet"}.staging-${process.pid}-${Date.now()}`);
|
|
24560
24995
|
try {
|
|
24561
24996
|
await options.assertLegacyStopped?.();
|
|
24562
|
-
|
|
24563
|
-
|
|
24564
|
-
const stagedConfig =
|
|
24565
|
-
const stagedDb =
|
|
24997
|
+
mkdirSync9(dirname7(targetDir), { recursive: true, mode: 448 });
|
|
24998
|
+
mkdirSync9(stagingDir, { mode: 448 });
|
|
24999
|
+
const stagedConfig = join8(stagingDir, "config.json");
|
|
25000
|
+
const stagedDb = join8(stagingDir, "coco.db");
|
|
24566
25001
|
copyFileSync(sourceConfig, stagedConfig);
|
|
24567
|
-
|
|
24568
|
-
const hasSourceDb =
|
|
25002
|
+
chmodSync2(stagedConfig, 384);
|
|
25003
|
+
const hasSourceDb = existsSync13(sourceDb);
|
|
24569
25004
|
if (hasSourceDb) {
|
|
24570
25005
|
snapshotDatabase(sourceDb, stagedDb);
|
|
24571
|
-
|
|
25006
|
+
chmodSync2(stagedDb, 384);
|
|
24572
25007
|
}
|
|
24573
|
-
if (
|
|
25008
|
+
if (statSync3(stagedConfig).size !== statSync3(sourceConfig).size) {
|
|
24574
25009
|
throw new Error("Cannot migrate wallet: staged config failed size validation.");
|
|
24575
25010
|
}
|
|
24576
|
-
|
|
25011
|
+
renameSync3(stagingDir, targetDir);
|
|
24577
25012
|
} catch (error) {
|
|
24578
25013
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
24579
25014
|
throw error;
|
|
@@ -24581,13 +25016,13 @@ async function migrateLegacyWallet(options = {}) {
|
|
|
24581
25016
|
releaseLegacyLock?.();
|
|
24582
25017
|
}
|
|
24583
25018
|
const cleanupWarnings = [];
|
|
24584
|
-
const sourceFiles = [sourceConfig, sourceDb, sourceWal, sourceShm].filter(
|
|
25019
|
+
const sourceFiles = [sourceConfig, sourceDb, sourceWal, sourceShm].filter(existsSync13);
|
|
24585
25020
|
if (sourceFiles.length > 0) {
|
|
24586
|
-
const archiveDir =
|
|
25021
|
+
const archiveDir = join8(sourceDir, `wallet-migrated-${Date.now()}`);
|
|
24587
25022
|
try {
|
|
24588
|
-
|
|
25023
|
+
mkdirSync9(archiveDir, { mode: 448 });
|
|
24589
25024
|
for (const path of sourceFiles)
|
|
24590
|
-
|
|
25025
|
+
renameSync3(path, join8(archiveDir, basename(path)));
|
|
24591
25026
|
} catch (error) {
|
|
24592
25027
|
const message = error instanceof Error ? error.message : String(error);
|
|
24593
25028
|
cleanupWarnings.push(`Could not archive legacy wallet files: ${message}`);
|
|
@@ -26721,7 +27156,7 @@ zoo`.split(`
|
|
|
26721
27156
|
// package.json
|
|
26722
27157
|
var package_default = {
|
|
26723
27158
|
name: "routstrd",
|
|
26724
|
-
version: "0.4.
|
|
27159
|
+
version: "0.4.2",
|
|
26725
27160
|
module: "src/index.ts",
|
|
26726
27161
|
type: "module",
|
|
26727
27162
|
private: false,
|
|
@@ -26747,7 +27182,7 @@ var package_default = {
|
|
|
26747
27182
|
"@cashu/cashu-ts": "^4.3.0",
|
|
26748
27183
|
"@cashu/coco-core": "^1.0.1",
|
|
26749
27184
|
"@cashu/coco-sqlite-bun": "^1.0.1",
|
|
26750
|
-
"@routstr/sdk": "^0.4.
|
|
27185
|
+
"@routstr/sdk": "^0.4.1",
|
|
26751
27186
|
"@scure/bip39": "^2.2.0",
|
|
26752
27187
|
"applesauce-core": "^5.1.0",
|
|
26753
27188
|
"applesauce-relay": "^5.1.0",
|
|
@@ -26787,11 +27222,11 @@ Invoice:
|
|
|
26787
27222
|
${invoice}`);
|
|
26788
27223
|
}
|
|
26789
27224
|
function initializeWallet(walletDir2 = walletDir()) {
|
|
26790
|
-
const walletConfig =
|
|
26791
|
-
|
|
26792
|
-
|
|
26793
|
-
if (
|
|
26794
|
-
|
|
27225
|
+
const walletConfig = join9(walletDir2, "config.json");
|
|
27226
|
+
mkdirSync10(walletDir2, { recursive: true, mode: 448 });
|
|
27227
|
+
chmodSync3(walletDir2, 448);
|
|
27228
|
+
if (existsSync14(walletConfig)) {
|
|
27229
|
+
chmodSync3(walletConfig, 384);
|
|
26795
27230
|
console.log("Wallet already initialized.");
|
|
26796
27231
|
return;
|
|
26797
27232
|
}
|
|
@@ -26802,7 +27237,7 @@ function initializeWallet(walletDir2 = walletDir()) {
|
|
|
26802
27237
|
encrypted: false,
|
|
26803
27238
|
createdAt: new Date().toISOString()
|
|
26804
27239
|
};
|
|
26805
|
-
|
|
27240
|
+
writeFileSync3(walletConfig, JSON.stringify(config, null, 2), {
|
|
26806
27241
|
mode: 384,
|
|
26807
27242
|
flag: "wx"
|
|
26808
27243
|
});
|
|
@@ -26830,7 +27265,7 @@ Restarting routstrd daemon...`);
|
|
|
26830
27265
|
for (let i2 = 0;i2 < 100; i2++) {
|
|
26831
27266
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
26832
27267
|
const healthDown = !await isDaemonRunning();
|
|
26833
|
-
const pidFileReleased = !
|
|
27268
|
+
const pidFileReleased = !existsSync14(pidFilePath);
|
|
26834
27269
|
if (healthDown && pidFileReleased)
|
|
26835
27270
|
break;
|
|
26836
27271
|
}
|
|
@@ -26873,16 +27308,16 @@ async function requireLocalDaemon() {
|
|
|
26873
27308
|
}
|
|
26874
27309
|
async function initDaemon() {
|
|
26875
27310
|
console.log("Initializing routstrd...");
|
|
26876
|
-
if (!
|
|
26877
|
-
mkdirSync9(CONFIG_DIR, { recursive: true });
|
|
27311
|
+
if (!existsSync14(CONFIG_DIR)) {
|
|
26878
27312
|
console.log(`Created config directory: ${CONFIG_DIR}`);
|
|
26879
27313
|
}
|
|
26880
|
-
|
|
27314
|
+
ensureDirsSync();
|
|
27315
|
+
if (!existsSync14(CONFIG_FILE)) {
|
|
26881
27316
|
const config2 = {
|
|
26882
27317
|
...DEFAULT_CONFIG,
|
|
26883
27318
|
cocodPath: null
|
|
26884
27319
|
};
|
|
26885
|
-
|
|
27320
|
+
saveDaemonConfig(config2);
|
|
26886
27321
|
console.log(`Created config file: ${CONFIG_FILE}`);
|
|
26887
27322
|
}
|
|
26888
27323
|
const config = await loadConfig();
|
|
@@ -26891,7 +27326,7 @@ async function initDaemon() {
|
|
|
26891
27326
|
const nsec = nip19_exports.nsecEncode(secretKey);
|
|
26892
27327
|
const npub = npubFromSecretKey(secretKey);
|
|
26893
27328
|
config.nsec = nsec;
|
|
26894
|
-
|
|
27329
|
+
saveDaemonConfig(config);
|
|
26895
27330
|
console.log(`
|
|
26896
27331
|
A new Nostr identity has been generated for authentication.`);
|
|
26897
27332
|
console.log(`Your npub: ${npub}`);
|
|
@@ -26900,26 +27335,18 @@ A new Nostr identity has been generated for authentication.`);
|
|
|
26900
27335
|
}
|
|
26901
27336
|
console.log(`Database will be stored at: ${DB_PATH}`);
|
|
26902
27337
|
await stopLegacyCocod();
|
|
26903
|
-
let migrationLockOwner;
|
|
26904
27338
|
const migration = await migrateLegacyWallet({
|
|
26905
27339
|
assertLegacyStopped: () => assertLegacyCocodNotRunning({
|
|
26906
|
-
socketPath: legacyCocodSocketPath()
|
|
26907
|
-
pidFilePath: legacyCocodPidPath(),
|
|
26908
|
-
ignorePid: migrationLockOwner
|
|
27340
|
+
socketPath: legacyCocodSocketPath()
|
|
26909
27341
|
}),
|
|
26910
27342
|
acquireLegacyLock: () => {
|
|
26911
|
-
|
|
27343
|
+
mkdirSync10(dirname8(legacyCocodPidPath()), {
|
|
26912
27344
|
recursive: true,
|
|
26913
27345
|
mode: 448
|
|
26914
27346
|
});
|
|
26915
|
-
|
|
27347
|
+
return claimLegacyCocodPidFile({
|
|
26916
27348
|
pidFilePath: legacyCocodPidPath()
|
|
26917
27349
|
});
|
|
26918
|
-
migrationLockOwner = process.pid;
|
|
26919
|
-
return () => {
|
|
26920
|
-
migrationLockOwner = undefined;
|
|
26921
|
-
release();
|
|
26922
|
-
};
|
|
26923
27350
|
}
|
|
26924
27351
|
});
|
|
26925
27352
|
if (migration.status === "migrated") {
|
|
@@ -26941,10 +27368,7 @@ To ensure routstrd persists across system restarts, run: 'routstrd service insta
|
|
|
26941
27368
|
}
|
|
26942
27369
|
program.name("routstrd").description("Routstr daemon - Manage routstr processes").version(package_default.version, "--version", "output the version number");
|
|
26943
27370
|
program.command("update").description("Update routstrd to the latest version").action(async () => {
|
|
26944
|
-
const packages = [
|
|
26945
|
-
{ name: "routstrd", label: "routstrd" },
|
|
26946
|
-
{ name: "@routstr/cocod", label: "cocod" }
|
|
26947
|
-
];
|
|
27371
|
+
const packages = [{ name: "routstrd", label: "routstrd" }];
|
|
26948
27372
|
let updatedAny = false;
|
|
26949
27373
|
for (const { name, label } of packages) {
|
|
26950
27374
|
const installed = await getGlobalPackageVersion(name);
|
|
@@ -26977,7 +27401,7 @@ program.command("update").description("Update routstrd to the latest version").a
|
|
|
26977
27401
|
All packages are already up to date \u2014 nothing to do.`);
|
|
26978
27402
|
}
|
|
26979
27403
|
});
|
|
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
|
|
27404
|
+
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
27405
|
await ensureDaemonRunning();
|
|
26982
27406
|
let mintUrl = options.mintUrl;
|
|
26983
27407
|
if (!mintUrl) {
|
|
@@ -26986,12 +27410,12 @@ program.command("refund").description("Refund pending tokens and API keys to a s
|
|
|
26986
27410
|
console.log(balanceResult.error);
|
|
26987
27411
|
process.exit(1);
|
|
26988
27412
|
}
|
|
26989
|
-
const
|
|
26990
|
-
|
|
27413
|
+
const output = balanceResult.output;
|
|
27414
|
+
mintUrl = output?.activeMint ?? (output?.balances ? Object.keys(output.balances)[0] : undefined);
|
|
27415
|
+
if (!mintUrl) {
|
|
26991
27416
|
console.log("No mint URLs found in wallet balance");
|
|
26992
27417
|
process.exit(1);
|
|
26993
27418
|
}
|
|
26994
|
-
mintUrl = Object.keys(balances)[0];
|
|
26995
27419
|
console.log(`Using mint URL: ${mintUrl}`);
|
|
26996
27420
|
}
|
|
26997
27421
|
try {
|
|
@@ -27078,8 +27502,8 @@ program.command("remote [url]").description("Show the configured remote daemon,
|
|
|
27078
27502
|
process.exit(1);
|
|
27079
27503
|
}
|
|
27080
27504
|
}
|
|
27081
|
-
if (!
|
|
27082
|
-
|
|
27505
|
+
if (!existsSync14(CONFIG_DIR)) {
|
|
27506
|
+
mkdirSync10(CONFIG_DIR, { recursive: true });
|
|
27083
27507
|
}
|
|
27084
27508
|
const updates = { daemonUrl: url };
|
|
27085
27509
|
if (options.authUrl) {
|
|
@@ -27097,7 +27521,7 @@ program.command("remote [url]").description("Show the configured remote daemon,
|
|
|
27097
27521
|
...config,
|
|
27098
27522
|
...updates
|
|
27099
27523
|
};
|
|
27100
|
-
|
|
27524
|
+
saveDaemonConfig(updatedConfig);
|
|
27101
27525
|
console.log(`Remote daemon URL set to: ${url}`);
|
|
27102
27526
|
if (options.authUrl) {
|
|
27103
27527
|
console.log(`Auth proxy URL set to: ${options.authUrl}`);
|
|
@@ -27119,7 +27543,7 @@ program.command("local").description("Switch back to local daemon mode (removes
|
|
|
27119
27543
|
const previousAuthUrl = config.authUrl;
|
|
27120
27544
|
const { daemonUrl: _daemonUrl, authUrl: _authUrl, ...rest } = config;
|
|
27121
27545
|
const updatedConfig = rest;
|
|
27122
|
-
|
|
27546
|
+
saveDaemonConfig(updatedConfig);
|
|
27123
27547
|
console.log("Switched back to local daemon mode.");
|
|
27124
27548
|
if (previousDaemonUrl) {
|
|
27125
27549
|
console.log(` Removed remote daemon URL: ${previousDaemonUrl}`);
|
|
@@ -27133,17 +27557,30 @@ Use 'routstrd onboard' for first time using local mode or 'routstrd start' to st
|
|
|
27133
27557
|
});
|
|
27134
27558
|
program.command("onboard").description("Initialize routstrd (creates config directory and initializes wallet)").action(async () => {
|
|
27135
27559
|
await requireLocalDaemon();
|
|
27136
|
-
|
|
27560
|
+
try {
|
|
27561
|
+
await initDaemon();
|
|
27562
|
+
} catch (error) {
|
|
27563
|
+
if (error instanceof WalletMigrationConflictError) {
|
|
27564
|
+
console.error(error.message);
|
|
27565
|
+
process.exit(1);
|
|
27566
|
+
}
|
|
27567
|
+
throw error;
|
|
27568
|
+
}
|
|
27137
27569
|
});
|
|
27138
27570
|
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
27571
|
await requireLocalDaemon();
|
|
27140
27572
|
const config = await loadConfig();
|
|
27141
27573
|
await stopLegacyCocod();
|
|
27142
|
-
|
|
27143
|
-
|
|
27144
|
-
|
|
27145
|
-
|
|
27146
|
-
|
|
27574
|
+
try {
|
|
27575
|
+
await startDaemon({
|
|
27576
|
+
port: options.port || String(config.port || 8008),
|
|
27577
|
+
host: options.host || config.host || undefined,
|
|
27578
|
+
provider: options.provider
|
|
27579
|
+
});
|
|
27580
|
+
} catch (error) {
|
|
27581
|
+
console.error(error instanceof Error ? error.message : error);
|
|
27582
|
+
process.exit(1);
|
|
27583
|
+
}
|
|
27147
27584
|
});
|
|
27148
27585
|
program.command("status").description("Check daemon and wallet status").action(async () => {
|
|
27149
27586
|
const running = await isDaemonRunning();
|
|
@@ -27169,7 +27606,7 @@ program.command("status").description("Check daemon and wallet status").action(a
|
|
|
27169
27606
|
}
|
|
27170
27607
|
}
|
|
27171
27608
|
});
|
|
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
|
|
27609
|
+
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
27610
|
await ensureDaemonRunning();
|
|
27174
27611
|
if (options.deleteApiKeys) {
|
|
27175
27612
|
const baseUrl = options.deleteApiKeys;
|
|
@@ -27437,6 +27874,46 @@ providersCmd.command("enable <indices...>").description("Enable providers by the
|
|
|
27437
27874
|
}
|
|
27438
27875
|
}
|
|
27439
27876
|
});
|
|
27877
|
+
providersCmd.command("reviews").description("Show all known providers with their stored review events and event IDs").action(async () => {
|
|
27878
|
+
await ensureDaemonRunning();
|
|
27879
|
+
const result = await callDaemon("/providers/reviews");
|
|
27880
|
+
if (result.error) {
|
|
27881
|
+
console.log(result.error);
|
|
27882
|
+
process.exit(1);
|
|
27883
|
+
}
|
|
27884
|
+
const output = result.output;
|
|
27885
|
+
if (!output) {
|
|
27886
|
+
console.log("No provider review data available.");
|
|
27887
|
+
return;
|
|
27888
|
+
}
|
|
27889
|
+
console.log(`Providers (${output.totalCount} total, ${output.reviewEventCount} stored review events):
|
|
27890
|
+
`);
|
|
27891
|
+
for (const provider of output.providers) {
|
|
27892
|
+
const status = provider.disabled ? "DISABLED" : "enabled ";
|
|
27893
|
+
console.log(` [${provider.index}] ${status} ${provider.baseUrl} (${provider.reviewCount} review${provider.reviewCount === 1 ? "" : "s"})`);
|
|
27894
|
+
if (provider.nodePubkeys.length > 0) {
|
|
27895
|
+
console.log(` node pubkeys: ${provider.nodePubkeys.join(", ")}`);
|
|
27896
|
+
}
|
|
27897
|
+
for (const review of provider.reviews) {
|
|
27898
|
+
const label = review.label || "review";
|
|
27899
|
+
const created = new Date(review.createdAt * 1000).toISOString();
|
|
27900
|
+
console.log(` - ${label} eventId: ${review.eventId}`);
|
|
27901
|
+
console.log(` node: ${review.nodePubkey ?? "(none)"}`);
|
|
27902
|
+
console.log(` author: ${review.authorPubkey ?? "(none)"}`);
|
|
27903
|
+
console.log(` at: ${created}`);
|
|
27904
|
+
}
|
|
27905
|
+
}
|
|
27906
|
+
if (output.unmatchedReviews.length > 0) {
|
|
27907
|
+
console.log(`
|
|
27908
|
+
Unmatched review events (${output.unmatchedReviews.length} \u2014 node pubkey not in any known provider):`);
|
|
27909
|
+
for (const review of output.unmatchedReviews) {
|
|
27910
|
+
const created = new Date(review.createdAt * 1000).toISOString();
|
|
27911
|
+
console.log(` - eventId: ${review.eventId}`);
|
|
27912
|
+
console.log(` node: ${review.nodePubkey ?? "(none)"}`);
|
|
27913
|
+
console.log(` at: ${created}`);
|
|
27914
|
+
}
|
|
27915
|
+
}
|
|
27916
|
+
});
|
|
27440
27917
|
var clientsCmd = program.command("clients").description("List and manage clients");
|
|
27441
27918
|
clientsCmd.command("list").description("List all clients").action(async () => {
|
|
27442
27919
|
await listClientsAction();
|
|
@@ -27714,6 +28191,13 @@ var walletCmd = program.command("wallet").description("Wallet operations");
|
|
|
27714
28191
|
walletCmd.command("status").description("Check wallet status").action(async () => {
|
|
27715
28192
|
await handleDaemonCommand("/wallet/status");
|
|
27716
28193
|
});
|
|
28194
|
+
walletCmd.command("doctor").description("Diagnose conflicting wallets (current routstrd wallet vs legacy cocod)").action(async () => {
|
|
28195
|
+
const target = summarizeWalletDirectory(walletDir(), "canonical");
|
|
28196
|
+
const source = summarizeWalletDirectory(legacyCocodDir(), "legacy");
|
|
28197
|
+
console.log(renderWalletDoctor(target, source));
|
|
28198
|
+
if (diagnoseWallets(target, source).conflict)
|
|
28199
|
+
process.exit(1);
|
|
28200
|
+
});
|
|
27717
28201
|
walletCmd.command("unlock <passphrase>").description("Unlock the wallet").action(async (passphrase) => {
|
|
27718
28202
|
await handleDaemonCommand("/wallet/unlock", {
|
|
27719
28203
|
method: "POST",
|
|
@@ -27723,6 +28207,68 @@ walletCmd.command("unlock <passphrase>").description("Unlock the wallet").action
|
|
|
27723
28207
|
walletCmd.command("balance").description("Get wallet balance").action(async () => {
|
|
27724
28208
|
await handleDaemonCommand("/wallet/balance");
|
|
27725
28209
|
});
|
|
28210
|
+
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) => {
|
|
28211
|
+
const minAgeHours = Number.parseFloat(options.minAge);
|
|
28212
|
+
if (!Number.isFinite(minAgeHours) || minAgeHours < 0) {
|
|
28213
|
+
console.error(`Invalid --min-age value: ${options.minAge}`);
|
|
28214
|
+
process.exit(1);
|
|
28215
|
+
}
|
|
28216
|
+
if (!options.dryRun && !options.yes) {
|
|
28217
|
+
const rl = __require("readline").createInterface({
|
|
28218
|
+
input: process.stdin,
|
|
28219
|
+
output: process.stdout
|
|
28220
|
+
});
|
|
28221
|
+
const answer = await new Promise((resolve) => {
|
|
28222
|
+
rl.question("This will fail expired mint quotes, reclaim old pending sends, and cancel prepared melts. Continue? [y/N] ", (value) => {
|
|
28223
|
+
rl.close();
|
|
28224
|
+
resolve(value.trim().toLowerCase());
|
|
28225
|
+
});
|
|
28226
|
+
});
|
|
28227
|
+
if (answer !== "y" && answer !== "yes") {
|
|
28228
|
+
console.log("Aborted.");
|
|
28229
|
+
return;
|
|
28230
|
+
}
|
|
28231
|
+
}
|
|
28232
|
+
try {
|
|
28233
|
+
await ensureDaemonRunning();
|
|
28234
|
+
const result = await callDaemon("/wallet/cleanup", {
|
|
28235
|
+
method: "POST",
|
|
28236
|
+
body: {
|
|
28237
|
+
mintUrl: options.mintUrl,
|
|
28238
|
+
minAgeMs: Math.round(minAgeHours * 60 * 60 * 1000),
|
|
28239
|
+
dryRun: options.dryRun === true
|
|
28240
|
+
}
|
|
28241
|
+
});
|
|
28242
|
+
if (result.error) {
|
|
28243
|
+
console.log(result.error);
|
|
28244
|
+
process.exit(1);
|
|
28245
|
+
}
|
|
28246
|
+
const output = result.output;
|
|
28247
|
+
if (output) {
|
|
28248
|
+
const prefix = output.dryRun ? "Would clean up:" : "Cleaned up:";
|
|
28249
|
+
console.log(prefix);
|
|
28250
|
+
console.log(` Expired mint quotes failed: ${output.failedMintQuotes ?? 0}`);
|
|
28251
|
+
console.log(` Pending sends reclaimed: ${output.reclaimedSends ?? 0}`);
|
|
28252
|
+
console.log(` Prepared melts cancelled: ${output.cancelledMelts ?? 0}`);
|
|
28253
|
+
console.log(` Skipped (still recent or already terminal): ${output.skipped ?? 0}`);
|
|
28254
|
+
if (output.errors && output.errors.length > 0) {
|
|
28255
|
+
console.log(`
|
|
28256
|
+
Errors:`);
|
|
28257
|
+
for (const e of output.errors) {
|
|
28258
|
+
console.log(` - ${e.operationId}: ${e.error}`);
|
|
28259
|
+
}
|
|
28260
|
+
}
|
|
28261
|
+
}
|
|
28262
|
+
} catch (error) {
|
|
28263
|
+
const message = error.message;
|
|
28264
|
+
if (message?.includes("fetch failed") || message?.includes("Connection refused")) {
|
|
28265
|
+
console.error("Daemon is not running");
|
|
28266
|
+
process.exit(1);
|
|
28267
|
+
}
|
|
28268
|
+
console.error(message);
|
|
28269
|
+
process.exit(1);
|
|
28270
|
+
}
|
|
28271
|
+
});
|
|
27726
28272
|
var walletReceiveCmd = walletCmd.command("receive").description("Wallet receive operations");
|
|
27727
28273
|
walletReceiveCmd.command("cashu <token>").description("Receive a Cashu token").action(async (token) => {
|
|
27728
28274
|
await handleDaemonCommand("/wallet/receive/cashu", {
|
|
@@ -27890,7 +28436,7 @@ serviceCmd.command("install").description("Install and start routstrd using PM2
|
|
|
27890
28436
|
const path = __require("path");
|
|
27891
28437
|
daemonPath = path.join(path.dirname(import.meta.url).replace("file://", ""), "daemon", "index.js");
|
|
27892
28438
|
}
|
|
27893
|
-
if (!
|
|
28439
|
+
if (!existsSync14(daemonPath)) {
|
|
27894
28440
|
console.error(`Could not find daemon at ${daemonPath}. Did you run 'bun run build'?`);
|
|
27895
28441
|
process.exit(1);
|
|
27896
28442
|
}
|
|
@@ -27938,7 +28484,7 @@ program.command("restart").description("Restart the background daemon").option("
|
|
|
27938
28484
|
for (let i2 = 0;i2 < 100; i2++) {
|
|
27939
28485
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
27940
28486
|
const healthDown = !await isDaemonRunning();
|
|
27941
|
-
const pidFileReleased = !
|
|
28487
|
+
const pidFileReleased = !existsSync14(pidFilePath);
|
|
27942
28488
|
if (healthDown && pidFileReleased) {
|
|
27943
28489
|
break;
|
|
27944
28490
|
}
|
|
@@ -27996,7 +28542,7 @@ xcashu mode is coming soon! Only lazyrefund/apikeys is available at this time.`)
|
|
|
27996
28542
|
...config,
|
|
27997
28543
|
mode: selectedMode
|
|
27998
28544
|
};
|
|
27999
|
-
|
|
28545
|
+
saveDaemonConfig(updatedConfig);
|
|
28000
28546
|
console.log(`Mode set to '${selectedMode}'. Restarting daemon...`);
|
|
28001
28547
|
const wasRunning = await isDaemonRunning();
|
|
28002
28548
|
if (wasRunning) {
|
|
@@ -28029,7 +28575,7 @@ function getLogFileForDate2(logsDir, date = new Date) {
|
|
|
28029
28575
|
return `${logsDir}/${year}-${month}-${day}.log`;
|
|
28030
28576
|
}
|
|
28031
28577
|
function readLastLines(file, lines) {
|
|
28032
|
-
const content =
|
|
28578
|
+
const content = readFileSync5(file, "utf8");
|
|
28033
28579
|
const allLines = content.replace(/\r\n/g, `
|
|
28034
28580
|
`).split(`
|
|
28035
28581
|
`);
|
|
@@ -28043,13 +28589,13 @@ async function followLogFile(file, lines) {
|
|
|
28043
28589
|
if (initial) {
|
|
28044
28590
|
console.log(initial);
|
|
28045
28591
|
}
|
|
28046
|
-
let position =
|
|
28592
|
+
let position = statSync4(file).size;
|
|
28047
28593
|
while (true) {
|
|
28048
28594
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
28049
|
-
if (!
|
|
28595
|
+
if (!existsSync14(file)) {
|
|
28050
28596
|
continue;
|
|
28051
28597
|
}
|
|
28052
|
-
const size =
|
|
28598
|
+
const size = statSync4(file).size;
|
|
28053
28599
|
if (size < position) {
|
|
28054
28600
|
position = 0;
|
|
28055
28601
|
}
|
|
@@ -28068,17 +28614,17 @@ program.command("logs").description("View daemon logs").option("-f, --follow", "
|
|
|
28068
28614
|
const yesterday = new Date;
|
|
28069
28615
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
28070
28616
|
const yesterdayFile = getLogFileForDate2(logsDir, yesterday);
|
|
28071
|
-
if (!
|
|
28617
|
+
if (!existsSync14(todayFile) && !existsSync14(yesterdayFile)) {
|
|
28072
28618
|
console.log("No log files found. Daemon may not have started yet.");
|
|
28073
28619
|
console.log(`Logs directory: ${logsDir}`);
|
|
28074
28620
|
process.exit(1);
|
|
28075
28621
|
}
|
|
28076
28622
|
const lines = parseInt(options.lines, 10);
|
|
28077
28623
|
const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => {
|
|
28078
|
-
return
|
|
28624
|
+
return existsSync14(file) && files.indexOf(file) === index;
|
|
28079
28625
|
});
|
|
28080
28626
|
if (options.follow) {
|
|
28081
|
-
if (
|
|
28627
|
+
if (existsSync14(todayFile)) {
|
|
28082
28628
|
await followLogFile(todayFile, lines);
|
|
28083
28629
|
} else {
|
|
28084
28630
|
console.log("No log file for today to follow.");
|