baton-host 0.1.6 → 0.1.8
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/bin/baton-host.js +239 -22
- package/package.json +5 -5
package/bin/baton-host.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const { spawn, spawnSync } = require("node:child_process");
|
|
4
4
|
const fs = require("node:fs");
|
|
5
|
+
const net = require("node:net");
|
|
5
6
|
const os = require("node:os");
|
|
6
7
|
const path = require("node:path");
|
|
8
|
+
const { createInterface } = require("node:readline/promises");
|
|
7
9
|
|
|
8
10
|
const mapping = {
|
|
9
11
|
"darwin-arm64": "baton-host-darwin-arm64",
|
|
@@ -17,6 +19,7 @@ const INSTALL_ROOT = path.join(os.homedir(), ".baton-host");
|
|
|
17
19
|
const INSTALL_BIN_DIR = path.join(INSTALL_ROOT, "bin");
|
|
18
20
|
const INSTALL_LOG_DIR = path.join(INSTALL_ROOT, "logs");
|
|
19
21
|
const ENV_FILE_PATH = path.join(INSTALL_ROOT, ".env");
|
|
22
|
+
const BRIDGE_INFO_PATH = path.join(INSTALL_ROOT, "bridge-info.json");
|
|
20
23
|
const WRAPPER_PATH = path.join(INSTALL_ROOT, "run.sh");
|
|
21
24
|
const INSTALL_META_PATH = path.join(INSTALL_ROOT, "install.json");
|
|
22
25
|
|
|
@@ -109,9 +112,9 @@ function ensureDefaultEnvFile() {
|
|
|
109
112
|
|
|
110
113
|
const defaultContent = [
|
|
111
114
|
"# Baton Host 服务环境变量",
|
|
112
|
-
"BATON_BRIDGE_MODE=
|
|
115
|
+
"BATON_BRIDGE_MODE=cloudflare",
|
|
113
116
|
"BRIDGE_LOG_PROFILE=core",
|
|
114
|
-
"PORT=
|
|
117
|
+
"PORT=9977",
|
|
115
118
|
""
|
|
116
119
|
].join("\n");
|
|
117
120
|
|
|
@@ -141,6 +144,183 @@ function readEnvFile() {
|
|
|
141
144
|
return result;
|
|
142
145
|
}
|
|
143
146
|
|
|
147
|
+
function writeEnvFile(envVars) {
|
|
148
|
+
const existingContent = fs.existsSync(ENV_FILE_PATH)
|
|
149
|
+
? fs.readFileSync(ENV_FILE_PATH, "utf8")
|
|
150
|
+
: "# Baton Host 服务环境变量\n";
|
|
151
|
+
const lines = existingContent.split(/\r?\n/);
|
|
152
|
+
const keysToWrite = new Set(Object.keys(envVars));
|
|
153
|
+
const seenKeys = new Set();
|
|
154
|
+
const nextLines = lines.map((rawLine) => {
|
|
155
|
+
const trimmed = rawLine.trim();
|
|
156
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
157
|
+
return rawLine;
|
|
158
|
+
}
|
|
159
|
+
const separatorIndex = rawLine.indexOf("=");
|
|
160
|
+
if (separatorIndex <= 0) {
|
|
161
|
+
return rawLine;
|
|
162
|
+
}
|
|
163
|
+
const key = rawLine.slice(0, separatorIndex).trim();
|
|
164
|
+
if (!keysToWrite.has(key)) {
|
|
165
|
+
return rawLine;
|
|
166
|
+
}
|
|
167
|
+
seenKeys.add(key);
|
|
168
|
+
return `${key}=${envVars[key]}`;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
for (const key of keysToWrite) {
|
|
172
|
+
if (!seenKeys.has(key)) {
|
|
173
|
+
nextLines.push(`${key}=${envVars[key]}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const content = `${nextLines.filter((line, index, array) => !(index === array.length - 1 && line === "")).join("\n")}\n`;
|
|
178
|
+
fs.writeFileSync(ENV_FILE_PATH, content, "utf8");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isInteractiveTerminal() {
|
|
182
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function pickAvailablePort() {
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
const server = net.createServer();
|
|
188
|
+
server.unref();
|
|
189
|
+
server.on("error", reject);
|
|
190
|
+
server.listen(0, "127.0.0.1", () => {
|
|
191
|
+
const address = server.address();
|
|
192
|
+
if (!address || typeof address === "string") {
|
|
193
|
+
server.close(() => reject(new Error("随机端口分配失败")));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
resolve(address.port);
|
|
197
|
+
server.close();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isValidPort(value) {
|
|
203
|
+
const port = Number(value);
|
|
204
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function ensurePortAvailable(port) {
|
|
208
|
+
await new Promise((resolve, reject) => {
|
|
209
|
+
const server = net.createServer();
|
|
210
|
+
server.unref();
|
|
211
|
+
server.once("error", reject);
|
|
212
|
+
server.listen(port, "127.0.0.1", () => {
|
|
213
|
+
server.close((error) => {
|
|
214
|
+
if (error) {
|
|
215
|
+
reject(error);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
resolve();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
return port;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function promptBridgeModeForInstall() {
|
|
226
|
+
if (!isInteractiveTerminal()) {
|
|
227
|
+
return "cloudflare";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const rl = createInterface({
|
|
231
|
+
input: process.stdin,
|
|
232
|
+
output: process.stdout
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
while (true) {
|
|
237
|
+
console.log("请选择连接方式:");
|
|
238
|
+
console.log("1) 局域网(同一 Wi‑Fi 下用)");
|
|
239
|
+
console.log("2) Cloudflare(推荐,直接回车也选这个)");
|
|
240
|
+
console.log("3) Tailscale");
|
|
241
|
+
const answer = (await rl.question("输入 1 / 2 / 3,直接回车默认 Cloudflare: ")).trim();
|
|
242
|
+
if (!answer || answer === "2") {
|
|
243
|
+
return "cloudflare";
|
|
244
|
+
}
|
|
245
|
+
if (answer === "1") {
|
|
246
|
+
return "lan";
|
|
247
|
+
}
|
|
248
|
+
if (answer === "3") {
|
|
249
|
+
return "tailscale";
|
|
250
|
+
}
|
|
251
|
+
console.log("⚠️ 请输入 1、2、3,或直接回车。\n");
|
|
252
|
+
}
|
|
253
|
+
} finally {
|
|
254
|
+
rl.close();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function promptPortForInstall() {
|
|
259
|
+
const randomPort = await pickAvailablePort();
|
|
260
|
+
if (!isInteractiveTerminal()) {
|
|
261
|
+
return randomPort;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const rl = createInterface({
|
|
265
|
+
input: process.stdin,
|
|
266
|
+
output: process.stdout
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
while (true) {
|
|
271
|
+
console.log("\n请选择端口:");
|
|
272
|
+
console.log(`直接回车:自动分配随机端口(推荐,当前候选 ${randomPort})`);
|
|
273
|
+
console.log("输入端口号:使用自定义端口");
|
|
274
|
+
const answer = (await rl.question("输入端口号,或直接回车使用随机端口: ")).trim();
|
|
275
|
+
if (!answer) {
|
|
276
|
+
return randomPort;
|
|
277
|
+
}
|
|
278
|
+
if (isValidPort(answer)) {
|
|
279
|
+
try {
|
|
280
|
+
return await ensurePortAvailable(Number(answer));
|
|
281
|
+
} catch {
|
|
282
|
+
console.log("⚠️ 这个端口当前不可用,请换一个。\n");
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
console.log("⚠️ 请输入 1-65535 的有效端口号,或直接回车。\n");
|
|
287
|
+
}
|
|
288
|
+
} finally {
|
|
289
|
+
rl.close();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function printBridgeInfoFromCache() {
|
|
294
|
+
if (!fs.existsSync(BRIDGE_INFO_PATH)) {
|
|
295
|
+
throw new Error("未找到已保存的连接信息,请稍后重试");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const envVars = readEnvFile();
|
|
299
|
+
const env = {
|
|
300
|
+
...process.env,
|
|
301
|
+
...envVars,
|
|
302
|
+
BATON_CLOUDFLARED_BIN: path.join(INSTALL_BIN_DIR, "cloudflared")
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
runInheritedCommand(path.join(INSTALL_BIN_DIR, "baton-host"), ["--print-bridge-info"], { env });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function waitForBridgeInfo(timeoutMs = 15000) {
|
|
309
|
+
const start = Date.now();
|
|
310
|
+
while (Date.now() - start < timeoutMs) {
|
|
311
|
+
if (fs.existsSync(BRIDGE_INFO_PATH)) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
315
|
+
}
|
|
316
|
+
throw new Error("等待连接信息生成超时,请稍后执行 service restart 再试");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function runBridgePrintCommand() {
|
|
320
|
+
await waitForBridgeInfo();
|
|
321
|
+
printBridgeInfoFromCache();
|
|
322
|
+
}
|
|
323
|
+
|
|
144
324
|
function buildWrapperScript() {
|
|
145
325
|
return `#!/bin/sh
|
|
146
326
|
set -eu
|
|
@@ -278,6 +458,26 @@ function runCommand(command, args, options = {}) {
|
|
|
278
458
|
return result;
|
|
279
459
|
}
|
|
280
460
|
|
|
461
|
+
function runInheritedCommand(command, args, options = {}) {
|
|
462
|
+
const result = spawnSync(command, args, {
|
|
463
|
+
stdio: "inherit",
|
|
464
|
+
env: options.env || process.env
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (result.error) {
|
|
468
|
+
if (options.allowFailure) {
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
throw result.error;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (!options.allowFailure && result.status !== 0) {
|
|
475
|
+
throw new Error(`${command} ${args.join(" ")} 失败,退出码 ${result.status}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
|
|
281
481
|
function installLinuxService() {
|
|
282
482
|
const unitPath = getLinuxUnitPath();
|
|
283
483
|
ensureDir(path.dirname(unitPath));
|
|
@@ -442,37 +642,50 @@ function ensureSupportedServicePlatform() {
|
|
|
442
642
|
}
|
|
443
643
|
}
|
|
444
644
|
|
|
445
|
-
function handleServiceInstall() {
|
|
645
|
+
async function handleServiceInstall(options = {}) {
|
|
446
646
|
ensureSupportedServicePlatform();
|
|
447
647
|
|
|
648
|
+
const { preserveConfig = false } = options;
|
|
448
649
|
const previousVersion = readInstallMeta()?.version || null;
|
|
449
650
|
const { binaryPath, cloudflaredPath, version } = resolveBundledAssets();
|
|
450
651
|
|
|
451
652
|
ensureDir(INSTALL_ROOT);
|
|
452
653
|
ensureDir(INSTALL_BIN_DIR);
|
|
453
654
|
ensureDir(INSTALL_LOG_DIR);
|
|
655
|
+
ensureDefaultEnvFile();
|
|
656
|
+
|
|
657
|
+
if (preserveConfig) {
|
|
658
|
+
const envVars = readEnvFile();
|
|
659
|
+
if (!envVars.BATON_BRIDGE_MODE || !envVars.PORT) {
|
|
660
|
+
fail("现有配置不完整,无法直接 upgrade。请先执行 npx baton-host@latest service install");
|
|
661
|
+
}
|
|
662
|
+
} else {
|
|
663
|
+
const mode = await promptBridgeModeForInstall();
|
|
664
|
+
const port = await promptPortForInstall();
|
|
665
|
+
writeEnvFile({
|
|
666
|
+
BATON_BRIDGE_MODE: mode,
|
|
667
|
+
PORT: String(port)
|
|
668
|
+
});
|
|
669
|
+
}
|
|
454
670
|
|
|
455
671
|
copyExecutable(binaryPath, path.join(INSTALL_BIN_DIR, "baton-host"));
|
|
456
672
|
copyExecutable(cloudflaredPath, path.join(INSTALL_BIN_DIR, "cloudflared"));
|
|
457
|
-
ensureDefaultEnvFile();
|
|
458
673
|
writeWrapperScript();
|
|
459
674
|
writeInstallMeta(version);
|
|
460
675
|
|
|
461
676
|
const serviceFile = process.platform === "linux" ? installLinuxService() : installMacService();
|
|
462
|
-
const action =
|
|
463
|
-
? "升级并重启"
|
|
464
|
-
: previousVersion
|
|
465
|
-
? "覆盖并重启"
|
|
466
|
-
: "安装并启动";
|
|
677
|
+
const action = preserveConfig
|
|
678
|
+
? (previousVersion && previousVersion !== version ? "升级并重启" : "覆盖并重启")
|
|
679
|
+
: (previousVersion ? "覆盖并重启" : "安装并启动");
|
|
467
680
|
|
|
468
681
|
console.log(`✅ Baton Host 服务已${action}`);
|
|
469
682
|
console.log(`Version: ${version}`);
|
|
470
|
-
console.log(`Binary: ${path.join(INSTALL_BIN_DIR, "baton-host")}`);
|
|
471
683
|
console.log(`Env file: ${ENV_FILE_PATH}`);
|
|
472
684
|
console.log(`Service file: ${serviceFile}`);
|
|
685
|
+
await runBridgePrintCommand();
|
|
473
686
|
}
|
|
474
687
|
|
|
475
|
-
function handleServiceRestart() {
|
|
688
|
+
async function handleServiceRestart() {
|
|
476
689
|
ensureSupportedServicePlatform();
|
|
477
690
|
if (process.platform === "linux") {
|
|
478
691
|
restartLinuxService();
|
|
@@ -480,6 +693,7 @@ function handleServiceRestart() {
|
|
|
480
693
|
restartMacService();
|
|
481
694
|
}
|
|
482
695
|
console.log("✅ Baton Host 服务已重启");
|
|
696
|
+
await runBridgePrintCommand();
|
|
483
697
|
}
|
|
484
698
|
|
|
485
699
|
function handleServiceUninstall() {
|
|
@@ -507,19 +721,24 @@ function printServiceUsage() {
|
|
|
507
721
|
console.log(" BATON_BRIDGE_MODE=lan|tailscale|cloudflare");
|
|
508
722
|
console.log(" PORT=9966");
|
|
509
723
|
console.log("");
|
|
510
|
-
console.log("说明: install
|
|
724
|
+
console.log("说明: install 会进入模式/端口向导;upgrade 复用现有配置并重启服务");
|
|
511
725
|
}
|
|
512
726
|
|
|
513
|
-
function handleServiceCommand(args) {
|
|
727
|
+
async function handleServiceCommand(args) {
|
|
514
728
|
const command = args[0] || "install";
|
|
515
729
|
|
|
516
|
-
if (command === "install"
|
|
517
|
-
handleServiceInstall();
|
|
730
|
+
if (command === "install") {
|
|
731
|
+
await handleServiceInstall();
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (command === "upgrade") {
|
|
736
|
+
await handleServiceInstall({ preserveConfig: true });
|
|
518
737
|
return;
|
|
519
738
|
}
|
|
520
739
|
|
|
521
740
|
if (command === "restart") {
|
|
522
|
-
handleServiceRestart();
|
|
741
|
+
await handleServiceRestart();
|
|
523
742
|
return;
|
|
524
743
|
}
|
|
525
744
|
|
|
@@ -561,17 +780,15 @@ function spawnHostBinary(args) {
|
|
|
561
780
|
});
|
|
562
781
|
}
|
|
563
782
|
|
|
564
|
-
function main() {
|
|
783
|
+
async function main() {
|
|
565
784
|
const args = process.argv.slice(2);
|
|
566
785
|
if (args[0] === "service") {
|
|
567
|
-
handleServiceCommand(args.slice(1));
|
|
786
|
+
await handleServiceCommand(args.slice(1));
|
|
568
787
|
return;
|
|
569
788
|
}
|
|
570
789
|
spawnHostBinary(args);
|
|
571
790
|
}
|
|
572
791
|
|
|
573
|
-
|
|
574
|
-
main();
|
|
575
|
-
} catch (error) {
|
|
792
|
+
void main().catch((error) => {
|
|
576
793
|
fail(error instanceof Error ? error.message : String(error));
|
|
577
|
-
}
|
|
794
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baton-host",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Baton Bridge Host CLI(二进制分发入口)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"bin"
|
|
11
11
|
],
|
|
12
12
|
"optionalDependencies": {
|
|
13
|
-
"baton-host-darwin-arm64": "0.1.
|
|
14
|
-
"baton-host-darwin-x64": "0.1.
|
|
15
|
-
"baton-host-linux-arm64": "0.1.
|
|
16
|
-
"baton-host-linux-x64": "0.1.
|
|
13
|
+
"baton-host-darwin-arm64": "0.1.8",
|
|
14
|
+
"baton-host-darwin-x64": "0.1.8",
|
|
15
|
+
"baton-host-linux-arm64": "0.1.8",
|
|
16
|
+
"baton-host-linux-x64": "0.1.8"
|
|
17
17
|
}
|
|
18
18
|
}
|