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

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
@@ -965,13 +965,13 @@ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, wr
965
965
  import { join as join3 } from "path";
966
966
 
967
967
  // src/lib/config-templates.ts
968
- var TESTNET_CONFIG_TEMPLATE_V061 = `# This configuration file only contains the necessary configurations for the testnet deployment.
968
+ var TESTNET_CONFIG_TEMPLATE_V071 = `# This configuration file only contains the necessary configurations for the testnet deployment.
969
969
  # All options' descriptions can be found via \`fnn --help\` and be overridden by command line arguments or environment variables.
970
970
  fiber:
971
- listening_addr: "/ip4/127.0.0.1/tcp/8228"
971
+ listening_addr: "/ip4/0.0.0.0/tcp/8228"
972
972
  bootnode_addrs:
973
973
  - "/ip4/54.179.226.154/tcp/8228/p2p/Qmes1EBD4yNo9Ywkfe6eRw9tG1nVNGLDmMud1xJMsoYFKy"
974
- - "/ip4/54.179.226.154/tcp/18228/p2p/QmdyQWjPtbK4NWWsvy8s69NGJaQULwgeQDT5ZpNDrTNaeV"
974
+ - "/ip4/16.163.7.105/tcp/8228/p2p/QmdyQWjPtbK4NWWsvy8s69NGJaQULwgeQDT5ZpNDrTNaeV"
975
975
  announce_listening_addr: true
976
976
  announced_addrs:
977
977
  # 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 +1037,7 @@ services:
1037
1037
  - rpc
1038
1038
  - ckb
1039
1039
  `;
1040
- var MAINNET_CONFIG_TEMPLATE_V061 = `# This configuration file only contains the necessary configurations for the mainnet deployment.
1040
+ var MAINNET_CONFIG_TEMPLATE_V071 = `# This configuration file only contains the necessary configurations for the mainnet deployment.
1041
1041
  # All options' descriptions can be found via \`fnn --help\` and be overridden by command line arguments or environment variables.
1042
1042
  fiber:
1043
1043
  listening_addr: "/ip4/0.0.0.0/tcp/8228"
@@ -1113,7 +1113,7 @@ services:
1113
1113
  - ckb
1114
1114
  `;
1115
1115
  function getConfigTemplate(network) {
1116
- return network === "mainnet" ? MAINNET_CONFIG_TEMPLATE_V061 : TESTNET_CONFIG_TEMPLATE_V061;
1116
+ return network === "mainnet" ? MAINNET_CONFIG_TEMPLATE_V071 : TESTNET_CONFIG_TEMPLATE_V071;
1117
1117
  }
1118
1118
 
1119
1119
  // src/lib/config.ts
@@ -2683,11 +2683,122 @@ Following logs (interval: ${intervalMs}ms). Press Ctrl+C to stop.`);
2683
2683
  import { nodeIdToPeerId as nodeIdToPeerId2, scriptToAddress as scriptToAddress2 } from "@fiber-pay/sdk";
2684
2684
  import { Command as Command8 } from "commander";
2685
2685
 
2686
+ // src/lib/node-start.ts
2687
+ import { spawn } from "child_process";
2688
+ import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
2689
+ import { join as join5 } from "path";
2690
+ import {
2691
+ createKeyManager,
2692
+ ensureFiberBinary,
2693
+ getDefaultBinaryPath,
2694
+ ProcessManager
2695
+ } from "@fiber-pay/node";
2696
+ import { startRuntimeService } from "@fiber-pay/runtime";
2697
+
2698
+ // src/lib/bootnode.ts
2699
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
2700
+ import { parse as parseYaml } from "yaml";
2701
+ function extractBootnodeAddrs(configFilePath) {
2702
+ if (!existsSync8(configFilePath)) return [];
2703
+ try {
2704
+ const content = readFileSync5(configFilePath, "utf-8");
2705
+ const doc = parseYaml(content);
2706
+ const addrs = doc?.fiber?.bootnode_addrs;
2707
+ if (!Array.isArray(addrs)) return [];
2708
+ return addrs.filter((a) => typeof a === "string" && a.startsWith("/ip"));
2709
+ } catch {
2710
+ return [];
2711
+ }
2712
+ }
2713
+ async function autoConnectBootnodes(rpc, bootnodes) {
2714
+ if (bootnodes.length === 0) return;
2715
+ console.log(`\u{1F517} Connecting to ${bootnodes.length} bootnode(s)...`);
2716
+ for (const addr of bootnodes) {
2717
+ const shortId = addr.match(/\/p2p\/(.+)$/)?.[1]?.slice(0, 12) || addr.slice(-12);
2718
+ try {
2719
+ await rpc.connectPeer({ address: addr });
2720
+ console.log(` \u2705 Connected to ${shortId}...`);
2721
+ } catch (err) {
2722
+ const msg = err instanceof Error ? err.message : String(err);
2723
+ if (msg.toLowerCase().includes("already")) {
2724
+ console.log(` \u2705 Already connected to ${shortId}...`);
2725
+ } else {
2726
+ console.error(` \u26A0\uFE0F Failed to connect to ${shortId}...: ${msg}`);
2727
+ }
2728
+ }
2729
+ }
2730
+ }
2731
+
2732
+ // src/lib/node-migration.ts
2733
+ import { dirname } from "path";
2734
+ import { BinaryManager, MigrationManager } from "@fiber-pay/node";
2735
+
2736
+ // src/lib/migration-utils.ts
2737
+ function replaceRawMigrateHint(message) {
2738
+ return message.replace(
2739
+ /Fiber need to run some database migrations, please run `fnn-migrate[^`]*` to start migrations\.?/g,
2740
+ "Fiber database migration is required."
2741
+ );
2742
+ }
2743
+ function normalizeMigrationCheck(check) {
2744
+ return {
2745
+ ...check,
2746
+ message: replaceRawMigrateHint(check.message)
2747
+ };
2748
+ }
2749
+
2750
+ // src/lib/node-migration.ts
2751
+ async function runMigrationGuard(opts) {
2752
+ const { dataDir, binaryPath, json } = opts;
2753
+ if (!MigrationManager.storeExists(dataDir)) {
2754
+ return { checked: false, skippedReason: "store does not exist" };
2755
+ }
2756
+ const storePath = MigrationManager.resolveStorePath(dataDir);
2757
+ const binaryDir = dirname(binaryPath);
2758
+ const bm = new BinaryManager(binaryDir);
2759
+ const migrateBinPath = bm.getMigrateBinaryPath();
2760
+ let migrationCheck;
2761
+ try {
2762
+ const migrationManager = new MigrationManager(migrateBinPath);
2763
+ migrationCheck = await migrationManager.check(storePath);
2764
+ } catch {
2765
+ return { checked: false, skippedReason: "fnn-migrate binary not available" };
2766
+ }
2767
+ if (migrationCheck.needed) {
2768
+ const message = migrationCheck.valid ? "Database migration required. Run `fiber-pay node upgrade --force-migrate` before starting." : replaceRawMigrateHint(migrationCheck.message);
2769
+ if (json) {
2770
+ printJsonError({
2771
+ code: "MIGRATION_REQUIRED",
2772
+ message,
2773
+ recoverable: true,
2774
+ 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.`,
2775
+ details: {
2776
+ storePath,
2777
+ migrationCheck: {
2778
+ ...migrationCheck,
2779
+ message
2780
+ }
2781
+ }
2782
+ });
2783
+ } else {
2784
+ console.error(`\u274C ${message}`);
2785
+ console.error(` 1) Back up store directory: ${storePath}`);
2786
+ console.error(" 2) Run: fiber-pay node upgrade --force-migrate");
2787
+ console.error(
2788
+ " 3) If it still fails, close channels on old fnn, remove store, then restart."
2789
+ );
2790
+ console.error(" 4) If backup exists, restore it to roll back.");
2791
+ }
2792
+ process.exit(1);
2793
+ }
2794
+ return { checked: true, migrationCheck };
2795
+ }
2796
+
2686
2797
  // src/lib/node-runtime-daemon.ts
2687
2798
  import { spawnSync } from "child_process";
2688
- import { existsSync as existsSync8 } from "fs";
2799
+ import { existsSync as existsSync9 } from "fs";
2689
2800
  function getCustomBinaryState(binaryPath) {
2690
- const exists = existsSync8(binaryPath);
2801
+ const exists = existsSync9(binaryPath);
2691
2802
  if (!exists) {
2692
2803
  return { path: binaryPath, ready: false, version: "unknown" };
2693
2804
  }
@@ -2777,52 +2888,6 @@ function stopRuntimeDaemonFromNode(params) {
2777
2888
  );
2778
2889
  }
2779
2890
 
2780
- // src/lib/node-start.ts
2781
- import { spawn } from "child_process";
2782
- import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
2783
- import { join as join5 } from "path";
2784
- import {
2785
- ensureFiberBinary,
2786
- getDefaultBinaryPath,
2787
- ProcessManager
2788
- } from "@fiber-pay/node";
2789
- import { startRuntimeService } from "@fiber-pay/runtime";
2790
- import { createKeyManager } from "@fiber-pay/sdk";
2791
-
2792
- // src/lib/bootnode.ts
2793
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
2794
- import { parse as parseYaml } from "yaml";
2795
- function extractBootnodeAddrs(configFilePath) {
2796
- if (!existsSync9(configFilePath)) return [];
2797
- try {
2798
- const content = readFileSync5(configFilePath, "utf-8");
2799
- const doc = parseYaml(content);
2800
- const addrs = doc?.fiber?.bootnode_addrs;
2801
- if (!Array.isArray(addrs)) return [];
2802
- return addrs.filter((a) => typeof a === "string" && a.startsWith("/ip"));
2803
- } catch {
2804
- return [];
2805
- }
2806
- }
2807
- async function autoConnectBootnodes(rpc, bootnodes) {
2808
- if (bootnodes.length === 0) return;
2809
- console.log(`\u{1F517} Connecting to ${bootnodes.length} bootnode(s)...`);
2810
- for (const addr of bootnodes) {
2811
- const shortId = addr.match(/\/p2p\/(.+)$/)?.[1]?.slice(0, 12) || addr.slice(-12);
2812
- try {
2813
- await rpc.connectPeer({ address: addr });
2814
- console.log(` \u2705 Connected to ${shortId}...`);
2815
- } catch (err) {
2816
- const msg = err instanceof Error ? err.message : String(err);
2817
- if (msg.toLowerCase().includes("already")) {
2818
- console.log(` \u2705 Already connected to ${shortId}...`);
2819
- } else {
2820
- console.error(` \u26A0\uFE0F Failed to connect to ${shortId}...: ${msg}`);
2821
- }
2822
- }
2823
- }
2824
- }
2825
-
2826
2891
  // src/lib/node-start.ts
2827
2892
  async function runNodeStartCommand(config, options) {
2828
2893
  const json = Boolean(options.json);
@@ -2951,6 +3016,19 @@ async function runNodeStartCommand(config, options) {
2951
3016
  console.log(`\u{1F9E9} Binary: ${binaryPath}`);
2952
3017
  console.log(`\u{1F9E9} Version: ${binaryVersion}`);
2953
3018
  }
3019
+ const guardResult = await runMigrationGuard({ dataDir: config.dataDir, binaryPath, json });
3020
+ if (guardResult.checked) {
3021
+ emitStage("migration_check", "ok", {
3022
+ storePath: `${config.dataDir}/store`,
3023
+ needed: false
3024
+ });
3025
+ } else {
3026
+ emitStage("migration_check", "ok", {
3027
+ storePath: `${config.dataDir}/store`,
3028
+ skipped: true,
3029
+ reason: guardResult.skippedReason
3030
+ });
3031
+ }
2954
3032
  const nodeConfig = {
2955
3033
  binaryPath,
2956
3034
  dataDir: config.dataDir,
@@ -3721,67 +3799,334 @@ async function runNodeReadyCommand(config, options) {
3721
3799
  }
3722
3800
  }
3723
3801
 
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 });
3802
+ // src/lib/node-stop.ts
3803
+ async function runNodeStopCommand(config, options) {
3804
+ const json = Boolean(options.json);
3805
+ const runtimeMeta = readRuntimeMeta(config.dataDir);
3806
+ const runtimePid = readRuntimePid(config.dataDir);
3807
+ if (runtimeMeta?.daemon && runtimePid && isProcessRunning(runtimePid)) {
3808
+ stopRuntimeDaemonFromNode({ dataDir: config.dataDir, rpcUrl: config.rpcUrl });
3809
+ }
3810
+ removeRuntimeFiles(config.dataDir);
3811
+ const pid = readPidFile(config.dataDir);
3812
+ if (!pid) {
3813
+ if (json) {
3814
+ printJsonError({
3815
+ code: "NODE_NOT_RUNNING",
3816
+ message: "No PID file found. Node may not be running.",
3817
+ recoverable: true,
3818
+ suggestion: "Run `fiber-pay node start` first if you intend to stop a node."
3819
+ });
3820
+ } else {
3821
+ console.log("\u274C No PID file found. Node may not be running.");
3736
3822
  }
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);
3823
+ process.exit(1);
3824
+ }
3825
+ if (!isProcessRunning(pid)) {
3826
+ if (json) {
3827
+ printJsonError({
3828
+ code: "NODE_NOT_RUNNING",
3829
+ message: `Process ${pid} is not running. Cleaning up PID file.`,
3830
+ recoverable: true,
3831
+ suggestion: "Start the node again if needed; stale PID has been cleaned.",
3832
+ details: { pid, stalePidFileCleaned: true }
3833
+ });
3834
+ } else {
3835
+ console.log(`\u274C Process ${pid} is not running. Cleaning up PID file.`);
3751
3836
  }
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.`);
3837
+ removePidFile(config.dataDir);
3838
+ process.exit(1);
3839
+ }
3840
+ if (!json) {
3841
+ console.log(`\u{1F6D1} Stopping node (PID: ${pid})...`);
3842
+ }
3843
+ process.kill(pid, "SIGTERM");
3844
+ let attempts = 0;
3845
+ while (isProcessRunning(pid) && attempts < 30) {
3846
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
3847
+ attempts++;
3848
+ }
3849
+ if (isProcessRunning(pid)) {
3850
+ process.kill(pid, "SIGKILL");
3851
+ }
3852
+ removePidFile(config.dataDir);
3853
+ if (json) {
3854
+ printJsonSuccess({ pid, stopped: true });
3855
+ } else {
3856
+ console.log("\u2705 Node stopped.");
3857
+ }
3858
+ }
3859
+
3860
+ // src/lib/node-upgrade.ts
3861
+ import { BinaryManager as BinaryManager2, MigrationManager as MigrationManager2 } from "@fiber-pay/node";
3862
+ async function runNodeUpgradeCommand(config, options) {
3863
+ const json = Boolean(options.json);
3864
+ const installDir = `${config.dataDir}/bin`;
3865
+ const binaryManager = new BinaryManager2(installDir);
3866
+ const pid = readPidFile(config.dataDir);
3867
+ if (pid && isProcessRunning(pid)) {
3868
+ const msg = "The Fiber node is currently running. Stop it before upgrading.";
3869
+ if (json) {
3870
+ printJsonError({
3871
+ code: "NODE_RUNNING",
3872
+ message: msg,
3873
+ recoverable: true,
3874
+ suggestion: "Run `fiber-pay node stop` first, then retry the upgrade."
3875
+ });
3876
+ } else {
3877
+ console.error(`\u274C ${msg}`);
3878
+ console.log(" Run: fiber-pay node stop");
3879
+ }
3880
+ process.exit(1);
3881
+ }
3882
+ let targetTag;
3883
+ if (options.version) {
3884
+ targetTag = binaryManager.normalizeTag(options.version);
3885
+ } else {
3886
+ if (!json) console.log("\u{1F50D} Resolving latest Fiber release...");
3887
+ targetTag = await binaryManager.getLatestTag();
3888
+ }
3889
+ if (!json) console.log(`\u{1F4E6} Target version: ${targetTag}`);
3890
+ const currentInfo = await binaryManager.getBinaryInfo();
3891
+ const targetVersion = targetTag.startsWith("v") ? targetTag.slice(1) : targetTag;
3892
+ const storePath = MigrationManager2.resolveStorePath(config.dataDir);
3893
+ const migrateBinaryPath = binaryManager.getMigrateBinaryPath();
3894
+ let migrationCheck = null;
3895
+ const storeExists = MigrationManager2.storeExists(config.dataDir);
3896
+ if (!json && storeExists) {
3897
+ console.log("\u{1F4C2} Existing store detected.");
3898
+ }
3899
+ if (currentInfo.ready && currentInfo.version === targetVersion && !options.forceMigrate) {
3900
+ if (storeExists) {
3901
+ migrationCheck = await runMigrationAndReport({
3902
+ migrateBinaryPath,
3903
+ storePath,
3904
+ json,
3905
+ checkOnly: Boolean(options.checkOnly),
3906
+ targetVersion,
3907
+ backup: options.backup !== false,
3908
+ forceMigrateAttempt: false
3909
+ });
3910
+ }
3911
+ const msg = migrationCheck ? `Already installed ${targetTag}. Store compatibility checked.` : `Already installed ${targetTag}. Use --force-migrate to run migration flow anyway.`;
3912
+ if (json) {
3913
+ printJsonSuccess({
3914
+ action: "none",
3915
+ currentVersion: currentInfo.version,
3916
+ targetVersion,
3917
+ message: msg,
3918
+ migration: migrationCheck
3919
+ });
3920
+ } else {
3921
+ console.log(`\u2705 ${msg}`);
3922
+ }
3923
+ return;
3924
+ }
3925
+ const versionMatches = currentInfo.ready && currentInfo.version === targetVersion;
3926
+ const shouldDownload = !versionMatches;
3927
+ if (!json && currentInfo.ready) {
3928
+ console.log(` Current version: v${currentInfo.version}`);
3929
+ }
3930
+ if (shouldDownload) {
3931
+ if (!json && storeExists) {
3932
+ console.log("\u{1F4C2} Existing store detected, will check migration after download.");
3933
+ }
3934
+ if (!json) console.log("\u2B07\uFE0F Downloading new binary...");
3935
+ const showProgress2 = (progress) => {
3936
+ if (!json) {
3937
+ const percent = progress.percent !== void 0 ? ` (${progress.percent}%)` : "";
3938
+ process.stdout.write(`\r [${progress.phase}]${percent} ${progress.message}`.padEnd(80));
3939
+ if (progress.phase === "installing") console.log();
3763
3940
  }
3764
- removePidFile(config.dataDir);
3765
- process.exit(1);
3941
+ };
3942
+ await binaryManager.download({
3943
+ version: targetTag,
3944
+ force: true,
3945
+ onProgress: showProgress2
3946
+ });
3947
+ } else if (!json && options.forceMigrate) {
3948
+ console.log("\u23ED\uFE0F Skipping binary download: target version is already installed.");
3949
+ console.log("\u{1F501} --force-migrate enabled: attempting migration flow on existing binaries.");
3950
+ }
3951
+ if (storeExists) {
3952
+ migrationCheck = await runMigrationAndReport({
3953
+ migrateBinaryPath,
3954
+ storePath,
3955
+ json,
3956
+ checkOnly: Boolean(options.checkOnly),
3957
+ targetVersion,
3958
+ backup: options.backup !== false,
3959
+ forceMigrateAttempt: Boolean(options.forceMigrate)
3960
+ });
3961
+ }
3962
+ const newInfo = await binaryManager.getBinaryInfo();
3963
+ if (json) {
3964
+ printJsonSuccess({
3965
+ action: "upgraded",
3966
+ previousVersion: currentInfo.ready ? currentInfo.version : null,
3967
+ currentVersion: newInfo.version,
3968
+ binaryPath: newInfo.path,
3969
+ migrateBinaryPath,
3970
+ migration: migrationCheck
3971
+ });
3972
+ } else {
3973
+ console.log("\n\u2705 Upgrade complete!");
3974
+ console.log(` Version: v${newInfo.version}`);
3975
+ console.log(` Binary: ${newInfo.path}`);
3976
+ console.log("\n Start the node with: fiber-pay node start");
3977
+ }
3978
+ }
3979
+ async function runMigrationAndReport(opts) {
3980
+ const {
3981
+ migrateBinaryPath,
3982
+ storePath,
3983
+ json,
3984
+ checkOnly,
3985
+ targetVersion,
3986
+ backup,
3987
+ forceMigrateAttempt
3988
+ } = opts;
3989
+ let migrationManager;
3990
+ try {
3991
+ migrationManager = new MigrationManager2(migrateBinaryPath);
3992
+ } catch (err) {
3993
+ const msg = err instanceof Error ? err.message : "fnn-migrate binary not available";
3994
+ if (json) {
3995
+ printJsonError({
3996
+ code: "MIGRATION_TOOL_MISSING",
3997
+ message: msg,
3998
+ recoverable: true,
3999
+ suggestion: "Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
4000
+ });
4001
+ } else {
4002
+ console.error(`
4003
+ \u26A0\uFE0F ${msg}`);
4004
+ console.log(
4005
+ " Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
4006
+ );
3766
4007
  }
3767
- if (!json) {
3768
- console.log(`\u{1F6D1} Stopping node (PID: ${pid})...`);
4008
+ process.exit(1);
4009
+ }
4010
+ if (!json) console.log("\u{1F50D} Checking store compatibility...");
4011
+ let migrationCheck;
4012
+ try {
4013
+ migrationCheck = await migrationManager.check(storePath);
4014
+ } catch (checkErr) {
4015
+ const msg = checkErr instanceof Error ? checkErr.message : String(checkErr);
4016
+ if (json) {
4017
+ printJsonError({
4018
+ code: "MIGRATION_TOOL_MISSING",
4019
+ message: `Migration check failed: ${msg}`,
4020
+ recoverable: true,
4021
+ suggestion: "Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
4022
+ });
4023
+ } else {
4024
+ console.error(`
4025
+ \u26A0\uFE0F Migration check failed: ${msg}`);
4026
+ console.log(
4027
+ " Run `fiber-pay node upgrade` to reinstall binaries, then retry `fiber-pay node upgrade --force-migrate`."
4028
+ );
3769
4029
  }
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++;
4030
+ process.exit(1);
4031
+ }
4032
+ if (checkOnly) {
4033
+ const normalizedCheck = normalizeMigrationCheck(migrationCheck);
4034
+ if (json) {
4035
+ printJsonSuccess({
4036
+ action: "check-only",
4037
+ targetVersion,
4038
+ migration: normalizedCheck
4039
+ });
4040
+ } else {
4041
+ console.log(`
4042
+ \u{1F4CB} Migration status: ${normalizedCheck.message}`);
3775
4043
  }
3776
- if (isProcessRunning(pid)) {
3777
- process.kill(pid, "SIGKILL");
4044
+ process.exit(0);
4045
+ }
4046
+ if (!migrationCheck.needed) {
4047
+ if (!json) console.log(" Store is compatible, no migration needed.");
4048
+ return normalizeMigrationCheck(migrationCheck);
4049
+ }
4050
+ if (!migrationCheck.valid && !forceMigrateAttempt) {
4051
+ const normalizedMessage = replaceRawMigrateHint(migrationCheck.message);
4052
+ if (json) {
4053
+ printJsonError({
4054
+ code: "MIGRATION_INCOMPATIBLE",
4055
+ message: normalizedMessage,
4056
+ recoverable: false,
4057
+ 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.`,
4058
+ details: {
4059
+ storePath,
4060
+ migrationCheck: {
4061
+ ...migrationCheck,
4062
+ message: normalizedMessage
4063
+ }
4064
+ }
4065
+ });
4066
+ } else {
4067
+ console.error("\n\u274C Store migration is not possible automatically.");
4068
+ console.log(normalizedMessage);
4069
+ console.log(` 1) Back up store directory: ${storePath}`);
4070
+ console.log(" 2) Try: fiber-pay node upgrade --force-migrate");
4071
+ console.log(
4072
+ " 3) If it still fails, close channels on old fnn, remove store, then restart."
4073
+ );
4074
+ console.log(" 4) If migration created a backup, you can roll back by restoring it.");
3778
4075
  }
3779
- removePidFile(config.dataDir);
4076
+ process.exit(1);
4077
+ }
4078
+ if (!migrationCheck.valid && !json) {
4079
+ console.log("\u26A0\uFE0F Store check reported incompatibility, but --force-migrate is set.");
4080
+ console.log(" Attempting migration anyway with backup enabled (unless --no-backup).");
4081
+ }
4082
+ if (!json) console.log("\u{1F504} Running database migration...");
4083
+ const result = await migrationManager.migrate({
4084
+ storePath,
4085
+ backup,
4086
+ force: forceMigrateAttempt
4087
+ });
4088
+ if (!result.success) {
3780
4089
  if (json) {
3781
- printJsonSuccess({ pid, stopped: true });
4090
+ printJsonError({
4091
+ code: "MIGRATION_FAILED",
4092
+ message: result.message,
4093
+ recoverable: !!result.backupPath,
4094
+ 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.",
4095
+ details: { output: result.output, backupPath: result.backupPath }
4096
+ });
3782
4097
  } else {
3783
- console.log("\u2705 Node stopped.");
4098
+ console.error("\n\u274C Migration failed.");
4099
+ console.log(result.message);
3784
4100
  }
4101
+ process.exit(1);
4102
+ }
4103
+ if (!json) {
4104
+ console.log(`\u2705 ${result.message}`);
4105
+ if (result.backupPath) {
4106
+ console.log(` Backup: ${result.backupPath}`);
4107
+ }
4108
+ }
4109
+ try {
4110
+ const postCheck = await migrationManager.check(storePath);
4111
+ return normalizeMigrationCheck(postCheck);
4112
+ } catch (err) {
4113
+ if (!json) {
4114
+ const message = err instanceof Error ? err.message : String(err);
4115
+ console.error("\u26A0\uFE0F Post-migration check failed; final migration status may be stale.");
4116
+ console.error(` ${message}`);
4117
+ }
4118
+ return normalizeMigrationCheck(migrationCheck);
4119
+ }
4120
+ }
4121
+
4122
+ // src/commands/node.ts
4123
+ function createNodeCommand(config) {
4124
+ const node = new Command8("node").description("Node management");
4125
+ 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) => {
4126
+ await runNodeStartCommand(config, options);
4127
+ });
4128
+ node.command("stop").option("--json").action(async (options) => {
4129
+ await runNodeStopCommand(config, options);
3785
4130
  });
3786
4131
  node.command("status").option("--json").action(async (options) => {
3787
4132
  await runNodeStatusCommand(config, options);
@@ -3812,6 +4157,12 @@ function createNodeCommand(config) {
3812
4157
  printNodeInfoHuman(output);
3813
4158
  }
3814
4159
  });
4160
+ node.command("upgrade").description("Upgrade the Fiber node binary and migrate the database if needed").option("--version <version>", "Target Fiber version (default: latest)").option("--no-backup", "Skip creating a store backup before migration").option("--check-only", "Only check if migration is needed, do not migrate").option(
4161
+ "--force-migrate",
4162
+ "Force migration attempt even when compatibility check reports incompatible data"
4163
+ ).option("--json").action(async (options) => {
4164
+ await runNodeUpgradeCommand(config, options);
4165
+ });
3815
4166
  return node;
3816
4167
  }
3817
4168
 
@@ -4575,8 +4926,8 @@ function createRuntimeCommand(config) {
4575
4926
  import { Command as Command12 } from "commander";
4576
4927
 
4577
4928
  // src/lib/build-info.ts
4578
- var CLI_VERSION = "0.1.0-rc.3";
4579
- var CLI_COMMIT = "6f39077caade6d3a307db28b8d3e9cfdee7eaf99";
4929
+ var CLI_VERSION = "0.1.0-rc.4";
4930
+ var CLI_COMMIT = "20dc82d290856d411382a4ec30f912b913b4f956";
4580
4931
 
4581
4932
  // src/commands/version.ts
4582
4933
  function createVersionCommand() {
@@ -4594,6 +4945,55 @@ function createVersionCommand() {
4594
4945
  });
4595
4946
  }
4596
4947
 
4948
+ // src/lib/argv.ts
4949
+ var GLOBAL_OPTIONS_WITH_VALUE = /* @__PURE__ */ new Set([
4950
+ "--profile",
4951
+ "--data-dir",
4952
+ "--rpc-url",
4953
+ "--network",
4954
+ "--key-password",
4955
+ "--binary-path"
4956
+ ]);
4957
+ function isOptionToken(token) {
4958
+ return token.startsWith("-") && token !== "-";
4959
+ }
4960
+ function hasInlineValue(token) {
4961
+ return token.includes("=");
4962
+ }
4963
+ function getFirstPositional(argv) {
4964
+ for (let index = 2; index < argv.length; index++) {
4965
+ const token = argv[index];
4966
+ if (token === "--") {
4967
+ return argv[index + 1];
4968
+ }
4969
+ if (!isOptionToken(token)) {
4970
+ return token;
4971
+ }
4972
+ if (GLOBAL_OPTIONS_WITH_VALUE.has(token) && !hasInlineValue(token)) {
4973
+ const next = argv[index + 1];
4974
+ if (next && !isOptionToken(next)) {
4975
+ index++;
4976
+ }
4977
+ }
4978
+ }
4979
+ return void 0;
4980
+ }
4981
+ function hasTopLevelVersionFlag(argv) {
4982
+ for (let index = 2; index < argv.length; index++) {
4983
+ const token = argv[index];
4984
+ if (token === "--") {
4985
+ return false;
4986
+ }
4987
+ if (token === "--version" || token === "-v") {
4988
+ return true;
4989
+ }
4990
+ }
4991
+ return false;
4992
+ }
4993
+ function isTopLevelVersionRequest(argv) {
4994
+ return !getFirstPositional(argv) && hasTopLevelVersionFlag(argv);
4995
+ }
4996
+
4597
4997
  // src/index.ts
4598
4998
  function shouldOutputJson() {
4599
4999
  return process.argv.includes("--json");
@@ -4695,10 +5095,15 @@ function printFatal(error) {
4695
5095
  }
4696
5096
  }
4697
5097
  async function main() {
4698
- applyGlobalOverrides(process.argv);
5098
+ const argv = process.argv;
5099
+ if (isTopLevelVersionRequest(argv)) {
5100
+ console.log(`${CLI_VERSION} (${CLI_COMMIT})`);
5101
+ return;
5102
+ }
5103
+ applyGlobalOverrides(argv);
4699
5104
  const config = getEffectiveConfig(explicitFlags).config;
4700
5105
  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();
5106
+ program.name("fiber-pay").description("AI Agent Payment SDK for CKB Lightning Network").option("--profile <name>", "Use profile at ~/.fiber-pay/profiles/<name>").option("--data-dir <path>", "Override data directory for all commands").option("--rpc-url <url>", "Override RPC URL for all commands").option("--network <network>", "Override network for all commands (testnet|mainnet)").option("--key-password <password>", "Override key password for all commands").option("--binary-path <path>", "Override fiber binary path for all commands").showHelpAfterError().showSuggestionAfterError();
4702
5107
  program.exitOverride();
4703
5108
  program.configureOutput({
4704
5109
  writeOut: (str) => process.stdout.write(str),
@@ -4720,7 +5125,7 @@ async function main() {
4720
5125
  program.addCommand(createConfigCommand(config));
4721
5126
  program.addCommand(createRuntimeCommand(config));
4722
5127
  program.addCommand(createVersionCommand());
4723
- await program.parseAsync(process.argv);
5128
+ await program.parseAsync(argv);
4724
5129
  }
4725
5130
  main().catch((error) => {
4726
5131
  const commanderCode = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;