-
Notifications
You must be signed in to change notification settings - Fork 374
Expand file tree
/
Copy pathremote.ts
More file actions
119 lines (105 loc) · 3.29 KB
/
remote.ts
File metadata and controls
119 lines (105 loc) · 3.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
* Remote session detection, port configuration, and hostname resolution
*
* Environment variables:
* PLANNOTATOR_REMOTE - Set to "1" or "true" to force remote mode (preferred)
* PLANNOTATOR_PORT - Fixed port to use (default: random)
* PLANNOTATOR_HOSTNAME - Explicit hostname for remote URLs (e.g. "mybox.ts.net")
*
* Legacy (still supported): SSH_TTY, SSH_CONNECTION
*
* When Tailscale is available, the server URL uses the Tailscale hostname
* so remote users can connect directly without port forwarding. This also
* allows random ports, so parallel sessions work.
*/
/**
* Check if running in a remote session (SSH, devcontainer, etc.)
*/
export function isRemoteSession(): boolean {
// New preferred env var
const remote = process.env.PLANNOTATOR_REMOTE;
if (remote === "1" || remote?.toLowerCase() === "true") {
return true;
}
// Legacy: SSH_TTY/SSH_CONNECTION (deprecated, silent)
if (process.env.SSH_TTY || process.env.SSH_CONNECTION) {
return true;
}
return false;
}
/**
* Get the server port to use.
*
* Always uses random port (0) unless PLANNOTATOR_PORT is explicitly set.
* The old default of 19432 for remote is no longer needed since we resolve
* the actual hostname (Tailscale/explicit) instead of relying on port forwarding.
*/
export function getServerPort(): number {
// Explicit port from environment takes precedence
const envPort = process.env.PLANNOTATOR_PORT;
if (envPort) {
const parsed = parseInt(envPort, 10);
if (!isNaN(parsed) && parsed > 0 && parsed < 65536) {
return parsed;
}
console.error(
`[Plannotator] Warning: Invalid PLANNOTATOR_PORT "${envPort}", using default`
);
}
return 0;
}
let cachedHostname: string | null | undefined;
/**
* Get the hostname to use in server URLs.
*
* Priority:
* 1. PLANNOTATOR_HOSTNAME env var (explicit override)
* 2. Tailscale hostname (auto-detected via `tailscale status`)
* 3. "localhost" (fallback)
*/
export async function getServerHostname(): Promise<string> {
if (cachedHostname !== undefined) {
return cachedHostname ?? "localhost";
}
// 1. Explicit env var
const envHostname = process.env.PLANNOTATOR_HOSTNAME;
if (envHostname) {
cachedHostname = envHostname;
return envHostname;
}
// 2. Auto-detect Tailscale
if (isRemoteSession()) {
const tsHostname = await detectTailscaleHostname();
if (tsHostname) {
cachedHostname = tsHostname;
return tsHostname;
}
}
// 3. Fallback
cachedHostname = null;
return "localhost";
}
/**
* Detect the Tailscale DNS name by running `tailscale status --self --json`.
* Returns null if Tailscale is not available or not running.
*/
async function detectTailscaleHostname(): Promise<string | null> {
try {
const proc = Bun.spawn(["tailscale", "status", "--self", "--json"], {
stdout: "pipe",
stderr: "ignore",
});
const text = await new Response(proc.stdout).text();
const exitCode = await proc.exited;
if (exitCode !== 0) return null;
const data = JSON.parse(text);
// DNSName has a trailing dot, e.g. "a4000.chaco-dory.ts.net."
const dnsName = data?.Self?.DNSName;
if (typeof dnsName === "string" && dnsName.length > 1) {
return dnsName.replace(/\.$/, "");
}
return null;
} catch {
return null;
}
}