baton-host 0.1.6 → 0.1.7

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/bin/baton-host.js +248 -22
  2. 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=lan",
115
+ "BATON_BRIDGE_MODE=cloudflare",
113
116
  "BRIDGE_LOG_PROFILE=core",
114
- "PORT=9966",
117
+ "PORT=9977",
115
118
  ""
116
119
  ].join("\n");
117
120
 
@@ -141,6 +144,212 @@ 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
+ async function printBridgeInfoFromCache() {
294
+ if (!fs.existsSync(BRIDGE_INFO_PATH)) {
295
+ throw new Error("未找到已保存的连接信息,请稍后重试");
296
+ }
297
+
298
+ let cached;
299
+ try {
300
+ cached = JSON.parse(fs.readFileSync(BRIDGE_INFO_PATH, "utf8"));
301
+ } catch {
302
+ throw new Error("已保存的连接信息损坏,请重新执行 service restart");
303
+ }
304
+
305
+ if (!cached?.bridgeUrl || !cached?.displayLabel) {
306
+ throw new Error("已保存的连接信息不完整,请稍后重试");
307
+ }
308
+
309
+ const payload = JSON.stringify({ bridgeUrl: cached.bridgeUrl });
310
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
311
+ const qrcode = require("qrcode-terminal");
312
+
313
+ console.log("\n✅ Baton Bridge 已启动");
314
+ console.log(`模式: ${cached.displayLabel}`);
315
+ console.log(`主连接地址: ${cached.bridgeUrl}`);
316
+ if (cached.lanBridgeUrl && cached.lanBridgeUrl !== cached.bridgeUrl) {
317
+ console.log(`局域网地址: ${cached.lanBridgeUrl}`);
318
+ }
319
+ if (cached.note) {
320
+ console.log(`说明: ${cached.note}`);
321
+ }
322
+ console.log("\n请使用 Baton App 扫描下方二维码连接:\n");
323
+
324
+ await new Promise((resolve) => {
325
+ qrcode.generate(payload, { small: true }, (qrText) => {
326
+ for (const line of qrText.split("\n")) {
327
+ console.log(line);
328
+ }
329
+ resolve();
330
+ });
331
+ });
332
+
333
+ console.log("\n在 Baton App → 设置 → Bridge 服务器 → 扫码连接");
334
+ console.log("现在可以关闭终端,服务会继续在后台运行。\n");
335
+ }
336
+
337
+ async function waitForBridgeInfo(timeoutMs = 15000) {
338
+ const start = Date.now();
339
+ while (Date.now() - start < timeoutMs) {
340
+ if (fs.existsSync(BRIDGE_INFO_PATH)) {
341
+ return;
342
+ }
343
+ await new Promise((resolve) => setTimeout(resolve, 250));
344
+ }
345
+ throw new Error("等待连接信息生成超时,请稍后执行 service restart 再试");
346
+ }
347
+
348
+ async function runBridgePrintCommand() {
349
+ await waitForBridgeInfo();
350
+ await printBridgeInfoFromCache();
351
+ }
352
+
144
353
  function buildWrapperScript() {
145
354
  return `#!/bin/sh
146
355
  set -eu
@@ -442,37 +651,50 @@ function ensureSupportedServicePlatform() {
442
651
  }
443
652
  }
444
653
 
445
- function handleServiceInstall() {
654
+ async function handleServiceInstall(options = {}) {
446
655
  ensureSupportedServicePlatform();
447
656
 
657
+ const { preserveConfig = false } = options;
448
658
  const previousVersion = readInstallMeta()?.version || null;
449
659
  const { binaryPath, cloudflaredPath, version } = resolveBundledAssets();
450
660
 
451
661
  ensureDir(INSTALL_ROOT);
452
662
  ensureDir(INSTALL_BIN_DIR);
453
663
  ensureDir(INSTALL_LOG_DIR);
664
+ ensureDefaultEnvFile();
665
+
666
+ if (preserveConfig) {
667
+ const envVars = readEnvFile();
668
+ if (!envVars.BATON_BRIDGE_MODE || !envVars.PORT) {
669
+ fail("现有配置不完整,无法直接 upgrade。请先执行 npx baton-host@latest service install");
670
+ }
671
+ } else {
672
+ const mode = await promptBridgeModeForInstall();
673
+ const port = await promptPortForInstall();
674
+ writeEnvFile({
675
+ BATON_BRIDGE_MODE: mode,
676
+ PORT: String(port)
677
+ });
678
+ }
454
679
 
455
680
  copyExecutable(binaryPath, path.join(INSTALL_BIN_DIR, "baton-host"));
456
681
  copyExecutable(cloudflaredPath, path.join(INSTALL_BIN_DIR, "cloudflared"));
457
- ensureDefaultEnvFile();
458
682
  writeWrapperScript();
459
683
  writeInstallMeta(version);
460
684
 
461
685
  const serviceFile = process.platform === "linux" ? installLinuxService() : installMacService();
462
- const action = previousVersion && previousVersion !== version
463
- ? "升级并重启"
464
- : previousVersion
465
- ? "覆盖并重启"
466
- : "安装并启动";
686
+ const action = preserveConfig
687
+ ? (previousVersion && previousVersion !== version ? "升级并重启" : "覆盖并重启")
688
+ : (previousVersion ? "覆盖并重启" : "安装并启动");
467
689
 
468
690
  console.log(`✅ Baton Host 服务已${action}`);
469
691
  console.log(`Version: ${version}`);
470
- console.log(`Binary: ${path.join(INSTALL_BIN_DIR, "baton-host")}`);
471
692
  console.log(`Env file: ${ENV_FILE_PATH}`);
472
693
  console.log(`Service file: ${serviceFile}`);
694
+ await runBridgePrintCommand();
473
695
  }
474
696
 
475
- function handleServiceRestart() {
697
+ async function handleServiceRestart() {
476
698
  ensureSupportedServicePlatform();
477
699
  if (process.platform === "linux") {
478
700
  restartLinuxService();
@@ -480,6 +702,7 @@ function handleServiceRestart() {
480
702
  restartMacService();
481
703
  }
482
704
  console.log("✅ Baton Host 服务已重启");
705
+ await runBridgePrintCommand();
483
706
  }
484
707
 
485
708
  function handleServiceUninstall() {
@@ -507,19 +730,24 @@ function printServiceUsage() {
507
730
  console.log(" BATON_BRIDGE_MODE=lan|tailscale|cloudflare");
508
731
  console.log(" PORT=9966");
509
732
  console.log("");
510
- console.log("说明: install/upgrade 都会覆盖二进制并重启服务");
733
+ console.log("说明: install 会进入模式/端口向导;upgrade 复用现有配置并重启服务");
511
734
  }
512
735
 
513
- function handleServiceCommand(args) {
736
+ async function handleServiceCommand(args) {
514
737
  const command = args[0] || "install";
515
738
 
516
- if (command === "install" || command === "upgrade") {
517
- handleServiceInstall();
739
+ if (command === "install") {
740
+ await handleServiceInstall();
741
+ return;
742
+ }
743
+
744
+ if (command === "upgrade") {
745
+ await handleServiceInstall({ preserveConfig: true });
518
746
  return;
519
747
  }
520
748
 
521
749
  if (command === "restart") {
522
- handleServiceRestart();
750
+ await handleServiceRestart();
523
751
  return;
524
752
  }
525
753
 
@@ -561,17 +789,15 @@ function spawnHostBinary(args) {
561
789
  });
562
790
  }
563
791
 
564
- function main() {
792
+ async function main() {
565
793
  const args = process.argv.slice(2);
566
794
  if (args[0] === "service") {
567
- handleServiceCommand(args.slice(1));
795
+ await handleServiceCommand(args.slice(1));
568
796
  return;
569
797
  }
570
798
  spawnHostBinary(args);
571
799
  }
572
800
 
573
- try {
574
- main();
575
- } catch (error) {
801
+ void main().catch((error) => {
576
802
  fail(error instanceof Error ? error.message : String(error));
577
- }
803
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baton-host",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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.6",
14
- "baton-host-darwin-x64": "0.1.6",
15
- "baton-host-linux-arm64": "0.1.6",
16
- "baton-host-linux-x64": "0.1.6"
13
+ "baton-host-darwin-arm64": "0.1.7",
14
+ "baton-host-darwin-x64": "0.1.7",
15
+ "baton-host-linux-arm64": "0.1.7",
16
+ "baton-host-linux-x64": "0.1.7"
17
17
  }
18
18
  }