coderaft 0.0.15 → 0.0.18

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 CHANGED
@@ -45,9 +45,11 @@ npx coderaft -o .
45
45
  ### Docker
46
46
 
47
47
  ```sh
48
- docker run -it --rm -p 6063:6063 node:alpine npx -y coderaft
48
+ docker run -it --rm -p 6063:6063 -v coderaft:/data ghcr.io/pithings/coderaft
49
49
  ```
50
50
 
51
+ Based on Alpine with Node.js LTS, bash, corepack (npm/pnpm/yarn) (~65 MB download, ~89 MB final). Data persists in `/data` volume (`/data/workspace` for project files, `/data/home` symlinked to `/root` for configs).
52
+
51
53
  ## Programmatic
52
54
 
53
55
  ```ts
@@ -137,13 +139,13 @@ interface CodeServerHandle {
137
139
 
138
140
  ### Server
139
141
 
140
- | Option | Description |
141
- | ----------------------------- | ---------------------------------------------- |
142
- | `-p, --port <port>` | Port to listen on (default: `$PORT` or `6063`) |
143
- | `-H, --host <host>` | Host/interface to bind |
144
- | `--server-base-path <path>` | Base path for the web UI (default: `/`) |
145
- | `--socket-path <path>` | Path to a socket file to listen on |
146
- | `--print-startup-performance` | Print startup timing to stdout |
142
+ | Option | Description |
143
+ | ----------------------------- | --------------------------------------------------- |
144
+ | `-p, --port <port>` | Port to listen on (default: `$PORT` or `6063`) |
145
+ | `-H, --host <host>` | Host/interface to bind |
146
+ | `--base-url <path>` | Base URL the server is mounted under (default: `/`) |
147
+ | `--socket-path <path>` | Path to a socket file to listen on |
148
+ | `--print-startup-performance` | Print startup timing to stdout |
147
149
 
148
150
  ### Auth
149
151
 
@@ -0,0 +1,54 @@
1
+ // Android/Termux preload — injected via NODE_OPTIONS --require into all child
2
+ // processes. VS Code strips LD_PRELOAD from forked processes, which breaks two
3
+ // things that termux-exec normally handles:
4
+ // 1. /proc/self/exe → process.execPath resolves to the Android linker
5
+ // 2. execve() of Termux binaries fails with EACCES (noexec mount / SELinux)
6
+ //
7
+ // VS Code's bundled code also explicitly `delete env.LD_PRELOAD` from the env
8
+ // object passed to child_process.fork/spawn, so simply restoring process.env
9
+ // is not enough — we must monkey-patch child_process.spawn to re-inject it.
10
+ "use strict";
11
+
12
+ Object.defineProperty(process, "platform", { value: "linux" });
13
+
14
+ // Fix process.execPath when termux-exec is unavailable
15
+ const execPath = process.execPath;
16
+ if (execPath.includes("linker64") || execPath.startsWith("/apex/")) {
17
+ const resolved =
18
+ process.env.TERMUX_EXEC__PROC_SELF_EXE || "/data/data/com.termux/files/usr/bin/node";
19
+ Object.defineProperty(process, "execPath", {
20
+ value: resolved,
21
+ writable: true,
22
+ configurable: true,
23
+ enumerable: true,
24
+ });
25
+ }
26
+
27
+ // Ensure LD_PRELOAD with termux-exec is always present in child process envs.
28
+ // VS Code strips it via `delete env.LD_PRELOAD` before spawning children.
29
+ // We patch child_process.spawn (which fork() calls internally) to re-inject it.
30
+ const TERMUX_EXEC_LIB = "/data/data/com.termux/files/usr/lib/libtermux-exec.so";
31
+ let _termuxExecExists;
32
+ function termuxExecExists() {
33
+ if (_termuxExecExists === undefined) {
34
+ try {
35
+ require("fs").accessSync(TERMUX_EXEC_LIB);
36
+ _termuxExecExists = true;
37
+ } catch {
38
+ _termuxExecExists = false;
39
+ }
40
+ }
41
+ return _termuxExecExists;
42
+ }
43
+
44
+ if (termuxExecExists()) {
45
+ process.env.LD_PRELOAD = TERMUX_EXEC_LIB;
46
+ const cp = require("child_process");
47
+ const _spawn = cp.spawn;
48
+ cp.spawn = function spawn(cmd, args, opts) {
49
+ if (opts && typeof opts === "object" && opts.env && !opts.env.LD_PRELOAD) {
50
+ opts.env.LD_PRELOAD = TERMUX_EXEC_LIB;
51
+ }
52
+ return _spawn.apply(this, arguments);
53
+ };
54
+ }
package/code.mjs CHANGED
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  // Auto-updated by scripts/pack.ts
7
- const codeArchiveHash = "28b47fa16ea99a3f";
7
+ const codeArchiveHash = "c2f3188affec085e";
8
8
 
9
9
  const archivePath = fileURLToPath(new URL("./code.tar.zst", import.meta.url));
10
10
 
package/code.tar.zst CHANGED
Binary file
@@ -0,0 +1,58 @@
1
+ # Licenses of Bundled Dependencies
2
+
3
+ The published artifact additionally contains code with the following licenses:
4
+ MIT
5
+
6
+ # Bundled Dependencies
7
+
8
+ ## httpxy
9
+
10
+ License: MIT
11
+ Repository: https://github.com/unjs/httpxy
12
+
13
+ > MIT License
14
+ >
15
+ > Copyright (c) Pooya Parsa <pooya@pi0.io>
16
+ >
17
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
18
+ > of this software and associated documentation files (the "Software"), to deal
19
+ > in the Software without restriction, including without limitation the rights
20
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21
+ > copies of the Software, and to permit persons to whom the Software is
22
+ > furnished to do so, subject to the following conditions:
23
+ >
24
+ > The above copyright notice and this permission notice shall be included in all
25
+ > copies or substantial portions of the Software.
26
+ >
27
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33
+ > SOFTWARE.
34
+ >
35
+ > ----
36
+ >
37
+ > Based on http-party/node-http-proxy (9b96cd7)
38
+ >
39
+ > Copyright (c) 2010-2016 Charlie Robbins, Jarrett Cruger & the Contributors.
40
+ >
41
+ > Permission is hereby granted, free of charge, to any person obtaining
42
+ > a copy of this software and associated documentation files (the
43
+ > "Software"), to deal in the Software without restriction, including
44
+ > without limitation the rights to use, copy, modify, merge, publish,
45
+ > distribute, sublicense, and/or sell copies of the Software, and to
46
+ > permit persons to whom the Software is furnished to do so, subject to
47
+ > the following conditions:
48
+ >
49
+ > The above copyright notice and this permission notice shall be
50
+ > included in all copies or substantial portions of the Software.
51
+ >
52
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
53
+ > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
54
+ > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
55
+ > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
56
+ > LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
57
+ > OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
58
+ > WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,561 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
3
+ import http2 from "node:http2";
4
+ import { EventEmitter } from "node:events";
5
+ const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i;
6
+ const defaultAgents = {
7
+ http: new http.Agent({
8
+ keepAlive: true,
9
+ maxSockets: 256,
10
+ maxFreeSockets: 64
11
+ }),
12
+ https: new https.Agent({
13
+ keepAlive: true,
14
+ maxSockets: 256,
15
+ maxFreeSockets: 64
16
+ })
17
+ };
18
+ const isSSL = /^https|wss/;
19
+ const HTTP2_HEADER_BLACKLIST = [
20
+ ":method",
21
+ ":path",
22
+ ":scheme",
23
+ ":authority"
24
+ ];
25
+ function setupOutgoing(outgoing, options, req, forward) {
26
+ outgoing.port = options[forward || "target"].port || (isSSL.test(options[forward || "target"].protocol ?? "http") ? 443 : 80);
27
+ for (const e of [
28
+ "host",
29
+ "hostname",
30
+ "socketPath",
31
+ "pfx",
32
+ "key",
33
+ "passphrase",
34
+ "cert",
35
+ "ca",
36
+ "ciphers",
37
+ "secureProtocol"
38
+ ]) {
39
+ const value = options[forward || "target"][e];
40
+ if (value !== void 0) outgoing[e] = value;
41
+ }
42
+ outgoing.method = options.method || req.method;
43
+ outgoing.headers = { ...req.headers };
44
+ if (req.headers?.[":authority"]) outgoing.headers.host = req.headers[":authority"];
45
+ if (options.headers) for (const key of Object.keys(options.headers)) outgoing.headers[key] = options.headers[key];
46
+ if (req.httpVersionMajor > 1) for (const header of HTTP2_HEADER_BLACKLIST) delete outgoing.headers[header];
47
+ if (options.auth) outgoing.auth = options.auth;
48
+ if (options.ca) outgoing.ca = options.ca;
49
+ if (isSSL.test(options[forward || "target"].protocol ?? "http")) outgoing.rejectUnauthorized = options.secure === void 0 ? true : options.secure;
50
+ if (options.agent !== void 0) outgoing.agent = options.agent || false;
51
+ else if (req.httpVersionMajor > 1) outgoing.agent = false;
52
+ else {
53
+ const targetProto = options[forward || "target"].protocol ?? "http";
54
+ outgoing.agent = isSSL.test(targetProto) ? defaultAgents.https : defaultAgents.http;
55
+ }
56
+ outgoing.localAddress = options.localAddress;
57
+ if (!outgoing.agent) {
58
+ outgoing.headers = outgoing.headers || {};
59
+ if (typeof outgoing.headers.connection !== "string" || !upgradeHeader.test(outgoing.headers.connection)) outgoing.headers.connection = "close";
60
+ }
61
+ const target = options[forward || "target"];
62
+ const targetPath = target && options.prependPath !== false ? target.pathname || "" : "";
63
+ const targetSearch = target instanceof URL && options.prependPath !== false ? target.search || "" : "";
64
+ const reqUrl = req.url || "";
65
+ const qIdx = reqUrl.indexOf("?");
66
+ const reqPath = qIdx === -1 ? reqUrl : reqUrl.slice(0, qIdx);
67
+ const reqSearch = qIdx === -1 ? "" : reqUrl.slice(qIdx);
68
+ const normalizedPath = reqPath ? reqPath[0] === "/" ? reqPath : "/" + reqPath : "/";
69
+ let outgoingPath = options.toProxy ? "/" + reqUrl : normalizedPath + reqSearch;
70
+ outgoingPath = options.ignorePath ? "" : outgoingPath;
71
+ let fullPath = joinURL(targetPath, outgoingPath);
72
+ if (targetSearch) fullPath = fullPath.includes("?") ? fullPath.replace("?", targetSearch + "&") : fullPath + targetSearch;
73
+ outgoing.path = fullPath;
74
+ if (options.changeOrigin) outgoing.headers.host = requiresPort(outgoing.port, options[forward || "target"].protocol) && !hasPort(outgoing.host) ? outgoing.host + ":" + outgoing.port : outgoing.host ?? void 0;
75
+ return outgoing;
76
+ }
77
+ function joinURL(base, path) {
78
+ if (!base || base === "/") return path || "/";
79
+ if (!path || path === "/") return base || "/";
80
+ const baseHasTrailing = base[base.length - 1] === "/";
81
+ const pathHasLeading = path[0] === "/";
82
+ if (baseHasTrailing && pathHasLeading) return base + path.slice(1);
83
+ if (!baseHasTrailing && !pathHasLeading) return base + "/" + path;
84
+ return base + path;
85
+ }
86
+ function setupSocket(socket) {
87
+ socket.setTimeout(0);
88
+ socket.setNoDelay(true);
89
+ socket.setKeepAlive(true, 0);
90
+ return socket;
91
+ }
92
+ function getPort(req) {
93
+ const hostHeader = req.headers[":authority"] || req.headers.host;
94
+ const res = hostHeader ? hostHeader.match(/:(\d+)/) : "";
95
+ if (res) return res[1];
96
+ return hasEncryptedConnection(req) ? "443" : "80";
97
+ }
98
+ function hasEncryptedConnection(req) {
99
+ const socket = req.socket;
100
+ return !!socket && "encrypted" in socket && socket.encrypted;
101
+ }
102
+ function rewriteCookieProperty(header, config, property) {
103
+ if (Array.isArray(header)) return header.map(function(headerElement) {
104
+ return rewriteCookieProperty(headerElement, config, property);
105
+ });
106
+ return header.replace(new RegExp(String.raw`(;\s*` + property + "=)([^;]+)", "i"), function(match, prefix, previousValue) {
107
+ let newValue;
108
+ if (previousValue in config) newValue = config[previousValue];
109
+ else if ("*" in config) newValue = config["*"];
110
+ else return match;
111
+ return newValue ? prefix + newValue : "";
112
+ });
113
+ }
114
+ function hasPort(host) {
115
+ return host ? !!~host.indexOf(":") : false;
116
+ }
117
+ function requiresPort(_port, _protocol) {
118
+ const protocol = _protocol?.split(":")[0];
119
+ const port = +_port;
120
+ if (!port) return false;
121
+ switch (protocol) {
122
+ case "http":
123
+ case "ws": return port !== 80;
124
+ case "https":
125
+ case "wss": return port !== 443;
126
+ case "ftp": return port !== 21;
127
+ case "gopher": return port !== 70;
128
+ case "file": return false;
129
+ }
130
+ return port !== 0;
131
+ }
132
+ function defineProxyMiddleware(m) {
133
+ return m;
134
+ }
135
+ function defineProxyOutgoingMiddleware(m) {
136
+ return m;
137
+ }
138
+ const redirectRegex = /^201|30([12378])$/;
139
+ const webOutgoingMiddleware = [
140
+ defineProxyOutgoingMiddleware((req, res, proxyRes) => {
141
+ if (req.httpVersion === "1.0" || req.httpVersionMajor >= 2 || proxyRes.statusCode === 204 || proxyRes.statusCode === 304) delete proxyRes.headers["transfer-encoding"];
142
+ }),
143
+ defineProxyOutgoingMiddleware((req, res, proxyRes) => {
144
+ if (req.httpVersion === "1.0") proxyRes.headers.connection = req.headers.connection || "close";
145
+ else if (req.httpVersionMajor < 2 && !proxyRes.headers.connection) proxyRes.headers.connection = req.headers.connection || "keep-alive";
146
+ else if (req.httpVersionMajor >= 2) delete proxyRes.headers.connection;
147
+ }),
148
+ defineProxyOutgoingMiddleware((req, res, proxyRes, options) => {
149
+ if ((options.hostRewrite || options.autoRewrite || options.protocolRewrite) && proxyRes.headers.location && redirectRegex.test(String(proxyRes.statusCode))) {
150
+ const target = _toURL(options.target);
151
+ const u = new URL(proxyRes.headers.location, target);
152
+ if (target.host !== u.host) return;
153
+ if (options.hostRewrite) u.host = options.hostRewrite;
154
+ else if (options.autoRewrite) {
155
+ if (req.headers[":authority"]) u.host = req.headers[":authority"];
156
+ else if (req.headers.host) u.host = req.headers.host;
157
+ }
158
+ if (options.protocolRewrite) u.protocol = options.protocolRewrite;
159
+ proxyRes.headers.location = u.toString();
160
+ }
161
+ }),
162
+ defineProxyOutgoingMiddleware((req, res, proxyRes, options) => {
163
+ const rewriteCookieDomainConfig = typeof options.cookieDomainRewrite === "string" ? { "*": options.cookieDomainRewrite } : options.cookieDomainRewrite;
164
+ const rewriteCookiePathConfig = typeof options.cookiePathRewrite === "string" ? { "*": options.cookiePathRewrite } : options.cookiePathRewrite;
165
+ const preserveHeaderKeyCase = options.preserveHeaderKeyCase;
166
+ let rawHeaderKeyMap;
167
+ const setHeader = function(key, header) {
168
+ if (header === void 0 || !String(key).trim()) return;
169
+ if (rewriteCookieDomainConfig && key.toLowerCase() === "set-cookie") header = rewriteCookieProperty(header, rewriteCookieDomainConfig, "domain");
170
+ if (rewriteCookiePathConfig && key.toLowerCase() === "set-cookie") header = rewriteCookieProperty(header, rewriteCookiePathConfig, "path");
171
+ try {
172
+ res.setHeader(String(key).trim(), header);
173
+ } catch {}
174
+ };
175
+ if (preserveHeaderKeyCase && proxyRes.rawHeaders !== void 0) {
176
+ rawHeaderKeyMap = {};
177
+ for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) {
178
+ const key = proxyRes.rawHeaders[i];
179
+ rawHeaderKeyMap[key.toLowerCase()] = key;
180
+ }
181
+ }
182
+ for (let key of Object.keys(proxyRes.headers)) {
183
+ const header = proxyRes.headers[key];
184
+ if (preserveHeaderKeyCase && rawHeaderKeyMap) key = rawHeaderKeyMap[key] || key;
185
+ setHeader(key, header);
186
+ }
187
+ }),
188
+ defineProxyOutgoingMiddleware((req, res, proxyRes) => {
189
+ res.statusCode = proxyRes.statusCode;
190
+ if (proxyRes.statusMessage && req.httpVersionMajor < 2) res.statusMessage = proxyRes.statusMessage;
191
+ })
192
+ ];
193
+ function _toURL(target) {
194
+ if (target instanceof URL) return target;
195
+ if (typeof target === "string") return new URL(target);
196
+ const protocol = target.protocol || "http:";
197
+ const host = target.host || target.hostname || "localhost";
198
+ const port = target.port;
199
+ return new URL(`${protocol}//${host}${port ? ":" + port : ""}`);
200
+ }
201
+ const nativeAgents = {
202
+ http,
203
+ https
204
+ };
205
+ const redirectStatuses = new Set([
206
+ 301,
207
+ 302,
208
+ 303,
209
+ 307,
210
+ 308
211
+ ]);
212
+ const webIncomingMiddleware = [
213
+ defineProxyMiddleware((req) => {
214
+ if ((req.method === "DELETE" || req.method === "OPTIONS") && !req.headers["content-length"]) {
215
+ req.headers["content-length"] = "0";
216
+ delete req.headers["transfer-encoding"];
217
+ }
218
+ }),
219
+ defineProxyMiddleware((req, res, options) => {
220
+ if (options.timeout) req.socket.setTimeout(options.timeout, () => {
221
+ req.socket.destroy();
222
+ });
223
+ }),
224
+ defineProxyMiddleware((req, res, options) => {
225
+ if (!options.xfwd) return;
226
+ const encrypted = req.isSpdy || hasEncryptedConnection(req);
227
+ const values = {
228
+ for: req.connection.remoteAddress || req.socket.remoteAddress,
229
+ port: getPort(req),
230
+ proto: encrypted ? "https" : "http"
231
+ };
232
+ for (const header of [
233
+ "for",
234
+ "port",
235
+ "proto"
236
+ ]) {
237
+ const key = "x-forwarded-" + header;
238
+ if (!req.headers[key]) req.headers[key] = values[header];
239
+ }
240
+ req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers[":authority"] || req.headers.host || "";
241
+ }),
242
+ defineProxyMiddleware((req, res, options, server, head, callback) => {
243
+ server.emit("start", req, res, options.target || options.forward);
244
+ const http = nativeAgents.http;
245
+ const https = nativeAgents.https;
246
+ const maxRedirects = typeof options.followRedirects === "number" ? options.followRedirects : options.followRedirects ? 5 : 0;
247
+ if (options.forward) {
248
+ const forwardReq = (isSSL.test(options.forward.protocol || "http") ? https : http).request(setupOutgoing(options.ssl || {}, options, req, "forward"));
249
+ const forwardError = createErrorHandler(forwardReq, options.forward);
250
+ req.on("error", forwardError);
251
+ forwardReq.on("error", forwardError);
252
+ (options.buffer || req).pipe(forwardReq);
253
+ if (!options.target) {
254
+ res.end();
255
+ return;
256
+ }
257
+ }
258
+ const proxyReq = (isSSL.test(options.target.protocol || "http") ? https : http).request(setupOutgoing(options.ssl || {}, options, req));
259
+ proxyReq.on("socket", (_socket) => {
260
+ if (server && !proxyReq.getHeader("expect")) server.emit("proxyReq", proxyReq, req, res, options);
261
+ });
262
+ if (options.proxyTimeout) proxyReq.setTimeout(options.proxyTimeout, function() {
263
+ proxyReq.destroy();
264
+ });
265
+ res.on("close", function() {
266
+ if (!res.writableFinished) proxyReq.destroy();
267
+ });
268
+ const proxyError = createErrorHandler(proxyReq, options.target);
269
+ req.on("error", proxyError);
270
+ proxyReq.on("error", proxyError);
271
+ function createErrorHandler(proxyReq, url) {
272
+ return function proxyError(err) {
273
+ if (!req.socket?.writable && err.code === "ECONNRESET") {
274
+ server.emit("econnreset", err, req, res, url);
275
+ return proxyReq.destroy();
276
+ }
277
+ if (callback) callback(err, req, res, url);
278
+ else server.emit("error", err, req, res, url);
279
+ };
280
+ }
281
+ let bodyBuffer;
282
+ if (maxRedirects > 0) {
283
+ const chunks = [];
284
+ const source = options.buffer || req;
285
+ source.on("data", (chunk) => {
286
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
287
+ proxyReq.write(chunk);
288
+ });
289
+ source.on("end", () => {
290
+ bodyBuffer = Buffer.concat(chunks);
291
+ proxyReq.end();
292
+ });
293
+ source.on("error", (err) => {
294
+ proxyReq.destroy(err);
295
+ });
296
+ } else proxyReq.on("socket", (socket) => {
297
+ if (socket.pending) socket.on("connect", () => (options.buffer || req).pipe(proxyReq));
298
+ else (options.buffer || req).pipe(proxyReq);
299
+ });
300
+ function handleResponse(proxyRes, redirectCount, currentUrl) {
301
+ const statusCode = proxyRes.statusCode;
302
+ if (maxRedirects > 0 && redirectStatuses.has(statusCode) && redirectCount < maxRedirects && proxyRes.headers.location) {
303
+ proxyRes.resume();
304
+ const location = new URL(proxyRes.headers.location, currentUrl);
305
+ const preserveMethod = statusCode === 307 || statusCode === 308;
306
+ const redirectMethod = preserveMethod ? req.method || "GET" : "GET";
307
+ const isHTTPS = isSSL.test(location.protocol);
308
+ const agent = isHTTPS ? https : http;
309
+ const redirectHeaders = { ...req.headers };
310
+ if (options.headers) Object.assign(redirectHeaders, options.headers);
311
+ redirectHeaders.host = location.host;
312
+ if (location.host !== currentUrl.host) {
313
+ delete redirectHeaders.authorization;
314
+ delete redirectHeaders.cookie;
315
+ }
316
+ if (!preserveMethod) {
317
+ delete redirectHeaders["content-length"];
318
+ delete redirectHeaders["content-type"];
319
+ delete redirectHeaders["transfer-encoding"];
320
+ }
321
+ const redirectOpts = {
322
+ hostname: location.hostname,
323
+ port: location.port || (isHTTPS ? 443 : 80),
324
+ path: location.pathname + location.search,
325
+ method: redirectMethod,
326
+ headers: redirectHeaders,
327
+ agent: options.agent || false
328
+ };
329
+ if (isHTTPS) redirectOpts.rejectUnauthorized = options.secure === void 0 ? true : options.secure;
330
+ const redirectReq = agent.request(redirectOpts);
331
+ if (server && !redirectReq.getHeader("expect")) server.emit("proxyReq", redirectReq, req, res, options);
332
+ if (options.proxyTimeout) redirectReq.setTimeout(options.proxyTimeout, () => {
333
+ redirectReq.destroy();
334
+ });
335
+ const redirectError = createErrorHandler(redirectReq, location);
336
+ redirectReq.on("error", redirectError);
337
+ redirectReq.on("response", (nextRes) => {
338
+ handleResponse(nextRes, redirectCount + 1, location);
339
+ });
340
+ if (preserveMethod && bodyBuffer && bodyBuffer.length > 0) redirectReq.end(bodyBuffer);
341
+ else redirectReq.end();
342
+ return;
343
+ }
344
+ if (server) server.emit("proxyRes", proxyRes, req, res);
345
+ if (!res.headersSent && !options.selfHandleResponse) {
346
+ for (const pass of webOutgoingMiddleware) if (pass(req, res, proxyRes, options)) break;
347
+ }
348
+ if (res.finished) {
349
+ if (server) server.emit("end", req, res, proxyRes);
350
+ } else {
351
+ res.on("close", function() {
352
+ proxyRes.destroy();
353
+ });
354
+ proxyRes.on("close", function() {
355
+ if (!proxyRes.complete && !res.destroyed) res.destroy();
356
+ });
357
+ proxyRes.on("error", function(err) {
358
+ if (!res.destroyed) res.destroy(err);
359
+ if (server.listenerCount("error") > 0) server.emit("error", err, req, res, currentUrl);
360
+ });
361
+ proxyRes.on("end", function() {
362
+ if (server) server.emit("end", req, res, proxyRes);
363
+ });
364
+ if (!options.selfHandleResponse) proxyRes.pipe(res);
365
+ }
366
+ }
367
+ proxyReq.on("response", function(proxyRes) {
368
+ handleResponse(proxyRes, 0, options.target);
369
+ });
370
+ })
371
+ ];
372
+ const websocketIncomingMiddleware = [
373
+ defineProxyMiddleware((req, socket) => {
374
+ if (req.method !== "GET" || !req.headers.upgrade) {
375
+ socket.destroy();
376
+ return true;
377
+ }
378
+ if (req.headers.upgrade.toLowerCase() !== "websocket") {
379
+ socket.destroy();
380
+ return true;
381
+ }
382
+ }),
383
+ defineProxyMiddleware((req, socket, options) => {
384
+ if (!options.xfwd) return;
385
+ const values = {
386
+ for: req.connection.remoteAddress || req.socket.remoteAddress,
387
+ port: getPort(req),
388
+ proto: hasEncryptedConnection(req) ? "wss" : "ws"
389
+ };
390
+ for (const header of [
391
+ "for",
392
+ "port",
393
+ "proto"
394
+ ]) {
395
+ const key = "x-forwarded-" + header;
396
+ if (!req.headers[key]) req.headers[key] = values[header];
397
+ }
398
+ }),
399
+ defineProxyMiddleware((req, socket, options, server, head, callback) => {
400
+ const createHttpHeader = function(line, headers) {
401
+ return Object.keys(headers).reduce(function(head, key) {
402
+ const value = headers[key];
403
+ if (!Array.isArray(value)) {
404
+ head.push(key + ": " + value);
405
+ return head;
406
+ }
407
+ for (const element of value) head.push(key + ": " + element);
408
+ return head;
409
+ }, [line]).join("\r\n") + "\r\n\r\n";
410
+ };
411
+ setupSocket(socket);
412
+ if (head && head.length > 0) socket.unshift(head);
413
+ socket.on("error", onSocketError);
414
+ const proxyReq = (isSSL.test(options.target.protocol || "http") ? https : http).request(setupOutgoing(options.ssl || {}, options, req));
415
+ if (server) server.emit("proxyReqWs", proxyReq, req, socket, options, head);
416
+ proxyReq.on("error", onOutgoingError);
417
+ proxyReq.on("response", function(res) {
418
+ if (!res.upgrade) if (!socket.destroyed && socket.writable) {
419
+ socket.write(createHttpHeader("HTTP/" + res.httpVersion + " " + res.statusCode + " " + res.statusMessage, res.headers));
420
+ res.on("error", onOutgoingError);
421
+ res.pipe(socket);
422
+ } else res.resume();
423
+ });
424
+ proxyReq.on("upgrade", function(proxyRes, proxySocket, proxyHead) {
425
+ proxySocket.on("error", onOutgoingError);
426
+ proxySocket.on("end", function() {
427
+ server.emit("close", proxyRes, proxySocket, proxyHead);
428
+ });
429
+ socket.removeListener("error", onSocketError);
430
+ socket.on("error", function() {
431
+ proxySocket.end();
432
+ });
433
+ setupSocket(proxySocket);
434
+ if (proxyHead && proxyHead.length > 0) proxySocket.unshift(proxyHead);
435
+ socket.write(createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers));
436
+ proxySocket.pipe(socket).pipe(proxySocket);
437
+ server.emit("open", proxySocket);
438
+ server.emit("proxySocket", proxySocket);
439
+ });
440
+ proxyReq.end();
441
+ function onSocketError(err) {
442
+ if (callback) callback(err, req, socket);
443
+ else server.emit("error", err, req, socket);
444
+ proxyReq.destroy();
445
+ }
446
+ function onOutgoingError(err) {
447
+ if (callback) callback(err, req, socket);
448
+ else server.emit("error", err, req, socket);
449
+ socket.end();
450
+ }
451
+ })
452
+ ];
453
+ var ProxyServer = class extends EventEmitter {
454
+ _server;
455
+ _webPasses = [...webIncomingMiddleware];
456
+ _wsPasses = [...websocketIncomingMiddleware];
457
+ options;
458
+ web;
459
+ ws;
460
+ constructor(options = {}) {
461
+ super();
462
+ this.options = options || {};
463
+ this.options.prependPath = options.prependPath !== false;
464
+ this.web = _createProxyFn("web", this);
465
+ this.ws = _createProxyFn("ws", this);
466
+ }
467
+ listen(port, hostname) {
468
+ const closure = (req, res) => {
469
+ return this.web(req, res);
470
+ };
471
+ if (this.options.http2) {
472
+ if (!this.options.ssl) throw new Error("HTTP/2 requires ssl option");
473
+ this._server = http2.createSecureServer({
474
+ ...this.options.ssl,
475
+ allowHTTP1: true
476
+ }, closure);
477
+ } else if (this.options.ssl) this._server = https.createServer(this.options.ssl, closure);
478
+ else this._server = http.createServer(closure);
479
+ if (this.options.ws) this._server.on("upgrade", (req, socket, head) => {
480
+ this.ws(req, socket, this.options, head).catch(() => {});
481
+ });
482
+ this._server.listen(port, hostname);
483
+ return this;
484
+ }
485
+ close(callback) {
486
+ if (this._server) this._server.close((...args) => {
487
+ this._server = void 0;
488
+ if (callback) Reflect.apply(callback, void 0, args);
489
+ });
490
+ }
491
+ before(type, passName, pass) {
492
+ if (type !== "ws" && type !== "web") throw new Error("type must be `web` or `ws`");
493
+ const passes = this._getPasses(type);
494
+ let i = false;
495
+ for (const [idx, v] of passes.entries()) if (v.name === passName) i = idx;
496
+ if (i === false) throw new Error("No such pass");
497
+ passes.splice(i, 0, pass);
498
+ }
499
+ after(type, passName, pass) {
500
+ if (type !== "ws" && type !== "web") throw new Error("type must be `web` or `ws`");
501
+ const passes = this._getPasses(type);
502
+ let i = false;
503
+ for (const [idx, v] of passes.entries()) if (v.name === passName) i = idx;
504
+ if (i === false) throw new Error("No such pass");
505
+ passes.splice(i++, 0, pass);
506
+ }
507
+ _getPasses(type) {
508
+ return type === "ws" ? this._wsPasses : this._webPasses;
509
+ }
510
+ };
511
+ function createProxyServer(options = {}) {
512
+ return new ProxyServer(options);
513
+ }
514
+ function _createProxyFn(type, server) {
515
+ return function(req, res, opts, head) {
516
+ const requestOptions = {
517
+ ...opts,
518
+ ...server.options
519
+ };
520
+ for (const key of ["target", "forward"]) if (typeof requestOptions[key] === "string") requestOptions[key] = new URL(requestOptions[key]);
521
+ if (!requestOptions.target && !requestOptions.forward) {
522
+ this.emit("error", /* @__PURE__ */ new Error("Must provide a proper URL as target"));
523
+ return Promise.resolve();
524
+ }
525
+ let _resolve;
526
+ let _reject;
527
+ const callbackPromise = new Promise((resolve, reject) => {
528
+ _resolve = resolve;
529
+ _reject = reject;
530
+ });
531
+ res.on("close", () => {
532
+ _resolve();
533
+ });
534
+ res.on("error", (error) => {
535
+ _reject(error);
536
+ });
537
+ for (const pass of server._getPasses(type)) {
538
+ let stop;
539
+ try {
540
+ stop = pass(req, res, requestOptions, server, head, (error) => {
541
+ if (server.listenerCount("error") > 0) {
542
+ server.emit("error", error, req, res);
543
+ _resolve();
544
+ } else _reject(error);
545
+ });
546
+ } catch (error) {
547
+ if (server.listenerCount("error") > 0) {
548
+ server.emit("error", error, req, res);
549
+ _resolve();
550
+ } else _reject(error);
551
+ break;
552
+ }
553
+ if (stop) {
554
+ _resolve();
555
+ break;
556
+ }
557
+ }
558
+ return callbackPromise;
559
+ };
560
+ }
561
+ export { createProxyServer as t };
@@ -0,0 +1,3 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+ export { __require as t };
@@ -1,11 +1,88 @@
1
- import { createRequire } from "node:module";
2
- import { randomBytes, randomUUID } from "node:crypto";
1
+ import { t as __require } from "./rolldown-runtime.mjs";
2
+ import { t as createProxyServer } from "./libs/httpxy.mjs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { createReadStream, readFileSync, readdirSync, unlinkSync } from "node:fs";
5
+ import { randomBytes, randomUUID } from "node:crypto";
4
6
  import { createServer } from "node:http";
5
7
  import { extname, join, normalize, sep } from "node:path";
6
8
  import { stat } from "node:fs/promises";
7
9
  import { loadCode } from "#code";
8
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
10
+ if (process.platform === "android") {
11
+ Object.defineProperty(process, "platform", { value: "linux" });
12
+ if (process.execPath.includes("linker64") || process.execPath.startsWith("/apex/")) {
13
+ const resolved = process.env.TERMUX_EXEC__PROC_SELF_EXE || "/data/data/com.termux/files/usr/bin/node";
14
+ Object.defineProperty(process, "execPath", {
15
+ value: resolved,
16
+ writable: true,
17
+ configurable: true,
18
+ enumerable: true
19
+ });
20
+ }
21
+ const preload = `--require "${fileURLToPath(import.meta.resolve("#android-preload"))}"`;
22
+ process.env.NODE_OPTIONS = process.env.NODE_OPTIONS ? `${process.env.NODE_OPTIONS} ${preload}` : preload;
23
+ const _fs = process.getBuiltinModule?.("fs") ?? __require("node:fs");
24
+ const { syncBuiltinESMExports: _syncESM } = process.getBuiltinModule?.("module") ?? __require("node:module");
25
+ const TERMUX_EXEC_LIB = "/data/data/com.termux/files/usr/lib/libtermux-exec.so";
26
+ try {
27
+ _fs.accessSync(TERMUX_EXEC_LIB);
28
+ const _cp = process.getBuiltinModule?.("child_process") ?? __require("node:child_process");
29
+ const _injectLdPreload = (opts) => {
30
+ if (opts && typeof opts === "object" && opts.env) {
31
+ const env = opts.env;
32
+ if (!env.LD_PRELOAD) env.LD_PRELOAD = TERMUX_EXEC_LIB;
33
+ }
34
+ };
35
+ const _spawn = _cp.spawn;
36
+ _cp.spawn = function spawn(...spawnArgs) {
37
+ _injectLdPreload(spawnArgs[2]);
38
+ return _spawn.apply(this, spawnArgs);
39
+ };
40
+ const _fork = _cp.fork;
41
+ _cp.fork = function fork(modulePath, ...rest) {
42
+ for (const arg of rest) if (arg && typeof arg === "object" && !Array.isArray(arg)) {
43
+ _injectLdPreload(arg);
44
+ break;
45
+ }
46
+ return _fork.call(this, modulePath, ...rest);
47
+ };
48
+ _syncESM();
49
+ } catch {}
50
+ if (process.env.PATH) {
51
+ const accessible = process.env.PATH.split(":").filter((dir) => {
52
+ try {
53
+ _fs.accessSync(dir, _fs.constants.R_OK);
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ });
59
+ if (accessible.length > 0) process.env.PATH = accessible.join(":");
60
+ }
61
+ const _os = process.getBuiltinModule?.("os") ?? __require("node:os");
62
+ const _crypto = process.getBuiltinModule?.("crypto") ?? __require("node:crypto");
63
+ const BLACKLISTED = new Set([
64
+ "00:00:00:00:00:00",
65
+ "ff:ff:ff:ff:ff:ff",
66
+ "ac:de:48:00:11:22"
67
+ ]);
68
+ const original = _os.networkInterfaces;
69
+ _os.networkInterfaces = function networkInterfaces() {
70
+ const ifaces = original.call(_os);
71
+ for (const name in ifaces) for (const info of ifaces[name]) if (info.mac && !BLACKLISTED.has(info.mac)) return ifaces;
72
+ const hash = _crypto.createHash("md5").update(_os.hostname()).digest();
73
+ hash[0] = (hash[0] | 2) & 254;
74
+ ifaces._coderaft = [{
75
+ address: "10.0.0.1",
76
+ netmask: "255.255.255.0",
77
+ family: "IPv4",
78
+ mac: [...hash.subarray(0, 6)].map((b) => b.toString(16).padStart(2, "0")).join(":"),
79
+ internal: false,
80
+ cidr: "10.0.0.1/24"
81
+ }];
82
+ return ifaces;
83
+ };
84
+ _syncESM();
85
+ }
9
86
  const STATIC_MIME = {
10
87
  ".js": "text/javascript",
11
88
  ".mjs": "text/javascript",
@@ -47,12 +124,7 @@ async function serveStatic(res, root, relPath) {
47
124
  return false;
48
125
  }
49
126
  }
50
- if (process.platform === "android") {
51
- Object.defineProperty(process, "platform", { value: "linux" });
52
- const preload = `--import "data:text/javascript,Object.defineProperty(process,'platform',{value:'linux'})"`;
53
- process.env.NODE_OPTIONS = process.env.NODE_OPTIONS ? `${process.env.NODE_OPTIONS} ${preload}` : preload;
54
- }
55
- const _os = __require("node:os");
127
+ const _os = process.getBuiltinModule?.("os") ?? __require("node:os");
56
128
  const MANIFEST_BODY = JSON.stringify({
57
129
  name: "coderaft",
58
130
  short_name: "coderaft",
@@ -79,12 +151,13 @@ async function createCodeServer(opts = {}) {
79
151
  const withoutToken = opts.vscode?.["without-connection-token"] === true || !explicitToken && isLocal;
80
152
  const connectionToken = withoutToken ? "" : opts.connectionToken ?? randomUUID();
81
153
  const defaultFolder = opts.defaultFolder ?? process.cwd();
154
+ const baseURL = normalizeBaseURL(opts.baseURL ?? opts.vscode?.["server-base-path"]);
82
155
  const mintKey = randomBytes(32);
83
156
  process.env.CODE_SERVER_PARENT_PID ??= String(process.pid);
84
157
  cleanupStaleLocks(opts.vscode?.["user-data-dir"] ?? join(_os.homedir(), ".vscode-server-oss", "data"));
158
+ watchChildProcessHealth();
85
159
  const { modulesDir } = await loadCode();
86
160
  const vsRootPath = join(modulesDir, "code-server", "lib", "vscode");
87
- ensureNetworkInterface();
88
161
  const _log = console.log;
89
162
  console.log = (...args) => {
90
163
  if (typeof args[0] === "string" && args[0].includes("[reconnection-grace-time]")) return;
@@ -100,17 +173,26 @@ async function createCodeServer(opts = {}) {
100
173
  }
101
174
  const vscodeServer = await (await mod.loadCodeWithNls()).createServer(null, {
102
175
  "default-folder": defaultFolder,
176
+ ...baseURL ? { "server-base-path": baseURL } : {},
103
177
  ...withoutToken ? { "without-connection-token": true } : { "connection-token": connectionToken },
104
- "reconnection-grace-time": "10800",
178
+ "reconnection-grace-time": "30",
105
179
  "disable-getting-started-override": true,
106
180
  ...opts.vscode
107
181
  });
108
182
  console.log = _log;
183
+ const proxy = createProxyServer({});
184
+ proxy.on("error", (err, req, res) => {
185
+ if (res && "writeHead" in res) {
186
+ res.writeHead(502, { "Content-Type": "text/plain" });
187
+ res.end(`Proxy error: ${err.message}`);
188
+ }
189
+ });
109
190
  return {
110
191
  connectionToken,
111
192
  handleRequest(req, res) {
112
193
  const method = req.method ?? "GET";
113
- const url = (req.url ?? "/").split("?")[0];
194
+ const strippedUrl = stripBaseURL(req.url ?? "/", baseURL);
195
+ const url = strippedUrl.split("?")[0];
114
196
  if (url === "/manifest.json") {
115
197
  res.writeHead(200, {
116
198
  "Content-Type": "application/manifest+json",
@@ -153,7 +235,7 @@ async function createCodeServer(opts = {}) {
153
235
  return;
154
236
  }
155
237
  if (url === "/login" || url === "/logout") {
156
- res.writeHead(302, { Location: "/" });
238
+ res.writeHead(302, { Location: `${baseURL}/` });
157
239
  res.end();
158
240
  return;
159
241
  }
@@ -163,11 +245,30 @@ async function createCodeServer(opts = {}) {
163
245
  });
164
246
  return;
165
247
  }
248
+ const proxyMatch = parseProxyPath(strippedUrl);
249
+ if (proxyMatch) {
250
+ const { port: targetPort, path: targetPath } = proxyMatch;
251
+ proxy.web(req, res, {
252
+ target: `http://127.0.0.1:${targetPort}${targetPath}`,
253
+ ignorePath: true,
254
+ xfwd: true
255
+ });
256
+ return;
257
+ }
166
258
  vscodeServer.handleRequest(req, res);
167
259
  },
168
- handleUpgrade(req, socket) {
169
- vscodeServer.handleUpgrade(req, socket);
170
- socket.resume();
260
+ handleUpgrade(req, socket, _head) {
261
+ const proxyMatch = parseProxyPath(stripBaseURL(req.url ?? "/", baseURL));
262
+ if (proxyMatch) {
263
+ const { port: targetPort, path: targetPath } = proxyMatch;
264
+ proxy.ws(req, socket, {
265
+ target: `http://127.0.0.1:${targetPort}${targetPath}`,
266
+ ignorePath: true,
267
+ xfwd: true
268
+ }, _head);
269
+ return;
270
+ }
271
+ vscodeServer.handleUpgrade(req, socket, _head);
171
272
  },
172
273
  async dispose() {
173
274
  vscodeServer.dispose();
@@ -175,15 +276,16 @@ async function createCodeServer(opts = {}) {
175
276
  };
176
277
  }
177
278
  async function startCodeServer(opts = {}) {
279
+ const socketPath = opts.socketPath;
178
280
  const port = opts.port ?? (Number(process.env.PORT) || 6063);
179
281
  const handler = await createCodeServer(opts);
180
282
  const server = createServer((req, res) => {
181
283
  handler.handleRequest(req, res);
182
284
  });
183
- server.on("upgrade", (req, socket) => {
184
- handler.handleUpgrade(req, socket);
285
+ server.on("upgrade", (req, socket, head) => {
286
+ handler.handleUpgrade(req, socket, head);
185
287
  });
186
- const listen = (p) => new Promise((resolve, reject) => {
288
+ const listenTcp = (p) => new Promise((resolve, reject) => {
187
289
  server.once("error", reject);
188
290
  const cb = () => {
189
291
  server.removeListener("error", reject);
@@ -192,17 +294,31 @@ async function startCodeServer(opts = {}) {
192
294
  if (opts.host) server.listen(p, opts.host, cb);
193
295
  else server.listen(p, cb);
194
296
  });
195
- try {
196
- await listen(port);
297
+ const listenSocket = (path) => new Promise((resolve, reject) => {
298
+ try {
299
+ unlinkSync(path);
300
+ } catch {}
301
+ server.once("error", reject);
302
+ server.listen(path, () => {
303
+ server.removeListener("error", reject);
304
+ resolve();
305
+ });
306
+ });
307
+ if (socketPath) await listenSocket(socketPath);
308
+ else try {
309
+ await listenTcp(port);
197
310
  } catch (err) {
198
- if (err?.code === "EADDRINUSE") await listen(0);
311
+ if (err?.code === "EADDRINUSE") await listenTcp(0);
199
312
  else throw err;
200
313
  }
201
- const actualPort = server.address().port;
314
+ const address = server.address();
315
+ const actualPort = address && typeof address === "object" && "port" in address ? address.port : void 0;
316
+ const basePath = normalizeBaseURL(opts.baseURL ?? opts.vscode?.["server-base-path"]);
202
317
  return {
203
318
  server,
204
319
  port: actualPort,
205
- url: handler.connectionToken ? `http://localhost:${actualPort}/?tkn=${handler.connectionToken}` : `http://localhost:${actualPort}/`,
320
+ socketPath,
321
+ url: socketPath ? `unix:${socketPath}` : handler.connectionToken ? `http://localhost:${actualPort}${basePath}/?tkn=${handler.connectionToken}` : `http://localhost:${actualPort}${basePath}/`,
206
322
  connectionToken: handler.connectionToken,
207
323
  async close() {
208
324
  await handler.dispose();
@@ -210,33 +326,12 @@ async function startCodeServer(opts = {}) {
210
326
  await new Promise((resolve, reject) => {
211
327
  server.close((err) => err ? reject(err) : resolve());
212
328
  });
329
+ if (socketPath) try {
330
+ unlinkSync(socketPath);
331
+ } catch {}
213
332
  }
214
333
  };
215
334
  }
216
- function ensureNetworkInterface() {
217
- const original = _os.networkInterfaces;
218
- const BLACKLISTED = new Set([
219
- "00:00:00:00:00:00",
220
- "ff:ff:ff:ff:ff:ff",
221
- "ac:de:48:00:11:22"
222
- ]);
223
- _os.networkInterfaces = function networkInterfaces() {
224
- const ifaces = original.call(_os);
225
- for (const name in ifaces) for (const info of ifaces[name]) if (info.mac && !BLACKLISTED.has(info.mac)) return ifaces;
226
- const { createHash } = __require("node:crypto");
227
- const hash = createHash("md5").update(_os.hostname()).digest();
228
- hash[0] = (hash[0] | 2) & 254;
229
- ifaces._coderaft = [{
230
- address: "10.0.0.1",
231
- netmask: "255.255.255.0",
232
- family: "IPv4",
233
- mac: [...hash.subarray(0, 6)].map((b) => b.toString(16).padStart(2, "0")).join(":"),
234
- internal: false,
235
- cidr: "10.0.0.1/24"
236
- }];
237
- return ifaces;
238
- };
239
- }
240
335
  function cleanupStaleLocks(userDataDir) {
241
336
  const storageDir = join(userDataDir, "User", "workspaceStorage");
242
337
  try {
@@ -244,10 +339,65 @@ function cleanupStaleLocks(userDataDir) {
244
339
  const lockPath = join(storageDir, entry, "vscode.lock");
245
340
  try {
246
341
  unlinkSync(lockPath);
342
+ console.log(`[coderaft] Removed stale lock: ${lockPath}`);
247
343
  } catch {}
248
344
  }
249
345
  } catch {}
250
346
  }
347
+ function watchChildProcessHealth() {
348
+ if (process.platform !== "linux" && process.platform !== "android") return;
349
+ const stuckCounts = /* @__PURE__ */ new Map();
350
+ const interval = setInterval(() => {
351
+ try {
352
+ for (const pid of readdirSync("/proc").filter((d) => /^\d+$/.test(d))) {
353
+ let cmdline;
354
+ try {
355
+ cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8");
356
+ } catch {
357
+ continue;
358
+ }
359
+ if (!cmdline.includes("extensionHost")) continue;
360
+ try {
361
+ const wchan = readFileSync(`/proc/${pid}/task/${pid}/wchan`, "utf8").trim();
362
+ if (wchan === "__futex_wait") {
363
+ const count = (stuckCounts.get(+pid) ?? 0) + 1;
364
+ stuckCounts.set(+pid, count);
365
+ if (count === 3) console.error(`[coderaft] Extension host (pid ${pid}) main thread stuck in ${wchan} for ${count * 5}s`);
366
+ } else stuckCounts.delete(+pid);
367
+ } catch {}
368
+ }
369
+ } catch {}
370
+ }, 5e3);
371
+ interval.unref();
372
+ return interval;
373
+ }
374
+ const PROXY_RE = /^\/proxy\/(\d+)(\/.*)?$/;
375
+ function parseProxyPath(url) {
376
+ const qIdx = url.indexOf("?");
377
+ const pathname = qIdx === -1 ? url : url.slice(0, qIdx);
378
+ const query = qIdx === -1 ? "" : url.slice(qIdx);
379
+ const match = PROXY_RE.exec(pathname);
380
+ if (!match) return;
381
+ const port = Number(match[1]);
382
+ if (port < 1 || port > 65535) return;
383
+ return {
384
+ port,
385
+ path: (match[2] || "/") + query
386
+ };
387
+ }
388
+ function normalizeBaseURL(input) {
389
+ if (!input || input === "/") return "";
390
+ let result = input.trim();
391
+ if (!result.startsWith("/")) result = "/" + result;
392
+ while (result.endsWith("/")) result = result.slice(0, -1);
393
+ return result;
394
+ }
395
+ function stripBaseURL(url, baseURL) {
396
+ if (!baseURL) return url;
397
+ if (url === baseURL) return "/";
398
+ if (url.startsWith(baseURL + "/") || url.startsWith(baseURL + "?")) return url.slice(baseURL.length) || "/";
399
+ return url;
400
+ }
251
401
  function sendJson(res, status, body) {
252
402
  const payload = JSON.stringify(body);
253
403
  res.writeHead(status, {
package/dist/cli.mjs CHANGED
@@ -12,6 +12,7 @@ const { values, positionals } = parseArgs({
12
12
  type: "string",
13
13
  short: "H"
14
14
  },
15
+ "base-url": { type: "string" },
15
16
  "server-base-path": { type: "string" },
16
17
  "socket-path": { type: "string" },
17
18
  "print-startup-performance": { type: "boolean" },
@@ -92,7 +93,7 @@ if (values.help) {
92
93
  Server:
93
94
  -p, --port <port> Port to listen on (default: $PORT or 6063)
94
95
  -H, --host <host> Host/interface to bind
95
- --server-base-path <path> Base path for the web UI (default: /)
96
+ --base-url <path> Base URL the server is mounted under (default: /)
96
97
  --socket-path <path> Path to a socket file to listen on
97
98
  --print-startup-performance Print startup timing to stdout
98
99
 
@@ -175,7 +176,6 @@ if (values.help) {
175
176
  const vscode = {};
176
177
  for (const key of [
177
178
  "server-base-path",
178
- "socket-path",
179
179
  "print-startup-performance",
180
180
  "connection-token-file",
181
181
  "without-connection-token",
@@ -224,9 +224,22 @@ for (const key of [
224
224
  if (values["logs-path"]) vscode.logsPath = values["logs-path"];
225
225
  const dir = positionals[0];
226
226
  if (dir) vscode["disable-workspace-trust"] = true;
227
- const handle = await startCodeServer({
227
+ let handle;
228
+ let shuttingDown = false;
229
+ const shutdown = () => {
230
+ if (shuttingDown) process.exit(0);
231
+ shuttingDown = true;
232
+ setTimeout(() => process.exit(0), 3e3).unref();
233
+ if (handle) handle.close().finally(() => process.exit(0));
234
+ else process.exit(0);
235
+ };
236
+ process.on("SIGINT", shutdown);
237
+ process.on("SIGTERM", shutdown);
238
+ handle = await startCodeServer({
228
239
  port: values.port ? Number(values.port) : void 0,
229
240
  host: values.host,
241
+ socketPath: values["socket-path"],
242
+ baseURL: values["base-url"] ?? values["server-base-path"],
230
243
  defaultFolder: dir || values["default-folder"],
231
244
  connectionToken: values["connection-token"] ?? values.token,
232
245
  vscode
@@ -247,13 +260,4 @@ if (values.open) {
247
260
  else if (platform === "win32") exec(`start chrome --app="${url}" || start msedge --app="${url}" || start "" "${url}"`);
248
261
  else exec(`google-chrome-stable --app="${url}" 2>/dev/null || google-chrome --app="${url}" 2>/dev/null || chromium --app="${url}" 2>/dev/null || xdg-open "${url}"`);
249
262
  }
250
- let shuttingDown = false;
251
- const shutdown = () => {
252
- if (shuttingDown) process.exit(0);
253
- shuttingDown = true;
254
- setTimeout(() => process.exit(0), 3e3).unref();
255
- handle.close().finally(() => process.exit(0));
256
- };
257
- process.on("SIGINT", shutdown);
258
- process.on("SIGTERM", shutdown);
259
263
  export {};
package/dist/index.d.mts CHANGED
@@ -85,24 +85,35 @@ interface CreateCodeServerOptions {
85
85
  connectionToken?: string;
86
86
  /** Host/interface to bind (used to infer local-only access for token default). */
87
87
  host?: string;
88
+ /**
89
+ * Base URL the server is mounted under (e.g. `/code`). Defaults to `/`.
90
+ * Forwarded to VS Code as `server-base-path` and honored by coderaft's own
91
+ * routes (`/healthz`, `/_static/*`, `/proxy/*`, `/login`, …).
92
+ */
93
+ baseURL?: string;
88
94
  /** Extra options forwarded to VS Code's `createServer()`. */
89
95
  vscode?: VSCodeServerOptions;
90
96
  }
91
97
  interface StartCodeServerOptions extends CreateCodeServerOptions {
92
- /** TCP port to listen on. Defaults to `$PORT` or `6063`. */
98
+ /** TCP port to listen on. Defaults to `$PORT` or `6063`. Ignored when `socketPath` is set. */
93
99
  port?: number;
100
+ /** Unix socket path to listen on. When set, `port` and `host` are ignored. */
101
+ socketPath?: string;
94
102
  }
95
103
  interface CodeServerHandler {
96
104
  /** Node-style HTTP request handler (middleware). */
97
105
  handleRequest(req: IncomingMessage, res: ServerResponse): void;
98
106
  /** Handle WebSocket upgrade. */
99
- handleUpgrade(req: IncomingMessage, socket: Duplex): void;
107
+ handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void;
100
108
  connectionToken: string;
101
109
  dispose(): Promise<void>;
102
110
  }
103
111
  interface CodeServerHandle {
104
112
  server: Server;
105
- port: number;
113
+ /** TCP port the server is bound to, or `undefined` when listening on a unix socket. */
114
+ port?: number;
115
+ /** Unix socket path the server is bound to, or `undefined` when listening on TCP. */
116
+ socketPath?: string;
106
117
  url: string;
107
118
  connectionToken: string;
108
119
  close(): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderaft",
3
- "version": "0.0.15",
3
+ "version": "0.0.18",
4
4
  "repository": "pithings/coderaft",
5
5
  "bin": {
6
6
  "coderaft": "./dist/cli.mjs"
@@ -11,11 +11,13 @@
11
11
  "ThirdPartyNotices.txt",
12
12
  "tar.mjs",
13
13
  "code.mjs",
14
+ "android-preload.cjs",
14
15
  "code.tar.zst"
15
16
  ],
16
17
  "type": "module",
17
18
  "imports": {
18
- "#code": "./code.mjs"
19
+ "#code": "./code.mjs",
20
+ "#android-preload": "./android-preload.cjs"
19
21
  },
20
22
  "scripts": {
21
23
  "build": "obuild"
@@ -62,6 +64,7 @@
62
64
  "vscode-oniguruma": "1.7.0",
63
65
  "vscode-regexpp": "^3.1.0",
64
66
  "vscode-textmate": "^9.3.2",
67
+ "httpxy": "^0.5.0",
65
68
  "ws": "^8.19.0",
66
69
  "yauzl": "^3.0.0",
67
70
  "yazl": "^2.4.3"