@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.
- package/LICENSE.md +21 -0
- package/README.md +84 -0
- package/bin.mjs +13 -0
- package/dist/cli.d.mts +12 -0
- package/dist/cli.mjs +13 -0
- package/dist/client/index.d.mts +75 -0
- package/dist/client/index.mjs +354 -0
- package/dist/constants-ICLvdiRe.d.mts +36 -0
- package/dist/constants.d.mts +2 -0
- package/dist/constants.mjs +38 -0
- package/dist/index.d.mts +26 -0
- package/dist/index.mjs +3 -0
- package/dist/node/index.d.mts +165 -0
- package/dist/node/index.mjs +491 -0
- package/dist/rpc/index.d.mts +71 -0
- package/dist/rpc/index.mjs +2 -0
- package/dist/rpc-njPEEDSx.mjs +96 -0
- package/dist/spa/assets/index-D5nEkfpr.js +2 -0
- package/dist/spa/assets/index-DzYBAZJX.css +1 -0
- package/dist/spa/index.html +14 -0
- package/dist/src-BDRH3xta.mjs +58 -0
- package/dist/types.d.mts +99 -0
- package/dist/types.mjs +1 -0
- package/dist/vite.d.mts +18 -0
- package/dist/vite.mjs +20 -0
- package/package.json +80 -0
package/dist/index.d.mts
ADDED
|
@@ -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 };
|