@devframes/plugin-code-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ import { CodeServerAuth, CodeServerDetection, CodeServerOptions, CodeServerServerInfo, CodeServerSharedState, CodeServerStartRequest, CodeServerStartResult, CodeServerStatus, CodeServerStatusResult } from "./types.mjs";
2
+ import { i as PLUGIN_ID, l as getCookieSessionName, n as DEFAULT_PORT, o as STATE_KEY, t as DEFAULT_CODE_SERVER_PORT } from "./constants-ICLvdiRe.mjs";
3
+ import { DevframeDefinition } from "devframe/types";
4
+
5
+ //#region src/index.d.ts
6
+ /**
7
+ * Build a {@link DevframeDefinition} for the code-server panel. The same
8
+ * definition runs standalone (`createCli`), mounts into a Vite host
9
+ * (`/vite`), or docks inside a hub — its `setup` only relies on the core
10
+ * devframe RPC + shared-state surface.
11
+ *
12
+ * @experimental This plugin is experimental and may change without a major
13
+ * version bump until it stabilizes.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { createCodeServerDevframe } from '@devframes/plugin-code-server'
18
+ *
19
+ * export default createCodeServerDevframe({ serverPort: 8080 })
20
+ * ```
21
+ */
22
+ declare function createCodeServerDevframe(options?: CodeServerOptions): DevframeDefinition;
23
+ /** Default-configured code-server devframe. */
24
+ declare const codeServer: DevframeDefinition;
25
+ //#endregion
26
+ export { CodeServerAuth, CodeServerDetection, CodeServerOptions, CodeServerServerInfo, CodeServerSharedState, CodeServerStartRequest, CodeServerStartResult, CodeServerStatus, CodeServerStatusResult, DEFAULT_CODE_SERVER_PORT, DEFAULT_PORT, PLUGIN_ID, STATE_KEY, createCodeServerDevframe, codeServer as default, getCookieSessionName };
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { n as createCodeServerDevframe, t as codeServer } from "./src-BDRH3xta.mjs";
2
+ import { DEFAULT_CODE_SERVER_PORT, DEFAULT_PORT, PLUGIN_ID, STATE_KEY, getCookieSessionName } from "./constants.mjs";
3
+ export { DEFAULT_CODE_SERVER_PORT, DEFAULT_PORT, PLUGIN_ID, STATE_KEY, createCodeServerDevframe, codeServer as default, getCookieSessionName };
@@ -0,0 +1,165 @@
1
+ import { CodeServerDetection, CodeServerOptions, CodeServerStartRequest, CodeServerStartResult, CodeServerStatusResult } from "../types.mjs";
2
+ import { DevframeNodeContext } from "devframe/types";
3
+ import * as _$nostics from "nostics";
4
+ import { Diagnostic } from "nostics";
5
+
6
+ //#region src/node/supervisor.d.ts
7
+ /**
8
+ * Owns the lifecycle of a single code-server child process: detects the
9
+ * binary, launches it with a freshly generated password-auth token, probes
10
+ * `/healthz` for readiness, and mirrors a secret-free status into shared
11
+ * state. The auth cookie is handed back only through `start()` / `status()`
12
+ * so the already-authorized client can sign the iframe in automatically.
13
+ *
14
+ * Depends only on the core devframe context (shared state), not on the hub.
15
+ */
16
+ declare class CodeServerSupervisor {
17
+ private readonly ctx;
18
+ private readonly bin;
19
+ private readonly workspace;
20
+ private readonly host;
21
+ private readonly forcedPort?;
22
+ private readonly extraArgs;
23
+ private readonly extraEnv;
24
+ private readonly cookieName;
25
+ private readonly cookieSuffix?;
26
+ private readonly startTimeout;
27
+ private state?;
28
+ private detection;
29
+ private server;
30
+ private proc?;
31
+ private cookieValue?;
32
+ private logBuffer;
33
+ private cleanupRegistered;
34
+ /** Stable id of the hub terminal session, reused across start/stop. */
35
+ private readonly sessionId;
36
+ /** The live hub terminal session when launched through `ctx.terminals`. */
37
+ private session?;
38
+ constructor(ctx: DevframeNodeContext, options?: CodeServerOptions);
39
+ /** Resolve shared state, register process-exit cleanup, run first detection. */
40
+ init(): Promise<void>;
41
+ /** Re-probe for the code-server binary and publish the result. */
42
+ detect(): Promise<CodeServerDetection>;
43
+ /** Current status (+ auth when running) for the launcher UI. */
44
+ status(): CodeServerStatusResult;
45
+ /**
46
+ * Launch code-server (if not already up) and resolve once it answers its
47
+ * readiness probe. Idempotent while starting/running — returns the live
48
+ * status instead of spawning a second process.
49
+ */
50
+ start(req?: CodeServerStartRequest): Promise<CodeServerStartResult>;
51
+ /** Stop the code-server process and reset to `stopped`. */
52
+ stop(): CodeServerStatusResult;
53
+ /** Kill the process on host shutdown / test teardown. */
54
+ dispose(): void;
55
+ private authInfo;
56
+ private terminate;
57
+ /**
58
+ * Resolve the hub's terminals subsystem when this devframe is mounted in a
59
+ * hub. `ctx.terminals` only exists on a `DevframeHubContext`, so it is
60
+ * duck-typed — standalone runtimes (CLI / Vite / build) have no such property
61
+ * and fall back to a direct child process.
62
+ */
63
+ private resolveHubTerminals;
64
+ /** Update the mirrored hub terminal session's status, when one exists. */
65
+ private reflectHub;
66
+ /**
67
+ * Launch code-server. In a hub, spawn it through `ctx.terminals` so it shows
68
+ * up as a read-only terminal session (proper icon + name) whose output the
69
+ * hub streams to its terminals panel; standalone, spawn it directly. Either
70
+ * way, return the underlying {@link ChildProcess} so the shared readiness /
71
+ * port / log wiring in `start()` is identical.
72
+ */
73
+ private launchProcess;
74
+ private appendLog;
75
+ private lastLog;
76
+ private publish;
77
+ /**
78
+ * Poll code-server's unauthenticated `/healthz` endpoint until it responds
79
+ * or the timeout elapses. Returns false if the process exits first.
80
+ */
81
+ private waitForReady;
82
+ private registerCleanup;
83
+ }
84
+ //#endregion
85
+ //#region src/node/context.d.ts
86
+ declare function setCodeServerSupervisor(ctx: DevframeNodeContext, supervisor: CodeServerSupervisor): void;
87
+ declare function getCodeServerSupervisor(ctx: DevframeNodeContext): CodeServerSupervisor;
88
+ //#endregion
89
+ //#region src/node/detect.d.ts
90
+ interface DetectCodeServerResult {
91
+ installed: boolean;
92
+ version?: string;
93
+ bin: string;
94
+ }
95
+ /**
96
+ * Probe the host for a usable code-server binary by running
97
+ * `<bin> --version`. Resolves to `installed: false` when the binary is
98
+ * missing (ENOENT), errors, or exits non-zero — never throws — so the
99
+ * launcher can fall back to install instructions.
100
+ *
101
+ * `code-server --version` prints e.g. `4.96.4 abc123 with Code 1.96.4`; the
102
+ * first semver-looking token is taken as the version. Matching a semver
103
+ * pattern (rather than the leading whitespace token) keeps cold-start i18n
104
+ * noise — e.g. an `i18next: …` initialization line printed before the version
105
+ * — from leaking into the reported version.
106
+ */
107
+ declare function detectCodeServer(bin?: string, timeoutMs?: number): Promise<DetectCodeServerResult>;
108
+ //#endregion
109
+ //#region src/node/diagnostics.d.ts
110
+ interface ReporterOptions {
111
+ method?: 'log' | 'warn' | 'error';
112
+ }
113
+ declare function reporter(d: Diagnostic, {
114
+ method
115
+ }?: ReporterOptions): void;
116
+ /**
117
+ * Structured diagnostics for the code-server plugin. Uses the plugin's own
118
+ * `DP_CODE_SERVER_` prefix per the built-in plugin convention, keeping it
119
+ * collision-free with devframe core (`DF`) and the hub (`DF8xxx`).
120
+ */
121
+ declare const diagnostics: _$nostics.Diagnostics<{
122
+ readonly DP_CODE_SERVER_0001: {
123
+ readonly why: (p: {
124
+ bin: string;
125
+ }) => string;
126
+ readonly fix: "Install code-server — e.g. `curl -fsSL https://code-server.dev/install.sh | sh` — or set the `bin` option to its path. See https://coder.com/docs/code-server/latest/install";
127
+ };
128
+ readonly DP_CODE_SERVER_0002: {
129
+ readonly why: (p: {
130
+ port: number;
131
+ timeout: number;
132
+ }) => string;
133
+ readonly fix: "Check the code-server logs for startup errors, raise `startTimeout`, or free the port.";
134
+ };
135
+ readonly DP_CODE_SERVER_0003: {
136
+ readonly why: (p: {
137
+ bin: string;
138
+ reason: string;
139
+ }) => string;
140
+ };
141
+ readonly DP_CODE_SERVER_0004: {
142
+ readonly why: "code-server supervisor is not initialised on this context";
143
+ readonly fix: "Call setupCodeServer(ctx) (or use createCodeServerDevframe) before invoking the code-server RPCs.";
144
+ };
145
+ readonly DP_CODE_SERVER_0005: {
146
+ readonly why: (p: {
147
+ code: number;
148
+ }) => string;
149
+ readonly fix: "Inspect the captured output in the launcher and re-launch.";
150
+ };
151
+ }, readonly [typeof reporter]>;
152
+ //#endregion
153
+ //#region src/node/index.d.ts
154
+ /**
155
+ * Wire the code-server subsystem onto a devframe node context: create the
156
+ * {@link CodeServerSupervisor}, run the initial binary detection, publish
157
+ * status into shared state, and register the control RPC functions. Returns
158
+ * the supervisor so callers can launch/stop or dispose it on shutdown.
159
+ *
160
+ * Works in any devframe runtime (CLI, Vite, embedded, build) — it only relies
161
+ * on the core `ctx.rpc` shared-state surface, not on the hub.
162
+ */
163
+ declare function setupCodeServer(ctx: DevframeNodeContext, options?: CodeServerOptions): Promise<CodeServerSupervisor>;
164
+ //#endregion
165
+ export { CodeServerSupervisor, detectCodeServer, diagnostics, getCodeServerSupervisor, setCodeServerSupervisor, setupCodeServer };
@@ -0,0 +1,491 @@
1
+ import { DEFAULT_CODE_SERVER_PORT, PLUGIN_ID, STATE_KEY, TERMINAL_SESSION_ICON, TERMINAL_SESSION_TITLE, getCookieSessionName } from "../constants.mjs";
2
+ import { i as diagnostics, n as getCodeServerSupervisor, r as setCodeServerSupervisor, t as serverFunctions } from "../rpc-njPEEDSx.mjs";
3
+ import { execSync, spawn } from "node:child_process";
4
+ import { createHash, randomBytes } from "node:crypto";
5
+ import { request } from "node:http";
6
+ import process from "node:process";
7
+ import { getPort } from "get-port-please";
8
+ //#region src/node/detect.ts
9
+ /**
10
+ * Probe the host for a usable code-server binary by running
11
+ * `<bin> --version`. Resolves to `installed: false` when the binary is
12
+ * missing (ENOENT), errors, or exits non-zero — never throws — so the
13
+ * launcher can fall back to install instructions.
14
+ *
15
+ * `code-server --version` prints e.g. `4.96.4 abc123 with Code 1.96.4`; the
16
+ * first semver-looking token is taken as the version. Matching a semver
17
+ * pattern (rather than the leading whitespace token) keeps cold-start i18n
18
+ * noise — e.g. an `i18next: …` initialization line printed before the version
19
+ * — from leaking into the reported version.
20
+ */
21
+ function detectCodeServer(bin = "code-server", timeoutMs = 5e3) {
22
+ return new Promise((resolve) => {
23
+ let settled = false;
24
+ let stdout = "";
25
+ const finish = (result) => {
26
+ if (settled) return;
27
+ settled = true;
28
+ resolve(result);
29
+ };
30
+ let child;
31
+ try {
32
+ child = spawn(bin, ["--version"], {
33
+ windowsHide: true,
34
+ stdio: [
35
+ "ignore",
36
+ "pipe",
37
+ "ignore"
38
+ ],
39
+ shell: process.platform === "win32" && bin.endsWith(".cmd")
40
+ });
41
+ } catch {
42
+ finish({
43
+ installed: false,
44
+ bin
45
+ });
46
+ return;
47
+ }
48
+ const timer = setTimeout(() => {
49
+ child.kill();
50
+ finish({
51
+ installed: false,
52
+ bin
53
+ });
54
+ }, timeoutMs);
55
+ timer.unref?.();
56
+ child.stdout?.on("data", (chunk) => {
57
+ stdout += chunk.toString("utf8");
58
+ });
59
+ child.on("error", () => {
60
+ clearTimeout(timer);
61
+ finish({
62
+ installed: false,
63
+ bin
64
+ });
65
+ });
66
+ child.on("exit", (code) => {
67
+ clearTimeout(timer);
68
+ if (code === 0) finish({
69
+ installed: true,
70
+ version: stdout.match(/\d+\.\d+\.\d\S*/)?.[0] ?? (stdout.trim().split(/\s+/)[0] || void 0),
71
+ bin
72
+ });
73
+ else finish({
74
+ installed: false,
75
+ bin
76
+ });
77
+ });
78
+ });
79
+ }
80
+ //#endregion
81
+ //#region src/node/supervisor.ts
82
+ /** Recent output lines retained for surfacing startup failures. */
83
+ const LOG_BUFFER_LINES = 200;
84
+ /** Interval between readiness probes. */
85
+ const PROBE_INTERVAL = 250;
86
+ /**
87
+ * Owns the lifecycle of a single code-server child process: detects the
88
+ * binary, launches it with a freshly generated password-auth token, probes
89
+ * `/healthz` for readiness, and mirrors a secret-free status into shared
90
+ * state. The auth cookie is handed back only through `start()` / `status()`
91
+ * so the already-authorized client can sign the iframe in automatically.
92
+ *
93
+ * Depends only on the core devframe context (shared state), not on the hub.
94
+ */
95
+ var CodeServerSupervisor = class {
96
+ ctx;
97
+ bin;
98
+ workspace;
99
+ host;
100
+ forcedPort;
101
+ extraArgs;
102
+ extraEnv;
103
+ cookieName;
104
+ cookieSuffix;
105
+ startTimeout;
106
+ state;
107
+ detection;
108
+ server = { status: "stopped" };
109
+ proc;
110
+ cookieValue;
111
+ logBuffer = [];
112
+ cleanupRegistered = false;
113
+ /** Stable id of the hub terminal session, reused across start/stop. */
114
+ sessionId;
115
+ /** The live hub terminal session when launched through `ctx.terminals`. */
116
+ session;
117
+ constructor(ctx, options = {}) {
118
+ this.ctx = ctx;
119
+ this.bin = options.bin ?? "code-server";
120
+ this.workspace = options.cwd ?? ctx.cwd;
121
+ this.host = options.host ?? "0.0.0.0";
122
+ this.forcedPort = options.serverPort;
123
+ this.extraArgs = options.args ?? [];
124
+ this.extraEnv = options.env ?? {};
125
+ this.cookieSuffix = options.cookieSuffix;
126
+ this.cookieName = getCookieSessionName(options.cookieSuffix);
127
+ this.startTimeout = options.startTimeout ?? 3e4;
128
+ this.detection = {
129
+ checked: false,
130
+ installed: false,
131
+ bin: this.bin
132
+ };
133
+ this.sessionId = options.cookieSuffix ? `${PLUGIN_ID}:${options.cookieSuffix}` : PLUGIN_ID;
134
+ }
135
+ /** Resolve shared state, register process-exit cleanup, run first detection. */
136
+ async init() {
137
+ if (this.state) return;
138
+ this.state = await this.ctx.rpc.sharedState.get(STATE_KEY, { initialValue: {
139
+ detection: this.detection,
140
+ server: this.server
141
+ } });
142
+ this.registerCleanup();
143
+ await this.detect();
144
+ }
145
+ /** Re-probe for the code-server binary and publish the result. */
146
+ async detect() {
147
+ const result = await detectCodeServer(this.bin);
148
+ this.detection = {
149
+ checked: true,
150
+ ...result
151
+ };
152
+ this.publish();
153
+ return this.detection;
154
+ }
155
+ /** Current status (+ auth when running) for the launcher UI. */
156
+ status() {
157
+ return {
158
+ detection: { ...this.detection },
159
+ server: { ...this.server },
160
+ auth: this.authInfo()
161
+ };
162
+ }
163
+ /**
164
+ * Launch code-server (if not already up) and resolve once it answers its
165
+ * readiness probe. Idempotent while starting/running — returns the live
166
+ * status instead of spawning a second process.
167
+ */
168
+ async start(req = {}) {
169
+ if (this.server.status === "running" || this.server.status === "starting") return this.status();
170
+ if (!this.detection.checked) await this.detect();
171
+ if (!this.detection.installed) throw diagnostics.DP_CODE_SERVER_0001({ bin: this.bin });
172
+ const initialPort = this.forcedPort !== void 0 && this.forcedPort !== 0 ? this.forcedPort : this.forcedPort === 0 ? 0 : await getPort({
173
+ host: "127.0.0.1",
174
+ port: DEFAULT_CODE_SERVER_PORT
175
+ });
176
+ const token = randomBytes(32).toString("hex");
177
+ this.cookieValue = createHash("sha256").update(token).digest("hex");
178
+ const folder = req.folder ?? this.workspace;
179
+ const args = [
180
+ "--auth",
181
+ "password",
182
+ "--bind-addr",
183
+ `${this.host}:${initialPort}`,
184
+ "--disable-telemetry",
185
+ "--disable-update-check",
186
+ ...this.cookieSuffix ? ["--cookie-suffix", this.cookieSuffix] : [],
187
+ ...this.extraArgs,
188
+ folder
189
+ ];
190
+ const env = {};
191
+ for (const [k, v] of Object.entries(process.env)) if (v !== void 0) env[k] = v;
192
+ delete env.PASSWORD;
193
+ env.HASHED_PASSWORD = this.cookieValue;
194
+ Object.assign(env, this.extraEnv);
195
+ this.logBuffer = [];
196
+ let actualPort = initialPort;
197
+ let portResolver;
198
+ let portRejecter;
199
+ const portPromise = initialPort === 0 ? new Promise((resolve, reject) => {
200
+ portResolver = resolve;
201
+ portRejecter = reject;
202
+ }) : Promise.resolve(initialPort);
203
+ const child = await this.launchProcess(args, env, folder);
204
+ this.proc = child;
205
+ this.server = {
206
+ status: "starting",
207
+ port: actualPort || void 0,
208
+ pid: child.pid,
209
+ startedAt: Date.now()
210
+ };
211
+ this.publish();
212
+ const capture = (chunk) => {
213
+ const text = chunk.toString("utf8");
214
+ this.appendLog(text);
215
+ if (actualPort === 0 && portResolver) for (const line of text.split("\n")) {
216
+ const match = line.match(/HTTP server listening on https?:\/\/(?:[^:]+|\[[^\]]+\]):(\d+)/);
217
+ if (match) {
218
+ actualPort = Number.parseInt(match[1], 10);
219
+ this.server.port = actualPort;
220
+ this.publish();
221
+ portResolver?.(actualPort);
222
+ portResolver = void 0;
223
+ }
224
+ }
225
+ };
226
+ child.stdout?.on("data", capture);
227
+ child.stderr?.on("data", capture);
228
+ child.on("error", (error) => {
229
+ if (this.proc !== child) return;
230
+ this.server = {
231
+ status: "error",
232
+ error: error.message
233
+ };
234
+ this.proc = void 0;
235
+ this.cookieValue = void 0;
236
+ if (portRejecter) portRejecter?.(error);
237
+ this.publish();
238
+ });
239
+ child.on("exit", (code) => {
240
+ if (this.proc !== child) return;
241
+ const exitCode = code ?? 0;
242
+ const crashed = this.server.status !== "stopped" && exitCode !== 0;
243
+ this.proc = void 0;
244
+ this.cookieValue = void 0;
245
+ this.server = crashed ? {
246
+ status: "error",
247
+ error: this.lastLog() || `code-server exited with code ${exitCode}`
248
+ } : { status: "stopped" };
249
+ if (crashed) diagnostics.DP_CODE_SERVER_0005({ code: exitCode }, { method: "warn" });
250
+ this.reflectHub(crashed ? "error" : "stopped");
251
+ this.session = void 0;
252
+ if (portRejecter) portRejecter?.(/* @__PURE__ */ new Error(`code-server exited before binding (code ${exitCode})`));
253
+ this.publish();
254
+ });
255
+ try {
256
+ const port = await Promise.race([portPromise, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("timeout waiting for dynamic port allocation")), this.startTimeout))]);
257
+ if (!await this.waitForReady(port)) throw new Error(this.lastLog() || "startup timed out");
258
+ if (this.proc !== child) return this.status();
259
+ this.server = {
260
+ status: "running",
261
+ port,
262
+ pid: child.pid,
263
+ startedAt: this.server.startedAt
264
+ };
265
+ this.publish();
266
+ return this.status();
267
+ } catch (error) {
268
+ if (this.proc === child) {
269
+ this.terminate(child);
270
+ this.reflectHub("error");
271
+ this.session = void 0;
272
+ this.server = {
273
+ status: "error",
274
+ error: error instanceof Error ? error.message : String(error)
275
+ };
276
+ this.proc = void 0;
277
+ this.cookieValue = void 0;
278
+ this.publish();
279
+ }
280
+ throw diagnostics.DP_CODE_SERVER_0002({
281
+ port: actualPort,
282
+ timeout: this.startTimeout
283
+ });
284
+ }
285
+ }
286
+ /** Stop the code-server process and reset to `stopped`. */
287
+ stop() {
288
+ const child = this.proc;
289
+ this.proc = void 0;
290
+ this.cookieValue = void 0;
291
+ this.server = { status: "stopped" };
292
+ if (child) this.terminate(child);
293
+ this.reflectHub("stopped");
294
+ this.session = void 0;
295
+ this.publish();
296
+ return this.status();
297
+ }
298
+ /** Kill the process on host shutdown / test teardown. */
299
+ dispose() {
300
+ if (this.proc) this.terminate(this.proc);
301
+ this.proc = void 0;
302
+ this.cookieValue = void 0;
303
+ this.session = void 0;
304
+ }
305
+ authInfo() {
306
+ if (this.server.status !== "running" || !this.cookieValue) return void 0;
307
+ return {
308
+ cookieName: this.cookieName,
309
+ cookieValue: this.cookieValue
310
+ };
311
+ }
312
+ terminate(child) {
313
+ if (this.session) {
314
+ this.session.terminate().catch(() => {});
315
+ return;
316
+ }
317
+ try {
318
+ if (process.platform === "win32" && this.bin.endsWith(".cmd") && child.pid) execSync(`taskkill /pid ${child.pid} /t /f`, { stdio: "ignore" });
319
+ else child.kill("SIGTERM");
320
+ } catch {}
321
+ }
322
+ /**
323
+ * Resolve the hub's terminals subsystem when this devframe is mounted in a
324
+ * hub. `ctx.terminals` only exists on a `DevframeHubContext`, so it is
325
+ * duck-typed — standalone runtimes (CLI / Vite / build) have no such property
326
+ * and fall back to a direct child process.
327
+ */
328
+ resolveHubTerminals() {
329
+ const terminals = this.ctx.terminals;
330
+ return terminals && typeof terminals.startChildProcess === "function" ? terminals : void 0;
331
+ }
332
+ /** Update the mirrored hub terminal session's status, when one exists. */
333
+ reflectHub(status) {
334
+ const hub = this.resolveHubTerminals();
335
+ if (hub?.sessions.has(this.sessionId)) hub.update({
336
+ id: this.sessionId,
337
+ status
338
+ });
339
+ }
340
+ /**
341
+ * Launch code-server. In a hub, spawn it through `ctx.terminals` so it shows
342
+ * up as a read-only terminal session (proper icon + name) whose output the
343
+ * hub streams to its terminals panel; standalone, spawn it directly. Either
344
+ * way, return the underlying {@link ChildProcess} so the shared readiness /
345
+ * port / log wiring in `start()` is identical.
346
+ */
347
+ async launchProcess(args, env, folder) {
348
+ const hub = this.resolveHubTerminals();
349
+ if (hub) {
350
+ const stale = hub.sessions.get(this.sessionId);
351
+ if (stale) hub.remove?.(stale);
352
+ try {
353
+ const session = await hub.startChildProcess({
354
+ command: this.bin,
355
+ args,
356
+ cwd: folder,
357
+ env: {
358
+ ...env,
359
+ PASSWORD: ""
360
+ }
361
+ }, {
362
+ id: this.sessionId,
363
+ title: TERMINAL_SESSION_TITLE,
364
+ description: folder,
365
+ icon: TERMINAL_SESSION_ICON
366
+ });
367
+ const child = session.getChildProcess();
368
+ if (!child) throw new Error("code-server process handle was unavailable");
369
+ this.session = session;
370
+ return child;
371
+ } catch (error) {
372
+ const reason = error instanceof Error ? error.message : String(error);
373
+ const orphan = hub.sessions.get(this.sessionId);
374
+ if (orphan) hub.remove?.(orphan);
375
+ this.session = void 0;
376
+ this.server = {
377
+ status: "error",
378
+ error: reason
379
+ };
380
+ this.publish();
381
+ throw diagnostics.DP_CODE_SERVER_0003({
382
+ bin: this.bin,
383
+ reason
384
+ });
385
+ }
386
+ }
387
+ try {
388
+ return spawn(this.bin, args, {
389
+ cwd: folder,
390
+ env,
391
+ windowsHide: true,
392
+ stdio: [
393
+ "ignore",
394
+ "pipe",
395
+ "pipe"
396
+ ],
397
+ shell: process.platform === "win32" && this.bin.endsWith(".cmd")
398
+ });
399
+ } catch (error) {
400
+ const reason = error instanceof Error ? error.message : String(error);
401
+ this.server = {
402
+ status: "error",
403
+ error: reason
404
+ };
405
+ this.publish();
406
+ throw diagnostics.DP_CODE_SERVER_0003({
407
+ bin: this.bin,
408
+ reason
409
+ });
410
+ }
411
+ }
412
+ appendLog(text) {
413
+ for (const line of text.split("\n")) {
414
+ if (line.length === 0) continue;
415
+ this.logBuffer.push(line);
416
+ }
417
+ if (this.logBuffer.length > LOG_BUFFER_LINES) this.logBuffer.splice(0, this.logBuffer.length - LOG_BUFFER_LINES);
418
+ }
419
+ lastLog() {
420
+ return this.logBuffer.length ? this.logBuffer[this.logBuffer.length - 1] : void 0;
421
+ }
422
+ publish() {
423
+ this.state?.mutate((draft) => {
424
+ draft.detection = { ...this.detection };
425
+ draft.server = { ...this.server };
426
+ });
427
+ }
428
+ /**
429
+ * Poll code-server's unauthenticated `/healthz` endpoint until it responds
430
+ * or the timeout elapses. Returns false if the process exits first.
431
+ */
432
+ async waitForReady(port) {
433
+ const deadline = Date.now() + this.startTimeout;
434
+ while (Date.now() < deadline) {
435
+ if (!this.proc) return false;
436
+ if (await probeHealthz(port)) return true;
437
+ await delay(PROBE_INTERVAL);
438
+ }
439
+ return false;
440
+ }
441
+ registerCleanup() {
442
+ if (this.cleanupRegistered) return;
443
+ this.cleanupRegistered = true;
444
+ process.once("exit", () => this.dispose());
445
+ }
446
+ };
447
+ function delay(ms) {
448
+ return new Promise((resolve) => {
449
+ setTimeout(resolve, ms).unref?.();
450
+ });
451
+ }
452
+ function probeHealthz(port) {
453
+ return new Promise((resolve) => {
454
+ const req = request({
455
+ host: "127.0.0.1",
456
+ port,
457
+ path: "/healthz",
458
+ method: "GET",
459
+ timeout: 1500
460
+ }, (res) => {
461
+ res.resume();
462
+ resolve((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 500);
463
+ });
464
+ req.on("error", () => resolve(false));
465
+ req.on("timeout", () => {
466
+ req.destroy();
467
+ resolve(false);
468
+ });
469
+ req.end();
470
+ });
471
+ }
472
+ //#endregion
473
+ //#region src/node/index.ts
474
+ /**
475
+ * Wire the code-server subsystem onto a devframe node context: create the
476
+ * {@link CodeServerSupervisor}, run the initial binary detection, publish
477
+ * status into shared state, and register the control RPC functions. Returns
478
+ * the supervisor so callers can launch/stop or dispose it on shutdown.
479
+ *
480
+ * Works in any devframe runtime (CLI, Vite, embedded, build) — it only relies
481
+ * on the core `ctx.rpc` shared-state surface, not on the hub.
482
+ */
483
+ async function setupCodeServer(ctx, options = {}) {
484
+ const supervisor = new CodeServerSupervisor(ctx, options);
485
+ setCodeServerSupervisor(ctx, supervisor);
486
+ await supervisor.init();
487
+ for (const fn of serverFunctions) ctx.rpc.register(fn);
488
+ return supervisor;
489
+ }
490
+ //#endregion
491
+ export { CodeServerSupervisor, detectCodeServer, diagnostics, getCodeServerSupervisor, setCodeServerSupervisor, setupCodeServer };