coderaft 0.0.24 → 0.0.26

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
@@ -135,6 +135,34 @@ interface CodeServerHandle {
135
135
  }
136
136
  ```
137
137
 
138
+ ### `spawnCodeServer(options)`
139
+
140
+ Runs the code-server in a forked Node.js child process — useful for isolating VS Code's singletons from the host or restarting the server without taking the host down. Accepts the same options as `startCodeServer`, plus a `spawn` sub-object (`env`, `execArgv`, `stdio`, `startupTimeout`).
141
+
142
+ ```ts
143
+ import { spawnCodeServer } from "coderaft";
144
+
145
+ const instance = await spawnCodeServer({
146
+ port: 6063,
147
+ defaultFolder: "/path/to/workspace",
148
+ });
149
+
150
+ console.log(`Ready at ${instance.url}`);
151
+
152
+ // Notify on unexpected worker exits (not fired during close/reload):
153
+ instance.on("exit", (code, signal) => {
154
+ console.warn(`worker died code=${code} signal=${signal}`);
155
+ });
156
+
157
+ // Restart the worker in place (reads fresh url/port/connectionToken after):
158
+ await instance.reload();
159
+
160
+ // Graceful shutdown — SIGTERM to the worker, then SIGKILL after 5s:
161
+ await instance.close();
162
+ ```
163
+
164
+ Options cross an IPC boundary, so nested values (`vscode`, etc.) must be JSON-compatible.
165
+
138
166
  ## CLI Options
139
167
 
140
168
  ### Server
package/code.tar.zst CHANGED
Binary file
@@ -317,11 +317,19 @@ async function startCodeServer(opts = {}) {
317
317
  const address = server.address();
318
318
  const actualPort = address && typeof address === "object" && "port" in address ? address.port : void 0;
319
319
  const basePath = normalizeBaseURL(opts.baseURL ?? opts.vscode?.["server-base-path"]);
320
+ let url;
321
+ if (socketPath) url = `unix:${socketPath}`;
322
+ else {
323
+ const baseUrl = new URL(`http://localhost:${actualPort}${basePath}/`);
324
+ if (handler.connectionToken) baseUrl.searchParams.set("tkn", handler.connectionToken);
325
+ const rawUrl = opts.formatURL ? opts.formatURL(baseUrl) ?? baseUrl : baseUrl;
326
+ url = rawUrl instanceof URL ? rawUrl.toString() : rawUrl;
327
+ }
320
328
  return {
321
329
  server,
322
330
  port: actualPort,
323
331
  socketPath,
324
- url: socketPath ? `unix:${socketPath}` : handler.connectionToken ? `http://localhost:${actualPort}${basePath}/?tkn=${handler.connectionToken}` : `http://localhost:${actualPort}${basePath}/`,
332
+ url,
325
333
  connectionToken: handler.connectionToken,
326
334
  async close() {
327
335
  await handler.dispose();
package/dist/index.d.mts CHANGED
@@ -1,4 +1,6 @@
1
+ import { ChildProcess } from "node:child_process";
1
2
  import { IncomingMessage, Server, ServerResponse } from "node:http";
3
+ import { EventEmitter } from "node:events";
2
4
  import { Duplex } from "node:stream";
3
5
  /**
4
6
  * Options for `createServer()` from VS Code server
@@ -81,6 +83,12 @@ interface VSCodeServerOptions {
81
83
  interface CreateCodeServerOptions {
82
84
  /** Workspace folder opened when no input is given in the URL. */
83
85
  defaultFolder?: string;
86
+ /**
87
+ * Customize the URL returned by the server. Called with the base URL after
88
+ * auth params are applied. Return a modified URL, a string, or `undefined`
89
+ * to keep the original.
90
+ */
91
+ formatURL?: (url: URL) => string | URL | undefined;
84
92
  /** Connection token (shared auth secret). Auto-generated if omitted. */
85
93
  connectionToken?: string;
86
94
  /** Host/interface to bind (used to infer local-only access for token default). */
@@ -120,4 +128,59 @@ interface CodeServerHandle {
120
128
  }
121
129
  declare function createCodeServer(opts?: CreateCodeServerOptions): Promise<CodeServerHandler>;
122
130
  declare function startCodeServer(opts?: StartCodeServerOptions): Promise<CodeServerHandle>;
123
- export { type CodeServerHandle, type CodeServerHandler, type CreateCodeServerOptions, type StartCodeServerOptions, createCodeServer, startCodeServer };
131
+ interface SpawnProcessOptions {
132
+ /** Extra environment variables merged into the worker's `process.env`. */
133
+ env?: NodeJS.ProcessEnv;
134
+ /** Node.js exec argv forwarded to the worker (e.g. `["--inspect"]`). */
135
+ execArgv?: string[];
136
+ /**
137
+ * stdio for the forked worker. Defaults to `"inherit"` for stdout/stderr and
138
+ * `"ignore"` for stdin. Pass `"pipe"` to capture output via `handle.proc`.
139
+ */
140
+ stdio?: "inherit" | "pipe" | "ignore";
141
+ /**
142
+ * Max ms to wait for the worker to report `ready`. Defaults to 60000.
143
+ * On timeout, the worker is killed and the promise rejects.
144
+ */
145
+ startupTimeout?: number;
146
+ }
147
+ interface SpawnCodeServerOptions extends StartCodeServerOptions {
148
+ /** Options for the forked worker process. */
149
+ spawn?: SpawnProcessOptions;
150
+ }
151
+ interface SpawnedCodeServer {
152
+ /**
153
+ * Emitted when the handle loses its worker: crash, explicit kill, or a
154
+ * `reload()` whose respawn failed (the old worker is already gone; the
155
+ * failure also rejects the `reload()` promise). Not emitted for `close()`
156
+ * or the successful half of `reload()`.
157
+ */
158
+ on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
159
+ /**
160
+ * Emitted for worker `error` events (spawn failure, IPC send failure) and
161
+ * for post-ready fatal errors forwarded by the worker (uncaughtException,
162
+ * unhandledRejection).
163
+ */
164
+ on(event: "error", listener: (err: Error) => void): this;
165
+ on(event: string, listener: (...args: unknown[]) => void): this;
166
+ }
167
+ declare class SpawnedCodeServer extends EventEmitter {
168
+ #private;
169
+ /** The forked worker process (changes after `reload`). */
170
+ proc: ChildProcess;
171
+ url: string;
172
+ /** TCP port the server is bound to, or `undefined` when listening on a unix socket. */
173
+ port?: number;
174
+ /** Unix socket path the server is bound to, or `undefined` when listening on TCP. */
175
+ socketPath?: string;
176
+ connectionToken: string;
177
+ private constructor();
178
+ /** Spawn a new worker and resolve with a handle once it reports ready. */
179
+ static spawn(opts?: SpawnCodeServerOptions): Promise<SpawnedCodeServer>;
180
+ /** Terminate the worker process. Sends SIGTERM, then SIGKILL after 5s. Idempotent. */
181
+ close(): Promise<void>;
182
+ /** Kill the current worker and spawn a new one with the original options. */
183
+ reload(): Promise<void>;
184
+ }
185
+ declare function spawnCodeServer(opts?: SpawnCodeServerOptions): Promise<SpawnedCodeServer>;
186
+ export { type CodeServerHandle, type CodeServerHandler, type CreateCodeServerOptions, type SpawnCodeServerOptions, type SpawnProcessOptions, SpawnedCodeServer, type StartCodeServerOptions, createCodeServer, spawnCodeServer, startCodeServer };
package/dist/index.mjs CHANGED
@@ -1,2 +1,171 @@
1
1
  import { r as startCodeServer, t as createCodeServer } from "./_chunks/server.mjs";
2
- export { createCodeServer, startCodeServer };
2
+ import { fileURLToPath } from "node:url";
3
+ import { fork } from "node:child_process";
4
+ import { EventEmitter } from "node:events";
5
+ var SpawnedCodeServer = class SpawnedCodeServer extends EventEmitter {
6
+ proc;
7
+ url;
8
+ port;
9
+ socketPath;
10
+ connectionToken;
11
+ #opts;
12
+ #detach;
13
+ #reloading;
14
+ #closed = false;
15
+ constructor(opts, state) {
16
+ super();
17
+ this.#opts = opts;
18
+ this.#adopt(state);
19
+ }
20
+ static async spawn(opts = {}) {
21
+ return new SpawnedCodeServer(opts, await spawnWorker(opts));
22
+ }
23
+ async close() {
24
+ if (this.#closed) return;
25
+ this.#closed = true;
26
+ if (this.#reloading) try {
27
+ await this.#reloading;
28
+ } catch {}
29
+ this.#detach();
30
+ await terminateChild(this.proc);
31
+ }
32
+ async reload() {
33
+ if (this.#closed) throw new Error("SpawnedCodeServer is closed");
34
+ if (this.#reloading) return this.#reloading;
35
+ this.#reloading = (async () => {
36
+ this.#detach();
37
+ await terminateChild(this.proc);
38
+ if (this.#closed) throw new Error("SpawnedCodeServer was closed during reload");
39
+ let next;
40
+ try {
41
+ next = await spawnWorker(this.#opts);
42
+ } catch (err) {
43
+ const dead = this.proc;
44
+ queueMicrotask(() => this.emit("exit", dead.exitCode, dead.signalCode));
45
+ throw err;
46
+ }
47
+ if (this.#closed) {
48
+ await terminateChild(next.proc);
49
+ throw new Error("SpawnedCodeServer was closed during reload");
50
+ }
51
+ this.#adopt(next);
52
+ })();
53
+ try {
54
+ await this.#reloading;
55
+ } finally {
56
+ this.#reloading = void 0;
57
+ }
58
+ }
59
+ #adopt(state) {
60
+ this.proc = state.proc;
61
+ this.url = state.url;
62
+ this.port = state.port;
63
+ this.socketPath = state.socketPath;
64
+ this.connectionToken = state.connectionToken;
65
+ this.#detach = this.#attachListeners(state.proc);
66
+ }
67
+ #attachListeners(proc) {
68
+ const onExit = (code, signal) => this.emit("exit", code, signal);
69
+ const onError = (err) => this.emit("error", err);
70
+ const onMessage = (msg) => {
71
+ if (msg?.type === "error") this.emit("error", new Error(msg.message ?? "worker error"));
72
+ };
73
+ proc.on("exit", onExit);
74
+ proc.on("error", onError);
75
+ proc.on("message", onMessage);
76
+ return () => {
77
+ proc.off("exit", onExit);
78
+ proc.off("error", onError);
79
+ proc.off("message", onMessage);
80
+ };
81
+ }
82
+ };
83
+ function spawnCodeServer(opts = {}) {
84
+ return SpawnedCodeServer.spawn(opts);
85
+ }
86
+ async function spawnWorker(opts) {
87
+ const { spawn: spawnOpts = {}, ...serverOpts } = opts;
88
+ const { env, execArgv, stdio = "inherit", startupTimeout = 6e4 } = spawnOpts;
89
+ const workerPath = fileURLToPath(import.meta.resolve("#worker"));
90
+ const childEnv = {
91
+ ...process.env,
92
+ ...env
93
+ };
94
+ delete childEnv.CODE_SERVER_PARENT_PID;
95
+ const proc = fork(workerPath, {
96
+ stdio: [
97
+ "ignore",
98
+ stdio,
99
+ stdio,
100
+ "ipc"
101
+ ],
102
+ env: childEnv,
103
+ ...execArgv !== void 0 ? { execArgv } : {}
104
+ });
105
+ const ready = await new Promise((resolve, reject) => {
106
+ const onMessage = (msg) => {
107
+ if (msg?.type === "ready") {
108
+ cleanup();
109
+ resolve(msg);
110
+ } else if (msg?.type === "error") {
111
+ cleanup();
112
+ reject(new Error(msg.message ?? "worker error"));
113
+ }
114
+ };
115
+ const onExit = (code) => {
116
+ cleanup();
117
+ reject(/* @__PURE__ */ new Error(`coderaft worker exited before ready (code=${code})`));
118
+ };
119
+ const onError = (err) => {
120
+ cleanup();
121
+ proc.kill("SIGKILL");
122
+ reject(err);
123
+ };
124
+ const timer = setTimeout(() => {
125
+ cleanup();
126
+ proc.kill("SIGKILL");
127
+ reject(/* @__PURE__ */ new Error(`coderaft worker did not become ready within ${startupTimeout}ms`));
128
+ }, startupTimeout);
129
+ timer.unref?.();
130
+ const cleanup = () => {
131
+ clearTimeout(timer);
132
+ proc.off("message", onMessage);
133
+ proc.off("exit", onExit);
134
+ proc.off("error", onError);
135
+ };
136
+ proc.on("message", onMessage);
137
+ proc.once("exit", onExit);
138
+ proc.once("error", onError);
139
+ proc.send({
140
+ type: "start",
141
+ opts: serverOpts
142
+ }, (err) => {
143
+ if (err) {
144
+ cleanup();
145
+ reject(err);
146
+ }
147
+ });
148
+ });
149
+ return {
150
+ proc,
151
+ url: ready.url,
152
+ port: ready.port,
153
+ socketPath: ready.socketPath,
154
+ connectionToken: ready.connectionToken
155
+ };
156
+ }
157
+ async function terminateChild(proc) {
158
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
159
+ await new Promise((resolve) => {
160
+ const timer = setTimeout(() => {
161
+ if (proc.exitCode === null && proc.signalCode === null) proc.kill("SIGKILL");
162
+ }, 5e3);
163
+ timer.unref?.();
164
+ proc.once("exit", () => {
165
+ clearTimeout(timer);
166
+ resolve();
167
+ });
168
+ proc.kill("SIGTERM");
169
+ });
170
+ }
171
+ export { SpawnedCodeServer, createCodeServer, spawnCodeServer, startCodeServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderaft",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "repository": "pithings/coderaft",
5
5
  "bin": {
6
6
  "coderaft": "./dist/cli.mjs"
@@ -11,12 +11,18 @@
11
11
  "ThirdPartyNotices.txt",
12
12
  "tar.mjs",
13
13
  "code.mjs",
14
+ "worker.mjs",
14
15
  "android-preload.cjs",
15
16
  "code.tar.zst"
16
17
  ],
17
18
  "type": "module",
19
+ "types": "./dist/index.d.mts",
20
+ "exports": {
21
+ ".": "./dist/index.mjs"
22
+ },
18
23
  "imports": {
19
24
  "#code": "./code.mjs",
25
+ "#worker": "./worker.mjs",
20
26
  "#android-preload": "./android-preload.cjs"
21
27
  },
22
28
  "scripts": {
package/worker.mjs ADDED
@@ -0,0 +1,64 @@
1
+ // Forked worker entry used by `spawnCodeServer`. Starts a coderaft code server
2
+ // on IPC `{ type: "start", opts }` and replies with `{ type: "ready", ... }`.
3
+ // On SIGTERM/SIGINT, gracefully closes the server before exiting so the parent's
4
+ // `handle.close()` cleanly disposes VS Code and releases sockets/locks.
5
+ import { startCodeServer } from "./dist/index.mjs";
6
+
7
+ let handle;
8
+ let shuttingDown = false;
9
+
10
+ process.on("message", async (msg) => {
11
+ if (msg?.type !== "start") return;
12
+ try {
13
+ handle = await startCodeServer(msg.opts);
14
+ process.send?.({
15
+ type: "ready",
16
+ url: handle.url,
17
+ port: handle.port,
18
+ socketPath: handle.socketPath,
19
+ connectionToken: handle.connectionToken,
20
+ });
21
+ } catch (err) {
22
+ // Flush the error message over IPC before exiting — `process.exit` can
23
+ // tear down the channel before the message reaches the parent, which
24
+ // would leave the parent rejecting with the generic `exited before ready`.
25
+ const exit = () => process.exit(1);
26
+ if (process.send) {
27
+ process.send({ type: "error", message: err?.message ?? String(err) }, exit);
28
+ } else {
29
+ exit();
30
+ }
31
+ }
32
+ });
33
+
34
+ const shutdown = async (signal) => {
35
+ if (shuttingDown) return;
36
+ shuttingDown = true;
37
+ try {
38
+ await handle?.close();
39
+ } catch (err) {
40
+ console.error(`[coderaft worker] close failed on ${signal}:`, err);
41
+ }
42
+ process.exit(0);
43
+ };
44
+
45
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
46
+ process.on("SIGINT", () => shutdown("SIGINT"));
47
+
48
+ // Forward fatal async errors to the parent before exiting. Without this, the
49
+ // parent only sees an anonymous non-zero exit and has to scrape stderr for the
50
+ // reason — which is the exact failure mode the pre-ready IPC flush avoids.
51
+ const forwardFatal = (kind) => (err) => {
52
+ if (shuttingDown) return;
53
+ shuttingDown = true;
54
+ const detail = err?.stack ?? err?.message ?? String(err);
55
+ const exit = () => process.exit(1);
56
+ if (process.send) {
57
+ process.send({ type: "error", message: `[${kind}] ${detail}` }, exit);
58
+ } else {
59
+ exit();
60
+ }
61
+ };
62
+
63
+ process.on("uncaughtException", forwardFatal("uncaughtException"));
64
+ process.on("unhandledRejection", forwardFatal("unhandledRejection"));