@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.
- package/dist/auth.d.ts +37 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +62 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +119 -0
- package/dist/config.js.map +1 -0
- package/dist/control-mode-parser.d.ts +68 -0
- package/dist/control-mode-parser.d.ts.map +1 -0
- package/dist/control-mode-parser.js +111 -0
- package/dist/control-mode-parser.js.map +1 -0
- package/dist/db.d.ts +44 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +63 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +114 -0
- package/dist/index.js.map +1 -0
- package/dist/power.d.ts +9 -0
- package/dist/power.d.ts.map +1 -0
- package/dist/power.js +29 -0
- package/dist/power.js.map +1 -0
- package/dist/pty-manager.d.ts +110 -0
- package/dist/pty-manager.d.ts.map +1 -0
- package/dist/pty-manager.js +468 -0
- package/dist/pty-manager.js.map +1 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +160 -0
- package/dist/server.js.map +1 -0
- package/dist/sse-handler.d.ts +13 -0
- package/dist/sse-handler.d.ts.map +1 -0
- package/dist/sse-handler.js +76 -0
- package/dist/sse-handler.js.map +1 -0
- package/dist/tmux.d.ts +34 -0
- package/dist/tmux.d.ts.map +1 -0
- package/dist/tmux.js +114 -0
- package/dist/tmux.js.map +1 -0
- package/dist/tunnel.d.ts +43 -0
- package/dist/tunnel.d.ts.map +1 -0
- package/dist/tunnel.js +294 -0
- package/dist/tunnel.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ws-handler.d.ts +13 -0
- package/dist/ws-handler.d.ts.map +1 -0
- package/dist/ws-handler.js +266 -0
- package/dist/ws-handler.js.map +1 -0
- 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
|
package/dist/tmux.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmux.js","sourceRoot":"","sources":["../src/tmux.ts"],"names":[],"mappings":"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"}
|
package/dist/tunnel.d.ts
ADDED
|
@@ -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":"AAAA,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,MAAM,cAAc,CAAC;AACjC,+DAA+D;AAC/D,OAAO,MAAM,MAAM,iBAAiB,CAAC;AASrC,IAAI,mBAAmB,GAA0B,IAAI,CAAC;AACtD,IAAI,gBAAgB,GAAwB,IAAI,CAAC;AASjD,IAAI,WAAW,GAAwB,IAAI,CAAC;AAC5C,IAAI,aAAa,GAAwB,IAAI,CAAC;AAC9C,2EAA2E;AAC3E,IAAI,UAAU,GAAG,KAAK,CAAC;AAEvB;;;GAGG;AACH,SAAS,UAAU;IACjB,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,KAAK,MAAM,UAAU,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7C,IAAI,CAAC,UAAU;YAAE,SAAS;QAC1B,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;YAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;gBAC/C,OAAO,KAAK,CAAC,OAAO,CAAC;YACvB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,SAAiB;IACxC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE;YACvB,IAAI,EAAE,gBAAgB,SAAS,EAAE;YACjC,qBAAqB;YACrB,IAAI,EAAE,0BAA0B;YAChC,IAAI,EAAE,8BAA8B;YACpC,IAAI,EAAE,gBAAgB;SACvB,CAAC,CAAC;QAEH,gBAAgB,GAAG,GAAG,CAAC;QAEvB,MAAM,UAAU,GAAG,uDAAuD,CAAC;QAC3E,IAAI,QAAQ,GAAG,KAAK,CAAC;QAErB,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,EAAE;YAClC,IAAI,QAAQ;gBAAE,OAAO;YACrB,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC/C,IAAI,KAAK,EAAE,CAAC;gBACV,QAAQ,GAAG,IAAI,CAAC;gBAChB,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACpB,CAAC;QACH,CAAC,CAAC;QAEF,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAClC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAElC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACtB,IAAI,CAAC,QAAQ;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACvB,gBAAgB,GAAG,IAAI,CAAC;YACxB,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,CAAC,IAAI,KAAK,CAAC,wBAAwB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;YAC5D,CAAC;iBAAM,CAAC;gBACN,0EAA0E;gBAC1E,UAAU,GAAG,IAAI,CAAC;YACpB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,UAAU,CAAC,GAAG,EAAE;YACd,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,GAAG,CAAC,IAAI,EAAE,CAAC;gBACX,MAAM,CAAC,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC,CAAC;YAChE,CAAC;QACH,CAAC,EAAE,MAAM,CAAC,CAAC;IACb,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAY,EACZ,cAAuB,EACvB,iBAA0B,EAC1B,YAAwC;IAExC,8CAA8C;IAC9C,WAAW,GAAG,EAAE,IAAI,EAAE,cAAc,EAAE,iBAAiB,EAAE,YAAY,EAAE,CAAC;IACxE,UAAU,GAAG,KAAK,CAAC;IAEnB,2DAA2D;IAC3D,IAAI,YAAY,KAAK,KAAK,IAAI,YAAY,KAAK,OAAO,IAAI,cAAc,EAAE,CAAC;QACzE,IAAI,CAAC;YACH,MAAM,SAAS,GAAwC;gBACrD,IAAI,EAAE,IAAI;gBACV,SAAS,EAAE,cAAc;aAC1B,CAAC;YACF,IAAI,iBAAiB;gBAAE,SAAS,CAAC,MAAM,GAAG,iBAAiB,CAAC;YAC5D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAChD,mBAAmB,GAAG,QAAQ,CAAC;YAC/B,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC;YAC3B,IAAI,GAAG,EAAE,CAAC;gBACR,aAAa,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;gBACzC,OAAO,aAAa,CAAC;YACvB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,yBAAyB;QAC3B,CAAC;IACH,CAAC;IAED,uEAAuE;IACvE,IAAI,YAAY,KAAK,OAAO,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,UAAU,OAAO,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,oBAAoB,IAAI,EAAE,CAAC;QAC/E,aAAa,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QACzC,OAAO,aAAa,CAAC;IACvB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;QACxC,aAAa,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QACvC,OAAO,aAAa,CAAC;IACvB,CAAC;IAAC,MAAM,CAAC;QACP,kCAAkC;IACpC,CAAC;IAED,+DAA+D;IAC/D,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,UAAU,OAAO,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,oBAAoB,IAAI,EAAE,CAAC;IAC/E,aAAa,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;IACzC,OAAO,aAAa,CAAC;AACvB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,aAAa,EAAE,GAAG,IAAI,IAAI,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAC7B,SAAiB,EACjB,cAAsB,EACtB,MAAoB;IAEpB,MAAM,OAAO,GAAG,GAAG,SAAS,WAAW,cAAc,EAAE,CAAC;IAExD,+BAA+B;IAC/B,MAAM,CAAC,GAAG,gBAAgB,CAAC;IAC3B,MAAM,GAAG,GAAG,SAAS,CAAC;IACtB,MAAM,CAAC,GAAG,SAAS,CAAC;IAEpB,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,sCAAsC,CAAC,EAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,sCAAsC,CAAC,EAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,sCAAsC,CAAC,EAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,sCAAsC,CAAC,EAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,sCAAsC,CAAC,EAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,sCAAsC,CAAC,EAAE,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,yBAAyB,CAAC,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAEhB,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,IAAY,EAAE,EAAE;QACzD,qFAAqF;QACrF,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,qBAAqB,GAAG,yBAAyB,CAAC,EAAE,CAAC,CAAC;QACtE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,SAAS,EAAE,CAAC,CAAC;QAC7C,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,cAAc,EAAE,CAAC,CAAC;QAClD,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,kBAAkB,EAAE,CAAC,CAAC;QAClI,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAChB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,iDAAiD,CAAC,EAAE,CAAC,CAAC;YACtE,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,sDAAsD,CAAC,EAAE,CAAC,CAAC;QAC/E,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,iEAAiE;AAEjE;;;GAGG;AACH,KAAK,UAAU,aAAa;IAC1B,IAAI,UAAU,IAAI,CAAC,aAAa;QAAE,OAAO,KAAK,CAAC;IAC/C,IAAI,aAAa,CAAC,MAAM,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAClD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,aAAa,CAAC,GAAG,aAAa,EAAE;YACzD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;SACnC,CAAC,CAAC;QACH,OAAO,GAAG,CAAC,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,QAAQ;IACrB,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,WAAW,EAAE,CAAC;IACpB,OAAO,YAAY,CACjB,WAAW,CAAC,IAAI,EAChB,WAAW,CAAC,cAAc,EAC1B,WAAW,CAAC,iBAAiB,EAC7B,WAAW,CAAC,YAAY,CACzB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAAwD;IAExD,MAAM,WAAW,GAAG,KAAK,CAAC;IAC1B,MAAM,iBAAiB,GAAG,MAAM,CAAC;IACjC,IAAI,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC1B,IAAI,UAAU,GAAG,KAAK,CAAC;IAEvB,MAAM,KAAK,GAAG,KAAK,IAAI,EAAE;QACvB,IAAI,UAAU;YAAE,OAAO;QAEvB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,GAAG,GAAG,QAAQ,GAAG,WAAW,CAAC;QACzC,QAAQ,GAAG,GAAG,CAAC;QAEf,MAAM,IAAI,GAAG,GAAG,GAAG,iBAAiB,CAAC;QACrC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAEjC,UAAU,GAAG,IAAI,CAAC;QAElB,IAAI,CAAC;YACH,IAAI,IAAI,EAAE,CAAC;gBACT,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,WAAW,CAAC,GAAG,IAAI,CAAC,4BAA4B,CAAC,CAAC;gBACpG,sDAAsD;gBACtD,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;YACvD,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;gBACpD,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;YACvD,CAAC;YAED,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,aAAa,EAAE,CAAC;YAEzD,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YAC7B,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;gBAC5C,MAAM,MAAM,GAAG,MAAM,QAAQ,EAAE,CAAC;gBAChC,IAAI,MAAM,EAAE,CAAC;oBACX,OAAO,CAAC,GAAG,CAAC,uBAAuB,MAAM,CAAC,GAAG,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;oBACpE,WAAW,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;gBACzC,CAAC;YACH,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,GAAG,CAAC,CAAC;QAClD,CAAC;QAED,UAAU,GAAG,KAAK,CAAC;IACrB,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,KAAK,EAAE,EAAE,WAAW,CAAC,CAAC;IAC3D,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,mBAAmB,EAAE,CAAC;QACxB,IAAI,CAAC;YAAC,MAAM,KAAK,CAAC,UAAU,EAAE,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QACxD,mBAAmB,GAAG,IAAI,CAAC;IAC7B,CAAC;IACD,IAAI,gBAAgB,EAAE,CAAC;QACrB,gBAAgB,CAAC,IAAI,EAAE,CAAC;QACxB,gBAAgB,GAAG,IAAI,CAAC;IAC1B,CAAC;IACD,UAAU,GAAG,KAAK,CAAC;AACrB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAmC;IAC1E,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;QACxC,OAAO,CAAC,GAAG,CAAC,gBAAgB,MAAM,oBAAoB,CAAC,CAAC;QACxD,IAAI,CAAC;YACH,MAAM,OAAO,EAAE,CAAC;YAChB,MAAM,WAAW,EAAE,CAAC;QACtB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;AACxD,CAAC"}
|