@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.
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>
@@ -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/svg+xml" href="/vite.svg" />
9
- <script type="module" crossorigin src="/assets/index-A-2YPePM.js"></script>
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-BLMivSS1.css">
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.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
- "description": "Coder Studio CLI - The only published package",
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
@@ -61,7 +61,7 @@ vi.mock("./browser.js", () => ({
61
61
  openBrowser,
62
62
  }));
63
63
 
64
- import { main } from "./bin";
64
+ import { main } from "./cli";
65
65
  import { parseArgs, RUNTIME_CONFIG_ERROR } from "./parse-args";
66
66
 
67
67
  beforeEach(() => {
package/src/bin.ts CHANGED
@@ -1,380 +1,7 @@
1
- import { existsSync, readFileSync } from "fs";
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
- const MANAGED_SERVER_WAIT_MS = 5000;
17
- const DEFAULT_LOG_TAIL_LINES = 40;
18
-
19
- function formatConfig(config: CliConfig | null): string {
20
- return JSON.stringify(config ?? {}, null, 2);
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
+ });
package/src/browser.ts CHANGED
@@ -18,6 +18,7 @@ export async function openBrowser(url: string): Promise<void> {
18
18
  const child = spawn(command, args, {
19
19
  detached: true,
20
20
  stdio: "ignore",
21
+ windowsHide: true,
21
22
  });
22
23
 
23
24
  child.once("error", reject);