9remote 0.1.64 → 2.0.2
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/README.md +179 -62
- package/cli/index.js +746 -296
- package/cli/utils/assets/trayIcon.ico +0 -0
- package/cli/utils/cloudflared.js +72 -36
- package/cli/utils/permissions.js +5 -5
- package/cli/utils/pids.js +114 -0
- package/cli/utils/token.js +3 -1
- package/cli/utils/tray.js +251 -0
- package/cli/utils/tui.js +231 -37
- package/cli/utils/updateChecker.js +107 -13
- package/dist/assets/trayIcon.ico +0 -0
- package/dist/cli.cjs +1 -36
- package/dist/ptyDaemon.cjs +1 -10
- package/dist/server.cjs +2 -184
- package/dist/ui/assets/index-BWfJSBGG.js +8 -0
- package/dist/ui/assets/{index-BfTPkO8b.css → index-COWVKicT.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/index.js +176 -618
- package/lib/constants.js +64 -0
- package/lib/deviceApproval.js +152 -0
- package/lib/router.js +134 -0
- package/lib/socketio.js +181 -25
- package/package.json +6 -1
- package/dist/ui/assets/index-B37vtDoz.js +0 -8
package/index.js
CHANGED
|
@@ -1,717 +1,275 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Server entry point - Socket.IO
|
|
2
|
+
* Server entry point - config-driven HTTP router + Socket.IO
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { createServer, request as httpRequest } from "http";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
|
|
6
|
+
import { exec, spawn } from "child_process";
|
|
7
|
+
import { readFileSync, existsSync } from "fs";
|
|
9
8
|
import { join, extname } from "path";
|
|
10
9
|
import { fileURLToPath } from "url";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
|
|
12
|
+
import { createRouter, jsonOk, jsonErr } from "./lib/router.js";
|
|
13
|
+
import { STEP, browserFetch, PERMISSION_POLL_MS } from "./lib/constants.js";
|
|
11
14
|
import { setupSocketIO, getIO } from "./lib/socketio.js";
|
|
12
|
-
import { handleLocalSites } from "./api/localSites.js";
|
|
13
15
|
import { setCorsHeaders, handlePreflight } from "./middleware/cors.js";
|
|
14
16
|
import { createProxyServer, handleProxyRequest, startProxySession, endProxySession } from "./proxy/index.js";
|
|
15
17
|
import { initializeTerminal } from "./features/terminal/terminalSocket.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
import { handleLocalSites } from "./api/localSites.js";
|
|
19
|
+
import { verifyApiKeyCrc } from "./cli/utils/apiKey.js";
|
|
20
|
+
import { isNewerVersion } from "./cli/utils/updateChecker.js";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
loadUiState, loadDesktopState, refreshPermissionsAsync, pushUiEvent, setRemoteAvailable,
|
|
24
|
+
handleSseEvents, handleStateGet, handleStatePost,
|
|
25
|
+
handleStop, handleStart, handleShutdown,
|
|
26
|
+
handleConnections, handleDesktopToggle,
|
|
27
|
+
handlePermissionsGet, handlePermissionsRequest,
|
|
28
|
+
} from "./api/ui.js";
|
|
29
|
+
import { handleOneTimeKey, handleRegenerate } from "./api/key.js";
|
|
30
|
+
import { handleApprove, handleReject, handlePending, handleApproved, handleRemove, handleDisconnect, handleRejected, handleApproveRejected, handleClearRejected, handleGetAutoApprove, handleSetAutoApprove } from "./api/device.js";
|
|
31
|
+
import { handleNotifyPost, handleNotifyGet } from "./api/notify.js";
|
|
23
32
|
|
|
24
33
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
25
34
|
const IS_DEV = process.env.NODE_ENV === "development";
|
|
26
35
|
const VITE_PORT = 5173;
|
|
36
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org/9remote/latest";
|
|
27
37
|
|
|
28
|
-
// UI dist: bundled → cli/dist/ui/, dev → server/ui/dist/
|
|
29
38
|
const UI_DIST = existsSync(join(__dirname, "ui", "dist"))
|
|
30
39
|
? join(__dirname, "ui", "dist")
|
|
31
40
|
: join(__dirname, "ui");
|
|
32
41
|
|
|
33
42
|
const MIME_TYPES = {
|
|
34
|
-
".html": "text/html",
|
|
35
|
-
".
|
|
36
|
-
".
|
|
37
|
-
".svg": "image/svg+xml",
|
|
38
|
-
".png": "image/png",
|
|
39
|
-
".ico": "image/x-icon",
|
|
40
|
-
".woff2": "font/woff2",
|
|
41
|
-
".woff": "font/woff",
|
|
43
|
+
".html": "text/html", ".js": "application/javascript", ".css": "text/css",
|
|
44
|
+
".svg": "image/svg+xml", ".png": "image/png", ".ico": "image/x-icon",
|
|
45
|
+
".woff2": "font/woff2", ".woff": "font/woff",
|
|
42
46
|
};
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`;
|
|
48
|
+
// ── Static / Dev helpers ─────────────────────────────────────────
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
function proxyToVite(req, res) {
|
|
50
|
+
function proxyToVite(req, res, fallbackFn) {
|
|
49
51
|
const proxy = httpRequest(
|
|
50
52
|
{ hostname: "localhost", port: VITE_PORT, path: req.url, method: req.method, headers: req.headers },
|
|
51
|
-
(proxyRes) => {
|
|
52
|
-
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
53
|
-
proxyRes.pipe(res);
|
|
54
|
-
}
|
|
53
|
+
(proxyRes) => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }
|
|
55
54
|
);
|
|
56
|
-
proxy.on("error", () => {
|
|
57
|
-
res.writeHead(502);
|
|
58
|
-
res.end("Vite dev server not ready yet, please wait...");
|
|
59
|
-
});
|
|
55
|
+
proxy.on("error", () => fallbackFn ? fallbackFn() : (() => { res.writeHead(502); res.end("Vite dev server not ready"); })());
|
|
60
56
|
req.pipe(proxy);
|
|
61
57
|
}
|
|
62
58
|
|
|
63
|
-
/** Serve a static file from ui/dist */
|
|
64
59
|
function serveStatic(res, filePath) {
|
|
65
60
|
if (!existsSync(filePath)) return false;
|
|
66
|
-
const
|
|
67
|
-
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
61
|
+
const mime = MIME_TYPES[extname(filePath)] || "application/octet-stream";
|
|
68
62
|
res.setHeader("Content-Type", mime);
|
|
69
63
|
res.writeHead(200);
|
|
70
64
|
res.end(readFileSync(filePath));
|
|
71
65
|
return true;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
/** Check npm registry for newer version, push via SSE if found */
|
|
75
68
|
async function checkForUpdate(currentVersion) {
|
|
76
69
|
try {
|
|
77
|
-
const res = await
|
|
70
|
+
const res = await browserFetch(NPM_REGISTRY_URL);
|
|
78
71
|
if (!res.ok) return;
|
|
79
72
|
const { version } = await res.json();
|
|
80
|
-
if (version && version
|
|
81
|
-
pushUiEvent("updateAvailable", { version });
|
|
82
|
-
}
|
|
73
|
+
if (version && isNewerVersion(currentVersion, version)) pushUiEvent("updateAvailable", { version });
|
|
83
74
|
} catch { /* non-critical */ }
|
|
84
75
|
}
|
|
85
76
|
|
|
86
|
-
// ──
|
|
87
|
-
|
|
88
|
-
const UI_STATE_FILE = join(
|
|
89
|
-
process.env.HOME || process.env.USERPROFILE || ".",
|
|
90
|
-
".9remote", "ui-state.json"
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
/** Persist uiState to disk */
|
|
94
|
-
function saveUiState() {
|
|
95
|
-
try {
|
|
96
|
-
mkdirSync(join(process.env.HOME || process.env.USERPROFILE || ".", ".9remote"), { recursive: true });
|
|
97
|
-
writeFileSync(UI_STATE_FILE, JSON.stringify(uiState));
|
|
98
|
-
} catch {}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Load uiState from disk */
|
|
102
|
-
function loadUiState() {
|
|
103
|
-
try {
|
|
104
|
-
if (existsSync(UI_STATE_FILE)) {
|
|
105
|
-
const saved = JSON.parse(readFileSync(UI_STATE_FILE, "utf8"));
|
|
106
|
-
// Only restore if tunnel was ready — otherwise start fresh
|
|
107
|
-
if (saved.step === 4 && saved.permanentKey) {
|
|
108
|
-
uiState = { ...uiState, ...saved };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
} catch {}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// In-memory UI state + SSE clients
|
|
115
|
-
let uiState = { step: 0, tunnelUrl: "", oneTimeKey: "", oneTimeKeyExpiresAt: null, permanentKey: "", qrUrl: "", latency: null, uptime: null };
|
|
116
|
-
const sseClients = new Set();
|
|
117
|
-
|
|
118
|
-
// Active socket connections for UI display
|
|
119
|
-
const activeConnections = new Map();
|
|
120
|
-
|
|
121
|
-
// Remote desktop toggle state — loaded from state file on start
|
|
122
|
-
let desktopEnabled = false;
|
|
123
|
-
|
|
124
|
-
const DESKTOP_STATE_FILE = join(
|
|
125
|
-
process.env.HOME || process.env.USERPROFILE || ".",
|
|
126
|
-
".9remote", "desktop.json"
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
/** Load desktopEnabled from state file */
|
|
130
|
-
function loadDesktopState() {
|
|
131
|
-
try {
|
|
132
|
-
if (existsSync(DESKTOP_STATE_FILE)) {
|
|
133
|
-
const data = JSON.parse(readFileSync(DESKTOP_STATE_FILE, "utf8"));
|
|
134
|
-
desktopEnabled = !!data.enabled;
|
|
135
|
-
}
|
|
136
|
-
} catch {}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Save desktopEnabled to state file */
|
|
140
|
-
function saveDesktopState() {
|
|
141
|
-
try {
|
|
142
|
-
mkdirSync(join(process.env.HOME || process.env.USERPROFILE || ".", ".9remote"), { recursive: true });
|
|
143
|
-
writeFileSync(DESKTOP_STATE_FILE, JSON.stringify({ enabled: desktopEnabled }));
|
|
144
|
-
} catch {}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Cached permissions — refreshed async, never blocks request
|
|
148
|
-
let cachedPermissions = { screenRecording: false, accessibility: false };
|
|
149
|
-
|
|
150
|
-
function refreshPermissionsAsync() {
|
|
151
|
-
checkPermissions().then((p) => { cachedPermissions = p; });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function getSystemPermissions() {
|
|
155
|
-
return cachedPermissions;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Open System Preferences pane for a permission, then poll until granted */
|
|
159
|
-
function requestSystemPermission(type) {
|
|
160
|
-
return new Promise((resolve) => {
|
|
161
|
-
if (process.platform !== "darwin") { resolve(); return; }
|
|
162
|
-
openPermissionPane(type);
|
|
163
|
-
resolve(); // resolve immediately — UI stays open
|
|
164
|
-
|
|
165
|
-
// Poll every 2s for up to 60s to detect when user grants permission
|
|
166
|
-
let attempts = 0;
|
|
167
|
-
const poll = setInterval(() => {
|
|
168
|
-
attempts++;
|
|
169
|
-
checkPermissions().then((p) => {
|
|
170
|
-
cachedPermissions = p;
|
|
171
|
-
if (p[type] || attempts >= 30) {
|
|
172
|
-
clearInterval(poll);
|
|
173
|
-
pushUiEvent("permissions", { ...cachedPermissions, desktopEnabled });
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
}, 2000);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Clear one-time key from UI state after client uses it */
|
|
181
|
-
export function clearOneTimeKey() {
|
|
182
|
-
uiState = { ...uiState, oneTimeKey: "", oneTimeKeyExpiresAt: null, qrUrl: "" };
|
|
183
|
-
pushUiEvent("state", uiState);
|
|
184
|
-
saveUiState();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Track a new socket connection */
|
|
188
|
-
export function trackConnection(socketId, ip, type = "ws") {
|
|
189
|
-
activeConnections.set(socketId, { ip, type, connectedAt: Date.now() });
|
|
190
|
-
pushUiEvent("connections", { connections: [...activeConnections.values()] });
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Remove a socket connection */
|
|
194
|
-
export function untrackConnection(socketId) {
|
|
195
|
-
activeConnections.delete(socketId);
|
|
196
|
-
pushUiEvent("connections", { connections: [...activeConnections.values()] });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Push a log line to UI */
|
|
200
|
-
export function pushUiLog(message) {
|
|
201
|
-
pushUiEvent("log", { message: `[${new Date().toLocaleTimeString()}] ${message}` });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** Push event to all connected UI SSE clients */
|
|
205
|
-
function pushUiEvent(type, data) {
|
|
206
|
-
const payload = `data: ${JSON.stringify({ type, ...data })}\n\n`;
|
|
207
|
-
for (const res of sseClients) {
|
|
208
|
-
try { res.write(payload); } catch { sseClients.delete(res); }
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Handle SSE connection from UI */
|
|
213
|
-
function handleUiEvents(req, res) {
|
|
214
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
215
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
216
|
-
res.setHeader("Connection", "keep-alive");
|
|
217
|
-
res.writeHead(200);
|
|
77
|
+
// ── Codespace handler ────────────────────────────────────────
|
|
218
78
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Handle UI state update from CLI */
|
|
229
|
-
function handleUiState(body) {
|
|
230
|
-
try {
|
|
231
|
-
const data = JSON.parse(body || "{}");
|
|
232
|
-
uiState = { ...uiState, ...data };
|
|
233
|
-
pushUiEvent("state", uiState);
|
|
234
|
-
saveUiState();
|
|
235
|
-
} catch { /* ignore malformed */ }
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const ORANGE = chalk.rgb(230, 138, 110);
|
|
239
|
-
|
|
240
|
-
function isCodespaces() {
|
|
241
|
-
return process.env.CODESPACES === "true";
|
|
79
|
+
function handleCodespaceStop(req, res) {
|
|
80
|
+
if (process.env.CODESPACES !== "true") { jsonErr(res, 400, "Not running on Codespaces"); return; }
|
|
81
|
+
const name = process.env.CODESPACE_NAME;
|
|
82
|
+
if (!name) { jsonErr(res, 400, "Codespace name not found"); return; }
|
|
83
|
+
const io = getIO();
|
|
84
|
+
if (io) io.emit("codespace:stopping");
|
|
85
|
+
jsonOk(res, { success: true, message: "Stopping codespace..." });
|
|
86
|
+
setTimeout(() => exec(`gh codespace stop -c ${name}`, { windowsHide: true }), 500);
|
|
242
87
|
}
|
|
243
88
|
|
|
244
|
-
|
|
245
|
-
if (req.method !== "POST") {
|
|
246
|
-
res.writeHead(405);
|
|
247
|
-
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
89
|
+
// ── Proxy handlers ────────────────────────────────────────────
|
|
250
90
|
|
|
251
|
-
|
|
252
|
-
res.writeHead(400);
|
|
253
|
-
res.end(JSON.stringify({ error: "Not running on Codespaces" }));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
91
|
+
let proxyServer;
|
|
256
92
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
93
|
+
async function handleProxyStartEnd(req, res, { pathname }) {
|
|
94
|
+
// Verify API key (required for tunnel access)
|
|
95
|
+
const authHeader = req.headers.authorization;
|
|
96
|
+
if (!authHeader || !authHeader.startsWith("Bearer ") || !verifyApiKeyCrc(authHeader.slice(7))) {
|
|
97
|
+
jsonErr(res, 401, "Unauthorized");
|
|
261
98
|
return;
|
|
262
99
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
res.setHeader("Content-Type", "application/json");
|
|
271
|
-
res.writeHead(200);
|
|
272
|
-
res.end(JSON.stringify({ success: true, message: "Stopping codespace..." }));
|
|
273
|
-
|
|
274
|
-
setTimeout(() => {
|
|
275
|
-
exec(`gh codespace stop -c ${codespaceName}`, (error) => {
|
|
276
|
-
if (error) {
|
|
277
|
-
console.error("Failed to stop codespace:", error);
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
}, 500);
|
|
100
|
+
const { parseJsonBody } = await import("./lib/router.js");
|
|
101
|
+
const data = await parseJsonBody(req, res);
|
|
102
|
+
if (!data) return;
|
|
103
|
+
if (!data.port) { jsonErr(res, 400, "Port required"); return; }
|
|
104
|
+
pathname.endsWith("start") ? startProxySession(data.port) : endProxySession(data.port);
|
|
105
|
+
jsonOk(res, { success: true });
|
|
281
106
|
}
|
|
282
107
|
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const pushLastTime = {};
|
|
288
|
-
const PUSH_RATE_LIMIT_MS = 10000;
|
|
289
|
-
|
|
290
|
-
function handleNotify(req, res, body, query) {
|
|
291
|
-
const now = Date.now();
|
|
292
|
-
|
|
293
|
-
let type, sessionId, tool;
|
|
294
|
-
|
|
295
|
-
if (query) {
|
|
296
|
-
type = query.type || "stop";
|
|
297
|
-
sessionId = query.sessionId || "";
|
|
298
|
-
tool = query.tool || "claude";
|
|
108
|
+
function handleProxy(req, res, { pathname, search }) {
|
|
109
|
+
const match = pathname.match(/^\/proxy\/(\d+)(\/.*)?$/);
|
|
110
|
+
if (match) {
|
|
111
|
+
handleProxyRequest(proxyServer, req, res, match[1], match[2] || "/", search);
|
|
299
112
|
} else {
|
|
300
|
-
|
|
301
|
-
const data = JSON.parse(body || "{}");
|
|
302
|
-
type = data.type || "stop";
|
|
303
|
-
sessionId = data.sessionId || "";
|
|
304
|
-
tool = data.tool || "claude";
|
|
305
|
-
} catch {
|
|
306
|
-
res.writeHead(400);
|
|
307
|
-
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
113
|
+
jsonErr(res, 404, "Not found");
|
|
310
114
|
}
|
|
115
|
+
}
|
|
311
116
|
|
|
117
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
// ROUTE TABLE — single source of truth for all HTTP endpoints
|
|
119
|
+
// public: true = accessible via tunnel, false/omit = localhost-only
|
|
120
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
const ROUTES = [
|
|
123
|
+
// Public routes (accessible via tunnel)
|
|
124
|
+
{ path: "/api/health", method: "GET", public: true, handler: (req, res) => jsonOk(res, { status: "ok", timestamp: Date.now() }) },
|
|
125
|
+
{ path: "/api/version", method: "GET", public: true, handler: (req, res) => {
|
|
126
|
+
const version = typeof __CLI_VERSION__ !== "undefined"
|
|
127
|
+
? __CLI_VERSION__
|
|
128
|
+
: JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8")).version;
|
|
129
|
+
jsonOk(res, { version });
|
|
130
|
+
}},
|
|
131
|
+
{ path: "/api/notify", method: "POST", public: true, handler: handleNotifyPost },
|
|
132
|
+
{ path: "/api/notify", method: "GET", public: true, handler: handleNotifyGet },
|
|
133
|
+
{ path: "/proxy/*", method: "*", public: true, handler: handleProxy },
|
|
134
|
+
|
|
135
|
+
// UI state & SSE (localhost-only)
|
|
136
|
+
{ path: "/api/ui/events", method: "GET", handler: handleSseEvents },
|
|
137
|
+
{ path: "/api/ui/state", method: "GET", handler: handleStateGet },
|
|
138
|
+
{ path: "/api/ui/state", method: "POST", handler: handleStatePost },
|
|
139
|
+
{ path: "/api/ui/stop", method: "POST", handler: handleStop },
|
|
140
|
+
{ path: "/api/ui/start", method: "POST", handler: handleStart },
|
|
141
|
+
{ path: "/api/ui/shutdown", method: "POST", handler: handleShutdown },
|
|
142
|
+
|
|
143
|
+
// Key management (localhost-only)
|
|
144
|
+
{ path: "/api/key/one-time", method: "POST", handler: handleOneTimeKey },
|
|
145
|
+
{ path: "/api/key/regenerate", method: "POST", handler: handleRegenerate },
|
|
146
|
+
|
|
147
|
+
// Device approval (localhost-only)
|
|
148
|
+
{ path: "/api/device/approve", method: "POST", handler: handleApprove },
|
|
149
|
+
{ path: "/api/device/reject", method: "POST", handler: handleReject },
|
|
150
|
+
{ path: "/api/device/pending", method: "GET", handler: handlePending },
|
|
151
|
+
{ path: "/api/device/approved", method: "GET", handler: handleApproved },
|
|
152
|
+
{ path: "/api/device/rejected", method: "GET", handler: handleRejected },
|
|
153
|
+
{ path: "/api/device/approve-rejected", method: "POST", handler: handleApproveRejected },
|
|
154
|
+
{ path: "/api/device/clear-rejected", method: "POST", handler: handleClearRejected },
|
|
155
|
+
{ path: "/api/device/remove", method: "POST", handler: handleRemove },
|
|
156
|
+
{ path: "/api/device/disconnect", method: "POST", handler: handleDisconnect },
|
|
157
|
+
{ path: "/api/device/auto-approve", method: "GET", handler: handleGetAutoApprove },
|
|
158
|
+
{ path: "/api/device/auto-approve", method: "POST", handler: handleSetAutoApprove },
|
|
159
|
+
|
|
160
|
+
// System (localhost-only)
|
|
161
|
+
{ path: "/api/connections", method: "GET", handler: handleConnections },
|
|
162
|
+
{ path: "/api/permissions", method: "GET", handler: handlePermissionsGet },
|
|
163
|
+
{ path: "/api/permissions/request", method: "POST", handler: handlePermissionsRequest },
|
|
164
|
+
{ path: "/api/desktop/toggle", method: "POST", handler: handleDesktopToggle },
|
|
165
|
+
{ path: "/api/local-sites", method: "*", public: true, handler: handleLocalSites },
|
|
166
|
+
{ path: "/api/codespace/stop", method: "POST", handler: handleCodespaceStop },
|
|
167
|
+
|
|
168
|
+
// Proxy session management (tunnel-accessible, requires API key)
|
|
169
|
+
{ path: "/api/proxy/start", method: "POST", public: true, handler: handleProxyStartEnd },
|
|
170
|
+
{ path: "/api/proxy/end", method: "POST", public: true, handler: handleProxyStartEnd },
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
// ── Server bootstrap ───────────────────────────────────────────────────────
|
|
312
174
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const notification = { type, sessionId, tool, timestamp: now };
|
|
316
|
-
|
|
317
|
-
if (sessionId)
|
|
318
|
-
|
|
319
|
-
// Always persist badge and notify all clients (no throttle)
|
|
320
|
-
if (sessionId) {
|
|
321
|
-
addNotification(sessionId, notification);
|
|
322
|
-
io.emit("chatNotification", notification);
|
|
323
|
-
|
|
324
|
-
// PWA push: throttle per tool+type to avoid spam
|
|
325
|
-
const pushKey = `${tool}:${type}`;
|
|
326
|
-
const pushThrottled = now - (pushLastTime[pushKey] || 0) < PUSH_RATE_LIMIT_MS;
|
|
327
|
-
if (!pushThrottled) {
|
|
328
|
-
pushLastTime[pushKey] = now;
|
|
329
|
-
io.timeout(5000).emit("chatNotificationAck", notification, (err, responses) => {
|
|
330
|
-
const connectedCount = io.sockets.sockets.size;
|
|
331
|
-
const hasFocused = !err && responses && responses.length > 0;
|
|
332
|
-
if (!hasFocused && connectedCount === 0) sendPushNotification(notification);
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
175
|
+
const hostname = "localhost";
|
|
176
|
+
const port = parseInt(process.env.PORT || "2208", 10);
|
|
337
177
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
178
|
+
// Auto-start Vite dev server in dev mode
|
|
179
|
+
let viteProcess = null;
|
|
180
|
+
function startViteDev() {
|
|
181
|
+
if (!IS_DEV) return;
|
|
182
|
+
const viteConfigPath = join(__dirname, "vite.config.js");
|
|
183
|
+
if (!existsSync(viteConfigPath)) return;
|
|
184
|
+
const viteBin = existsSync(join(__dirname, "node_modules", ".bin", "vite"))
|
|
185
|
+
? join(__dirname, "node_modules", ".bin", "vite")
|
|
186
|
+
: join(__dirname, "..", "node_modules", ".bin", "vite");
|
|
187
|
+
viteProcess = spawn("node", [viteBin, "--config", viteConfigPath], {
|
|
188
|
+
cwd: __dirname,
|
|
189
|
+
stdio: "ignore",
|
|
190
|
+
detached: false,
|
|
191
|
+
});
|
|
192
|
+
viteProcess.on("error", () => {});
|
|
193
|
+
viteProcess.unref();
|
|
341
194
|
}
|
|
342
195
|
|
|
343
196
|
export async function startServer() {
|
|
344
|
-
// Initialize terminal sessions
|
|
345
197
|
await initializeTerminal();
|
|
198
|
+
proxyServer = createProxyServer();
|
|
199
|
+
startViteDev();
|
|
346
200
|
|
|
347
|
-
|
|
201
|
+
// Static file / Vite dev fallback (localhost-only)
|
|
202
|
+
const staticFallback = (req, res, { pathname }) => {
|
|
203
|
+
if (pathname.startsWith("/api/") || pathname.startsWith("/socket.io")) {
|
|
204
|
+
jsonErr(res, 404, "Not found");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const isTunnel = !!req.headers["cf-connecting-ip"];
|
|
208
|
+
const isLocal = req.socket.remoteAddress === "127.0.0.1" || req.socket.remoteAddress === "::1";
|
|
209
|
+
if (isTunnel || !isLocal) { jsonErr(res, 403, "Forbidden"); return; }
|
|
210
|
+
const serveStaticFiles = () => {
|
|
211
|
+
const filePath = (pathname === "/" || pathname === "") ? join(UI_DIST, "index.html") : join(UI_DIST, pathname);
|
|
212
|
+
if (serveStatic(res, filePath)) return;
|
|
213
|
+
serveStatic(res, join(UI_DIST, "index.html"));
|
|
214
|
+
};
|
|
215
|
+
if (IS_DEV) { proxyToVite(req, res, serveStaticFiles); return; }
|
|
216
|
+
serveStaticFiles();
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const router = createRouter(ROUTES, { fallback: staticFallback });
|
|
348
220
|
|
|
349
221
|
const server = createServer(async (req, res) => {
|
|
350
222
|
setCorsHeaders(res);
|
|
351
|
-
|
|
352
223
|
if (handlePreflight(req, res)) return;
|
|
353
|
-
|
|
354
|
-
try {
|
|
355
|
-
const parsedUrl = parse(req.url, true);
|
|
356
|
-
const { pathname, search } = parsedUrl;
|
|
357
|
-
|
|
358
|
-
// UI routes: only accessible from localhost (not via tunnel)
|
|
359
|
-
const isLocalhost = req.socket.remoteAddress === "127.0.0.1" || req.socket.remoteAddress === "::1";
|
|
360
|
-
const isUiRoute = pathname === "/api/ui/events" || pathname === "/api/ui/state"
|
|
361
|
-
|| (!pathname.startsWith("/api/") && !pathname.startsWith("/proxy/") && !pathname.startsWith("/socket.io"));
|
|
362
|
-
if (isUiRoute && !isLocalhost) {
|
|
363
|
-
res.writeHead(403);
|
|
364
|
-
res.end(JSON.stringify({ error: "forbidden" }));
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// UI SSE stream
|
|
369
|
-
if (pathname === "/api/ui/events") {
|
|
370
|
-
handleUiEvents(req, res);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// UI state update from CLI
|
|
375
|
-
if (pathname === "/api/ui/state" && req.method === "POST") {
|
|
376
|
-
let body = "";
|
|
377
|
-
req.on("data", chunk => body += chunk);
|
|
378
|
-
req.on("end", () => {
|
|
379
|
-
handleUiState(body);
|
|
380
|
-
res.setHeader("Content-Type", "application/json");
|
|
381
|
-
res.writeHead(200);
|
|
382
|
-
res.end(JSON.stringify({ ok: true }));
|
|
383
|
-
});
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// UI state snapshot
|
|
388
|
-
if (pathname === "/api/ui/state" && req.method === "GET") {
|
|
389
|
-
res.setHeader("Content-Type", "application/json");
|
|
390
|
-
res.writeHead(200);
|
|
391
|
-
// Include permissions + desktopEnabled so UI only needs 1 fetch
|
|
392
|
-
res.end(JSON.stringify({
|
|
393
|
-
...uiState,
|
|
394
|
-
...getSystemPermissions(),
|
|
395
|
-
desktopEnabled,
|
|
396
|
-
}));
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Stop tunnel (disconnect), keep server alive
|
|
401
|
-
if (pathname === "/api/ui/stop" && req.method === "POST") {
|
|
402
|
-
res.setHeader("Content-Type", "application/json");
|
|
403
|
-
res.writeHead(200);
|
|
404
|
-
res.end(JSON.stringify({ ok: true }));
|
|
405
|
-
// Immediately reset state so UI transitions to welcome screen
|
|
406
|
-
uiState = { ...uiState, step: 0, tunnelUrl: "", oneTimeKey: "", oneTimeKeyExpiresAt: null };
|
|
407
|
-
pushUiEvent("state", uiState);
|
|
408
|
-
saveUiState();
|
|
409
|
-
writeCmd("stop-tunnel");
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Start tunnel (reconnect)
|
|
414
|
-
if (pathname === "/api/ui/start" && req.method === "POST") {
|
|
415
|
-
res.setHeader("Content-Type", "application/json");
|
|
416
|
-
res.writeHead(200);
|
|
417
|
-
res.end(JSON.stringify({ ok: true }));
|
|
418
|
-
// Immediately push step:1 so UI transitions to progress screen
|
|
419
|
-
uiState = { ...uiState, step: 1 };
|
|
420
|
-
pushUiEvent("state", uiState);
|
|
421
|
-
saveUiState();
|
|
422
|
-
writeCmd("start-tunnel");
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Generate new one-time key
|
|
427
|
-
if (pathname === "/api/key/one-time" && req.method === "POST") {
|
|
428
|
-
const workerUrl = uiState.workerUrl || "https://9remote.cc";
|
|
429
|
-
const permanentKey = uiState.permanentKey;
|
|
430
|
-
if (!permanentKey) {
|
|
431
|
-
res.writeHead(400);
|
|
432
|
-
res.end(JSON.stringify({ error: "No permanent key set" }));
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
try {
|
|
436
|
-
const r = await fetch(`${workerUrl}/api/temp-key/create`, {
|
|
437
|
-
method: "POST",
|
|
438
|
-
headers: { "Content-Type": "application/json" },
|
|
439
|
-
body: JSON.stringify({ apiKey: permanentKey, expiryMinutes: 30 }),
|
|
440
|
-
});
|
|
441
|
-
const data = await r.json();
|
|
442
|
-
const qrUrl = `${workerUrl}/login?k=${data.tempKey}`;
|
|
443
|
-
uiState = { ...uiState, oneTimeKey: data.tempKey, oneTimeKeyExpiresAt: data.expiresAt, qrUrl };
|
|
444
|
-
pushUiEvent("state", uiState);
|
|
445
|
-
res.setHeader("Content-Type", "application/json");
|
|
446
|
-
res.writeHead(200);
|
|
447
|
-
res.end(JSON.stringify({ oneTimeKey: data.tempKey, expiresAt: data.expiresAt, qrUrl }));
|
|
448
|
-
} catch (err) {
|
|
449
|
-
res.writeHead(500);
|
|
450
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
451
|
-
}
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Regenerate permanent key
|
|
456
|
-
if (pathname === "/api/key/regenerate" && req.method === "POST") {
|
|
457
|
-
res.setHeader("Content-Type", "application/json");
|
|
458
|
-
try {
|
|
459
|
-
const machineId = await getConsistentMachineId();
|
|
460
|
-
const { key } = generateApiKeyWithMachine(machineId);
|
|
461
|
-
const existing = loadKey();
|
|
462
|
-
saveKey(machineId, key, existing?.name || "Default");
|
|
463
|
-
pushUiEvent("state", { ...uiState, permanentKey: key });
|
|
464
|
-
uiState = { ...uiState, permanentKey: key };
|
|
465
|
-
res.writeHead(200);
|
|
466
|
-
res.end(JSON.stringify({ ok: true, permanentKey: key }));
|
|
467
|
-
} catch (err) {
|
|
468
|
-
res.writeHead(500);
|
|
469
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
470
|
-
}
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Get system permissions status
|
|
475
|
-
if (pathname === "/api/permissions" && req.method === "GET") {
|
|
476
|
-
res.setHeader("Content-Type", "application/json");
|
|
477
|
-
res.writeHead(200);
|
|
478
|
-
res.end(JSON.stringify(getSystemPermissions()));
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Request a system permission
|
|
483
|
-
if (pathname === "/api/permissions/request" && req.method === "POST") {
|
|
484
|
-
let body = "";
|
|
485
|
-
req.on("data", (chunk) => body += chunk);
|
|
486
|
-
req.on("end", async () => {
|
|
487
|
-
try {
|
|
488
|
-
const { type } = JSON.parse(body || "{}");
|
|
489
|
-
await requestSystemPermission(type);
|
|
490
|
-
res.setHeader("Content-Type", "application/json");
|
|
491
|
-
res.writeHead(200);
|
|
492
|
-
res.end(JSON.stringify({ ok: true }));
|
|
493
|
-
} catch (err) {
|
|
494
|
-
res.writeHead(500);
|
|
495
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Toggle remote desktop
|
|
502
|
-
if (pathname === "/api/desktop/toggle" && req.method === "POST") {
|
|
503
|
-
let body = "";
|
|
504
|
-
req.on("data", (chunk) => body += chunk);
|
|
505
|
-
req.on("end", () => {
|
|
506
|
-
try {
|
|
507
|
-
const { enabled } = JSON.parse(body || "{}");
|
|
508
|
-
desktopEnabled = !!enabled;
|
|
509
|
-
saveDesktopState();
|
|
510
|
-
// Push updated state including desktopEnabled + fresh permissions
|
|
511
|
-
pushUiEvent("permissions", { ...getSystemPermissions(), desktopEnabled });
|
|
512
|
-
res.setHeader("Content-Type", "application/json");
|
|
513
|
-
res.writeHead(200);
|
|
514
|
-
res.end(JSON.stringify({ ok: true, enabled: desktopEnabled }));
|
|
515
|
-
} catch (err) {
|
|
516
|
-
res.writeHead(500);
|
|
517
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Get active connections
|
|
524
|
-
if (pathname === "/api/connections" && req.method === "GET") {
|
|
525
|
-
res.setHeader("Content-Type", "application/json");
|
|
526
|
-
res.writeHead(200);
|
|
527
|
-
res.end(JSON.stringify({ connections: [...activeConnections.values()] }));
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Health check endpoint
|
|
532
|
-
if (pathname === "/api/health") {
|
|
533
|
-
res.setHeader("Content-Type", "application/json");
|
|
534
|
-
res.writeHead(200);
|
|
535
|
-
res.end(JSON.stringify({ status: "ok", timestamp: Date.now() }));
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Version endpoint
|
|
540
|
-
if (pathname === "/api/version") {
|
|
541
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
|
|
542
|
-
res.setHeader("Content-Type", "application/json");
|
|
543
|
-
res.writeHead(200);
|
|
544
|
-
res.end(JSON.stringify({ version: pkg.version }));
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// Serve UI: proxy to Vite (dev) or serve static (production)
|
|
549
|
-
if (!pathname.startsWith("/api/") && !pathname.startsWith("/proxy/") && !pathname.startsWith("/socket.io")) {
|
|
550
|
-
if (IS_DEV) {
|
|
551
|
-
proxyToVite(req, res);
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
const filePath = pathname === "/" || pathname === ""
|
|
555
|
-
? join(UI_DIST, "index.html")
|
|
556
|
-
: join(UI_DIST, pathname);
|
|
557
|
-
if (serveStatic(res, filePath)) return;
|
|
558
|
-
// SPA fallback
|
|
559
|
-
if (serveStatic(res, join(UI_DIST, "index.html"))) return;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// API routes
|
|
563
|
-
if (pathname === "/api/local-sites") {
|
|
564
|
-
await handleLocalSites(req, res);
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Codespace stop endpoint
|
|
569
|
-
if (pathname === "/api/codespace/stop") {
|
|
570
|
-
await handleCodespaceStop(req, res);
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Notification endpoint (called by AI tool hooks)
|
|
575
|
-
if (pathname === "/api/notify" && req.method === "POST") {
|
|
576
|
-
let body = "";
|
|
577
|
-
req.on("data", chunk => body += chunk);
|
|
578
|
-
req.on("end", () => {
|
|
579
|
-
handleNotify(req, res, body);
|
|
580
|
-
});
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
if (pathname === "/api/notify" && req.method === "GET") {
|
|
585
|
-
handleNotify(req, res, null, parsedUrl.query);
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Proxy session management
|
|
590
|
-
if (pathname === "/api/proxy/start" && req.method === "POST") {
|
|
591
|
-
let body = "";
|
|
592
|
-
req.on("data", chunk => body += chunk);
|
|
593
|
-
req.on("end", () => {
|
|
594
|
-
const { port } = JSON.parse(body || "{}");
|
|
595
|
-
if (port) {
|
|
596
|
-
startProxySession(port);
|
|
597
|
-
res.setHeader("Content-Type", "application/json");
|
|
598
|
-
res.writeHead(200);
|
|
599
|
-
res.end(JSON.stringify({ success: true }));
|
|
600
|
-
} else {
|
|
601
|
-
res.writeHead(400);
|
|
602
|
-
res.end(JSON.stringify({ error: "Port required" }));
|
|
603
|
-
}
|
|
604
|
-
});
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
if (pathname === "/api/proxy/end" && req.method === "POST") {
|
|
609
|
-
let body = "";
|
|
610
|
-
req.on("data", chunk => body += chunk);
|
|
611
|
-
req.on("end", () => {
|
|
612
|
-
const { port } = JSON.parse(body || "{}");
|
|
613
|
-
if (port) {
|
|
614
|
-
endProxySession(port);
|
|
615
|
-
res.setHeader("Content-Type", "application/json");
|
|
616
|
-
res.writeHead(200);
|
|
617
|
-
res.end(JSON.stringify({ success: true }));
|
|
618
|
-
} else {
|
|
619
|
-
res.writeHead(400);
|
|
620
|
-
res.end(JSON.stringify({ error: "Port required" }));
|
|
621
|
-
}
|
|
622
|
-
});
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// Proxy routes
|
|
627
|
-
if (pathname.startsWith("/proxy/")) {
|
|
628
|
-
const match = pathname.match(/^\/proxy\/(\d+)(\/.*)?$/);
|
|
629
|
-
if (match) {
|
|
630
|
-
handleProxyRequest(proxy, req, res, match[1], match[2] || "/", search);
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// 404 for unknown routes
|
|
636
|
-
res.writeHead(404);
|
|
637
|
-
res.end(JSON.stringify({ error: "Not found" }));
|
|
638
|
-
} catch (err) {
|
|
639
|
-
console.error("Error:", req.url, err);
|
|
640
|
-
res.statusCode = 500;
|
|
641
|
-
res.end("Internal server error");
|
|
642
|
-
}
|
|
224
|
+
await router(req, res);
|
|
643
225
|
});
|
|
644
226
|
|
|
645
|
-
// Load persisted states + warm up permission cache
|
|
646
227
|
loadUiState();
|
|
647
228
|
loadDesktopState();
|
|
648
229
|
refreshPermissionsAsync();
|
|
230
|
+
// macOS TCC has no change event — poll to detect permission revoke/grant
|
|
231
|
+
if (process.platform === "darwin") {
|
|
232
|
+
setInterval(refreshPermissionsAsync, PERMISSION_POLL_MS);
|
|
233
|
+
}
|
|
649
234
|
|
|
650
235
|
await setupSocketIO(server);
|
|
651
236
|
|
|
652
237
|
server.listen(port, (err) => {
|
|
653
238
|
if (err) throw err;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
checkForUpdate(pkg.version);
|
|
239
|
+
const version = typeof __CLI_VERSION__ !== "undefined"
|
|
240
|
+
? __CLI_VERSION__
|
|
241
|
+
: JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8")).version;
|
|
242
|
+
checkForUpdate(version);
|
|
659
243
|
});
|
|
660
244
|
|
|
661
|
-
// Graceful shutdown
|
|
245
|
+
// Graceful shutdown
|
|
662
246
|
let isShuttingDown = false;
|
|
663
|
-
|
|
664
247
|
const gracefulShutdown = async (signal) => {
|
|
665
248
|
if (isShuttingDown) return;
|
|
666
249
|
isShuttingDown = true;
|
|
667
|
-
|
|
668
250
|
console.log(chalk.yellow(`\n🛑 Received ${signal}, shutting down gracefully...`));
|
|
669
|
-
|
|
670
|
-
// Set timeout to force exit if cleanup takes too long
|
|
671
|
-
const forceExitTimeout = setTimeout(() => {
|
|
672
|
-
console.log(chalk.red("⚠️ Forced exit after 5s timeout"));
|
|
673
|
-
process.exit(1);
|
|
674
|
-
}, 5000);
|
|
675
|
-
|
|
251
|
+
const forceExit = setTimeout(() => { console.log(chalk.red("⚠️ Forced exit")); process.exit(1); }, 5000);
|
|
676
252
|
try {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
console.log(chalk.gray("✓ HTTP server closed"));
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
// 2. Close all Socket.IO connections
|
|
253
|
+
server.close(() => console.log(chalk.gray("✓ HTTP server closed")));
|
|
254
|
+
if (viteProcess) { try { viteProcess.kill(); } catch {} }
|
|
683
255
|
const io = getIO();
|
|
684
|
-
if (io) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
console.log(chalk.gray("✓ Socket.IO closed"));
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// 3. Wait a bit for cleanup
|
|
692
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
693
|
-
|
|
694
|
-
clearTimeout(forceExitTimeout);
|
|
256
|
+
if (io) { io.emit("server:shutdown"); io.close(() => console.log(chalk.gray("✓ Socket.IO closed"))); }
|
|
257
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
258
|
+
clearTimeout(forceExit);
|
|
695
259
|
console.log(chalk.green("✅ Server stopped cleanly"));
|
|
696
260
|
process.exit(0);
|
|
697
261
|
} catch (error) {
|
|
698
262
|
console.error(chalk.red("❌ Error during shutdown:"), error);
|
|
699
|
-
clearTimeout(
|
|
263
|
+
clearTimeout(forceExit);
|
|
700
264
|
process.exit(1);
|
|
701
265
|
}
|
|
702
266
|
};
|
|
703
|
-
|
|
704
|
-
// Register signal handlers
|
|
267
|
+
|
|
705
268
|
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
706
269
|
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (process.platform === "win32") {
|
|
710
|
-
process.on("SIGBREAK", () => gracefulShutdown("SIGBREAK"));
|
|
711
|
-
}
|
|
712
|
-
|
|
270
|
+
if (process.platform === "win32") process.on("SIGBREAK", () => gracefulShutdown("SIGBREAK"));
|
|
271
|
+
|
|
713
272
|
return server;
|
|
714
273
|
}
|
|
715
274
|
|
|
716
|
-
// Auto start if run directly
|
|
717
275
|
startServer();
|