@spencer-kit/coder-studio 0.4.0 → 0.4.1

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-CNoLfuZU.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-g7vn9KmI.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-PK4VJ5X2.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-cMGhBE_V.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-BkUU2b7M.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.4.0",
3
+ "version": "0.4.1",
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",
@@ -8,19 +8,19 @@ describe("auth-control", () => {
8
8
  const originalHome = process.env.HOME;
9
9
  const originalUserProfile = process.env.USERPROFILE;
10
10
  let testHomeDir: string;
11
- let dataPath: string;
11
+ let legacyStateFilePath: string;
12
12
  let authBlocksPath: string;
13
13
 
14
14
  beforeEach(() => {
15
15
  testHomeDir = mkdtempSync(join(tmpdir(), "cs-auth-control-home-"));
16
16
  process.env.HOME = testHomeDir;
17
17
  process.env.USERPROFILE = testHomeDir;
18
- dataPath = join(testHomeDir, "auth-control.db");
18
+ legacyStateFilePath = join(testHomeDir, "legacy-state.sqlite");
19
19
  authBlocksPath = join(testHomeDir, "state", "auth-login-blocks.json");
20
20
  mkdirSync(join(testHomeDir, ".coder-studio"), { recursive: true });
21
21
  writeFileSync(
22
22
  join(testHomeDir, ".coder-studio", "config.json"),
23
- JSON.stringify({ dataDir: dataPath }, null, 2),
23
+ JSON.stringify({ dataDir: legacyStateFilePath }, null, 2),
24
24
  "utf-8"
25
25
  );
26
26
  });
@@ -1,4 +1,4 @@
1
- import { dirname, join } from "node:path";
1
+ import { join } from "node:path";
2
2
  import { AuthLoginBlockRepo, parseServerConfig } from "@coder-studio/server";
3
3
  import { readCliConfig } from "./config-store.js";
4
4
 
@@ -10,16 +10,16 @@ export interface CliAuthBlock {
10
10
  blockedUntil: number;
11
11
  }
12
12
 
13
- function resolveDataDir(): string {
13
+ function resolveStateDir(): string {
14
14
  const savedConfig = readCliConfig();
15
15
  return parseServerConfig({
16
- ...(savedConfig?.dataDir !== undefined ? { dataDir: savedConfig.dataDir } : {}),
17
- }).dataDir;
16
+ ...(savedConfig?.stateDir !== undefined ? { stateDir: savedConfig.stateDir } : {}),
17
+ }).stateDir;
18
18
  }
19
19
 
20
20
  export async function listAuthBlocks(now = Date.now()): Promise<CliAuthBlock[]> {
21
21
  const repo = new AuthLoginBlockRepo({
22
- filePath: join(dirname(resolveDataDir()), "state", "auth-login-blocks.json"),
22
+ filePath: join(resolveStateDir(), "state", "auth-login-blocks.json"),
23
23
  });
24
24
  return repo.listActiveBlocks(now).map((record) => ({
25
25
  ip: record.ip,
@@ -32,7 +32,7 @@ export async function listAuthBlocks(now = Date.now()): Promise<CliAuthBlock[]>
32
32
 
33
33
  export async function clearAuthBlockByIp(ip: string): Promise<boolean> {
34
34
  const repo = new AuthLoginBlockRepo({
35
- filePath: join(dirname(resolveDataDir()), "state", "auth-login-blocks.json"),
35
+ filePath: join(resolveStateDir(), "state", "auth-login-blocks.json"),
36
36
  });
37
37
  return repo.delete(ip);
38
38
  }
package/src/bin.test.ts CHANGED
@@ -500,7 +500,7 @@ describe("main", () => {
500
500
  readCliConfig.mockReturnValue({
501
501
  host: "0.0.0.0",
502
502
  port: 0,
503
- dataDir: "/tmp/cs-data/coder-studio.db",
503
+ stateDir: "/tmp/cs-data",
504
504
  password: "sekrit",
505
505
  });
506
506
 
@@ -508,7 +508,7 @@ describe("main", () => {
508
508
 
509
509
  expect(writeCliConfig).toHaveBeenCalledWith({
510
510
  host: "127.0.0.1",
511
- dataDir: "/tmp/cs-data/coder-studio.db",
511
+ stateDir: "/tmp/cs-data",
512
512
  password: "sekrit",
513
513
  });
514
514
  });
@@ -577,9 +577,17 @@ describe("parseArgs", () => {
577
577
  });
578
578
 
579
579
  it("parses config command with data-dir and password values", () => {
580
+ expect(parseArgs(["config", "--state-dir", "/tmp/cs-data", "--password", "sekrit"])).toEqual({
581
+ command: "config",
582
+ stateDir: "/tmp/cs-data",
583
+ password: "sekrit",
584
+ });
585
+ });
586
+
587
+ it("accepts legacy data-dir as a config alias", () => {
580
588
  expect(parseArgs(["config", "--data-dir", "/tmp/cs-data", "--password", "sekrit"])).toEqual({
581
589
  command: "config",
582
- dataDir: "/tmp/cs-data",
590
+ stateDir: "/tmp/cs-data",
583
591
  password: "sekrit",
584
592
  });
585
593
  });
@@ -705,14 +713,18 @@ describe("parseArgs", () => {
705
713
  expect(() => parseArgs(["serve", "--port", "4186"])).toThrow(RUNTIME_CONFIG_ERROR);
706
714
  });
707
715
 
708
- it("rejects serve-time data-dir overrides", () => {
709
- expect(() => parseArgs(["serve", "--data-dir", "/tmp/data"])).toThrow(RUNTIME_CONFIG_ERROR);
716
+ it("rejects serve-time state-dir overrides", () => {
717
+ expect(() => parseArgs(["serve", "--state-dir", "/tmp/data"])).toThrow(RUNTIME_CONFIG_ERROR);
710
718
  });
711
719
 
712
- it("rejects bare data-dir overrides", () => {
720
+ it("rejects bare legacy data-dir overrides", () => {
713
721
  expect(() => parseArgs(["--data-dir", "/tmp/cs-data"])).toThrow(RUNTIME_CONFIG_ERROR);
714
722
  });
715
723
 
724
+ it("rejects bare state-dir overrides", () => {
725
+ expect(() => parseArgs(["--state-dir", "/tmp/cs-data"])).toThrow(RUNTIME_CONFIG_ERROR);
726
+ });
727
+
716
728
  it("rejects serve-time password overrides", () => {
717
729
  expect(() => parseArgs(["serve", "--password", "sekrit"])).toThrow(RUNTIME_CONFIG_ERROR);
718
730
  });
@@ -757,9 +769,9 @@ describe("parseArgs", () => {
757
769
  expect(() => parseArgs(["logs", "--tail", "10junk"])).toThrow("Invalid tail number");
758
770
  });
759
771
 
760
- it("rejects stop-time data-dir overrides", () => {
761
- expect(() => parseArgs(["stop", "--data-dir", "/tmp/cs-data"])).toThrow(
762
- "Unknown option: --data-dir"
772
+ it("rejects stop-time state-dir overrides", () => {
773
+ expect(() => parseArgs(["stop", "--state-dir", "/tmp/cs-data"])).toThrow(
774
+ "Unknown option: --state-dir"
763
775
  );
764
776
  });
765
777
 
@@ -839,10 +851,10 @@ describe("parseArgs", () => {
839
851
  });
840
852
  });
841
853
 
842
- it("allows config-time data-dir updates", () => {
843
- expect(parseArgs(["config", "--data-dir", "/custom/path"])).toEqual({
854
+ it("allows config-time state-dir updates", () => {
855
+ expect(parseArgs(["config", "--state-dir", "/custom/path"])).toEqual({
844
856
  command: "config",
845
- dataDir: "/custom/path",
857
+ stateDir: "/custom/path",
846
858
  });
847
859
  });
848
860
 
package/src/cli.ts CHANGED
@@ -71,7 +71,7 @@ COMMANDS:
71
71
  server Alias for serve
72
72
  open Start the server if needed and open Coder Studio in a browser
73
73
  auth Manage auth login blocks in local server storage
74
- config Persist CLI host/port/data-dir/password settings
74
+ config Persist CLI host/port/state-dir/password settings
75
75
  stop Stop the managed Coder Studio server
76
76
  status Show the managed server status
77
77
  logs Show the managed server logs
@@ -81,7 +81,7 @@ COMMANDS:
81
81
  OPTIONS:
82
82
  --host <string> Save server host for future runs
83
83
  --port, -p <number> Save server port for future runs
84
- --data-dir, -d <path> Save data directory for future runs
84
+ --state-dir, --data-dir, -d <path> Save state directory for future runs
85
85
  --password <string> Save auth password for future runs
86
86
  --restart Restart an already running managed server for serve/open
87
87
  --help Show help
@@ -119,7 +119,7 @@ BEHAVIOR:
119
119
  OPTIONS:
120
120
  --host <string> Save server host for future runs
121
121
  --port, -p <number> Save server port for future runs
122
- --data-dir, -d <path> Save data directory for future runs
122
+ --state-dir, --data-dir, -d <path> Save state directory for future runs
123
123
  --password <string> Save auth password for future runs
124
124
  --help Show config help
125
125
 
@@ -127,7 +127,7 @@ EXAMPLES:
127
127
  coder-studio config
128
128
  coder-studio config --host 0.0.0.0
129
129
  coder-studio config --port 8080
130
- coder-studio config --data-dir /tmp/cs-data
130
+ coder-studio config --state-dir /tmp/cs-data
131
131
  coder-studio config --password sekrit
132
132
  coder-studio config --host 0.0.0.0 --port 8080
133
133
  `);
@@ -246,7 +246,7 @@ export async function main(argv = process.argv.slice(2)): Promise<void> {
246
246
  if (
247
247
  args.host === undefined &&
248
248
  args.port === undefined &&
249
- args.dataDir === undefined &&
249
+ args.stateDir === undefined &&
250
250
  args.password === undefined
251
251
  ) {
252
252
  console.log(formatConfig(readCliConfig()));
@@ -259,11 +259,11 @@ export async function main(argv = process.argv.slice(2)): Promise<void> {
259
259
  ...(savedConfig?.port !== undefined && savedConfig.port > 0
260
260
  ? { port: savedConfig.port }
261
261
  : {}),
262
- ...(savedConfig?.dataDir !== undefined ? { dataDir: savedConfig.dataDir } : {}),
262
+ ...(savedConfig?.stateDir !== undefined ? { stateDir: savedConfig.stateDir } : {}),
263
263
  ...(savedConfig?.password !== undefined ? { password: savedConfig.password } : {}),
264
264
  ...(args.host !== undefined ? { host: args.host } : {}),
265
265
  ...(args.port !== undefined ? { port: args.port } : {}),
266
- ...(args.dataDir !== undefined ? { dataDir: args.dataDir } : {}),
266
+ ...(args.stateDir !== undefined ? { stateDir: args.stateDir } : {}),
267
267
  ...(args.password !== undefined ? { password: args.password } : {}),
268
268
  };
269
269
  writeCliConfig(nextConfig);
@@ -1,11 +1,11 @@
1
- import { existsSync, mkdtempSync, readFileSync, rmSync } from "fs";
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
2
2
  import { tmpdir } from "os";
3
3
  import { join } from "path";
4
4
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
5
  import {
6
6
  type CliConfig,
7
7
  getCliConfigPath,
8
- normalizeDataDir,
8
+ normalizeLegacyDataDir,
9
9
  readCliConfig,
10
10
  writeCliConfig,
11
11
  } from "./config-store.js";
@@ -43,43 +43,86 @@ describe("config-store", () => {
43
43
  expect(readCliConfig()).toBeNull();
44
44
  });
45
45
 
46
- it("writes and reads host port data-dir and password config", () => {
47
- const config: CliConfig = {
46
+ it("writes and reads host port state-dir and password config", () => {
47
+ const config = {
48
48
  host: "0.0.0.0",
49
49
  port: 4186,
50
- dataDir: "/tmp/cs-data/coder-studio.db",
50
+ stateDir: "/tmp/cs-data",
51
51
  password: "sekrit",
52
52
  };
53
53
 
54
- writeCliConfig(config);
54
+ writeCliConfig(config as CliConfig);
55
55
 
56
56
  expect(readCliConfig()).toEqual(config);
57
57
  });
58
58
 
59
- it("normalizes a directory input into the default state anchor path", () => {
60
- expect(normalizeDataDir("/tmp/cs-data")).toBe("/tmp/cs-data/coder-studio.db");
59
+ it("keeps a directory input as the state directory", () => {
60
+ expect(normalizeLegacyDataDir("/tmp/cs-data")).toBe("/tmp/cs-data");
61
61
  });
62
62
 
63
- it("keeps an explicit state anchor file path unchanged", () => {
64
- expect(normalizeDataDir("/tmp/cs-data/custom.db")).toBe("/tmp/cs-data/custom.db");
63
+ it("normalizes a legacy sqlite file path to its parent state directory", () => {
64
+ expect(normalizeLegacyDataDir("/tmp/cs-data/custom.sqlite")).toBe("/tmp/cs-data");
65
+ });
66
+
67
+ it("preserves a saved stateDir that ends with .db", () => {
68
+ writeCliConfig({
69
+ stateDir: "/tmp/modern-state/custom-dir.db",
70
+ } as CliConfig);
71
+
72
+ expect(readCliConfig()).toEqual({
73
+ stateDir: "/tmp/modern-state/custom-dir.db",
74
+ });
75
+ });
76
+
77
+ it("preserves an in-memory saved stateDir", () => {
78
+ writeCliConfig({
79
+ stateDir: ":memory:",
80
+ } as CliConfig);
81
+
82
+ expect(readCliConfig()).toEqual({
83
+ stateDir: ":memory:",
84
+ });
85
+ });
86
+
87
+ it("reads legacy dataDir config as stateDir", () => {
88
+ writeCliConfig({
89
+ host: "127.0.0.1",
90
+ port: 4186,
91
+ stateDir: "/tmp/modern-state",
92
+ password: "sekrit",
93
+ } as CliConfig);
94
+
95
+ const configPath = getCliConfigPath();
96
+ const stored = JSON.parse(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
97
+ stored.dataDir = "/tmp/legacy-state/legacy-state.sqlite";
98
+ delete stored.stateDir;
99
+
100
+ writeFileSync(configPath, JSON.stringify(stored, null, 2), "utf-8");
101
+
102
+ expect(readCliConfig()).toEqual({
103
+ host: "127.0.0.1",
104
+ port: 4186,
105
+ stateDir: "/tmp/legacy-state",
106
+ password: "sekrit",
107
+ });
65
108
  });
66
109
 
67
110
  it("does not persist ephemeral port zero in config", () => {
68
111
  writeCliConfig({
69
112
  host: "127.0.0.1",
70
113
  port: 0,
71
- dataDir: "/tmp/cs-data/coder-studio.db",
114
+ stateDir: "/tmp/cs-data",
72
115
  password: "sekrit",
73
- });
116
+ } as CliConfig);
74
117
 
75
118
  expect(JSON.parse(readFileSync(getCliConfigPath(), "utf-8"))).toEqual({
76
119
  host: "127.0.0.1",
77
- dataDir: "/tmp/cs-data/coder-studio.db",
120
+ stateDir: "/tmp/cs-data",
78
121
  password: "sekrit",
79
122
  });
80
123
  expect(readCliConfig()).toEqual({
81
124
  host: "127.0.0.1",
82
- dataDir: "/tmp/cs-data/coder-studio.db",
125
+ stateDir: "/tmp/cs-data",
83
126
  password: "sekrit",
84
127
  });
85
128
  });
@@ -1,13 +1,12 @@
1
+ import { normalizeLegacyStateDir, normalizeStateDir } from "@coder-studio/core/state-paths";
1
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
3
  import { homedir } from "os";
3
- import { basename, join } from "path";
4
-
5
- const DEFAULT_STATE_ANCHOR_FILE = "coder-studio.db";
4
+ import { join } from "path";
6
5
 
7
6
  export interface CliConfig {
8
7
  host?: string;
9
8
  port?: number;
10
- dataDir?: string;
9
+ stateDir?: string;
11
10
  password?: string;
12
11
  }
13
12
 
@@ -15,14 +14,8 @@ export function getCliConfigPath(): string {
15
14
  return join(homedir(), ".coder-studio", "config.json");
16
15
  }
17
16
 
18
- export function normalizeDataDir(input: string): string {
19
- if (input.endsWith(".db")) {
20
- return input;
21
- }
22
- if (basename(input).includes(".")) {
23
- return input;
24
- }
25
- return join(input, DEFAULT_STATE_ANCHOR_FILE);
17
+ export function normalizeLegacyDataDir(input: string): string {
18
+ return normalizeLegacyStateDir(input);
26
19
  }
27
20
 
28
21
  export function readCliConfig(): CliConfig | null {
@@ -32,16 +25,33 @@ export function readCliConfig(): CliConfig | null {
32
25
  }
33
26
 
34
27
  try {
35
- const parsed = JSON.parse(readFileSync(path, "utf-8")) as CliConfig;
28
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as {
29
+ host?: unknown;
30
+ port?: unknown;
31
+ stateDir?: unknown;
32
+ dataDir?: unknown;
33
+ password?: unknown;
34
+ };
36
35
  if (
37
36
  (parsed.host !== undefined && typeof parsed.host !== "string") ||
38
37
  (parsed.port !== undefined && typeof parsed.port !== "number") ||
38
+ (parsed.stateDir !== undefined && typeof parsed.stateDir !== "string") ||
39
39
  (parsed.dataDir !== undefined && typeof parsed.dataDir !== "string") ||
40
40
  (parsed.password !== undefined && typeof parsed.password !== "string")
41
41
  ) {
42
42
  return null;
43
43
  }
44
- return parsed;
44
+
45
+ return {
46
+ ...(parsed.host !== undefined ? { host: parsed.host } : {}),
47
+ ...(parsed.port !== undefined ? { port: parsed.port } : {}),
48
+ ...(parsed.stateDir !== undefined
49
+ ? { stateDir: normalizeStateDir(parsed.stateDir) }
50
+ : parsed.dataDir !== undefined
51
+ ? { stateDir: normalizeLegacyDataDir(parsed.dataDir) }
52
+ : {}),
53
+ ...(parsed.password !== undefined ? { password: parsed.password } : {}),
54
+ };
45
55
  } catch {
46
56
  return null;
47
57
  }
@@ -53,7 +63,7 @@ export function writeCliConfig(config: CliConfig): void {
53
63
  const normalizedConfig: CliConfig = {
54
64
  ...(config.host !== undefined ? { host: config.host } : {}),
55
65
  ...(config.port !== undefined && config.port > 0 ? { port: config.port } : {}),
56
- ...(config.dataDir !== undefined ? { dataDir: normalizeDataDir(config.dataDir) } : {}),
66
+ ...(config.stateDir !== undefined ? { stateDir: normalizeStateDir(config.stateDir) } : {}),
57
67
  ...(config.password !== undefined ? { password: config.password } : {}),
58
68
  };
59
69
  if (!existsSync(dir)) {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
2
 
3
3
  interface CliPackageManifest {
4
+ name?: string;
4
5
  version?: string;
5
6
  }
6
7
 
@@ -26,3 +27,7 @@ export function getCliPackageManifest(importMetaUrl: string): CliPackageManifest
26
27
  export function getCliVersion(importMetaUrl: string): string {
27
28
  return getCliPackageManifest(importMetaUrl).version ?? "0.0.0";
28
29
  }
30
+
31
+ export function getCliPackageName(importMetaUrl: string): string {
32
+ return getCliPackageManifest(importMetaUrl).name ?? "@spencer-kit/coder-studio";
33
+ }
package/src/parse-args.ts CHANGED
@@ -11,7 +11,7 @@ type CliCommand =
11
11
  type AuthCommand = "ban-list" | "unblock";
12
12
 
13
13
  export const RUNTIME_CONFIG_ERROR =
14
- "Host, port, data-dir, password, and auth settings must be configured via the config command";
14
+ "Host, port, state-dir, password, and auth settings must be configured via the config command";
15
15
 
16
16
  export interface CliArgs {
17
17
  foreground?: boolean;
@@ -23,7 +23,7 @@ export interface CliArgs {
23
23
  configHelp?: boolean;
24
24
  port?: number;
25
25
  host?: string;
26
- dataDir?: string;
26
+ stateDir?: string;
27
27
  password?: string;
28
28
  noAuth?: boolean;
29
29
  ip?: string;
@@ -37,7 +37,7 @@ function clearConfigArgs(args: CliArgs): void {
37
37
  delete args.configHelp;
38
38
  delete args.port;
39
39
  delete args.host;
40
- delete args.dataDir;
40
+ delete args.stateDir;
41
41
  delete args.password;
42
42
  delete args.noAuth;
43
43
  }
@@ -217,10 +217,11 @@ export function parseArgs(argv: string[]): CliArgs {
217
217
  i += 1;
218
218
  break;
219
219
 
220
+ case "--state-dir":
220
221
  case "--data-dir":
221
222
  case "-d":
222
223
  ensureConfigContext(args, arg);
223
- args.dataDir = readOptionValue(argv, i + 1, "data-dir");
224
+ args.stateDir = readOptionValue(argv, i + 1, "state-dir");
224
225
  i += 1;
225
226
  break;
226
227
 
@@ -1,6 +1,10 @@
1
+ import { existsSync, mkdtempSync, rmSync } from "fs";
2
+ import { tmpdir } from "os";
3
+ import { join } from "path";
1
4
  import { fileURLToPath } from "url";
2
5
  import { afterEach, describe, expect, it, vi } from "vitest";
3
6
  import { getCliVersion } from "./package-manifest.js";
7
+ import { getUpdateRuntimeInfo } from "./update-runtime.js";
4
8
 
5
9
  const { createServer, parseServerConfig, readCliConfig, hasWebAssets, getStaticAssetsDir } =
6
10
  vi.hoisted(() => ({
@@ -45,6 +49,7 @@ describe("server-runner", () => {
45
49
 
46
50
  expect(buildServerConfig()).toMatchObject({
47
51
  appVersion: getCliVersion(import.meta.url),
52
+ update: getUpdateRuntimeInfo(import.meta.url),
48
53
  webRoot: "/tmp/web",
49
54
  });
50
55
  });
@@ -53,7 +58,7 @@ describe("server-runner", () => {
53
58
  readCliConfig.mockReturnValue({
54
59
  host: "127.0.0.1",
55
60
  port: 0,
56
- dataDir: "/tmp/cs-data/coder-studio.db",
61
+ stateDir: "/tmp/cs-data",
57
62
  password: "sekrit",
58
63
  });
59
64
  hasWebAssets.mockReturnValue(true);
@@ -61,8 +66,9 @@ describe("server-runner", () => {
61
66
 
62
67
  expect(buildServerConfig()).toEqual({
63
68
  appVersion: getCliVersion(import.meta.url),
69
+ update: getUpdateRuntimeInfo(import.meta.url),
64
70
  host: "127.0.0.1",
65
- dataDir: "/tmp/cs-data/coder-studio.db",
71
+ stateDir: "/tmp/cs-data",
66
72
  auth: {
67
73
  enabled: true,
68
74
  password: "sekrit",
@@ -89,6 +95,7 @@ describe("server-runner", () => {
89
95
 
90
96
  expect(createServer).toHaveBeenCalledWith({
91
97
  appVersion: getCliVersion(import.meta.url),
98
+ update: getUpdateRuntimeInfo(import.meta.url),
92
99
  host: "127.0.0.1",
93
100
  port: 4173,
94
101
  webRoot: "/tmp/web",
@@ -107,23 +114,47 @@ describe("server-runner", () => {
107
114
 
108
115
  it("prepares local state storage using the resolved server config", () => {
109
116
  readCliConfig.mockReturnValue({
110
- dataDir: "/tmp/cs-data/coder-studio.db",
117
+ stateDir: "/tmp/cs-data",
111
118
  });
112
119
  hasWebAssets.mockReturnValue(true);
113
120
  getStaticAssetsDir.mockReturnValue("/tmp/web");
114
121
  parseServerConfig.mockReturnValue({
115
- dataDir: "/tmp/cs-data/coder-studio.db",
122
+ stateDir: "/tmp/cs-data",
116
123
  });
117
124
 
118
125
  prepareLocalStateStorage();
119
126
 
120
127
  expect(parseServerConfig).toHaveBeenCalledWith({
121
128
  appVersion: getCliVersion(import.meta.url),
122
- dataDir: "/tmp/cs-data/coder-studio.db",
129
+ update: getUpdateRuntimeInfo(import.meta.url),
130
+ stateDir: "/tmp/cs-data",
123
131
  webRoot: "/tmp/web",
124
132
  });
125
133
  });
126
134
 
135
+ it("does not create on-disk state for an in-memory stateDir", () => {
136
+ readCliConfig.mockReturnValue({
137
+ stateDir: ":memory:",
138
+ });
139
+ hasWebAssets.mockReturnValue(true);
140
+ getStaticAssetsDir.mockReturnValue("/tmp/web");
141
+ parseServerConfig.mockReturnValue({
142
+ stateDir: ":memory:",
143
+ });
144
+ const originalCwd = process.cwd();
145
+ const testCwd = mkdtempSync(join(tmpdir(), "cs-server-runner-memory-"));
146
+
147
+ try {
148
+ process.chdir(testCwd);
149
+ prepareLocalStateStorage();
150
+
151
+ expect(existsSync(join(testCwd, ":memory:"))).toBe(false);
152
+ } finally {
153
+ process.chdir(originalCwd);
154
+ rmSync(testCwd, { recursive: true, force: true });
155
+ }
156
+ });
157
+
127
158
  it("starts the server when executed as the entrypoint", async () => {
128
159
  readCliConfig.mockReturnValue(null);
129
160
  hasWebAssets.mockReturnValue(true);
@@ -1,22 +1,23 @@
1
- import type { Server, ServerConfig } from "@coder-studio/server";
1
+ import type { Server, ServerConfigInput } from "@coder-studio/server";
2
2
  import { parseServerConfig } from "@coder-studio/server";
3
3
  import { mkdirSync } from "fs";
4
- import { dirname } from "path";
5
4
  import { fileURLToPath } from "url";
6
5
  import { readCliConfig } from "./config-store.js";
7
6
  import { getStaticAssetsDir, hasWebAssets } from "./embed.js";
8
7
  import { assertSupportedNodeVersion } from "./node-version.js";
9
8
  import { getCliVersion } from "./package-manifest.js";
9
+ import { getUpdateRuntimeInfo } from "./update-runtime.js";
10
10
 
11
11
  const MISSING_WEB_ASSETS_WARNING = "Warning: Web assets not found. Frontend will not be available.";
12
12
 
13
- export const buildServerConfig = (): Partial<ServerConfig> => {
13
+ export const buildServerConfig = (): ServerConfigInput => {
14
14
  const savedConfig = readCliConfig();
15
- const config: Partial<ServerConfig> = {
15
+ const config: ServerConfigInput = {
16
16
  appVersion: getCliVersion(import.meta.url),
17
+ update: getUpdateRuntimeInfo(import.meta.url),
17
18
  ...(savedConfig?.host !== undefined ? { host: savedConfig.host } : {}),
18
19
  ...(savedConfig?.port !== undefined && savedConfig.port > 0 ? { port: savedConfig.port } : {}),
19
- ...(savedConfig?.dataDir !== undefined ? { dataDir: savedConfig.dataDir } : {}),
20
+ ...(savedConfig?.stateDir !== undefined ? { stateDir: savedConfig.stateDir } : {}),
20
21
  ...(savedConfig?.password !== undefined
21
22
  ? {
22
23
  auth: {
@@ -40,8 +41,8 @@ export const buildServerConfig = (): Partial<ServerConfig> => {
40
41
 
41
42
  export const prepareLocalStateStorage = (): void => {
42
43
  const config = parseServerConfig(buildServerConfig());
43
- if (config.dataDir !== ":memory:") {
44
- mkdirSync(dirname(config.dataDir), { recursive: true });
44
+ if (config.stateDir !== ":memory:") {
45
+ mkdirSync(config.stateDir, { recursive: true });
45
46
  }
46
47
  };
47
48
 
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getUpdateRuntimeInfo } from "./update-runtime.js";
3
+
4
+ describe("update-runtime", () => {
5
+ it("exposes the package name and restart command contract", () => {
6
+ const runtime = getUpdateRuntimeInfo(import.meta.url);
7
+
8
+ expect(runtime.packageName).toBe("@spencer-kit/coder-studio");
9
+ expect(runtime.cliCommand).toBe("coder-studio");
10
+ expect(runtime.restartArgs).toEqual(["serve", "--restart"]);
11
+ expect(runtime.installArgsPrefix).toEqual(["install", "-g"]);
12
+ });
13
+ });