@spencer-kit/coder-studio 0.3.4 → 0.3.6

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.
@@ -6,13 +6,13 @@
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/index-xgtwbfqN.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-C4xnb_C5.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-gL8kTxHV.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-mc7Xu3WV.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spencer-kit/coder-studio",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "description": "Deploy once, code everywhere. Browser-based AI coding workspace for Claude Code and Codex.",
6
6
  "main": "./dist/esm/index.mjs",
package/src/bin.test.ts CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  readCliConfig,
14
14
  startManagedServer,
15
15
  startServer,
16
+ verifyLocalDatabaseCompatibility,
16
17
  stopRunningServer,
17
18
  writeCliConfig,
18
19
  } = vi.hoisted(() => ({
@@ -25,6 +26,7 @@ const {
25
26
  readCliConfig: vi.fn(),
26
27
  startManagedServer: vi.fn(),
27
28
  startServer: vi.fn(),
29
+ verifyLocalDatabaseCompatibility: vi.fn(),
28
30
  stopRunningServer: vi.fn(),
29
31
  writeCliConfig: vi.fn(),
30
32
  }));
@@ -50,6 +52,7 @@ vi.mock("./auth-control.js", () => ({
50
52
 
51
53
  vi.mock("./server-runner.js", () => ({
52
54
  startServer,
55
+ verifyLocalDatabaseCompatibility,
53
56
  }));
54
57
 
55
58
  vi.mock("./prompts.js", () => ({
@@ -69,6 +72,7 @@ beforeEach(() => {
69
72
  writeCliConfig.mockImplementation(() => undefined);
70
73
  startManagedServer.mockResolvedValue(undefined);
71
74
  startServer.mockResolvedValue({ stop: vi.fn() });
75
+ verifyLocalDatabaseCompatibility.mockImplementation(() => undefined);
72
76
  stopRunningServer.mockResolvedValue(false);
73
77
  clearAuthBlockByIp.mockResolvedValue(false);
74
78
  listAuthBlocks.mockResolvedValue([]);
@@ -147,6 +151,33 @@ describe("main", () => {
147
151
  expect(logSpy).toHaveBeenCalledWith("Starting Coder Studio Server in foreground...");
148
152
  });
149
153
 
154
+ it("prompts to delete and rebuild when foreground startup sees an incompatible schema", async () => {
155
+ const tempDir = mkdtempSync(join(tmpdir(), "cs-cli-db-rebuild-"));
156
+ const dbPath = join(tempDir, "coder-studio.db");
157
+ writeFileSync(dbPath, "broken", "utf-8");
158
+
159
+ const incompatibleError = Object.assign(new Error("schema mismatch"), {
160
+ code: "db_incompatible_schema",
161
+ dbPath,
162
+ });
163
+ startServer.mockRejectedValueOnce(incompatibleError).mockResolvedValueOnce({ stop: vi.fn() });
164
+ confirmYesNo.mockResolvedValue(true);
165
+
166
+ try {
167
+ await expect(main(["serve", "--foreground"])).resolves.toBeUndefined();
168
+
169
+ expect(confirmYesNo).toHaveBeenCalledWith(
170
+ expect.stringContaining("Delete and rebuild the local database")
171
+ );
172
+ expect(startServer).toHaveBeenCalledTimes(2);
173
+ expect(existsSync(dbPath)).toBe(false);
174
+ } finally {
175
+ if (existsSync(tempDir)) {
176
+ rmSync(tempDir, { recursive: true, force: true });
177
+ }
178
+ }
179
+ });
180
+
150
181
  it("starts pm2-managed mode for bare serve", async () => {
151
182
  const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
152
183
 
@@ -161,6 +192,45 @@ describe("main", () => {
161
192
  expect(logSpy).toHaveBeenCalledWith("Run `coder-studio status` to inspect the server.");
162
193
  });
163
194
 
195
+ it("preflights incompatible schemas before managed startup and rebuilds before launching pm2", async () => {
196
+ const tempDir = mkdtempSync(join(tmpdir(), "cs-cli-db-preflight-"));
197
+ const dbPath = join(tempDir, "coder-studio.db");
198
+ writeFileSync(dbPath, "broken", "utf-8");
199
+
200
+ const incompatibleError = Object.assign(new Error("schema mismatch"), {
201
+ code: "db_incompatible_schema",
202
+ dbPath,
203
+ });
204
+ verifyLocalDatabaseCompatibility
205
+ .mockImplementationOnce(() => {
206
+ throw incompatibleError;
207
+ })
208
+ .mockImplementationOnce(() => undefined);
209
+ confirmYesNo.mockResolvedValue(true);
210
+
211
+ try {
212
+ await main(["serve"]);
213
+
214
+ expect(confirmYesNo).toHaveBeenCalledWith(
215
+ expect.stringContaining("Delete and rebuild the local database")
216
+ );
217
+ expect(verifyLocalDatabaseCompatibility).toHaveBeenCalledTimes(2);
218
+ expect(startManagedServer).toHaveBeenCalledTimes(1);
219
+ const verifyOrder = verifyLocalDatabaseCompatibility.mock.invocationCallOrder[0];
220
+ const startOrder = startManagedServer.mock.invocationCallOrder[0];
221
+ expect(verifyOrder).toBeDefined();
222
+ expect(startOrder).toBeDefined();
223
+ expect(verifyOrder ?? Number.POSITIVE_INFINITY).toBeLessThan(
224
+ startOrder ?? Number.POSITIVE_INFINITY
225
+ );
226
+ expect(existsSync(dbPath)).toBe(false);
227
+ } finally {
228
+ if (existsSync(tempDir)) {
229
+ rmSync(tempDir, { recursive: true, force: true });
230
+ }
231
+ }
232
+ });
233
+
164
234
  it("prints status output for status command", async () => {
165
235
  getServerStatus.mockResolvedValue({
166
236
  status: "running",
package/src/cli.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync } from "fs";
1
+ import { existsSync, rmSync } from "fs";
2
2
  import { dirname, join } from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { clearAuthBlockByIp, listAuthBlocks } from "./auth-control.js";
@@ -11,7 +11,7 @@ import { parseArgs } from "./parse-args.js";
11
11
  import { startManagedServer } from "./pm2-control.js";
12
12
  import { confirmYesNo, isInteractiveSession } from "./prompts.js";
13
13
  import { getServerStatus, type ServerStatus, stopRunningServer } from "./server-control.js";
14
- import { startServer } from "./server-runner.js";
14
+ import { startServer, verifyLocalDatabaseCompatibility } from "./server-runner.js";
15
15
  import { getBrowserUrl, getListenIp, getListenUrl } from "./server-url.js";
16
16
 
17
17
  const MANAGED_SERVER_WAIT_MS = 5000;
@@ -221,6 +221,60 @@ async function startManagedServerFlow(): Promise<void> {
221
221
  });
222
222
  }
223
223
 
224
+ interface IncompatibleSchemaErrorLike {
225
+ code: "db_incompatible_schema";
226
+ dbPath: string;
227
+ }
228
+
229
+ function parseIncompatibleSchemaError(error: unknown): IncompatibleSchemaErrorLike | null {
230
+ if (!error || typeof error !== "object") {
231
+ return null;
232
+ }
233
+
234
+ const candidate = error as { code?: unknown; dbPath?: unknown };
235
+ if (candidate.code !== "db_incompatible_schema" || typeof candidate.dbPath !== "string") {
236
+ return null;
237
+ }
238
+
239
+ return {
240
+ code: "db_incompatible_schema",
241
+ dbPath: candidate.dbPath,
242
+ };
243
+ }
244
+
245
+ async function handleIncompatibleSchema(error: unknown): Promise<boolean> {
246
+ const payload = parseIncompatibleSchemaError(error);
247
+ if (!payload) {
248
+ return false;
249
+ }
250
+
251
+ const approved = isInteractiveSession()
252
+ ? await confirmYesNo(
253
+ `Local database is incompatible at ${payload.dbPath}. Delete and rebuild the local database? [y/N] `
254
+ )
255
+ : false;
256
+
257
+ if (!approved) {
258
+ throw error;
259
+ }
260
+
261
+ rmSync(payload.dbPath, { force: true });
262
+ return true;
263
+ }
264
+
265
+ async function verifyManagedDatabaseCompatibility(): Promise<void> {
266
+ try {
267
+ verifyLocalDatabaseCompatibility();
268
+ } catch (error) {
269
+ const rebuilt = await handleIncompatibleSchema(error);
270
+ if (!rebuilt) {
271
+ throw error;
272
+ }
273
+
274
+ verifyLocalDatabaseCompatibility();
275
+ }
276
+ }
277
+
224
278
  async function openManagedServerInBrowser(existingStatus?: ServerStatus | null): Promise<void> {
225
279
  const status = existingStatus ?? (await getServerStatus());
226
280
  const browserUrl = getBrowserUrl(status);
@@ -313,6 +367,7 @@ export async function main(argv = process.argv.slice(2)): Promise<void> {
313
367
  if (args.command === "open") {
314
368
  const startup = await prepareManagedStartup(args.restart);
315
369
  if (startup.existingStatus === null) {
370
+ await verifyManagedDatabaseCompatibility();
316
371
  await startManagedServerFlow();
317
372
  }
318
373
 
@@ -331,7 +386,15 @@ export async function main(argv = process.argv.slice(2)): Promise<void> {
331
386
  }
332
387
 
333
388
  console.log("Starting Coder Studio Server in foreground...");
334
- await startServer();
389
+ try {
390
+ await startServer();
391
+ } catch (error) {
392
+ const rebuilt = await handleIncompatibleSchema(error);
393
+ if (!rebuilt) {
394
+ throw error;
395
+ }
396
+ await startServer();
397
+ }
335
398
  return;
336
399
  }
337
400
 
@@ -340,6 +403,7 @@ export async function main(argv = process.argv.slice(2)): Promise<void> {
340
403
  return;
341
404
  }
342
405
 
406
+ await verifyManagedDatabaseCompatibility();
343
407
  await startManagedServerFlow();
344
408
 
345
409
  console.log("Coder Studio server started in background.");
@@ -2,8 +2,19 @@ import { fileURLToPath } from "url";
2
2
  import { afterEach, describe, expect, it, vi } from "vitest";
3
3
  import { getCliVersion } from "./package-manifest.js";
4
4
 
5
- const { createServer, readCliConfig, hasWebAssets, getStaticAssetsDir } = vi.hoisted(() => ({
5
+ const {
6
+ createServer,
7
+ parseServerConfig,
8
+ openDatabase,
9
+ closeDatabase,
10
+ readCliConfig,
11
+ hasWebAssets,
12
+ getStaticAssetsDir,
13
+ } = vi.hoisted(() => ({
6
14
  createServer: vi.fn(),
15
+ parseServerConfig: vi.fn(),
16
+ openDatabase: vi.fn(),
17
+ closeDatabase: vi.fn(),
7
18
  readCliConfig: vi.fn(),
8
19
  hasWebAssets: vi.fn(),
9
20
  getStaticAssetsDir: vi.fn(),
@@ -11,6 +22,9 @@ const { createServer, readCliConfig, hasWebAssets, getStaticAssetsDir } = vi.hoi
11
22
 
12
23
  vi.mock("@coder-studio/server", () => ({
13
24
  createServer,
25
+ parseServerConfig,
26
+ openDatabase,
27
+ closeDatabase,
14
28
  }));
15
29
 
16
30
  vi.mock("./config-store.js", () => ({
@@ -22,7 +36,12 @@ vi.mock("./embed.js", () => ({
22
36
  getStaticAssetsDir,
23
37
  }));
24
38
 
25
- import { buildServerConfig, runServerEntrypoint, startServer } from "./server-runner";
39
+ import {
40
+ buildServerConfig,
41
+ runServerEntrypoint,
42
+ startServer,
43
+ verifyLocalDatabaseCompatibility,
44
+ } from "./server-runner";
26
45
 
27
46
  describe("server-runner", () => {
28
47
  afterEach(() => {
@@ -97,6 +116,30 @@ describe("server-runner", () => {
97
116
  expect(processExitSpy).toHaveBeenCalledWith(0);
98
117
  });
99
118
 
119
+ it("verifies local database compatibility using the resolved server config", () => {
120
+ readCliConfig.mockReturnValue({
121
+ dataDir: "/tmp/cs-data/coder-studio.db",
122
+ });
123
+ hasWebAssets.mockReturnValue(true);
124
+ getStaticAssetsDir.mockReturnValue("/tmp/web");
125
+ parseServerConfig.mockReturnValue({
126
+ dataDir: "/tmp/cs-data/coder-studio.db",
127
+ });
128
+
129
+ const db = { close: vi.fn() };
130
+ openDatabase.mockReturnValue(db);
131
+
132
+ verifyLocalDatabaseCompatibility();
133
+
134
+ expect(parseServerConfig).toHaveBeenCalledWith({
135
+ appVersion: getCliVersion(import.meta.url),
136
+ dataDir: "/tmp/cs-data/coder-studio.db",
137
+ webRoot: "/tmp/web",
138
+ });
139
+ expect(openDatabase).toHaveBeenCalledWith("/tmp/cs-data/coder-studio.db");
140
+ expect(closeDatabase).toHaveBeenCalledWith(db);
141
+ });
142
+
100
143
  it("starts the server when executed as the entrypoint", async () => {
101
144
  readCliConfig.mockReturnValue(null);
102
145
  hasWebAssets.mockReturnValue(true);
@@ -1,4 +1,7 @@
1
1
  import type { Server, ServerConfig } from "@coder-studio/server";
2
+ import { closeDatabase, openDatabase, parseServerConfig } from "@coder-studio/server";
3
+ import { mkdirSync } from "fs";
4
+ import { dirname } from "path";
2
5
  import { fileURLToPath } from "url";
3
6
  import { readCliConfig } from "./config-store.js";
4
7
  import { getStaticAssetsDir, hasWebAssets } from "./embed.js";
@@ -35,6 +38,16 @@ export const buildServerConfig = (): Partial<ServerConfig> => {
35
38
  return config;
36
39
  };
37
40
 
41
+ export const verifyLocalDatabaseCompatibility = (): void => {
42
+ const config = parseServerConfig(buildServerConfig());
43
+ if (config.dataDir !== ":memory:") {
44
+ mkdirSync(dirname(config.dataDir), { recursive: true });
45
+ }
46
+
47
+ const db = openDatabase(config.dataDir);
48
+ closeDatabase(db);
49
+ };
50
+
38
51
  const createShutdownHandler = (server: Server) => async () => {
39
52
  await server.stop();
40
53
  process.exit(0);