@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
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="1024"
|
|
4
|
+
height="1024"
|
|
5
|
+
viewBox="0 0 1024 1024"
|
|
6
|
+
fill="none"
|
|
7
|
+
>
|
|
8
|
+
<path
|
|
9
|
+
d="M558 160C540 278 480 370 408 446C358 499 301 536 287 568C270 607 332 613 416 614C512 615 566 634 566 684C566 739 516 797 449 867"
|
|
10
|
+
stroke="#8CCFFF"
|
|
11
|
+
stroke-width="108"
|
|
12
|
+
stroke-linecap="round"
|
|
13
|
+
stroke-linejoin="round"
|
|
14
|
+
/>
|
|
15
|
+
<path
|
|
16
|
+
d="M512 388C528 470 554 496 636 512C554 528 528 554 512 636C496 554 470 528 388 512C470 496 496 470 512 388Z"
|
|
17
|
+
fill="#FFFFFF"
|
|
18
|
+
/>
|
|
19
|
+
</svg>
|
package/dist/web/index.html
CHANGED
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta name="description" content="Coder Studio - Agent-First Development Environment" />
|
|
7
7
|
<title>Coder Studio</title>
|
|
8
|
-
<link rel="icon" type="image/
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BjrMfcUG.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-S-ySWqyJ.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/monaco-editor-CZixARFH.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/xterm-0bvFymvt.js">
|
|
13
13
|
<link rel="stylesheet" crossorigin href="/assets/monaco-editor-Br_kD0ds.css">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/xterm-BrP-ENHg.css">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-mL_Aq31j.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
19
19
|
</body>
|
|
20
|
-
</html>
|
|
20
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spencer-kit/coder-studio",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Deploy once, code everywhere. Browser-based AI coding workspace for Claude Code and Codex.",
|
|
6
6
|
"main": "./dist/esm/index.mjs",
|
|
7
7
|
"bin": {
|
|
8
8
|
"coder-studio": "./dist/bin.js"
|
package/src/bin.test.ts
CHANGED
package/src/bin.ts
CHANGED
|
@@ -1,380 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join, resolve } 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 { parseArgs } from "./parse-args.js";
|
|
10
|
-
import { startManagedServer } from "./pm2-control.js";
|
|
11
|
-
import { confirmYesNo, isInteractiveSession } from "./prompts.js";
|
|
12
|
-
import { getServerStatus, type ServerStatus, stopRunningServer } from "./server-control.js";
|
|
13
|
-
import { startServer } from "./server-runner.js";
|
|
14
|
-
import { getBrowserUrl, getListenIp, getListenUrl } from "./server-url.js";
|
|
1
|
+
import { main } from "./cli.js";
|
|
15
2
|
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function formatStatus(status: ServerStatus): string {
|
|
24
|
-
const listenUrl = getListenUrl(status) ?? "n/a";
|
|
25
|
-
const browserUrl = getBrowserUrl(status) ?? "n/a";
|
|
26
|
-
const startedAt = status.startedAt === null ? "n/a" : new Date(status.startedAt).toISOString();
|
|
27
|
-
|
|
28
|
-
return [
|
|
29
|
-
`Status: ${status.status}`,
|
|
30
|
-
`Listen host: ${status.host ?? "n/a"}`,
|
|
31
|
-
`Listen IP: ${getListenIp(status) ?? "n/a"}`,
|
|
32
|
-
`Port: ${status.port ?? "n/a"}`,
|
|
33
|
-
`Listen URL: ${listenUrl}`,
|
|
34
|
-
`Local URL: ${browserUrl}`,
|
|
35
|
-
`PID: ${status.pid ?? "n/a"}`,
|
|
36
|
-
`Started: ${startedAt}`,
|
|
37
|
-
`Restarts: ${status.restartCount}`,
|
|
38
|
-
`Out log: ${status.outFile}`,
|
|
39
|
-
`Error log: ${status.errFile}`,
|
|
40
|
-
].join("\n");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function showLogs(
|
|
44
|
-
status: ServerStatus,
|
|
45
|
-
{
|
|
46
|
-
tail = DEFAULT_LOG_TAIL_LINES,
|
|
47
|
-
errorsOnly = false,
|
|
48
|
-
}: { tail?: number; errorsOnly?: boolean } = {}
|
|
49
|
-
): void {
|
|
50
|
-
const paths = errorsOnly ? [status.errFile] : [status.outFile, status.errFile];
|
|
51
|
-
const contents = paths
|
|
52
|
-
.filter((path, index, paths) => paths.indexOf(path) === index)
|
|
53
|
-
.flatMap((path) => {
|
|
54
|
-
const content = readLogExcerpt(path, { maxLines: tail, maxChars: null });
|
|
55
|
-
return content ? [content] : [];
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
console.log(contents.length === 0 ? "No logs available." : contents.join("\n"));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function showHelp(): void {
|
|
62
|
-
console.log(`
|
|
63
|
-
@spencer-kit/coder-studio - Coder Studio CLI
|
|
64
|
-
|
|
65
|
-
USAGE:
|
|
66
|
-
coder-studio [COMMAND]
|
|
67
|
-
|
|
68
|
-
COMMANDS:
|
|
69
|
-
serve Start the Coder Studio server in background (default)
|
|
70
|
-
server Alias for serve
|
|
71
|
-
open Start the server if needed and open Coder Studio in a browser
|
|
72
|
-
auth Manage auth login blocks in local server storage
|
|
73
|
-
config Persist CLI host/port/data-dir/password settings
|
|
74
|
-
stop Stop the managed Coder Studio server
|
|
75
|
-
status Show the managed server status
|
|
76
|
-
logs Show the managed server logs
|
|
77
|
-
help Show this help message
|
|
78
|
-
version Show version
|
|
79
|
-
|
|
80
|
-
OPTIONS:
|
|
81
|
-
--host <string> Save server host for future runs
|
|
82
|
-
--port, -p <number> Save server port for future runs
|
|
83
|
-
--data-dir, -d <path> Save data directory for future runs
|
|
84
|
-
--password <string> Save auth password for future runs
|
|
85
|
-
--restart Restart an already running managed server for serve/open
|
|
86
|
-
--help Show help
|
|
87
|
-
--version, -v Show version
|
|
88
|
-
|
|
89
|
-
EXAMPLES:
|
|
90
|
-
coder-studio
|
|
91
|
-
coder-studio serve
|
|
92
|
-
coder-studio server
|
|
93
|
-
coder-studio auth ban-list
|
|
94
|
-
coder-studio auth unblock --ip 198.51.100.24
|
|
95
|
-
coder-studio serve --foreground
|
|
96
|
-
coder-studio serve --restart
|
|
97
|
-
coder-studio open
|
|
98
|
-
coder-studio open --restart
|
|
99
|
-
coder-studio status
|
|
100
|
-
coder-studio logs
|
|
101
|
-
coder-studio stop
|
|
102
|
-
coder-studio config --host 0.0.0.0 --port 8080
|
|
103
|
-
`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function showConfigHelp(): void {
|
|
107
|
-
console.log(`
|
|
108
|
-
@spencer-kit/coder-studio - config
|
|
109
|
-
|
|
110
|
-
USAGE:
|
|
111
|
-
coder-studio config [OPTIONS]
|
|
112
|
-
coder-studio config help
|
|
113
|
-
|
|
114
|
-
BEHAVIOR:
|
|
115
|
-
Without options, prints the current saved config.
|
|
116
|
-
Bare serve reads this saved config for future runs.
|
|
117
|
-
|
|
118
|
-
OPTIONS:
|
|
119
|
-
--host <string> Save server host for future runs
|
|
120
|
-
--port, -p <number> Save server port for future runs
|
|
121
|
-
--data-dir, -d <path> Save data directory for future runs
|
|
122
|
-
--password <string> Save auth password for future runs
|
|
123
|
-
--help Show config help
|
|
124
|
-
|
|
125
|
-
EXAMPLES:
|
|
126
|
-
coder-studio config
|
|
127
|
-
coder-studio config --host 0.0.0.0
|
|
128
|
-
coder-studio config --port 8080
|
|
129
|
-
coder-studio config --data-dir /tmp/cs-data
|
|
130
|
-
coder-studio config --password sekrit
|
|
131
|
-
coder-studio config --host 0.0.0.0 --port 8080
|
|
132
|
-
`);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function showVersion(): void {
|
|
136
|
-
const manifestPath = [
|
|
137
|
-
new URL("../package.json", import.meta.url),
|
|
138
|
-
new URL("../../package.json", import.meta.url),
|
|
139
|
-
].find((candidate) => existsSync(candidate));
|
|
140
|
-
if (!manifestPath) {
|
|
141
|
-
throw new Error("Unable to locate CLI package.json");
|
|
142
|
-
}
|
|
143
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { version?: string };
|
|
144
|
-
const version = manifest.version ?? "0.0.0";
|
|
145
|
-
console.log(`@spencer-kit/coder-studio v${version}`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function formatAuthBlocks(blocks: Awaited<ReturnType<typeof listAuthBlocks>>): string {
|
|
149
|
-
if (blocks.length === 0) {
|
|
150
|
-
return "No blocked IPs.";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return JSON.stringify(blocks, null, 2);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function resolveManagedScriptPath(): string {
|
|
157
|
-
const currentFile = fileURLToPath(import.meta.url);
|
|
158
|
-
const currentDir = dirname(currentFile);
|
|
159
|
-
const candidates = [
|
|
160
|
-
join(currentDir, "server-runner.js"),
|
|
161
|
-
join(currentDir, "server-runner.mjs"),
|
|
162
|
-
join(currentDir, "../src/server-runner.ts"),
|
|
163
|
-
];
|
|
164
|
-
|
|
165
|
-
const scriptPath = candidates.find((candidate) => existsSync(candidate));
|
|
166
|
-
if (!scriptPath) {
|
|
167
|
-
throw new Error("Unable to locate the managed server entry script");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return scriptPath;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function isCliEntrypoint(): boolean {
|
|
174
|
-
if (process.argv[1] === undefined) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const currentFile = fileURLToPath(import.meta.url);
|
|
179
|
-
const currentDir = dirname(currentFile);
|
|
180
|
-
const entryScript = resolve(process.argv[1]);
|
|
181
|
-
const entryCandidates = new Set([
|
|
182
|
-
currentFile,
|
|
183
|
-
join(currentDir, "../bin.js"),
|
|
184
|
-
join(currentDir, "../dist/bin.js"),
|
|
185
|
-
]);
|
|
186
|
-
|
|
187
|
-
return entryCandidates.has(entryScript);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function isRunningStatus(status: ServerStatus): boolean {
|
|
191
|
-
return status.status === "running" || status.status === "starting";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
interface ManagedStartupDecision {
|
|
195
|
-
existingStatus: ServerStatus | null;
|
|
196
|
-
restartRequested: boolean;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function shouldRestartRunningServer(status: ServerStatus): Promise<boolean> {
|
|
200
|
-
const currentUrl = getBrowserUrl(status) ?? getListenUrl(status) ?? "the existing server";
|
|
201
|
-
|
|
202
|
-
if (!isInteractiveSession()) {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return confirmYesNo(`Coder Studio is already running at ${currentUrl}. Restart it? [y/N] `);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function prepareManagedStartup(forceRestart = false): Promise<ManagedStartupDecision> {
|
|
210
|
-
const status = await getServerStatus();
|
|
211
|
-
if (!isRunningStatus(status)) {
|
|
212
|
-
return {
|
|
213
|
-
existingStatus: null,
|
|
214
|
-
restartRequested: false,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const restart = forceRestart ? true : await shouldRestartRunningServer(status);
|
|
219
|
-
if (!restart) {
|
|
220
|
-
const currentUrl = getBrowserUrl(status) ?? getListenUrl(status) ?? "n/a";
|
|
221
|
-
if (!isInteractiveSession()) {
|
|
222
|
-
console.log(
|
|
223
|
-
`Coder Studio is already running at ${currentUrl}. Service already exists and was not restarted.`
|
|
224
|
-
);
|
|
225
|
-
} else {
|
|
226
|
-
console.log(`Leaving the existing Coder Studio server running at ${currentUrl}.`);
|
|
227
|
-
}
|
|
228
|
-
return {
|
|
229
|
-
existingStatus: status,
|
|
230
|
-
restartRequested: false,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
console.log("Restarting the managed Coder Studio server...");
|
|
235
|
-
return {
|
|
236
|
-
existingStatus: null,
|
|
237
|
-
restartRequested: true,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function startManagedServerFlow(): Promise<void> {
|
|
242
|
-
await startManagedServer({
|
|
243
|
-
script: resolveManagedScriptPath(),
|
|
244
|
-
cwd: process.cwd(),
|
|
245
|
-
waitMs: MANAGED_SERVER_WAIT_MS,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function openManagedServerInBrowser(existingStatus?: ServerStatus | null): Promise<void> {
|
|
250
|
-
const status = existingStatus ?? (await getServerStatus());
|
|
251
|
-
const browserUrl = getBrowserUrl(status);
|
|
252
|
-
|
|
253
|
-
if (browserUrl === null) {
|
|
254
|
-
throw new Error("Unable to determine the running Coder Studio URL.");
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
console.log(`Opening Coder Studio in your browser: ${browserUrl}`);
|
|
258
|
-
await openBrowser(browserUrl);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export async function main(argv = process.argv.slice(2)): Promise<void> {
|
|
262
|
-
assertSupportedNodeVersion();
|
|
263
|
-
const args = parseArgs(argv);
|
|
264
|
-
|
|
265
|
-
if (args.command === "config") {
|
|
266
|
-
if (args.configHelp) {
|
|
267
|
-
showConfigHelp();
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
args.host === undefined &&
|
|
273
|
-
args.port === undefined &&
|
|
274
|
-
args.dataDir === undefined &&
|
|
275
|
-
args.password === undefined
|
|
276
|
-
) {
|
|
277
|
-
console.log(formatConfig(readCliConfig()));
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const savedConfig = readCliConfig();
|
|
282
|
-
const nextConfig: CliConfig = {
|
|
283
|
-
...(savedConfig?.host !== undefined ? { host: savedConfig.host } : {}),
|
|
284
|
-
...(savedConfig?.port !== undefined && savedConfig.port > 0
|
|
285
|
-
? { port: savedConfig.port }
|
|
286
|
-
: {}),
|
|
287
|
-
...(savedConfig?.dataDir !== undefined ? { dataDir: savedConfig.dataDir } : {}),
|
|
288
|
-
...(savedConfig?.password !== undefined ? { password: savedConfig.password } : {}),
|
|
289
|
-
...(args.host !== undefined ? { host: args.host } : {}),
|
|
290
|
-
...(args.port !== undefined ? { port: args.port } : {}),
|
|
291
|
-
...(args.dataDir !== undefined ? { dataDir: args.dataDir } : {}),
|
|
292
|
-
...(args.password !== undefined ? { password: args.password } : {}),
|
|
293
|
-
};
|
|
294
|
-
writeCliConfig(nextConfig);
|
|
295
|
-
console.log(formatConfig(nextConfig));
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (args.command === "stop") {
|
|
300
|
-
const stopped = await stopRunningServer();
|
|
301
|
-
console.log(stopped ? "Stopped Coder Studio server." : "No running Coder Studio server found.");
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (args.command === "status") {
|
|
306
|
-
console.log(formatStatus(await getServerStatus()));
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (args.command === "logs") {
|
|
311
|
-
showLogs(await getServerStatus(), { tail: args.tail, errorsOnly: args.errorsOnly });
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (args.command === "help") {
|
|
316
|
-
showHelp();
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (args.command === "version") {
|
|
321
|
-
showVersion();
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (args.command === "auth") {
|
|
326
|
-
if (args.authCommand === "ban-list") {
|
|
327
|
-
console.log(formatAuthBlocks(await listAuthBlocks()));
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (args.authCommand === "unblock") {
|
|
332
|
-
const cleared = await clearAuthBlockByIp(args.ip!);
|
|
333
|
-
console.log(cleared ? `Unblocked IP: ${args.ip}` : `No block found for IP: ${args.ip}`);
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (args.command === "open") {
|
|
339
|
-
const startup = await prepareManagedStartup(args.restart);
|
|
340
|
-
if (startup.existingStatus === null) {
|
|
341
|
-
await startManagedServerFlow();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
await openManagedServerInBrowser(startup.existingStatus);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (args.foreground) {
|
|
349
|
-
const startup = await prepareManagedStartup(args.restart);
|
|
350
|
-
if (startup.existingStatus !== null) {
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (startup.restartRequested) {
|
|
355
|
-
await stopRunningServer();
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
console.log("Starting Coder Studio Server in foreground...");
|
|
359
|
-
await startServer();
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const startup = await prepareManagedStartup(args.restart);
|
|
364
|
-
if (startup.existingStatus !== null) {
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
await startManagedServerFlow();
|
|
369
|
-
|
|
370
|
-
console.log("Coder Studio server started in background.");
|
|
371
|
-
console.log("Run `coder-studio status` to inspect the server.");
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (isCliEntrypoint()) {
|
|
375
|
-
main().catch((error) => {
|
|
376
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
377
|
-
console.error("CLI error:", message);
|
|
378
|
-
process.exit(1);
|
|
379
|
-
});
|
|
380
|
-
}
|
|
3
|
+
void main().catch((error) => {
|
|
4
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5
|
+
console.error("CLI error:", message);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const { spawnMock } = vi.hoisted(() => ({
|
|
5
|
+
spawnMock: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("node:child_process", () => ({
|
|
9
|
+
spawn: spawnMock,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { openBrowser } from "./browser.js";
|
|
13
|
+
|
|
14
|
+
const originalPlatform = process.platform;
|
|
15
|
+
|
|
16
|
+
describe("openBrowser windows child-process options", () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
Object.defineProperty(process, "platform", {
|
|
19
|
+
value: originalPlatform,
|
|
20
|
+
configurable: true,
|
|
21
|
+
});
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("uses the Windows open command and passes windowsHide to spawn", async () => {
|
|
26
|
+
Object.defineProperty(process, "platform", {
|
|
27
|
+
value: "win32",
|
|
28
|
+
configurable: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
spawnMock.mockImplementation(() => {
|
|
32
|
+
const child = new EventEmitter() as EventEmitter & { unref: ReturnType<typeof vi.fn> };
|
|
33
|
+
child.unref = vi.fn();
|
|
34
|
+
|
|
35
|
+
queueMicrotask(() => {
|
|
36
|
+
child.emit("spawn");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return child;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await expect(openBrowser("https://example.com")).resolves.toBeUndefined();
|
|
43
|
+
|
|
44
|
+
expect(spawnMock).toHaveBeenCalledWith(
|
|
45
|
+
"cmd",
|
|
46
|
+
["/c", "start", "", "https://example.com"],
|
|
47
|
+
expect.objectContaining({ windowsHide: true })
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|