@fiber-pay/cli 0.1.0-rc.4 → 0.1.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,29 +1,229 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { join as join7 } from "path";
5
- import { Command as Command13 } from "commander";
4
+ import { join as join9 } from "path";
5
+ import { Command as Command14 } from "commander";
6
6
 
7
7
  // src/commands/binary.ts
8
- import {
9
- DEFAULT_FIBER_VERSION,
10
- downloadFiberBinary,
11
- getFiberBinaryInfo
12
- } from "@fiber-pay/node";
8
+ import { DEFAULT_FIBER_VERSION, downloadFiberBinary } from "@fiber-pay/node";
13
9
  import { Command } from "commander";
14
10
 
11
+ // src/lib/binary-path.ts
12
+ import { dirname, join } from "path";
13
+ import { BinaryManager, getFiberBinaryInfo } from "@fiber-pay/node";
14
+
15
+ // src/lib/node-runtime-daemon.ts
16
+ import { spawnSync } from "child_process";
17
+ import { existsSync } from "fs";
18
+ function getCustomBinaryState(binaryPath) {
19
+ const exists = existsSync(binaryPath);
20
+ if (!exists) {
21
+ return { path: binaryPath, ready: false, version: "unknown" };
22
+ }
23
+ try {
24
+ const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
25
+ if (result.status !== 0) {
26
+ return { path: binaryPath, ready: false, version: "unknown" };
27
+ }
28
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
29
+ const firstLine = output.split("\n").find((line) => line.trim().length > 0) ?? "unknown";
30
+ return { path: binaryPath, ready: true, version: firstLine.trim() };
31
+ } catch {
32
+ return { path: binaryPath, ready: false, version: "unknown" };
33
+ }
34
+ }
35
+ function getBinaryVersion(binaryPath) {
36
+ try {
37
+ const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
38
+ if (result.status !== 0) {
39
+ return "unknown";
40
+ }
41
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
42
+ if (!output) {
43
+ return "unknown";
44
+ }
45
+ const firstLine = output.split("\n").find((line) => line.trim().length > 0);
46
+ return firstLine?.trim() ?? "unknown";
47
+ } catch {
48
+ return "unknown";
49
+ }
50
+ }
51
+ function getCliEntrypoint() {
52
+ const entrypoint = process.argv[1];
53
+ if (!entrypoint) {
54
+ throw new Error("Unable to resolve CLI entrypoint path");
55
+ }
56
+ return entrypoint;
57
+ }
58
+ function startRuntimeDaemonFromNode(params) {
59
+ const cliEntrypoint = getCliEntrypoint();
60
+ const result = spawnSync(
61
+ process.execPath,
62
+ [
63
+ cliEntrypoint,
64
+ "--data-dir",
65
+ params.dataDir,
66
+ "--rpc-url",
67
+ params.rpcUrl,
68
+ "runtime",
69
+ "start",
70
+ "--daemon",
71
+ "--fiber-rpc-url",
72
+ params.rpcUrl,
73
+ "--proxy-listen",
74
+ params.proxyListen,
75
+ "--state-file",
76
+ params.stateFilePath,
77
+ "--alert-logs-base-dir",
78
+ params.alertLogsBaseDir,
79
+ "--json"
80
+ ],
81
+ { encoding: "utf-8" }
82
+ );
83
+ if (result.status === 0) {
84
+ return { ok: true };
85
+ }
86
+ const stderr = (result.stderr ?? "").trim();
87
+ const stdout = (result.stdout ?? "").trim();
88
+ const details = stderr || stdout || `exit code ${result.status ?? "unknown"}`;
89
+ return { ok: false, message: details };
90
+ }
91
+ function stopRuntimeDaemonFromNode(params) {
92
+ const cliEntrypoint = getCliEntrypoint();
93
+ spawnSync(
94
+ process.execPath,
95
+ [
96
+ cliEntrypoint,
97
+ "--data-dir",
98
+ params.dataDir,
99
+ "--rpc-url",
100
+ params.rpcUrl,
101
+ "runtime",
102
+ "stop",
103
+ "--json"
104
+ ],
105
+ { encoding: "utf-8" }
106
+ );
107
+ }
108
+
109
+ // src/lib/binary-path.ts
110
+ function getProfileBinaryInstallDir(dataDir) {
111
+ return join(dataDir, "bin");
112
+ }
113
+ function getProfileManagedBinaryPath(dataDir) {
114
+ return new BinaryManager(getProfileBinaryInstallDir(dataDir)).getBinaryPath();
115
+ }
116
+ function validateConfiguredBinaryPath(binaryPath) {
117
+ const value = binaryPath.trim();
118
+ if (!value) {
119
+ throw new Error("Configured binaryPath cannot be empty");
120
+ }
121
+ if (value.includes("\0")) {
122
+ throw new Error("Configured binaryPath contains an invalid null byte");
123
+ }
124
+ return value;
125
+ }
126
+ function resolveBinaryPath(config) {
127
+ const managedPath = getProfileManagedBinaryPath(config.dataDir);
128
+ if (config.binaryPath) {
129
+ const binaryPath2 = validateConfiguredBinaryPath(config.binaryPath);
130
+ const installDir2 = dirname(binaryPath2);
131
+ const expectedPath = new BinaryManager(installDir2).getBinaryPath();
132
+ const managedByBinaryManager = expectedPath === binaryPath2;
133
+ const source = binaryPath2 === managedPath ? "profile-managed" : "configured-path";
134
+ return {
135
+ binaryPath: binaryPath2,
136
+ installDir: managedByBinaryManager ? installDir2 : null,
137
+ managedPath,
138
+ managedByBinaryManager,
139
+ source
140
+ };
141
+ }
142
+ const installDir = getProfileBinaryInstallDir(config.dataDir);
143
+ const binaryPath = managedPath;
144
+ return {
145
+ binaryPath,
146
+ installDir,
147
+ managedPath,
148
+ managedByBinaryManager: true,
149
+ source: "profile-managed"
150
+ };
151
+ }
152
+ function getBinaryManagerInstallDirOrThrow(resolvedBinary) {
153
+ if (resolvedBinary.installDir) {
154
+ return resolvedBinary.installDir;
155
+ }
156
+ throw new Error(
157
+ `Configured binaryPath "${resolvedBinary.binaryPath}" is incompatible with BinaryManager-managed path naming. BinaryManager expects "${new BinaryManager(dirname(resolvedBinary.binaryPath)).getBinaryPath()}". Set binaryPath to a standard managed name (fnn/fnn.exe) in the target directory, or unset binaryPath to use the profile-managed binary.`
158
+ );
159
+ }
160
+ async function getBinaryDetails(config) {
161
+ const resolvedBinary = resolveBinaryPath(config);
162
+ const info = resolvedBinary.installDir ? await getFiberBinaryInfo(resolvedBinary.installDir) : getCustomBinaryState(resolvedBinary.binaryPath);
163
+ return { resolvedBinary, info };
164
+ }
165
+
15
166
  // src/lib/format.ts
16
167
  import {
17
168
  ChannelState,
18
169
  shannonsToCkb,
19
170
  toHex
20
171
  } from "@fiber-pay/sdk";
172
+ var SHANNONS_PER_CKB = 100000000n;
21
173
  function truncateMiddle(value, start = 10, end = 8) {
22
174
  if (!value || value.length <= start + end + 3) {
23
175
  return value;
24
176
  }
25
177
  return `${value.slice(0, start)}...${value.slice(-end)}`;
26
178
  }
179
+ function sanitizeForTerminal(value) {
180
+ const input = String(value ?? "");
181
+ let output = "";
182
+ for (let i = 0; i < input.length; i++) {
183
+ const code = input.charCodeAt(i);
184
+ if (code === 27) {
185
+ i++;
186
+ if (i >= input.length) break;
187
+ if (input.charCodeAt(i) === 91) {
188
+ i++;
189
+ while (i < input.length) {
190
+ const csiCode = input.charCodeAt(i);
191
+ if (csiCode >= 64 && csiCode <= 126) {
192
+ break;
193
+ }
194
+ i++;
195
+ }
196
+ }
197
+ continue;
198
+ }
199
+ if (code === 9 || code === 10 || code === 13) {
200
+ output += " ";
201
+ continue;
202
+ }
203
+ if (code >= 0 && code <= 8 || code >= 11 && code <= 31) {
204
+ continue;
205
+ }
206
+ if (code >= 127 && code <= 159) {
207
+ continue;
208
+ }
209
+ output += input[i];
210
+ }
211
+ return output;
212
+ }
213
+ function formatShannonsAsCkb(shannons, fractionDigits = 8) {
214
+ const value = typeof shannons === "bigint" ? shannons : BigInt(shannons);
215
+ const sign = value < 0n ? "-" : "";
216
+ const abs = value < 0n ? -value : value;
217
+ const safeDigits = Math.max(0, Math.min(8, Math.trunc(fractionDigits)));
218
+ const multiplier = 10n ** BigInt(safeDigits);
219
+ const scaled = (abs * multiplier + SHANNONS_PER_CKB / 2n) / SHANNONS_PER_CKB;
220
+ const whole = scaled / multiplier;
221
+ if (safeDigits === 0) {
222
+ return `${sign}${whole}`;
223
+ }
224
+ const fraction = (scaled % multiplier).toString().padStart(safeDigits, "0");
225
+ return `${sign}${whole}.${fraction}`;
226
+ }
27
227
  function parseHexTimestampMs(hexTimestamp) {
28
228
  if (!hexTimestamp) return null;
29
229
  try {
@@ -284,21 +484,6 @@ function printPeerListHuman(peers) {
284
484
  console.log(`${peerId} ${pubkey} ${peer.address}`);
285
485
  }
286
486
  }
287
- function printNodeInfoHuman(data) {
288
- console.log("Node Info");
289
- console.log(` Node ID: ${data.nodeId}`);
290
- console.log(` Version: ${data.version}`);
291
- console.log(` Chain Hash: ${data.chainHash}`);
292
- console.log(` Funding Address: ${data.fundingAddress}`);
293
- console.log(` Channels: ${data.channelCount} (${data.pendingChannelCount} pending)`);
294
- console.log(` Peers: ${data.peersCount}`);
295
- if (data.addresses.length > 0) {
296
- console.log(" Addresses:");
297
- for (const addr of data.addresses) {
298
- console.log(` - ${addr}`);
299
- }
300
- }
301
- }
302
487
 
303
488
  // src/commands/binary.ts
304
489
  function showProgress(progress) {
@@ -311,14 +496,36 @@ function showProgress(progress) {
311
496
  function createBinaryCommand(config) {
312
497
  const binary = new Command("binary").description("Fiber binary management");
313
498
  binary.command("download").option("--version <version>", "Fiber binary version", DEFAULT_FIBER_VERSION).option("--force", "Force re-download").option("--json").action(async (options) => {
499
+ const resolvedBinary = resolveBinaryPath(config);
500
+ let installDir;
501
+ try {
502
+ installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
503
+ } catch (error) {
504
+ const message = error instanceof Error ? error.message : String(error);
505
+ if (options.json) {
506
+ printJsonError({
507
+ code: "BINARY_PATH_INCOMPATIBLE",
508
+ message,
509
+ recoverable: true,
510
+ suggestion: "Use `fiber-pay config profile unset binaryPath` or set binaryPath to a standard fnn filename in the target directory."
511
+ });
512
+ } else {
513
+ console.error(`\u274C ${message}`);
514
+ }
515
+ process.exit(1);
516
+ }
314
517
  const info = await downloadFiberBinary({
315
- installDir: `${config.dataDir}/bin`,
518
+ installDir,
316
519
  version: options.version,
317
520
  force: Boolean(options.force),
318
521
  onProgress: options.json ? void 0 : showProgress
319
522
  });
320
523
  if (options.json) {
321
- printJsonSuccess(info);
524
+ printJsonSuccess({
525
+ ...info,
526
+ source: resolvedBinary.source,
527
+ resolvedPath: resolvedBinary.binaryPath
528
+ });
322
529
  } else {
323
530
  console.log("\n\u2705 Binary installed successfully!");
324
531
  console.log(` Path: ${info.path}`);
@@ -327,9 +534,13 @@ function createBinaryCommand(config) {
327
534
  }
328
535
  });
329
536
  binary.command("info").option("--json").action(async (options) => {
330
- const info = await getFiberBinaryInfo(`${config.dataDir}/bin`);
537
+ const { resolvedBinary, info } = await getBinaryDetails(config);
331
538
  if (options.json) {
332
- printJsonSuccess(info);
539
+ printJsonSuccess({
540
+ ...info,
541
+ source: resolvedBinary.source,
542
+ resolvedPath: resolvedBinary.binaryPath
543
+ });
333
544
  } else {
334
545
  console.log(info.ready ? "\u2705 Binary is ready" : "\u274C Binary not found or not executable");
335
546
  console.log(` Path: ${info.path}`);
@@ -341,7 +552,7 @@ function createBinaryCommand(config) {
341
552
 
342
553
  // src/commands/channel.ts
343
554
  import { randomUUID } from "crypto";
344
- import { ckbToShannons } from "@fiber-pay/sdk";
555
+ import { ckbToShannons as ckbToShannons2 } from "@fiber-pay/sdk";
345
556
  import { Command as Command2 } from "commander";
346
557
 
347
558
  // src/lib/async.ts
@@ -353,17 +564,17 @@ function sleep(ms) {
353
564
  import { FiberRpcClient } from "@fiber-pay/sdk";
354
565
 
355
566
  // src/lib/pid.ts
356
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
357
- import { join } from "path";
567
+ import { existsSync as existsSync2, readFileSync, unlinkSync, writeFileSync } from "fs";
568
+ import { join as join2 } from "path";
358
569
  function getPidFilePath(dataDir) {
359
- return join(dataDir, "fiber.pid");
570
+ return join2(dataDir, "fiber.pid");
360
571
  }
361
572
  function writePidFile(dataDir, pid) {
362
573
  writeFileSync(getPidFilePath(dataDir), String(pid));
363
574
  }
364
575
  function readPidFile(dataDir) {
365
576
  const pidPath = getPidFilePath(dataDir);
366
- if (!existsSync(pidPath)) return null;
577
+ if (!existsSync2(pidPath)) return null;
367
578
  try {
368
579
  return parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
369
580
  } catch {
@@ -372,7 +583,7 @@ function readPidFile(dataDir) {
372
583
  }
373
584
  function removePidFile(dataDir) {
374
585
  const pidPath = getPidFilePath(dataDir);
375
- if (existsSync(pidPath)) {
586
+ if (existsSync2(pidPath)) {
376
587
  unlinkSync(pidPath);
377
588
  }
378
589
  }
@@ -386,20 +597,20 @@ function isProcessRunning(pid) {
386
597
  }
387
598
 
388
599
  // src/lib/runtime-meta.ts
389
- import { existsSync as existsSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
390
- import { join as join2 } from "path";
600
+ import { existsSync as existsSync3, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
601
+ import { join as join3 } from "path";
391
602
  function getRuntimePidFilePath(dataDir) {
392
- return join2(dataDir, "runtime.pid");
603
+ return join3(dataDir, "runtime.pid");
393
604
  }
394
605
  function getRuntimeMetaFilePath(dataDir) {
395
- return join2(dataDir, "runtime.meta.json");
606
+ return join3(dataDir, "runtime.meta.json");
396
607
  }
397
608
  function writeRuntimePid(dataDir, pid) {
398
609
  writeFileSync2(getRuntimePidFilePath(dataDir), String(pid));
399
610
  }
400
611
  function readRuntimePid(dataDir) {
401
612
  const pidPath = getRuntimePidFilePath(dataDir);
402
- if (!existsSync2(pidPath)) return null;
613
+ if (!existsSync3(pidPath)) return null;
403
614
  try {
404
615
  return Number.parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
405
616
  } catch {
@@ -411,7 +622,7 @@ function writeRuntimeMeta(dataDir, meta) {
411
622
  }
412
623
  function readRuntimeMeta(dataDir) {
413
624
  const metaPath = getRuntimeMetaFilePath(dataDir);
414
- if (!existsSync2(metaPath)) return null;
625
+ if (!existsSync3(metaPath)) return null;
415
626
  try {
416
627
  return JSON.parse(readFileSync2(metaPath, "utf-8"));
417
628
  } catch {
@@ -421,10 +632,10 @@ function readRuntimeMeta(dataDir) {
421
632
  function removeRuntimeFiles(dataDir) {
422
633
  const pidPath = getRuntimePidFilePath(dataDir);
423
634
  const metaPath = getRuntimeMetaFilePath(dataDir);
424
- if (existsSync2(pidPath)) {
635
+ if (existsSync3(pidPath)) {
425
636
  unlinkSync2(pidPath);
426
637
  }
427
- if (existsSync2(metaPath)) {
638
+ if (existsSync3(metaPath)) {
428
639
  unlinkSync2(metaPath);
429
640
  }
430
641
  }
@@ -457,7 +668,10 @@ function resolveRuntimeProxyUrl(config) {
457
668
  }
458
669
  function createRpcClient(config) {
459
670
  const resolved = resolveRpcEndpoint(config);
460
- return new FiberRpcClient({ url: resolved.url });
671
+ return new FiberRpcClient({
672
+ url: resolved.url,
673
+ biscuitToken: config.rpcBiscuitToken
674
+ });
461
675
  }
462
676
  function resolveRpcEndpoint(config) {
463
677
  const runtimeProxyUrl = resolveRuntimeProxyUrl(config);
@@ -534,23 +748,252 @@ async function waitForRuntimeJobTerminal(runtimeUrl, jobId, timeoutSeconds) {
534
748
  throw new Error(`Timed out waiting for runtime job ${jobId}`);
535
749
  }
536
750
 
751
+ // src/commands/rebalance.ts
752
+ import { ckbToShannons, shannonsToCkb as shannonsToCkb2 } from "@fiber-pay/sdk";
753
+ async function executeRebalance(config, params) {
754
+ const rpc = await createReadyRpcClient(config);
755
+ const amountCkb = parseFloat(params.amountInput);
756
+ const maxFeeCkb = params.maxFeeInput !== void 0 ? parseFloat(params.maxFeeInput) : void 0;
757
+ const manualHops = params.hops ?? [];
758
+ if (!Number.isFinite(amountCkb) || amountCkb <= 0) {
759
+ const message = "Invalid --amount value. Expected a positive CKB amount.";
760
+ if (params.json) {
761
+ printJsonError({
762
+ code: params.errorCode,
763
+ message,
764
+ recoverable: true,
765
+ suggestion: "Provide a positive number, e.g. `--amount 10`.",
766
+ details: { amount: params.amountInput }
767
+ });
768
+ } else {
769
+ console.error(`Error: ${message}`);
770
+ }
771
+ process.exit(1);
772
+ }
773
+ if (maxFeeCkb !== void 0 && (!Number.isFinite(maxFeeCkb) || maxFeeCkb < 0 || manualHops.length > 0)) {
774
+ const message = manualHops.length > 0 ? "--max-fee is only supported in auto rebalance mode (without manual hops)." : "Invalid --max-fee value. Expected a non-negative CKB amount.";
775
+ if (params.json) {
776
+ printJsonError({
777
+ code: params.errorCode,
778
+ message,
779
+ recoverable: true,
780
+ suggestion: manualHops.length > 0 ? "Remove `--max-fee` or run auto mode without manual hops." : "Provide a non-negative number, e.g. `--max-fee 0.01`.",
781
+ details: { maxFee: params.maxFeeInput, hasManualHops: manualHops.length > 0 }
782
+ });
783
+ } else {
784
+ console.error(`Error: ${message}`);
785
+ }
786
+ process.exit(1);
787
+ }
788
+ const selfPubkey = (await rpc.nodeInfo()).node_id;
789
+ const amount = ckbToShannons(amountCkb);
790
+ const isManual = manualHops.length > 0;
791
+ let routeHopCount;
792
+ const result = isManual ? await (async () => {
793
+ const hopsInfo = [
794
+ ...manualHops.map((pubkey) => ({ pubkey })),
795
+ ...manualHops[manualHops.length - 1] === selfPubkey ? [] : [{ pubkey: selfPubkey }]
796
+ ];
797
+ const route = await rpc.buildRouter({
798
+ amount,
799
+ hops_info: hopsInfo
800
+ });
801
+ routeHopCount = route.router_hops.length;
802
+ return rpc.sendPaymentWithRouter({
803
+ router: route.router_hops,
804
+ keysend: true,
805
+ allow_self_payment: true,
806
+ dry_run: params.dryRun ? true : void 0
807
+ });
808
+ })() : await rpc.sendPayment({
809
+ target_pubkey: selfPubkey,
810
+ amount,
811
+ keysend: true,
812
+ allow_self_payment: true,
813
+ max_fee_amount: maxFeeCkb !== void 0 ? ckbToShannons(maxFeeCkb) : void 0,
814
+ dry_run: params.dryRun ? true : void 0
815
+ });
816
+ const payload = {
817
+ mode: isManual ? "manual" : "auto",
818
+ selfPubkey,
819
+ amountCkb,
820
+ maxFeeCkb: isManual ? void 0 : maxFeeCkb,
821
+ routeHopCount,
822
+ paymentHash: result.payment_hash,
823
+ status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
824
+ feeCkb: shannonsToCkb2(result.fee),
825
+ failureReason: result.failed_error,
826
+ dryRun: params.dryRun
827
+ };
828
+ if (params.json) {
829
+ printJsonSuccess(payload);
830
+ } else {
831
+ console.log(
832
+ payload.dryRun ? `Rebalance dry-run complete (${payload.mode} route)` : `Rebalance sent (${payload.mode} route)`
833
+ );
834
+ console.log(` Self: ${payload.selfPubkey}`);
835
+ console.log(` Amount: ${payload.amountCkb} CKB`);
836
+ if (payload.mode === "manual" && payload.routeHopCount !== void 0) {
837
+ console.log(` Hops: ${payload.routeHopCount}`);
838
+ }
839
+ console.log(` Hash: ${payload.paymentHash}`);
840
+ console.log(` Status: ${payload.status}`);
841
+ console.log(` Fee: ${payload.feeCkb} CKB`);
842
+ if (payload.mode === "auto" && payload.maxFeeCkb !== void 0) {
843
+ console.log(` MaxFee: ${payload.maxFeeCkb} CKB`);
844
+ }
845
+ if (payload.failureReason) {
846
+ console.log(` Error: ${payload.failureReason}`);
847
+ }
848
+ }
849
+ }
850
+ function registerPaymentRebalanceCommand(parent, config) {
851
+ parent.command("rebalance").description("Technical rebalance command mapped to payment-layer circular self-payment").requiredOption("--amount <ckb>", "Amount in CKB to rebalance").option("--max-fee <ckb>", "Maximum fee in CKB (auto mode only)").option(
852
+ "--hops <pubkeys>",
853
+ "Comma-separated peer pubkeys for manual route mode (self pubkey appended automatically)"
854
+ ).option("--dry-run", "Simulate route/payment and return estimated result").option("--json").action(async (options) => {
855
+ const hasHopsOption = typeof options.hops === "string";
856
+ const manualHops = hasHopsOption ? options.hops.split(",").map((item) => item.trim()).filter(Boolean) : [];
857
+ if (hasHopsOption && manualHops.length === 0) {
858
+ const message = "Invalid --hops value. Expected a non-empty comma-separated list of pubkeys.";
859
+ if (options.json) {
860
+ printJsonError({
861
+ code: "PAYMENT_REBALANCE_INPUT_INVALID",
862
+ message,
863
+ recoverable: true,
864
+ suggestion: "Provide pubkeys like `--hops 0xabc...,0xdef...`.",
865
+ details: { hops: options.hops }
866
+ });
867
+ } else {
868
+ console.error(`Error: ${message}`);
869
+ }
870
+ process.exit(1);
871
+ }
872
+ await executeRebalance(config, {
873
+ amountInput: options.amount,
874
+ maxFeeInput: options.maxFee,
875
+ hops: manualHops,
876
+ dryRun: Boolean(options.dryRun),
877
+ json: Boolean(options.json),
878
+ errorCode: "PAYMENT_REBALANCE_INPUT_INVALID"
879
+ });
880
+ });
881
+ }
882
+ function registerChannelRebalanceCommand(parent, config) {
883
+ parent.command("rebalance").description("High-level channel rebalance wrapper using payment-layer orchestration").requiredOption("--amount <ckb>", "Amount in CKB to rebalance").option("--from-channel <channelId>", "Source-biased channel id (optional)").option("--to-channel <channelId>", "Destination-biased channel id (optional)").option("--max-fee <ckb>", "Maximum fee in CKB (auto mode only)").option("--dry-run", "Simulate route/payment and return estimated result").option("--json").action(async (options) => {
884
+ const json = Boolean(options.json);
885
+ const fromChannelId = options.fromChannel;
886
+ const toChannelId = options.toChannel;
887
+ if (fromChannelId && !toChannelId || !fromChannelId && toChannelId) {
888
+ const message = "Both --from-channel and --to-channel must be provided together for guided channel rebalance.";
889
+ if (json) {
890
+ printJsonError({
891
+ code: "CHANNEL_REBALANCE_INPUT_INVALID",
892
+ message,
893
+ recoverable: true,
894
+ suggestion: "Provide both channel ids, or provide neither to run auto mode.",
895
+ details: { fromChannel: fromChannelId, toChannel: toChannelId }
896
+ });
897
+ } else {
898
+ console.error(`Error: ${message}`);
899
+ }
900
+ process.exit(1);
901
+ }
902
+ let guidedHops;
903
+ if (fromChannelId && toChannelId) {
904
+ const rpc = await createReadyRpcClient(config);
905
+ const channels = (await rpc.listChannels({ include_closed: true })).channels;
906
+ const fromChannel = channels.find((item) => item.channel_id === fromChannelId);
907
+ const toChannel = channels.find((item) => item.channel_id === toChannelId);
908
+ if (!fromChannel || !toChannel) {
909
+ const message = "Invalid channel selection: source/target channel id not found.";
910
+ if (json) {
911
+ printJsonError({
912
+ code: "CHANNEL_REBALANCE_INPUT_INVALID",
913
+ message,
914
+ recoverable: true,
915
+ suggestion: "Run `channel list --json` and retry with valid channel ids.",
916
+ details: { fromChannel: fromChannelId, toChannel: toChannelId }
917
+ });
918
+ } else {
919
+ console.error(`Error: ${message}`);
920
+ }
921
+ process.exit(1);
922
+ }
923
+ if (fromChannel.peer_id === toChannel.peer_id) {
924
+ const message = "Source and target channels point to the same peer; choose two different channel peers.";
925
+ if (json) {
926
+ printJsonError({
927
+ code: "CHANNEL_REBALANCE_INPUT_INVALID",
928
+ message,
929
+ recoverable: true,
930
+ suggestion: "Select channels with different peer ids for guided rebalance.",
931
+ details: {
932
+ fromChannel: fromChannelId,
933
+ toChannel: toChannelId,
934
+ peerId: fromChannel.peer_id
935
+ }
936
+ });
937
+ } else {
938
+ console.error(`Error: ${message}`);
939
+ }
940
+ process.exit(1);
941
+ }
942
+ const peers = (await rpc.listPeers()).peers;
943
+ const pubkeyByPeerId = new Map(peers.map((peer) => [peer.peer_id, peer.pubkey]));
944
+ const fromPubkey = pubkeyByPeerId.get(fromChannel.peer_id);
945
+ const toPubkey = pubkeyByPeerId.get(toChannel.peer_id);
946
+ if (!fromPubkey || !toPubkey) {
947
+ const message = "Unable to resolve selected channel peer_id to pubkey for guided rebalance route.";
948
+ if (json) {
949
+ printJsonError({
950
+ code: "CHANNEL_REBALANCE_INPUT_INVALID",
951
+ message,
952
+ recoverable: true,
953
+ suggestion: "Ensure both peers are connected (`peer list --json`) and retry guided mode, or use `payment rebalance --hops`.",
954
+ details: {
955
+ fromChannel: fromChannelId,
956
+ toChannel: toChannelId,
957
+ fromPeerId: fromChannel.peer_id,
958
+ toPeerId: toChannel.peer_id,
959
+ resolvedPeers: peers.length
960
+ }
961
+ });
962
+ } else {
963
+ console.error(`Error: ${message}`);
964
+ }
965
+ process.exit(1);
966
+ }
967
+ guidedHops = [fromPubkey, toPubkey];
968
+ }
969
+ await executeRebalance(config, {
970
+ amountInput: options.amount,
971
+ maxFeeInput: options.maxFee,
972
+ hops: guidedHops,
973
+ dryRun: Boolean(options.dryRun),
974
+ json,
975
+ errorCode: "CHANNEL_REBALANCE_INPUT_INVALID"
976
+ });
977
+ });
978
+ }
979
+
537
980
  // src/commands/channel.ts
538
981
  function createChannelCommand(config) {
539
982
  const channel = new Command2("channel").description("Channel lifecycle and status commands");
540
- channel.command("list").option("--state <state>").option("--peer <peerId>").option("--include-closed").option("--raw").option("--json").action(async (options) => {
983
+ channel.command("list").option("--state <state>").option("--peer <peerId>").option("--include-closed").option("--json").action(async (options) => {
541
984
  const rpc = await createReadyRpcClient(config);
542
985
  const stateFilter = parseChannelState(options.state);
543
986
  const response = await rpc.listChannels(
544
987
  options.peer ? { peer_id: options.peer, include_closed: Boolean(options.includeClosed) } : { include_closed: Boolean(options.includeClosed) }
545
988
  );
546
989
  const channels = stateFilter ? response.channels.filter((item) => item.state.state_name === stateFilter) : response.channels;
547
- if (options.raw || options.json) {
990
+ if (options.json) {
548
991
  printJsonSuccess({ channels, count: channels.length });
549
992
  } else {
550
993
  printChannelListHuman(channels);
551
994
  }
552
995
  });
553
- channel.command("get").argument("<channelId>").option("--raw").option("--json").action(async (channelId, options) => {
996
+ channel.command("get").argument("<channelId>").option("--json").action(async (channelId, options) => {
554
997
  const rpc = await createReadyRpcClient(config);
555
998
  const response = await rpc.listChannels({ include_closed: true });
556
999
  const found = response.channels.find((item) => item.channel_id === channelId);
@@ -568,7 +1011,7 @@ function createChannelCommand(config) {
568
1011
  }
569
1012
  process.exit(1);
570
1013
  }
571
- if (options.raw || options.json) {
1014
+ if (options.json) {
572
1015
  printJsonSuccess(found);
573
1016
  } else {
574
1017
  printChannelDetailHuman(found);
@@ -708,7 +1151,7 @@ function createChannelCommand(config) {
708
1151
  peerId,
709
1152
  openChannelParams: {
710
1153
  peer_id: peerId,
711
- funding_amount: ckbToShannons(fundingCkb),
1154
+ funding_amount: ckbToShannons2(fundingCkb),
712
1155
  public: !options.private
713
1156
  },
714
1157
  waitForReady: false
@@ -741,7 +1184,7 @@ function createChannelCommand(config) {
741
1184
  }
742
1185
  const result = await rpc.openChannel({
743
1186
  peer_id: peerId,
744
- funding_amount: ckbToShannons(fundingCkb),
1187
+ funding_amount: ckbToShannons2(fundingCkb),
745
1188
  public: !options.private
746
1189
  });
747
1190
  const payload = { temporaryChannelId: result.temporary_channel_id, peer: peerId, fundingCkb };
@@ -765,7 +1208,7 @@ function createChannelCommand(config) {
765
1208
  action: "accept",
766
1209
  acceptChannelParams: {
767
1210
  temporary_channel_id: temporaryChannelId,
768
- funding_amount: ckbToShannons(fundingCkb)
1211
+ funding_amount: ckbToShannons2(fundingCkb)
769
1212
  }
770
1213
  },
771
1214
  options: {
@@ -793,7 +1236,7 @@ function createChannelCommand(config) {
793
1236
  }
794
1237
  const result = await rpc.acceptChannel({
795
1238
  temporary_channel_id: temporaryChannelId,
796
- funding_amount: ckbToShannons(fundingCkb)
1239
+ funding_amount: ckbToShannons2(fundingCkb)
797
1240
  });
798
1241
  const payload = { channelId: result.channel_id, temporaryChannelId, fundingCkb };
799
1242
  if (json) {
@@ -818,7 +1261,7 @@ function createChannelCommand(config) {
818
1261
  channel_id: channelId,
819
1262
  force: Boolean(options.force)
820
1263
  },
821
- waitForClosed: false
1264
+ waitForClosed: Boolean(options.force)
822
1265
  },
823
1266
  options: {
824
1267
  idempotencyKey: `shutdown:channel:${channelId}`
@@ -903,6 +1346,7 @@ function createChannelCommand(config) {
903
1346
  console.log(` Channel ID: ${payload.channelId}`);
904
1347
  }
905
1348
  });
1349
+ registerChannelRebalanceCommand(channel, config);
906
1350
  channel.command("update").argument("<channelId>").option("--enabled <enabled>").option("--tlc-expiry-delta <ms>").option("--tlc-minimum-value <shannonsHex>").option("--tlc-fee-proportional-millionths <value>").option("--json").action(async (channelId, options) => {
907
1351
  const rpc = await createReadyRpcClient(config);
908
1352
  const json = Boolean(options.json);
@@ -956,13 +1400,13 @@ function createChannelCommand(config) {
956
1400
  }
957
1401
 
958
1402
  // src/commands/config.ts
959
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
1403
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
960
1404
  import { Command as Command3 } from "commander";
961
1405
  import { parseDocument, stringify as yamlStringify } from "yaml";
962
1406
 
963
1407
  // src/lib/config.ts
964
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
965
- import { join as join3 } from "path";
1408
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1409
+ import { join as join4 } from "path";
966
1410
 
967
1411
  // src/lib/config-templates.ts
968
1412
  var TESTNET_CONFIG_TEMPLATE_V071 = `# This configuration file only contains the necessary configurations for the testnet deployment.
@@ -1121,7 +1565,7 @@ var DEFAULT_DATA_DIR = `${process.env.HOME}/.fiber-pay`;
1121
1565
  var DEFAULT_RPC_URL = "http://127.0.0.1:8227";
1122
1566
  var DEFAULT_NETWORK = "testnet";
1123
1567
  function getConfigPath(dataDir) {
1124
- return join3(dataDir, "config.yml");
1568
+ return join4(dataDir, "config.yml");
1125
1569
  }
1126
1570
  function parseNetworkFromConfig(configContent) {
1127
1571
  const match = configContent.match(/^\s*chain:\s*(testnet|mainnet)\s*$/m);
@@ -1153,11 +1597,11 @@ function parseCkbRpcUrlFromConfig(configContent) {
1153
1597
  return match?.[1]?.trim() || void 0;
1154
1598
  }
1155
1599
  function getProfilePath(dataDir) {
1156
- return join3(dataDir, "profile.json");
1600
+ return join4(dataDir, "profile.json");
1157
1601
  }
1158
1602
  function loadProfileConfig(dataDir) {
1159
1603
  const profilePath = getProfilePath(dataDir);
1160
- if (!existsSync3(profilePath)) return void 0;
1604
+ if (!existsSync4(profilePath)) return void 0;
1161
1605
  try {
1162
1606
  const raw = readFileSync3(profilePath, "utf-8");
1163
1607
  return JSON.parse(raw);
@@ -1166,7 +1610,7 @@ function loadProfileConfig(dataDir) {
1166
1610
  }
1167
1611
  }
1168
1612
  function saveProfileConfig(dataDir, profile) {
1169
- if (!existsSync3(dataDir)) {
1613
+ if (!existsSync4(dataDir)) {
1170
1614
  mkdirSync(dataDir, { recursive: true });
1171
1615
  }
1172
1616
  const profilePath = getProfilePath(dataDir);
@@ -1175,11 +1619,11 @@ function saveProfileConfig(dataDir, profile) {
1175
1619
  }
1176
1620
  function writeNetworkConfigFile(dataDir, network, options = {}) {
1177
1621
  const configPath = getConfigPath(dataDir);
1178
- const alreadyExists = existsSync3(configPath);
1622
+ const alreadyExists = existsSync4(configPath);
1179
1623
  if (alreadyExists && !options.force) {
1180
1624
  return { path: configPath, created: false, overwritten: false };
1181
1625
  }
1182
- if (!existsSync3(dataDir)) {
1626
+ if (!existsSync4(dataDir)) {
1183
1627
  mkdirSync(dataDir, { recursive: true });
1184
1628
  }
1185
1629
  let content = getConfigTemplate(network);
@@ -1205,7 +1649,7 @@ function writeNetworkConfigFile(dataDir, network, options = {}) {
1205
1649
  }
1206
1650
  function ensureNodeConfigFile(dataDir, network) {
1207
1651
  const configPath = getConfigPath(dataDir);
1208
- if (!existsSync3(configPath)) {
1652
+ if (!existsSync4(configPath)) {
1209
1653
  writeNetworkConfigFile(dataDir, network);
1210
1654
  }
1211
1655
  return configPath;
@@ -1214,7 +1658,7 @@ function getEffectiveConfig(explicitFlags2) {
1214
1658
  const dataDir = process.env.FIBER_DATA_DIR || DEFAULT_DATA_DIR;
1215
1659
  const dataDirSource = explicitFlags2?.has("dataDir") ? "cli" : process.env.FIBER_DATA_DIR ? "env" : "default";
1216
1660
  const configPath = getConfigPath(dataDir);
1217
- const configExists = existsSync3(configPath);
1661
+ const configExists = existsSync4(configPath);
1218
1662
  const configContent = configExists ? readFileSync3(configPath, "utf-8") : void 0;
1219
1663
  const profile = loadProfileConfig(dataDir);
1220
1664
  const cliNetwork = explicitFlags2?.has("network") ? process.env.FIBER_NETWORK : void 0;
@@ -1227,6 +1671,10 @@ function getEffectiveConfig(explicitFlags2) {
1227
1671
  const fileRpcUrl = configContent ? parseRpcUrlFromConfig(configContent) : void 0;
1228
1672
  const rpcUrl = cliRpcUrl || envRpcUrl || fileRpcUrl || DEFAULT_RPC_URL;
1229
1673
  const rpcUrlSource = cliRpcUrl ? "cli" : envRpcUrl ? "env" : fileRpcUrl ? "config" : "default";
1674
+ const cliRpcBiscuitToken = explicitFlags2?.has("rpcBiscuitToken") ? process.env.FIBER_RPC_BISCUIT_TOKEN : void 0;
1675
+ const envRpcBiscuitToken = !explicitFlags2?.has("rpcBiscuitToken") ? process.env.FIBER_RPC_BISCUIT_TOKEN : void 0;
1676
+ const rpcBiscuitToken = cliRpcBiscuitToken || envRpcBiscuitToken || void 0;
1677
+ const rpcBiscuitTokenSource = cliRpcBiscuitToken ? "cli" : envRpcBiscuitToken ? "env" : "unset";
1230
1678
  const cliBinaryPath = explicitFlags2?.has("binaryPath") ? process.env.FIBER_BINARY_PATH : void 0;
1231
1679
  const profileBinaryPath = profile?.binaryPath;
1232
1680
  const envBinaryPath = !explicitFlags2?.has("binaryPath") ? process.env.FIBER_BINARY_PATH : void 0;
@@ -1253,6 +1701,7 @@ function getEffectiveConfig(explicitFlags2) {
1253
1701
  configPath,
1254
1702
  network,
1255
1703
  rpcUrl,
1704
+ rpcBiscuitToken,
1256
1705
  keyPassword,
1257
1706
  ckbRpcUrl,
1258
1707
  runtimeProxyListen
@@ -1262,6 +1711,7 @@ function getEffectiveConfig(explicitFlags2) {
1262
1711
  configPath: "derived",
1263
1712
  network: networkSource,
1264
1713
  rpcUrl: rpcUrlSource,
1714
+ rpcBiscuitToken: rpcBiscuitTokenSource,
1265
1715
  ckbRpcUrl: ckbRpcUrlSource,
1266
1716
  runtimeProxyListen: runtimeProxyListenSource
1267
1717
  }
@@ -1356,7 +1806,7 @@ function parseTypedValue(raw, valueType) {
1356
1806
  return raw;
1357
1807
  }
1358
1808
  function ensureConfigFileOrExit(configPath, json) {
1359
- if (!existsSync4(configPath)) {
1809
+ if (!existsSync5(configPath)) {
1360
1810
  const msg = `Config file not found: ${configPath}. Run \`fiber-pay config init\` first.`;
1361
1811
  if (json) {
1362
1812
  printJsonError({
@@ -1728,7 +2178,7 @@ function createConfigCommand(_config) {
1728
2178
  }
1729
2179
 
1730
2180
  // src/commands/graph.ts
1731
- import { shannonsToCkb as shannonsToCkb2, toHex as toHex2 } from "@fiber-pay/sdk";
2181
+ import { shannonsToCkb as shannonsToCkb3, toHex as toHex2 } from "@fiber-pay/sdk";
1732
2182
  import { Command as Command4 } from "commander";
1733
2183
  function printGraphNodeListHuman(nodes) {
1734
2184
  if (nodes.length === 0) {
@@ -1743,7 +2193,7 @@ function printGraphNodeListHuman(nodes) {
1743
2193
  const nodeId = truncateMiddle(node.node_id, 10, 8).padEnd(22, " ");
1744
2194
  const alias = (node.node_name || "(unnamed)").slice(0, 20).padEnd(20, " ");
1745
2195
  const version = (node.version || "?").slice(0, 10).padEnd(10, " ");
1746
- const minFunding = shannonsToCkb2(node.auto_accept_min_ckb_funding_amount).toString().padStart(12, " ");
2196
+ const minFunding = shannonsToCkb3(node.auto_accept_min_ckb_funding_amount).toString().padStart(12, " ");
1747
2197
  const age = formatAge(parseHexTimestampMs(node.timestamp));
1748
2198
  console.log(`${nodeId} ${alias} ${version} ${minFunding} ${age}`);
1749
2199
  }
@@ -1765,7 +2215,7 @@ function printGraphChannelListHuman(channels) {
1765
2215
  const outpoint = ch.channel_outpoint ? truncateMiddle(`${ch.channel_outpoint.tx_hash}:${ch.channel_outpoint.index}`, 10, 8) : "n/a";
1766
2216
  const n1 = truncateMiddle(ch.node1, 10, 8).padEnd(22, " ");
1767
2217
  const n2 = truncateMiddle(ch.node2, 10, 8).padEnd(22, " ");
1768
- const capacity = `${shannonsToCkb2(ch.capacity)} CKB`.padStart(12, " ");
2218
+ const capacity = `${shannonsToCkb3(ch.capacity)} CKB`.padStart(12, " ");
1769
2219
  const age = formatAge(parseHexTimestampMs(ch.created_timestamp));
1770
2220
  console.log(`${outpoint.padEnd(22, " ")} ${n1} ${n2} ${capacity} ${age}`);
1771
2221
  }
@@ -1810,7 +2260,7 @@ Next cursor: ${result.last_cursor}`);
1810
2260
  }
1811
2261
 
1812
2262
  // src/commands/invoice.ts
1813
- import { ckbToShannons as ckbToShannons2, randomBytes32, shannonsToCkb as shannonsToCkb3, toHex as toHex3 } from "@fiber-pay/sdk";
2263
+ import { ckbToShannons as ckbToShannons3, randomBytes32, shannonsToCkb as shannonsToCkb4, toHex as toHex3 } from "@fiber-pay/sdk";
1814
2264
  import { Command as Command5 } from "commander";
1815
2265
  function createInvoiceCommand(config) {
1816
2266
  const invoice = new Command5("invoice").description("Invoice lifecycle and status commands");
@@ -1839,7 +2289,7 @@ function createInvoiceCommand(config) {
1839
2289
  params: {
1840
2290
  action: "create",
1841
2291
  newInvoiceParams: {
1842
- amount: ckbToShannons2(amountCkb),
2292
+ amount: ckbToShannons3(amountCkb),
1843
2293
  currency,
1844
2294
  description: options.description,
1845
2295
  expiry: toHex3(expirySeconds),
@@ -1876,7 +2326,7 @@ function createInvoiceCommand(config) {
1876
2326
  }
1877
2327
  }
1878
2328
  const result = await rpc.newInvoice({
1879
- amount: ckbToShannons2(amountCkb),
2329
+ amount: ckbToShannons3(amountCkb),
1880
2330
  currency,
1881
2331
  description: options.description,
1882
2332
  expiry: toHex3(expirySeconds),
@@ -1908,7 +2358,7 @@ function createInvoiceCommand(config) {
1908
2358
  paymentHash,
1909
2359
  status: result.status,
1910
2360
  invoice: result.invoice_address,
1911
- amountCkb: result.invoice.amount ? shannonsToCkb3(result.invoice.amount) : void 0,
2361
+ amountCkb: result.invoice.amount ? shannonsToCkb4(result.invoice.amount) : void 0,
1912
2362
  currency: result.invoice.currency,
1913
2363
  description: metadata.description,
1914
2364
  createdAt: createdAtMs ? new Date(createdAtMs).toISOString() : result.invoice.data.timestamp,
@@ -2028,19 +2478,104 @@ function createInvoiceCommand(config) {
2028
2478
  }
2029
2479
 
2030
2480
  // src/commands/job.ts
2031
- import { existsSync as existsSync6 } from "fs";
2481
+ import { existsSync as existsSync7 } from "fs";
2032
2482
  import { Command as Command6 } from "commander";
2033
2483
 
2034
2484
  // src/lib/log-files.ts
2035
- import { closeSync, createReadStream, existsSync as existsSync5, openSync, readSync, statSync } from "fs";
2036
- import { join as join4 } from "path";
2037
- function resolvePersistedLogPaths(dataDir, meta) {
2485
+ import {
2486
+ appendFileSync,
2487
+ closeSync,
2488
+ createReadStream,
2489
+ existsSync as existsSync6,
2490
+ mkdirSync as mkdirSync2,
2491
+ openSync,
2492
+ readdirSync,
2493
+ readSync,
2494
+ statSync
2495
+ } from "fs";
2496
+ import { join as join5 } from "path";
2497
+ var DATE_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
2498
+ function todayDateString() {
2499
+ const now = /* @__PURE__ */ new Date();
2500
+ const y = now.getUTCFullYear();
2501
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
2502
+ const d = String(now.getUTCDate()).padStart(2, "0");
2503
+ return `${y}-${m}-${d}`;
2504
+ }
2505
+ function validateLogDate(date) {
2506
+ const value = date.trim();
2507
+ if (!DATE_DIR_PATTERN.test(value)) {
2508
+ throw new Error(`Invalid date '${value}'. Expected format YYYY-MM-DD.`);
2509
+ }
2510
+ if (value.includes("/") || value.includes("\\") || value.includes("..")) {
2511
+ throw new Error(`Invalid date '${value}'. Path separators or '..' are not allowed.`);
2512
+ }
2513
+ return value;
2514
+ }
2515
+ function resolveLogDirForDate(dataDir, date) {
2516
+ return resolveLogDirForDateWithOptions(dataDir, date, {});
2517
+ }
2518
+ function resolveLogDirForDateWithOptions(dataDir, date, options) {
2519
+ const dateStr = date ?? todayDateString();
2520
+ const logsBaseDir = options.logsBaseDir ?? join5(dataDir, "logs");
2521
+ if (date !== void 0) {
2522
+ validateLogDate(dateStr);
2523
+ }
2524
+ const dir = join5(logsBaseDir, dateStr);
2525
+ const ensureExists = options.ensureExists ?? true;
2526
+ if (ensureExists) {
2527
+ mkdirSync2(dir, { recursive: true });
2528
+ }
2529
+ return dir;
2530
+ }
2531
+ function resolvePersistedLogPaths(dataDir, meta, date) {
2532
+ const logsBaseDir = meta?.logsBaseDir ?? join5(dataDir, "logs");
2533
+ if (date) {
2534
+ const dir2 = resolveLogDirForDateWithOptions(dataDir, date, {
2535
+ logsBaseDir,
2536
+ ensureExists: false
2537
+ });
2538
+ return {
2539
+ runtimeAlerts: join5(dir2, "runtime.alerts.jsonl"),
2540
+ fnnStdout: join5(dir2, "fnn.stdout.log"),
2541
+ fnnStderr: join5(dir2, "fnn.stderr.log")
2542
+ };
2543
+ }
2544
+ if (meta?.alertLogFilePath || meta?.fnnStdoutLogPath || meta?.fnnStderrLogPath) {
2545
+ const defaultDir = resolveLogDirForDateWithOptions(dataDir, void 0, {
2546
+ logsBaseDir,
2547
+ ensureExists: false
2548
+ });
2549
+ return {
2550
+ runtimeAlerts: meta.alertLogFilePath ?? join5(defaultDir, "runtime.alerts.jsonl"),
2551
+ fnnStdout: meta.fnnStdoutLogPath ?? join5(defaultDir, "fnn.stdout.log"),
2552
+ fnnStderr: meta.fnnStderrLogPath ?? join5(defaultDir, "fnn.stderr.log")
2553
+ };
2554
+ }
2555
+ const dir = resolveLogDirForDateWithOptions(dataDir, void 0, {
2556
+ logsBaseDir,
2557
+ ensureExists: false
2558
+ });
2038
2559
  return {
2039
- runtimeAlerts: meta?.alertLogFilePath ?? join4(dataDir, "logs", "runtime.alerts.jsonl"),
2040
- fnnStdout: meta?.fnnStdoutLogPath ?? join4(dataDir, "logs", "fnn.stdout.log"),
2041
- fnnStderr: meta?.fnnStderrLogPath ?? join4(dataDir, "logs", "fnn.stderr.log")
2560
+ runtimeAlerts: join5(dir, "runtime.alerts.jsonl"),
2561
+ fnnStdout: join5(dir, "fnn.stdout.log"),
2562
+ fnnStderr: join5(dir, "fnn.stderr.log")
2042
2563
  };
2043
2564
  }
2565
+ function listLogDates(dataDir, logsBaseDir) {
2566
+ const logsDir = logsBaseDir ?? join5(dataDir, "logs");
2567
+ if (!existsSync6(logsDir)) {
2568
+ return [];
2569
+ }
2570
+ const entries = readdirSync(logsDir, { withFileTypes: true });
2571
+ const dates = entries.filter((entry) => entry.isDirectory() && DATE_DIR_PATTERN.test(entry.name)).map((entry) => entry.name);
2572
+ dates.sort((a, b) => a > b ? -1 : a < b ? 1 : 0);
2573
+ return dates;
2574
+ }
2575
+ function appendToTodayLog(dataDir, filename, text) {
2576
+ const dir = resolveLogDirForDate(dataDir);
2577
+ appendFileSync(join5(dir, filename), text, "utf-8");
2578
+ }
2044
2579
  function resolvePersistedLogTargets(paths, source) {
2045
2580
  const all = [
2046
2581
  {
@@ -2065,7 +2600,7 @@ function resolvePersistedLogTargets(paths, source) {
2065
2600
  return all.filter((target) => target.source === source);
2066
2601
  }
2067
2602
  function readLastLines(filePath, maxLines) {
2068
- if (!existsSync5(filePath)) {
2603
+ if (!existsSync6(filePath)) {
2069
2604
  return [];
2070
2605
  }
2071
2606
  if (!Number.isFinite(maxLines) || maxLines <= 0) {
@@ -2109,7 +2644,7 @@ function readLastLines(filePath, maxLines) {
2109
2644
  }
2110
2645
  }
2111
2646
  async function readAppendedLines(filePath, offset, remainder = "") {
2112
- if (!existsSync5(filePath)) {
2647
+ if (!existsSync6(filePath)) {
2113
2648
  return { lines: [], nextOffset: 0, remainder: "" };
2114
2649
  }
2115
2650
  const size = statSync(filePath).size;
@@ -2209,10 +2744,11 @@ function createJobCommand(config) {
2209
2744
  console.log(` ${JSON.stringify(payload.result)}`);
2210
2745
  }
2211
2746
  });
2212
- job.command("trace").argument("<jobId>").option("--tail <n>", "Max lines to inspect per log file", "400").option("--json").action(async (jobId, options) => {
2747
+ job.command("trace").argument("<jobId>").option("--tail <n>", "Max lines to inspect per log file", "400").option("--date <YYYY-MM-DD>", "Date of log directory to search (default: today UTC)").option("--json").action(async (jobId, options) => {
2213
2748
  const json = Boolean(options.json);
2214
2749
  const tailInput = Number.parseInt(String(options.tail ?? "400"), 10);
2215
2750
  const tail = Number.isFinite(tailInput) && tailInput > 0 ? tailInput : 400;
2751
+ const date = options.date ? String(options.date).trim() : void 0;
2216
2752
  const runtimeUrl = getRuntimeUrlOrExit(config, json);
2217
2753
  const jobResponse = await fetch(`${runtimeUrl}/jobs/${jobId}`);
2218
2754
  if (!jobResponse.ok) {
@@ -2226,7 +2762,24 @@ function createJobCommand(config) {
2226
2762
  const eventsPayload = await eventsResponse.json();
2227
2763
  const tokens = collectTraceTokens(jobRecord, eventsPayload.events);
2228
2764
  const meta = readRuntimeMeta(config.dataDir);
2229
- const logPaths = resolvePersistedLogPaths(config.dataDir, meta);
2765
+ let logPaths;
2766
+ try {
2767
+ logPaths = resolvePersistedLogPaths(config.dataDir, meta, date);
2768
+ } catch (error) {
2769
+ const message = error instanceof Error ? error.message : "Invalid --date value.";
2770
+ if (json) {
2771
+ printJsonError({
2772
+ code: "JOB_TRACE_DATE_INVALID",
2773
+ message,
2774
+ recoverable: true,
2775
+ suggestion: "Retry with --date in YYYY-MM-DD format.",
2776
+ details: { date }
2777
+ });
2778
+ } else {
2779
+ console.error(`Error: ${message}`);
2780
+ }
2781
+ process.exit(1);
2782
+ }
2230
2783
  const runtimeAlertMatches = collectRelatedLines(logPaths.runtimeAlerts, tokens, tail);
2231
2784
  const fnnStdoutMatches = collectRelatedLines(logPaths.fnnStdout, tokens, tail);
2232
2785
  const fnnStderrMatches = collectRelatedLines(logPaths.fnnStderr, tokens, tail);
@@ -2429,7 +2982,7 @@ function collectStructuredTokens(set, input, depth = 0) {
2429
2982
  }
2430
2983
  }
2431
2984
  function collectRelatedLines(filePath, tokens, tail) {
2432
- if (!existsSync6(filePath)) {
2985
+ if (!existsSync7(filePath)) {
2433
2986
  return [];
2434
2987
  }
2435
2988
  const lines = readLastLines(filePath, tail);
@@ -2445,7 +2998,7 @@ function collectRelatedLines(filePath, tokens, tail) {
2445
2998
  function printTraceSection(title, filePath, lines) {
2446
2999
  console.log(`
2447
3000
  ${title}: ${filePath}`);
2448
- if (!existsSync6(filePath)) {
3001
+ if (!existsSync7(filePath)) {
2449
3002
  console.log(" (file not found)");
2450
3003
  return;
2451
3004
  }
@@ -2459,7 +3012,8 @@ ${title}: ${filePath}`);
2459
3012
  }
2460
3013
 
2461
3014
  // src/commands/logs.ts
2462
- import { existsSync as existsSync7, statSync as statSync2 } from "fs";
3015
+ import { existsSync as existsSync8, statSync as statSync2 } from "fs";
3016
+ import { join as join6 } from "path";
2463
3017
  import { formatRuntimeAlert } from "@fiber-pay/runtime";
2464
3018
  import { Command as Command7 } from "commander";
2465
3019
  var ALLOWED_SOURCES = /* @__PURE__ */ new Set([
@@ -2468,6 +3022,7 @@ var ALLOWED_SOURCES = /* @__PURE__ */ new Set([
2468
3022
  "fnn-stdout",
2469
3023
  "fnn-stderr"
2470
3024
  ]);
3025
+ var DATE_DIR_PATTERN2 = /^\d{4}-\d{2}-\d{2}$/;
2471
3026
  function parseRuntimeAlertLine(line) {
2472
3027
  try {
2473
3028
  const parsed = JSON.parse(line);
@@ -2493,9 +3048,45 @@ function coerceJsonLineForOutput(source, line) {
2493
3048
  return parseRuntimeAlertLine(line) ?? line;
2494
3049
  }
2495
3050
  function createLogsCommand(config) {
2496
- return new Command7("logs").alias("log").description("View persisted runtime/fnn logs").option("--source <source>", "Log source: all|runtime|fnn-stdout|fnn-stderr", "all").option("--tail <n>", "Number of recent lines per source", "80").option("--follow", "Keep streaming appended log lines (human output mode only)").option("--interval-ms <ms>", "Polling interval for --follow mode", "1000").option("--json").action(async (options) => {
3051
+ return new Command7("logs").alias("log").description("View persisted runtime/fnn logs").option("--source <source>", "Log source: all|runtime|fnn-stdout|fnn-stderr", "all").option("--tail <n>", "Number of recent lines per source", "80").option("--date <YYYY-MM-DD>", "Date of log directory to read (default: today UTC)").option("--list-dates", "List available log dates and exit").option("--follow", "Keep streaming appended log lines (human output mode only)").option("--interval-ms <ms>", "Polling interval for --follow mode", "1000").option("--json").action(async (options) => {
2497
3052
  const json = Boolean(options.json);
2498
3053
  const follow = Boolean(options.follow);
3054
+ const listDates = Boolean(options.listDates);
3055
+ const date = options.date ? String(options.date).trim() : void 0;
3056
+ const meta = readRuntimeMeta(config.dataDir);
3057
+ if (follow && date) {
3058
+ const message = "--follow cannot be used with --date. --follow only streams today's logs.";
3059
+ if (json) {
3060
+ printJsonError({
3061
+ code: "LOG_FOLLOW_DATE_UNSUPPORTED",
3062
+ message,
3063
+ recoverable: true,
3064
+ suggestion: "Remove --date or remove --follow and retry."
3065
+ });
3066
+ } else {
3067
+ console.error(`Error: ${message}`);
3068
+ }
3069
+ process.exit(1);
3070
+ }
3071
+ if (listDates) {
3072
+ const logsDir = meta?.logsBaseDir ?? join6(config.dataDir, "logs");
3073
+ const dates = listLogDates(config.dataDir, logsDir);
3074
+ if (json) {
3075
+ printJsonSuccess({ dates, logsDir });
3076
+ } else {
3077
+ if (dates.length === 0) {
3078
+ console.log("No log dates found.");
3079
+ } else {
3080
+ console.log(`Log dates (${dates.length}):`);
3081
+ for (const date2 of dates) {
3082
+ console.log(` ${date2}`);
3083
+ }
3084
+ console.log(`
3085
+ Logs directory: ${logsDir}`);
3086
+ }
3087
+ }
3088
+ return;
3089
+ }
2499
3090
  const sourceInput = String(options.source ?? "all").trim().toLowerCase();
2500
3091
  if (json && follow) {
2501
3092
  const message = "--follow is not supported with --json. Use human mode for streaming logs.";
@@ -2527,18 +3118,36 @@ function createLogsCommand(config) {
2527
3118
  const tail = Number.isFinite(tailInput) && tailInput > 0 ? tailInput : 80;
2528
3119
  const intervalInput = Number.parseInt(String(options.intervalMs ?? "1000"), 10);
2529
3120
  const intervalMs = Number.isFinite(intervalInput) && intervalInput > 0 ? intervalInput : 1e3;
2530
- const meta = readRuntimeMeta(config.dataDir);
2531
- const paths = resolvePersistedLogPaths(config.dataDir, meta);
3121
+ let paths;
3122
+ try {
3123
+ paths = resolvePersistedLogPaths(config.dataDir, meta, date);
3124
+ } catch (error) {
3125
+ const message = error instanceof Error ? error.message : "Invalid --date value.";
3126
+ if (json) {
3127
+ printJsonError({
3128
+ code: "LOG_DATE_INVALID",
3129
+ message,
3130
+ recoverable: true,
3131
+ suggestion: "Retry with --date in YYYY-MM-DD format.",
3132
+ details: { date }
3133
+ });
3134
+ } else {
3135
+ console.error(`Error: ${message}`);
3136
+ }
3137
+ process.exit(1);
3138
+ }
2532
3139
  const targets = resolvePersistedLogTargets(paths, source);
2533
- if (source !== "all" && targets.length === 1 && !existsSync7(targets[0].path)) {
2534
- const message = `Log file not found for source ${source}: ${targets[0].path}`;
3140
+ const displayDate = date ?? inferDateFromPaths(paths);
3141
+ if (source !== "all" && targets.length === 1 && !existsSync8(targets[0].path)) {
3142
+ const dateLabel = displayDate ? ` on ${displayDate}` : "";
3143
+ const message = `Log file not found for source ${source}${dateLabel}: ${targets[0].path}`;
2535
3144
  if (json) {
2536
3145
  printJsonError({
2537
3146
  code: "LOG_FILE_NOT_FOUND",
2538
3147
  message,
2539
3148
  recoverable: true,
2540
- suggestion: "Start node/runtime or generate activity, then retry logs command.",
2541
- details: { source, path: targets[0].path }
3149
+ suggestion: "Start node/runtime or generate activity, then retry logs command. Use --list-dates to see available dates.",
3150
+ details: { source, date: displayDate ?? null, path: targets[0].path }
2542
3151
  });
2543
3152
  } else {
2544
3153
  console.error(`Error: ${message}`);
@@ -2547,7 +3156,7 @@ function createLogsCommand(config) {
2547
3156
  }
2548
3157
  const entries = [];
2549
3158
  for (const target of targets) {
2550
- const exists = existsSync7(target.path);
3159
+ const exists = existsSync8(target.path);
2551
3160
  let lines = [];
2552
3161
  if (exists) {
2553
3162
  try {
@@ -2582,6 +3191,7 @@ function createLogsCommand(config) {
2582
3191
  printJsonSuccess({
2583
3192
  source,
2584
3193
  tail,
3194
+ date: displayDate ?? null,
2585
3195
  entries: entries.map((entry) => ({
2586
3196
  source: entry.source,
2587
3197
  title: entry.title,
@@ -2593,7 +3203,8 @@ function createLogsCommand(config) {
2593
3203
  });
2594
3204
  return;
2595
3205
  }
2596
- console.log(`Logs (source: ${source}, tail: ${tail})`);
3206
+ const headerDate = displayDate ? `, date: ${displayDate}` : "";
3207
+ console.log(`Logs (source: ${source}${headerDate}, tail: ${tail})`);
2597
3208
  for (const entry of entries) {
2598
3209
  console.log(`
2599
3210
  ${entry.title}: ${entry.path}`);
@@ -2647,7 +3258,7 @@ Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
2647
3258
  for (const target of targets) {
2648
3259
  const state = states.get(target.source);
2649
3260
  if (!state) continue;
2650
- if (!existsSync7(state.path)) {
3261
+ if (!existsSync8(state.path)) {
2651
3262
  state.offset = 0;
2652
3263
  state.remainder = "";
2653
3264
  continue;
@@ -2678,28 +3289,199 @@ Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
2678
3289
  });
2679
3290
  });
2680
3291
  }
3292
+ function inferDateFromPaths(paths) {
3293
+ const candidate = paths.runtimeAlerts.split("/").at(-2);
3294
+ if (!candidate || !DATE_DIR_PATTERN2.test(candidate)) {
3295
+ return void 0;
3296
+ }
3297
+ const stdoutDate = paths.fnnStdout.split("/").at(-2);
3298
+ const stderrDate = paths.fnnStderr.split("/").at(-2);
3299
+ if (stdoutDate !== candidate || stderrDate !== candidate) {
3300
+ return void 0;
3301
+ }
3302
+ return candidate;
3303
+ }
2681
3304
 
2682
3305
  // src/commands/node.ts
2683
- import { nodeIdToPeerId as nodeIdToPeerId2, scriptToAddress as scriptToAddress2 } from "@fiber-pay/sdk";
2684
3306
  import { Command as Command8 } from "commander";
2685
3307
 
3308
+ // src/lib/node-info.ts
3309
+ async function runNodeInfoCommand(config, options) {
3310
+ const rpc = await createReadyRpcClient(config);
3311
+ const nodeInfo = await rpc.nodeInfo();
3312
+ if (options.json) {
3313
+ printJsonSuccess(nodeInfo);
3314
+ return;
3315
+ }
3316
+ console.log("\u2705 Node info retrieved");
3317
+ console.log(` Version: ${nodeInfo.version}`);
3318
+ console.log(` Commit: ${nodeInfo.commit_hash}`);
3319
+ console.log(` Node ID: ${nodeInfo.node_id}`);
3320
+ if (nodeInfo.features.length > 0) {
3321
+ console.log(" Features:");
3322
+ for (const feature of nodeInfo.features) {
3323
+ console.log(` - ${sanitizeForTerminal(feature)}`);
3324
+ }
3325
+ }
3326
+ console.log(` Name: ${sanitizeForTerminal(nodeInfo.node_name ?? "-")}`);
3327
+ if (nodeInfo.addresses.length > 0) {
3328
+ console.log(" Addresses:");
3329
+ for (const address of nodeInfo.addresses) {
3330
+ console.log(` - ${sanitizeForTerminal(address)}`);
3331
+ }
3332
+ }
3333
+ console.log(` Chain Hash: ${nodeInfo.chain_hash}`);
3334
+ console.log(` Channels: ${BigInt(nodeInfo.channel_count)}`);
3335
+ console.log(` Pending Channels: ${BigInt(nodeInfo.pending_channel_count)}`);
3336
+ console.log(` Peers: ${BigInt(nodeInfo.peers_count)}`);
3337
+ if (nodeInfo.udt_cfg_infos.length > 0) {
3338
+ console.log(" UDT Configs:");
3339
+ for (const udt of nodeInfo.udt_cfg_infos) {
3340
+ console.log(` - Name: ${sanitizeForTerminal(udt.name)}`);
3341
+ console.log(` Script: ${JSON.stringify(udt.script, null, 6)}`);
3342
+ if (udt.auto_accept_amount) {
3343
+ console.log(` Auto Accept Amount: ${BigInt(udt.auto_accept_amount)}`);
3344
+ }
3345
+ if (udt.cell_deps.length > 0) {
3346
+ console.log(" Cell Deps:");
3347
+ for (const dep of udt.cell_deps) {
3348
+ console.log(
3349
+ ` - Cell Dep: ${dep.cell_dep ? JSON.stringify(dep.cell_dep) : "null"}`
3350
+ );
3351
+ console.log(` Type ID: ${dep.type_id ? JSON.stringify(dep.type_id) : "null"}`);
3352
+ }
3353
+ }
3354
+ }
3355
+ }
3356
+ }
3357
+
3358
+ // src/lib/node-network.ts
3359
+ import { shannonsToCkb as shannonsToCkb5, toHex as toHex4 } from "@fiber-pay/sdk";
3360
+ async function runNodeNetworkCommand(config, options) {
3361
+ const rpc = await createReadyRpcClient(config);
3362
+ const [nodeInfo, localPeers, localChannels, graphNodes, graphChannels] = await Promise.all([
3363
+ rpc.nodeInfo(),
3364
+ rpc.listPeers(),
3365
+ rpc.listChannels({ include_closed: false }),
3366
+ rpc.graphNodes({}),
3367
+ rpc.graphChannels({})
3368
+ ]);
3369
+ const graphNodesMap = /* @__PURE__ */ new Map();
3370
+ for (const node of graphNodes.nodes) {
3371
+ graphNodesMap.set(node.node_id, node);
3372
+ }
3373
+ const peerIdToNodeIdMap = /* @__PURE__ */ new Map();
3374
+ for (const peer of localPeers.peers) {
3375
+ peerIdToNodeIdMap.set(peer.peer_id, peer.pubkey);
3376
+ }
3377
+ const graphChannelsMap = /* @__PURE__ */ new Map();
3378
+ for (const channel of graphChannels.channels) {
3379
+ if (channel.channel_outpoint) {
3380
+ const outpointKey = `${channel.channel_outpoint.tx_hash}:${channel.channel_outpoint.index}`;
3381
+ graphChannelsMap.set(outpointKey, channel);
3382
+ }
3383
+ }
3384
+ const enrichedPeers = localPeers.peers.map((peer) => ({
3385
+ ...peer,
3386
+ nodeInfo: graphNodesMap.get(peer.pubkey)
3387
+ }));
3388
+ const enrichedChannels = localChannels.channels.map((channel) => {
3389
+ const nodeId = peerIdToNodeIdMap.get(channel.peer_id) || channel.peer_id;
3390
+ const peerNodeInfo = graphNodesMap.get(nodeId);
3391
+ let graphChannelInfo;
3392
+ if (channel.channel_outpoint) {
3393
+ const outpointKey = `${channel.channel_outpoint.tx_hash}:${channel.channel_outpoint.index}`;
3394
+ graphChannelInfo = graphChannelsMap.get(outpointKey);
3395
+ }
3396
+ return {
3397
+ ...channel,
3398
+ peerNodeInfo,
3399
+ graphChannelInfo
3400
+ };
3401
+ });
3402
+ const activeChannels = enrichedChannels.filter((ch) => ch.state?.state_name === "CHANNEL_READY");
3403
+ const totalChannelCapacityShannons = activeChannels.reduce((sum, ch) => {
3404
+ const capacity = ch.graphChannelInfo?.capacity ? ch.graphChannelInfo.capacity : toHex4(BigInt(ch.local_balance) + BigInt(ch.remote_balance));
3405
+ return sum + BigInt(capacity);
3406
+ }, 0n);
3407
+ const totalChannelCapacity = formatShannonsAsCkb(totalChannelCapacityShannons, 1);
3408
+ const networkData = {
3409
+ localNodeId: nodeInfo.node_id,
3410
+ peers: enrichedPeers,
3411
+ channels: enrichedChannels,
3412
+ graphNodes: graphNodes.nodes,
3413
+ graphChannels: graphChannels.channels,
3414
+ summary: {
3415
+ connectedPeers: enrichedPeers.length,
3416
+ activeChannels: activeChannels.length,
3417
+ totalChannelCapacity
3418
+ }
3419
+ };
3420
+ if (options.json) {
3421
+ printJsonSuccess(networkData);
3422
+ return;
3423
+ }
3424
+ printNodeNetworkHuman(networkData);
3425
+ }
3426
+ function printNodeNetworkHuman(data) {
3427
+ console.log("Node Network Overview");
3428
+ console.log("=====================");
3429
+ console.log("");
3430
+ console.log(`Connected Peers: ${data.summary.connectedPeers}`);
3431
+ console.log(`Active Channels: ${data.summary.activeChannels}`);
3432
+ console.log(`Total Channel Capacity: ${data.summary.totalChannelCapacity} CKB`);
3433
+ console.log("");
3434
+ if (data.peers.length > 0) {
3435
+ console.log("Peers:");
3436
+ console.log(" PEER_ID ALIAS ADDRESS VERSION");
3437
+ console.log(
3438
+ " --------------------------------------------------------------------------------"
3439
+ );
3440
+ for (const peer of data.peers) {
3441
+ const peerId = truncateMiddle(peer.peer_id, 10, 8).padEnd(22, " ");
3442
+ const alias = sanitizeForTerminal(peer.nodeInfo?.node_name || "(unnamed)").slice(0, 20).padEnd(20, " ");
3443
+ const address = truncateMiddle(sanitizeForTerminal(peer.address), 15, 8).padEnd(25, " ");
3444
+ const version = sanitizeForTerminal(peer.nodeInfo?.version || "?").slice(0, 8).padEnd(8, " ");
3445
+ console.log(` ${peerId} ${alias} ${address} ${version}`);
3446
+ }
3447
+ console.log("");
3448
+ }
3449
+ if (data.channels.length > 0) {
3450
+ console.log("Channels:");
3451
+ console.log(
3452
+ " CHANNEL_ID PEER_ALIAS STATE LOCAL_BAL REMOTE_BAL CAPACITY"
3453
+ );
3454
+ console.log(
3455
+ " -----------------------------------------------------------------------------------------------"
3456
+ );
3457
+ for (const channel of data.channels) {
3458
+ const channelId = truncateMiddle(channel.channel_id, 10, 8).padEnd(22, " ");
3459
+ const peerAlias = sanitizeForTerminal(channel.peerNodeInfo?.node_name || "(unnamed)").slice(0, 18).padEnd(18, " ");
3460
+ const state = (channel.state?.state_name || "UNKNOWN").slice(0, 13).padEnd(13, " ");
3461
+ const localBal = shannonsToCkb5(channel.local_balance).toFixed(1).padStart(11, " ");
3462
+ const remoteBal = shannonsToCkb5(channel.remote_balance).toFixed(1).padStart(11, " ");
3463
+ const capacity = channel.graphChannelInfo?.capacity ? shannonsToCkb5(channel.graphChannelInfo.capacity).toFixed(1).padStart(8, " ") : shannonsToCkb5(toHex4(BigInt(channel.local_balance) + BigInt(channel.remote_balance))).toFixed(1).padStart(8, " ");
3464
+ console.log(` ${channelId} ${peerAlias} ${state} ${localBal} ${remoteBal} ${capacity}`);
3465
+ }
3466
+ }
3467
+ }
3468
+
2686
3469
  // src/lib/node-start.ts
2687
3470
  import { spawn } from "child_process";
2688
- import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
2689
- import { join as join5 } from "path";
3471
+ import { mkdirSync as mkdirSync3 } from "fs";
3472
+ import { join as join7 } from "path";
2690
3473
  import {
2691
3474
  createKeyManager,
2692
3475
  ensureFiberBinary,
2693
- getDefaultBinaryPath,
2694
3476
  ProcessManager
2695
3477
  } from "@fiber-pay/node";
2696
3478
  import { startRuntimeService } from "@fiber-pay/runtime";
2697
3479
 
2698
3480
  // src/lib/bootnode.ts
2699
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
3481
+ import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
2700
3482
  import { parse as parseYaml } from "yaml";
2701
3483
  function extractBootnodeAddrs(configFilePath) {
2702
- if (!existsSync8(configFilePath)) return [];
3484
+ if (!existsSync9(configFilePath)) return [];
2703
3485
  try {
2704
3486
  const content = readFileSync5(configFilePath, "utf-8");
2705
3487
  const doc = parseYaml(content);
@@ -2730,8 +3512,8 @@ async function autoConnectBootnodes(rpc, bootnodes) {
2730
3512
  }
2731
3513
 
2732
3514
  // src/lib/node-migration.ts
2733
- import { dirname } from "path";
2734
- import { BinaryManager, MigrationManager } from "@fiber-pay/node";
3515
+ import { dirname as dirname2 } from "path";
3516
+ import { BinaryManager as BinaryManager2, MigrationManager } from "@fiber-pay/node";
2735
3517
 
2736
3518
  // src/lib/migration-utils.ts
2737
3519
  function replaceRawMigrateHint(message) {
@@ -2754,8 +3536,8 @@ async function runMigrationGuard(opts) {
2754
3536
  return { checked: false, skippedReason: "store does not exist" };
2755
3537
  }
2756
3538
  const storePath = MigrationManager.resolveStorePath(dataDir);
2757
- const binaryDir = dirname(binaryPath);
2758
- const bm = new BinaryManager(binaryDir);
3539
+ const binaryDir = dirname2(binaryPath);
3540
+ const bm = new BinaryManager2(binaryDir);
2759
3541
  const migrateBinPath = bm.getMigrateBinaryPath();
2760
3542
  let migrationCheck;
2761
3543
  try {
@@ -2794,98 +3576,115 @@ async function runMigrationGuard(opts) {
2794
3576
  return { checked: true, migrationCheck };
2795
3577
  }
2796
3578
 
2797
- // src/lib/node-runtime-daemon.ts
2798
- import { spawnSync } from "child_process";
2799
- import { existsSync as existsSync9 } from "fs";
2800
- function getCustomBinaryState(binaryPath) {
2801
- const exists = existsSync9(binaryPath);
2802
- if (!exists) {
2803
- return { path: binaryPath, ready: false, version: "unknown" };
3579
+ // src/lib/runtime-port.ts
3580
+ import { spawnSync as spawnSync2 } from "child_process";
3581
+ function parsePortFromListen(listen) {
3582
+ const value = listen.trim();
3583
+ if (!value) {
3584
+ return void 0;
2804
3585
  }
2805
- try {
2806
- const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
2807
- if (result.status !== 0) {
2808
- return { path: binaryPath, ready: false, version: "unknown" };
2809
- }
2810
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
2811
- const firstLine = output.split("\n").find((line) => line.trim().length > 0) ?? "unknown";
2812
- return { path: binaryPath, ready: true, version: firstLine.trim() };
2813
- } catch {
2814
- return { path: binaryPath, ready: false, version: "unknown" };
3586
+ const lastColon = value.lastIndexOf(":");
3587
+ if (lastColon < 0 || lastColon === value.length - 1) {
3588
+ return void 0;
2815
3589
  }
3590
+ const port = Number(value.slice(lastColon + 1));
3591
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
3592
+ return void 0;
3593
+ }
3594
+ return port;
2816
3595
  }
2817
- function getBinaryVersion(binaryPath) {
2818
- try {
2819
- const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
2820
- if (result.status !== 0) {
2821
- return "unknown";
3596
+ function extractFirstPidFromLsofOutput(output) {
3597
+ for (const line of output.split("\n")) {
3598
+ const trimmed = line.trim();
3599
+ if (!trimmed.startsWith("p") || trimmed.length < 2) {
3600
+ continue;
2822
3601
  }
2823
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
2824
- if (!output) {
2825
- return "unknown";
3602
+ const pid = Number(trimmed.slice(1));
3603
+ if (Number.isInteger(pid) && pid > 0) {
3604
+ return pid;
2826
3605
  }
2827
- const firstLine = output.split("\n").find((line) => line.trim().length > 0);
2828
- return firstLine?.trim() ?? "unknown";
2829
- } catch {
2830
- return "unknown";
2831
3606
  }
3607
+ return void 0;
2832
3608
  }
2833
- function getCliEntrypoint() {
2834
- const entrypoint = process.argv[1];
2835
- if (!entrypoint) {
2836
- throw new Error("Unable to resolve CLI entrypoint path");
3609
+ function readProcessCommand(pid) {
3610
+ const result = spawnSync2("ps", ["-p", String(pid), "-o", "command="], {
3611
+ encoding: "utf-8"
3612
+ });
3613
+ if (result.error || result.status !== 0) {
3614
+ return void 0;
2837
3615
  }
2838
- return entrypoint;
3616
+ const command = (result.stdout ?? "").trim();
3617
+ return command.length > 0 ? command : void 0;
2839
3618
  }
2840
- function startRuntimeDaemonFromNode(params) {
2841
- const cliEntrypoint = getCliEntrypoint();
2842
- const result = spawnSync(
2843
- process.execPath,
2844
- [
2845
- cliEntrypoint,
2846
- "--data-dir",
2847
- params.dataDir,
2848
- "--rpc-url",
2849
- params.rpcUrl,
2850
- "runtime",
2851
- "start",
2852
- "--daemon",
2853
- "--fiber-rpc-url",
2854
- params.rpcUrl,
2855
- "--proxy-listen",
2856
- params.proxyListen,
2857
- "--state-file",
2858
- params.stateFilePath,
2859
- "--alert-log-file",
2860
- params.alertLogFile,
2861
- "--json"
2862
- ],
2863
- { encoding: "utf-8" }
2864
- );
2865
- if (result.status === 0) {
2866
- return { ok: true };
3619
+ function findListeningProcessByPort(listen) {
3620
+ const port = parsePortFromListen(listen);
3621
+ if (!port) {
3622
+ return void 0;
2867
3623
  }
2868
- const stderr = (result.stderr ?? "").trim();
2869
- const stdout = (result.stdout ?? "").trim();
2870
- const details = stderr || stdout || `exit code ${result.status ?? "unknown"}`;
2871
- return { ok: false, message: details };
3624
+ const result = spawnSync2("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], {
3625
+ encoding: "utf-8"
3626
+ });
3627
+ if (result.error || result.status !== 0) {
3628
+ return void 0;
3629
+ }
3630
+ const pid = extractFirstPidFromLsofOutput(result.stdout ?? "");
3631
+ if (!pid) {
3632
+ return void 0;
3633
+ }
3634
+ return {
3635
+ pid,
3636
+ command: readProcessCommand(pid)
3637
+ };
2872
3638
  }
2873
- function stopRuntimeDaemonFromNode(params) {
2874
- const cliEntrypoint = getCliEntrypoint();
2875
- spawnSync(
2876
- process.execPath,
2877
- [
2878
- cliEntrypoint,
2879
- "--data-dir",
2880
- params.dataDir,
2881
- "--rpc-url",
2882
- params.rpcUrl,
2883
- "runtime",
2884
- "stop",
2885
- "--json"
2886
- ],
2887
- { encoding: "utf-8" }
2888
- );
3639
+ function isFiberRuntimeCommand(command) {
3640
+ if (!command) {
3641
+ return false;
3642
+ }
3643
+ const normalized = command.toLowerCase();
3644
+ const hasFiberIdentifier = normalized.includes("fiber-pay") || normalized.includes("@fiber-pay/cli") || normalized.includes("/packages/cli/dist/cli.js") || normalized.includes("\\packages\\cli\\dist\\cli.js") || normalized.includes("/dist/cli.js") || normalized.includes("\\dist\\cli.js");
3645
+ if (!hasFiberIdentifier) {
3646
+ return false;
3647
+ }
3648
+ return normalized.includes("runtime") && normalized.includes("start");
3649
+ }
3650
+ async function terminateProcess(pid, timeoutMs = 5e3) {
3651
+ if (!isProcessRunning(pid)) {
3652
+ return true;
3653
+ }
3654
+ try {
3655
+ process.kill(pid, "SIGTERM");
3656
+ } catch (error) {
3657
+ if (error.code === "ESRCH") {
3658
+ return true;
3659
+ }
3660
+ return false;
3661
+ }
3662
+ const deadline = Date.now() + timeoutMs;
3663
+ while (Date.now() < deadline) {
3664
+ if (!isProcessRunning(pid)) {
3665
+ return true;
3666
+ }
3667
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
3668
+ }
3669
+ if (!isProcessRunning(pid)) {
3670
+ return true;
3671
+ }
3672
+ try {
3673
+ process.kill(pid, "SIGKILL");
3674
+ } catch (error) {
3675
+ if (error.code === "ESRCH") {
3676
+ return true;
3677
+ }
3678
+ return false;
3679
+ }
3680
+ const killDeadline = Date.now() + 1e3;
3681
+ while (Date.now() < killDeadline) {
3682
+ if (!isProcessRunning(pid)) {
3683
+ return true;
3684
+ }
3685
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
3686
+ }
3687
+ return !isProcessRunning(pid);
2889
3688
  }
2890
3689
 
2891
3690
  // src/lib/node-start.ts
@@ -2997,19 +3796,22 @@ async function runNodeStartCommand(config, options) {
2997
3796
  options.runtimeProxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
2998
3797
  );
2999
3798
  const proxyListenSource = options.runtimeProxyListen ? "cli" : config.runtimeProxyListen ? "profile" : "default";
3000
- const runtimeStateFilePath = join5(config.dataDir, "runtime-state.json");
3001
- const logsDir = join5(config.dataDir, "logs");
3002
- const fnnStdoutLogPath = join5(logsDir, "fnn.stdout.log");
3003
- const fnnStderrLogPath = join5(logsDir, "fnn.stderr.log");
3004
- const runtimeAlertLogPath = join5(logsDir, "runtime.alerts.jsonl");
3005
- mkdirSync2(logsDir, { recursive: true });
3006
- const binaryPath = config.binaryPath || getDefaultBinaryPath();
3007
- await ensureFiberBinary();
3799
+ const runtimeStateFilePath = join7(config.dataDir, "runtime-state.json");
3800
+ const logsBaseDir = join7(config.dataDir, "logs");
3801
+ mkdirSync3(logsBaseDir, { recursive: true });
3802
+ resolveLogDirForDate(config.dataDir);
3803
+ const resolvedBinary = resolveBinaryPath(config);
3804
+ const binaryPath = resolvedBinary.binaryPath;
3805
+ if (resolvedBinary.source === "profile-managed") {
3806
+ const installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
3807
+ await ensureFiberBinary({ installDir });
3808
+ }
3008
3809
  const binaryVersion = getBinaryVersion(binaryPath);
3009
3810
  const configFilePath = ensureNodeConfigFile(config.dataDir, config.network);
3010
3811
  emitStage("binary_resolved", "ok", {
3011
3812
  binaryPath,
3012
3813
  binaryVersion,
3814
+ binarySource: resolvedBinary.source,
3013
3815
  configFilePath
3014
3816
  });
3015
3817
  if (!json) {
@@ -3070,11 +3872,11 @@ async function runNodeStartCommand(config, options) {
3070
3872
  removePidFile(config.dataDir);
3071
3873
  });
3072
3874
  processManager.on("stdout", (text) => {
3073
- appendFileSync(fnnStdoutLogPath, text, "utf-8");
3875
+ appendToTodayLog(config.dataDir, "fnn.stdout.log", text);
3074
3876
  emitFnnLog("stdout", text);
3075
3877
  });
3076
3878
  processManager.on("stderr", (text) => {
3077
- appendFileSync(fnnStderrLogPath, text, "utf-8");
3879
+ appendToTodayLog(config.dataDir, "fnn.stderr.log", text);
3078
3880
  emitFnnLog("stderr", text);
3079
3881
  });
3080
3882
  await processManager.start();
@@ -3119,13 +3921,38 @@ async function runNodeStartCommand(config, options) {
3119
3921
  process.exit(1);
3120
3922
  }
3121
3923
  try {
3924
+ const runtimePortProcess = findListeningProcessByPort(runtimeProxyListen);
3925
+ if (runtimePortProcess) {
3926
+ if (isFiberRuntimeCommand(runtimePortProcess.command)) {
3927
+ const terminated = await terminateProcess(runtimePortProcess.pid);
3928
+ if (!terminated) {
3929
+ throw new Error(
3930
+ `Runtime proxy ${runtimeProxyListen} is occupied by stale fiber-pay runtime PID ${runtimePortProcess.pid}, but termination failed`
3931
+ );
3932
+ }
3933
+ removeRuntimeFiles(config.dataDir);
3934
+ emitStage("runtime_preflight", "ok", {
3935
+ proxyListen: runtimeProxyListen,
3936
+ cleanedStaleProcessPid: runtimePortProcess.pid
3937
+ });
3938
+ } else if (runtimePortProcess.command) {
3939
+ const details = runtimePortProcess.command ? `PID ${runtimePortProcess.pid} (${runtimePortProcess.command})` : `PID ${runtimePortProcess.pid}`;
3940
+ throw new Error(
3941
+ `Runtime proxy ${runtimeProxyListen} is already in use by non-fiber-pay process: ${details}`
3942
+ );
3943
+ } else {
3944
+ throw new Error(
3945
+ `Runtime proxy ${runtimeProxyListen} is already in use by process PID ${runtimePortProcess.pid}. Unable to determine command owner; inspect this PID manually before retrying.`
3946
+ );
3947
+ }
3948
+ }
3122
3949
  if (runtimeDaemon) {
3123
3950
  const daemonStart = startRuntimeDaemonFromNode({
3124
3951
  dataDir: config.dataDir,
3125
3952
  rpcUrl: config.rpcUrl,
3126
3953
  proxyListen: runtimeProxyListen,
3127
3954
  stateFilePath: runtimeStateFilePath,
3128
- alertLogFile: runtimeAlertLogPath
3955
+ alertLogsBaseDir: logsBaseDir
3129
3956
  });
3130
3957
  if (!daemonStart.ok) {
3131
3958
  throw new Error(daemonStart.message);
@@ -3140,23 +3967,25 @@ async function runNodeStartCommand(config, options) {
3140
3967
  storage: {
3141
3968
  stateFilePath: runtimeStateFilePath
3142
3969
  },
3143
- alerts: [{ type: "stdout" }, { type: "file", path: runtimeAlertLogPath }],
3970
+ alerts: [{ type: "stdout" }, { type: "daily-file", baseLogsDir: logsBaseDir }],
3144
3971
  jobs: {
3145
3972
  enabled: true,
3146
- dbPath: join5(config.dataDir, "runtime-jobs.db")
3973
+ dbPath: join7(config.dataDir, "runtime-jobs.db")
3147
3974
  }
3148
3975
  });
3149
3976
  const runtimeStatus = runtime.service.getStatus();
3150
3977
  writeRuntimePid(config.dataDir, process.pid);
3978
+ const todayLogDir = resolveLogDirForDate(config.dataDir);
3151
3979
  writeRuntimeMeta(config.dataDir, {
3152
3980
  pid: process.pid,
3153
3981
  startedAt: runtimeStatus.startedAt,
3154
3982
  fiberRpcUrl: runtimeStatus.targetUrl,
3155
3983
  proxyListen: runtimeStatus.proxyListen,
3156
3984
  stateFilePath: runtimeStateFilePath,
3157
- alertLogFilePath: runtimeAlertLogPath,
3158
- fnnStdoutLogPath,
3159
- fnnStderrLogPath,
3985
+ alertLogFilePath: join7(todayLogDir, "runtime.alerts.jsonl"),
3986
+ fnnStdoutLogPath: join7(todayLogDir, "fnn.stdout.log"),
3987
+ fnnStderrLogPath: join7(todayLogDir, "fnn.stderr.log"),
3988
+ logsBaseDir,
3160
3989
  daemon: false
3161
3990
  });
3162
3991
  }
@@ -3226,7 +4055,7 @@ async function runNodeStartCommand(config, options) {
3226
4055
  process.exit(1);
3227
4056
  }
3228
4057
  emitStage("rpc_ready", "ok", { rpcUrl: config.rpcUrl });
3229
- const bootnodes = nodeConfig.configFilePath ? extractBootnodeAddrs(nodeConfig.configFilePath) : extractBootnodeAddrs(join5(config.dataDir, "config.yml"));
4058
+ const bootnodes = nodeConfig.configFilePath ? extractBootnodeAddrs(nodeConfig.configFilePath) : extractBootnodeAddrs(join7(config.dataDir, "config.yml"));
3230
4059
  if (bootnodes.length > 0) {
3231
4060
  await autoConnectBootnodes(rpc, bootnodes);
3232
4061
  }
@@ -3267,9 +4096,8 @@ async function runNodeStartCommand(config, options) {
3267
4096
  proxyUrl: `http://${runtimeProxyListen}`,
3268
4097
  proxyListenSource,
3269
4098
  logs: {
3270
- fnnStdout: fnnStdoutLogPath,
3271
- fnnStderr: fnnStderrLogPath,
3272
- runtimeAlerts: runtimeAlertLogPath
4099
+ baseDir: logsBaseDir,
4100
+ todayDir: resolveLogDirForDate(config.dataDir)
3273
4101
  }
3274
4102
  });
3275
4103
  } else {
@@ -3279,7 +4107,7 @@ async function runNodeStartCommand(config, options) {
3279
4107
  ` Runtime proxy: http://${runtimeProxyListen} (browser-safe endpoint + monitoring)`
3280
4108
  );
3281
4109
  console.log(` Runtime mode: ${runtimeDaemon ? "daemon" : "embedded"}`);
3282
- console.log(` Log files: ${logsDir}`);
4110
+ console.log(` Log files: ${logsBaseDir}`);
3283
4111
  console.log(" Press Ctrl+C to stop.");
3284
4112
  }
3285
4113
  let shutdownRequested = false;
@@ -3347,8 +4175,6 @@ async function runNodeStartCommand(config, options) {
3347
4175
 
3348
4176
  // src/lib/node-status.ts
3349
4177
  import { existsSync as existsSync10 } from "fs";
3350
- import { join as join6 } from "path";
3351
- import { getFiberBinaryInfo as getFiberBinaryInfo2 } from "@fiber-pay/node";
3352
4178
  import {
3353
4179
  buildMultiaddrFromNodeId,
3354
4180
  buildMultiaddrFromRpcUrl,
@@ -3515,24 +4341,30 @@ async function runNodeStatusCommand(config, options) {
3515
4341
  const json = Boolean(options.json);
3516
4342
  const pid = readPidFile(config.dataDir);
3517
4343
  const resolvedRpc = resolveRpcEndpoint(config);
3518
- const managedBinaryPath = join6(config.dataDir, "bin", "fnn");
3519
- const binaryInfo = config.binaryPath ? getCustomBinaryState(config.binaryPath) : await getFiberBinaryInfo2(join6(config.dataDir, "bin"));
4344
+ const { resolvedBinary, info: binaryInfo } = await getBinaryDetails(config);
3520
4345
  const configExists = existsSync10(config.configPath);
3521
4346
  const nodeRunning = Boolean(pid && isProcessRunning(pid));
3522
4347
  let rpcResponsive = false;
3523
4348
  let nodeId = null;
4349
+ let nodeName = null;
4350
+ let addresses = [];
4351
+ let chainHash = null;
4352
+ let version = null;
3524
4353
  let peerId = null;
4354
+ let peersCount = 0;
3525
4355
  let peerIdError = null;
3526
4356
  let multiaddr = null;
3527
4357
  let multiaddrError = null;
3528
4358
  let multiaddrInferred = false;
3529
4359
  let channelsTotal = 0;
3530
4360
  let channelsReady = 0;
4361
+ let pendingChannelCount = 0;
3531
4362
  let canSend = false;
3532
4363
  let canReceive = false;
3533
4364
  let localCkb = 0;
3534
4365
  let remoteCkb = 0;
3535
4366
  let fundingAddress = null;
4367
+ let fundingLockScript = null;
3536
4368
  let fundingCkb = 0;
3537
4369
  let fundingBalanceError = null;
3538
4370
  if (nodeRunning) {
@@ -3542,6 +4374,11 @@ async function runNodeStatusCommand(config, options) {
3542
4374
  const channels = await rpc.listChannels({ include_closed: false });
3543
4375
  rpcResponsive = true;
3544
4376
  nodeId = nodeInfo.node_id;
4377
+ nodeName = nodeInfo.node_name;
4378
+ addresses = nodeInfo.addresses;
4379
+ chainHash = nodeInfo.chain_hash;
4380
+ version = nodeInfo.version;
4381
+ peersCount = parseInt(nodeInfo.peers_count, 16);
3545
4382
  try {
3546
4383
  peerId = await nodeIdToPeerId(nodeInfo.node_id);
3547
4384
  } catch (error) {
@@ -3568,18 +4405,17 @@ async function runNodeStatusCommand(config, options) {
3568
4405
  (channel) => channel.state?.state_name === ChannelState2.ChannelReady
3569
4406
  );
3570
4407
  channelsReady = readyChannels.length;
4408
+ pendingChannelCount = Math.max(channelsTotal - channelsReady, 0);
3571
4409
  const liquidity = summarizeChannelLiquidity(readyChannels);
3572
4410
  canSend = liquidity.canSend;
3573
4411
  canReceive = liquidity.canReceive;
3574
4412
  localCkb = liquidity.localCkb;
3575
4413
  remoteCkb = liquidity.remoteCkb;
3576
4414
  fundingAddress = scriptToAddress(nodeInfo.default_funding_lock_script, config.network);
4415
+ fundingLockScript = nodeInfo.default_funding_lock_script;
3577
4416
  if (config.ckbRpcUrl) {
3578
4417
  try {
3579
- const fundingBalance = await getLockBalanceShannons(
3580
- config.ckbRpcUrl,
3581
- nodeInfo.default_funding_lock_script
3582
- );
4418
+ const fundingBalance = await getLockBalanceShannons(config.ckbRpcUrl, fundingLockScript);
3583
4419
  fundingCkb = Number(fundingBalance) / 1e8;
3584
4420
  } catch (error) {
3585
4421
  fundingBalanceError = error instanceof Error ? error.message : "Failed to query CKB balance for funding address";
@@ -3610,18 +4446,25 @@ async function runNodeStatusCommand(config, options) {
3610
4446
  rpcTarget: resolvedRpc.target,
3611
4447
  resolvedRpcUrl: resolvedRpc.url,
3612
4448
  nodeId,
4449
+ nodeName,
4450
+ addresses,
4451
+ chainHash,
4452
+ version,
3613
4453
  peerId,
4454
+ peersCount,
3614
4455
  peerIdError,
3615
4456
  multiaddr,
3616
4457
  multiaddrError,
3617
4458
  multiaddrInferred,
4459
+ fundingLockScript,
3618
4460
  checks: {
3619
4461
  binary: {
3620
4462
  path: binaryInfo.path,
3621
4463
  ready: binaryInfo.ready,
3622
4464
  version: binaryInfo.version,
3623
- source: config.binaryPath ? "env-binary-path" : "managed-binary-dir",
3624
- managedPath: managedBinaryPath
4465
+ source: resolvedBinary.source,
4466
+ managedPath: resolvedBinary.managedPath,
4467
+ resolvedPath: resolvedBinary.binaryPath
3625
4468
  },
3626
4469
  config: {
3627
4470
  path: config.configPath,
@@ -3639,6 +4482,7 @@ async function runNodeStatusCommand(config, options) {
3639
4482
  channels: {
3640
4483
  total: channelsTotal,
3641
4484
  ready: channelsReady,
4485
+ pending: pendingChannelCount,
3642
4486
  canSend,
3643
4487
  canReceive
3644
4488
  }
@@ -3665,6 +4509,15 @@ async function runNodeStatusCommand(config, options) {
3665
4509
  console.log(`\u2705 Node is running (PID: ${output.pid})`);
3666
4510
  if (output.rpcResponsive) {
3667
4511
  console.log(` Node ID: ${String(output.nodeId)}`);
4512
+ if (output.nodeName) {
4513
+ console.log(` Name: ${sanitizeForTerminal(output.nodeName)}`);
4514
+ }
4515
+ if (output.version) {
4516
+ console.log(` Version: ${sanitizeForTerminal(output.version)}`);
4517
+ }
4518
+ if (output.chainHash) {
4519
+ console.log(` Chain Hash: ${String(output.chainHash)}`);
4520
+ }
3668
4521
  if (output.peerId) {
3669
4522
  console.log(` Peer ID: ${String(output.peerId)}`);
3670
4523
  } else if (output.peerIdError) {
@@ -3680,6 +4533,12 @@ async function runNodeStatusCommand(config, options) {
3680
4533
  } else {
3681
4534
  console.log(" Multiaddr: unavailable");
3682
4535
  }
4536
+ if (output.addresses.length > 0) {
4537
+ console.log(" Addresses:");
4538
+ for (const address of output.addresses) {
4539
+ console.log(` - ${sanitizeForTerminal(address)}`);
4540
+ }
4541
+ }
3683
4542
  } else {
3684
4543
  console.log(" \u26A0\uFE0F RPC not responding");
3685
4544
  }
@@ -3691,11 +4550,17 @@ async function runNodeStatusCommand(config, options) {
3691
4550
  console.log("");
3692
4551
  console.log("Diagnostics");
3693
4552
  console.log(` Binary: ${output.checks.binary.ready ? "ready" : "missing"}`);
4553
+ console.log(` Binary Path: ${output.checks.binary.resolvedPath}`);
3694
4554
  console.log(` Config: ${output.checks.config.exists ? "present" : "missing"}`);
3695
4555
  console.log(` RPC: ${output.checks.node.rpcReachable ? "reachable" : "unreachable"}`);
3696
4556
  console.log(
3697
- ` Channels: ${output.checks.channels.ready}/${output.checks.channels.total} ready/total`
4557
+ ` Channels: ${output.checks.channels.ready}/${output.checks.channels.pending}/${output.checks.channels.total} ready/pending/total`
3698
4558
  );
4559
+ if (output.rpcResponsive) {
4560
+ console.log(` Peers: ${output.peersCount}`);
4561
+ } else {
4562
+ console.log(" Peers: unavailable");
4563
+ }
3699
4564
  console.log(` Can Send: ${output.checks.channels.canSend ? "yes" : "no"}`);
3700
4565
  console.log(` Can Receive: ${output.checks.channels.canReceive ? "yes" : "no"}`);
3701
4566
  console.log(` Recommendation:${output.recommendation}`);
@@ -3858,11 +4723,28 @@ async function runNodeStopCommand(config, options) {
3858
4723
  }
3859
4724
 
3860
4725
  // src/lib/node-upgrade.ts
3861
- import { BinaryManager as BinaryManager2, MigrationManager as MigrationManager2 } from "@fiber-pay/node";
4726
+ import { BinaryManager as BinaryManager3, MigrationManager as MigrationManager2 } from "@fiber-pay/node";
3862
4727
  async function runNodeUpgradeCommand(config, options) {
3863
4728
  const json = Boolean(options.json);
3864
- const installDir = `${config.dataDir}/bin`;
3865
- const binaryManager = new BinaryManager2(installDir);
4729
+ const resolvedBinary = resolveBinaryPath(config);
4730
+ let installDir;
4731
+ try {
4732
+ installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
4733
+ } catch (error) {
4734
+ const message = error instanceof Error ? error.message : String(error);
4735
+ if (json) {
4736
+ printJsonError({
4737
+ code: "BINARY_PATH_INCOMPATIBLE",
4738
+ message,
4739
+ recoverable: true,
4740
+ suggestion: "Use `fiber-pay config profile unset binaryPath` or set binaryPath to a standard fnn filename in the target directory."
4741
+ });
4742
+ } else {
4743
+ console.error(`\u274C ${message}`);
4744
+ }
4745
+ process.exit(1);
4746
+ }
4747
+ const binaryManager = new BinaryManager3(installDir);
3866
4748
  const pid = readPidFile(config.dataDir);
3867
4749
  if (pid && isProcessRunning(pid)) {
3868
4750
  const msg = "The Fiber node is currently running. Stop it before upgrading.";
@@ -4131,32 +5013,15 @@ function createNodeCommand(config) {
4131
5013
  node.command("status").option("--json").action(async (options) => {
4132
5014
  await runNodeStatusCommand(config, options);
4133
5015
  });
5016
+ node.command("network").description("Display comprehensive network topology and connections").option("--json").action(async (options) => {
5017
+ await runNodeNetworkCommand(config, options);
5018
+ });
5019
+ node.command("info").description("Display information about the running node").option("--json").action(async (options) => {
5020
+ await runNodeInfoCommand(config, options);
5021
+ });
4134
5022
  node.command("ready").description("Agent-oriented readiness summary for automation").option("--json").action(async (options) => {
4135
5023
  await runNodeReadyCommand(config, options);
4136
5024
  });
4137
- node.command("info").option("--json").action(async (options) => {
4138
- const rpc = await createReadyRpcClient(config);
4139
- const nodeInfo = await rpc.nodeInfo();
4140
- const fundingAddress = scriptToAddress2(nodeInfo.default_funding_lock_script, config.network);
4141
- const peerId = await nodeIdToPeerId2(nodeInfo.node_id);
4142
- const output = {
4143
- nodeId: nodeInfo.node_id,
4144
- peerId,
4145
- addresses: nodeInfo.addresses,
4146
- chainHash: nodeInfo.chain_hash,
4147
- fundingAddress,
4148
- fundingLockScript: nodeInfo.default_funding_lock_script,
4149
- version: nodeInfo.version,
4150
- channelCount: parseInt(nodeInfo.channel_count, 16),
4151
- pendingChannelCount: parseInt(nodeInfo.pending_channel_count, 16),
4152
- peersCount: parseInt(nodeInfo.peers_count, 16)
4153
- };
4154
- if (options.json) {
4155
- printJsonSuccess(output);
4156
- } else {
4157
- printNodeInfoHuman(output);
4158
- }
4159
- });
4160
5025
  node.command("upgrade").description("Upgrade the Fiber node binary and migrate the database if needed").option("--version <version>", "Target Fiber version (default: latest)").option("--no-backup", "Skip creating a store backup before migration").option("--check-only", "Only check if migration is needed, do not migrate").option(
4161
5026
  "--force-migrate",
4162
5027
  "Force migration attempt even when compatibility check reports incompatible data"
@@ -4167,7 +5032,7 @@ function createNodeCommand(config) {
4167
5032
  }
4168
5033
 
4169
5034
  // src/commands/payment.ts
4170
- import { ckbToShannons as ckbToShannons3, shannonsToCkb as shannonsToCkb4 } from "@fiber-pay/sdk";
5035
+ import { ckbToShannons as ckbToShannons4, shannonsToCkb as shannonsToCkb6 } from "@fiber-pay/sdk";
4171
5036
  import { Command as Command9 } from "commander";
4172
5037
  function createPaymentCommand(config) {
4173
5038
  const payment = new Command9("payment").description("Payment lifecycle and status commands");
@@ -4209,9 +5074,9 @@ function createPaymentCommand(config) {
4209
5074
  const paymentParams = {
4210
5075
  invoice,
4211
5076
  target_pubkey: recipientNodeId,
4212
- amount: amountCkb ? ckbToShannons3(amountCkb) : void 0,
5077
+ amount: amountCkb ? ckbToShannons4(amountCkb) : void 0,
4213
5078
  keysend: recipientNodeId ? true : void 0,
4214
- max_fee_amount: maxFeeCkb ? ckbToShannons3(maxFeeCkb) : void 0
5079
+ max_fee_amount: maxFeeCkb ? ckbToShannons4(maxFeeCkb) : void 0
4215
5080
  };
4216
5081
  const endpoint = resolveRpcEndpoint(config);
4217
5082
  if (endpoint.target === "runtime-proxy") {
@@ -4253,7 +5118,7 @@ function createPaymentCommand(config) {
4253
5118
  const payload = {
4254
5119
  paymentHash: result.payment_hash,
4255
5120
  status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
4256
- feeCkb: shannonsToCkb4(result.fee),
5121
+ feeCkb: shannonsToCkb6(result.fee),
4257
5122
  failureReason: result.failed_error
4258
5123
  };
4259
5124
  if (json) {
@@ -4268,6 +5133,7 @@ function createPaymentCommand(config) {
4268
5133
  }
4269
5134
  }
4270
5135
  });
5136
+ registerPaymentRebalanceCommand(payment, config);
4271
5137
  payment.command("get").argument("<paymentHash>").option("--json").action(async (paymentHash, options) => {
4272
5138
  const rpc = await createReadyRpcClient(config);
4273
5139
  const result = await rpc.getPayment({ payment_hash: paymentHash });
@@ -4416,7 +5282,7 @@ function createPaymentCommand(config) {
4416
5282
  process.exit(1);
4417
5283
  }
4418
5284
  const hopsInfo = pubkeys.map((pubkey) => ({ pubkey }));
4419
- const amount = options.amount ? ckbToShannons3(parseFloat(options.amount)) : void 0;
5285
+ const amount = options.amount ? ckbToShannons4(parseFloat(options.amount)) : void 0;
4420
5286
  const result = await rpc.buildRouter({
4421
5287
  hops_info: hopsInfo,
4422
5288
  amount
@@ -4432,7 +5298,7 @@ function createPaymentCommand(config) {
4432
5298
  console.log(
4433
5299
  ` Outpoint: ${hop.channel_outpoint.tx_hash}:${hop.channel_outpoint.index}`
4434
5300
  );
4435
- console.log(` Amount: ${shannonsToCkb4(hop.amount_received)} CKB`);
5301
+ console.log(` Amount: ${shannonsToCkb6(hop.amount_received)} CKB`);
4436
5302
  console.log(` Expiry: ${hop.incoming_tlc_expiry}`);
4437
5303
  }
4438
5304
  }
@@ -4440,7 +5306,7 @@ function createPaymentCommand(config) {
4440
5306
  payment.command("send-route").description("Send a payment using a pre-built route from `payment route`").requiredOption(
4441
5307
  "--router <json>",
4442
5308
  "JSON array of router hops (output of `payment route --json`)"
4443
- ).option("--invoice <invoice>", "Invoice to pay").option("--payment-hash <hash>", "Payment hash (for keysend)").option("--keysend", "Keysend mode").option("--dry-run", "Simulate\u2014do not actually send").option("--json").action(async (options) => {
5309
+ ).option("--invoice <invoice>", "Invoice to pay").option("--payment-hash <hash>", "Payment hash (for keysend)").option("--keysend", "Keysend mode").option("--allow-self-payment", "Allow self-payment for circular route rebalancing").option("--dry-run", "Simulate\u2014do not actually send").option("--json").action(async (options) => {
4444
5310
  const rpc = await createReadyRpcClient(config);
4445
5311
  const json = Boolean(options.json);
4446
5312
  let router;
@@ -4465,12 +5331,13 @@ function createPaymentCommand(config) {
4465
5331
  invoice: options.invoice,
4466
5332
  payment_hash: options.paymentHash,
4467
5333
  keysend: options.keysend ? true : void 0,
5334
+ allow_self_payment: options.allowSelfPayment ? true : void 0,
4468
5335
  dry_run: options.dryRun ? true : void 0
4469
5336
  });
4470
5337
  const payload = {
4471
5338
  paymentHash: result.payment_hash,
4472
5339
  status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
4473
- feeCkb: shannonsToCkb4(result.fee),
5340
+ feeCkb: shannonsToCkb6(result.fee),
4474
5341
  failureReason: result.failed_error,
4475
5342
  dryRun: Boolean(options.dryRun)
4476
5343
  };
@@ -4494,7 +5361,7 @@ function getJobPaymentHash(job) {
4494
5361
  }
4495
5362
  function getJobFeeCkb(job) {
4496
5363
  const result = job.result;
4497
- return result?.fee ? shannonsToCkb4(result.fee) : 0;
5364
+ return result?.fee ? shannonsToCkb6(result.fee) : 0;
4498
5365
  }
4499
5366
  function getJobFailure(job) {
4500
5367
  const result = job.result;
@@ -4566,7 +5433,7 @@ function createPeerCommand(config) {
4566
5433
 
4567
5434
  // src/commands/runtime.ts
4568
5435
  import { spawn as spawn2 } from "child_process";
4569
- import { resolve } from "path";
5436
+ import { join as join8, resolve } from "path";
4570
5437
  import {
4571
5438
  alertPriorityOrder,
4572
5439
  formatRuntimeAlert as formatRuntimeAlert2,
@@ -4640,15 +5507,22 @@ function shouldPrintAlert(alert, filter) {
4640
5507
  }
4641
5508
  return true;
4642
5509
  }
5510
+ function resolveRuntimeRecoveryListen(config) {
5511
+ const meta = readRuntimeMeta(config.dataDir);
5512
+ return meta?.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229";
5513
+ }
4643
5514
  function createRuntimeCommand(config) {
4644
5515
  const runtime = new Command11("runtime").description("Polling monitor and alert runtime service");
4645
- runtime.command("start").description("Start runtime monitor service in foreground").option("--daemon", "Start runtime monitor in detached background mode").option("--fiber-rpc-url <url>", "Target fiber rpc URL (defaults to --rpc-url/global config)").option("--proxy-listen <host:port>", "Monitor proxy listen address").option("--channel-poll-ms <ms>", "Channel polling interval in milliseconds").option("--invoice-poll-ms <ms>", "Invoice polling interval in milliseconds").option("--payment-poll-ms <ms>", "Payment polling interval in milliseconds").option("--peer-poll-ms <ms>", "Peer polling interval in milliseconds").option("--health-poll-ms <ms>", "RPC health polling interval in milliseconds").option("--include-closed <bool>", "Monitor closed channels (true|false)").option("--completed-ttl-seconds <seconds>", "TTL for completed invoices/payments in tracker").option("--state-file <path>", "State file path for snapshots and history").option("--alert-log-file <path>", "Path to runtime alert JSONL log file").option("--flush-ms <ms>", "State flush interval in milliseconds").option("--webhook <url>", "Webhook URL to receive alert POST payloads").option("--websocket <host:port>", "WebSocket alert broadcast listen address").option(
5516
+ runtime.command("start").description("Start runtime monitor service in foreground").option("--daemon", "Start runtime monitor in detached background mode").option("--fiber-rpc-url <url>", "Target fiber rpc URL (defaults to --rpc-url/global config)").option("--proxy-listen <host:port>", "Monitor proxy listen address").option("--channel-poll-ms <ms>", "Channel polling interval in milliseconds").option("--invoice-poll-ms <ms>", "Invoice polling interval in milliseconds").option("--payment-poll-ms <ms>", "Payment polling interval in milliseconds").option("--peer-poll-ms <ms>", "Peer polling interval in milliseconds").option("--health-poll-ms <ms>", "RPC health polling interval in milliseconds").option("--include-closed <bool>", "Monitor closed channels (true|false)").option("--completed-ttl-seconds <seconds>", "TTL for completed invoices/payments in tracker").option("--state-file <path>", "State file path for snapshots and history").option("--alert-log-file <path>", "Path to runtime alert JSONL log file (legacy static path)").option("--alert-logs-base-dir <dir>", "Base logs directory for daily-rotated alert files").option("--flush-ms <ms>", "State flush interval in milliseconds").option("--webhook <url>", "Webhook URL to receive alert POST payloads").option("--websocket <host:port>", "WebSocket alert broadcast listen address").option(
4646
5517
  "--log-min-priority <priority>",
4647
5518
  "Minimum runtime log priority (critical|high|medium|low)"
4648
5519
  ).option("--log-type <types>", "Comma-separated runtime alert types to print").option("--json").action(async (options) => {
4649
5520
  const asJson = Boolean(options.json);
4650
5521
  const daemon = Boolean(options.daemon);
4651
5522
  const isRuntimeChild = process.env.FIBER_RUNTIME_CHILD === "1";
5523
+ const runtimeListen = String(
5524
+ options.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
5525
+ );
4652
5526
  try {
4653
5527
  const existingPid = readRuntimePid(config.dataDir);
4654
5528
  if (existingPid && isProcessRunning(existingPid) && (!isRuntimeChild || existingPid !== process.pid)) {
@@ -4668,6 +5542,32 @@ function createRuntimeCommand(config) {
4668
5542
  if (existingPid && !isProcessRunning(existingPid)) {
4669
5543
  removeRuntimeFiles(config.dataDir);
4670
5544
  }
5545
+ const discovered = findListeningProcessByPort(runtimeListen);
5546
+ if (discovered && (!existingPid || discovered.pid !== existingPid)) {
5547
+ if (isFiberRuntimeCommand(discovered.command)) {
5548
+ const terminated = await terminateProcess(discovered.pid);
5549
+ if (!terminated) {
5550
+ throw new Error(
5551
+ `Runtime port ${runtimeListen} is occupied by stale fiber-pay runtime PID ${discovered.pid} but termination failed.`
5552
+ );
5553
+ }
5554
+ removeRuntimeFiles(config.dataDir);
5555
+ if (!asJson) {
5556
+ console.log(
5557
+ `Recovered stale runtime process on ${runtimeListen} (PID: ${discovered.pid}).`
5558
+ );
5559
+ }
5560
+ } else if (discovered.command) {
5561
+ const details = discovered.command ? `PID ${discovered.pid} (${discovered.command})` : `PID ${discovered.pid}`;
5562
+ throw new Error(
5563
+ `Runtime proxy listen ${runtimeListen} is already in use by non-fiber-pay process: ${details}`
5564
+ );
5565
+ } else {
5566
+ throw new Error(
5567
+ `Runtime proxy listen ${runtimeListen} is already in use by process PID ${discovered.pid}. Unable to determine the owning command; inspect this PID manually before retrying.`
5568
+ );
5569
+ }
5570
+ }
4671
5571
  if (daemon && !isRuntimeChild) {
4672
5572
  const childArgv = process.argv.filter((arg) => arg !== "--daemon");
4673
5573
  const child = spawn2(process.execPath, childArgv.slice(1), {
@@ -4710,7 +5610,7 @@ function createRuntimeCommand(config) {
4710
5610
  ),
4711
5611
  proxy: {
4712
5612
  enabled: true,
4713
- listen: String(options.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229")
5613
+ listen: runtimeListen
4714
5614
  },
4715
5615
  storage: {
4716
5616
  stateFilePath: options.stateFile ? resolve(String(options.stateFile)) : resolve(config.dataDir, "runtime-state.json"),
@@ -4721,9 +5621,21 @@ function createRuntimeCommand(config) {
4721
5621
  dbPath: resolve(config.dataDir, "runtime-jobs.db")
4722
5622
  }
4723
5623
  };
4724
- const alertLogFile = options.alertLogFile ? resolve(String(options.alertLogFile)) : resolve(config.dataDir, "logs", "runtime.alerts.jsonl");
5624
+ let alertLogsBaseDir;
5625
+ let alertLogFile;
5626
+ if (options.alertLogsBaseDir) {
5627
+ alertLogsBaseDir = resolve(String(options.alertLogsBaseDir));
5628
+ } else if (options.alertLogFile) {
5629
+ alertLogFile = resolve(String(options.alertLogFile));
5630
+ } else {
5631
+ alertLogsBaseDir = resolve(config.dataDir, "logs");
5632
+ }
4725
5633
  const alerts = [{ type: "stdout" }];
4726
- alerts.push({ type: "file", path: alertLogFile });
5634
+ if (alertLogsBaseDir) {
5635
+ alerts.push({ type: "daily-file", baseLogsDir: alertLogsBaseDir });
5636
+ } else if (alertLogFile) {
5637
+ alerts.push({ type: "file", path: alertLogFile });
5638
+ }
4727
5639
  if (options.webhook) {
4728
5640
  alerts.push({ type: "webhook", url: String(options.webhook) });
4729
5641
  }
@@ -4745,6 +5657,12 @@ function createRuntimeCommand(config) {
4745
5657
  }
4746
5658
  const runtime2 = await startRuntimeService2(runtimeConfig);
4747
5659
  const status = runtime2.service.getStatus();
5660
+ const logsBaseDir = alertLogsBaseDir ?? resolve(config.dataDir, "logs");
5661
+ const todayLogDir = resolveLogDirForDateWithOptions(config.dataDir, void 0, {
5662
+ logsBaseDir,
5663
+ ensureExists: false
5664
+ });
5665
+ const effectiveAlertLogPath = alertLogsBaseDir ? join8(todayLogDir, "runtime.alerts.jsonl") : alertLogFile ?? join8(todayLogDir, "runtime.alerts.jsonl");
4748
5666
  writeRuntimePid(config.dataDir, process.pid);
4749
5667
  writeRuntimeMeta(config.dataDir, {
4750
5668
  pid: process.pid,
@@ -4752,7 +5670,10 @@ function createRuntimeCommand(config) {
4752
5670
  fiberRpcUrl: status.targetUrl,
4753
5671
  proxyListen: status.proxyListen,
4754
5672
  stateFilePath: runtimeConfig.storage?.stateFilePath,
4755
- alertLogFilePath: alertLogFile,
5673
+ alertLogFilePath: effectiveAlertLogPath,
5674
+ fnnStdoutLogPath: join8(todayLogDir, "fnn.stdout.log"),
5675
+ fnnStderrLogPath: join8(todayLogDir, "fnn.stderr.log"),
5676
+ logsBaseDir,
4756
5677
  daemon: daemon || isRuntimeChild
4757
5678
  });
4758
5679
  runtime2.service.on("alert", (alert) => {
@@ -4771,14 +5692,16 @@ function createRuntimeCommand(config) {
4771
5692
  fiberRpcUrl: status.targetUrl,
4772
5693
  proxyListen: status.proxyListen,
4773
5694
  stateFilePath: runtimeConfig.storage?.stateFilePath,
4774
- alertLogFile
5695
+ alertLogFile: effectiveAlertLogPath,
5696
+ logsBaseDir
4775
5697
  });
4776
5698
  printJsonEvent("runtime_started", status);
4777
5699
  } else {
4778
5700
  console.log(`Fiber RPC: ${status.targetUrl}`);
4779
5701
  console.log(`Proxy listen: ${status.proxyListen}`);
4780
5702
  console.log(`State file: ${runtimeConfig.storage?.stateFilePath}`);
4781
- console.log(`Alert log: ${alertLogFile}`);
5703
+ console.log(`Logs dir: ${logsBaseDir}`);
5704
+ console.log(`Alert log: ${effectiveAlertLogPath}`);
4782
5705
  console.log("Runtime monitor is running. Press Ctrl+C to stop.");
4783
5706
  }
4784
5707
  const signal = await runtime2.waitForShutdownSignal();
@@ -4812,8 +5735,42 @@ function createRuntimeCommand(config) {
4812
5735
  });
4813
5736
  runtime.command("status").description("Show runtime process and health status").option("--json").action(async (options) => {
4814
5737
  const asJson = Boolean(options.json);
4815
- const pid = readRuntimePid(config.dataDir);
5738
+ let pid = readRuntimePid(config.dataDir);
4816
5739
  const meta = readRuntimeMeta(config.dataDir);
5740
+ const recoveryListen = resolveRuntimeRecoveryListen(config);
5741
+ if (!pid) {
5742
+ const fallback = findListeningProcessByPort(recoveryListen);
5743
+ if (fallback && isFiberRuntimeCommand(fallback.command)) {
5744
+ pid = fallback.pid;
5745
+ writeRuntimePid(config.dataDir, pid);
5746
+ } else if (fallback?.command) {
5747
+ const details = fallback.command ? `PID ${fallback.pid} (${fallback.command})` : `PID ${fallback.pid}`;
5748
+ if (asJson) {
5749
+ printJsonError({
5750
+ code: "RUNTIME_PORT_IN_USE",
5751
+ message: `Runtime proxy port is in use by non-fiber-pay process: ${details}`,
5752
+ recoverable: true,
5753
+ suggestion: "Stop that process or use a different --proxy-listen port."
5754
+ });
5755
+ } else {
5756
+ console.log(`Runtime proxy port is in use by non-fiber-pay process: ${details}`);
5757
+ }
5758
+ process.exit(1);
5759
+ } else if (fallback) {
5760
+ const message = `Runtime proxy port is in use by process PID ${fallback.pid}. The owning command could not be determined; inspect this PID manually.`;
5761
+ if (asJson) {
5762
+ printJsonError({
5763
+ code: "RUNTIME_PORT_IN_USE",
5764
+ message,
5765
+ recoverable: true,
5766
+ suggestion: "Inspect the PID owner manually or use a different --proxy-listen port."
5767
+ });
5768
+ } else {
5769
+ console.log(message);
5770
+ }
5771
+ process.exit(1);
5772
+ }
5773
+ }
4817
5774
  if (!pid) {
4818
5775
  if (asJson) {
4819
5776
  printJsonError({
@@ -4844,9 +5801,11 @@ function createRuntimeCommand(config) {
4844
5801
  process.exit(1);
4845
5802
  }
4846
5803
  let rpcStatus;
4847
- if (meta?.proxyListen) {
5804
+ if (meta?.proxyListen ?? recoveryListen) {
4848
5805
  try {
4849
- const response = await fetch(`http://${meta.proxyListen}/monitor/status`);
5806
+ const response = await fetch(
5807
+ `http://${meta?.proxyListen ?? recoveryListen}/monitor/status`
5808
+ );
4850
5809
  if (response.ok) {
4851
5810
  rpcStatus = await response.json();
4852
5811
  }
@@ -4874,7 +5833,41 @@ function createRuntimeCommand(config) {
4874
5833
  });
4875
5834
  runtime.command("stop").description("Stop runtime process by PID").option("--json").action(async (options) => {
4876
5835
  const asJson = Boolean(options.json);
4877
- const pid = readRuntimePid(config.dataDir);
5836
+ let pid = readRuntimePid(config.dataDir);
5837
+ const recoveryListen = resolveRuntimeRecoveryListen(config);
5838
+ if (!pid) {
5839
+ const fallback = findListeningProcessByPort(recoveryListen);
5840
+ if (fallback && isFiberRuntimeCommand(fallback.command)) {
5841
+ pid = fallback.pid;
5842
+ writeRuntimePid(config.dataDir, pid);
5843
+ } else if (fallback?.command) {
5844
+ const details = fallback.command ? `PID ${fallback.pid} (${fallback.command})` : `PID ${fallback.pid}`;
5845
+ if (asJson) {
5846
+ printJsonError({
5847
+ code: "RUNTIME_PORT_IN_USE",
5848
+ message: `Runtime proxy port is in use by non-fiber-pay process: ${details}`,
5849
+ recoverable: true,
5850
+ suggestion: "Stop that process manually; it is not managed by fiber-pay runtime PID files."
5851
+ });
5852
+ } else {
5853
+ console.log(`Runtime proxy port is in use by non-fiber-pay process: ${details}`);
5854
+ }
5855
+ process.exit(1);
5856
+ } else if (fallback) {
5857
+ const message = `Runtime proxy port is in use by process PID ${fallback.pid}. The owning command could not be determined; inspect this PID manually.`;
5858
+ if (asJson) {
5859
+ printJsonError({
5860
+ code: "RUNTIME_PORT_IN_USE",
5861
+ message,
5862
+ recoverable: true,
5863
+ suggestion: "Inspect the PID owner manually; it may not be managed by fiber-pay runtime PID files."
5864
+ });
5865
+ } else {
5866
+ console.log(message);
5867
+ }
5868
+ process.exit(1);
5869
+ }
5870
+ }
4878
5871
  if (!pid) {
4879
5872
  if (asJson) {
4880
5873
  printJsonError({
@@ -4903,14 +5896,19 @@ function createRuntimeCommand(config) {
4903
5896
  }
4904
5897
  process.exit(1);
4905
5898
  }
4906
- process.kill(pid, "SIGTERM");
4907
- let attempts = 0;
4908
- while (isProcessRunning(pid) && attempts < 50) {
4909
- await new Promise((resolve2) => setTimeout(resolve2, 100));
4910
- attempts += 1;
4911
- }
4912
- if (isProcessRunning(pid)) {
4913
- process.kill(pid, "SIGKILL");
5899
+ const terminated = await terminateProcess(pid);
5900
+ if (!terminated) {
5901
+ if (asJson) {
5902
+ printJsonError({
5903
+ code: "RUNTIME_STOP_FAILED",
5904
+ message: `Failed to terminate runtime process ${pid}.`,
5905
+ recoverable: true,
5906
+ suggestion: `Try stopping PID ${pid} manually and rerun runtime stop.`
5907
+ });
5908
+ } else {
5909
+ console.log(`Failed to terminate runtime process ${pid}.`);
5910
+ }
5911
+ process.exit(1);
4914
5912
  }
4915
5913
  removeRuntimeFiles(config.dataDir);
4916
5914
  if (asJson) {
@@ -4926,8 +5924,8 @@ function createRuntimeCommand(config) {
4926
5924
  import { Command as Command12 } from "commander";
4927
5925
 
4928
5926
  // src/lib/build-info.ts
4929
- var CLI_VERSION = "0.1.0-rc.4";
4930
- var CLI_COMMIT = "20dc82d290856d411382a4ec30f912b913b4f956";
5927
+ var CLI_VERSION = "0.1.0-rc.6";
5928
+ var CLI_COMMIT = "632dc5658ea5122cea5af371decc1b758b004461";
4931
5929
 
4932
5930
  // src/commands/version.ts
4933
5931
  function createVersionCommand() {
@@ -4945,6 +5943,64 @@ function createVersionCommand() {
4945
5943
  });
4946
5944
  }
4947
5945
 
5946
+ // src/commands/wallet.ts
5947
+ import { Command as Command13 } from "commander";
5948
+
5949
+ // src/lib/wallet-address.ts
5950
+ import { scriptToAddress as scriptToAddress2 } from "@fiber-pay/sdk";
5951
+ async function runWalletAddressCommand(config, options) {
5952
+ const rpc = await createReadyRpcClient(config);
5953
+ const nodeInfo = await rpc.nodeInfo();
5954
+ const address = scriptToAddress2(
5955
+ nodeInfo.default_funding_lock_script,
5956
+ config.network === "mainnet" ? "mainnet" : "testnet"
5957
+ );
5958
+ if (options.json) {
5959
+ printJsonSuccess({ address });
5960
+ return;
5961
+ }
5962
+ console.log("\u2705 Funding address retrieved");
5963
+ console.log(` Address: ${address}`);
5964
+ }
5965
+
5966
+ // src/lib/wallet-balance.ts
5967
+ async function runWalletBalanceCommand(config, options) {
5968
+ if (!config.ckbRpcUrl) {
5969
+ throw new Error(
5970
+ "CKB RPC URL is not configured. Set FIBER_CKB_RPC_URL or add ckb.rpc_url to config.yml."
5971
+ );
5972
+ }
5973
+ const rpc = await createReadyRpcClient(config);
5974
+ const nodeInfo = await rpc.nodeInfo();
5975
+ const balanceShannons = await getLockBalanceShannons(
5976
+ config.ckbRpcUrl,
5977
+ nodeInfo.default_funding_lock_script
5978
+ );
5979
+ const balanceCkb = formatShannonsAsCkb(balanceShannons, 8);
5980
+ if (options.json) {
5981
+ printJsonSuccess({
5982
+ balance_ckb: balanceCkb,
5983
+ balance_shannons: balanceShannons.toString()
5984
+ });
5985
+ return;
5986
+ }
5987
+ console.log("\u2705 CKB balance retrieved");
5988
+ console.log(` Balance: ${balanceCkb} CKB`);
5989
+ console.log(` Balance (shannons): ${balanceShannons.toString()}`);
5990
+ }
5991
+
5992
+ // src/commands/wallet.ts
5993
+ function createWalletCommand(config) {
5994
+ const wallet = new Command13("wallet").description("Wallet management");
5995
+ wallet.command("address").description("Display the funding address").option("--json").action(async (options) => {
5996
+ await runWalletAddressCommand(config, options);
5997
+ });
5998
+ wallet.command("balance").description("Display the CKB balance").option("--json").action(async (options) => {
5999
+ await runWalletBalanceCommand(config, options);
6000
+ });
6001
+ return wallet;
6002
+ }
6003
+
4948
6004
  // src/lib/argv.ts
4949
6005
  var GLOBAL_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
4950
6006
  "--profile",
@@ -5035,6 +6091,14 @@ function applyGlobalOverrides(argv) {
5035
6091
  }
5036
6092
  break;
5037
6093
  }
6094
+ case "--rpc-biscuit-token": {
6095
+ const value = getFlagValue(argv, index);
6096
+ if (value) {
6097
+ process.env.FIBER_RPC_BISCUIT_TOKEN = value;
6098
+ explicitFlags.add("rpcBiscuitToken");
6099
+ }
6100
+ break;
6101
+ }
5038
6102
  case "--network": {
5039
6103
  const value = getFlagValue(argv, index);
5040
6104
  if (value) {
@@ -5074,7 +6138,7 @@ function applyGlobalOverrides(argv) {
5074
6138
  }
5075
6139
  if (!explicitDataDir && profileName) {
5076
6140
  const homeDir = process.env.HOME ?? process.cwd();
5077
- process.env.FIBER_DATA_DIR = join7(homeDir, ".fiber-pay", "profiles", profileName);
6141
+ process.env.FIBER_DATA_DIR = join9(homeDir, ".fiber-pay", "profiles", profileName);
5078
6142
  }
5079
6143
  }
5080
6144
  function printFatal(error) {
@@ -5102,8 +6166,8 @@ async function main() {
5102
6166
  }
5103
6167
  applyGlobalOverrides(argv);
5104
6168
  const config = getEffectiveConfig(explicitFlags).config;
5105
- const program = new Command13();
5106
- program.name("fiber-pay").description("AI Agent Payment SDK for CKB Lightning Network").option("--profile <name>", "Use profile at ~/.fiber-pay/profiles/<name>").option("--data-dir <path>", "Override data directory for all commands").option("--rpc-url <url>", "Override RPC URL for all commands").option("--network <network>", "Override network for all commands (testnet|mainnet)").option("--key-password <password>", "Override key password for all commands").option("--binary-path <path>", "Override fiber binary path for all commands").showHelpAfterError().showSuggestionAfterError();
6169
+ const program = new Command14();
6170
+ program.name("fiber-pay").description("AI Agent Payment SDK for CKB Lightning Network").option("--profile <name>", "Use profile at ~/.fiber-pay/profiles/<name>").option("--data-dir <path>", "Override data directory for all commands").option("--rpc-url <url>", "Override RPC URL for all commands").option("--rpc-biscuit-token <token>", "Set RPC Authorization Bearer token for all commands").option("--network <network>", "Override network for all commands (testnet|mainnet)").option("--key-password <password>", "Override key password for all commands").option("--binary-path <path>", "Override fiber binary path for all commands").showHelpAfterError().showSuggestionAfterError();
5107
6171
  program.exitOverride();
5108
6172
  program.configureOutput({
5109
6173
  writeOut: (str) => process.stdout.write(str),
@@ -5125,6 +6189,7 @@ async function main() {
5125
6189
  program.addCommand(createConfigCommand(config));
5126
6190
  program.addCommand(createRuntimeCommand(config));
5127
6191
  program.addCommand(createVersionCommand());
6192
+ program.addCommand(createWalletCommand(config));
5128
6193
  await program.parseAsync(argv);
5129
6194
  }
5130
6195
  main().catch((error) => {