coderaft 0.0.16 → 0.0.19

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:lts-slim 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
 
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 = "c2f3188affec085e";
7
+ const codeArchiveHash = "30c02e4b77df8ee2";
8
8
 
9
9
  const archivePath = fileURLToPath(new URL("./code.tar.zst", import.meta.url));
10
10
 
package/code.tar.zst CHANGED
Binary file
@@ -151,6 +151,7 @@ async function createCodeServer(opts = {}) {
151
151
  const withoutToken = opts.vscode?.["without-connection-token"] === true || !explicitToken && isLocal;
152
152
  const connectionToken = withoutToken ? "" : opts.connectionToken ?? randomUUID();
153
153
  const defaultFolder = opts.defaultFolder ?? process.cwd();
154
+ const baseURL = normalizeBaseURL(opts.baseURL ?? opts.vscode?.["server-base-path"]);
154
155
  const mintKey = randomBytes(32);
155
156
  process.env.CODE_SERVER_PARENT_PID ??= String(process.pid);
156
157
  cleanupStaleLocks(opts.vscode?.["user-data-dir"] ?? join(_os.homedir(), ".vscode-server-oss", "data"));
@@ -172,6 +173,7 @@ async function createCodeServer(opts = {}) {
172
173
  }
173
174
  const vscodeServer = await (await mod.loadCodeWithNls()).createServer(null, {
174
175
  "default-folder": defaultFolder,
176
+ ...baseURL ? { "server-base-path": baseURL } : {},
175
177
  ...withoutToken ? { "without-connection-token": true } : { "connection-token": connectionToken },
176
178
  "reconnection-grace-time": "30",
177
179
  "disable-getting-started-override": true,
@@ -189,7 +191,8 @@ async function createCodeServer(opts = {}) {
189
191
  connectionToken,
190
192
  handleRequest(req, res) {
191
193
  const method = req.method ?? "GET";
192
- const url = (req.url ?? "/").split("?")[0];
194
+ const strippedUrl = stripBaseURL(req.url ?? "/", baseURL);
195
+ const url = strippedUrl.split("?")[0];
193
196
  if (url === "/manifest.json") {
194
197
  res.writeHead(200, {
195
198
  "Content-Type": "application/manifest+json",
@@ -232,7 +235,7 @@ async function createCodeServer(opts = {}) {
232
235
  return;
233
236
  }
234
237
  if (url === "/login" || url === "/logout") {
235
- res.writeHead(302, { Location: "/" });
238
+ res.writeHead(302, { Location: `${baseURL}/` });
236
239
  res.end();
237
240
  return;
238
241
  }
@@ -242,7 +245,7 @@ async function createCodeServer(opts = {}) {
242
245
  });
243
246
  return;
244
247
  }
245
- const proxyMatch = parseProxyPath(req.url ?? "/");
248
+ const proxyMatch = parseProxyPath(strippedUrl);
246
249
  if (proxyMatch) {
247
250
  const { port: targetPort, path: targetPath } = proxyMatch;
248
251
  proxy.web(req, res, {
@@ -255,7 +258,7 @@ async function createCodeServer(opts = {}) {
255
258
  vscodeServer.handleRequest(req, res);
256
259
  },
257
260
  handleUpgrade(req, socket, _head) {
258
- const proxyMatch = parseProxyPath(req.url ?? "/");
261
+ const proxyMatch = parseProxyPath(stripBaseURL(req.url ?? "/", baseURL));
259
262
  if (proxyMatch) {
260
263
  const { port: targetPort, path: targetPath } = proxyMatch;
261
264
  proxy.ws(req, socket, {
@@ -273,6 +276,7 @@ async function createCodeServer(opts = {}) {
273
276
  };
274
277
  }
275
278
  async function startCodeServer(opts = {}) {
279
+ const socketPath = opts.socketPath;
276
280
  const port = opts.port ?? (Number(process.env.PORT) || 6063);
277
281
  const handler = await createCodeServer(opts);
278
282
  const server = createServer((req, res) => {
@@ -281,7 +285,7 @@ async function startCodeServer(opts = {}) {
281
285
  server.on("upgrade", (req, socket, head) => {
282
286
  handler.handleUpgrade(req, socket, head);
283
287
  });
284
- const listen = (p) => new Promise((resolve, reject) => {
288
+ const listenTcp = (p) => new Promise((resolve, reject) => {
285
289
  server.once("error", reject);
286
290
  const cb = () => {
287
291
  server.removeListener("error", reject);
@@ -290,17 +294,31 @@ async function startCodeServer(opts = {}) {
290
294
  if (opts.host) server.listen(p, opts.host, cb);
291
295
  else server.listen(p, cb);
292
296
  });
293
- try {
294
- 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);
295
310
  } catch (err) {
296
- if (err?.code === "EADDRINUSE") await listen(0);
311
+ if (err?.code === "EADDRINUSE") await listenTcp(0);
297
312
  else throw err;
298
313
  }
299
- 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"]);
300
317
  return {
301
318
  server,
302
319
  port: actualPort,
303
- 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}/`,
304
322
  connectionToken: handler.connectionToken,
305
323
  async close() {
306
324
  await handler.dispose();
@@ -308,6 +326,9 @@ async function startCodeServer(opts = {}) {
308
326
  await new Promise((resolve, reject) => {
309
327
  server.close((err) => err ? reject(err) : resolve());
310
328
  });
329
+ if (socketPath) try {
330
+ unlinkSync(socketPath);
331
+ } catch {}
311
332
  }
312
333
  };
313
334
  }
@@ -364,6 +385,19 @@ function parseProxyPath(url) {
364
385
  path: (match[2] || "/") + query
365
386
  };
366
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
+ }
367
401
  function sendJson(res, status, body) {
368
402
  const payload = JSON.stringify(body);
369
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,12 +85,20 @@ 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). */
@@ -102,7 +110,10 @@ interface CodeServerHandler {
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.16",
3
+ "version": "0.0.19",
4
4
  "repository": "pithings/coderaft",
5
5
  "bin": {
6
6
  "coderaft": "./dist/cli.mjs"