@clsh/agent 0.0.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.
Files changed (53) hide show
  1. package/dist/auth.d.ts +37 -0
  2. package/dist/auth.d.ts.map +1 -0
  3. package/dist/auth.js +62 -0
  4. package/dist/auth.js.map +1 -0
  5. package/dist/config.d.ts +23 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +119 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/control-mode-parser.d.ts +68 -0
  10. package/dist/control-mode-parser.d.ts.map +1 -0
  11. package/dist/control-mode-parser.js +111 -0
  12. package/dist/control-mode-parser.js.map +1 -0
  13. package/dist/db.d.ts +44 -0
  14. package/dist/db.d.ts.map +1 -0
  15. package/dist/db.js +63 -0
  16. package/dist/db.js.map +1 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +114 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/power.d.ts +9 -0
  22. package/dist/power.d.ts.map +1 -0
  23. package/dist/power.js +29 -0
  24. package/dist/power.js.map +1 -0
  25. package/dist/pty-manager.d.ts +110 -0
  26. package/dist/pty-manager.d.ts.map +1 -0
  27. package/dist/pty-manager.js +468 -0
  28. package/dist/pty-manager.js.map +1 -0
  29. package/dist/server.d.ts +24 -0
  30. package/dist/server.d.ts.map +1 -0
  31. package/dist/server.js +160 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/sse-handler.d.ts +13 -0
  34. package/dist/sse-handler.d.ts.map +1 -0
  35. package/dist/sse-handler.js +76 -0
  36. package/dist/sse-handler.js.map +1 -0
  37. package/dist/tmux.d.ts +34 -0
  38. package/dist/tmux.d.ts.map +1 -0
  39. package/dist/tmux.js +114 -0
  40. package/dist/tmux.js.map +1 -0
  41. package/dist/tunnel.d.ts +43 -0
  42. package/dist/tunnel.d.ts.map +1 -0
  43. package/dist/tunnel.js +294 -0
  44. package/dist/tunnel.js.map +1 -0
  45. package/dist/types.d.ts +76 -0
  46. package/dist/types.d.ts.map +1 -0
  47. package/dist/types.js +2 -0
  48. package/dist/types.js.map +1 -0
  49. package/dist/ws-handler.d.ts +13 -0
  50. package/dist/ws-handler.d.ts.map +1 -0
  51. package/dist/ws-handler.js +266 -0
  52. package/dist/ws-handler.js.map +1 -0
  53. package/package.json +47 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { Router } from 'express';
2
+ /**
3
+ * Emits an auth-complete event for a given pendingId.
4
+ * If a client is already listening via SSE, the JWT is sent immediately.
5
+ * Otherwise, the JWT is stored for later retrieval (stored-event pattern).
6
+ */
7
+ export declare function emitAuthComplete(pendingId: string, jwt: string): void;
8
+ /**
9
+ * Creates the SSE router with a single endpoint: GET /events/:pendingId.
10
+ * The client opens this connection and waits for an auth-complete event.
11
+ */
12
+ export declare function createSSERouter(): Router;
13
+ //# sourceMappingURL=sse-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-handler.d.ts","sourceRoot":"","sources":["../src/sse-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,MAAM,EAAE,MAAM,SAAS,CAAC;AAgBzD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAiBrE;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CA6CxC"}
@@ -0,0 +1,76 @@
1
+ import { Router as createRouter } from 'express';
2
+ /**
3
+ * Stored completed auth events, keyed by pendingId.
4
+ * This solves the race condition where auth completes before
5
+ * the client starts listening for SSE events.
6
+ */
7
+ const completedAuths = new Map();
8
+ /** Active SSE connections waiting for auth completion, keyed by pendingId. */
9
+ const pendingListeners = new Map();
10
+ /** Heartbeat interval in milliseconds. */
11
+ const HEARTBEAT_INTERVAL_MS = 30_000;
12
+ /**
13
+ * Emits an auth-complete event for a given pendingId.
14
+ * If a client is already listening via SSE, the JWT is sent immediately.
15
+ * Otherwise, the JWT is stored for later retrieval (stored-event pattern).
16
+ */
17
+ export function emitAuthComplete(pendingId, jwt) {
18
+ const res = pendingListeners.get(pendingId);
19
+ if (res) {
20
+ // Client is already listening -- send the JWT immediately
21
+ res.write(`event: auth-complete\ndata: ${JSON.stringify({ token: jwt })}\n\n`);
22
+ res.end();
23
+ pendingListeners.delete(pendingId);
24
+ }
25
+ else {
26
+ // Client not yet listening -- store for later retrieval
27
+ completedAuths.set(pendingId, jwt);
28
+ // Auto-expire stored events after 5 minutes to prevent memory leaks
29
+ setTimeout(() => {
30
+ completedAuths.delete(pendingId);
31
+ }, 5 * 60 * 1000);
32
+ }
33
+ }
34
+ /**
35
+ * Creates the SSE router with a single endpoint: GET /events/:pendingId.
36
+ * The client opens this connection and waits for an auth-complete event.
37
+ */
38
+ export function createSSERouter() {
39
+ const router = createRouter();
40
+ router.get('/events/:pendingId', (req, res) => {
41
+ const rawPendingId = req.params['pendingId'];
42
+ const pendingId = Array.isArray(rawPendingId) ? rawPendingId[0] : rawPendingId;
43
+ if (!pendingId) {
44
+ res.status(400).json({ error: 'Missing pendingId' });
45
+ return;
46
+ }
47
+ // Set SSE headers
48
+ res.writeHead(200, {
49
+ 'Content-Type': 'text/event-stream',
50
+ 'Cache-Control': 'no-cache',
51
+ Connection: 'keep-alive',
52
+ 'X-Accel-Buffering': 'no',
53
+ });
54
+ // Race condition fix: check if auth already completed before we started listening
55
+ const storedJwt = completedAuths.get(pendingId);
56
+ if (storedJwt) {
57
+ completedAuths.delete(pendingId);
58
+ res.write(`event: auth-complete\ndata: ${JSON.stringify({ token: storedJwt })}\n\n`);
59
+ res.end();
60
+ return;
61
+ }
62
+ // Register this connection as a pending listener
63
+ pendingListeners.set(pendingId, res);
64
+ // Heartbeat to keep the connection alive through proxies
65
+ const heartbeat = setInterval(() => {
66
+ res.write(': heartbeat\n\n');
67
+ }, HEARTBEAT_INTERVAL_MS);
68
+ // Clean up on client disconnect
69
+ req.on('close', () => {
70
+ clearInterval(heartbeat);
71
+ pendingListeners.delete(pendingId);
72
+ });
73
+ });
74
+ return router;
75
+ }
76
+ //# sourceMappingURL=sse-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse-handler.js","sourceRoot":"","sources":["../src/sse-handler.ts"],"names":[],"mappings":"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"}
package/dist/tmux.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /** Socket name for clsh's isolated tmux server (avoids conflicts with user's tmux). */
2
+ export declare const TMUX_SOCKET = "clsh";
3
+ /**
4
+ * Checks if tmux is available on the system.
5
+ */
6
+ export declare function isTmuxAvailable(): boolean;
7
+ /**
8
+ * Writes the invisible tmux config to ~/.clsh/tmux.conf and returns its path.
9
+ */
10
+ export declare function ensureTmuxConfig(): string;
11
+ /**
12
+ * Lists all tmux sessions matching the clsh- prefix.
13
+ * Returns an array of session names (e.g. ["clsh-abc123", "clsh-def456"]).
14
+ */
15
+ export declare function listClshTmuxSessions(): string[];
16
+ /**
17
+ * Checks if a specific tmux session exists.
18
+ */
19
+ export declare function tmuxSessionExists(name: string): boolean;
20
+ /**
21
+ * Kills a single tmux session by name.
22
+ */
23
+ export declare function killTmuxSession(name: string): void;
24
+ /**
25
+ * Kills all clsh-prefixed tmux sessions.
26
+ */
27
+ export declare function killAllClshTmuxSessions(): void;
28
+ /**
29
+ * Captures the full scrollback + visible screen of a tmux pane.
30
+ * Returns the content as text with ANSI escape sequences preserved.
31
+ * Used to bootstrap the buffer on reattach after server restart.
32
+ */
33
+ export declare function capturePaneContent(tmuxName: string): string;
34
+ //# sourceMappingURL=tmux.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tmux.d.ts","sourceRoot":"","sources":["../src/tmux.ts"],"names":[],"mappings":"AAKA,uFAAuF;AACvF,eAAO,MAAM,WAAW,SAAS,CAAC;AAiBlC;;GAEG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAOzC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAMzC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,EAAE,CAc/C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOvD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMlD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAI9C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAc3D"}
package/dist/tmux.js ADDED
@@ -0,0 +1,114 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { writeFileSync, mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ /** Socket name for clsh's isolated tmux server (avoids conflicts with user's tmux). */
6
+ export const TMUX_SOCKET = 'clsh';
7
+ /** Invisible tmux config — no status bar, no prefix, passthrough for OSC 7. */
8
+ const TMUX_CONF = `set -g status off
9
+ set -g prefix None
10
+ unbind-key C-b
11
+ set -g allow-passthrough on
12
+ set -g default-terminal "xterm-256color"
13
+ set -g mouse off
14
+ set -g history-limit 5000
15
+ set -g escape-time 0
16
+ set -ga terminal-overrides ",xterm-256color:Tc"
17
+ `;
18
+ /** Prefix used for all clsh-managed tmux sessions. */
19
+ const SESSION_PREFIX = 'clsh-';
20
+ /**
21
+ * Checks if tmux is available on the system.
22
+ */
23
+ export function isTmuxAvailable() {
24
+ try {
25
+ execFileSync('tmux', ['-V'], { stdio: 'ignore' });
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ /**
33
+ * Writes the invisible tmux config to ~/.clsh/tmux.conf and returns its path.
34
+ */
35
+ export function ensureTmuxConfig() {
36
+ const clshDir = join(homedir(), '.clsh');
37
+ const confPath = join(clshDir, 'tmux.conf');
38
+ mkdirSync(clshDir, { recursive: true });
39
+ writeFileSync(confPath, TMUX_CONF, { mode: 0o644 });
40
+ return confPath;
41
+ }
42
+ /**
43
+ * Lists all tmux sessions matching the clsh- prefix.
44
+ * Returns an array of session names (e.g. ["clsh-abc123", "clsh-def456"]).
45
+ */
46
+ export function listClshTmuxSessions() {
47
+ try {
48
+ const output = execFileSync('tmux', ['-L', TMUX_SOCKET, 'list-sessions', '-F', '#{session_name}'], {
49
+ encoding: 'utf-8',
50
+ stdio: ['ignore', 'pipe', 'ignore'],
51
+ });
52
+ return output
53
+ .trim()
54
+ .split('\n')
55
+ .filter((name) => name.startsWith(SESSION_PREFIX));
56
+ }
57
+ catch {
58
+ // tmux server not running or no sessions — both fine
59
+ return [];
60
+ }
61
+ }
62
+ /**
63
+ * Checks if a specific tmux session exists.
64
+ */
65
+ export function tmuxSessionExists(name) {
66
+ try {
67
+ execFileSync('tmux', ['-L', TMUX_SOCKET, 'has-session', '-t', name], { stdio: 'ignore' });
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ /**
75
+ * Kills a single tmux session by name.
76
+ */
77
+ export function killTmuxSession(name) {
78
+ try {
79
+ execFileSync('tmux', ['-L', TMUX_SOCKET, 'kill-session', '-t', name], { stdio: 'ignore' });
80
+ }
81
+ catch {
82
+ // Session already gone — fine
83
+ }
84
+ }
85
+ /**
86
+ * Kills all clsh-prefixed tmux sessions.
87
+ */
88
+ export function killAllClshTmuxSessions() {
89
+ for (const name of listClshTmuxSessions()) {
90
+ killTmuxSession(name);
91
+ }
92
+ }
93
+ /**
94
+ * Captures the full scrollback + visible screen of a tmux pane.
95
+ * Returns the content as text with ANSI escape sequences preserved.
96
+ * Used to bootstrap the buffer on reattach after server restart.
97
+ */
98
+ export function capturePaneContent(tmuxName) {
99
+ try {
100
+ const content = execFileSync('tmux', [
101
+ '-L', TMUX_SOCKET,
102
+ 'capture-pane', '-t', tmuxName,
103
+ '-p', '-S', '-', '-e',
104
+ ], { encoding: 'utf-8' });
105
+ // capture-pane outputs \n line endings, but xterm.js needs \r\n —
106
+ // without \r the cursor doesn't return to column 0, causing text to cascade right.
107
+ // Also trim trailing blank lines (empty terminal rows below content).
108
+ return content.replace(/\n+$/, '\n').replace(/\n/g, '\r\n');
109
+ }
110
+ catch {
111
+ return '';
112
+ }
113
+ }
114
+ //# sourceMappingURL=tmux.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tmux.js","sourceRoot":"","sources":["../src/tmux.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ export type TunnelMethod = 'ngrok' | 'ssh' | 'local';
2
+ export interface TunnelResult {
3
+ url: string;
4
+ method: TunnelMethod;
5
+ }
6
+ /**
7
+ * Creates a public tunnel to expose the local server.
8
+ *
9
+ * Priority order:
10
+ * 1. ngrok — if NGROK_AUTHTOKEN is set (most reliable, needs free account)
11
+ * 2. SSH — localhost.run via SSH, zero install, no account needed
12
+ * 3. local — local network IP, works when phone is on the same Wi-Fi
13
+ */
14
+ export declare function createTunnel(port: number, ngrokAuthtoken?: string, ngrokStaticDomain?: string, forcedMethod?: 'ngrok' | 'ssh' | 'local'): Promise<TunnelResult>;
15
+ /**
16
+ * Returns the current tunnel URL, or null if no tunnel is active.
17
+ */
18
+ export declare function getTunnelUrl(): string | null;
19
+ /**
20
+ * Prints a clean startup banner with QR code and access info.
21
+ */
22
+ export declare function printAccessInfo(publicUrl: string, bootstrapToken: string, method: TunnelMethod): void;
23
+ /**
24
+ * Starts a background monitor that detects system sleep/wake and SSH tunnel
25
+ * death, then automatically recreates the tunnel.
26
+ *
27
+ * Uses a "time drift" detector: if the interval timer fires after a gap much
28
+ * larger than expected, the system was sleeping. On wake, it waits for the
29
+ * network to stabilize, checks tunnel health, and recreates if necessary.
30
+ *
31
+ * @param onRecovered — called when the tunnel is recreated with a new URL
32
+ * @returns cleanup function to stop the monitor
33
+ */
34
+ export declare function startTunnelMonitor(onRecovered: (url: string, method: TunnelMethod) => void): () => void;
35
+ /**
36
+ * Closes any active tunnels (ngrok listener and/or SSH process).
37
+ */
38
+ export declare function closeTunnel(): Promise<void>;
39
+ /**
40
+ * Registers SIGINT/SIGTERM handlers for graceful shutdown.
41
+ */
42
+ export declare function registerShutdownHandlers(cleanup: () => void | Promise<void>): void;
43
+ //# sourceMappingURL=tunnel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../src/tunnel.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;AAErD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;CACtB;AAyFD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,cAAc,CAAC,EAAE,MAAM,EACvB,iBAAiB,CAAC,EAAE,MAAM,EAC1B,YAAY,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,GACvC,OAAO,CAAC,YAAY,CAAC,CA6CvB;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,YAAY,GACnB,IAAI,CAiCN;AAmCD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,KAAK,IAAI,GACvD,MAAM,IAAI,CAiDZ;AAED;;GAEG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAUjD;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAclF"}
package/dist/tunnel.js ADDED
@@ -0,0 +1,294 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { networkInterfaces } from 'node:os';
3
+ import ngrok from '@ngrok/ngrok';
4
+ // @ts-expect-error -- qrcode-terminal has no type declarations
5
+ import qrcode from 'qrcode-terminal';
6
+ let activeNgrokListener = null;
7
+ let activeSSHProcess = null;
8
+ let savedConfig = null;
9
+ let currentTunnel = null;
10
+ /** Set to true when an SSH process dies after a tunnel was established. */
11
+ let tunnelDead = false;
12
+ /**
13
+ * Returns the first non-internal IPv4 address for this machine.
14
+ * Used so phones on the same Wi-Fi can connect without any tunnel.
15
+ */
16
+ function getLocalIP() {
17
+ const nets = networkInterfaces();
18
+ for (const interfaces of Object.values(nets)) {
19
+ if (!interfaces)
20
+ continue;
21
+ for (const iface of interfaces) {
22
+ if (iface.family === 'IPv4' && !iface.internal) {
23
+ return iface.address;
24
+ }
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+ /**
30
+ * Creates a public tunnel via localhost.run using SSH.
31
+ * SSH is pre-installed on macOS — no account, no binary download required.
32
+ * Resolves with the public HTTPS URL once the tunnel is established.
33
+ */
34
+ function createSSHTunnel(localPort) {
35
+ return new Promise((resolve, reject) => {
36
+ const ssh = spawn('ssh', [
37
+ '-R', `80:localhost:${localPort}`,
38
+ 'nokey@localhost.run',
39
+ '-o', 'StrictHostKeyChecking=no',
40
+ '-o', 'UserKnownHostsFile=/dev/null',
41
+ '-o', 'LogLevel=ERROR',
42
+ ]);
43
+ activeSSHProcess = ssh;
44
+ const urlPattern = /https:\/\/[a-zA-Z0-9-]+\.(?:localhost\.run|lhr\.life)/;
45
+ let resolved = false;
46
+ const tryResolve = (data) => {
47
+ if (resolved)
48
+ return;
49
+ const match = urlPattern.exec(data.toString());
50
+ if (match) {
51
+ resolved = true;
52
+ resolve(match[0]);
53
+ }
54
+ };
55
+ ssh.stdout.on('data', tryResolve);
56
+ ssh.stderr.on('data', tryResolve);
57
+ ssh.on('error', (err) => {
58
+ if (!resolved)
59
+ reject(err);
60
+ });
61
+ ssh.on('close', (code) => {
62
+ activeSSHProcess = null;
63
+ if (!resolved) {
64
+ reject(new Error(`SSH exited with code ${String(code)}`));
65
+ }
66
+ else {
67
+ // Tunnel was established but SSH process died (network drop, sleep, etc.)
68
+ tunnelDead = true;
69
+ }
70
+ });
71
+ setTimeout(() => {
72
+ if (!resolved) {
73
+ ssh.kill();
74
+ reject(new Error('localhost.run tunnel timed out after 12s'));
75
+ }
76
+ }, 12_000);
77
+ });
78
+ }
79
+ /**
80
+ * Creates a public tunnel to expose the local server.
81
+ *
82
+ * Priority order:
83
+ * 1. ngrok — if NGROK_AUTHTOKEN is set (most reliable, needs free account)
84
+ * 2. SSH — localhost.run via SSH, zero install, no account needed
85
+ * 3. local — local network IP, works when phone is on the same Wi-Fi
86
+ */
87
+ export async function createTunnel(port, ngrokAuthtoken, ngrokStaticDomain, forcedMethod) {
88
+ // Store config for recreation on tunnel death
89
+ savedConfig = { port, ngrokAuthtoken, ngrokStaticDomain, forcedMethod };
90
+ tunnelDead = false;
91
+ // 1. ngrok — best reliability, optional free-account token
92
+ if (forcedMethod !== 'ssh' && forcedMethod !== 'local' && ngrokAuthtoken) {
93
+ try {
94
+ const ngrokOpts = {
95
+ addr: port,
96
+ authtoken: ngrokAuthtoken,
97
+ };
98
+ if (ngrokStaticDomain)
99
+ ngrokOpts.domain = ngrokStaticDomain;
100
+ const listener = await ngrok.forward(ngrokOpts);
101
+ activeNgrokListener = listener;
102
+ const url = listener.url();
103
+ if (url) {
104
+ currentTunnel = { url, method: 'ngrok' };
105
+ return currentTunnel;
106
+ }
107
+ }
108
+ catch {
109
+ // ngrok failed — try SSH
110
+ }
111
+ }
112
+ // 2. localhost.run via SSH (pre-installed on macOS, no account needed)
113
+ if (forcedMethod === 'local') {
114
+ const localIp = getLocalIP();
115
+ const url = localIp ? `http://${localIp}:${port}` : `http://localhost:${port}`;
116
+ currentTunnel = { url, method: 'local' };
117
+ return currentTunnel;
118
+ }
119
+ try {
120
+ const url = await createSSHTunnel(port);
121
+ currentTunnel = { url, method: 'ssh' };
122
+ return currentTunnel;
123
+ }
124
+ catch {
125
+ // SSH failed — fall back to local
126
+ }
127
+ // 3. Local network — works on same Wi-Fi, no internet required
128
+ const localIp = getLocalIP();
129
+ const url = localIp ? `http://${localIp}:${port}` : `http://localhost:${port}`;
130
+ currentTunnel = { url, method: 'local' };
131
+ return currentTunnel;
132
+ }
133
+ /**
134
+ * Returns the current tunnel URL, or null if no tunnel is active.
135
+ */
136
+ export function getTunnelUrl() {
137
+ return currentTunnel?.url ?? null;
138
+ }
139
+ /**
140
+ * Prints a clean startup banner with QR code and access info.
141
+ */
142
+ export function printAccessInfo(publicUrl, bootstrapToken, method) {
143
+ const authUrl = `${publicUrl}/?token=${bootstrapToken}`;
144
+ // ANSI orange (256-color: 208)
145
+ const o = '\x1b[38;5;208m';
146
+ const dim = '\x1b[2m';
147
+ const r = '\x1b[0m';
148
+ console.log('');
149
+ console.log(`${o} ██████╗██╗ ███████╗██╗ ██╗${r}`);
150
+ console.log(`${o} ██╔════╝██║ ██╔════╝██║ ██║${r}`);
151
+ console.log(`${o} ██║ ██║ ███████╗███████║${r}`);
152
+ console.log(`${o} ██║ ██║ ╚════██║██╔══██║${r}`);
153
+ console.log(`${o} ╚██████╗███████╗███████║██║ ██║${r}`);
154
+ console.log(`${o} ╚═════╝╚══════╝╚══════╝╚═╝ ╚═╝${r}`);
155
+ console.log(`${dim} clsh.dev${r}`);
156
+ console.log('');
157
+ qrcode.generate(authUrl, { small: true }, (code) => {
158
+ // Print QR in default terminal colors (high contrast in both light & dark terminals)
159
+ console.log(code);
160
+ console.log(`${o} Scan to connect ${dim}(token embedded in QR)${r}`);
161
+ console.log('');
162
+ console.log(`${o} URL: ${r}${publicUrl}`);
163
+ console.log(`${o} Token: ${r}${bootstrapToken}`);
164
+ console.log(`${o} Mode: ${r}${method === 'ngrok' ? 'remote (ngrok)' : method === 'ssh' ? 'remote (ssh)' : 'local Wi-Fi only'}`);
165
+ if (method === 'local') {
166
+ console.log('');
167
+ console.log(`${o} ⚠ Local mode — phone must be on same Wi-Fi.${r}`);
168
+ console.log(`${dim} Set NGROK_AUTHTOKEN in .env for remote access.${r}`);
169
+ }
170
+ console.log('');
171
+ });
172
+ }
173
+ // --------------- Tunnel monitoring and recovery ---------------
174
+ /**
175
+ * Checks if the tunnel is alive by hitting our own health endpoint through it.
176
+ * Verifies the full path: server → tunnel → internet → back.
177
+ */
178
+ async function isTunnelAlive() {
179
+ if (tunnelDead || !currentTunnel)
180
+ return false;
181
+ if (currentTunnel.method === 'local')
182
+ return true;
183
+ try {
184
+ const res = await fetch(`${currentTunnel.url}/api/health`, {
185
+ signal: AbortSignal.timeout(5_000),
186
+ });
187
+ return res.ok;
188
+ }
189
+ catch {
190
+ return false;
191
+ }
192
+ }
193
+ /**
194
+ * Closes the current tunnel and creates a new one using the saved config.
195
+ */
196
+ async function recreate() {
197
+ if (!savedConfig)
198
+ return null;
199
+ await closeTunnel();
200
+ return createTunnel(savedConfig.port, savedConfig.ngrokAuthtoken, savedConfig.ngrokStaticDomain, savedConfig.forcedMethod);
201
+ }
202
+ /**
203
+ * Starts a background monitor that detects system sleep/wake and SSH tunnel
204
+ * death, then automatically recreates the tunnel.
205
+ *
206
+ * Uses a "time drift" detector: if the interval timer fires after a gap much
207
+ * larger than expected, the system was sleeping. On wake, it waits for the
208
+ * network to stabilize, checks tunnel health, and recreates if necessary.
209
+ *
210
+ * @param onRecovered — called when the tunnel is recreated with a new URL
211
+ * @returns cleanup function to stop the monitor
212
+ */
213
+ export function startTunnelMonitor(onRecovered) {
214
+ const INTERVAL_MS = 5_000;
215
+ const WAKE_THRESHOLD_MS = 15_000;
216
+ let lastTick = Date.now();
217
+ let recovering = false;
218
+ const check = async () => {
219
+ if (recovering)
220
+ return;
221
+ const now = Date.now();
222
+ const gap = now - lastTick - INTERVAL_MS;
223
+ lastTick = now;
224
+ const woke = gap > WAKE_THRESHOLD_MS;
225
+ if (!woke && !tunnelDead)
226
+ return;
227
+ recovering = true;
228
+ try {
229
+ if (woke) {
230
+ console.log(` Wake detected (${Math.round((gap + INTERVAL_MS) / 1000)}s gap), checking tunnel...`);
231
+ // Give the network interface a moment to come back up
232
+ await new Promise((r) => setTimeout(r, 3_000));
233
+ }
234
+ else {
235
+ console.log(' Tunnel process died, restarting...');
236
+ await new Promise((r) => setTimeout(r, 2_000));
237
+ }
238
+ const alive = tunnelDead ? false : await isTunnelAlive();
239
+ if (alive) {
240
+ console.log(' Tunnel OK');
241
+ }
242
+ else {
243
+ console.log(' Tunnel down, recreating...');
244
+ const result = await recreate();
245
+ if (result) {
246
+ console.log(` Tunnel recovered: ${result.url} (${result.method})`);
247
+ onRecovered(result.url, result.method);
248
+ }
249
+ }
250
+ }
251
+ catch (err) {
252
+ console.error(' Tunnel recovery failed:', err);
253
+ }
254
+ recovering = false;
255
+ };
256
+ const timer = setInterval(() => void check(), INTERVAL_MS);
257
+ return () => clearInterval(timer);
258
+ }
259
+ /**
260
+ * Closes any active tunnels (ngrok listener and/or SSH process).
261
+ */
262
+ export async function closeTunnel() {
263
+ if (activeNgrokListener) {
264
+ try {
265
+ await ngrok.disconnect();
266
+ }
267
+ catch { /* ignore */ }
268
+ activeNgrokListener = null;
269
+ }
270
+ if (activeSSHProcess) {
271
+ activeSSHProcess.kill();
272
+ activeSSHProcess = null;
273
+ }
274
+ tunnelDead = false;
275
+ }
276
+ /**
277
+ * Registers SIGINT/SIGTERM handlers for graceful shutdown.
278
+ */
279
+ export function registerShutdownHandlers(cleanup) {
280
+ const shutdown = async (signal) => {
281
+ console.log(`\n Received ${signal}, shutting down...`);
282
+ try {
283
+ await cleanup();
284
+ await closeTunnel();
285
+ }
286
+ catch (err) {
287
+ console.error(' Error during shutdown:', err);
288
+ }
289
+ process.exit(0);
290
+ };
291
+ process.on('SIGINT', () => void shutdown('SIGINT'));
292
+ process.on('SIGTERM', () => void shutdown('SIGTERM'));
293
+ }
294
+ //# sourceMappingURL=tunnel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel.js","sourceRoot":"","sources":["../src/tunnel.ts"],"names":[],"mappings":"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"}