@spencer-kit/coder-studio 0.4.2 → 0.4.3
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/CHANGELOG.md +8 -0
- package/dist/esm/bin.mjs +32 -2
- package/dist/esm/bin.mjs.map +2 -2
- package/dist/esm/update-worker.mjs +27 -3
- package/dist/esm/update-worker.mjs.map +2 -2
- package/dist/web/assets/components-BZf_jLGv.js +110 -0
- package/dist/web/assets/components-BZf_jLGv.js.map +1 -0
- package/dist/web/assets/{main-D3dXqSaA.js → main-CEv6hML1.js} +2 -2
- package/dist/web/assets/{main-D3dXqSaA.js.map → main-CEv6hML1.js.map} +1 -1
- package/dist/web/assets/{ui-preview-BGZz053-.js → ui-preview-Cd3euuid.js} +2 -2
- package/dist/web/assets/{ui-preview-BGZz053-.js.map → ui-preview-Cd3euuid.js.map} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/ui-preview.html +2 -2
- package/package.json +1 -1
- package/src/pm2-control.test.ts +48 -6
- package/src/pm2-control.ts +43 -2
- package/src/update-worker.test.ts +60 -0
- package/src/update-worker.ts +37 -4
- package/dist/web/assets/components-omWbMLvf.js +0 -110
- package/dist/web/assets/components-omWbMLvf.js.map +0 -1
package/dist/web/index.html
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
<meta name="description" content="Coder Studio - Agent-First Development Environment" />
|
|
7
7
|
<title>Coder Studio</title>
|
|
8
8
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
9
|
-
<script type="module" crossorigin src="/assets/main-
|
|
9
|
+
<script type="module" crossorigin src="/assets/main-CEv6hML1.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CpWojdLp.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/monaco-editor-VTbRDH-J.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/xterm-BVlcrOZ1.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/assets/components-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/components-BZf_jLGv.js">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/monaco-editor-Br_kD0ds.css">
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/xterm-BrP-ENHg.css">
|
|
16
16
|
<link rel="stylesheet" crossorigin href="/assets/components-AKM1pxhf.css">
|
package/dist/web/ui-preview.html
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Coder Studio UI Preview</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/ui-preview-
|
|
7
|
+
<script type="module" crossorigin src="/assets/ui-preview-Cd3euuid.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CpWojdLp.js">
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/monaco-editor-VTbRDH-J.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/xterm-BVlcrOZ1.js">
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/components-
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/components-BZf_jLGv.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/monaco-editor-Br_kD0ds.css">
|
|
13
13
|
<link rel="stylesheet" crossorigin href="/assets/xterm-BrP-ENHg.css">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/components-AKM1pxhf.css">
|
package/package.json
CHANGED
package/src/pm2-control.test.ts
CHANGED
|
@@ -148,14 +148,15 @@ describe("pm2-control", () => {
|
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
it("waits for the previous PM2 app to disappear before starting a replacement", async () => {
|
|
151
|
+
const previousPid = 999_990;
|
|
151
152
|
describeProcess
|
|
152
153
|
.mockImplementationOnce(
|
|
153
154
|
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
154
|
-
callback(null, [{ pid:
|
|
155
|
+
callback(null, [{ pid: previousPid, pm2_env: { status: "online", restart_time: 0 } }])
|
|
155
156
|
)
|
|
156
157
|
.mockImplementationOnce(
|
|
157
158
|
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
158
|
-
callback(null, [{ pid:
|
|
159
|
+
callback(null, [{ pid: previousPid, pm2_env: { status: "stopping", restart_time: 0 } }])
|
|
159
160
|
)
|
|
160
161
|
.mockImplementationOnce(
|
|
161
162
|
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
@@ -180,14 +181,15 @@ describe("pm2-control", () => {
|
|
|
180
181
|
});
|
|
181
182
|
|
|
182
183
|
it("reuses one pm2 session while polling deletion during startup", async () => {
|
|
184
|
+
const previousPid = 999_992;
|
|
183
185
|
describeProcess
|
|
184
186
|
.mockImplementationOnce(
|
|
185
187
|
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
186
|
-
callback(null, [{ pid:
|
|
188
|
+
callback(null, [{ pid: previousPid, pm2_env: { status: "online", restart_time: 0 } }])
|
|
187
189
|
)
|
|
188
190
|
.mockImplementationOnce(
|
|
189
191
|
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
190
|
-
callback(null, [{ pid:
|
|
192
|
+
callback(null, [{ pid: previousPid, pm2_env: { status: "stopping", restart_time: 0 } }])
|
|
191
193
|
)
|
|
192
194
|
.mockImplementationOnce(
|
|
193
195
|
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
@@ -204,15 +206,55 @@ describe("pm2-control", () => {
|
|
|
204
206
|
expect(disconnect).toHaveBeenCalledTimes(1);
|
|
205
207
|
});
|
|
206
208
|
|
|
209
|
+
it("waits for the previous process pid to exit before starting a replacement", async () => {
|
|
210
|
+
const previousPid = 999_991;
|
|
211
|
+
const killSpy = vi.spyOn(process, "kill");
|
|
212
|
+
killSpy
|
|
213
|
+
.mockImplementationOnce(() => true)
|
|
214
|
+
.mockImplementationOnce(() => true)
|
|
215
|
+
.mockImplementationOnce(() => {
|
|
216
|
+
const error = new Error("process not found") as NodeJS.ErrnoException;
|
|
217
|
+
error.code = "ESRCH";
|
|
218
|
+
throw error;
|
|
219
|
+
});
|
|
220
|
+
describeProcess
|
|
221
|
+
.mockImplementationOnce(
|
|
222
|
+
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
223
|
+
callback(null, [{ pid: previousPid, pm2_env: { status: "online", restart_time: 0 } }])
|
|
224
|
+
)
|
|
225
|
+
.mockImplementationOnce(
|
|
226
|
+
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
227
|
+
callback(null, [])
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const pendingStart = startManagedServer({
|
|
231
|
+
script: "/cli/dist/esm/server-runner.js",
|
|
232
|
+
cwd: "/repo",
|
|
233
|
+
waitMs: 10,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await expect(
|
|
237
|
+
Promise.race([
|
|
238
|
+
pendingStart.then(() => "started"),
|
|
239
|
+
new Promise((resolve) => setTimeout(() => resolve("waiting"), 20)),
|
|
240
|
+
])
|
|
241
|
+
).resolves.toBe("waiting");
|
|
242
|
+
|
|
243
|
+
expect(start).not.toHaveBeenCalled();
|
|
244
|
+
await pendingStart;
|
|
245
|
+
expect(killSpy).toHaveBeenCalledWith(previousPid, 0);
|
|
246
|
+
});
|
|
247
|
+
|
|
207
248
|
it("keeps waiting during startup when delete reports missing but the old app still lingers", async () => {
|
|
249
|
+
const previousPid = 999_993;
|
|
208
250
|
describeProcess
|
|
209
251
|
.mockImplementationOnce(
|
|
210
252
|
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
211
|
-
callback(null, [{ pid:
|
|
253
|
+
callback(null, [{ pid: previousPid, pm2_env: { status: "online", restart_time: 0 } }])
|
|
212
254
|
)
|
|
213
255
|
.mockImplementationOnce(
|
|
214
256
|
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
215
|
-
callback(null, [{ pid:
|
|
257
|
+
callback(null, [{ pid: previousPid, pm2_env: { status: "stopping", restart_time: 0 } }])
|
|
216
258
|
)
|
|
217
259
|
.mockImplementationOnce(
|
|
218
260
|
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
package/src/pm2-control.ts
CHANGED
|
@@ -265,6 +265,39 @@ const waitForManagedServerDeletion = async (pm2: Pm2Module, waitMs: number): Pro
|
|
|
265
265
|
throw new Error(`Timed out waiting for the managed server to stop after ${waitMs}ms.`);
|
|
266
266
|
};
|
|
267
267
|
|
|
268
|
+
const isMissingProcessError = (error: unknown): boolean =>
|
|
269
|
+
Boolean(
|
|
270
|
+
error &&
|
|
271
|
+
typeof error === "object" &&
|
|
272
|
+
"code" in error &&
|
|
273
|
+
(error as NodeJS.ErrnoException).code === "ESRCH"
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const waitForProcessExit = async (pid: number, waitMs: number): Promise<void> => {
|
|
277
|
+
const deadline = Date.now() + waitMs;
|
|
278
|
+
|
|
279
|
+
while (Date.now() <= deadline) {
|
|
280
|
+
try {
|
|
281
|
+
process.kill(pid, 0);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (isMissingProcessError(error)) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const remainingMs = deadline - Date.now();
|
|
291
|
+
if (remainingMs <= 0) {
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
await sleep(Math.min(STARTUP_POLL_INTERVAL_MS, remainingMs));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
throw new Error(`Timed out waiting for the managed server pid ${pid} to exit after ${waitMs}ms.`);
|
|
299
|
+
};
|
|
300
|
+
|
|
268
301
|
const deleteManagedServerInSession = async (
|
|
269
302
|
pm2: Pm2Module,
|
|
270
303
|
{
|
|
@@ -277,19 +310,27 @@ const deleteManagedServerInSession = async (
|
|
|
277
310
|
if (processes.length === 0) {
|
|
278
311
|
return false;
|
|
279
312
|
}
|
|
313
|
+
const previousPid = processes[0]?.pid;
|
|
314
|
+
const deadline = Date.now() + PM2_DELETE_WAIT_MS;
|
|
280
315
|
|
|
281
316
|
try {
|
|
282
317
|
await removeManagedServer(pm2);
|
|
283
318
|
} catch (error) {
|
|
284
319
|
if (ignoreMissing && isMissingManagedServerError(error)) {
|
|
285
|
-
await waitForManagedServerDeletion(pm2,
|
|
320
|
+
await waitForManagedServerDeletion(pm2, Math.max(0, deadline - Date.now()));
|
|
321
|
+
if (typeof previousPid === "number" && previousPid > 0) {
|
|
322
|
+
await waitForProcessExit(previousPid, Math.max(0, deadline - Date.now()));
|
|
323
|
+
}
|
|
286
324
|
return false;
|
|
287
325
|
}
|
|
288
326
|
|
|
289
327
|
throw error;
|
|
290
328
|
}
|
|
291
329
|
|
|
292
|
-
await waitForManagedServerDeletion(pm2,
|
|
330
|
+
await waitForManagedServerDeletion(pm2, Math.max(0, deadline - Date.now()));
|
|
331
|
+
if (typeof previousPid === "number" && previousPid > 0) {
|
|
332
|
+
await waitForProcessExit(previousPid, Math.max(0, deadline - Date.now()));
|
|
333
|
+
}
|
|
293
334
|
return true;
|
|
294
335
|
};
|
|
295
336
|
|
|
@@ -96,4 +96,64 @@ describe("update-worker", () => {
|
|
|
96
96
|
expect(state.manualCommand).toBe("coder-studio serve --restart");
|
|
97
97
|
expect(state.errorSummary).toContain("restart failed");
|
|
98
98
|
});
|
|
99
|
+
|
|
100
|
+
it("sanitizes pm2 and runtime override env before invoking install and restart commands", async () => {
|
|
101
|
+
const env = createEnv();
|
|
102
|
+
const runCommand = vi.fn(async () => {});
|
|
103
|
+
const originalEnv = {
|
|
104
|
+
PM2_HOME: process.env.PM2_HOME,
|
|
105
|
+
PM2_PROGRAMMATIC: process.env.PM2_PROGRAMMATIC,
|
|
106
|
+
PM2_JSON_PROCESSING: process.env.PM2_JSON_PROCESSING,
|
|
107
|
+
PM2_INTERACTOR_PROCESSING: process.env.PM2_INTERACTOR_PROCESSING,
|
|
108
|
+
NODE_APP_INSTANCE: process.env.NODE_APP_INSTANCE,
|
|
109
|
+
NODE_CHANNEL_FD: process.env.NODE_CHANNEL_FD,
|
|
110
|
+
NODE_CHANNEL_SERIALIZATION_MODE: process.env.NODE_CHANNEL_SERIALIZATION_MODE,
|
|
111
|
+
CODER_STUDIO_RUNTIME_JSON_PATH: process.env.CODER_STUDIO_RUNTIME_JSON_PATH,
|
|
112
|
+
CODER_STUDIO_SESSION_ID: process.env.CODER_STUDIO_SESSION_ID,
|
|
113
|
+
CODER_STUDIO_UPDATE_STATE_PATH: process.env.CODER_STUDIO_UPDATE_STATE_PATH,
|
|
114
|
+
pm_id: process.env.pm_id,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
process.env.PM2_HOME = "/tmp/custom-pm2-home";
|
|
118
|
+
process.env.PM2_PROGRAMMATIC = "true";
|
|
119
|
+
process.env.PM2_JSON_PROCESSING = "true";
|
|
120
|
+
process.env.PM2_INTERACTOR_PROCESSING = "true";
|
|
121
|
+
process.env.NODE_APP_INSTANCE = "0";
|
|
122
|
+
process.env.NODE_CHANNEL_FD = "3";
|
|
123
|
+
process.env.NODE_CHANNEL_SERIALIZATION_MODE = "json";
|
|
124
|
+
process.env.CODER_STUDIO_RUNTIME_JSON_PATH = "/tmp/runtime.json";
|
|
125
|
+
process.env.CODER_STUDIO_SESSION_ID = "sess_test";
|
|
126
|
+
process.env.CODER_STUDIO_UPDATE_STATE_PATH = "/tmp/update-state.json";
|
|
127
|
+
process.env.pm_id = "0";
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await runUpdateWorker(env, {
|
|
131
|
+
runCommand,
|
|
132
|
+
now: () => 1000,
|
|
133
|
+
});
|
|
134
|
+
} finally {
|
|
135
|
+
for (const [key, value] of Object.entries(originalEnv)) {
|
|
136
|
+
if (value === undefined) {
|
|
137
|
+
delete process.env[key];
|
|
138
|
+
} else {
|
|
139
|
+
process.env[key] = value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const call of runCommand.mock.calls) {
|
|
145
|
+
const options = call[2] as { env?: NodeJS.ProcessEnv };
|
|
146
|
+
expect(options.env?.PM2_HOME).toBe("/tmp/custom-pm2-home");
|
|
147
|
+
expect(options.env?.PM2_PROGRAMMATIC).toBeUndefined();
|
|
148
|
+
expect(options.env?.PM2_JSON_PROCESSING).toBeUndefined();
|
|
149
|
+
expect(options.env?.PM2_INTERACTOR_PROCESSING).toBeUndefined();
|
|
150
|
+
expect(options.env?.NODE_APP_INSTANCE).toBeUndefined();
|
|
151
|
+
expect(options.env?.NODE_CHANNEL_FD).toBeUndefined();
|
|
152
|
+
expect(options.env?.NODE_CHANNEL_SERIALIZATION_MODE).toBeUndefined();
|
|
153
|
+
expect(options.env?.CODER_STUDIO_RUNTIME_JSON_PATH).toBeUndefined();
|
|
154
|
+
expect(options.env?.CODER_STUDIO_SESSION_ID).toBeUndefined();
|
|
155
|
+
expect(options.env?.CODER_STUDIO_UPDATE_STATE_PATH).toBeUndefined();
|
|
156
|
+
expect(options.env?.pm_id).toBeUndefined();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
99
159
|
});
|
package/src/update-worker.ts
CHANGED
|
@@ -97,15 +97,47 @@ function buildManualCommand(input: WorkerEnv): string {
|
|
|
97
97
|
].join("\n");
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
const INTERNAL_ENV_KEYS = new Set([
|
|
101
|
+
"CODER_STUDIO_RUNTIME_JSON_PATH",
|
|
102
|
+
"CODER_STUDIO_SESSION_ID",
|
|
103
|
+
"NODE_APP_INSTANCE",
|
|
104
|
+
"NODE_CHANNEL_FD",
|
|
105
|
+
"NODE_CHANNEL_SERIALIZATION_MODE",
|
|
106
|
+
"PM2_INTERACTOR_PROCESSING",
|
|
107
|
+
"PM2_JSON_PROCESSING",
|
|
108
|
+
"PM2_PROGRAMMATIC",
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
function buildChildProcessEnv(env = process.env): NodeJS.ProcessEnv {
|
|
112
|
+
const nextEnv: NodeJS.ProcessEnv = { ...env };
|
|
113
|
+
|
|
114
|
+
for (const key of Object.keys(nextEnv)) {
|
|
115
|
+
if (INTERNAL_ENV_KEYS.has(key)) {
|
|
116
|
+
delete nextEnv[key];
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (key.startsWith("CODER_STUDIO_UPDATE_") || key.startsWith("pm_")) {
|
|
121
|
+
delete nextEnv[key];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return nextEnv;
|
|
126
|
+
}
|
|
127
|
+
|
|
100
128
|
function runCommand(
|
|
101
129
|
command: string,
|
|
102
130
|
args: string[],
|
|
103
|
-
options?: {
|
|
131
|
+
options?: {
|
|
132
|
+
stdio?: "ignore" | "pipe";
|
|
133
|
+
logStream?: NodeJS.WritableStream;
|
|
134
|
+
env?: NodeJS.ProcessEnv;
|
|
135
|
+
}
|
|
104
136
|
): Promise<void> {
|
|
105
137
|
return new Promise((resolve, reject) => {
|
|
106
138
|
const child = spawn(command, args, {
|
|
107
139
|
stdio: options?.stdio === "ignore" ? "ignore" : "pipe",
|
|
108
|
-
env: process.env,
|
|
140
|
+
env: options?.env ?? process.env,
|
|
109
141
|
});
|
|
110
142
|
|
|
111
143
|
if (options?.logStream && child.stdout) {
|
|
@@ -137,12 +169,13 @@ export async function runUpdateWorker(
|
|
|
137
169
|
await mkdir(dirname(input.logFilePath), { recursive: true });
|
|
138
170
|
const logStream = createWriteStream(input.logFilePath, { flags: "a" });
|
|
139
171
|
const execute = deps?.runCommand ?? runCommand;
|
|
172
|
+
const childEnv = buildChildProcessEnv(process.env);
|
|
140
173
|
|
|
141
174
|
try {
|
|
142
175
|
await execute(
|
|
143
176
|
input.npmCommand,
|
|
144
177
|
[...input.installArgsPrefix, `${input.packageName}@${input.targetVersion}`],
|
|
145
|
-
{ logStream }
|
|
178
|
+
{ logStream, env: childEnv }
|
|
146
179
|
);
|
|
147
180
|
} catch (error) {
|
|
148
181
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -183,7 +216,7 @@ export async function runUpdateWorker(
|
|
|
183
216
|
});
|
|
184
217
|
|
|
185
218
|
try {
|
|
186
|
-
await execute(input.cliCommand, input.restartArgs, { logStream });
|
|
219
|
+
await execute(input.cliCommand, input.restartArgs, { logStream, env: childEnv });
|
|
187
220
|
} catch (error) {
|
|
188
221
|
const message = error instanceof Error ? error.message : String(error);
|
|
189
222
|
await writeState(input.stateFilePath, {
|