@spencer-kit/coder-studio 0.3.0 → 0.3.2
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 +12 -0
- package/README.md +14 -79
- package/dist/esm/bin.mjs +572 -284
- package/dist/esm/bin.mjs.map +4 -4
- package/dist/esm/server-runner.mjs +450 -168
- package/dist/esm/server-runner.mjs.map +4 -4
- package/dist/web/assets/index-BjrMfcUG.js +111 -0
- package/dist/web/assets/index-BjrMfcUG.js.map +1 -0
- package/dist/web/assets/index-mL_Aq31j.css +1 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/favicon.svg +19 -0
- package/dist/web/index.html +4 -4
- package/package.json +2 -2
- package/src/bin.test.ts +1 -1
- package/src/bin.ts +6 -379
- package/src/browser.test.ts +50 -0
- package/src/browser.ts +1 -0
- package/src/cli.ts +347 -0
- package/src/package-manifest.test.ts +14 -0
- package/src/package-manifest.ts +28 -0
- package/src/pm2-control.test.ts +89 -1
- package/src/pm2-control.ts +99 -57
- package/src/server-control.test.ts +19 -0
- package/src/server-control.ts +1 -1
- package/src/server-runner.test.ts +14 -0
- package/src/server-runner.ts +2 -0
- package/dist/web/assets/index-A-2YPePM.js +0 -111
- package/dist/web/assets/index-A-2YPePM.js.map +0 -1
- package/dist/web/assets/index-BLMivSS1.css +0 -1
package/src/cli.ts
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { clearAuthBlockByIp, listAuthBlocks } from "./auth-control.js";
|
|
5
|
+
import { openBrowser } from "./browser.js";
|
|
6
|
+
import { type CliConfig, readCliConfig, writeCliConfig } from "./config-store.js";
|
|
7
|
+
import { readLogExcerpt } from "./log-excerpt.js";
|
|
8
|
+
import { assertSupportedNodeVersion } from "./node-version.js";
|
|
9
|
+
import { getCliVersion } from "./package-manifest.js";
|
|
10
|
+
import { parseArgs } from "./parse-args.js";
|
|
11
|
+
import { startManagedServer } from "./pm2-control.js";
|
|
12
|
+
import { confirmYesNo, isInteractiveSession } from "./prompts.js";
|
|
13
|
+
import { getServerStatus, type ServerStatus, stopRunningServer } from "./server-control.js";
|
|
14
|
+
import { startServer } from "./server-runner.js";
|
|
15
|
+
import { getBrowserUrl, getListenIp, getListenUrl } from "./server-url.js";
|
|
16
|
+
|
|
17
|
+
const MANAGED_SERVER_WAIT_MS = 5000;
|
|
18
|
+
const DEFAULT_LOG_TAIL_LINES = 40;
|
|
19
|
+
|
|
20
|
+
function formatConfig(config: CliConfig | null): string {
|
|
21
|
+
return JSON.stringify(config ?? {}, null, 2);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatStatus(status: ServerStatus): string {
|
|
25
|
+
const listenUrl = getListenUrl(status) ?? "n/a";
|
|
26
|
+
const browserUrl = getBrowserUrl(status) ?? "n/a";
|
|
27
|
+
const startedAt = status.startedAt === null ? "n/a" : new Date(status.startedAt).toISOString();
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
`Status: ${status.status}`,
|
|
31
|
+
`Listen host: ${status.host ?? "n/a"}`,
|
|
32
|
+
`Listen IP: ${getListenIp(status) ?? "n/a"}`,
|
|
33
|
+
`Port: ${status.port ?? "n/a"}`,
|
|
34
|
+
`Listen URL: ${listenUrl}`,
|
|
35
|
+
`Local URL: ${browserUrl}`,
|
|
36
|
+
`PID: ${status.pid ?? "n/a"}`,
|
|
37
|
+
`Started: ${startedAt}`,
|
|
38
|
+
`Restarts: ${status.restartCount}`,
|
|
39
|
+
`Out log: ${status.outFile}`,
|
|
40
|
+
`Error log: ${status.errFile}`,
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function showLogs(
|
|
45
|
+
status: ServerStatus,
|
|
46
|
+
{
|
|
47
|
+
tail = DEFAULT_LOG_TAIL_LINES,
|
|
48
|
+
errorsOnly = false,
|
|
49
|
+
}: { tail?: number; errorsOnly?: boolean } = {}
|
|
50
|
+
): void {
|
|
51
|
+
const paths = errorsOnly ? [status.errFile] : [status.outFile, status.errFile];
|
|
52
|
+
const contents = paths
|
|
53
|
+
.filter((path, index, paths) => paths.indexOf(path) === index)
|
|
54
|
+
.flatMap((path) => {
|
|
55
|
+
const content = readLogExcerpt(path, { maxLines: tail, maxChars: null });
|
|
56
|
+
return content ? [content] : [];
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
console.log(contents.length === 0 ? "No logs available." : contents.join("\n"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function showHelp(): void {
|
|
63
|
+
console.log(`
|
|
64
|
+
@spencer-kit/coder-studio - Coder Studio CLI
|
|
65
|
+
|
|
66
|
+
USAGE:
|
|
67
|
+
coder-studio [COMMAND]
|
|
68
|
+
|
|
69
|
+
COMMANDS:
|
|
70
|
+
serve Start the Coder Studio server in background (default)
|
|
71
|
+
server Alias for serve
|
|
72
|
+
open Start the server if needed and open Coder Studio in a browser
|
|
73
|
+
auth Manage auth login blocks in local server storage
|
|
74
|
+
config Persist CLI host/port/data-dir/password settings
|
|
75
|
+
stop Stop the managed Coder Studio server
|
|
76
|
+
status Show the managed server status
|
|
77
|
+
logs Show the managed server logs
|
|
78
|
+
help Show this help message
|
|
79
|
+
version Show version
|
|
80
|
+
|
|
81
|
+
OPTIONS:
|
|
82
|
+
--host <string> Save server host for future runs
|
|
83
|
+
--port, -p <number> Save server port for future runs
|
|
84
|
+
--data-dir, -d <path> Save data directory for future runs
|
|
85
|
+
--password <string> Save auth password for future runs
|
|
86
|
+
--restart Restart an already running managed server for serve/open
|
|
87
|
+
--help Show help
|
|
88
|
+
--version, -v Show version
|
|
89
|
+
|
|
90
|
+
EXAMPLES:
|
|
91
|
+
coder-studio
|
|
92
|
+
coder-studio serve
|
|
93
|
+
coder-studio server
|
|
94
|
+
coder-studio auth ban-list
|
|
95
|
+
coder-studio auth unblock --ip 198.51.100.24
|
|
96
|
+
coder-studio serve --foreground
|
|
97
|
+
coder-studio serve --restart
|
|
98
|
+
coder-studio open
|
|
99
|
+
coder-studio open --restart
|
|
100
|
+
coder-studio status
|
|
101
|
+
coder-studio logs
|
|
102
|
+
coder-studio stop
|
|
103
|
+
coder-studio config --host 0.0.0.0 --port 8080
|
|
104
|
+
`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function showConfigHelp(): void {
|
|
108
|
+
console.log(`
|
|
109
|
+
@spencer-kit/coder-studio - config
|
|
110
|
+
|
|
111
|
+
USAGE:
|
|
112
|
+
coder-studio config [OPTIONS]
|
|
113
|
+
coder-studio config help
|
|
114
|
+
|
|
115
|
+
BEHAVIOR:
|
|
116
|
+
Without options, prints the current saved config.
|
|
117
|
+
Bare serve reads this saved config for future runs.
|
|
118
|
+
|
|
119
|
+
OPTIONS:
|
|
120
|
+
--host <string> Save server host for future runs
|
|
121
|
+
--port, -p <number> Save server port for future runs
|
|
122
|
+
--data-dir, -d <path> Save data directory for future runs
|
|
123
|
+
--password <string> Save auth password for future runs
|
|
124
|
+
--help Show config help
|
|
125
|
+
|
|
126
|
+
EXAMPLES:
|
|
127
|
+
coder-studio config
|
|
128
|
+
coder-studio config --host 0.0.0.0
|
|
129
|
+
coder-studio config --port 8080
|
|
130
|
+
coder-studio config --data-dir /tmp/cs-data
|
|
131
|
+
coder-studio config --password sekrit
|
|
132
|
+
coder-studio config --host 0.0.0.0 --port 8080
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function showVersion(): void {
|
|
137
|
+
console.log(`@spencer-kit/coder-studio v${getCliVersion(import.meta.url)}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatAuthBlocks(blocks: Awaited<ReturnType<typeof listAuthBlocks>>): string {
|
|
141
|
+
if (blocks.length === 0) {
|
|
142
|
+
return "No blocked IPs.";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return JSON.stringify(blocks, null, 2);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveManagedScriptPath(): string {
|
|
149
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
150
|
+
const currentDir = dirname(currentFile);
|
|
151
|
+
const candidates = [
|
|
152
|
+
join(currentDir, "server-runner.js"),
|
|
153
|
+
join(currentDir, "server-runner.mjs"),
|
|
154
|
+
join(currentDir, "../src/server-runner.ts"),
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const scriptPath = candidates.find((candidate) => existsSync(candidate));
|
|
158
|
+
if (!scriptPath) {
|
|
159
|
+
throw new Error("Unable to locate the managed server entry script");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return scriptPath;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isRunningStatus(status: ServerStatus): boolean {
|
|
166
|
+
return status.status === "running" || status.status === "starting";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface ManagedStartupDecision {
|
|
170
|
+
existingStatus: ServerStatus | null;
|
|
171
|
+
restartRequested: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function shouldRestartRunningServer(status: ServerStatus): Promise<boolean> {
|
|
175
|
+
const currentUrl = getBrowserUrl(status) ?? getListenUrl(status) ?? "the existing server";
|
|
176
|
+
|
|
177
|
+
if (!isInteractiveSession()) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return confirmYesNo(`Coder Studio is already running at ${currentUrl}. Restart it? [y/N] `);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function prepareManagedStartup(forceRestart = false): Promise<ManagedStartupDecision> {
|
|
185
|
+
const status = await getServerStatus();
|
|
186
|
+
if (!isRunningStatus(status)) {
|
|
187
|
+
return {
|
|
188
|
+
existingStatus: null,
|
|
189
|
+
restartRequested: false,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const restart = forceRestart ? true : await shouldRestartRunningServer(status);
|
|
194
|
+
if (!restart) {
|
|
195
|
+
const currentUrl = getBrowserUrl(status) ?? getListenUrl(status) ?? "n/a";
|
|
196
|
+
if (!isInteractiveSession()) {
|
|
197
|
+
console.log(
|
|
198
|
+
`Coder Studio is already running at ${currentUrl}. Service already exists and was not restarted.`
|
|
199
|
+
);
|
|
200
|
+
} else {
|
|
201
|
+
console.log(`Leaving the existing Coder Studio server running at ${currentUrl}.`);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
existingStatus: status,
|
|
205
|
+
restartRequested: false,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log("Restarting the managed Coder Studio server...");
|
|
210
|
+
return {
|
|
211
|
+
existingStatus: null,
|
|
212
|
+
restartRequested: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function startManagedServerFlow(): Promise<void> {
|
|
217
|
+
await startManagedServer({
|
|
218
|
+
script: resolveManagedScriptPath(),
|
|
219
|
+
cwd: process.cwd(),
|
|
220
|
+
waitMs: MANAGED_SERVER_WAIT_MS,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function openManagedServerInBrowser(existingStatus?: ServerStatus | null): Promise<void> {
|
|
225
|
+
const status = existingStatus ?? (await getServerStatus());
|
|
226
|
+
const browserUrl = getBrowserUrl(status);
|
|
227
|
+
|
|
228
|
+
if (browserUrl === null) {
|
|
229
|
+
throw new Error("Unable to determine the running Coder Studio URL.");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(`Opening Coder Studio in your browser: ${browserUrl}`);
|
|
233
|
+
await openBrowser(browserUrl);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function main(argv = process.argv.slice(2)): Promise<void> {
|
|
237
|
+
assertSupportedNodeVersion();
|
|
238
|
+
const args = parseArgs(argv);
|
|
239
|
+
|
|
240
|
+
if (args.command === "config") {
|
|
241
|
+
if (args.configHelp) {
|
|
242
|
+
showConfigHelp();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
args.host === undefined &&
|
|
248
|
+
args.port === undefined &&
|
|
249
|
+
args.dataDir === undefined &&
|
|
250
|
+
args.password === undefined
|
|
251
|
+
) {
|
|
252
|
+
console.log(formatConfig(readCliConfig()));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const savedConfig = readCliConfig();
|
|
257
|
+
const nextConfig: CliConfig = {
|
|
258
|
+
...(savedConfig?.host !== undefined ? { host: savedConfig.host } : {}),
|
|
259
|
+
...(savedConfig?.port !== undefined && savedConfig.port > 0
|
|
260
|
+
? { port: savedConfig.port }
|
|
261
|
+
: {}),
|
|
262
|
+
...(savedConfig?.dataDir !== undefined ? { dataDir: savedConfig.dataDir } : {}),
|
|
263
|
+
...(savedConfig?.password !== undefined ? { password: savedConfig.password } : {}),
|
|
264
|
+
...(args.host !== undefined ? { host: args.host } : {}),
|
|
265
|
+
...(args.port !== undefined ? { port: args.port } : {}),
|
|
266
|
+
...(args.dataDir !== undefined ? { dataDir: args.dataDir } : {}),
|
|
267
|
+
...(args.password !== undefined ? { password: args.password } : {}),
|
|
268
|
+
};
|
|
269
|
+
writeCliConfig(nextConfig);
|
|
270
|
+
console.log(formatConfig(nextConfig));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (args.command === "stop") {
|
|
275
|
+
const stopped = await stopRunningServer();
|
|
276
|
+
console.log(stopped ? "Stopped Coder Studio server." : "No running Coder Studio server found.");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (args.command === "status") {
|
|
281
|
+
console.log(formatStatus(await getServerStatus()));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (args.command === "logs") {
|
|
286
|
+
showLogs(await getServerStatus(), { tail: args.tail, errorsOnly: args.errorsOnly });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (args.command === "help") {
|
|
291
|
+
showHelp();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (args.command === "version") {
|
|
296
|
+
showVersion();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (args.command === "auth") {
|
|
301
|
+
if (args.authCommand === "ban-list") {
|
|
302
|
+
console.log(formatAuthBlocks(await listAuthBlocks()));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (args.authCommand === "unblock") {
|
|
307
|
+
const cleared = await clearAuthBlockByIp(args.ip!);
|
|
308
|
+
console.log(cleared ? `Unblocked IP: ${args.ip}` : `No block found for IP: ${args.ip}`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (args.command === "open") {
|
|
314
|
+
const startup = await prepareManagedStartup(args.restart);
|
|
315
|
+
if (startup.existingStatus === null) {
|
|
316
|
+
await startManagedServerFlow();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
await openManagedServerInBrowser(startup.existingStatus);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (args.foreground) {
|
|
324
|
+
const startup = await prepareManagedStartup(args.restart);
|
|
325
|
+
if (startup.existingStatus !== null) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (startup.restartRequested) {
|
|
330
|
+
await stopRunningServer();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log("Starting Coder Studio Server in foreground...");
|
|
334
|
+
await startServer();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const startup = await prepareManagedStartup(args.restart);
|
|
339
|
+
if (startup.existingStatus !== null) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
await startManagedServerFlow();
|
|
344
|
+
|
|
345
|
+
console.log("Coder Studio server started in background.");
|
|
346
|
+
console.log("Run `coder-studio status` to inspect the server.");
|
|
347
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
2
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { getCliVersion, resolveCliPackageManifestUrl } from "./package-manifest.js";
|
|
3
5
|
|
|
4
6
|
interface PackageManifest {
|
|
5
7
|
dependencies?: Record<string, string>;
|
|
@@ -12,6 +14,18 @@ function readPackageManifest(relativePath: string): PackageManifest {
|
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
describe("cli package manifest", () => {
|
|
17
|
+
it("resolves the CLI package manifest instead of the workspace root manifest", () => {
|
|
18
|
+
expect(fileURLToPath(resolveCliPackageManifestUrl(import.meta.url))).toBe(
|
|
19
|
+
fileURLToPath(new URL("../package.json", import.meta.url))
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("reads the published CLI version from the CLI package manifest", () => {
|
|
24
|
+
const cliPackage = readPackageManifest("../package.json") as { version?: string };
|
|
25
|
+
|
|
26
|
+
expect(getCliVersion(import.meta.url)).toBe(cliPackage.version);
|
|
27
|
+
});
|
|
28
|
+
|
|
15
29
|
it("declares every external server runtime dependency", () => {
|
|
16
30
|
const cliPackage = readPackageManifest("../package.json");
|
|
17
31
|
const serverPackage = readPackageManifest("../../server/package.json");
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
|
|
3
|
+
interface CliPackageManifest {
|
|
4
|
+
version?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveCliPackageManifestUrl(importMetaUrl: string): URL {
|
|
8
|
+
const manifestUrl = [
|
|
9
|
+
new URL("../package.json", importMetaUrl),
|
|
10
|
+
new URL("../../package.json", importMetaUrl),
|
|
11
|
+
].find((candidate) => existsSync(candidate));
|
|
12
|
+
|
|
13
|
+
if (!manifestUrl) {
|
|
14
|
+
throw new Error("Unable to locate CLI package.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return manifestUrl;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getCliPackageManifest(importMetaUrl: string): CliPackageManifest {
|
|
21
|
+
return JSON.parse(
|
|
22
|
+
readFileSync(resolveCliPackageManifestUrl(importMetaUrl), "utf-8")
|
|
23
|
+
) as CliPackageManifest;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getCliVersion(importMetaUrl: string): string {
|
|
27
|
+
return getCliPackageManifest(importMetaUrl).version ?? "0.0.0";
|
|
28
|
+
}
|
package/src/pm2-control.test.ts
CHANGED
|
@@ -33,15 +33,22 @@ import {
|
|
|
33
33
|
describe("pm2-control", () => {
|
|
34
34
|
const originalHome = process.env.HOME;
|
|
35
35
|
const originalUserProfile = process.env.USERPROFILE;
|
|
36
|
+
const originalRuntimeDir = process.env.CODER_STUDIO_RUNTIME_DIR;
|
|
37
|
+
const originalRuntimeJsonPath = process.env.CODER_STUDIO_RUNTIME_JSON_PATH;
|
|
36
38
|
let testHomeDir: string;
|
|
37
39
|
|
|
38
40
|
beforeEach(() => {
|
|
39
41
|
testHomeDir = mkdtempSync(join(tmpdir(), "cs-pm2-control-home-"));
|
|
40
42
|
process.env.HOME = testHomeDir;
|
|
41
43
|
process.env.USERPROFILE = testHomeDir;
|
|
44
|
+
process.env.CODER_STUDIO_RUNTIME_DIR = join(testHomeDir, ".coder-studio");
|
|
45
|
+
delete process.env.CODER_STUDIO_RUNTIME_JSON_PATH;
|
|
42
46
|
|
|
43
47
|
connect.mockImplementation((callback: (error: Error | null) => void) => callback(null));
|
|
44
|
-
disconnect.mockImplementation(() =>
|
|
48
|
+
disconnect.mockImplementation((callback?: (error: Error | null) => void) => {
|
|
49
|
+
callback?.(null);
|
|
50
|
+
return undefined;
|
|
51
|
+
});
|
|
45
52
|
start.mockImplementation(
|
|
46
53
|
(_config: unknown, callback: (error: Error | null, apps: unknown[]) => void) => {
|
|
47
54
|
writeRuntimeConfig({
|
|
@@ -80,6 +87,18 @@ describe("pm2-control", () => {
|
|
|
80
87
|
process.env.USERPROFILE = originalUserProfile;
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
if (originalRuntimeDir === undefined) {
|
|
91
|
+
delete process.env.CODER_STUDIO_RUNTIME_DIR;
|
|
92
|
+
} else {
|
|
93
|
+
process.env.CODER_STUDIO_RUNTIME_DIR = originalRuntimeDir;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (originalRuntimeJsonPath === undefined) {
|
|
97
|
+
delete process.env.CODER_STUDIO_RUNTIME_JSON_PATH;
|
|
98
|
+
} else {
|
|
99
|
+
process.env.CODER_STUDIO_RUNTIME_JSON_PATH = originalRuntimeJsonPath;
|
|
100
|
+
}
|
|
101
|
+
|
|
83
102
|
if (existsSync(testHomeDir)) {
|
|
84
103
|
rmSync(testHomeDir, { recursive: true, force: true });
|
|
85
104
|
}
|
|
@@ -157,6 +176,67 @@ describe("pm2-control", () => {
|
|
|
157
176
|
).resolves.toBe("waiting");
|
|
158
177
|
|
|
159
178
|
expect(start).not.toHaveBeenCalled();
|
|
179
|
+
await pendingStart;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("reuses one pm2 session while polling deletion during startup", async () => {
|
|
183
|
+
describeProcess
|
|
184
|
+
.mockImplementationOnce(
|
|
185
|
+
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
186
|
+
callback(null, [{ pid: 111, pm2_env: { status: "online", restart_time: 0 } }])
|
|
187
|
+
)
|
|
188
|
+
.mockImplementationOnce(
|
|
189
|
+
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
190
|
+
callback(null, [{ pid: 111, pm2_env: { status: "stopping", restart_time: 0 } }])
|
|
191
|
+
)
|
|
192
|
+
.mockImplementationOnce(
|
|
193
|
+
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
194
|
+
callback(null, [])
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
await startManagedServer({
|
|
198
|
+
script: "/cli/dist/esm/server-runner.js",
|
|
199
|
+
cwd: "/repo",
|
|
200
|
+
waitMs: 10,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(connect).toHaveBeenCalledTimes(1);
|
|
204
|
+
expect(disconnect).toHaveBeenCalledTimes(1);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("keeps waiting during startup when delete reports missing but the old app still lingers", async () => {
|
|
208
|
+
describeProcess
|
|
209
|
+
.mockImplementationOnce(
|
|
210
|
+
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
211
|
+
callback(null, [{ pid: 111, pm2_env: { status: "online", restart_time: 0 } }])
|
|
212
|
+
)
|
|
213
|
+
.mockImplementationOnce(
|
|
214
|
+
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
215
|
+
callback(null, [{ pid: 111, pm2_env: { status: "stopping", restart_time: 0 } }])
|
|
216
|
+
)
|
|
217
|
+
.mockImplementationOnce(
|
|
218
|
+
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
219
|
+
callback(null, [])
|
|
220
|
+
);
|
|
221
|
+
deleteProcess.mockImplementationOnce((_name: string, callback: (error: Error | null) => void) =>
|
|
222
|
+
callback(new Error("process or namespace not found"))
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const pendingStart = startManagedServer({
|
|
226
|
+
script: "/cli/dist/esm/server-runner.js",
|
|
227
|
+
cwd: "/repo",
|
|
228
|
+
waitMs: 10,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await expect(
|
|
232
|
+
Promise.race([
|
|
233
|
+
pendingStart.then(() => "started"),
|
|
234
|
+
new Promise((resolve) => setTimeout(() => resolve("waiting"), 20)),
|
|
235
|
+
])
|
|
236
|
+
).resolves.toBe("waiting");
|
|
237
|
+
|
|
238
|
+
expect(start).not.toHaveBeenCalled();
|
|
239
|
+
await pendingStart;
|
|
160
240
|
});
|
|
161
241
|
|
|
162
242
|
it("ignores delete-time missing errors when requested", async () => {
|
|
@@ -169,6 +249,8 @@ describe("pm2-control", () => {
|
|
|
169
249
|
);
|
|
170
250
|
|
|
171
251
|
await expect(deleteManagedServer({ ignoreMissing: true })).resolves.toBe(false);
|
|
252
|
+
expect(connect).toHaveBeenCalledTimes(1);
|
|
253
|
+
expect(disconnect).toHaveBeenCalledTimes(1);
|
|
172
254
|
});
|
|
173
255
|
|
|
174
256
|
it("fails background startup when runtime readiness times out", async () => {
|
|
@@ -183,6 +265,10 @@ describe("pm2-control", () => {
|
|
|
183
265
|
callback(null, [])
|
|
184
266
|
)
|
|
185
267
|
.mockImplementationOnce(
|
|
268
|
+
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
269
|
+
callback(null, [])
|
|
270
|
+
)
|
|
271
|
+
.mockImplementation(
|
|
186
272
|
(_name: string, callback: (error: Error | null, result: unknown[]) => void) =>
|
|
187
273
|
callback(null, [{ pid: 424242, pm2_env: { status: "online", restart_time: 0 } }])
|
|
188
274
|
);
|
|
@@ -243,6 +329,8 @@ describe("pm2-control", () => {
|
|
|
243
329
|
pm2Pid: null,
|
|
244
330
|
restartCount: 0,
|
|
245
331
|
});
|
|
332
|
+
expect(connect).toHaveBeenCalledTimes(1);
|
|
333
|
+
expect(disconnect).toHaveBeenCalledTimes(1);
|
|
246
334
|
});
|
|
247
335
|
|
|
248
336
|
it("maps an online PM2 app to running status", async () => {
|