@fiber-pay/cli 0.1.0-rc.3 → 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,22 +1351,22 @@ 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
- var TESTNET_CONFIG_TEMPLATE_V061 = `# This configuration file only contains the necessary configurations for the testnet deployment.
1363
+ var TESTNET_CONFIG_TEMPLATE_V071 = `# This configuration file only contains the necessary configurations for the testnet deployment.
969
1364
  # All options' descriptions can be found via \`fnn --help\` and be overridden by command line arguments or environment variables.
970
1365
  fiber:
971
- listening_addr: "/ip4/127.0.0.1/tcp/8228"
1366
+ listening_addr: "/ip4/0.0.0.0/tcp/8228"
972
1367
  bootnode_addrs:
973
1368
  - "/ip4/54.179.226.154/tcp/8228/p2p/Qmes1EBD4yNo9Ywkfe6eRw9tG1nVNGLDmMud1xJMsoYFKy"
974
- - "/ip4/54.179.226.154/tcp/18228/p2p/QmdyQWjPtbK4NWWsvy8s69NGJaQULwgeQDT5ZpNDrTNaeV"
1369
+ - "/ip4/16.163.7.105/tcp/8228/p2p/QmdyQWjPtbK4NWWsvy8s69NGJaQULwgeQDT5ZpNDrTNaeV"
975
1370
  announce_listening_addr: true
976
1371
  announced_addrs:
977
1372
  # If you want to announce your fiber node public address to the network, you need to add the address here, please change the ip to your public ip accordingly.
@@ -1037,7 +1432,7 @@ services:
1037
1432
  - rpc
1038
1433
  - ckb
1039
1434
  `;
1040
- var MAINNET_CONFIG_TEMPLATE_V061 = `# This configuration file only contains the necessary configurations for the mainnet deployment.
1435
+ var MAINNET_CONFIG_TEMPLATE_V071 = `# This configuration file only contains the necessary configurations for the mainnet deployment.
1041
1436
  # All options' descriptions can be found via \`fnn --help\` and be overridden by command line arguments or environment variables.
1042
1437
  fiber:
1043
1438
  listening_addr: "/ip4/0.0.0.0/tcp/8228"
@@ -1113,7 +1508,7 @@ services:
1113
1508
  - ckb
1114
1509
  `;
1115
1510
  function getConfigTemplate(network) {
1116
- return network === "mainnet" ? MAINNET_CONFIG_TEMPLATE_V061 : TESTNET_CONFIG_TEMPLATE_V061;
1511
+ return network === "mainnet" ? MAINNET_CONFIG_TEMPLATE_V071 : TESTNET_CONFIG_TEMPLATE_V071;
1117
1512
  }
1118
1513
 
1119
1514
  // src/lib/config.ts
@@ -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,116 +3240,32 @@ Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
2678
3240
  });
2679
3241
  });
2680
3242
  }
2681
-
2682
- // src/commands/node.ts
2683
- import { nodeIdToPeerId as nodeIdToPeerId2, scriptToAddress as scriptToAddress2 } from "@fiber-pay/sdk";
2684
- import { Command as Command8 } from "commander";
2685
-
2686
- // src/lib/node-runtime-daemon.ts
2687
- import { spawnSync } from "child_process";
2688
- import { existsSync as existsSync8 } from "fs";
2689
- function getCustomBinaryState(binaryPath) {
2690
- const exists = existsSync8(binaryPath);
2691
- if (!exists) {
2692
- return { path: binaryPath, ready: false, version: "unknown" };
2693
- }
2694
- try {
2695
- const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
2696
- if (result.status !== 0) {
2697
- return { path: binaryPath, ready: false, version: "unknown" };
2698
- }
2699
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
2700
- const firstLine = output.split("\n").find((line) => line.trim().length > 0) ?? "unknown";
2701
- return { path: binaryPath, ready: true, version: firstLine.trim() };
2702
- } catch {
2703
- return { path: binaryPath, ready: false, version: "unknown" };
2704
- }
2705
- }
2706
- function getBinaryVersion(binaryPath) {
2707
- try {
2708
- const result = spawnSync(binaryPath, ["--version"], { encoding: "utf-8" });
2709
- if (result.status !== 0) {
2710
- return "unknown";
2711
- }
2712
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
2713
- if (!output) {
2714
- return "unknown";
2715
- }
2716
- const firstLine = output.split("\n").find((line) => line.trim().length > 0);
2717
- return firstLine?.trim() ?? "unknown";
2718
- } catch {
2719
- return "unknown";
2720
- }
2721
- }
2722
- function getCliEntrypoint() {
2723
- const entrypoint = process.argv[1];
2724
- if (!entrypoint) {
2725
- throw new Error("Unable to resolve CLI entrypoint path");
3243
+ function inferDateFromPaths(paths) {
3244
+ const candidate = paths.runtimeAlerts.split("/").at(-2);
3245
+ if (!candidate || !DATE_DIR_PATTERN2.test(candidate)) {
3246
+ return void 0;
2726
3247
  }
2727
- return entrypoint;
2728
- }
2729
- function startRuntimeDaemonFromNode(params) {
2730
- const cliEntrypoint = getCliEntrypoint();
2731
- const result = spawnSync(
2732
- process.execPath,
2733
- [
2734
- cliEntrypoint,
2735
- "--data-dir",
2736
- params.dataDir,
2737
- "--rpc-url",
2738
- params.rpcUrl,
2739
- "runtime",
2740
- "start",
2741
- "--daemon",
2742
- "--fiber-rpc-url",
2743
- params.rpcUrl,
2744
- "--proxy-listen",
2745
- params.proxyListen,
2746
- "--state-file",
2747
- params.stateFilePath,
2748
- "--alert-log-file",
2749
- params.alertLogFile,
2750
- "--json"
2751
- ],
2752
- { encoding: "utf-8" }
2753
- );
2754
- if (result.status === 0) {
2755
- return { ok: true };
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;
2756
3252
  }
2757
- const stderr = (result.stderr ?? "").trim();
2758
- const stdout = (result.stdout ?? "").trim();
2759
- const details = stderr || stdout || `exit code ${result.status ?? "unknown"}`;
2760
- return { ok: false, message: details };
2761
- }
2762
- function stopRuntimeDaemonFromNode(params) {
2763
- const cliEntrypoint = getCliEntrypoint();
2764
- spawnSync(
2765
- process.execPath,
2766
- [
2767
- cliEntrypoint,
2768
- "--data-dir",
2769
- params.dataDir,
2770
- "--rpc-url",
2771
- params.rpcUrl,
2772
- "runtime",
2773
- "stop",
2774
- "--json"
2775
- ],
2776
- { encoding: "utf-8" }
2777
- );
3253
+ return candidate;
2778
3254
  }
2779
3255
 
3256
+ // src/commands/node.ts
3257
+ import { Command as Command8 } from "commander";
3258
+
2780
3259
  // src/lib/node-start.ts
2781
3260
  import { spawn } from "child_process";
2782
- import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
2783
- import { join as join5 } from "path";
3261
+ import { mkdirSync as mkdirSync3 } from "fs";
3262
+ import { join as join7 } from "path";
2784
3263
  import {
3264
+ createKeyManager,
2785
3265
  ensureFiberBinary,
2786
- getDefaultBinaryPath,
2787
3266
  ProcessManager
2788
3267
  } from "@fiber-pay/node";
2789
3268
  import { startRuntimeService } from "@fiber-pay/runtime";
2790
- import { createKeyManager } from "@fiber-pay/sdk";
2791
3269
 
2792
3270
  // src/lib/bootnode.ts
2793
3271
  import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
@@ -2823,6 +3301,182 @@ async function autoConnectBootnodes(rpc, bootnodes) {
2823
3301
  }
2824
3302
  }
2825
3303
 
3304
+ // src/lib/node-migration.ts
3305
+ import { dirname as dirname2 } from "path";
3306
+ import { BinaryManager as BinaryManager2, MigrationManager } from "@fiber-pay/node";
3307
+
3308
+ // src/lib/migration-utils.ts
3309
+ function replaceRawMigrateHint(message) {
3310
+ return message.replace(
3311
+ /Fiber need to run some database migrations, please run `fnn-migrate[^`]*` to start migrations\.?/g,
3312
+ "Fiber database migration is required."
3313
+ );
3314
+ }
3315
+ function normalizeMigrationCheck(check) {
3316
+ return {
3317
+ ...check,
3318
+ message: replaceRawMigrateHint(check.message)
3319
+ };
3320
+ }
3321
+
3322
+ // src/lib/node-migration.ts
3323
+ async function runMigrationGuard(opts) {
3324
+ const { dataDir, binaryPath, json } = opts;
3325
+ if (!MigrationManager.storeExists(dataDir)) {
3326
+ return { checked: false, skippedReason: "store does not exist" };
3327
+ }
3328
+ const storePath = MigrationManager.resolveStorePath(dataDir);
3329
+ const binaryDir = dirname2(binaryPath);
3330
+ const bm = new BinaryManager2(binaryDir);
3331
+ const migrateBinPath = bm.getMigrateBinaryPath();
3332
+ let migrationCheck;
3333
+ try {
3334
+ const migrationManager = new MigrationManager(migrateBinPath);
3335
+ migrationCheck = await migrationManager.check(storePath);
3336
+ } catch {
3337
+ return { checked: false, skippedReason: "fnn-migrate binary not available" };
3338
+ }
3339
+ if (migrationCheck.needed) {
3340
+ const message = migrationCheck.valid ? "Database migration required. Run `fiber-pay node upgrade --force-migrate` before starting." : replaceRawMigrateHint(migrationCheck.message);
3341
+ if (json) {
3342
+ printJsonError({
3343
+ code: "MIGRATION_REQUIRED",
3344
+ message,
3345
+ recoverable: true,
3346
+ suggestion: `Back up your store first (directory: "${storePath}"). Then run \`fiber-pay node upgrade --force-migrate\`. If migration still fails, close channels on the old fnn version, remove the store, and restart with a fresh store. If backup exists, restore it to roll back.`,
3347
+ details: {
3348
+ storePath,
3349
+ migrationCheck: {
3350
+ ...migrationCheck,
3351
+ message
3352
+ }
3353
+ }
3354
+ });
3355
+ } else {
3356
+ console.error(`\u274C ${message}`);
3357
+ console.error(` 1) Back up store directory: ${storePath}`);
3358
+ console.error(" 2) Run: fiber-pay node upgrade --force-migrate");
3359
+ console.error(
3360
+ " 3) If it still fails, close channels on old fnn, remove store, then restart."
3361
+ );
3362
+ console.error(" 4) If backup exists, restore it to roll back.");
3363
+ }
3364
+ process.exit(1);
3365
+ }
3366
+ return { checked: true, migrationCheck };
3367
+ }
3368
+
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;
3375
+ }
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;
3383
+ }
3384
+ return port;
3385
+ }
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;
3391
+ }
3392
+ const pid = Number(trimmed.slice(1));
3393
+ if (Number.isInteger(pid) && pid > 0) {
3394
+ return pid;
3395
+ }
3396
+ }
3397
+ return void 0;
3398
+ }
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;
3405
+ }
3406
+ const command = (result.stdout ?? "").trim();
3407
+ return command.length > 0 ? command : void 0;
3408
+ }
3409
+ function findListeningProcessByPort(listen) {
3410
+ const port = parsePortFromListen(listen);
3411
+ if (!port) {
3412
+ return void 0;
3413
+ }
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
+ };
3428
+ }
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);
3478
+ }
3479
+
2826
3480
  // src/lib/node-start.ts
2827
3481
  async function runNodeStartCommand(config, options) {
2828
3482
  const json = Boolean(options.json);
@@ -2932,25 +3586,41 @@ async function runNodeStartCommand(config, options) {
2932
3586
  options.runtimeProxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
2933
3587
  );
2934
3588
  const proxyListenSource = options.runtimeProxyListen ? "cli" : config.runtimeProxyListen ? "profile" : "default";
2935
- const runtimeStateFilePath = join5(config.dataDir, "runtime-state.json");
2936
- const logsDir = join5(config.dataDir, "logs");
2937
- const fnnStdoutLogPath = join5(logsDir, "fnn.stdout.log");
2938
- const fnnStderrLogPath = join5(logsDir, "fnn.stderr.log");
2939
- const runtimeAlertLogPath = join5(logsDir, "runtime.alerts.jsonl");
2940
- mkdirSync2(logsDir, { recursive: true });
2941
- const binaryPath = config.binaryPath || getDefaultBinaryPath();
2942
- 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
+ }
2943
3599
  const binaryVersion = getBinaryVersion(binaryPath);
2944
3600
  const configFilePath = ensureNodeConfigFile(config.dataDir, config.network);
2945
3601
  emitStage("binary_resolved", "ok", {
2946
3602
  binaryPath,
2947
3603
  binaryVersion,
3604
+ binarySource: resolvedBinary.source,
2948
3605
  configFilePath
2949
3606
  });
2950
3607
  if (!json) {
2951
3608
  console.log(`\u{1F9E9} Binary: ${binaryPath}`);
2952
3609
  console.log(`\u{1F9E9} Version: ${binaryVersion}`);
2953
3610
  }
3611
+ const guardResult = await runMigrationGuard({ dataDir: config.dataDir, binaryPath, json });
3612
+ if (guardResult.checked) {
3613
+ emitStage("migration_check", "ok", {
3614
+ storePath: `${config.dataDir}/store`,
3615
+ needed: false
3616
+ });
3617
+ } else {
3618
+ emitStage("migration_check", "ok", {
3619
+ storePath: `${config.dataDir}/store`,
3620
+ skipped: true,
3621
+ reason: guardResult.skippedReason
3622
+ });
3623
+ }
2954
3624
  const nodeConfig = {
2955
3625
  binaryPath,
2956
3626
  dataDir: config.dataDir,
@@ -2992,11 +3662,11 @@ async function runNodeStartCommand(config, options) {
2992
3662
  removePidFile(config.dataDir);
2993
3663
  });
2994
3664
  processManager.on("stdout", (text) => {
2995
- appendFileSync(fnnStdoutLogPath, text, "utf-8");
3665
+ appendToTodayLog(config.dataDir, "fnn.stdout.log", text);
2996
3666
  emitFnnLog("stdout", text);
2997
3667
  });
2998
3668
  processManager.on("stderr", (text) => {
2999
- appendFileSync(fnnStderrLogPath, text, "utf-8");
3669
+ appendToTodayLog(config.dataDir, "fnn.stderr.log", text);
3000
3670
  emitFnnLog("stderr", text);
3001
3671
  });
3002
3672
  await processManager.start();
@@ -3041,13 +3711,38 @@ async function runNodeStartCommand(config, options) {
3041
3711
  process.exit(1);
3042
3712
  }
3043
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
+ }
3044
3739
  if (runtimeDaemon) {
3045
3740
  const daemonStart = startRuntimeDaemonFromNode({
3046
3741
  dataDir: config.dataDir,
3047
3742
  rpcUrl: config.rpcUrl,
3048
3743
  proxyListen: runtimeProxyListen,
3049
3744
  stateFilePath: runtimeStateFilePath,
3050
- alertLogFile: runtimeAlertLogPath
3745
+ alertLogsBaseDir: logsBaseDir
3051
3746
  });
3052
3747
  if (!daemonStart.ok) {
3053
3748
  throw new Error(daemonStart.message);
@@ -3062,23 +3757,25 @@ async function runNodeStartCommand(config, options) {
3062
3757
  storage: {
3063
3758
  stateFilePath: runtimeStateFilePath
3064
3759
  },
3065
- alerts: [{ type: "stdout" }, { type: "file", path: runtimeAlertLogPath }],
3760
+ alerts: [{ type: "stdout" }, { type: "daily-file", baseLogsDir: logsBaseDir }],
3066
3761
  jobs: {
3067
3762
  enabled: true,
3068
- dbPath: join5(config.dataDir, "runtime-jobs.db")
3763
+ dbPath: join7(config.dataDir, "runtime-jobs.db")
3069
3764
  }
3070
3765
  });
3071
3766
  const runtimeStatus = runtime.service.getStatus();
3072
3767
  writeRuntimePid(config.dataDir, process.pid);
3768
+ const todayLogDir = resolveLogDirForDate(config.dataDir);
3073
3769
  writeRuntimeMeta(config.dataDir, {
3074
3770
  pid: process.pid,
3075
3771
  startedAt: runtimeStatus.startedAt,
3076
3772
  fiberRpcUrl: runtimeStatus.targetUrl,
3077
3773
  proxyListen: runtimeStatus.proxyListen,
3078
3774
  stateFilePath: runtimeStateFilePath,
3079
- alertLogFilePath: runtimeAlertLogPath,
3080
- fnnStdoutLogPath,
3081
- fnnStderrLogPath,
3775
+ alertLogFilePath: join7(todayLogDir, "runtime.alerts.jsonl"),
3776
+ fnnStdoutLogPath: join7(todayLogDir, "fnn.stdout.log"),
3777
+ fnnStderrLogPath: join7(todayLogDir, "fnn.stderr.log"),
3778
+ logsBaseDir,
3082
3779
  daemon: false
3083
3780
  });
3084
3781
  }
@@ -3148,7 +3845,7 @@ async function runNodeStartCommand(config, options) {
3148
3845
  process.exit(1);
3149
3846
  }
3150
3847
  emitStage("rpc_ready", "ok", { rpcUrl: config.rpcUrl });
3151
- 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"));
3152
3849
  if (bootnodes.length > 0) {
3153
3850
  await autoConnectBootnodes(rpc, bootnodes);
3154
3851
  }
@@ -3189,9 +3886,8 @@ async function runNodeStartCommand(config, options) {
3189
3886
  proxyUrl: `http://${runtimeProxyListen}`,
3190
3887
  proxyListenSource,
3191
3888
  logs: {
3192
- fnnStdout: fnnStdoutLogPath,
3193
- fnnStderr: fnnStderrLogPath,
3194
- runtimeAlerts: runtimeAlertLogPath
3889
+ baseDir: logsBaseDir,
3890
+ todayDir: resolveLogDirForDate(config.dataDir)
3195
3891
  }
3196
3892
  });
3197
3893
  } else {
@@ -3201,7 +3897,7 @@ async function runNodeStartCommand(config, options) {
3201
3897
  ` Runtime proxy: http://${runtimeProxyListen} (browser-safe endpoint + monitoring)`
3202
3898
  );
3203
3899
  console.log(` Runtime mode: ${runtimeDaemon ? "daemon" : "embedded"}`);
3204
- console.log(` Log files: ${logsDir}`);
3900
+ console.log(` Log files: ${logsBaseDir}`);
3205
3901
  console.log(" Press Ctrl+C to stop.");
3206
3902
  }
3207
3903
  let shutdownRequested = false;
@@ -3269,8 +3965,6 @@ async function runNodeStartCommand(config, options) {
3269
3965
 
3270
3966
  // src/lib/node-status.ts
3271
3967
  import { existsSync as existsSync10 } from "fs";
3272
- import { join as join6 } from "path";
3273
- import { getFiberBinaryInfo as getFiberBinaryInfo2 } from "@fiber-pay/node";
3274
3968
  import {
3275
3969
  buildMultiaddrFromNodeId,
3276
3970
  buildMultiaddrFromRpcUrl,
@@ -3437,24 +4131,29 @@ async function runNodeStatusCommand(config, options) {
3437
4131
  const json = Boolean(options.json);
3438
4132
  const pid = readPidFile(config.dataDir);
3439
4133
  const resolvedRpc = resolveRpcEndpoint(config);
3440
- const managedBinaryPath = join6(config.dataDir, "bin", "fnn");
3441
- const binaryInfo = config.binaryPath ? getCustomBinaryState(config.binaryPath) : await getFiberBinaryInfo2(join6(config.dataDir, "bin"));
4134
+ const { resolvedBinary, info: binaryInfo } = await getBinaryDetails(config);
3442
4135
  const configExists = existsSync10(config.configPath);
3443
4136
  const nodeRunning = Boolean(pid && isProcessRunning(pid));
3444
4137
  let rpcResponsive = false;
3445
4138
  let nodeId = null;
4139
+ let addresses = [];
4140
+ let chainHash = null;
4141
+ let version = null;
3446
4142
  let peerId = null;
4143
+ let peersCount = 0;
3447
4144
  let peerIdError = null;
3448
4145
  let multiaddr = null;
3449
4146
  let multiaddrError = null;
3450
4147
  let multiaddrInferred = false;
3451
4148
  let channelsTotal = 0;
3452
4149
  let channelsReady = 0;
4150
+ let pendingChannelCount = 0;
3453
4151
  let canSend = false;
3454
4152
  let canReceive = false;
3455
4153
  let localCkb = 0;
3456
4154
  let remoteCkb = 0;
3457
4155
  let fundingAddress = null;
4156
+ let fundingLockScript = null;
3458
4157
  let fundingCkb = 0;
3459
4158
  let fundingBalanceError = null;
3460
4159
  if (nodeRunning) {
@@ -3464,6 +4163,10 @@ async function runNodeStatusCommand(config, options) {
3464
4163
  const channels = await rpc.listChannels({ include_closed: false });
3465
4164
  rpcResponsive = true;
3466
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);
3467
4170
  try {
3468
4171
  peerId = await nodeIdToPeerId(nodeInfo.node_id);
3469
4172
  } catch (error) {
@@ -3490,18 +4193,17 @@ async function runNodeStatusCommand(config, options) {
3490
4193
  (channel) => channel.state?.state_name === ChannelState2.ChannelReady
3491
4194
  );
3492
4195
  channelsReady = readyChannels.length;
4196
+ pendingChannelCount = Math.max(channelsTotal - channelsReady, 0);
3493
4197
  const liquidity = summarizeChannelLiquidity(readyChannels);
3494
4198
  canSend = liquidity.canSend;
3495
4199
  canReceive = liquidity.canReceive;
3496
4200
  localCkb = liquidity.localCkb;
3497
4201
  remoteCkb = liquidity.remoteCkb;
3498
4202
  fundingAddress = scriptToAddress(nodeInfo.default_funding_lock_script, config.network);
4203
+ fundingLockScript = nodeInfo.default_funding_lock_script;
3499
4204
  if (config.ckbRpcUrl) {
3500
4205
  try {
3501
- const fundingBalance = await getLockBalanceShannons(
3502
- config.ckbRpcUrl,
3503
- nodeInfo.default_funding_lock_script
3504
- );
4206
+ const fundingBalance = await getLockBalanceShannons(config.ckbRpcUrl, fundingLockScript);
3505
4207
  fundingCkb = Number(fundingBalance) / 1e8;
3506
4208
  } catch (error) {
3507
4209
  fundingBalanceError = error instanceof Error ? error.message : "Failed to query CKB balance for funding address";
@@ -3532,18 +4234,24 @@ async function runNodeStatusCommand(config, options) {
3532
4234
  rpcTarget: resolvedRpc.target,
3533
4235
  resolvedRpcUrl: resolvedRpc.url,
3534
4236
  nodeId,
4237
+ addresses,
4238
+ chainHash,
4239
+ version,
3535
4240
  peerId,
4241
+ peersCount,
3536
4242
  peerIdError,
3537
4243
  multiaddr,
3538
4244
  multiaddrError,
3539
4245
  multiaddrInferred,
4246
+ fundingLockScript,
3540
4247
  checks: {
3541
4248
  binary: {
3542
4249
  path: binaryInfo.path,
3543
4250
  ready: binaryInfo.ready,
3544
4251
  version: binaryInfo.version,
3545
- source: config.binaryPath ? "env-binary-path" : "managed-binary-dir",
3546
- managedPath: managedBinaryPath
4252
+ source: resolvedBinary.source,
4253
+ managedPath: resolvedBinary.managedPath,
4254
+ resolvedPath: resolvedBinary.binaryPath
3547
4255
  },
3548
4256
  config: {
3549
4257
  path: config.configPath,
@@ -3561,6 +4269,7 @@ async function runNodeStatusCommand(config, options) {
3561
4269
  channels: {
3562
4270
  total: channelsTotal,
3563
4271
  ready: channelsReady,
4272
+ pending: pendingChannelCount,
3564
4273
  canSend,
3565
4274
  canReceive
3566
4275
  }
@@ -3587,6 +4296,12 @@ async function runNodeStatusCommand(config, options) {
3587
4296
  console.log(`\u2705 Node is running (PID: ${output.pid})`);
3588
4297
  if (output.rpcResponsive) {
3589
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
+ }
3590
4305
  if (output.peerId) {
3591
4306
  console.log(` Peer ID: ${String(output.peerId)}`);
3592
4307
  } else if (output.peerIdError) {
@@ -3602,6 +4317,12 @@ async function runNodeStatusCommand(config, options) {
3602
4317
  } else {
3603
4318
  console.log(" Multiaddr: unavailable");
3604
4319
  }
4320
+ if (output.addresses.length > 0) {
4321
+ console.log(" Addresses:");
4322
+ for (const address of output.addresses) {
4323
+ console.log(` - ${address}`);
4324
+ }
4325
+ }
3605
4326
  } else {
3606
4327
  console.log(" \u26A0\uFE0F RPC not responding");
3607
4328
  }
@@ -3613,11 +4334,17 @@ async function runNodeStatusCommand(config, options) {
3613
4334
  console.log("");
3614
4335
  console.log("Diagnostics");
3615
4336
  console.log(` Binary: ${output.checks.binary.ready ? "ready" : "missing"}`);
4337
+ console.log(` Binary Path: ${output.checks.binary.resolvedPath}`);
3616
4338
  console.log(` Config: ${output.checks.config.exists ? "present" : "missing"}`);
3617
4339
  console.log(` RPC: ${output.checks.node.rpcReachable ? "reachable" : "unreachable"}`);
3618
4340
  console.log(
3619
- ` 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`
3620
4342
  );
4343
+ if (output.rpcResponsive) {
4344
+ console.log(` Peers: ${output.peersCount}`);
4345
+ } else {
4346
+ console.log(" Peers: unavailable");
4347
+ }
3621
4348
  console.log(` Can Send: ${output.checks.channels.canSend ? "yes" : "no"}`);
3622
4349
  console.log(` Can Receive: ${output.checks.channels.canReceive ? "yes" : "no"}`);
3623
4350
  console.log(` Recommendation:${output.recommendation}`);
@@ -3721,67 +4448,351 @@ async function runNodeReadyCommand(config, options) {
3721
4448
  }
3722
4449
  }
3723
4450
 
3724
- // src/commands/node.ts
3725
- function createNodeCommand(config) {
3726
- const node = new Command8("node").description("Node management");
3727
- node.command("start").option("--daemon", "Start node in detached background mode (node + runtime)").option("--runtime-proxy-listen <host:port>", "Runtime monitor proxy listen address").option("--event-stream <format>", "Event stream format for --json mode (jsonl)", "jsonl").option("--quiet-fnn", "Do not mirror fnn stdout/stderr to console; keep file persistence").option("--json").action(async (options) => {
3728
- await runNodeStartCommand(config, options);
3729
- });
3730
- node.command("stop").option("--json").action(async (options) => {
3731
- const json = Boolean(options.json);
3732
- const runtimeMeta = readRuntimeMeta(config.dataDir);
3733
- const runtimePid = readRuntimePid(config.dataDir);
3734
- if (runtimeMeta?.daemon && runtimePid && isProcessRunning(runtimePid)) {
3735
- stopRuntimeDaemonFromNode({ dataDir: config.dataDir, rpcUrl: config.rpcUrl });
4451
+ // src/lib/node-stop.ts
4452
+ async function runNodeStopCommand(config, options) {
4453
+ const json = Boolean(options.json);
4454
+ const runtimeMeta = readRuntimeMeta(config.dataDir);
4455
+ const runtimePid = readRuntimePid(config.dataDir);
4456
+ if (runtimeMeta?.daemon && runtimePid && isProcessRunning(runtimePid)) {
4457
+ stopRuntimeDaemonFromNode({ dataDir: config.dataDir, rpcUrl: config.rpcUrl });
4458
+ }
4459
+ removeRuntimeFiles(config.dataDir);
4460
+ const pid = readPidFile(config.dataDir);
4461
+ if (!pid) {
4462
+ if (json) {
4463
+ printJsonError({
4464
+ code: "NODE_NOT_RUNNING",
4465
+ message: "No PID file found. Node may not be running.",
4466
+ recoverable: true,
4467
+ suggestion: "Run `fiber-pay node start` first if you intend to stop a node."
4468
+ });
4469
+ } else {
4470
+ console.log("\u274C No PID file found. Node may not be running.");
3736
4471
  }
3737
- removeRuntimeFiles(config.dataDir);
3738
- const pid = readPidFile(config.dataDir);
3739
- if (!pid) {
3740
- if (json) {
3741
- printJsonError({
3742
- code: "NODE_NOT_RUNNING",
3743
- message: "No PID file found. Node may not be running.",
3744
- recoverable: true,
3745
- suggestion: "Run `fiber-pay node start` first if you intend to stop a node."
3746
- });
3747
- } else {
3748
- console.log("\u274C No PID file found. Node may not be running.");
3749
- }
3750
- process.exit(1);
4472
+ process.exit(1);
4473
+ }
4474
+ if (!isProcessRunning(pid)) {
4475
+ if (json) {
4476
+ printJsonError({
4477
+ code: "NODE_NOT_RUNNING",
4478
+ message: `Process ${pid} is not running. Cleaning up PID file.`,
4479
+ recoverable: true,
4480
+ suggestion: "Start the node again if needed; stale PID has been cleaned.",
4481
+ details: { pid, stalePidFileCleaned: true }
4482
+ });
4483
+ } else {
4484
+ console.log(`\u274C Process ${pid} is not running. Cleaning up PID file.`);
3751
4485
  }
3752
- if (!isProcessRunning(pid)) {
3753
- if (json) {
3754
- printJsonError({
3755
- code: "NODE_NOT_RUNNING",
3756
- message: `Process ${pid} is not running. Cleaning up PID file.`,
3757
- recoverable: true,
3758
- suggestion: "Start the node again if needed; stale PID has been cleaned.",
3759
- details: { pid, stalePidFileCleaned: true }
3760
- });
3761
- } else {
3762
- console.log(`\u274C Process ${pid} is not running. Cleaning up PID file.`);
4486
+ removePidFile(config.dataDir);
4487
+ process.exit(1);
4488
+ }
4489
+ if (!json) {
4490
+ console.log(`\u{1F6D1} Stopping node (PID: ${pid})...`);
4491
+ }
4492
+ process.kill(pid, "SIGTERM");
4493
+ let attempts = 0;
4494
+ while (isProcessRunning(pid) && attempts < 30) {
4495
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
4496
+ attempts++;
4497
+ }
4498
+ if (isProcessRunning(pid)) {
4499
+ process.kill(pid, "SIGKILL");
4500
+ }
4501
+ removePidFile(config.dataDir);
4502
+ if (json) {
4503
+ printJsonSuccess({ pid, stopped: true });
4504
+ } else {
4505
+ console.log("\u2705 Node stopped.");
4506
+ }
4507
+ }
4508
+
4509
+ // src/lib/node-upgrade.ts
4510
+ import { BinaryManager as BinaryManager3, MigrationManager as MigrationManager2 } from "@fiber-pay/node";
4511
+ async function runNodeUpgradeCommand(config, options) {
4512
+ const json = Boolean(options.json);
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);
4532
+ const pid = readPidFile(config.dataDir);
4533
+ if (pid && isProcessRunning(pid)) {
4534
+ const msg = "The Fiber node is currently running. Stop it before upgrading.";
4535
+ if (json) {
4536
+ printJsonError({
4537
+ code: "NODE_RUNNING",
4538
+ message: msg,
4539
+ recoverable: true,
4540
+ suggestion: "Run `fiber-pay node stop` first, then retry the upgrade."
4541
+ });
4542
+ } else {
4543
+ console.error(`\u274C ${msg}`);
4544
+ console.log(" Run: fiber-pay node stop");
4545
+ }
4546
+ process.exit(1);
4547
+ }
4548
+ let targetTag;
4549
+ if (options.version) {
4550
+ targetTag = binaryManager.normalizeTag(options.version);
4551
+ } else {
4552
+ if (!json) console.log("\u{1F50D} Resolving latest Fiber release...");
4553
+ targetTag = await binaryManager.getLatestTag();
4554
+ }
4555
+ if (!json) console.log(`\u{1F4E6} Target version: ${targetTag}`);
4556
+ const currentInfo = await binaryManager.getBinaryInfo();
4557
+ const targetVersion = targetTag.startsWith("v") ? targetTag.slice(1) : targetTag;
4558
+ const storePath = MigrationManager2.resolveStorePath(config.dataDir);
4559
+ const migrateBinaryPath = binaryManager.getMigrateBinaryPath();
4560
+ let migrationCheck = null;
4561
+ const storeExists = MigrationManager2.storeExists(config.dataDir);
4562
+ if (!json && storeExists) {
4563
+ console.log("\u{1F4C2} Existing store detected.");
4564
+ }
4565
+ if (currentInfo.ready && currentInfo.version === targetVersion && !options.forceMigrate) {
4566
+ if (storeExists) {
4567
+ migrationCheck = await runMigrationAndReport({
4568
+ migrateBinaryPath,
4569
+ storePath,
4570
+ json,
4571
+ checkOnly: Boolean(options.checkOnly),
4572
+ targetVersion,
4573
+ backup: options.backup !== false,
4574
+ forceMigrateAttempt: false
4575
+ });
4576
+ }
4577
+ const msg = migrationCheck ? `Already installed ${targetTag}. Store compatibility checked.` : `Already installed ${targetTag}. Use --force-migrate to run migration flow anyway.`;
4578
+ if (json) {
4579
+ printJsonSuccess({
4580
+ action: "none",
4581
+ currentVersion: currentInfo.version,
4582
+ targetVersion,
4583
+ message: msg,
4584
+ migration: migrationCheck
4585
+ });
4586
+ } else {
4587
+ console.log(`\u2705 ${msg}`);
4588
+ }
4589
+ return;
4590
+ }
4591
+ const versionMatches = currentInfo.ready && currentInfo.version === targetVersion;
4592
+ const shouldDownload = !versionMatches;
4593
+ if (!json && currentInfo.ready) {
4594
+ console.log(` Current version: v${currentInfo.version}`);
4595
+ }
4596
+ if (shouldDownload) {
4597
+ if (!json && storeExists) {
4598
+ console.log("\u{1F4C2} Existing store detected, will check migration after download.");
4599
+ }
4600
+ if (!json) console.log("\u2B07\uFE0F Downloading new binary...");
4601
+ const showProgress2 = (progress) => {
4602
+ if (!json) {
4603
+ const percent = progress.percent !== void 0 ? ` (${progress.percent}%)` : "";
4604
+ process.stdout.write(`\r [${progress.phase}]${percent} ${progress.message}`.padEnd(80));
4605
+ if (progress.phase === "installing") console.log();
3763
4606
  }
3764
- removePidFile(config.dataDir);
3765
- process.exit(1);
4607
+ };
4608
+ await binaryManager.download({
4609
+ version: targetTag,
4610
+ force: true,
4611
+ onProgress: showProgress2
4612
+ });
4613
+ } else if (!json && options.forceMigrate) {
4614
+ console.log("\u23ED\uFE0F Skipping binary download: target version is already installed.");
4615
+ console.log("\u{1F501} --force-migrate enabled: attempting migration flow on existing binaries.");
4616
+ }
4617
+ if (storeExists) {
4618
+ migrationCheck = await runMigrationAndReport({
4619
+ migrateBinaryPath,
4620
+ storePath,
4621
+ json,
4622
+ checkOnly: Boolean(options.checkOnly),
4623
+ targetVersion,
4624
+ backup: options.backup !== false,
4625
+ forceMigrateAttempt: Boolean(options.forceMigrate)
4626
+ });
4627
+ }
4628
+ const newInfo = await binaryManager.getBinaryInfo();
4629
+ if (json) {
4630
+ printJsonSuccess({
4631
+ action: "upgraded",
4632
+ previousVersion: currentInfo.ready ? currentInfo.version : null,
4633
+ currentVersion: newInfo.version,
4634
+ binaryPath: newInfo.path,
4635
+ migrateBinaryPath,
4636
+ migration: migrationCheck
4637
+ });
4638
+ } else {
4639
+ console.log("\n\u2705 Upgrade complete!");
4640
+ console.log(` Version: v${newInfo.version}`);
4641
+ console.log(` Binary: ${newInfo.path}`);
4642
+ console.log("\n Start the node with: fiber-pay node start");
4643
+ }
4644
+ }
4645
+ async function runMigrationAndReport(opts) {
4646
+ const {
4647
+ migrateBinaryPath,
4648
+ storePath,
4649
+ json,
4650
+ checkOnly,
4651
+ targetVersion,
4652
+ backup,
4653
+ forceMigrateAttempt
4654
+ } = opts;
4655
+ let migrationManager;
4656
+ try {
4657
+ migrationManager = new MigrationManager2(migrateBinaryPath);
4658
+ } catch (err) {
4659
+ const msg = err instanceof Error ? err.message : "fnn-migrate binary not available";
4660
+ if (json) {
4661
+ printJsonError({
4662
+ code: "MIGRATION_TOOL_MISSING",
4663
+ message: msg,
4664
+ recoverable: true,
4665
+ suggestion: "Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
4666
+ });
4667
+ } else {
4668
+ console.error(`
4669
+ \u26A0\uFE0F ${msg}`);
4670
+ console.log(
4671
+ " Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
4672
+ );
3766
4673
  }
3767
- if (!json) {
3768
- console.log(`\u{1F6D1} Stopping node (PID: ${pid})...`);
4674
+ process.exit(1);
4675
+ }
4676
+ if (!json) console.log("\u{1F50D} Checking store compatibility...");
4677
+ let migrationCheck;
4678
+ try {
4679
+ migrationCheck = await migrationManager.check(storePath);
4680
+ } catch (checkErr) {
4681
+ const msg = checkErr instanceof Error ? checkErr.message : String(checkErr);
4682
+ if (json) {
4683
+ printJsonError({
4684
+ code: "MIGRATION_TOOL_MISSING",
4685
+ message: `Migration check failed: ${msg}`,
4686
+ recoverable: true,
4687
+ suggestion: "Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
4688
+ });
4689
+ } else {
4690
+ console.error(`
4691
+ \u26A0\uFE0F Migration check failed: ${msg}`);
4692
+ console.log(
4693
+ " Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
4694
+ );
3769
4695
  }
3770
- process.kill(pid, "SIGTERM");
3771
- let attempts = 0;
3772
- while (isProcessRunning(pid) && attempts < 30) {
3773
- await new Promise((resolve2) => setTimeout(resolve2, 100));
3774
- attempts++;
4696
+ process.exit(1);
4697
+ }
4698
+ if (checkOnly) {
4699
+ const normalizedCheck = normalizeMigrationCheck(migrationCheck);
4700
+ if (json) {
4701
+ printJsonSuccess({
4702
+ action: "check-only",
4703
+ targetVersion,
4704
+ migration: normalizedCheck
4705
+ });
4706
+ } else {
4707
+ console.log(`
4708
+ \u{1F4CB} Migration status: ${normalizedCheck.message}`);
3775
4709
  }
3776
- if (isProcessRunning(pid)) {
3777
- process.kill(pid, "SIGKILL");
4710
+ process.exit(0);
4711
+ }
4712
+ if (!migrationCheck.needed) {
4713
+ if (!json) console.log(" Store is compatible, no migration needed.");
4714
+ return normalizeMigrationCheck(migrationCheck);
4715
+ }
4716
+ if (!migrationCheck.valid && !forceMigrateAttempt) {
4717
+ const normalizedMessage = replaceRawMigrateHint(migrationCheck.message);
4718
+ if (json) {
4719
+ printJsonError({
4720
+ code: "MIGRATION_INCOMPATIBLE",
4721
+ message: normalizedMessage,
4722
+ recoverable: false,
4723
+ suggestion: `Back up your store first (directory: "${storePath}"). Then run \`fiber-pay node upgrade --force-migrate\`. If it still fails, close all channels with the old fnn version, remove the store, and restart with a fresh store. If you attempted migration with backup enabled, you can roll back by restoring the backup directory.`,
4724
+ details: {
4725
+ storePath,
4726
+ migrationCheck: {
4727
+ ...migrationCheck,
4728
+ message: normalizedMessage
4729
+ }
4730
+ }
4731
+ });
4732
+ } else {
4733
+ console.error("\n\u274C Store migration is not possible automatically.");
4734
+ console.log(normalizedMessage);
4735
+ console.log(` 1) Back up store directory: ${storePath}`);
4736
+ console.log(" 2) Try: fiber-pay node upgrade --force-migrate");
4737
+ console.log(
4738
+ " 3) If it still fails, close channels on old fnn, remove store, then restart."
4739
+ );
4740
+ console.log(" 4) If migration created a backup, you can roll back by restoring it.");
3778
4741
  }
3779
- removePidFile(config.dataDir);
4742
+ process.exit(1);
4743
+ }
4744
+ if (!migrationCheck.valid && !json) {
4745
+ console.log("\u26A0\uFE0F Store check reported incompatibility, but --force-migrate is set.");
4746
+ console.log(" Attempting migration anyway with backup enabled (unless --no-backup).");
4747
+ }
4748
+ if (!json) console.log("\u{1F504} Running database migration...");
4749
+ const result = await migrationManager.migrate({
4750
+ storePath,
4751
+ backup,
4752
+ force: forceMigrateAttempt
4753
+ });
4754
+ if (!result.success) {
3780
4755
  if (json) {
3781
- printJsonSuccess({ pid, stopped: true });
4756
+ printJsonError({
4757
+ code: "MIGRATION_FAILED",
4758
+ message: result.message,
4759
+ recoverable: !!result.backupPath,
4760
+ suggestion: result.backupPath ? `To roll back, delete the current store at "${storePath}" and restore the backup from "${result.backupPath}".` : "Re-download the previous version or start fresh.",
4761
+ details: { output: result.output, backupPath: result.backupPath }
4762
+ });
3782
4763
  } else {
3783
- console.log("\u2705 Node stopped.");
4764
+ console.error("\n\u274C Migration failed.");
4765
+ console.log(result.message);
4766
+ }
4767
+ process.exit(1);
4768
+ }
4769
+ if (!json) {
4770
+ console.log(`\u2705 ${result.message}`);
4771
+ if (result.backupPath) {
4772
+ console.log(` Backup: ${result.backupPath}`);
4773
+ }
4774
+ }
4775
+ try {
4776
+ const postCheck = await migrationManager.check(storePath);
4777
+ return normalizeMigrationCheck(postCheck);
4778
+ } catch (err) {
4779
+ if (!json) {
4780
+ const message = err instanceof Error ? err.message : String(err);
4781
+ console.error("\u26A0\uFE0F Post-migration check failed; final migration status may be stale.");
4782
+ console.error(` ${message}`);
3784
4783
  }
4784
+ return normalizeMigrationCheck(migrationCheck);
4785
+ }
4786
+ }
4787
+
4788
+ // src/commands/node.ts
4789
+ function createNodeCommand(config) {
4790
+ const node = new Command8("node").description("Node management");
4791
+ node.command("start").option("--daemon", "Start node in detached background mode (node + runtime)").option("--runtime-proxy-listen <host:port>", "Runtime monitor proxy listen address").option("--event-stream <format>", "Event stream format for --json mode (jsonl)", "jsonl").option("--quiet-fnn", "Do not mirror fnn stdout/stderr to console; keep file persistence").option("--json").action(async (options) => {
4792
+ await runNodeStartCommand(config, options);
4793
+ });
4794
+ node.command("stop").option("--json").action(async (options) => {
4795
+ await runNodeStopCommand(config, options);
3785
4796
  });
3786
4797
  node.command("status").option("--json").action(async (options) => {
3787
4798
  await runNodeStatusCommand(config, options);
@@ -3789,34 +4800,17 @@ function createNodeCommand(config) {
3789
4800
  node.command("ready").description("Agent-oriented readiness summary for automation").option("--json").action(async (options) => {
3790
4801
  await runNodeReadyCommand(config, options);
3791
4802
  });
3792
- node.command("info").option("--json").action(async (options) => {
3793
- const rpc = await createReadyRpcClient(config);
3794
- const nodeInfo = await rpc.nodeInfo();
3795
- const fundingAddress = scriptToAddress2(nodeInfo.default_funding_lock_script, config.network);
3796
- const peerId = await nodeIdToPeerId2(nodeInfo.node_id);
3797
- const output = {
3798
- nodeId: nodeInfo.node_id,
3799
- peerId,
3800
- addresses: nodeInfo.addresses,
3801
- chainHash: nodeInfo.chain_hash,
3802
- fundingAddress,
3803
- fundingLockScript: nodeInfo.default_funding_lock_script,
3804
- version: nodeInfo.version,
3805
- channelCount: parseInt(nodeInfo.channel_count, 16),
3806
- pendingChannelCount: parseInt(nodeInfo.pending_channel_count, 16),
3807
- peersCount: parseInt(nodeInfo.peers_count, 16)
3808
- };
3809
- if (options.json) {
3810
- printJsonSuccess(output);
3811
- } else {
3812
- printNodeInfoHuman(output);
3813
- }
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(
4804
+ "--force-migrate",
4805
+ "Force migration attempt even when compatibility check reports incompatible data"
4806
+ ).option("--json").action(async (options) => {
4807
+ await runNodeUpgradeCommand(config, options);
3814
4808
  });
3815
4809
  return node;
3816
4810
  }
3817
4811
 
3818
4812
  // src/commands/payment.ts
3819
- import { ckbToShannons as ckbToShannons3, shannonsToCkb as shannonsToCkb4 } from "@fiber-pay/sdk";
4813
+ import { ckbToShannons as ckbToShannons4, shannonsToCkb as shannonsToCkb5 } from "@fiber-pay/sdk";
3820
4814
  import { Command as Command9 } from "commander";
3821
4815
  function createPaymentCommand(config) {
3822
4816
  const payment = new Command9("payment").description("Payment lifecycle and status commands");
@@ -3858,9 +4852,9 @@ function createPaymentCommand(config) {
3858
4852
  const paymentParams = {
3859
4853
  invoice,
3860
4854
  target_pubkey: recipientNodeId,
3861
- amount: amountCkb ? ckbToShannons3(amountCkb) : void 0,
4855
+ amount: amountCkb ? ckbToShannons4(amountCkb) : void 0,
3862
4856
  keysend: recipientNodeId ? true : void 0,
3863
- max_fee_amount: maxFeeCkb ? ckbToShannons3(maxFeeCkb) : void 0
4857
+ max_fee_amount: maxFeeCkb ? ckbToShannons4(maxFeeCkb) : void 0
3864
4858
  };
3865
4859
  const endpoint = resolveRpcEndpoint(config);
3866
4860
  if (endpoint.target === "runtime-proxy") {
@@ -3902,7 +4896,7 @@ function createPaymentCommand(config) {
3902
4896
  const payload = {
3903
4897
  paymentHash: result.payment_hash,
3904
4898
  status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
3905
- feeCkb: shannonsToCkb4(result.fee),
4899
+ feeCkb: shannonsToCkb5(result.fee),
3906
4900
  failureReason: result.failed_error
3907
4901
  };
3908
4902
  if (json) {
@@ -3917,6 +4911,7 @@ function createPaymentCommand(config) {
3917
4911
  }
3918
4912
  }
3919
4913
  });
4914
+ registerPaymentRebalanceCommand(payment, config);
3920
4915
  payment.command("get").argument("<paymentHash>").option("--json").action(async (paymentHash, options) => {
3921
4916
  const rpc = await createReadyRpcClient(config);
3922
4917
  const result = await rpc.getPayment({ payment_hash: paymentHash });
@@ -4065,7 +5060,7 @@ function createPaymentCommand(config) {
4065
5060
  process.exit(1);
4066
5061
  }
4067
5062
  const hopsInfo = pubkeys.map((pubkey) => ({ pubkey }));
4068
- const amount = options.amount ? ckbToShannons3(parseFloat(options.amount)) : void 0;
5063
+ const amount = options.amount ? ckbToShannons4(parseFloat(options.amount)) : void 0;
4069
5064
  const result = await rpc.buildRouter({
4070
5065
  hops_info: hopsInfo,
4071
5066
  amount
@@ -4081,7 +5076,7 @@ function createPaymentCommand(config) {
4081
5076
  console.log(
4082
5077
  ` Outpoint: ${hop.channel_outpoint.tx_hash}:${hop.channel_outpoint.index}`
4083
5078
  );
4084
- console.log(` Amount: ${shannonsToCkb4(hop.amount_received)} CKB`);
5079
+ console.log(` Amount: ${shannonsToCkb5(hop.amount_received)} CKB`);
4085
5080
  console.log(` Expiry: ${hop.incoming_tlc_expiry}`);
4086
5081
  }
4087
5082
  }
@@ -4089,7 +5084,7 @@ function createPaymentCommand(config) {
4089
5084
  payment.command("send-route").description("Send a payment using a pre-built route from `payment route`").requiredOption(
4090
5085
  "--router <json>",
4091
5086
  "JSON array of router hops (output of `payment route --json`)"
4092
- ).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) => {
4093
5088
  const rpc = await createReadyRpcClient(config);
4094
5089
  const json = Boolean(options.json);
4095
5090
  let router;
@@ -4114,12 +5109,13 @@ function createPaymentCommand(config) {
4114
5109
  invoice: options.invoice,
4115
5110
  payment_hash: options.paymentHash,
4116
5111
  keysend: options.keysend ? true : void 0,
5112
+ allow_self_payment: options.allowSelfPayment ? true : void 0,
4117
5113
  dry_run: options.dryRun ? true : void 0
4118
5114
  });
4119
5115
  const payload = {
4120
5116
  paymentHash: result.payment_hash,
4121
5117
  status: result.status === "Success" ? "success" : result.status === "Failed" ? "failed" : "pending",
4122
- feeCkb: shannonsToCkb4(result.fee),
5118
+ feeCkb: shannonsToCkb5(result.fee),
4123
5119
  failureReason: result.failed_error,
4124
5120
  dryRun: Boolean(options.dryRun)
4125
5121
  };
@@ -4143,7 +5139,7 @@ function getJobPaymentHash(job) {
4143
5139
  }
4144
5140
  function getJobFeeCkb(job) {
4145
5141
  const result = job.result;
4146
- return result?.fee ? shannonsToCkb4(result.fee) : 0;
5142
+ return result?.fee ? shannonsToCkb5(result.fee) : 0;
4147
5143
  }
4148
5144
  function getJobFailure(job) {
4149
5145
  const result = job.result;
@@ -4215,7 +5211,7 @@ function createPeerCommand(config) {
4215
5211
 
4216
5212
  // src/commands/runtime.ts
4217
5213
  import { spawn as spawn2 } from "child_process";
4218
- import { resolve } from "path";
5214
+ import { join as join8, resolve } from "path";
4219
5215
  import {
4220
5216
  alertPriorityOrder,
4221
5217
  formatRuntimeAlert as formatRuntimeAlert2,
@@ -4289,15 +5285,22 @@ function shouldPrintAlert(alert, filter) {
4289
5285
  }
4290
5286
  return true;
4291
5287
  }
5288
+ function resolveRuntimeRecoveryListen(config) {
5289
+ const meta = readRuntimeMeta(config.dataDir);
5290
+ return meta?.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229";
5291
+ }
4292
5292
  function createRuntimeCommand(config) {
4293
5293
  const runtime = new Command11("runtime").description("Polling monitor and alert runtime service");
4294
- 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(
4295
5295
  "--log-min-priority <priority>",
4296
5296
  "Minimum runtime log priority (critical|high|medium|low)"
4297
5297
  ).option("--log-type <types>", "Comma-separated runtime alert types to print").option("--json").action(async (options) => {
4298
5298
  const asJson = Boolean(options.json);
4299
5299
  const daemon = Boolean(options.daemon);
4300
5300
  const isRuntimeChild = process.env.FIBER_RUNTIME_CHILD === "1";
5301
+ const runtimeListen = String(
5302
+ options.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229"
5303
+ );
4301
5304
  try {
4302
5305
  const existingPid = readRuntimePid(config.dataDir);
4303
5306
  if (existingPid && isProcessRunning(existingPid) && (!isRuntimeChild || existingPid !== process.pid)) {
@@ -4317,6 +5320,32 @@ function createRuntimeCommand(config) {
4317
5320
  if (existingPid && !isProcessRunning(existingPid)) {
4318
5321
  removeRuntimeFiles(config.dataDir);
4319
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
+ }
4320
5349
  if (daemon && !isRuntimeChild) {
4321
5350
  const childArgv = process.argv.filter((arg) => arg !== "--daemon");
4322
5351
  const child = spawn2(process.execPath, childArgv.slice(1), {
@@ -4359,7 +5388,7 @@ function createRuntimeCommand(config) {
4359
5388
  ),
4360
5389
  proxy: {
4361
5390
  enabled: true,
4362
- listen: String(options.proxyListen ?? config.runtimeProxyListen ?? "127.0.0.1:8229")
5391
+ listen: runtimeListen
4363
5392
  },
4364
5393
  storage: {
4365
5394
  stateFilePath: options.stateFile ? resolve(String(options.stateFile)) : resolve(config.dataDir, "runtime-state.json"),
@@ -4370,9 +5399,21 @@ function createRuntimeCommand(config) {
4370
5399
  dbPath: resolve(config.dataDir, "runtime-jobs.db")
4371
5400
  }
4372
5401
  };
4373
- 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
+ }
4374
5411
  const alerts = [{ type: "stdout" }];
4375
- 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
+ }
4376
5417
  if (options.webhook) {
4377
5418
  alerts.push({ type: "webhook", url: String(options.webhook) });
4378
5419
  }
@@ -4394,6 +5435,12 @@ function createRuntimeCommand(config) {
4394
5435
  }
4395
5436
  const runtime2 = await startRuntimeService2(runtimeConfig);
4396
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");
4397
5444
  writeRuntimePid(config.dataDir, process.pid);
4398
5445
  writeRuntimeMeta(config.dataDir, {
4399
5446
  pid: process.pid,
@@ -4401,7 +5448,10 @@ function createRuntimeCommand(config) {
4401
5448
  fiberRpcUrl: status.targetUrl,
4402
5449
  proxyListen: status.proxyListen,
4403
5450
  stateFilePath: runtimeConfig.storage?.stateFilePath,
4404
- alertLogFilePath: alertLogFile,
5451
+ alertLogFilePath: effectiveAlertLogPath,
5452
+ fnnStdoutLogPath: join8(todayLogDir, "fnn.stdout.log"),
5453
+ fnnStderrLogPath: join8(todayLogDir, "fnn.stderr.log"),
5454
+ logsBaseDir,
4405
5455
  daemon: daemon || isRuntimeChild
4406
5456
  });
4407
5457
  runtime2.service.on("alert", (alert) => {
@@ -4420,14 +5470,16 @@ function createRuntimeCommand(config) {
4420
5470
  fiberRpcUrl: status.targetUrl,
4421
5471
  proxyListen: status.proxyListen,
4422
5472
  stateFilePath: runtimeConfig.storage?.stateFilePath,
4423
- alertLogFile
5473
+ alertLogFile: effectiveAlertLogPath,
5474
+ logsBaseDir
4424
5475
  });
4425
5476
  printJsonEvent("runtime_started", status);
4426
5477
  } else {
4427
5478
  console.log(`Fiber RPC: ${status.targetUrl}`);
4428
5479
  console.log(`Proxy listen: ${status.proxyListen}`);
4429
5480
  console.log(`State file: ${runtimeConfig.storage?.stateFilePath}`);
4430
- console.log(`Alert log: ${alertLogFile}`);
5481
+ console.log(`Logs dir: ${logsBaseDir}`);
5482
+ console.log(`Alert log: ${effectiveAlertLogPath}`);
4431
5483
  console.log("Runtime monitor is running. Press Ctrl+C to stop.");
4432
5484
  }
4433
5485
  const signal = await runtime2.waitForShutdownSignal();
@@ -4461,8 +5513,42 @@ function createRuntimeCommand(config) {
4461
5513
  });
4462
5514
  runtime.command("status").description("Show runtime process and health status").option("--json").action(async (options) => {
4463
5515
  const asJson = Boolean(options.json);
4464
- const pid = readRuntimePid(config.dataDir);
5516
+ let pid = readRuntimePid(config.dataDir);
4465
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
+ }
4466
5552
  if (!pid) {
4467
5553
  if (asJson) {
4468
5554
  printJsonError({
@@ -4493,9 +5579,11 @@ function createRuntimeCommand(config) {
4493
5579
  process.exit(1);
4494
5580
  }
4495
5581
  let rpcStatus;
4496
- if (meta?.proxyListen) {
5582
+ if (meta?.proxyListen ?? recoveryListen) {
4497
5583
  try {
4498
- const response = await fetch(`http://${meta.proxyListen}/monitor/status`);
5584
+ const response = await fetch(
5585
+ `http://${meta?.proxyListen ?? recoveryListen}/monitor/status`
5586
+ );
4499
5587
  if (response.ok) {
4500
5588
  rpcStatus = await response.json();
4501
5589
  }
@@ -4523,7 +5611,41 @@ function createRuntimeCommand(config) {
4523
5611
  });
4524
5612
  runtime.command("stop").description("Stop runtime process by PID").option("--json").action(async (options) => {
4525
5613
  const asJson = Boolean(options.json);
4526
- 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
+ }
4527
5649
  if (!pid) {
4528
5650
  if (asJson) {
4529
5651
  printJsonError({
@@ -4552,14 +5674,19 @@ function createRuntimeCommand(config) {
4552
5674
  }
4553
5675
  process.exit(1);
4554
5676
  }
4555
- process.kill(pid, "SIGTERM");
4556
- let attempts = 0;
4557
- while (isProcessRunning(pid) && attempts < 50) {
4558
- await new Promise((resolve2) => setTimeout(resolve2, 100));
4559
- attempts += 1;
4560
- }
4561
- if (isProcessRunning(pid)) {
4562
- 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);
4563
5690
  }
4564
5691
  removeRuntimeFiles(config.dataDir);
4565
5692
  if (asJson) {
@@ -4575,8 +5702,8 @@ function createRuntimeCommand(config) {
4575
5702
  import { Command as Command12 } from "commander";
4576
5703
 
4577
5704
  // src/lib/build-info.ts
4578
- var CLI_VERSION = "0.1.0-rc.3";
4579
- var CLI_COMMIT = "6f39077caade6d3a307db28b8d3e9cfdee7eaf99";
5705
+ var CLI_VERSION = "0.1.0-rc.5";
5706
+ var CLI_COMMIT = "28cee07226e004d775ed747bbcbf61474f93c492";
4580
5707
 
4581
5708
  // src/commands/version.ts
4582
5709
  function createVersionCommand() {
@@ -4594,6 +5721,55 @@ function createVersionCommand() {
4594
5721
  });
4595
5722
  }
4596
5723
 
5724
+ // src/lib/argv.ts
5725
+ var GLOBAL_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
5726
+ "--profile",
5727
+ "--data-dir",
5728
+ "--rpc-url",
5729
+ "--network",
5730
+ "--key-password",
5731
+ "--binary-path"
5732
+ ]);
5733
+ function isOptionToken(token) {
5734
+ return token.startsWith("-") && token !== "-";
5735
+ }
5736
+ function hasInlineValue(token) {
5737
+ return token.includes("=");
5738
+ }
5739
+ function getFirstPositional(argv) {
5740
+ for (let index = 2; index < argv.length; index++) {
5741
+ const token = argv[index];
5742
+ if (token === "--") {
5743
+ return argv[index + 1];
5744
+ }
5745
+ if (!isOptionToken(token)) {
5746
+ return token;
5747
+ }
5748
+ if (GLOBAL_OPTIONS_WITH_VALUE.has(token) && !hasInlineValue(token)) {
5749
+ const next = argv[index + 1];
5750
+ if (next && !isOptionToken(next)) {
5751
+ index++;
5752
+ }
5753
+ }
5754
+ }
5755
+ return void 0;
5756
+ }
5757
+ function hasTopLevelVersionFlag(argv) {
5758
+ for (let index = 2; index < argv.length; index++) {
5759
+ const token = argv[index];
5760
+ if (token === "--") {
5761
+ return false;
5762
+ }
5763
+ if (token === "--version" || token === "-v") {
5764
+ return true;
5765
+ }
5766
+ }
5767
+ return false;
5768
+ }
5769
+ function isTopLevelVersionRequest(argv) {
5770
+ return !getFirstPositional(argv) && hasTopLevelVersionFlag(argv);
5771
+ }
5772
+
4597
5773
  // src/index.ts
4598
5774
  function shouldOutputJson() {
4599
5775
  return process.argv.includes("--json");
@@ -4635,6 +5811,14 @@ function applyGlobalOverrides(argv) {
4635
5811
  }
4636
5812
  break;
4637
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
+ }
4638
5822
  case "--network": {
4639
5823
  const value = getFlagValue(argv, index);
4640
5824
  if (value) {
@@ -4674,7 +5858,7 @@ function applyGlobalOverrides(argv) {
4674
5858
  }
4675
5859
  if (!explicitDataDir && profileName) {
4676
5860
  const homeDir = process.env.HOME ?? process.cwd();
4677
- process.env.FIBER_DATA_DIR = join7(homeDir, ".fiber-pay", "profiles", profileName);
5861
+ process.env.FIBER_DATA_DIR = join9(homeDir, ".fiber-pay", "profiles", profileName);
4678
5862
  }
4679
5863
  }
4680
5864
  function printFatal(error) {
@@ -4695,10 +5879,15 @@ function printFatal(error) {
4695
5879
  }
4696
5880
  }
4697
5881
  async function main() {
4698
- applyGlobalOverrides(process.argv);
5882
+ const argv = process.argv;
5883
+ if (isTopLevelVersionRequest(argv)) {
5884
+ console.log(`${CLI_VERSION} (${CLI_COMMIT})`);
5885
+ return;
5886
+ }
5887
+ applyGlobalOverrides(argv);
4699
5888
  const config = getEffectiveConfig(explicitFlags).config;
4700
5889
  const program = new Command13();
4701
- program.name("fiber-pay").description("AI Agent Payment SDK for CKB Lightning Network").version(`${CLI_VERSION} (${CLI_COMMIT})`, "-v, --version", "Show version and commit id").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();
4702
5891
  program.exitOverride();
4703
5892
  program.configureOutput({
4704
5893
  writeOut: (str) => process.stdout.write(str),
@@ -4720,7 +5909,7 @@ async function main() {
4720
5909
  program.addCommand(createConfigCommand(config));
4721
5910
  program.addCommand(createRuntimeCommand(config));
4722
5911
  program.addCommand(createVersionCommand());
4723
- await program.parseAsync(process.argv);
5912
+ await program.parseAsync(argv);
4724
5913
  }
4725
5914
  main().catch((error) => {
4726
5915
  const commanderCode = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;