lianrouter 2.1.1 → 2.2.0

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.
Files changed (2) hide show
  1. package/dist/cli.js +163 -9
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ var package_default;
17
17
  var init_package = __esm(() => {
18
18
  package_default = {
19
19
  name: "lianrouter",
20
- version: "2.1.1",
20
+ version: "2.2.0",
21
21
  description: "LianRouter launcher: install, run and control the gateway from one command",
22
22
  type: "module",
23
23
  bin: {
@@ -868,6 +868,9 @@ function resolveBinaryPath(installRoot = resolveInstallRoot(), platform = proces
868
868
  function resolveLogFile(installRoot = resolveInstallRoot()) {
869
869
  return join5(installRoot, LOGS_DIR, LOG_FILE_NAME);
870
870
  }
871
+ function resolveConfigFile(installRoot = resolveInstallRoot()) {
872
+ return join5(installRoot, CONFIG_FILE_NAME);
873
+ }
871
874
  function resolvePidFile(installRoot = resolveInstallRoot()) {
872
875
  return join5(resolveStateDir(installRoot), PID_FILE_NAME);
873
876
  }
@@ -930,7 +933,7 @@ function consoleUrl(port, host = "localhost") {
930
933
  function healthUrl(port) {
931
934
  return `http://127.0.0.1:${port}/health`;
932
935
  }
933
- var HOME_ENV = "LIANROUTER_HOME", APP_DIR_NAME = "LianRouter", LOGS_DIR = "logs", CACHE_DIR = "cache", RUN_DIR = "run", UPDATE_CACHE_DIR = "updates", LOG_FILE_NAME = "lianrouter.log", PID_FILE_NAME = "lianrouter.pid", CONTROL_FILE_NAME = "control.json", TRAY_PID_FILE_NAME = "tray.pid", DEFAULT_PORT = 12800;
936
+ var HOME_ENV = "LIANROUTER_HOME", APP_DIR_NAME = "LianRouter", LOGS_DIR = "logs", CACHE_DIR = "cache", RUN_DIR = "run", UPDATE_CACHE_DIR = "updates", CONFIG_FILE_NAME = ".env", LOG_FILE_NAME = "lianrouter.log", PID_FILE_NAME = "lianrouter.pid", CONTROL_FILE_NAME = "control.json", TRAY_PID_FILE_NAME = "tray.pid", DEFAULT_PORT = 12800;
934
937
  var init_paths = () => {};
935
938
 
936
939
  // cli/tui/terminal.ts
@@ -1557,7 +1560,7 @@ var init_app = __esm(async () => {
1557
1560
 
1558
1561
  // cli/src/launcher.ts
1559
1562
  init_version();
1560
- import { existsSync as existsSync3 } from "fs";
1563
+ import { existsSync as existsSync4 } from "fs";
1561
1564
  import { join as join8 } from "path";
1562
1565
 
1563
1566
  // cli/src/bootstrap.ts
@@ -2604,6 +2607,134 @@ class GatewaySupervisor {
2604
2607
  }
2605
2608
  }
2606
2609
 
2610
+ // cli/src/install-config.ts
2611
+ init_paths();
2612
+ import { randomBytes } from "crypto";
2613
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
2614
+ var ALWAYS_REQUIRED = ["DATABASE_URL", "LIANROUTER_ENCRYPTION_KEY"];
2615
+ function parseEnvFile(text) {
2616
+ const values = {};
2617
+ for (const rawLine of text.split(/\r?\n/)) {
2618
+ const line = rawLine.trim();
2619
+ if (line === "" || line.startsWith("#")) {
2620
+ continue;
2621
+ }
2622
+ const separator = line.indexOf("=");
2623
+ if (separator <= 0) {
2624
+ continue;
2625
+ }
2626
+ const key = line.slice(0, separator).trim();
2627
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
2628
+ continue;
2629
+ }
2630
+ let value = line.slice(separator + 1).trim();
2631
+ const quote = value[0];
2632
+ if (quote === '"' || quote === "'") {
2633
+ if (value.length >= 2 && value.endsWith(quote)) {
2634
+ value = value.slice(1, -1);
2635
+ }
2636
+ }
2637
+ values[key] = value;
2638
+ }
2639
+ return values;
2640
+ }
2641
+ function resolveGatewayConfig(installRoot, shellEnv = process.env) {
2642
+ const path = resolveConfigFile(installRoot);
2643
+ const present = existsSync2(path);
2644
+ const fileValues = present ? parseEnvFile(readFileSync2(path, "utf8")) : {};
2645
+ const env = {};
2646
+ for (const [key, value] of Object.entries(fileValues)) {
2647
+ if (shellEnv[key] !== undefined && shellEnv[key].trim() !== "") {
2648
+ continue;
2649
+ }
2650
+ if (value.trim() === "") {
2651
+ continue;
2652
+ }
2653
+ env[key] = value;
2654
+ }
2655
+ const effective = (name) => {
2656
+ const fromShell = shellEnv[name];
2657
+ if (fromShell !== undefined && fromShell.trim() !== "") {
2658
+ return fromShell.trim();
2659
+ }
2660
+ const fromFile = fileValues[name];
2661
+ return fromFile !== undefined && fromFile.trim() !== "" ? fromFile.trim() : undefined;
2662
+ };
2663
+ const required = [...ALWAYS_REQUIRED];
2664
+ if ((effective("REDIS_MODE") ?? "normal") === "normal") {
2665
+ required.push("REDIS_URL");
2666
+ }
2667
+ const missing = required.filter((name) => effective(name) === undefined);
2668
+ return { path, present, env, missing };
2669
+ }
2670
+ function describeMissingConfig(config) {
2671
+ const location = config.present ? ` config file: ${config.path}` : ` config file: ${config.path} (not found)`;
2672
+ const examples = {
2673
+ DATABASE_URL: "postgres://user:password@localhost:5432/lianrouter",
2674
+ LIANROUTER_ENCRYPTION_KEY: "<base64 or 64-character hex that decodes to exactly 32 bytes>",
2675
+ REDIS_URL: "redis://localhost:6379"
2676
+ };
2677
+ const lines = [
2678
+ "the gateway is not configured, so it cannot start",
2679
+ location,
2680
+ ` missing: ${config.missing.join(", ")}`,
2681
+ "",
2682
+ "add these lines to that file:",
2683
+ ...config.missing.map((name) => ` ${name}=${examples[name] ?? "<value>"}`),
2684
+ "",
2685
+ "or set them in your shell before starting; a shell value wins over the file.",
2686
+ `run \`lianrouter init\` to write a starter file at ${config.path}`
2687
+ ];
2688
+ if (config.missing.includes("REDIS_URL")) {
2689
+ lines.push("to run a single gateway process without Redis, set REDIS_MODE=single_instance_local instead of REDIS_URL");
2690
+ }
2691
+ return lines.join(`
2692
+ `);
2693
+ }
2694
+ function assertGatewayConfigured(config) {
2695
+ if (config.missing.length > 0) {
2696
+ throw new Error(describeMissingConfig(config));
2697
+ }
2698
+ }
2699
+ function starterConfigText(encryptionKey) {
2700
+ return `# LianRouter gateway configuration.
2701
+ # The launcher reads this file and passes its values to the gateway process.
2702
+ # A variable already set in your shell wins over the value written here.
2703
+ # Generated by \`lianrouter init\`.
2704
+
2705
+ # PostgreSQL connection string. Required.
2706
+ # Replace the placeholder below and remove the leading "# ". The gateway never
2707
+ # infers a Docker or Laragon connection.
2708
+ # DATABASE_URL=postgres://user:password@localhost:5432/lianrouter
2709
+
2710
+ # 256-bit key for encrypting provider credentials and hashing API keys.
2711
+ # Required. Base64 (44 characters) or hex (64 characters); it must decode to
2712
+ # exactly 32 bytes. Keep it secret and keep it stable: a new key cannot decrypt
2713
+ # credentials encrypted with the old one.
2714
+ LIANROUTER_ENCRYPTION_KEY=${encryptionKey}
2715
+
2716
+ # Redis coordination. REDIS_URL is required in the default mode; set
2717
+ # REDIS_MODE=single_instance_local to run a single gateway process without
2718
+ # Redis at all.
2719
+ REDIS_MODE=normal
2720
+ # REDIS_URL=redis://localhost:6379
2721
+ `;
2722
+ }
2723
+ function writeStarterConfig(installRoot) {
2724
+ const path = resolveConfigFile(installRoot);
2725
+ mkdirSync2(installRoot, { recursive: true });
2726
+ const encryptionKey = randomBytes(32).toString("base64");
2727
+ try {
2728
+ writeFileSync(path, starterConfigText(encryptionKey), { encoding: "utf8", flag: "wx" });
2729
+ } catch (error) {
2730
+ if (error.code === "EEXIST") {
2731
+ throw new Error(`refusing to overwrite the existing config file at ${path}`);
2732
+ }
2733
+ throw error;
2734
+ }
2735
+ return path;
2736
+ }
2737
+
2607
2738
  // cli/src/update-flow.ts
2608
2739
  init_version_file();
2609
2740
  async function resolveGatewayVersion(installRoot, launcherVersion) {
@@ -2667,11 +2798,11 @@ async function runUpdateSequence(deps) {
2667
2798
 
2668
2799
  // cli/src/tray-launcher.ts
2669
2800
  init_paths();
2670
- import { existsSync as existsSync2 } from "fs";
2801
+ import { existsSync as existsSync3 } from "fs";
2671
2802
  import { readFile as readFile2, rm as rm8, writeFile as writeFile3 } from "fs/promises";
2672
2803
  import { join as join7 } from "path";
2673
2804
  function resolveTrayEntry(options) {
2674
- return trayEntryCandidates(options).find((candidate) => existsSync2(candidate));
2805
+ return trayEntryCandidates(options).find((candidate) => existsSync3(candidate));
2675
2806
  }
2676
2807
  function trayEntryCandidates(options) {
2677
2808
  const bundleDir = options.bundleDir ?? import.meta.dir;
@@ -2805,6 +2936,7 @@ Commands:
2805
2936
  update Check for and install an update
2806
2937
  tray Run the gateway in the background with a tray icon
2807
2938
  tui Open the terminal UI
2939
+ init Write a starter config file into the install root
2808
2940
 
2809
2941
  Options:
2810
2942
  --port <n> Gateway port (default ${DEFAULT_PORT}, or $PORT)
@@ -2818,7 +2950,15 @@ Options:
2818
2950
  Environment:
2819
2951
  LIANROUTER_HOME Override the install/state root
2820
2952
  PORT Default gateway port`;
2821
- var KNOWN_COMMANDS = ["start", "stop", "status", "update", "tray", "tui"];
2953
+ var KNOWN_COMMANDS = [
2954
+ "start",
2955
+ "stop",
2956
+ "status",
2957
+ "update",
2958
+ "tray",
2959
+ "tui",
2960
+ "init"
2961
+ ];
2822
2962
  function parseArgs(argv) {
2823
2963
  let port;
2824
2964
  let host = "localhost";
@@ -2906,7 +3046,7 @@ function readPortEnv() {
2906
3046
  }
2907
3047
  function resolveRuntime() {
2908
3048
  const repoRoot = resolveDevRepoRoot();
2909
- if (repoRoot !== undefined && existsSync3(join8(repoRoot, "src", "app.ts"))) {
3049
+ if (repoRoot !== undefined && existsSync4(join8(repoRoot, "src", "app.ts"))) {
2910
3050
  return {
2911
3051
  installRoot: resolveInstallRoot(),
2912
3052
  cwd: repoRoot,
@@ -2918,7 +3058,7 @@ function resolveRuntime() {
2918
3058
  return { installRoot, cwd: installRoot, command: undefined, devRepoRoot: undefined };
2919
3059
  }
2920
3060
  async function ensureInstalledGateway(installRoot) {
2921
- const present = existsSync3(resolveBinaryPath(installRoot));
3061
+ const present = existsSync4(resolveBinaryPath(installRoot));
2922
3062
  if (present) {
2923
3063
  await ensureGatewayInstalled({ installRoot, currentVersion: CLI_VERSION });
2924
3064
  return;
@@ -2952,6 +3092,17 @@ async function run(argv) {
2952
3092
  return 0;
2953
3093
  }
2954
3094
  const runtime = resolveRuntime();
3095
+ if (options.command === "init") {
3096
+ const path = writeStarterConfig(runtime.installRoot);
3097
+ console.log(`Wrote ${path}`);
3098
+ console.log("Set DATABASE_URL in that file (uncomment the line), then run lianrouter.");
3099
+ return 0;
3100
+ }
3101
+ const startsGateway = options.command === undefined || options.command === "start" || options.command === "tray" || options.command === "tui";
3102
+ const gatewayConfig = runtime.command === undefined && startsGateway ? resolveGatewayConfig(runtime.installRoot) : undefined;
3103
+ if (gatewayConfig !== undefined) {
3104
+ assertGatewayConfigured(gatewayConfig);
3105
+ }
2955
3106
  if (runtime.command === undefined && options.command !== "status" && options.command !== "stop") {
2956
3107
  await ensureInstalledGateway(runtime.installRoot);
2957
3108
  }
@@ -2960,7 +3111,10 @@ async function run(argv) {
2960
3111
  installRoot: runtime.installRoot,
2961
3112
  logFile: resolveLogFile(runtime.installRoot),
2962
3113
  cwd: runtime.cwd,
2963
- env: { LIANROUTER_LAUNCHER_HOME: runtime.installRoot },
3114
+ env: {
3115
+ LIANROUTER_LAUNCHER_HOME: runtime.installRoot,
3116
+ ...gatewayConfig?.env ?? {}
3117
+ },
2964
3118
  ...runtime.command === undefined ? {} : { command: runtime.command }
2965
3119
  });
2966
3120
  switch (options.command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lianrouter",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "LianRouter launcher: install, run and control the gateway from one command",
5
5
  "type": "module",
6
6
  "bin": {