lianrouter 2.1.1 → 2.2.1

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 +198 -20
  2. package/package.json +43 -43
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.1",
21
21
  description: "LianRouter launcher: install, run and control the gateway from one command",
22
22
  type: "module",
23
23
  bin: {
@@ -482,7 +482,7 @@ class GitHubReleaseSource {
482
482
  };
483
483
  });
484
484
  return {
485
- version: newest.tag_name,
485
+ version: normalizeVersion(newest.tag_name),
486
486
  notesUrl: newest.html_url,
487
487
  ...newest.published_at.length > 0 ? { publishedAt: newest.published_at } : {},
488
488
  assets
@@ -507,7 +507,7 @@ var init_github_source = __esm(() => {
507
507
  });
508
508
 
509
509
  // cli/update/install.ts
510
- import { cp, mkdir as mkdir2, rename, rm as rm3, stat } from "fs/promises";
510
+ import { cp, mkdir as mkdir2, readdir as readdir2, rename, rm as rm3, stat } from "fs/promises";
511
511
  import { dirname, join as join2, relative, resolve as resolve2, sep as sep2 } from "path";
512
512
  async function pathExists(target) {
513
513
  try {
@@ -534,6 +534,26 @@ async function assertNotDevCheckout(installRoot) {
534
534
  throw new Error(`refusing to self-update ${root}: it is a source checkout (.git and src/main.ts present). ` + "Build locally with `bun run build` instead of installing a release over the working tree.");
535
535
  }
536
536
  }
537
+ async function sweepBackups(root, remove = rm3) {
538
+ const backupDir = join2(resolve2(root), BACKUP_DIR_NAME);
539
+ let entries;
540
+ try {
541
+ entries = await readdir2(backupDir);
542
+ } catch {
543
+ return;
544
+ }
545
+ let failed = false;
546
+ for (const entry of entries) {
547
+ await remove(join2(backupDir, entry), { recursive: true, force: true }).catch(() => {
548
+ failed = true;
549
+ });
550
+ }
551
+ if (!failed) {
552
+ await remove(backupDir, { recursive: true, force: true }).catch(() => {
553
+ return;
554
+ });
555
+ }
556
+ }
537
557
  async function installAtomically(options) {
538
558
  const root = resolve2(options.installRoot);
539
559
  const staged = resolve2(options.stagedDir);
@@ -590,7 +610,7 @@ async function installAtomically(options) {
590
610
  throw error;
591
611
  }
592
612
  onProgress?.({ percent: 100, message: "install complete" });
593
- await rm3(backupRoot, { recursive: true, force: true });
613
+ await sweepBackups(root, options.remove ?? rm3);
594
614
  }
595
615
  var BACKUP_DIR_NAME = ".backup";
596
616
  var init_install = () => {};
@@ -618,15 +638,19 @@ async function readInstalledVersion(installRoot) {
618
638
  if (typeof version !== "string" || version.trim().length === 0) {
619
639
  return;
620
640
  }
641
+ const normalized = normalizeVersion(version);
642
+ if (normalized.length === 0) {
643
+ return;
644
+ }
621
645
  return {
622
- version: version.trim(),
646
+ version: normalized,
623
647
  installedAt: typeof installedAt === "string" ? installedAt : "",
624
648
  assetName: typeof assetName === "string" ? assetName : ""
625
649
  };
626
650
  }
627
651
  async function writeInstalledVersion(installRoot, record) {
628
652
  const payload = {
629
- version: record.version,
653
+ version: normalizeVersion(record.version),
630
654
  installedAt: record.installedAt,
631
655
  assetName: record.assetName
632
656
  };
@@ -646,7 +670,7 @@ var init_version_file = __esm(() => {
646
670
  });
647
671
 
648
672
  // cli/update/engine.ts
649
- import { mkdtemp, readdir as readdir2, rm as rm4 } from "fs/promises";
673
+ import { mkdtemp, readdir as readdir3, rm as rm4 } from "fs/promises";
650
674
  import { tmpdir } from "os";
651
675
  import { join as join4 } from "path";
652
676
  function withinSlice(start, end, percent) {
@@ -700,7 +724,7 @@ async function resolveInstallCandidate(opts) {
700
724
  };
701
725
  }
702
726
  async function payloadEntries(stagedDir) {
703
- const entries = await readdir2(stagedDir, { withFileTypes: true });
727
+ const entries = await readdir3(stagedDir, { withFileTypes: true });
704
728
  return entries.map((entry) => entry.name);
705
729
  }
706
730
  async function runUpdate(opts) {
@@ -868,6 +892,9 @@ function resolveBinaryPath(installRoot = resolveInstallRoot(), platform = proces
868
892
  function resolveLogFile(installRoot = resolveInstallRoot()) {
869
893
  return join5(installRoot, LOGS_DIR, LOG_FILE_NAME);
870
894
  }
895
+ function resolveConfigFile(installRoot = resolveInstallRoot()) {
896
+ return join5(installRoot, CONFIG_FILE_NAME);
897
+ }
871
898
  function resolvePidFile(installRoot = resolveInstallRoot()) {
872
899
  return join5(resolveStateDir(installRoot), PID_FILE_NAME);
873
900
  }
@@ -930,7 +957,7 @@ function consoleUrl(port, host = "localhost") {
930
957
  function healthUrl(port) {
931
958
  return `http://127.0.0.1:${port}/health`;
932
959
  }
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;
960
+ 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
961
  var init_paths = () => {};
935
962
 
936
963
  // cli/tui/terminal.ts
@@ -1557,7 +1584,7 @@ var init_app = __esm(async () => {
1557
1584
 
1558
1585
  // cli/src/launcher.ts
1559
1586
  init_version();
1560
- import { existsSync as existsSync3 } from "fs";
1587
+ import { existsSync as existsSync4 } from "fs";
1561
1588
  import { join as join8 } from "path";
1562
1589
 
1563
1590
  // cli/src/bootstrap.ts
@@ -1569,7 +1596,7 @@ init_install();
1569
1596
  init_version_file();
1570
1597
  init_paths();
1571
1598
  import { statSync } from "fs";
1572
- import { mkdir as mkdir3, mkdtemp as mkdtemp2, readdir as readdir3, rm as rm5 } from "fs/promises";
1599
+ import { mkdir as mkdir3, mkdtemp as mkdtemp2, readdir as readdir4, rm as rm5 } from "fs/promises";
1573
1600
  import { tmpdir as tmpdir2 } from "os";
1574
1601
  import { join as join6 } from "path";
1575
1602
  var SLICE2 = {
@@ -1598,7 +1625,7 @@ function withinSlice2(start, end, percent) {
1598
1625
  return start + (end - start) * clamped / 100;
1599
1626
  }
1600
1627
  async function payloadEntries2(stagedDir) {
1601
- const entries = await readdir3(stagedDir, { withFileTypes: true });
1628
+ const entries = await readdir4(stagedDir, { withFileTypes: true });
1602
1629
  return entries.map((entry) => entry.name);
1603
1630
  }
1604
1631
  async function ensureGatewayInstalled(options) {
@@ -1987,7 +2014,7 @@ function buildMenuItems(input) {
1987
2014
  if (input.update !== undefined) {
1988
2015
  items.push({
1989
2016
  action: "update",
1990
- label: `Update to v${input.update.latest} (current: v${input.version})`
2017
+ label: `Update to v${input.update.latest} (current: v${input.update.current})`
1991
2018
  });
1992
2019
  }
1993
2020
  items.push({ action: "web", label: "Web UI (Open in Browser)" }, { action: "tui", label: "Terminal UI (Interactive CLI)" }, { action: "tray", label: "Hide to Tray (Background)" }, { action: "exit", label: "Exit" });
@@ -2604,6 +2631,134 @@ class GatewaySupervisor {
2604
2631
  }
2605
2632
  }
2606
2633
 
2634
+ // cli/src/install-config.ts
2635
+ init_paths();
2636
+ import { randomBytes } from "crypto";
2637
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
2638
+ var ALWAYS_REQUIRED = ["DATABASE_URL", "LIANROUTER_ENCRYPTION_KEY"];
2639
+ function parseEnvFile(text) {
2640
+ const values = {};
2641
+ for (const rawLine of text.split(/\r?\n/)) {
2642
+ const line = rawLine.trim();
2643
+ if (line === "" || line.startsWith("#")) {
2644
+ continue;
2645
+ }
2646
+ const separator = line.indexOf("=");
2647
+ if (separator <= 0) {
2648
+ continue;
2649
+ }
2650
+ const key = line.slice(0, separator).trim();
2651
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
2652
+ continue;
2653
+ }
2654
+ let value = line.slice(separator + 1).trim();
2655
+ const quote = value[0];
2656
+ if (quote === '"' || quote === "'") {
2657
+ if (value.length >= 2 && value.endsWith(quote)) {
2658
+ value = value.slice(1, -1);
2659
+ }
2660
+ }
2661
+ values[key] = value;
2662
+ }
2663
+ return values;
2664
+ }
2665
+ function resolveGatewayConfig(installRoot, shellEnv = process.env) {
2666
+ const path = resolveConfigFile(installRoot);
2667
+ const present = existsSync2(path);
2668
+ const fileValues = present ? parseEnvFile(readFileSync2(path, "utf8")) : {};
2669
+ const env = {};
2670
+ for (const [key, value] of Object.entries(fileValues)) {
2671
+ if (shellEnv[key] !== undefined && shellEnv[key].trim() !== "") {
2672
+ continue;
2673
+ }
2674
+ if (value.trim() === "") {
2675
+ continue;
2676
+ }
2677
+ env[key] = value;
2678
+ }
2679
+ const effective = (name) => {
2680
+ const fromShell = shellEnv[name];
2681
+ if (fromShell !== undefined && fromShell.trim() !== "") {
2682
+ return fromShell.trim();
2683
+ }
2684
+ const fromFile = fileValues[name];
2685
+ return fromFile !== undefined && fromFile.trim() !== "" ? fromFile.trim() : undefined;
2686
+ };
2687
+ const required = [...ALWAYS_REQUIRED];
2688
+ if ((effective("REDIS_MODE") ?? "normal") === "normal") {
2689
+ required.push("REDIS_URL");
2690
+ }
2691
+ const missing = required.filter((name) => effective(name) === undefined);
2692
+ return { path, present, env, missing };
2693
+ }
2694
+ function describeMissingConfig(config) {
2695
+ const location = config.present ? ` config file: ${config.path}` : ` config file: ${config.path} (not found)`;
2696
+ const examples = {
2697
+ DATABASE_URL: "postgres://user:password@localhost:5432/lianrouter",
2698
+ LIANROUTER_ENCRYPTION_KEY: "<base64 or 64-character hex that decodes to exactly 32 bytes>",
2699
+ REDIS_URL: "redis://localhost:6379"
2700
+ };
2701
+ const lines = [
2702
+ "the gateway is not configured, so it cannot start",
2703
+ location,
2704
+ ` missing: ${config.missing.join(", ")}`,
2705
+ "",
2706
+ "add these lines to that file:",
2707
+ ...config.missing.map((name) => ` ${name}=${examples[name] ?? "<value>"}`),
2708
+ "",
2709
+ "or set them in your shell before starting; a shell value wins over the file.",
2710
+ `run \`lianrouter init\` to write a starter file at ${config.path}`
2711
+ ];
2712
+ if (config.missing.includes("REDIS_URL")) {
2713
+ lines.push("to run a single gateway process without Redis, set REDIS_MODE=single_instance_local instead of REDIS_URL");
2714
+ }
2715
+ return lines.join(`
2716
+ `);
2717
+ }
2718
+ function assertGatewayConfigured(config) {
2719
+ if (config.missing.length > 0) {
2720
+ throw new Error(describeMissingConfig(config));
2721
+ }
2722
+ }
2723
+ function starterConfigText(encryptionKey) {
2724
+ return `# LianRouter gateway configuration.
2725
+ # The launcher reads this file and passes its values to the gateway process.
2726
+ # A variable already set in your shell wins over the value written here.
2727
+ # Generated by \`lianrouter init\`.
2728
+
2729
+ # PostgreSQL connection string. Required.
2730
+ # Replace the placeholder below and remove the leading "# ". The gateway never
2731
+ # infers a Docker or Laragon connection.
2732
+ # DATABASE_URL=postgres://user:password@localhost:5432/lianrouter
2733
+
2734
+ # 256-bit key for encrypting provider credentials and hashing API keys.
2735
+ # Required. Base64 (44 characters) or hex (64 characters); it must decode to
2736
+ # exactly 32 bytes. Keep it secret and keep it stable: a new key cannot decrypt
2737
+ # credentials encrypted with the old one.
2738
+ LIANROUTER_ENCRYPTION_KEY=${encryptionKey}
2739
+
2740
+ # Redis coordination. REDIS_URL is required in the default mode; set
2741
+ # REDIS_MODE=single_instance_local to run a single gateway process without
2742
+ # Redis at all.
2743
+ REDIS_MODE=normal
2744
+ # REDIS_URL=redis://localhost:6379
2745
+ `;
2746
+ }
2747
+ function writeStarterConfig(installRoot) {
2748
+ const path = resolveConfigFile(installRoot);
2749
+ mkdirSync2(installRoot, { recursive: true });
2750
+ const encryptionKey = randomBytes(32).toString("base64");
2751
+ try {
2752
+ writeFileSync(path, starterConfigText(encryptionKey), { encoding: "utf8", flag: "wx" });
2753
+ } catch (error) {
2754
+ if (error.code === "EEXIST") {
2755
+ throw new Error(`refusing to overwrite the existing config file at ${path}`);
2756
+ }
2757
+ throw error;
2758
+ }
2759
+ return path;
2760
+ }
2761
+
2607
2762
  // cli/src/update-flow.ts
2608
2763
  init_version_file();
2609
2764
  async function resolveGatewayVersion(installRoot, launcherVersion) {
@@ -2667,11 +2822,11 @@ async function runUpdateSequence(deps) {
2667
2822
 
2668
2823
  // cli/src/tray-launcher.ts
2669
2824
  init_paths();
2670
- import { existsSync as existsSync2 } from "fs";
2825
+ import { existsSync as existsSync3 } from "fs";
2671
2826
  import { readFile as readFile2, rm as rm8, writeFile as writeFile3 } from "fs/promises";
2672
2827
  import { join as join7 } from "path";
2673
2828
  function resolveTrayEntry(options) {
2674
- return trayEntryCandidates(options).find((candidate) => existsSync2(candidate));
2829
+ return trayEntryCandidates(options).find((candidate) => existsSync3(candidate));
2675
2830
  }
2676
2831
  function trayEntryCandidates(options) {
2677
2832
  const bundleDir = options.bundleDir ?? import.meta.dir;
@@ -2805,6 +2960,7 @@ Commands:
2805
2960
  update Check for and install an update
2806
2961
  tray Run the gateway in the background with a tray icon
2807
2962
  tui Open the terminal UI
2963
+ init Write a starter config file into the install root
2808
2964
 
2809
2965
  Options:
2810
2966
  --port <n> Gateway port (default ${DEFAULT_PORT}, or $PORT)
@@ -2818,7 +2974,15 @@ Options:
2818
2974
  Environment:
2819
2975
  LIANROUTER_HOME Override the install/state root
2820
2976
  PORT Default gateway port`;
2821
- var KNOWN_COMMANDS = ["start", "stop", "status", "update", "tray", "tui"];
2977
+ var KNOWN_COMMANDS = [
2978
+ "start",
2979
+ "stop",
2980
+ "status",
2981
+ "update",
2982
+ "tray",
2983
+ "tui",
2984
+ "init"
2985
+ ];
2822
2986
  function parseArgs(argv) {
2823
2987
  let port;
2824
2988
  let host = "localhost";
@@ -2906,7 +3070,7 @@ function readPortEnv() {
2906
3070
  }
2907
3071
  function resolveRuntime() {
2908
3072
  const repoRoot = resolveDevRepoRoot();
2909
- if (repoRoot !== undefined && existsSync3(join8(repoRoot, "src", "app.ts"))) {
3073
+ if (repoRoot !== undefined && existsSync4(join8(repoRoot, "src", "app.ts"))) {
2910
3074
  return {
2911
3075
  installRoot: resolveInstallRoot(),
2912
3076
  cwd: repoRoot,
@@ -2918,7 +3082,7 @@ function resolveRuntime() {
2918
3082
  return { installRoot, cwd: installRoot, command: undefined, devRepoRoot: undefined };
2919
3083
  }
2920
3084
  async function ensureInstalledGateway(installRoot) {
2921
- const present = existsSync3(resolveBinaryPath(installRoot));
3085
+ const present = existsSync4(resolveBinaryPath(installRoot));
2922
3086
  if (present) {
2923
3087
  await ensureGatewayInstalled({ installRoot, currentVersion: CLI_VERSION });
2924
3088
  return;
@@ -2952,6 +3116,17 @@ async function run(argv) {
2952
3116
  return 0;
2953
3117
  }
2954
3118
  const runtime = resolveRuntime();
3119
+ if (options.command === "init") {
3120
+ const path = writeStarterConfig(runtime.installRoot);
3121
+ console.log(`Wrote ${path}`);
3122
+ console.log("Set DATABASE_URL in that file (uncomment the line), then run lianrouter.");
3123
+ return 0;
3124
+ }
3125
+ const startsGateway = options.command === undefined || options.command === "start" || options.command === "tray" || options.command === "tui";
3126
+ const gatewayConfig = runtime.command === undefined && startsGateway ? resolveGatewayConfig(runtime.installRoot) : undefined;
3127
+ if (gatewayConfig !== undefined) {
3128
+ assertGatewayConfigured(gatewayConfig);
3129
+ }
2955
3130
  if (runtime.command === undefined && options.command !== "status" && options.command !== "stop") {
2956
3131
  await ensureInstalledGateway(runtime.installRoot);
2957
3132
  }
@@ -2960,7 +3135,10 @@ async function run(argv) {
2960
3135
  installRoot: runtime.installRoot,
2961
3136
  logFile: resolveLogFile(runtime.installRoot),
2962
3137
  cwd: runtime.cwd,
2963
- env: { LIANROUTER_LAUNCHER_HOME: runtime.installRoot },
3138
+ env: {
3139
+ LIANROUTER_LAUNCHER_HOME: runtime.installRoot,
3140
+ ...gatewayConfig?.env ?? {}
3141
+ },
2964
3142
  ...runtime.command === undefined ? {} : { command: runtime.command }
2965
3143
  });
2966
3144
  switch (options.command) {
@@ -3117,7 +3295,7 @@ async function runInteractive(supervisor, options, runtime) {
3117
3295
  const action = await promptMenu({
3118
3296
  version: CLI_VERSION,
3119
3297
  port: supervisor.status.port,
3120
- update: update !== null && update.available ? { latest: update.latest } : undefined
3298
+ update: update !== null && update.available ? { latest: update.latest, current: update.current } : undefined
3121
3299
  });
3122
3300
  return await dispatch(action, supervisor, session, options, runtime);
3123
3301
  }
package/package.json CHANGED
@@ -1,43 +1,43 @@
1
- {
2
- "name": "lianrouter",
3
- "version": "2.1.1",
4
- "description": "LianRouter launcher: install, run and control the gateway from one command",
5
- "type": "module",
6
- "bin": {
7
- "lianrouter": "./dist/cli.js"
8
- },
9
- "engines": {
10
- "bun": ">=1.4.2"
11
- },
12
- "files": [
13
- "dist",
14
- "README.md"
15
- ],
16
- "scripts": {
17
- "build": "bun run ../scripts/build-cli.ts",
18
- "typecheck": "tsc --noEmit -p .",
19
- "postinstall": "echo lianrouter: the gateway binary installs on first run, not during this install",
20
- "prepublishOnly": "bun run build"
21
- },
22
- "repository": {
23
- "type": "git",
24
- "url": "git+https://github.com/ReXooGen/Lian-Router.git",
25
- "directory": "cli"
26
- },
27
- "bugs": {
28
- "url": "https://github.com/ReXooGen/Lian-Router/issues"
29
- },
30
- "homepage": "https://github.com/ReXooGen/Lian-Router#readme",
31
- "publishConfig": {
32
- "access": "public"
33
- },
34
- "keywords": [
35
- "lianrouter",
36
- "ai-gateway",
37
- "openai-compatible",
38
- "llm-router",
39
- "self-hosted",
40
- "cli",
41
- "launcher"
42
- ]
43
- }
1
+ {
2
+ "name": "lianrouter",
3
+ "version": "2.2.1",
4
+ "description": "LianRouter launcher: install, run and control the gateway from one command",
5
+ "type": "module",
6
+ "bin": {
7
+ "lianrouter": "./dist/cli.js"
8
+ },
9
+ "engines": {
10
+ "bun": ">=1.4.2"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "bun run ../scripts/build-cli.ts",
18
+ "typecheck": "tsc --noEmit -p .",
19
+ "postinstall": "echo lianrouter: the gateway binary installs on first run, not during this install",
20
+ "prepublishOnly": "bun run build"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/ReXooGen/Lian-Router.git",
25
+ "directory": "cli"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/ReXooGen/Lian-Router/issues"
29
+ },
30
+ "homepage": "https://github.com/ReXooGen/Lian-Router#readme",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "keywords": [
35
+ "lianrouter",
36
+ "ai-gateway",
37
+ "openai-compatible",
38
+ "llm-router",
39
+ "self-hosted",
40
+ "cli",
41
+ "launcher"
42
+ ]
43
+ }