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.
- package/dist/cli.js +179 -17
- 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.
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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) =>
|
|
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 = [
|
|
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 &&
|
|
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 =
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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;
|