@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 +81 -17
- package/docs/getting-started.md +1 -1
- package/docs/tech-selection-2026.md +2 -2
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -649,7 +649,10 @@ function createDaemonFromEnv() {
|
|
|
649
649
|
}
|
|
650
650
|
|
|
651
651
|
// agent/src/relay-admin.ts
|
|
652
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const
|
|
683
|
-
|
|
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
|
-
|
|
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",
|
|
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`,
|
|
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}`,
|
|
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}`,
|
|
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
|
|
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:" &&
|
|
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 =
|
|
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
|
-
|
|
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;
|
package/docs/getting-started.md
CHANGED
|
@@ -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
|
|
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
|
|
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": ">=
|
|
18
|
+
"node": ">=22"
|
|
19
19
|
},
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public"
|