@fengye404/termpilot 0.1.9 → 0.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.
package/dist/cli.js CHANGED
@@ -649,7 +649,10 @@ function createDaemonFromEnv() {
649
649
  }
650
650
 
651
651
  // agent/src/relay-admin.ts
652
- function getRelayBaseUrl() {
652
+ function isLocalRelayHost(hostname) {
653
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || /^10\./.test(hostname) || /^192\.168\./.test(hostname) || /^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname);
654
+ }
655
+ function getRelayBaseCandidates() {
653
656
  const relayUrl = process.env.TERMPILOT_RELAY_URL ?? "ws://127.0.0.1:8787/ws";
654
657
  let url;
655
658
  try {
@@ -657,10 +660,31 @@ function getRelayBaseUrl() {
657
660
  } catch {
658
661
  throw new Error("TERMPILOT_RELAY_URL \u65E0\u6548\uFF0C\u8BF7\u63D0\u4F9B\u5B8C\u6574\u7684 ws:// \u6216 wss:// \u5730\u5740\u3002");
659
662
  }
663
+ const wsUrl = new URL(url.toString());
664
+ wsUrl.search = "";
665
+ wsUrl.hash = "";
666
+ if (!wsUrl.pathname || wsUrl.pathname === "/") {
667
+ wsUrl.pathname = "/ws";
668
+ }
660
669
  url.protocol = url.protocol === "wss:" ? "https:" : "http:";
661
670
  url.pathname = "/";
662
671
  url.search = "";
663
- return url.toString();
672
+ url.hash = "";
673
+ const candidates = [{
674
+ baseUrl: url.toString(),
675
+ relayUrl: wsUrl.toString()
676
+ }];
677
+ if (!isLocalRelayHost(url.hostname) && url.port === "8787") {
678
+ const fallbackBase = new URL(url.toString());
679
+ fallbackBase.port = "";
680
+ const fallbackRelay = new URL(wsUrl.toString());
681
+ fallbackRelay.port = "";
682
+ candidates.push({
683
+ baseUrl: fallbackBase.toString(),
684
+ relayUrl: fallbackRelay.toString()
685
+ });
686
+ }
687
+ return candidates;
664
688
  }
665
689
  function getAgentToken() {
666
690
  return process.env.TERMPILOT_AGENT_TOKEN ?? DEFAULT_AGENT_TOKEN;
@@ -675,18 +699,31 @@ async function readJsonOrThrow(response, message) {
675
699
  return response.json();
676
700
  }
677
701
  async function fetchJson(input, init, message) {
678
- let response;
679
- try {
680
- response = await fetch(input, init);
681
- } catch (error) {
682
- const detail = error instanceof Error ? error.message : "\u672A\u77E5\u7F51\u7EDC\u9519\u8BEF";
683
- throw new Error(`${message}: \u65E0\u6CD5\u8FDE\u63A5 relay (${input.origin})\uFF0C${detail}`);
702
+ const candidates = getRelayBaseCandidates();
703
+ let lastError = null;
704
+ let lastOrigin = input.origin;
705
+ for (const candidate of candidates) {
706
+ const target = new URL(input.pathname + input.search, candidate.baseUrl);
707
+ lastOrigin = target.origin;
708
+ let response;
709
+ try {
710
+ response = await fetch(target, init);
711
+ } catch (error) {
712
+ lastError = error;
713
+ continue;
714
+ }
715
+ const payload = await readJsonOrThrow(response, message);
716
+ if (process.env.TERMPILOT_RELAY_URL !== candidate.relayUrl) {
717
+ process.env.TERMPILOT_RELAY_URL = candidate.relayUrl;
718
+ }
719
+ return payload;
684
720
  }
685
- return readJsonOrThrow(response, message);
721
+ const detail = lastError instanceof Error ? lastError.message : "\u672A\u77E5\u7F51\u7EDC\u9519\u8BEF";
722
+ throw new Error(`${message}: \u65E0\u6CD5\u8FDE\u63A5 relay (${lastOrigin})\uFF0C${detail}`);
686
723
  }
687
724
  async function createPairingCode(deviceId) {
688
725
  return fetchJson(
689
- new URL("/api/pairing-codes", getRelayBaseUrl()),
726
+ new URL("/api/pairing-codes", "https://placeholder.invalid"),
690
727
  {
691
728
  method: "POST",
692
729
  headers: {
@@ -700,7 +737,7 @@ async function createPairingCode(deviceId) {
700
737
  }
701
738
  async function listDeviceGrants(deviceId) {
702
739
  return fetchJson(
703
- new URL(`/api/devices/${deviceId}/grants`, getRelayBaseUrl()),
740
+ new URL(`/api/devices/${deviceId}/grants`, "https://placeholder.invalid"),
704
741
  {
705
742
  headers: {
706
743
  authorization: `Bearer ${getAgentToken()}`
@@ -711,7 +748,7 @@ async function listDeviceGrants(deviceId) {
711
748
  }
712
749
  async function revokeDeviceGrant(deviceId, accessToken) {
713
750
  await fetchJson(
714
- new URL(`/api/devices/${deviceId}/grants/${accessToken}`, getRelayBaseUrl()),
751
+ new URL(`/api/devices/${deviceId}/grants/${accessToken}`, "https://placeholder.invalid"),
715
752
  {
716
753
  method: "DELETE",
717
754
  headers: {
@@ -724,7 +761,7 @@ async function revokeDeviceGrant(deviceId, accessToken) {
724
761
  async function listAuditEvents(deviceId, limit) {
725
762
  const constrainedLimit = Math.max(1, Math.min(limit, 100));
726
763
  return fetchJson(
727
- new URL(`/api/devices/${deviceId}/audit-events?limit=${constrainedLimit}`, getRelayBaseUrl()),
764
+ new URL(`/api/devices/${deviceId}/audit-events?limit=${constrainedLimit}`, "https://placeholder.invalid"),
728
765
  {
729
766
  headers: {
730
767
  authorization: `Bearer ${getAgentToken()}`
@@ -844,7 +881,7 @@ function getDeviceId(argv) {
844
881
  const saved = loadAgentConfig();
845
882
  return resolveDeviceId(saved?.deviceId);
846
883
  }
847
- function isLocalRelayHost(hostname) {
884
+ function isLocalRelayHost2(hostname) {
848
885
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || /^10\./.test(hostname) || /^192\.168\./.test(hostname) || /^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname);
849
886
  }
850
887
  function normalizeRelayUrl(rawHost, rawPort) {
@@ -863,7 +900,7 @@ function normalizeRelayUrl(rawHost, rawPort) {
863
900
  throw new Error("\u7AEF\u53E3\u65E0\u6548\uFF0C\u8BF7\u8F93\u5165 1 \u5230 65535 \u4E4B\u95F4\u7684\u6570\u5B57\u3002");
864
901
  }
865
902
  parsed.port = String(normalizedPort2);
866
- } else if (!parsed.port && parsed.protocol === "ws:" && isLocalRelayHost(parsed.hostname)) {
903
+ } else if (!parsed.port && parsed.protocol === "ws:" && isLocalRelayHost2(parsed.hostname)) {
867
904
  parsed.port = "8787";
868
905
  }
869
906
  if (!parsed.pathname || parsed.pathname === "/") {
@@ -873,7 +910,7 @@ function normalizeRelayUrl(rawHost, rawPort) {
873
910
  parsed.hash = "";
874
911
  return parsed.toString();
875
912
  }
876
- const protocol = isLocalRelayHost(hostInput) ? "ws:" : "wss:";
913
+ const protocol = isLocalRelayHost2(hostInput) ? "ws:" : "wss:";
877
914
  if (!portInput) {
878
915
  if (protocol === "ws:") {
879
916
  return `${protocol}//${hostInput}:8787/ws`;
@@ -956,6 +993,27 @@ function applyAgentConfig(config) {
956
993
  process.env.TERMPILOT_RELAY_URL = config.relayUrl;
957
994
  process.env.TERMPILOT_DEVICE_ID = config.deviceId;
958
995
  }
996
+ function persistMigratedRelayUrl(deviceId) {
997
+ const migratedRelayUrl = process.env.TERMPILOT_RELAY_URL?.trim();
998
+ if (!migratedRelayUrl) {
999
+ return;
1000
+ }
1001
+ const saved = loadAgentConfig();
1002
+ if (saved && saved.deviceId === deviceId && saved.relayUrl !== migratedRelayUrl) {
1003
+ saveAgentConfig({
1004
+ relayUrl: migratedRelayUrl,
1005
+ deviceId
1006
+ });
1007
+ console.log(`\u5DF2\u81EA\u52A8\u66F4\u65B0 relay \u5730\u5740\u4E3A: ${migratedRelayUrl}`);
1008
+ }
1009
+ const runtime = loadAgentRuntime();
1010
+ if (runtime && runtime.deviceId === deviceId && runtime.relayUrl !== migratedRelayUrl) {
1011
+ saveAgentRuntime({
1012
+ ...runtime,
1013
+ relayUrl: migratedRelayUrl
1014
+ });
1015
+ }
1016
+ }
959
1017
  function printRuntimeStatus(runtime = readRuntimeStatus().runtime) {
960
1018
  if (!runtime) {
961
1019
  console.log("\u540E\u53F0 agent \u5F53\u524D\u672A\u8FD0\u884C\u3002");
@@ -998,7 +1056,9 @@ async function waitForPairingCode(deviceId) {
998
1056
  let lastError = null;
999
1057
  for (let attempt = 0; attempt < 12; attempt += 1) {
1000
1058
  try {
1001
- return await createPairingCode(deviceId);
1059
+ const payload = await createPairingCode(deviceId);
1060
+ persistMigratedRelayUrl(deviceId);
1061
+ return payload;
1002
1062
  } catch (error) {
1003
1063
  lastError = error;
1004
1064
  await delay2(500);
@@ -1171,6 +1231,7 @@ async function runPair(argv) {
1171
1231
  applyAgentConfig(config.config);
1172
1232
  const deviceId = getDeviceId(argv);
1173
1233
  const payload = await createPairingCode(deviceId);
1234
+ persistMigratedRelayUrl(deviceId);
1174
1235
  console.log(`\u8BBE\u5907: ${payload.deviceId}`);
1175
1236
  console.log(`\u914D\u5BF9\u7801: ${payload.pairingCode}`);
1176
1237
  console.log(`\u6709\u6548\u671F\u81F3: ${payload.expiresAt}`);
@@ -1181,6 +1242,7 @@ async function runGrants(argv) {
1181
1242
  applyAgentConfig(config.config);
1182
1243
  const deviceId = getDeviceId(argv);
1183
1244
  const payload = await listDeviceGrants(deviceId);
1245
+ persistMigratedRelayUrl(deviceId);
1184
1246
  if (payload.grants.length === 0) {
1185
1247
  console.log(`\u8BBE\u5907 ${payload.deviceId} \u5F53\u524D\u6CA1\u6709\u4EFB\u4F55\u5DF2\u7ED1\u5B9A\u8BBF\u95EE\u4EE4\u724C\u3002`);
1186
1248
  return;
@@ -1203,6 +1265,7 @@ async function runRevoke(argv) {
1203
1265
  applyAgentConfig(config.config);
1204
1266
  const deviceId = getDeviceId(argv);
1205
1267
  await revokeDeviceGrant(deviceId, accessToken);
1268
+ persistMigratedRelayUrl(deviceId);
1206
1269
  console.log(`\u5DF2\u64A4\u9500\u8BBE\u5907 ${deviceId} \u7684\u8BBF\u95EE\u4EE4\u724C ${accessToken}`);
1207
1270
  }
1208
1271
  async function runAudit(argv) {
@@ -1216,6 +1279,7 @@ async function runAudit(argv) {
1216
1279
  }
1217
1280
  const limit = Math.floor(parsedLimit);
1218
1281
  const payload = await listAuditEvents(deviceId, limit);
1282
+ persistMigratedRelayUrl(deviceId);
1219
1283
  if (payload.events.length === 0) {
1220
1284
  console.log(`\u8BBE\u5907 ${payload.deviceId} \u5F53\u524D\u6CA1\u6709\u5BA1\u8BA1\u65E5\u5FD7\u3002`);
1221
1285
  return;
@@ -7,7 +7,7 @@
7
7
  - 一台服务器,或者一台手机能访问到的局域网机器
8
8
  - 一台作为主力开发机的电脑
9
9
  - 电脑上已经安装 `tmux`
10
- - 两端都安装了 `Node.js`
10
+ - 两端都安装了 `Node.js 22+`
11
11
 
12
12
  安装 TermPilot:
13
13
 
@@ -5,7 +5,7 @@
5
5
  这次技术选型只保留已经拍板的方案,不再保留多余备选。
6
6
 
7
7
  - 语言:TypeScript
8
- - 运行时:Node.js 24 LTS
8
+ - 运行时:Node.js 22+(推荐 24 LTS
9
9
  - 包管理:pnpm workspace
10
10
 
11
11
  手机端 `app/`:
@@ -74,4 +74,4 @@ TermPilot 现在要的不是“技术看起来完整”,而是:
74
74
 
75
75
  ## 一句话总结
76
76
 
77
- **TermPilot 当前最合理、最 AI 友好的全栈方案,就是 `TypeScript + Node.js 24 LTS + pnpm workspace`,在此基础上,手机端用 `React + Vite + Tailwind CSS + xterm.js`,中继服务用 `Fastify + WebSocket + PostgreSQL`,PC 端围绕 `tmux`。**
77
+ **TermPilot 当前最合理、最 AI 友好的全栈方案,就是 `TypeScript + Node.js 22+ + pnpm workspace`,在此基础上,手机端用 `React + Vite + Tailwind CSS + xterm.js`,中继服务用 `Fastify + WebSocket + PostgreSQL`,PC 端围绕 `tmux`。推荐运行在 Node.js 24 LTS 上,但不强制要求。**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fengye404/termpilot",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@10.31.0",
6
6
  "description": "一个基于 tmux 的终端会话跨端查看与控制原型。",
@@ -15,7 +15,7 @@
15
15
  ".env.example"
16
16
  ],
17
17
  "engines": {
18
- "node": ">=24"
18
+ "node": ">=22"
19
19
  },
20
20
  "publishConfig": {
21
21
  "access": "public"