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

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,17 +1,168 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { join as join7 } from "path";
4
+ import { join as join9 } from "path";
5
5
  import { Command as Command13 } 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,
@@ -284,21 +435,6 @@ function printPeerListHuman(peers) {
284
435
  console.log(`${peerId} ${pubkey} ${peer.address}`);
285
436
  }
286
437
  }
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
438
 
303
439
  // src/commands/binary.ts
304
440
  function showProgress(progress) {
@@ -311,14 +447,36 @@ function showProgress(progress) {
311
447
  function createBinaryCommand(config) {
312
448
  const binary = new Command("binary").description("Fiber binary management");
313
449
  binary.command("download").option("--version <version>", "Fiber binary version", DEFAULT_FIBER_VERSION).option("--force", "Force re-download").option("--json").action(async (options) => {
450
+ const resolvedBinary = resolveBinaryPath(config);
451
+ let installDir;
452
+ try {
453
+ installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
454
+ } catch (error) {
455
+ const message = error instanceof Error ? error.message : String(error);
456
+ if (options.json) {
457
+ printJsonError({
458
+ code: "BINARY_PATH_INCOMPATIBLE",
459
+ message,
460
+ recoverable: true,
461
+ suggestion: "Use `fiber-pay config profile unset binaryPath` or set binaryPath to a standard fnn filename in the target directory."
462
+ });
463
+ } else {
464
+ console.error(`\u274C ${message}`);
465
+ }
466
+ process.exit(1);
467
+ }
314
468
  const info = await downloadFiberBinary({
315
- installDir: `${config.dataDir}/bin`,
469
+ installDir,
316
470
  version: options.version,
317
471
  force: Boolean(options.force),
318
472
  onProgress: options.json ? void 0 : showProgress
319
473
  });
320
474
  if (options.json) {
321
- printJsonSuccess(info);
475
+ printJsonSuccess({
476
+ ...info,
477
+ source: resolvedBinary.source,
478
+ resolvedPath: resolvedBinary.binaryPath
479
+ });
322
480
  } else {
323
481
  console.log("\n\u2705 Binary installed successfully!");
324
482
  console.log(` Path: ${info.path}`);
@@ -327,9 +485,13 @@ function createBinaryCommand(config) {
327
485
  }
328
486
  });
329
487
  binary.command("info").option("--json").action(async (options) => {
330
- const info = await getFiberBinaryInfo(`${config.dataDir}/bin`);
488
+ const { resolvedBinary, info } = await getBinaryDetails(config);
331
489
  if (options.json) {
332
- printJsonSuccess(info);
490
+ printJsonSuccess({
491
+ ...info,
492
+ source: resolvedBinary.source,
493
+ resolvedPath: resolvedBinary.binaryPath
494
+ });
333
495
  } else {
334
496
  console.log(info.ready ? "\u2705 Binary is ready" : "\u274C Binary not found or not executable");
335
497
  console.log(` Path: ${info.path}`);
@@ -341,7 +503,7 @@ function createBinaryCommand(config) {
341
503
 
342
504
  // src/commands/channel.ts
343
505
  import { randomUUID } from "crypto";
344
- import { ckbToShannons } from "@fiber-pay/sdk";
506
+ import { ckbToShannons as ckbToShannons2 } from "@fiber-pay/sdk";
345
507
  import { Command as Command2 } from "commander";
346
508
 
347
509
  // src/lib/async.ts
@@ -353,17 +515,17 @@ function sleep(ms) {
353
515
  import { FiberRpcClient } from "@fiber-pay/sdk";
354
516
 
355
517
  // src/lib/pid.ts
356
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
357
- import { join } from "path";
518
+ import { existsSync as existsSync2, readFileSync, unlinkSync, writeFileSync } from "fs";
519
+ import { join as join2 } from "path";
358
520
  function getPidFilePath(dataDir) {
359
- return join(dataDir, "fiber.pid");
521
+ return join2(dataDir, "fiber.pid");
360
522
  }
361
523
  function writePidFile(dataDir, pid) {
362
524
  writeFileSync(getPidFilePath(dataDir), String(pid));
363
525
  }
364
526
  function readPidFile(dataDir) {
365
527
  const pidPath = getPidFilePath(dataDir);
366
- if (!existsSync(pidPath)) return null;
528
+ if (!existsSync2(pidPath)) return null;
367
529
  try {
368
530
  return parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
369
531
  } catch {
@@ -372,7 +534,7 @@ function readPidFile(dataDir) {
372
534
  }
373
535
  function removePidFile(dataDir) {
374
536
  const pidPath = getPidFilePath(dataDir);
375
- if (existsSync(pidPath)) {
537
+ if (existsSync2(pidPath)) {
376
538
  unlinkSync(pidPath);
377
539
  }
378
540
  }
@@ -386,20 +548,20 @@ function isProcessRunning(pid) {
386
548
  }
387
549
 
388
550
  // 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";
551
+ import { existsSync as existsSync3, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
552
+ import { join as join3 } from "path";
391
553
  function getRuntimePidFilePath(dataDir) {
392
- return join2(dataDir, "runtime.pid");
554
+ return join3(dataDir, "runtime.pid");
393
555
  }
394
556
  function getRuntimeMetaFilePath(dataDir) {
395
- return join2(dataDir, "runtime.meta.json");
557
+ return join3(dataDir, "runtime.meta.json");
396
558
  }
397
559
  function writeRuntimePid(dataDir, pid) {
398
560
  writeFileSync2(getRuntimePidFilePath(dataDir), String(pid));
399
561
  }
400
562
  function readRuntimePid(dataDir) {
401
563
  const pidPath = getRuntimePidFilePath(dataDir);
402
- if (!existsSync2(pidPath)) return null;
564
+ if (!existsSync3(pidPath)) return null;
403
565
  try {
404
566
  return Number.parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
405
567
  } catch {
@@ -411,7 +573,7 @@ function writeRuntimeMeta(dataDir, meta) {
411
573
  }
412
574
  function readRuntimeMeta(dataDir) {
413
575
  const metaPath = getRuntimeMetaFilePath(dataDir);
414
- if (!existsSync2(metaPath)) return null;
576
+ if (!existsSync3(metaPath)) return null;
415
577
  try {
416
578
  return JSON.parse(readFileSync2(metaPath, "utf-8"));
417
579
  } catch {
@@ -421,10 +583,10 @@ function readRuntimeMeta(dataDir) {
421
583
  function removeRuntimeFiles(dataDir) {
422
584
  const pidPath = getRuntimePidFilePath(dataDir);
423
585
  const metaPath = getRuntimeMetaFilePath(dataDir);
424
- if (existsSync2(pidPath)) {
586
+ if (existsSync3(pidPath)) {
425
587
  unlinkSync2(pidPath);
426
588
  }
427
- if (existsSync2(metaPath)) {
589
+ if (existsSync3(metaPath)) {
428
590
  unlinkSync2(metaPath);
429
591
  }
430
592
  }
@@ -457,7 +619,10 @@ function resolveRuntimeProxyUrl(config) {
457
619
  }
458
620
  function createRpcClient(config) {
459
621
  const resolved = resolveRpcEndpoint(config);
460
- return new FiberRpcClient({ url: resolved.url });
622
+ return new FiberRpcClient({
623
+ url: resolved.url,
624
+ biscuitToken: config.rpcBiscuitToken
625
+ });
461
626
  }
462
627
  function resolveRpcEndpoint(config) {
463
628
  const runtimeProxyUrl = resolveRuntimeProxyUrl(config);
@@ -534,6 +699,235 @@ async function waitForRuntimeJobTerminal(runtimeUrl, jobId, timeoutSeconds) {
534
699
  throw new Error(`Timed out waiting for runtime job ${jobId}`);
535
700
  }
536
701
 
702
+ // src/commands/rebalance.ts
703
+ import { ckbToShannons, shannonsToCkb as shannonsToCkb2 } from "@fiber-pay/sdk";
704
+ async function executeRebalance(config, params) {
705
+ const rpc = await createReadyRpcClient(config);
706
+ const amountCkb = parseFloat(params.amountInput);
707
+ const maxFeeCkb = params.maxFeeInput !== void 0 ? parseFloat(params.maxFeeInput) : void 0;
708
+ const manualHops = params.hops ?? [];
709
+ if (!Number.isFinite(amountCkb) || amountCkb <= 0) {
710
+ const message = "Invalid --amount value. Expected a positive CKB amount.";
711
+ if (params.json) {
712
+ printJsonError({
713
+ code: params.errorCode,
714
+ message,
715
+ recoverable: true,
716
+ suggestion: "Provide a positive number, e.g. `--amount 10`.",
717
+ details: { amount: params.amountInput }
718
+ });
719
+ } else {
720
+ console.error(`Error: ${message}`);
721
+ }
722
+ process.exit(1);
723
+ }
724
+ if (maxFeeCkb !== void 0 && (!Number.isFinite(maxFeeCkb) || maxFeeCkb < 0 || manualHops.length > 0)) {
725
+ 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.";
726
+ if (params.json) {
727
+ printJsonError({
728
+ code: params.errorCode,
729
+ message,
730
+ recoverable: true,
731
+ 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`.",
732
+ details: { maxFee: params.maxFeeInput, hasManualHops: manualHops.length > 0 }
733
+ });
734
+ } else {
735
+ console.error(`Error: ${message}`);
736
+ }
737
+ process.exit(1);
738
+ }
739
+ const selfPubkey = (await rpc.nodeInfo()).node_id;
740
+ const amount = ckbToShannons(amountCkb);
741
+ const isManual = manualHops.length > 0;
742
+ let routeHopCount;
743
+ const result = isManual ? await (async () => {
744
+ const hopsInfo = [
745
+ ...manualHops.map((pubkey) => ({ pubkey })),
746
+ ...manualHops[manualHops.length - 1] === selfPubkey ? [] : [{ pubkey: selfPubkey }]
747
+ ];
748
+ const route = await rpc.buildRouter({
749
+ amount,
750
+ hops_info: hopsInfo
751
+ });
752
+ routeHopCount = route.router_hops.length;
753
+ return rpc.sendPaymentWithRouter({
754
+ router: route.router_hops,
755
+ keysend: true,
756
+ allow_self_payment: true,
757
+ dry_run: params.dryRun ? true : void 0
758
+ });
759
+ })() : await rpc.sendPayment({
760
+ target_pubkey: selfPubkey,
761
+ amount,
762
+ keysend: true,
763
+ allow_self_payment: true,
764
+ max_fee_amount: maxFeeCkb !== void 0 ? ckbToShannons(maxFeeCkb) : void 0,
765
+ dry_run: params.dryRun ? true : void 0
766
+ });
767
+ const payload = {
768
+ mode: isManual ? "manual" : "auto",
769
+ selfPubkey,
770
+ amountCkb,
771
+ maxFeeCkb: isManual ? void 0 : maxFeeCkb,
772
+ routeHopCount,
773
+ paymentHash: result.payment_hash,
774
+ status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
775
+ feeCkb: shannonsToCkb2(result.fee),
776
+ failureReason: result.failed_error,
777
+ dryRun: params.dryRun
778
+ };
779
+ if (params.json) {
780
+ printJsonSuccess(payload);
781
+ } else {
782
+ console.log(
783
+ payload.dryRun ? `Rebalance dry-run complete (${payload.mode} route)` : `Rebalance sent (${payload.mode} route)`
784
+ );
785
+ console.log(` Self: ${payload.selfPubkey}`);
786
+ console.log(` Amount: ${payload.amountCkb} CKB`);
787
+ if (payload.mode === "manual" && payload.routeHopCount !== void 0) {
788
+ console.log(` Hops: ${payload.routeHopCount}`);
789
+ }
790
+ console.log(` Hash: ${payload.paymentHash}`);
791
+ console.log(` Status: ${payload.status}`);
792
+ console.log(` Fee: ${payload.feeCkb} CKB`);
793
+ if (payload.mode === "auto" && payload.maxFeeCkb !== void 0) {
794
+ console.log(` MaxFee: ${payload.maxFeeCkb} CKB`);
795
+ }
796
+ if (payload.failureReason) {
797
+ console.log(` Error: ${payload.failureReason}`);
798
+ }
799
+ }
800
+ }
801
+ function registerPaymentRebalanceCommand(parent, config) {
802
+ 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(
803
+ "--hops <pubkeys>",
804
+ "Comma-separated peer pubkeys for manual route mode (self pubkey appended automatically)"
805
+ ).option("--dry-run", "Simulate route/payment and return estimated result").option("--json").action(async (options) => {
806
+ const hasHopsOption = typeof options.hops === "string";
807
+ const manualHops = hasHopsOption ? options.hops.split(",").map((item) => item.trim()).filter(Boolean) : [];
808
+ if (hasHopsOption && manualHops.length === 0) {
809
+ const message = "Invalid --hops value. Expected a non-empty comma-separated list of pubkeys.";
810
+ if (options.json) {
811
+ printJsonError({
812
+ code: "PAYMENT_REBALANCE_INPUT_INVALID",
813
+ message,
814
+ recoverable: true,
815
+ suggestion: "Provide pubkeys like `--hops 0xabc...,0xdef...`.",
816
+ details: { hops: options.hops }
817
+ });
818
+ } else {
819
+ console.error(`Error: ${message}`);
820
+ }
821
+ process.exit(1);
822
+ }
823
+ await executeRebalance(config, {
824
+ amountInput: options.amount,
825
+ maxFeeInput: options.maxFee,
826
+ hops: manualHops,
827
+ dryRun: Boolean(options.dryRun),
828
+ json: Boolean(options.json),
829
+ errorCode: "PAYMENT_REBALANCE_INPUT_INVALID"
830
+ });
831
+ });
832
+ }
833
+ function registerChannelRebalanceCommand(parent, config) {
834
+ 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) => {
835
+ const json = Boolean(options.json);
836
+ const fromChannelId = options.fromChannel;
837
+ const toChannelId = options.toChannel;
838
+ if (fromChannelId && !toChannelId || !fromChannelId && toChannelId) {
839
+ const message = "Both --from-channel and --to-channel must be provided together for guided channel rebalance.";
840
+ if (json) {
841
+ printJsonError({
842
+ code: "CHANNEL_REBALANCE_INPUT_INVALID",
843
+ message,
844
+ recoverable: true,
845
+ suggestion: "Provide both channel ids, or provide neither to run auto mode.",
846
+ details: { fromChannel: fromChannelId, toChannel: toChannelId }
847
+ });
848
+ } else {
849
+ console.error(`Error: ${message}`);
850
+ }
851
+ process.exit(1);
852
+ }
853
+ let guidedHops;
854
+ if (fromChannelId && toChannelId) {
855
+ const rpc = await createReadyRpcClient(config);
856
+ const channels = (await rpc.listChannels({ include_closed: true })).channels;
857
+ const fromChannel = channels.find((item) => item.channel_id === fromChannelId);
858
+ const toChannel = channels.find((item) => item.channel_id === toChannelId);
859
+ if (!fromChannel || !toChannel) {
860
+ const message = "Invalid channel selection: source/target channel id not found.";
861
+ if (json) {
862
+ printJsonError({
863
+ code: "CHANNEL_REBALANCE_INPUT_INVALID",
864
+ message,
865
+ recoverable: true,
866
+ suggestion: "Run `channel list --json` and retry with valid channel ids.",
867
+ details: { fromChannel: fromChannelId, toChannel: toChannelId }
868
+ });
869
+ } else {
870
+ console.error(`Error: ${message}`);
871
+ }
872
+ process.exit(1);
873
+ }
874
+ if (fromChannel.peer_id === toChannel.peer_id) {
875
+ const message = "Source and target channels point to the same peer; choose two different channel peers.";
876
+ if (json) {
877
+ printJsonError({
878
+ code: "CHANNEL_REBALANCE_INPUT_INVALID",
879
+ message,
880
+ recoverable: true,
881
+ suggestion: "Select channels with different peer ids for guided rebalance.",
882
+ details: {
883
+ fromChannel: fromChannelId,
884
+ toChannel: toChannelId,
885
+ peerId: fromChannel.peer_id
886
+ }
887
+ });
888
+ } else {
889
+ console.error(`Error: ${message}`);
890
+ }
891
+ process.exit(1);
892
+ }
893
+ const peers = (await rpc.listPeers()).peers;
894
+ const pubkeyByPeerId = new Map(peers.map((peer) => [peer.peer_id, peer.pubkey]));
895
+ const fromPubkey = pubkeyByPeerId.get(fromChannel.peer_id);
896
+ const toPubkey = pubkeyByPeerId.get(toChannel.peer_id);
897
+ if (!fromPubkey || !toPubkey) {
898
+ const message = "Unable to resolve selected channel peer_id to pubkey for guided rebalance route.";
899
+ if (json) {
900
+ printJsonError({
901
+ code: "CHANNEL_REBALANCE_INPUT_INVALID",
902
+ message,
903
+ recoverable: true,
904
+ suggestion: "Ensure both peers are connected (`peer list --json`) and retry guided mode, or use `payment rebalance --hops`.",
905
+ details: {
906
+ fromChannel: fromChannelId,
907
+ toChannel: toChannelId,
908
+ fromPeerId: fromChannel.peer_id,
909
+ toPeerId: toChannel.peer_id,
910
+ resolvedPeers: peers.length
911
+ }
912
+ });
913
+ } else {
914
+ console.error(`Error: ${message}`);
915
+ }
916
+ process.exit(1);
917
+ }
918
+ guidedHops = [fromPubkey, toPubkey];
919
+ }
920
+ await executeRebalance(config, {
921
+ amountInput: options.amount,
922
+ maxFeeInput: options.maxFee,
923
+ hops: guidedHops,
924
+ dryRun: Boolean(options.dryRun),
925
+ json,
926
+ errorCode: "CHANNEL_REBALANCE_INPUT_INVALID"
927
+ });
928
+ });
929
+ }
930
+
537
931
  // src/commands/channel.ts
538
932
  function createChannelCommand(config) {
539
933
  const channel = new Command2("channel").description("Channel lifecycle and status commands");
@@ -708,7 +1102,7 @@ function createChannelCommand(config) {
708
1102
  peerId,
709
1103
  openChannelParams: {
710
1104
  peer_id: peerId,
711
- funding_amount: ckbToShannons(fundingCkb),
1105
+ funding_amount: ckbToShannons2(fundingCkb),
712
1106
  public: !options.private
713
1107
  },
714
1108
  waitForReady: false
@@ -741,7 +1135,7 @@ function createChannelCommand(config) {
741
1135
  }
742
1136
  const result = await rpc.openChannel({
743
1137
  peer_id: peerId,
744
- funding_amount: ckbToShannons(fundingCkb),
1138
+ funding_amount: ckbToShannons2(fundingCkb),
745
1139
  public: !options.private
746
1140
  });
747
1141
  const payload = { temporaryChannelId: result.temporary_channel_id, peer: peerId, fundingCkb };
@@ -765,7 +1159,7 @@ function createChannelCommand(config) {
765
1159
  action: "accept",
766
1160
  acceptChannelParams: {
767
1161
  temporary_channel_id: temporaryChannelId,
768
- funding_amount: ckbToShannons(fundingCkb)
1162
+ funding_amount: ckbToShannons2(fundingCkb)
769
1163
  }
770
1164
  },
771
1165
  options: {
@@ -793,7 +1187,7 @@ function createChannelCommand(config) {
793
1187
  }
794
1188
  const result = await rpc.acceptChannel({
795
1189
  temporary_channel_id: temporaryChannelId,
796
- funding_amount: ckbToShannons(fundingCkb)
1190
+ funding_amount: ckbToShannons2(fundingCkb)
797
1191
  });
798
1192
  const payload = { channelId: result.channel_id, temporaryChannelId, fundingCkb };
799
1193
  if (json) {
@@ -818,7 +1212,7 @@ function createChannelCommand(config) {
818
1212
  channel_id: channelId,
819
1213
  force: Boolean(options.force)
820
1214
  },
821
- waitForClosed: false
1215
+ waitForClosed: Boolean(options.force)
822
1216
  },
823
1217
  options: {
824
1218
  idempotencyKey: `shutdown:channel:${channelId}`
@@ -903,6 +1297,7 @@ function createChannelCommand(config) {
903
1297
  console.log(` Channel ID: ${payload.channelId}`);
904
1298
  }
905
1299
  });
1300
+ registerChannelRebalanceCommand(channel, config);
906
1301
  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
1302
  const rpc = await createReadyRpcClient(config);
908
1303
  const json = Boolean(options.json);
@@ -956,13 +1351,13 @@ function createChannelCommand(config) {
956
1351
  }
957
1352
 
958
1353
  // src/commands/config.ts
959
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
1354
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
960
1355
  import { Command as Command3 } from "commander";
961
1356
  import { parseDocument, stringify as yamlStringify } from "yaml";
962
1357
 
963
1358
  // 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";
1359
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
1360
+ import { join as join4 } from "path";
966
1361
 
967
1362
  // src/lib/config-templates.ts
968
1363
  var TESTNET_CONFIG_TEMPLATE_V071 = `# This configuration file only contains the necessary configurations for the testnet deployment.
@@ -1121,7 +1516,7 @@ var DEFAULT_DATA_DIR = `${process.env.HOME}/.fiber-pay`;
1121
1516
  var DEFAULT_RPC_URL = "http://127.0.0.1:8227";
1122
1517
  var DEFAULT_NETWORK = "testnet";
1123
1518
  function getConfigPath(dataDir) {
1124
- return join3(dataDir, "config.yml");
1519
+ return join4(dataDir, "config.yml");
1125
1520
  }
1126
1521
  function parseNetworkFromConfig(configContent) {
1127
1522
  const match = configContent.match(/^\s*chain:\s*(testnet|mainnet)\s*$/m);
@@ -1153,11 +1548,11 @@ function parseCkbRpcUrlFromConfig(configContent) {
1153
1548
  return match?.[1]?.trim() || void 0;
1154
1549
  }
1155
1550
  function getProfilePath(dataDir) {
1156
- return join3(dataDir, "profile.json");
1551
+ return join4(dataDir, "profile.json");
1157
1552
  }
1158
1553
  function loadProfileConfig(dataDir) {
1159
1554
  const profilePath = getProfilePath(dataDir);
1160
- if (!existsSync3(profilePath)) return void 0;
1555
+ if (!existsSync4(profilePath)) return void 0;
1161
1556
  try {
1162
1557
  const raw = readFileSync3(profilePath, "utf-8");
1163
1558
  return JSON.parse(raw);
@@ -1166,7 +1561,7 @@ function loadProfileConfig(dataDir) {
1166
1561
  }
1167
1562
  }
1168
1563
  function saveProfileConfig(dataDir, profile) {
1169
- if (!existsSync3(dataDir)) {
1564
+ if (!existsSync4(dataDir)) {
1170
1565
  mkdirSync(dataDir, { recursive: true });
1171
1566
  }
1172
1567
  const profilePath = getProfilePath(dataDir);
@@ -1175,11 +1570,11 @@ function saveProfileConfig(dataDir, profile) {
1175
1570
  }
1176
1571
  function writeNetworkConfigFile(dataDir, network, options = {}) {
1177
1572
  const configPath = getConfigPath(dataDir);
1178
- const alreadyExists = existsSync3(configPath);
1573
+ const alreadyExists = existsSync4(configPath);
1179
1574
  if (alreadyExists && !options.force) {
1180
1575
  return { path: configPath, created: false, overwritten: false };
1181
1576
  }
1182
- if (!existsSync3(dataDir)) {
1577
+ if (!existsSync4(dataDir)) {
1183
1578
  mkdirSync(dataDir, { recursive: true });
1184
1579
  }
1185
1580
  let content = getConfigTemplate(network);
@@ -1205,7 +1600,7 @@ function writeNetworkConfigFile(dataDir, network, options = {}) {
1205
1600
  }
1206
1601
  function ensureNodeConfigFile(dataDir, network) {
1207
1602
  const configPath = getConfigPath(dataDir);
1208
- if (!existsSync3(configPath)) {
1603
+ if (!existsSync4(configPath)) {
1209
1604
  writeNetworkConfigFile(dataDir, network);
1210
1605
  }
1211
1606
  return configPath;
@@ -1214,7 +1609,7 @@ function getEffectiveConfig(explicitFlags2) {
1214
1609
  const dataDir = process.env.FIBER_DATA_DIR || DEFAULT_DATA_DIR;
1215
1610
  const dataDirSource = explicitFlags2?.has("dataDir") ? "cli" : process.env.FIBER_DATA_DIR ? "env" : "default";
1216
1611
  const configPath = getConfigPath(dataDir);
1217
- const configExists = existsSync3(configPath);
1612
+ const configExists = existsSync4(configPath);
1218
1613
  const configContent = configExists ? readFileSync3(configPath, "utf-8") : void 0;
1219
1614
  const profile = loadProfileConfig(dataDir);
1220
1615
  const cliNetwork = explicitFlags2?.has("network") ? process.env.FIBER_NETWORK : void 0;
@@ -1227,6 +1622,10 @@ function getEffectiveConfig(explicitFlags2) {
1227
1622
  const fileRpcUrl = configContent ? parseRpcUrlFromConfig(configContent) : void 0;
1228
1623
  const rpcUrl = cliRpcUrl || envRpcUrl || fileRpcUrl || DEFAULT_RPC_URL;
1229
1624
  const rpcUrlSource = cliRpcUrl ? "cli" : envRpcUrl ? "env" : fileRpcUrl ? "config" : "default";
1625
+ const cliRpcBiscuitToken = explicitFlags2?.has("rpcBiscuitToken") ? process.env.FIBER_RPC_BISCUIT_TOKEN : void 0;
1626
+ const envRpcBiscuitToken = !explicitFlags2?.has("rpcBiscuitToken") ? process.env.FIBER_RPC_BISCUIT_TOKEN : void 0;
1627
+ const rpcBiscuitToken = cliRpcBiscuitToken || envRpcBiscuitToken || void 0;
1628
+ const rpcBiscuitTokenSource = cliRpcBiscuitToken ? "cli" : envRpcBiscuitToken ? "env" : "unset";
1230
1629
  const cliBinaryPath = explicitFlags2?.has("binaryPath") ? process.env.FIBER_BINARY_PATH : void 0;
1231
1630
  const profileBinaryPath = profile?.binaryPath;
1232
1631
  const envBinaryPath = !explicitFlags2?.has("binaryPath") ? process.env.FIBER_BINARY_PATH : void 0;
@@ -1253,6 +1652,7 @@ function getEffectiveConfig(explicitFlags2) {
1253
1652
  configPath,
1254
1653
  network,
1255
1654
  rpcUrl,
1655
+ rpcBiscuitToken,
1256
1656
  keyPassword,
1257
1657
  ckbRpcUrl,
1258
1658
  runtimeProxyListen
@@ -1262,6 +1662,7 @@ function getEffectiveConfig(explicitFlags2) {
1262
1662
  configPath: "derived",
1263
1663
  network: networkSource,
1264
1664
  rpcUrl: rpcUrlSource,
1665
+ rpcBiscuitToken: rpcBiscuitTokenSource,
1265
1666
  ckbRpcUrl: ckbRpcUrlSource,
1266
1667
  runtimeProxyListen: runtimeProxyListenSource
1267
1668
  }
@@ -1356,7 +1757,7 @@ function parseTypedValue(raw, valueType) {
1356
1757
  return raw;
1357
1758
  }
1358
1759
  function ensureConfigFileOrExit(configPath, json) {
1359
- if (!existsSync4(configPath)) {
1760
+ if (!existsSync5(configPath)) {
1360
1761
  const msg = `Config file not found: ${configPath}. Run \`fiber-pay config init\` first.`;
1361
1762
  if (json) {
1362
1763
  printJsonError({
@@ -1728,7 +2129,7 @@ function createConfigCommand(_config) {
1728
2129
  }
1729
2130
 
1730
2131
  // src/commands/graph.ts
1731
- import { shannonsToCkb as shannonsToCkb2, toHex as toHex2 } from "@fiber-pay/sdk";
2132
+ import { shannonsToCkb as shannonsToCkb3, toHex as toHex2 } from "@fiber-pay/sdk";
1732
2133
  import { Command as Command4 } from "commander";
1733
2134
  function printGraphNodeListHuman(nodes) {
1734
2135
  if (nodes.length === 0) {
@@ -1743,7 +2144,7 @@ function printGraphNodeListHuman(nodes) {
1743
2144
  const nodeId = truncateMiddle(node.node_id, 10, 8).padEnd(22, " ");
1744
2145
  const alias = (node.node_name || "(unnamed)").slice(0, 20).padEnd(20, " ");
1745
2146
  const version = (node.version || "?").slice(0, 10).padEnd(10, " ");
1746
- const minFunding = shannonsToCkb2(node.auto_accept_min_ckb_funding_amount).toString().padStart(12, " ");
2147
+ const minFunding = shannonsToCkb3(node.auto_accept_min_ckb_funding_amount).toString().padStart(12, " ");
1747
2148
  const age = formatAge(parseHexTimestampMs(node.timestamp));
1748
2149
  console.log(`${nodeId} ${alias} ${version} ${minFunding} ${age}`);
1749
2150
  }
@@ -1765,7 +2166,7 @@ function printGraphChannelListHuman(channels) {
1765
2166
  const outpoint = ch.channel_outpoint ? truncateMiddle(`${ch.channel_outpoint.tx_hash}:${ch.channel_outpoint.index}`, 10, 8) : "n/a";
1766
2167
  const n1 = truncateMiddle(ch.node1, 10, 8).padEnd(22, " ");
1767
2168
  const n2 = truncateMiddle(ch.node2, 10, 8).padEnd(22, " ");
1768
- const capacity = `${shannonsToCkb2(ch.capacity)} CKB`.padStart(12, " ");
2169
+ const capacity = `${shannonsToCkb3(ch.capacity)} CKB`.padStart(12, " ");
1769
2170
  const age = formatAge(parseHexTimestampMs(ch.created_timestamp));
1770
2171
  console.log(`${outpoint.padEnd(22, " ")} ${n1} ${n2} ${capacity} ${age}`);
1771
2172
  }
@@ -1810,7 +2211,7 @@ Next cursor: ${result.last_cursor}`);
1810
2211
  }
1811
2212
 
1812
2213
  // src/commands/invoice.ts
1813
- import { ckbToShannons as ckbToShannons2, randomBytes32, shannonsToCkb as shannonsToCkb3, toHex as toHex3 } from "@fiber-pay/sdk";
2214
+ import { ckbToShannons as ckbToShannons3, randomBytes32, shannonsToCkb as shannonsToCkb4, toHex as toHex3 } from "@fiber-pay/sdk";
1814
2215
  import { Command as Command5 } from "commander";
1815
2216
  function createInvoiceCommand(config) {
1816
2217
  const invoice = new Command5("invoice").description("Invoice lifecycle and status commands");
@@ -1839,7 +2240,7 @@ function createInvoiceCommand(config) {
1839
2240
  params: {
1840
2241
  action: "create",
1841
2242
  newInvoiceParams: {
1842
- amount: ckbToShannons2(amountCkb),
2243
+ amount: ckbToShannons3(amountCkb),
1843
2244
  currency,
1844
2245
  description: options.description,
1845
2246
  expiry: toHex3(expirySeconds),
@@ -1876,7 +2277,7 @@ function createInvoiceCommand(config) {
1876
2277
  }
1877
2278
  }
1878
2279
  const result = await rpc.newInvoice({
1879
- amount: ckbToShannons2(amountCkb),
2280
+ amount: ckbToShannons3(amountCkb),
1880
2281
  currency,
1881
2282
  description: options.description,
1882
2283
  expiry: toHex3(expirySeconds),
@@ -1908,7 +2309,7 @@ function createInvoiceCommand(config) {
1908
2309
  paymentHash,
1909
2310
  status: result.status,
1910
2311
  invoice: result.invoice_address,
1911
- amountCkb: result.invoice.amount ? shannonsToCkb3(result.invoice.amount) : void 0,
2312
+ amountCkb: result.invoice.amount ? shannonsToCkb4(result.invoice.amount) : void 0,
1912
2313
  currency: result.invoice.currency,
1913
2314
  description: metadata.description,
1914
2315
  createdAt: createdAtMs ? new Date(createdAtMs).toISOString() : result.invoice.data.timestamp,
@@ -2028,19 +2429,104 @@ function createInvoiceCommand(config) {
2028
2429
  }
2029
2430
 
2030
2431
  // src/commands/job.ts
2031
- import { existsSync as existsSync6 } from "fs";
2432
+ import { existsSync as existsSync7 } from "fs";
2032
2433
  import { Command as Command6 } from "commander";
2033
2434
 
2034
2435
  // 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) {
2436
+ import {
2437
+ appendFileSync,
2438
+ closeSync,
2439
+ createReadStream,
2440
+ existsSync as existsSync6,
2441
+ mkdirSync as mkdirSync2,
2442
+ openSync,
2443
+ readdirSync,
2444
+ readSync,
2445
+ statSync
2446
+ } from "fs";
2447
+ import { join as join5 } from "path";
2448
+ var DATE_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
2449
+ function todayDateString() {
2450
+ const now = /* @__PURE__ */ new Date();
2451
+ const y = now.getUTCFullYear();
2452
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
2453
+ const d = String(now.getUTCDate()).padStart(2, "0");
2454
+ return `${y}-${m}-${d}`;
2455
+ }
2456
+ function validateLogDate(date) {
2457
+ const value = date.trim();
2458
+ if (!DATE_DIR_PATTERN.test(value)) {
2459
+ throw new Error(`Invalid date '${value}'. Expected format YYYY-MM-DD.`);
2460
+ }
2461
+ if (value.includes("/") || value.includes("\\") || value.includes("..")) {
2462
+ throw new Error(`Invalid date '${value}'. Path separators or '..' are not allowed.`);
2463
+ }
2464
+ return value;
2465
+ }
2466
+ function resolveLogDirForDate(dataDir, date) {
2467
+ return resolveLogDirForDateWithOptions(dataDir, date, {});
2468
+ }
2469
+ function resolveLogDirForDateWithOptions(dataDir, date, options) {
2470
+ const dateStr = date ?? todayDateString();
2471
+ const logsBaseDir = options.logsBaseDir ?? join5(dataDir, "logs");
2472
+ if (date !== void 0) {
2473
+ validateLogDate(dateStr);
2474
+ }
2475
+ const dir = join5(logsBaseDir, dateStr);
2476
+ const ensureExists = options.ensureExists ?? true;
2477
+ if (ensureExists) {
2478
+ mkdirSync2(dir, { recursive: true });
2479
+ }
2480
+ return dir;
2481
+ }
2482
+ function resolvePersistedLogPaths(dataDir, meta, date) {
2483
+ const logsBaseDir = meta?.logsBaseDir ?? join5(dataDir, "logs");
2484
+ if (date) {
2485
+ const dir2 = resolveLogDirForDateWithOptions(dataDir, date, {
2486
+ logsBaseDir,
2487
+ ensureExists: false
2488
+ });
2489
+ return {
2490
+ runtimeAlerts: join5(dir2, "runtime.alerts.jsonl"),
2491
+ fnnStdout: join5(dir2, "fnn.stdout.log"),
2492
+ fnnStderr: join5(dir2, "fnn.stderr.log")
2493
+ };
2494
+ }
2495
+ if (meta?.alertLogFilePath || meta?.fnnStdoutLogPath || meta?.fnnStderrLogPath) {
2496
+ const defaultDir = resolveLogDirForDateWithOptions(dataDir, void 0, {
2497
+ logsBaseDir,
2498
+ ensureExists: false
2499
+ });
2500
+ return {
2501
+ runtimeAlerts: meta.alertLogFilePath ?? join5(defaultDir, "runtime.alerts.jsonl"),
2502
+ fnnStdout: meta.fnnStdoutLogPath ?? join5(defaultDir, "fnn.stdout.log"),
2503
+ fnnStderr: meta.fnnStderrLogPath ?? join5(defaultDir, "fnn.stderr.log")
2504
+ };
2505
+ }
2506
+ const dir = resolveLogDirForDateWithOptions(dataDir, void 0, {
2507
+ logsBaseDir,
2508
+ ensureExists: false
2509
+ });
2038
2510
  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")
2511
+ runtimeAlerts: join5(dir, "runtime.alerts.jsonl"),
2512
+ fnnStdout: join5(dir, "fnn.stdout.log"),
2513
+ fnnStderr: join5(dir, "fnn.stderr.log")
2042
2514
  };
2043
2515
  }
2516
+ function listLogDates(dataDir, logsBaseDir) {
2517
+ const logsDir = logsBaseDir ?? join5(dataDir, "logs");
2518
+ if (!existsSync6(logsDir)) {
2519
+ return [];
2520
+ }
2521
+ const entries = readdirSync(logsDir, { withFileTypes: true });
2522
+ const dates = entries.filter((entry) => entry.isDirectory() && DATE_DIR_PATTERN.test(entry.name)).map((entry) => entry.name);
2523
+ dates.sort((a, b) => a > b ? -1 : a < b ? 1 : 0);
2524
+ return dates;
2525
+ }
2526
+ function appendToTodayLog(dataDir, filename, text) {
2527
+ const dir = resolveLogDirForDate(dataDir);
2528
+ appendFileSync(join5(dir, filename), text, "utf-8");
2529
+ }
2044
2530
  function resolvePersistedLogTargets(paths, source) {
2045
2531
  const all = [
2046
2532
  {
@@ -2065,7 +2551,7 @@ function resolvePersistedLogTargets(paths, source) {
2065
2551
  return all.filter((target) => target.source === source);
2066
2552
  }
2067
2553
  function readLastLines(filePath, maxLines) {
2068
- if (!existsSync5(filePath)) {
2554
+ if (!existsSync6(filePath)) {
2069
2555
  return [];
2070
2556
  }
2071
2557
  if (!Number.isFinite(maxLines) || maxLines <= 0) {
@@ -2109,7 +2595,7 @@ function readLastLines(filePath, maxLines) {
2109
2595
  }
2110
2596
  }
2111
2597
  async function readAppendedLines(filePath, offset, remainder = "") {
2112
- if (!existsSync5(filePath)) {
2598
+ if (!existsSync6(filePath)) {
2113
2599
  return { lines: [], nextOffset: 0, remainder: "" };
2114
2600
  }
2115
2601
  const size = statSync(filePath).size;
@@ -2209,10 +2695,11 @@ function createJobCommand(config) {
2209
2695
  console.log(` ${JSON.stringify(payload.result)}`);
2210
2696
  }
2211
2697
  });
2212
- job.command("trace").argument("<jobId>").option("--tail <n>", "Max lines to inspect per log file", "400").option("--json").action(async (jobId, options) => {
2698
+ 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
2699
  const json = Boolean(options.json);
2214
2700
  const tailInput = Number.parseInt(String(options.tail ?? "400"), 10);
2215
2701
  const tail = Number.isFinite(tailInput) && tailInput > 0 ? tailInput : 400;
2702
+ const date = options.date ? String(options.date).trim() : void 0;
2216
2703
  const runtimeUrl = getRuntimeUrlOrExit(config, json);
2217
2704
  const jobResponse = await fetch(`${runtimeUrl}/jobs/${jobId}`);
2218
2705
  if (!jobResponse.ok) {
@@ -2226,7 +2713,24 @@ function createJobCommand(config) {
2226
2713
  const eventsPayload = await eventsResponse.json();
2227
2714
  const tokens = collectTraceTokens(jobRecord, eventsPayload.events);
2228
2715
  const meta = readRuntimeMeta(config.dataDir);
2229
- const logPaths = resolvePersistedLogPaths(config.dataDir, meta);
2716
+ let logPaths;
2717
+ try {
2718
+ logPaths = resolvePersistedLogPaths(config.dataDir, meta, date);
2719
+ } catch (error) {
2720
+ const message = error instanceof Error ? error.message : "Invalid --date value.";
2721
+ if (json) {
2722
+ printJsonError({
2723
+ code: "JOB_TRACE_DATE_INVALID",
2724
+ message,
2725
+ recoverable: true,
2726
+ suggestion: "Retry with --date in YYYY-MM-DD format.",
2727
+ details: { date }
2728
+ });
2729
+ } else {
2730
+ console.error(`Error: ${message}`);
2731
+ }
2732
+ process.exit(1);
2733
+ }
2230
2734
  const runtimeAlertMatches = collectRelatedLines(logPaths.runtimeAlerts, tokens, tail);
2231
2735
  const fnnStdoutMatches = collectRelatedLines(logPaths.fnnStdout, tokens, tail);
2232
2736
  const fnnStderrMatches = collectRelatedLines(logPaths.fnnStderr, tokens, tail);
@@ -2429,7 +2933,7 @@ function collectStructuredTokens(set, input, depth = 0) {
2429
2933
  }
2430
2934
  }
2431
2935
  function collectRelatedLines(filePath, tokens, tail) {
2432
- if (!existsSync6(filePath)) {
2936
+ if (!existsSync7(filePath)) {
2433
2937
  return [];
2434
2938
  }
2435
2939
  const lines = readLastLines(filePath, tail);
@@ -2445,7 +2949,7 @@ function collectRelatedLines(filePath, tokens, tail) {
2445
2949
  function printTraceSection(title, filePath, lines) {
2446
2950
  console.log(`
2447
2951
  ${title}: ${filePath}`);
2448
- if (!existsSync6(filePath)) {
2952
+ if (!existsSync7(filePath)) {
2449
2953
  console.log(" (file not found)");
2450
2954
  return;
2451
2955
  }
@@ -2459,7 +2963,8 @@ ${title}: ${filePath}`);
2459
2963
  }
2460
2964
 
2461
2965
  // src/commands/logs.ts
2462
- import { existsSync as existsSync7, statSync as statSync2 } from "fs";
2966
+ import { existsSync as existsSync8, statSync as statSync2 } from "fs";
2967
+ import { join as join6 } from "path";
2463
2968
  import { formatRuntimeAlert } from "@fiber-pay/runtime";
2464
2969
  import { Command as Command7 } from "commander";
2465
2970
  var ALLOWED_SOURCES = /* @__PURE__ */ new Set([
@@ -2468,6 +2973,7 @@ var ALLOWED_SOURCES = /* @__PURE__ */ new Set([
2468
2973
  "fnn-stdout",
2469
2974
  "fnn-stderr"
2470
2975
  ]);
2976
+ var DATE_DIR_PATTERN2 = /^\d{4}-\d{2}-\d{2}$/;
2471
2977
  function parseRuntimeAlertLine(line) {
2472
2978
  try {
2473
2979
  const parsed = JSON.parse(line);
@@ -2493,9 +2999,45 @@ function coerceJsonLineForOutput(source, line) {
2493
2999
  return parseRuntimeAlertLine(line) ?? line;
2494
3000
  }
2495
3001
  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) => {
3002
+ 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
3003
  const json = Boolean(options.json);
2498
3004
  const follow = Boolean(options.follow);
3005
+ const listDates = Boolean(options.listDates);
3006
+ const date = options.date ? String(options.date).trim() : void 0;
3007
+ const meta = readRuntimeMeta(config.dataDir);
3008
+ if (follow && date) {
3009
+ const message = "--follow cannot be used with --date. --follow only streams today's logs.";
3010
+ if (json) {
3011
+ printJsonError({
3012
+ code: "LOG_FOLLOW_DATE_UNSUPPORTED",
3013
+ message,
3014
+ recoverable: true,
3015
+ suggestion: "Remove --date or remove --follow and retry."
3016
+ });
3017
+ } else {
3018
+ console.error(`Error: ${message}`);
3019
+ }
3020
+ process.exit(1);
3021
+ }
3022
+ if (listDates) {
3023
+ const logsDir = meta?.logsBaseDir ?? join6(config.dataDir, "logs");
3024
+ const dates = listLogDates(config.dataDir, logsDir);
3025
+ if (json) {
3026
+ printJsonSuccess({ dates, logsDir });
3027
+ } else {
3028
+ if (dates.length === 0) {
3029
+ console.log("No log dates found.");
3030
+ } else {
3031
+ console.log(`Log dates (${dates.length}):`);
3032
+ for (const date2 of dates) {
3033
+ console.log(` ${date2}`);
3034
+ }
3035
+ console.log(`
3036
+ Logs directory: ${logsDir}`);
3037
+ }
3038
+ }
3039
+ return;
3040
+ }
2499
3041
  const sourceInput = String(options.source ?? "all").trim().toLowerCase();
2500
3042
  if (json && follow) {
2501
3043
  const message = "--follow is not supported with --json. Use human mode for streaming logs.";
@@ -2527,18 +3069,36 @@ function createLogsCommand(config) {
2527
3069
  const tail = Number.isFinite(tailInput) && tailInput > 0 ? tailInput : 80;
2528
3070
  const intervalInput = Number.parseInt(String(options.intervalMs ?? "1000"), 10);
2529
3071
  const intervalMs = Number.isFinite(intervalInput) && intervalInput > 0 ? intervalInput : 1e3;
2530
- const meta = readRuntimeMeta(config.dataDir);
2531
- const paths = resolvePersistedLogPaths(config.dataDir, meta);
3072
+ let paths;
3073
+ try {
3074
+ paths = resolvePersistedLogPaths(config.dataDir, meta, date);
3075
+ } catch (error) {
3076
+ const message = error instanceof Error ? error.message : "Invalid --date value.";
3077
+ if (json) {
3078
+ printJsonError({
3079
+ code: "LOG_DATE_INVALID",
3080
+ message,
3081
+ recoverable: true,
3082
+ suggestion: "Retry with --date in YYYY-MM-DD format.",
3083
+ details: { date }
3084
+ });
3085
+ } else {
3086
+ console.error(`Error: ${message}`);
3087
+ }
3088
+ process.exit(1);
3089
+ }
2532
3090
  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}`;
3091
+ const displayDate = date ?? inferDateFromPaths(paths);
3092
+ if (source !== "all" && targets.length === 1 && !existsSync8(targets[0].path)) {
3093
+ const dateLabel = displayDate ? ` on ${displayDate}` : "";
3094
+ const message = `Log file not found for source ${source}${dateLabel}: ${targets[0].path}`;
2535
3095
  if (json) {
2536
3096
  printJsonError({
2537
3097
  code: "LOG_FILE_NOT_FOUND",
2538
3098
  message,
2539
3099
  recoverable: true,
2540
- suggestion: "Start node/runtime or generate activity, then retry logs command.",
2541
- details: { source, path: targets[0].path }
3100
+ suggestion: "Start node/runtime or generate activity, then retry logs command. Use --list-dates to see available dates.",
3101
+ details: { source, date: displayDate ?? null, path: targets[0].path }
2542
3102
  });
2543
3103
  } else {
2544
3104
  console.error(`Error: ${message}`);
@@ -2547,7 +3107,7 @@ function createLogsCommand(config) {
2547
3107
  }
2548
3108
  const entries = [];
2549
3109
  for (const target of targets) {
2550
- const exists = existsSync7(target.path);
3110
+ const exists = existsSync8(target.path);
2551
3111
  let lines = [];
2552
3112
  if (exists) {
2553
3113
  try {
@@ -2582,6 +3142,7 @@ function createLogsCommand(config) {
2582
3142
  printJsonSuccess({
2583
3143
  source,
2584
3144
  tail,
3145
+ date: displayDate ?? null,
2585
3146
  entries: entries.map((entry) => ({
2586
3147
  source: entry.source,
2587
3148
  title: entry.title,
@@ -2593,7 +3154,8 @@ function createLogsCommand(config) {
2593
3154
  });
2594
3155
  return;
2595
3156
  }
2596
- console.log(`Logs (source: ${source}, tail: ${tail})`);
3157
+ const headerDate = displayDate ? `, date: ${displayDate}` : "";
3158
+ console.log(`Logs (source: ${source}${headerDate}, tail: ${tail})`);
2597
3159
  for (const entry of entries) {
2598
3160
  console.log(`
2599
3161
  ${entry.title}: ${entry.path}`);
@@ -2647,7 +3209,7 @@ Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
2647
3209
  for (const target of targets) {
2648
3210
  const state = states.get(target.source);
2649
3211
  if (!state) continue;
2650
- if (!existsSync7(state.path)) {
3212
+ if (!existsSync8(state.path)) {
2651
3213
  state.offset = 0;
2652
3214
  state.remainder = "";
2653
3215
  continue;
@@ -2678,28 +3240,38 @@ Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
2678
3240
  });
2679
3241
  });
2680
3242
  }
3243
+ function inferDateFromPaths(paths) {
3244
+ const candidate = paths.runtimeAlerts.split("/").at(-2);
3245
+ if (!candidate || !DATE_DIR_PATTERN2.test(candidate)) {
3246
+ return void 0;
3247
+ }
3248
+ const stdoutDate = paths.fnnStdout.split("/").at(-2);
3249
+ const stderrDate = paths.fnnStderr.split("/").at(-2);
3250
+ if (stdoutDate !== candidate || stderrDate !== candidate) {
3251
+ return void 0;
3252
+ }
3253
+ return candidate;
3254
+ }
2681
3255
 
2682
3256
  // src/commands/node.ts
2683
- import { nodeIdToPeerId as nodeIdToPeerId2, scriptToAddress as scriptToAddress2 } from "@fiber-pay/sdk";
2684
3257
  import { Command as Command8 } from "commander";
2685
3258
 
2686
3259
  // src/lib/node-start.ts
2687
3260
  import { spawn } from "child_process";
2688
- import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
2689
- import { join as join5 } from "path";
3261
+ import { mkdirSync as mkdirSync3 } from "fs";
3262
+ import { join as join7 } from "path";
2690
3263
  import {
2691
3264
  createKeyManager,
2692
3265
  ensureFiberBinary,
2693
- getDefaultBinaryPath,
2694
3266
  ProcessManager
2695
3267
  } from "@fiber-pay/node";
2696
3268
  import { startRuntimeService } from "@fiber-pay/runtime";
2697
3269
 
2698
3270
  // src/lib/bootnode.ts
2699
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
3271
+ import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
2700
3272
  import { parse as parseYaml } from "yaml";
2701
3273
  function extractBootnodeAddrs(configFilePath) {
2702
- if (!existsSync8(configFilePath)) return [];
3274
+ if (!existsSync9(configFilePath)) return [];
2703
3275
  try {
2704
3276
  const content = readFileSync5(configFilePath, "utf-8");
2705
3277
  const doc = parseYaml(content);
@@ -2730,8 +3302,8 @@ async function autoConnectBootnodes(rpc, bootnodes) {
2730
3302
  }
2731
3303
 
2732
3304
  // src/lib/node-migration.ts
2733
- import { dirname } from "path";
2734
- import { BinaryManager, MigrationManager } from "@fiber-pay/node";
3305
+ import { dirname as dirname2 } from "path";
3306
+ import { BinaryManager as BinaryManager2, MigrationManager } from "@fiber-pay/node";
2735
3307
 
2736
3308
  // src/lib/migration-utils.ts
2737
3309
  function replaceRawMigrateHint(message) {
@@ -2754,8 +3326,8 @@ async function runMigrationGuard(opts) {
2754
3326
  return { checked: false, skippedReason: "store does not exist" };
2755
3327
  }
2756
3328
  const storePath = MigrationManager.resolveStorePath(dataDir);
2757
- const binaryDir = dirname(binaryPath);
2758
- const bm = new BinaryManager(binaryDir);
3329
+ const binaryDir = dirname2(binaryPath);
3330
+ const bm = new BinaryManager2(binaryDir);
2759
3331
  const migrateBinPath = bm.getMigrateBinaryPath();
2760
3332
  let migrationCheck;
2761
3333
  try {
@@ -2794,98 +3366,115 @@ async function runMigrationGuard(opts) {
2794
3366
  return { checked: true, migrationCheck };
2795
3367
  }
2796
3368
 
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" };
3369
+ // src/lib/runtime-port.ts
3370
+ import { spawnSync as spawnSync2 } from "child_process";
3371
+ function parsePortFromListen(listen) {
3372
+ const value = listen.trim();
3373
+ if (!value) {
3374
+ return void 0;
2804
3375
  }
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" };
3376
+ const lastColon = value.lastIndexOf(":");
3377
+ if (lastColon < 0 || lastColon === value.length - 1) {
3378
+ return void 0;
3379
+ }
3380
+ const port = Number(value.slice(lastColon + 1));
3381
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
3382
+ return void 0;
2815
3383
  }
3384
+ return port;
2816
3385
  }
2817
- function getBinaryVersion(binaryPath) {
2818
- try {
2819
- const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
2820
- if (result.status !== 0) {
2821
- return "unknown";
3386
+ function extractFirstPidFromLsofOutput(output) {
3387
+ for (const line of output.split("\n")) {
3388
+ const trimmed = line.trim();
3389
+ if (!trimmed.startsWith("p") || trimmed.length < 2) {
3390
+ continue;
2822
3391
  }
2823
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
2824
- if (!output) {
2825
- return "unknown";
3392
+ const pid = Number(trimmed.slice(1));
3393
+ if (Number.isInteger(pid) && pid > 0) {
3394
+ return pid;
2826
3395
  }
2827
- const firstLine = output.split("\n").find((line) => line.trim().length > 0);
2828
- return firstLine?.trim() ?? "unknown";
2829
- } catch {
2830
- return "unknown";
2831
3396
  }
3397
+ return void 0;
2832
3398
  }
2833
- function getCliEntrypoint() {
2834
- const entrypoint = process.argv[1];
2835
- if (!entrypoint) {
2836
- throw new Error("Unable to resolve CLI entrypoint path");
3399
+ function readProcessCommand(pid) {
3400
+ const result = spawnSync2("ps", ["-p", String(pid), "-o", "command="], {
3401
+ encoding: "utf-8"
3402
+ });
3403
+ if (result.error || result.status !== 0) {
3404
+ return void 0;
2837
3405
  }
2838
- return entrypoint;
3406
+ const command = (result.stdout ?? "").trim();
3407
+ return command.length > 0 ? command : void 0;
2839
3408
  }
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 };
3409
+ function findListeningProcessByPort(listen) {
3410
+ const port = parsePortFromListen(listen);
3411
+ if (!port) {
3412
+ return void 0;
2867
3413
  }
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 };
3414
+ const result = spawnSync2("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], {
3415
+ encoding: "utf-8"
3416
+ });
3417
+ if (result.error || result.status !== 0) {
3418
+ return void 0;
3419
+ }
3420
+ const pid = extractFirstPidFromLsofOutput(result.stdout ?? "");
3421
+ if (!pid) {
3422
+ return void 0;
3423
+ }
3424
+ return {
3425
+ pid,
3426
+ command: readProcessCommand(pid)
3427
+ };
2872
3428
  }
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
- );
3429
+ function isFiberRuntimeCommand(command) {
3430
+ if (!command) {
3431
+ return false;
3432
+ }
3433
+ const normalized = command.toLowerCase();
3434
+ 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");
3435
+ if (!hasFiberIdentifier) {
3436
+ return false;
3437
+ }
3438
+ return normalized.includes("runtime") && normalized.includes("start");
3439
+ }
3440
+ async function terminateProcess(pid, timeoutMs = 5e3) {
3441
+ if (!isProcessRunning(pid)) {
3442
+ return true;
3443
+ }
3444
+ try {
3445
+ process.kill(pid, "SIGTERM");
3446
+ } catch (error) {
3447
+ if (error.code === "ESRCH") {
3448
+ return true;
3449
+ }
3450
+ return false;
3451
+ }
3452
+ const deadline = Date.now() + timeoutMs;
3453
+ while (Date.now() < deadline) {
3454
+ if (!isProcessRunning(pid)) {
3455
+ return true;
3456
+ }
3457
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
3458
+ }
3459
+ if (!isProcessRunning(pid)) {
3460
+ return true;
3461
+ }
3462
+ try {
3463
+ process.kill(pid, "SIGKILL");
3464
+ } catch (error) {
3465
+ if (error.code === "ESRCH") {
3466
+ return true;
3467
+ }
3468
+ return false;
3469
+ }
3470
+ const killDeadline = Date.now() + 1e3;
3471
+ while (Date.now() < killDeadline) {
3472
+ if (!isProcessRunning(pid)) {
3473
+ return true;
3474
+ }
3475
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
3476
+ }
3477
+ return !isProcessRunning(pid);
2889
3478
  }
2890
3479
 
2891
3480
  // src/lib/node-start.ts
@@ -2997,19 +3586,22 @@ async function runNodeStartCommand(config, options) {
2997
3586
  options.runtimeProxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
2998
3587
  );
2999
3588
  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();
3589
+ const runtimeStateFilePath = join7(config.dataDir, "runtime-state.json");
3590
+ const logsBaseDir = join7(config.dataDir, "logs");
3591
+ mkdirSync3(logsBaseDir, { recursive: true });
3592
+ resolveLogDirForDate(config.dataDir);
3593
+ const resolvedBinary = resolveBinaryPath(config);
3594
+ const binaryPath = resolvedBinary.binaryPath;
3595
+ if (resolvedBinary.source === "profile-managed") {
3596
+ const installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
3597
+ await ensureFiberBinary({ installDir });
3598
+ }
3008
3599
  const binaryVersion = getBinaryVersion(binaryPath);
3009
3600
  const configFilePath = ensureNodeConfigFile(config.dataDir, config.network);
3010
3601
  emitStage("binary_resolved", "ok", {
3011
3602
  binaryPath,
3012
3603
  binaryVersion,
3604
+ binarySource: resolvedBinary.source,
3013
3605
  configFilePath
3014
3606
  });
3015
3607
  if (!json) {
@@ -3070,11 +3662,11 @@ async function runNodeStartCommand(config, options) {
3070
3662
  removePidFile(config.dataDir);
3071
3663
  });
3072
3664
  processManager.on("stdout", (text) => {
3073
- appendFileSync(fnnStdoutLogPath, text, "utf-8");
3665
+ appendToTodayLog(config.dataDir, "fnn.stdout.log", text);
3074
3666
  emitFnnLog("stdout", text);
3075
3667
  });
3076
3668
  processManager.on("stderr", (text) => {
3077
- appendFileSync(fnnStderrLogPath, text, "utf-8");
3669
+ appendToTodayLog(config.dataDir, "fnn.stderr.log", text);
3078
3670
  emitFnnLog("stderr", text);
3079
3671
  });
3080
3672
  await processManager.start();
@@ -3119,13 +3711,38 @@ async function runNodeStartCommand(config, options) {
3119
3711
  process.exit(1);
3120
3712
  }
3121
3713
  try {
3714
+ const runtimePortProcess = findListeningProcessByPort(runtimeProxyListen);
3715
+ if (runtimePortProcess) {
3716
+ if (isFiberRuntimeCommand(runtimePortProcess.command)) {
3717
+ const terminated = await terminateProcess(runtimePortProcess.pid);
3718
+ if (!terminated) {
3719
+ throw new Error(
3720
+ `Runtime proxy ${runtimeProxyListen} is occupied by stale fiber-pay runtime PID ${runtimePortProcess.pid}, but termination failed`
3721
+ );
3722
+ }
3723
+ removeRuntimeFiles(config.dataDir);
3724
+ emitStage("runtime_preflight", "ok", {
3725
+ proxyListen: runtimeProxyListen,
3726
+ cleanedStaleProcessPid: runtimePortProcess.pid
3727
+ });
3728
+ } else if (runtimePortProcess.command) {
3729
+ const details = runtimePortProcess.command ? `PID ${runtimePortProcess.pid} (${runtimePortProcess.command})` : `PID ${runtimePortProcess.pid}`;
3730
+ throw new Error(
3731
+ `Runtime proxy ${runtimeProxyListen} is already in use by non-fiber-pay process: ${details}`
3732
+ );
3733
+ } else {
3734
+ throw new Error(
3735
+ `Runtime proxy ${runtimeProxyListen} is already in use by process PID ${runtimePortProcess.pid}. Unable to determine command owner; inspect this PID manually before retrying.`
3736
+ );
3737
+ }
3738
+ }
3122
3739
  if (runtimeDaemon) {
3123
3740
  const daemonStart = startRuntimeDaemonFromNode({
3124
3741
  dataDir: config.dataDir,
3125
3742
  rpcUrl: config.rpcUrl,
3126
3743
  proxyListen: runtimeProxyListen,
3127
3744
  stateFilePath: runtimeStateFilePath,
3128
- alertLogFile: runtimeAlertLogPath
3745
+ alertLogsBaseDir: logsBaseDir
3129
3746
  });
3130
3747
  if (!daemonStart.ok) {
3131
3748
  throw new Error(daemonStart.message);
@@ -3140,23 +3757,25 @@ async function runNodeStartCommand(config, options) {
3140
3757
  storage: {
3141
3758
  stateFilePath: runtimeStateFilePath
3142
3759
  },
3143
- alerts: [{ type: "stdout" }, { type: "file", path: runtimeAlertLogPath }],
3760
+ alerts: [{ type: "stdout" }, { type: "daily-file", baseLogsDir: logsBaseDir }],
3144
3761
  jobs: {
3145
3762
  enabled: true,
3146
- dbPath: join5(config.dataDir, "runtime-jobs.db")
3763
+ dbPath: join7(config.dataDir, "runtime-jobs.db")
3147
3764
  }
3148
3765
  });
3149
3766
  const runtimeStatus = runtime.service.getStatus();
3150
3767
  writeRuntimePid(config.dataDir, process.pid);
3768
+ const todayLogDir = resolveLogDirForDate(config.dataDir);
3151
3769
  writeRuntimeMeta(config.dataDir, {
3152
3770
  pid: process.pid,
3153
3771
  startedAt: runtimeStatus.startedAt,
3154
3772
  fiberRpcUrl: runtimeStatus.targetUrl,
3155
3773
  proxyListen: runtimeStatus.proxyListen,
3156
3774
  stateFilePath: runtimeStateFilePath,
3157
- alertLogFilePath: runtimeAlertLogPath,
3158
- fnnStdoutLogPath,
3159
- fnnStderrLogPath,
3775
+ alertLogFilePath: join7(todayLogDir, "runtime.alerts.jsonl"),
3776
+ fnnStdoutLogPath: join7(todayLogDir, "fnn.stdout.log"),
3777
+ fnnStderrLogPath: join7(todayLogDir, "fnn.stderr.log"),
3778
+ logsBaseDir,
3160
3779
  daemon: false
3161
3780
  });
3162
3781
  }
@@ -3226,7 +3845,7 @@ async function runNodeStartCommand(config, options) {
3226
3845
  process.exit(1);
3227
3846
  }
3228
3847
  emitStage("rpc_ready", "ok", { rpcUrl: config.rpcUrl });
3229
- const bootnodes = nodeConfig.configFilePath ? extractBootnodeAddrs(nodeConfig.configFilePath) : extractBootnodeAddrs(join5(config.dataDir, "config.yml"));
3848
+ const bootnodes = nodeConfig.configFilePath ? extractBootnodeAddrs(nodeConfig.configFilePath) : extractBootnodeAddrs(join7(config.dataDir, "config.yml"));
3230
3849
  if (bootnodes.length > 0) {
3231
3850
  await autoConnectBootnodes(rpc, bootnodes);
3232
3851
  }
@@ -3267,9 +3886,8 @@ async function runNodeStartCommand(config, options) {
3267
3886
  proxyUrl: `http://${runtimeProxyListen}`,
3268
3887
  proxyListenSource,
3269
3888
  logs: {
3270
- fnnStdout: fnnStdoutLogPath,
3271
- fnnStderr: fnnStderrLogPath,
3272
- runtimeAlerts: runtimeAlertLogPath
3889
+ baseDir: logsBaseDir,
3890
+ todayDir: resolveLogDirForDate(config.dataDir)
3273
3891
  }
3274
3892
  });
3275
3893
  } else {
@@ -3279,7 +3897,7 @@ async function runNodeStartCommand(config, options) {
3279
3897
  ` Runtime proxy: http://${runtimeProxyListen} (browser-safe endpoint + monitoring)`
3280
3898
  );
3281
3899
  console.log(` Runtime mode: ${runtimeDaemon ? "daemon" : "embedded"}`);
3282
- console.log(` Log files: ${logsDir}`);
3900
+ console.log(` Log files: ${logsBaseDir}`);
3283
3901
  console.log(" Press Ctrl+C to stop.");
3284
3902
  }
3285
3903
  let shutdownRequested = false;
@@ -3347,8 +3965,6 @@ async function runNodeStartCommand(config, options) {
3347
3965
 
3348
3966
  // src/lib/node-status.ts
3349
3967
  import { existsSync as existsSync10 } from "fs";
3350
- import { join as join6 } from "path";
3351
- import { getFiberBinaryInfo as getFiberBinaryInfo2 } from "@fiber-pay/node";
3352
3968
  import {
3353
3969
  buildMultiaddrFromNodeId,
3354
3970
  buildMultiaddrFromRpcUrl,
@@ -3515,24 +4131,29 @@ async function runNodeStatusCommand(config, options) {
3515
4131
  const json = Boolean(options.json);
3516
4132
  const pid = readPidFile(config.dataDir);
3517
4133
  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"));
4134
+ const { resolvedBinary, info: binaryInfo } = await getBinaryDetails(config);
3520
4135
  const configExists = existsSync10(config.configPath);
3521
4136
  const nodeRunning = Boolean(pid && isProcessRunning(pid));
3522
4137
  let rpcResponsive = false;
3523
4138
  let nodeId = null;
4139
+ let addresses = [];
4140
+ let chainHash = null;
4141
+ let version = null;
3524
4142
  let peerId = null;
4143
+ let peersCount = 0;
3525
4144
  let peerIdError = null;
3526
4145
  let multiaddr = null;
3527
4146
  let multiaddrError = null;
3528
4147
  let multiaddrInferred = false;
3529
4148
  let channelsTotal = 0;
3530
4149
  let channelsReady = 0;
4150
+ let pendingChannelCount = 0;
3531
4151
  let canSend = false;
3532
4152
  let canReceive = false;
3533
4153
  let localCkb = 0;
3534
4154
  let remoteCkb = 0;
3535
4155
  let fundingAddress = null;
4156
+ let fundingLockScript = null;
3536
4157
  let fundingCkb = 0;
3537
4158
  let fundingBalanceError = null;
3538
4159
  if (nodeRunning) {
@@ -3542,6 +4163,10 @@ async function runNodeStatusCommand(config, options) {
3542
4163
  const channels = await rpc.listChannels({ include_closed: false });
3543
4164
  rpcResponsive = true;
3544
4165
  nodeId = nodeInfo.node_id;
4166
+ addresses = nodeInfo.addresses;
4167
+ chainHash = nodeInfo.chain_hash;
4168
+ version = nodeInfo.version;
4169
+ peersCount = parseInt(nodeInfo.peers_count, 16);
3545
4170
  try {
3546
4171
  peerId = await nodeIdToPeerId(nodeInfo.node_id);
3547
4172
  } catch (error) {
@@ -3568,18 +4193,17 @@ async function runNodeStatusCommand(config, options) {
3568
4193
  (channel) => channel.state?.state_name === ChannelState2.ChannelReady
3569
4194
  );
3570
4195
  channelsReady = readyChannels.length;
4196
+ pendingChannelCount = Math.max(channelsTotal - channelsReady, 0);
3571
4197
  const liquidity = summarizeChannelLiquidity(readyChannels);
3572
4198
  canSend = liquidity.canSend;
3573
4199
  canReceive = liquidity.canReceive;
3574
4200
  localCkb = liquidity.localCkb;
3575
4201
  remoteCkb = liquidity.remoteCkb;
3576
4202
  fundingAddress = scriptToAddress(nodeInfo.default_funding_lock_script, config.network);
4203
+ fundingLockScript = nodeInfo.default_funding_lock_script;
3577
4204
  if (config.ckbRpcUrl) {
3578
4205
  try {
3579
- const fundingBalance = await getLockBalanceShannons(
3580
- config.ckbRpcUrl,
3581
- nodeInfo.default_funding_lock_script
3582
- );
4206
+ const fundingBalance = await getLockBalanceShannons(config.ckbRpcUrl, fundingLockScript);
3583
4207
  fundingCkb = Number(fundingBalance) / 1e8;
3584
4208
  } catch (error) {
3585
4209
  fundingBalanceError = error instanceof Error ? error.message : "Failed to query CKB balance for funding address";
@@ -3610,18 +4234,24 @@ async function runNodeStatusCommand(config, options) {
3610
4234
  rpcTarget: resolvedRpc.target,
3611
4235
  resolvedRpcUrl: resolvedRpc.url,
3612
4236
  nodeId,
4237
+ addresses,
4238
+ chainHash,
4239
+ version,
3613
4240
  peerId,
4241
+ peersCount,
3614
4242
  peerIdError,
3615
4243
  multiaddr,
3616
4244
  multiaddrError,
3617
4245
  multiaddrInferred,
4246
+ fundingLockScript,
3618
4247
  checks: {
3619
4248
  binary: {
3620
4249
  path: binaryInfo.path,
3621
4250
  ready: binaryInfo.ready,
3622
4251
  version: binaryInfo.version,
3623
- source: config.binaryPath ? "env-binary-path" : "managed-binary-dir",
3624
- managedPath: managedBinaryPath
4252
+ source: resolvedBinary.source,
4253
+ managedPath: resolvedBinary.managedPath,
4254
+ resolvedPath: resolvedBinary.binaryPath
3625
4255
  },
3626
4256
  config: {
3627
4257
  path: config.configPath,
@@ -3639,6 +4269,7 @@ async function runNodeStatusCommand(config, options) {
3639
4269
  channels: {
3640
4270
  total: channelsTotal,
3641
4271
  ready: channelsReady,
4272
+ pending: pendingChannelCount,
3642
4273
  canSend,
3643
4274
  canReceive
3644
4275
  }
@@ -3665,6 +4296,12 @@ async function runNodeStatusCommand(config, options) {
3665
4296
  console.log(`\u2705 Node is running (PID: ${output.pid})`);
3666
4297
  if (output.rpcResponsive) {
3667
4298
  console.log(` Node ID: ${String(output.nodeId)}`);
4299
+ if (output.version) {
4300
+ console.log(` Version: ${String(output.version)}`);
4301
+ }
4302
+ if (output.chainHash) {
4303
+ console.log(` Chain Hash: ${String(output.chainHash)}`);
4304
+ }
3668
4305
  if (output.peerId) {
3669
4306
  console.log(` Peer ID: ${String(output.peerId)}`);
3670
4307
  } else if (output.peerIdError) {
@@ -3680,6 +4317,12 @@ async function runNodeStatusCommand(config, options) {
3680
4317
  } else {
3681
4318
  console.log(" Multiaddr: unavailable");
3682
4319
  }
4320
+ if (output.addresses.length > 0) {
4321
+ console.log(" Addresses:");
4322
+ for (const address of output.addresses) {
4323
+ console.log(` - ${address}`);
4324
+ }
4325
+ }
3683
4326
  } else {
3684
4327
  console.log(" \u26A0\uFE0F RPC not responding");
3685
4328
  }
@@ -3691,11 +4334,17 @@ async function runNodeStatusCommand(config, options) {
3691
4334
  console.log("");
3692
4335
  console.log("Diagnostics");
3693
4336
  console.log(` Binary: ${output.checks.binary.ready ? "ready" : "missing"}`);
4337
+ console.log(` Binary Path: ${output.checks.binary.resolvedPath}`);
3694
4338
  console.log(` Config: ${output.checks.config.exists ? "present" : "missing"}`);
3695
4339
  console.log(` RPC: ${output.checks.node.rpcReachable ? "reachable" : "unreachable"}`);
3696
4340
  console.log(
3697
- ` Channels: ${output.checks.channels.ready}/${output.checks.channels.total} ready/total`
4341
+ ` Channels: ${output.checks.channels.ready}/${output.checks.channels.pending}/${output.checks.channels.total} ready/pending/total`
3698
4342
  );
4343
+ if (output.rpcResponsive) {
4344
+ console.log(` Peers: ${output.peersCount}`);
4345
+ } else {
4346
+ console.log(" Peers: unavailable");
4347
+ }
3699
4348
  console.log(` Can Send: ${output.checks.channels.canSend ? "yes" : "no"}`);
3700
4349
  console.log(` Can Receive: ${output.checks.channels.canReceive ? "yes" : "no"}`);
3701
4350
  console.log(` Recommendation:${output.recommendation}`);
@@ -3858,11 +4507,28 @@ async function runNodeStopCommand(config, options) {
3858
4507
  }
3859
4508
 
3860
4509
  // src/lib/node-upgrade.ts
3861
- import { BinaryManager as BinaryManager2, MigrationManager as MigrationManager2 } from "@fiber-pay/node";
4510
+ import { BinaryManager as BinaryManager3, MigrationManager as MigrationManager2 } from "@fiber-pay/node";
3862
4511
  async function runNodeUpgradeCommand(config, options) {
3863
4512
  const json = Boolean(options.json);
3864
- const installDir = `${config.dataDir}/bin`;
3865
- const binaryManager = new BinaryManager2(installDir);
4513
+ const resolvedBinary = resolveBinaryPath(config);
4514
+ let installDir;
4515
+ try {
4516
+ installDir = getBinaryManagerInstallDirOrThrow(resolvedBinary);
4517
+ } catch (error) {
4518
+ const message = error instanceof Error ? error.message : String(error);
4519
+ if (json) {
4520
+ printJsonError({
4521
+ code: "BINARY_PATH_INCOMPATIBLE",
4522
+ message,
4523
+ recoverable: true,
4524
+ suggestion: "Use `fiber-pay config profile unset binaryPath` or set binaryPath to a standard fnn filename in the target directory."
4525
+ });
4526
+ } else {
4527
+ console.error(`\u274C ${message}`);
4528
+ }
4529
+ process.exit(1);
4530
+ }
4531
+ const binaryManager = new BinaryManager3(installDir);
3866
4532
  const pid = readPidFile(config.dataDir);
3867
4533
  if (pid && isProcessRunning(pid)) {
3868
4534
  const msg = "The Fiber node is currently running. Stop it before upgrading.";
@@ -4134,29 +4800,6 @@ function createNodeCommand(config) {
4134
4800
  node.command("ready").description("Agent-oriented readiness summary for automation").option("--json").action(async (options) => {
4135
4801
  await runNodeReadyCommand(config, options);
4136
4802
  });
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
4803
  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
4804
  "--force-migrate",
4162
4805
  "Force migration attempt even when compatibility check reports incompatible data"
@@ -4167,7 +4810,7 @@ function createNodeCommand(config) {
4167
4810
  }
4168
4811
 
4169
4812
  // src/commands/payment.ts
4170
- import { ckbToShannons as ckbToShannons3, shannonsToCkb as shannonsToCkb4 } from "@fiber-pay/sdk";
4813
+ import { ckbToShannons as ckbToShannons4, shannonsToCkb as shannonsToCkb5 } from "@fiber-pay/sdk";
4171
4814
  import { Command as Command9 } from "commander";
4172
4815
  function createPaymentCommand(config) {
4173
4816
  const payment = new Command9("payment").description("Payment lifecycle and status commands");
@@ -4209,9 +4852,9 @@ function createPaymentCommand(config) {
4209
4852
  const paymentParams = {
4210
4853
  invoice,
4211
4854
  target_pubkey: recipientNodeId,
4212
- amount: amountCkb ? ckbToShannons3(amountCkb) : void 0,
4855
+ amount: amountCkb ? ckbToShannons4(amountCkb) : void 0,
4213
4856
  keysend: recipientNodeId ? true : void 0,
4214
- max_fee_amount: maxFeeCkb ? ckbToShannons3(maxFeeCkb) : void 0
4857
+ max_fee_amount: maxFeeCkb ? ckbToShannons4(maxFeeCkb) : void 0
4215
4858
  };
4216
4859
  const endpoint = resolveRpcEndpoint(config);
4217
4860
  if (endpoint.target === "runtime-proxy") {
@@ -4253,7 +4896,7 @@ function createPaymentCommand(config) {
4253
4896
  const payload = {
4254
4897
  paymentHash: result.payment_hash,
4255
4898
  status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
4256
- feeCkb: shannonsToCkb4(result.fee),
4899
+ feeCkb: shannonsToCkb5(result.fee),
4257
4900
  failureReason: result.failed_error
4258
4901
  };
4259
4902
  if (json) {
@@ -4268,6 +4911,7 @@ function createPaymentCommand(config) {
4268
4911
  }
4269
4912
  }
4270
4913
  });
4914
+ registerPaymentRebalanceCommand(payment, config);
4271
4915
  payment.command("get").argument("<paymentHash>").option("--json").action(async (paymentHash, options) => {
4272
4916
  const rpc = await createReadyRpcClient(config);
4273
4917
  const result = await rpc.getPayment({ payment_hash: paymentHash });
@@ -4416,7 +5060,7 @@ function createPaymentCommand(config) {
4416
5060
  process.exit(1);
4417
5061
  }
4418
5062
  const hopsInfo = pubkeys.map((pubkey) => ({ pubkey }));
4419
- const amount = options.amount ? ckbToShannons3(parseFloat(options.amount)) : void 0;
5063
+ const amount = options.amount ? ckbToShannons4(parseFloat(options.amount)) : void 0;
4420
5064
  const result = await rpc.buildRouter({
4421
5065
  hops_info: hopsInfo,
4422
5066
  amount
@@ -4432,7 +5076,7 @@ function createPaymentCommand(config) {
4432
5076
  console.log(
4433
5077
  ` Outpoint: ${hop.channel_outpoint.tx_hash}:${hop.channel_outpoint.index}`
4434
5078
  );
4435
- console.log(` Amount: ${shannonsToCkb4(hop.amount_received)} CKB`);
5079
+ console.log(` Amount: ${shannonsToCkb5(hop.amount_received)} CKB`);
4436
5080
  console.log(` Expiry: ${hop.incoming_tlc_expiry}`);
4437
5081
  }
4438
5082
  }
@@ -4440,7 +5084,7 @@ function createPaymentCommand(config) {
4440
5084
  payment.command("send-route").description("Send a payment using a pre-built route from `payment route`").requiredOption(
4441
5085
  "--router <json>",
4442
5086
  "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) => {
5087
+ ).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
5088
  const rpc = await createReadyRpcClient(config);
4445
5089
  const json = Boolean(options.json);
4446
5090
  let router;
@@ -4465,12 +5109,13 @@ function createPaymentCommand(config) {
4465
5109
  invoice: options.invoice,
4466
5110
  payment_hash: options.paymentHash,
4467
5111
  keysend: options.keysend ? true : void 0,
5112
+ allow_self_payment: options.allowSelfPayment ? true : void 0,
4468
5113
  dry_run: options.dryRun ? true : void 0
4469
5114
  });
4470
5115
  const payload = {
4471
5116
  paymentHash: result.payment_hash,
4472
5117
  status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
4473
- feeCkb: shannonsToCkb4(result.fee),
5118
+ feeCkb: shannonsToCkb5(result.fee),
4474
5119
  failureReason: result.failed_error,
4475
5120
  dryRun: Boolean(options.dryRun)
4476
5121
  };
@@ -4494,7 +5139,7 @@ function getJobPaymentHash(job) {
4494
5139
  }
4495
5140
  function getJobFeeCkb(job) {
4496
5141
  const result = job.result;
4497
- return result?.fee ? shannonsToCkb4(result.fee) : 0;
5142
+ return result?.fee ? shannonsToCkb5(result.fee) : 0;
4498
5143
  }
4499
5144
  function getJobFailure(job) {
4500
5145
  const result = job.result;
@@ -4566,7 +5211,7 @@ function createPeerCommand(config) {
4566
5211
 
4567
5212
  // src/commands/runtime.ts
4568
5213
  import { spawn as spawn2 } from "child_process";
4569
- import { resolve } from "path";
5214
+ import { join as join8, resolve } from "path";
4570
5215
  import {
4571
5216
  alertPriorityOrder,
4572
5217
  formatRuntimeAlert as formatRuntimeAlert2,
@@ -4640,15 +5285,22 @@ function shouldPrintAlert(alert, filter) {
4640
5285
  }
4641
5286
  return true;
4642
5287
  }
5288
+ function resolveRuntimeRecoveryListen(config) {
5289
+ const meta = readRuntimeMeta(config.dataDir);
5290
+ return meta?.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229";
5291
+ }
4643
5292
  function createRuntimeCommand(config) {
4644
5293
  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(
5294
+ 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
5295
  "--log-min-priority <priority>",
4647
5296
  "Minimum runtime log priority (critical|high|medium|low)"
4648
5297
  ).option("--log-type <types>", "Comma-separated runtime alert types to print").option("--json").action(async (options) => {
4649
5298
  const asJson = Boolean(options.json);
4650
5299
  const daemon = Boolean(options.daemon);
4651
5300
  const isRuntimeChild = process.env.FIBER_RUNTIME_CHILD === "1";
5301
+ const runtimeListen = String(
5302
+ options.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
5303
+ );
4652
5304
  try {
4653
5305
  const existingPid = readRuntimePid(config.dataDir);
4654
5306
  if (existingPid && isProcessRunning(existingPid) && (!isRuntimeChild || existingPid !== process.pid)) {
@@ -4668,6 +5320,32 @@ function createRuntimeCommand(config) {
4668
5320
  if (existingPid && !isProcessRunning(existingPid)) {
4669
5321
  removeRuntimeFiles(config.dataDir);
4670
5322
  }
5323
+ const discovered = findListeningProcessByPort(runtimeListen);
5324
+ if (discovered && (!existingPid || discovered.pid !== existingPid)) {
5325
+ if (isFiberRuntimeCommand(discovered.command)) {
5326
+ const terminated = await terminateProcess(discovered.pid);
5327
+ if (!terminated) {
5328
+ throw new Error(
5329
+ `Runtime port ${runtimeListen} is occupied by stale fiber-pay runtime PID ${discovered.pid} but termination failed.`
5330
+ );
5331
+ }
5332
+ removeRuntimeFiles(config.dataDir);
5333
+ if (!asJson) {
5334
+ console.log(
5335
+ `Recovered stale runtime process on ${runtimeListen} (PID: ${discovered.pid}).`
5336
+ );
5337
+ }
5338
+ } else if (discovered.command) {
5339
+ const details = discovered.command ? `PID ${discovered.pid} (${discovered.command})` : `PID ${discovered.pid}`;
5340
+ throw new Error(
5341
+ `Runtime proxy listen ${runtimeListen} is already in use by non-fiber-pay process: ${details}`
5342
+ );
5343
+ } else {
5344
+ throw new Error(
5345
+ `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.`
5346
+ );
5347
+ }
5348
+ }
4671
5349
  if (daemon && !isRuntimeChild) {
4672
5350
  const childArgv = process.argv.filter((arg) => arg !== "--daemon");
4673
5351
  const child = spawn2(process.execPath, childArgv.slice(1), {
@@ -4710,7 +5388,7 @@ function createRuntimeCommand(config) {
4710
5388
  ),
4711
5389
  proxy: {
4712
5390
  enabled: true,
4713
- listen: String(options.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229")
5391
+ listen: runtimeListen
4714
5392
  },
4715
5393
  storage: {
4716
5394
  stateFilePath: options.stateFile ? resolve(String(options.stateFile)) : resolve(config.dataDir, "runtime-state.json"),
@@ -4721,9 +5399,21 @@ function createRuntimeCommand(config) {
4721
5399
  dbPath: resolve(config.dataDir, "runtime-jobs.db")
4722
5400
  }
4723
5401
  };
4724
- const alertLogFile = options.alertLogFile ? resolve(String(options.alertLogFile)) : resolve(config.dataDir, "logs", "runtime.alerts.jsonl");
5402
+ let alertLogsBaseDir;
5403
+ let alertLogFile;
5404
+ if (options.alertLogsBaseDir) {
5405
+ alertLogsBaseDir = resolve(String(options.alertLogsBaseDir));
5406
+ } else if (options.alertLogFile) {
5407
+ alertLogFile = resolve(String(options.alertLogFile));
5408
+ } else {
5409
+ alertLogsBaseDir = resolve(config.dataDir, "logs");
5410
+ }
4725
5411
  const alerts = [{ type: "stdout" }];
4726
- alerts.push({ type: "file", path: alertLogFile });
5412
+ if (alertLogsBaseDir) {
5413
+ alerts.push({ type: "daily-file", baseLogsDir: alertLogsBaseDir });
5414
+ } else if (alertLogFile) {
5415
+ alerts.push({ type: "file", path: alertLogFile });
5416
+ }
4727
5417
  if (options.webhook) {
4728
5418
  alerts.push({ type: "webhook", url: String(options.webhook) });
4729
5419
  }
@@ -4745,6 +5435,12 @@ function createRuntimeCommand(config) {
4745
5435
  }
4746
5436
  const runtime2 = await startRuntimeService2(runtimeConfig);
4747
5437
  const status = runtime2.service.getStatus();
5438
+ const logsBaseDir = alertLogsBaseDir ?? resolve(config.dataDir, "logs");
5439
+ const todayLogDir = resolveLogDirForDateWithOptions(config.dataDir, void 0, {
5440
+ logsBaseDir,
5441
+ ensureExists: false
5442
+ });
5443
+ const effectiveAlertLogPath = alertLogsBaseDir ? join8(todayLogDir, "runtime.alerts.jsonl") : alertLogFile ?? join8(todayLogDir, "runtime.alerts.jsonl");
4748
5444
  writeRuntimePid(config.dataDir, process.pid);
4749
5445
  writeRuntimeMeta(config.dataDir, {
4750
5446
  pid: process.pid,
@@ -4752,7 +5448,10 @@ function createRuntimeCommand(config) {
4752
5448
  fiberRpcUrl: status.targetUrl,
4753
5449
  proxyListen: status.proxyListen,
4754
5450
  stateFilePath: runtimeConfig.storage?.stateFilePath,
4755
- alertLogFilePath: alertLogFile,
5451
+ alertLogFilePath: effectiveAlertLogPath,
5452
+ fnnStdoutLogPath: join8(todayLogDir, "fnn.stdout.log"),
5453
+ fnnStderrLogPath: join8(todayLogDir, "fnn.stderr.log"),
5454
+ logsBaseDir,
4756
5455
  daemon: daemon || isRuntimeChild
4757
5456
  });
4758
5457
  runtime2.service.on("alert", (alert) => {
@@ -4771,14 +5470,16 @@ function createRuntimeCommand(config) {
4771
5470
  fiberRpcUrl: status.targetUrl,
4772
5471
  proxyListen: status.proxyListen,
4773
5472
  stateFilePath: runtimeConfig.storage?.stateFilePath,
4774
- alertLogFile
5473
+ alertLogFile: effectiveAlertLogPath,
5474
+ logsBaseDir
4775
5475
  });
4776
5476
  printJsonEvent("runtime_started", status);
4777
5477
  } else {
4778
5478
  console.log(`Fiber RPC: ${status.targetUrl}`);
4779
5479
  console.log(`Proxy listen: ${status.proxyListen}`);
4780
5480
  console.log(`State file: ${runtimeConfig.storage?.stateFilePath}`);
4781
- console.log(`Alert log: ${alertLogFile}`);
5481
+ console.log(`Logs dir: ${logsBaseDir}`);
5482
+ console.log(`Alert log: ${effectiveAlertLogPath}`);
4782
5483
  console.log("Runtime monitor is running. Press Ctrl+C to stop.");
4783
5484
  }
4784
5485
  const signal = await runtime2.waitForShutdownSignal();
@@ -4812,8 +5513,42 @@ function createRuntimeCommand(config) {
4812
5513
  });
4813
5514
  runtime.command("status").description("Show runtime process and health status").option("--json").action(async (options) => {
4814
5515
  const asJson = Boolean(options.json);
4815
- const pid = readRuntimePid(config.dataDir);
5516
+ let pid = readRuntimePid(config.dataDir);
4816
5517
  const meta = readRuntimeMeta(config.dataDir);
5518
+ const recoveryListen = resolveRuntimeRecoveryListen(config);
5519
+ if (!pid) {
5520
+ const fallback = findListeningProcessByPort(recoveryListen);
5521
+ if (fallback && isFiberRuntimeCommand(fallback.command)) {
5522
+ pid = fallback.pid;
5523
+ writeRuntimePid(config.dataDir, pid);
5524
+ } else if (fallback && fallback.command) {
5525
+ const details = fallback.command ? `PID ${fallback.pid} (${fallback.command})` : `PID ${fallback.pid}`;
5526
+ if (asJson) {
5527
+ printJsonError({
5528
+ code: "RUNTIME_PORT_IN_USE",
5529
+ message: `Runtime proxy port is in use by non-fiber-pay process: ${details}`,
5530
+ recoverable: true,
5531
+ suggestion: "Stop that process or use a different --proxy-listen port."
5532
+ });
5533
+ } else {
5534
+ console.log(`Runtime proxy port is in use by non-fiber-pay process: ${details}`);
5535
+ }
5536
+ process.exit(1);
5537
+ } else if (fallback) {
5538
+ const message = `Runtime proxy port is in use by process PID ${fallback.pid}. The owning command could not be determined; inspect this PID manually.`;
5539
+ if (asJson) {
5540
+ printJsonError({
5541
+ code: "RUNTIME_PORT_IN_USE",
5542
+ message,
5543
+ recoverable: true,
5544
+ suggestion: "Inspect the PID owner manually or use a different --proxy-listen port."
5545
+ });
5546
+ } else {
5547
+ console.log(message);
5548
+ }
5549
+ process.exit(1);
5550
+ }
5551
+ }
4817
5552
  if (!pid) {
4818
5553
  if (asJson) {
4819
5554
  printJsonError({
@@ -4844,9 +5579,11 @@ function createRuntimeCommand(config) {
4844
5579
  process.exit(1);
4845
5580
  }
4846
5581
  let rpcStatus;
4847
- if (meta?.proxyListen) {
5582
+ if (meta?.proxyListen ?? recoveryListen) {
4848
5583
  try {
4849
- const response = await fetch(`http://${meta.proxyListen}/monitor/status`);
5584
+ const response = await fetch(
5585
+ `http://${meta?.proxyListen ?? recoveryListen}/monitor/status`
5586
+ );
4850
5587
  if (response.ok) {
4851
5588
  rpcStatus = await response.json();
4852
5589
  }
@@ -4874,7 +5611,41 @@ function createRuntimeCommand(config) {
4874
5611
  });
4875
5612
  runtime.command("stop").description("Stop runtime process by PID").option("--json").action(async (options) => {
4876
5613
  const asJson = Boolean(options.json);
4877
- const pid = readRuntimePid(config.dataDir);
5614
+ let pid = readRuntimePid(config.dataDir);
5615
+ const recoveryListen = resolveRuntimeRecoveryListen(config);
5616
+ if (!pid) {
5617
+ const fallback = findListeningProcessByPort(recoveryListen);
5618
+ if (fallback && isFiberRuntimeCommand(fallback.command)) {
5619
+ pid = fallback.pid;
5620
+ writeRuntimePid(config.dataDir, pid);
5621
+ } else if (fallback && fallback.command) {
5622
+ const details = fallback.command ? `PID ${fallback.pid} (${fallback.command})` : `PID ${fallback.pid}`;
5623
+ if (asJson) {
5624
+ printJsonError({
5625
+ code: "RUNTIME_PORT_IN_USE",
5626
+ message: `Runtime proxy port is in use by non-fiber-pay process: ${details}`,
5627
+ recoverable: true,
5628
+ suggestion: "Stop that process manually; it is not managed by fiber-pay runtime PID files."
5629
+ });
5630
+ } else {
5631
+ console.log(`Runtime proxy port is in use by non-fiber-pay process: ${details}`);
5632
+ }
5633
+ process.exit(1);
5634
+ } else if (fallback) {
5635
+ const message = `Runtime proxy port is in use by process PID ${fallback.pid}. The owning command could not be determined; inspect this PID manually.`;
5636
+ if (asJson) {
5637
+ printJsonError({
5638
+ code: "RUNTIME_PORT_IN_USE",
5639
+ message,
5640
+ recoverable: true,
5641
+ suggestion: "Inspect the PID owner manually; it may not be managed by fiber-pay runtime PID files."
5642
+ });
5643
+ } else {
5644
+ console.log(message);
5645
+ }
5646
+ process.exit(1);
5647
+ }
5648
+ }
4878
5649
  if (!pid) {
4879
5650
  if (asJson) {
4880
5651
  printJsonError({
@@ -4903,14 +5674,19 @@ function createRuntimeCommand(config) {
4903
5674
  }
4904
5675
  process.exit(1);
4905
5676
  }
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");
5677
+ const terminated = await terminateProcess(pid);
5678
+ if (!terminated) {
5679
+ if (asJson) {
5680
+ printJsonError({
5681
+ code: "RUNTIME_STOP_FAILED",
5682
+ message: `Failed to terminate runtime process ${pid}.`,
5683
+ recoverable: true,
5684
+ suggestion: `Try stopping PID ${pid} manually and rerun runtime stop.`
5685
+ });
5686
+ } else {
5687
+ console.log(`Failed to terminate runtime process ${pid}.`);
5688
+ }
5689
+ process.exit(1);
4914
5690
  }
4915
5691
  removeRuntimeFiles(config.dataDir);
4916
5692
  if (asJson) {
@@ -4926,8 +5702,8 @@ function createRuntimeCommand(config) {
4926
5702
  import { Command as Command12 } from "commander";
4927
5703
 
4928
5704
  // src/lib/build-info.ts
4929
- var CLI_VERSION = "0.1.0-rc.4";
4930
- var CLI_COMMIT = "20dc82d290856d411382a4ec30f912b913b4f956";
5705
+ var CLI_VERSION = "0.1.0-rc.5";
5706
+ var CLI_COMMIT = "28cee07226e004d775ed747bbcbf61474f93c492";
4931
5707
 
4932
5708
  // src/commands/version.ts
4933
5709
  function createVersionCommand() {
@@ -5035,6 +5811,14 @@ function applyGlobalOverrides(argv) {
5035
5811
  }
5036
5812
  break;
5037
5813
  }
5814
+ case "--rpc-biscuit-token": {
5815
+ const value = getFlagValue(argv, index);
5816
+ if (value) {
5817
+ process.env.FIBER_RPC_BISCUIT_TOKEN = value;
5818
+ explicitFlags.add("rpcBiscuitToken");
5819
+ }
5820
+ break;
5821
+ }
5038
5822
  case "--network": {
5039
5823
  const value = getFlagValue(argv, index);
5040
5824
  if (value) {
@@ -5074,7 +5858,7 @@ function applyGlobalOverrides(argv) {
5074
5858
  }
5075
5859
  if (!explicitDataDir && profileName) {
5076
5860
  const homeDir = process.env.HOME ?? process.cwd();
5077
- process.env.FIBER_DATA_DIR = join7(homeDir, ".fiber-pay", "profiles", profileName);
5861
+ process.env.FIBER_DATA_DIR = join9(homeDir, ".fiber-pay", "profiles", profileName);
5078
5862
  }
5079
5863
  }
5080
5864
  function printFatal(error) {
@@ -5103,7 +5887,7 @@ async function main() {
5103
5887
  applyGlobalOverrides(argv);
5104
5888
  const config = getEffectiveConfig(explicitFlags).config;
5105
5889
  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();
5890
+ 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
5891
  program.exitOverride();
5108
5892
  program.configureOutput({
5109
5893
  writeOut: (str) => process.stdout.write(str),