@spencer-kit/coder-studio 0.3.5 → 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.
- package/CHANGELOG.md +6 -0
- package/dist/esm/bin.mjs +1311 -306
- package/dist/esm/bin.mjs.map +4 -4
- package/dist/esm/migrations/001_init.sql +15 -1
- package/dist/esm/server-runner.mjs +1348 -390
- package/dist/esm/server-runner.mjs.map +4 -4
- package/dist/web/assets/index-C4xnb_C5.js +111 -0
- package/dist/web/assets/index-C4xnb_C5.js.map +1 -0
- package/dist/web/assets/index-mc7Xu3WV.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/bin.test.ts +70 -0
- package/src/cli.ts +67 -3
- package/src/server-runner.test.ts +45 -2
- package/src/server-runner.ts +13 -0
- package/dist/web/assets/index-B3PO5hz_.js +0 -111
- package/dist/web/assets/index-B3PO5hz_.js.map +0 -1
- package/dist/web/assets/index-BbpuXQCm.css +0 -1
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
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-
|
|
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
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
|
-
|
|
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 {
|
|
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 {
|
|
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);
|
package/src/server-runner.ts
CHANGED
|
@@ -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);
|