lianrouter 2.1.0 → 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 +179 -17
  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.0",
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,9 +2607,143 @@ 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
2739
+ init_version_file();
2740
+ async function resolveGatewayVersion(installRoot, launcherVersion) {
2741
+ const record = await readInstalledVersion(installRoot);
2742
+ return record?.version ?? launcherVersion;
2743
+ }
2608
2744
  async function runUpdateSequence(deps) {
2609
- const info = await deps.checkForUpdate({ currentVersion: deps.currentVersion });
2745
+ const gatewayVersion = await resolveGatewayVersion(deps.installRoot, deps.launcherVersion);
2746
+ const info = await deps.checkForUpdate({ currentVersion: gatewayVersion });
2610
2747
  deps.publish({
2611
2748
  current: info.current,
2612
2749
  latest: info.latest,
@@ -2625,7 +2762,7 @@ async function runUpdateSequence(deps) {
2625
2762
  try {
2626
2763
  await deps.runUpdate({
2627
2764
  installRoot: deps.installRoot,
2628
- currentVersion: deps.currentVersion,
2765
+ currentVersion: gatewayVersion,
2629
2766
  onProgress: (progress) => {
2630
2767
  deps.publishProgress({
2631
2768
  phase: progress.phase,
@@ -2661,11 +2798,11 @@ async function runUpdateSequence(deps) {
2661
2798
 
2662
2799
  // cli/src/tray-launcher.ts
2663
2800
  init_paths();
2664
- import { existsSync as existsSync2 } from "fs";
2801
+ import { existsSync as existsSync3 } from "fs";
2665
2802
  import { readFile as readFile2, rm as rm8, writeFile as writeFile3 } from "fs/promises";
2666
2803
  import { join as join7 } from "path";
2667
2804
  function resolveTrayEntry(options) {
2668
- return trayEntryCandidates(options).find((candidate) => existsSync2(candidate));
2805
+ return trayEntryCandidates(options).find((candidate) => existsSync3(candidate));
2669
2806
  }
2670
2807
  function trayEntryCandidates(options) {
2671
2808
  const bundleDir = options.bundleDir ?? import.meta.dir;
@@ -2799,6 +2936,7 @@ Commands:
2799
2936
  update Check for and install an update
2800
2937
  tray Run the gateway in the background with a tray icon
2801
2938
  tui Open the terminal UI
2939
+ init Write a starter config file into the install root
2802
2940
 
2803
2941
  Options:
2804
2942
  --port <n> Gateway port (default ${DEFAULT_PORT}, or $PORT)
@@ -2812,7 +2950,15 @@ Options:
2812
2950
  Environment:
2813
2951
  LIANROUTER_HOME Override the install/state root
2814
2952
  PORT Default gateway port`;
2815
- 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
+ ];
2816
2962
  function parseArgs(argv) {
2817
2963
  let port;
2818
2964
  let host = "localhost";
@@ -2900,7 +3046,7 @@ function readPortEnv() {
2900
3046
  }
2901
3047
  function resolveRuntime() {
2902
3048
  const repoRoot = resolveDevRepoRoot();
2903
- if (repoRoot !== undefined && existsSync3(join8(repoRoot, "src", "app.ts"))) {
3049
+ if (repoRoot !== undefined && existsSync4(join8(repoRoot, "src", "app.ts"))) {
2904
3050
  return {
2905
3051
  installRoot: resolveInstallRoot(),
2906
3052
  cwd: repoRoot,
@@ -2912,7 +3058,7 @@ function resolveRuntime() {
2912
3058
  return { installRoot, cwd: installRoot, command: undefined, devRepoRoot: undefined };
2913
3059
  }
2914
3060
  async function ensureInstalledGateway(installRoot) {
2915
- const present = existsSync3(resolveBinaryPath(installRoot));
3061
+ const present = existsSync4(resolveBinaryPath(installRoot));
2916
3062
  if (present) {
2917
3063
  await ensureGatewayInstalled({ installRoot, currentVersion: CLI_VERSION });
2918
3064
  return;
@@ -2946,6 +3092,17 @@ async function run(argv) {
2946
3092
  return 0;
2947
3093
  }
2948
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
+ }
2949
3106
  if (runtime.command === undefined && options.command !== "status" && options.command !== "stop") {
2950
3107
  await ensureInstalledGateway(runtime.installRoot);
2951
3108
  }
@@ -2954,7 +3111,10 @@ async function run(argv) {
2954
3111
  installRoot: runtime.installRoot,
2955
3112
  logFile: resolveLogFile(runtime.installRoot),
2956
3113
  cwd: runtime.cwd,
2957
- env: { LIANROUTER_LAUNCHER_HOME: runtime.installRoot },
3114
+ env: {
3115
+ LIANROUTER_LAUNCHER_HOME: runtime.installRoot,
3116
+ ...gatewayConfig?.env ?? {}
3117
+ },
2958
3118
  ...runtime.command === undefined ? {} : { command: runtime.command }
2959
3119
  });
2960
3120
  switch (options.command) {
@@ -3041,7 +3201,7 @@ async function openSession(supervisor, runtime, host) {
3041
3201
  try {
3042
3202
  const engine = await Promise.resolve().then(() => (init_engine(), {}));
3043
3203
  await runUpdateSequence({
3044
- currentVersion: CLI_VERSION,
3204
+ launcherVersion: CLI_VERSION,
3045
3205
  installRoot: runtime.installRoot,
3046
3206
  checkForUpdate: (options) => checkForUpdate(options),
3047
3207
  runUpdate: (options) => runUpdate(options),
@@ -3107,7 +3267,7 @@ async function runInteractive(supervisor, options, runtime) {
3107
3267
  if (options.headless) {
3108
3268
  return await blockUntilTrayExit(supervisor, session, runtime);
3109
3269
  }
3110
- const update = await readUpdateInfo();
3270
+ const update = await readUpdateInfo(runtime.installRoot);
3111
3271
  const action = await promptMenu({
3112
3272
  version: CLI_VERSION,
3113
3273
  port: supervisor.status.port,
@@ -3158,7 +3318,7 @@ async function runUpdateWithProgress(supervisor, session, runtime) {
3158
3318
  session.control.setUpdateRunning(true);
3159
3319
  try {
3160
3320
  await runUpdateSequence({
3161
- currentVersion: CLI_VERSION,
3321
+ launcherVersion: CLI_VERSION,
3162
3322
  installRoot: runtime.installRoot,
3163
3323
  checkForUpdate: (options) => checkForUpdate(options),
3164
3324
  runUpdate: (options) => runUpdate(options),
@@ -3266,7 +3426,7 @@ async function runUpdateFlow(supervisor, runtime) {
3266
3426
  const wasRunning = await supervisor.adoptExisting();
3267
3427
  try {
3268
3428
  await runUpdateSequence({
3269
- currentVersion: CLI_VERSION,
3429
+ launcherVersion: CLI_VERSION,
3270
3430
  installRoot: runtime.installRoot,
3271
3431
  checkForUpdate: (options) => checkForUpdate(options),
3272
3432
  runUpdate: (options) => runUpdate(options),
@@ -3294,10 +3454,12 @@ async function runUpdateFlow(supervisor, runtime) {
3294
3454
  console.log("Update complete.");
3295
3455
  return 0;
3296
3456
  }
3297
- async function readUpdateInfo() {
3457
+ async function readUpdateInfo(installRoot) {
3298
3458
  try {
3299
3459
  const engine = await Promise.resolve().then(() => (init_engine(), {}));
3300
- const info = await checkForUpdate({ currentVersion: CLI_VERSION });
3460
+ const info = await checkForUpdate({
3461
+ currentVersion: await resolveGatewayVersion(installRoot, CLI_VERSION)
3462
+ });
3301
3463
  return { current: info.current, latest: info.latest, available: info.available };
3302
3464
  } catch {
3303
3465
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lianrouter",
3
- "version": "2.1.0",
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": {