spacetime-studio 0.0.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # spacetime-studio
2
+
3
+ ## 0.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`a6acc7d`](https://github.com/Blankeos/spacetimedb-studio/commit/a6acc7deb92dc148eaaf79294892ee1cf3b6dcf5) Thanks [@Blankeos](https://github.com/Blankeos)! - feat: first release!
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "spacetime-studio",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "bin": {
6
+ "spacetime-studio": "./dist/spacetime-studio"
7
+ },
8
+ "scripts": {
9
+ "dev": "bun run src/cli.ts",
10
+ "build:cli": "bun build ./src/cli.ts --compile --outfile ./dist/spacetime-studio --minify",
11
+ "build:copy-studio": "cp -r ../../apps/studio/dist ./dist/studio/dist",
12
+ "build": "bun run build:cli && bun run build:copy-studio",
13
+ "check": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "chalk": "^5.4.1",
17
+ "commander": "^12.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/bun": "^1.3.10",
21
+ "typescript": "^5.9.3"
22
+ }
23
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env bun
2
+ import process from "node:process"
3
+ import chalk from "chalk"
4
+ import { spawn } from "child_process"
5
+ import { program } from "commander"
6
+ import { existsSync } from "fs"
7
+ import { join, resolve } from "path"
8
+
9
+ const DEFAULT_PORT = 5555
10
+ const DEFAULT_HOST = "localhost"
11
+
12
+ function findStudioPath(): string | null {
13
+ // 1. Check environment variable override
14
+ if (process.env.SPACETIME_STUDIO_PATH) {
15
+ if (existsSync(process.env.SPACETIME_STUDIO_PATH)) {
16
+ return process.env.SPACETIME_STUDIO_PATH
17
+ }
18
+ }
19
+
20
+ // 2. Use import.meta.dir which works in both dev and compiled mode
21
+ const baseDir = import.meta.dir
22
+
23
+ // Check if we're in compiled mode - bun compile puts us in /$bunfs/root
24
+ const isCompiled = baseDir === "/$bunfs/root" || baseDir === "/$bunfs"
25
+
26
+ // For compiled binaries: prioritize paths relative to cwd
27
+ if (isCompiled) {
28
+ // Check packages/cli/dist/studio (for running from packages/cli)
29
+ const cliDistPath = join(process.cwd(), "dist/studio")
30
+ if (existsSync(join(cliDistPath, "dist/server/index.mjs"))) {
31
+ // Check if apps/studio exists (for node_modules)
32
+ const appsStudioPath = join(process.cwd(), "../../apps/studio")
33
+ if (existsSync(join(appsStudioPath, "dist/server/index.mjs"))) {
34
+ return appsStudioPath // Prefer original location for node_modules access
35
+ }
36
+ return cliDistPath
37
+ }
38
+
39
+ // Check current working directory for studio folder
40
+ const cwdStudioPath = join(process.cwd(), "studio")
41
+ if (existsSync(join(cwdStudioPath, "dist/server/index.mjs"))) {
42
+ return cwdStudioPath
43
+ }
44
+
45
+ // Check apps/studio from cwd (for monorepo root)
46
+ const cwdAppsPath = join(process.cwd(), "apps/studio")
47
+ if (existsSync(join(cwdAppsPath, "dist/server/index.mjs"))) {
48
+ return cwdAppsPath
49
+ }
50
+
51
+ return null
52
+ }
53
+
54
+ // Development mode: apps/studio relative to packages/cli/src
55
+ const devPath = resolve(baseDir, "../../../apps/studio")
56
+ const devServerPath = join(devPath, "dist/server/index.mjs")
57
+ if (existsSync(devServerPath)) {
58
+ return devPath
59
+ }
60
+
61
+ // Fallback: check cwd-based paths
62
+ const cwdStudioPath = join(process.cwd(), "studio")
63
+ if (existsSync(join(cwdStudioPath, "dist/server/index.mjs"))) {
64
+ return cwdStudioPath
65
+ }
66
+
67
+ const cwdAppsPath = join(process.cwd(), "apps/studio")
68
+ if (existsSync(join(cwdAppsPath, "dist/server/index.mjs"))) {
69
+ return cwdAppsPath
70
+ }
71
+
72
+ return null
73
+ }
74
+
75
+ function debugPaths(): void {
76
+ const baseDir = import.meta.dir
77
+ const isCompiled = baseDir === "/$bunfs/root" || baseDir === "/$bunfs"
78
+
79
+ console.log("Debug: Path resolution")
80
+ console.log(chalk.gray(` import.meta.dir: ${baseDir}`))
81
+ console.log(chalk.gray(` isCompiled: ${isCompiled}`))
82
+ console.log(chalk.gray(` cwd: ${process.cwd()}`))
83
+
84
+ if (isCompiled) {
85
+ const cliDistPath = join(process.cwd(), "dist/studio")
86
+ const appsStudioPath = join(process.cwd(), "../../apps/studio")
87
+
88
+ console.log(chalk.gray(` cliDistPath: ${cliDistPath}`))
89
+ console.log(
90
+ chalk.gray(
91
+ ` cliDistPath server exists: ${existsSync(join(cliDistPath, "dist/server/index.mjs"))}`
92
+ )
93
+ )
94
+ console.log(chalk.gray(` appsStudioPath: ${appsStudioPath}`))
95
+ console.log(
96
+ chalk.gray(
97
+ ` appsStudioPath server exists: ${existsSync(join(appsStudioPath, "dist/server/index.mjs"))}`
98
+ )
99
+ )
100
+ } else {
101
+ const devPath = resolve(baseDir, "../../../apps/studio")
102
+ console.log(chalk.gray(` devPath: ${devPath}`))
103
+ console.log(
104
+ chalk.gray(` devServerPath exists: ${existsSync(join(devPath, "dist/server/index.mjs"))}`)
105
+ )
106
+ }
107
+ }
108
+
109
+ program
110
+ .name("spacetime-studio")
111
+ .description("A local web-based database studio for SpacetimeDB databases")
112
+ .version("0.0.1")
113
+ .argument("[database]", "Database name to connect to")
114
+ .option("-d, --db <database>", "Database name to connect to")
115
+ .option("-p, --port <port>", "Port to run studio on", String(DEFAULT_PORT))
116
+ .option("-h, --host <host>", "Host to bind to", DEFAULT_HOST)
117
+ .option("--dev", "Run in development mode with hot reload")
118
+ .option("--debug", "Show debug information about paths")
119
+ .action(
120
+ async (
121
+ database: string | undefined,
122
+ options: { db?: string; port: string; host: string; dev?: boolean; debug?: boolean }
123
+ ) => {
124
+ const dbName = database || options.db
125
+
126
+ if (options.debug) {
127
+ debugPaths()
128
+ process.exit(0)
129
+ }
130
+
131
+ if (!dbName) {
132
+ console.error(chalk.red("Error: Database name is required"))
133
+ console.log(chalk.gray("Usage: spacetime-studio <database>"))
134
+ console.log(chalk.gray(" spacetime-studio --db <database>"))
135
+ process.exit(1)
136
+ }
137
+
138
+ const port = parseInt(options.port, 10)
139
+ const host = options.host
140
+ const devMode = options.dev
141
+
142
+ console.log(chalk.cyan(`\n SpacetimeDB Studio\n`))
143
+ console.log(chalk.gray(` Database: ${chalk.white(dbName)}`))
144
+ console.log(chalk.gray(` Host: ${host}`))
145
+ console.log(chalk.gray(` Port: ${port}`))
146
+
147
+ // Validate database exists
148
+ console.log(chalk.dim("\n Validating database..."))
149
+ const validation = await validateDatabase(dbName)
150
+
151
+ if (!validation.valid) {
152
+ console.error(chalk.red(`\n Error: ${validation.error}`))
153
+ console.log(chalk.gray("\n Make sure:"))
154
+ console.log(chalk.gray(" 1. SpacetimeDB is running: spacetime start"))
155
+ console.log(chalk.cyan(` 2. Database exists: spacetime publish ${dbName}`))
156
+ process.exit(1)
157
+ }
158
+
159
+ console.log(chalk.green(` ✓ Database "${dbName}" found`))
160
+
161
+ if (validation.tables !== undefined) {
162
+ console.log(
163
+ chalk.dim(` ${validation.tables} table(s), ${validation.reducers} reducer(s)`)
164
+ )
165
+ }
166
+
167
+ // Set environment for the studio app
168
+ process.env.SPACETIME_DB = dbName
169
+ process.env.PORT = String(port)
170
+ process.env.HOST = host
171
+
172
+ // Find studio path
173
+ const studioPath = findStudioPath()
174
+
175
+ if (!studioPath) {
176
+ console.error(chalk.red("\n Error: Studio app not found."))
177
+ console.log(chalk.gray("\n The studio assets are not bundled."))
178
+ console.log(chalk.gray(" For development, run from the project root:"))
179
+ console.log(chalk.cyan(" cd packages/cli && bun run src/cli.ts <database>"))
180
+ console.log(chalk.gray("\n For production, build first:"))
181
+ console.log(chalk.cyan(" cd packages/cli && bun run build"))
182
+ process.exit(1)
183
+ }
184
+
185
+ const studioUrl = `http://${host}:${port}`
186
+ console.log(chalk.green(`\n Studio running at: ${chalk.bold(studioUrl)}`))
187
+ if (devMode) {
188
+ console.log(chalk.dim(` Mode: development (hot reload enabled)`))
189
+ }
190
+ console.log(chalk.gray(` Press Ctrl+C to stop\n`))
191
+
192
+ // Run the studio - always use bun to run the server script
193
+ const serverPath = join(studioPath, "dist/server/index.mjs")
194
+
195
+ if (!existsSync(serverPath)) {
196
+ console.error(chalk.red("\n Error: Studio not built."))
197
+ console.log(chalk.gray(" Run: cd apps/studio && bun run build"))
198
+ process.exit(1)
199
+ }
200
+
201
+ if (devMode) {
202
+ // Development: spawn vike dev server
203
+ const serverProcess = spawn("bun", ["run", "dev"], {
204
+ cwd: studioPath,
205
+ env: {
206
+ ...process.env,
207
+ SPACETIME_DB: dbName,
208
+ PORT: String(port),
209
+ },
210
+ stdio: "inherit",
211
+ shell: true,
212
+ })
213
+
214
+ serverProcess.on("error", (err: Error) => {
215
+ console.error(chalk.red(`Server error: ${err.message}`))
216
+ process.exit(1)
217
+ })
218
+
219
+ process.on("SIGINT", () => {
220
+ console.log(chalk.yellow("\n Shutting down..."))
221
+ serverProcess.kill()
222
+ process.exit(0)
223
+ })
224
+ } else {
225
+ // Production: run the built server from studio path
226
+ // Note: studioPath should point to apps/studio which has node_modules
227
+ const serverProcess = spawn("bun", ["run", serverPath], {
228
+ cwd: studioPath,
229
+ env: {
230
+ ...process.env,
231
+ SPACETIME_DB: dbName,
232
+ PORT: String(port),
233
+ NODE_ENV: "production",
234
+ },
235
+ stdio: "inherit",
236
+ })
237
+
238
+ serverProcess.on("error", (err: Error) => {
239
+ console.error(chalk.red(`\n Server error: ${err.message}`))
240
+ process.exit(1)
241
+ })
242
+
243
+ process.on("SIGINT", () => {
244
+ console.log(chalk.yellow("\n Shutting down..."))
245
+ serverProcess.kill()
246
+ process.exit(0)
247
+ })
248
+ }
249
+ }
250
+ )
251
+
252
+ async function validateDatabase(dbName: string): Promise<{
253
+ valid: boolean
254
+ error?: string
255
+ tables?: number
256
+ reducers?: number
257
+ }> {
258
+ return new Promise((resolve) => {
259
+ const child = spawn("spacetime", ["describe", dbName, "--json"], {
260
+ stdio: ["ignore", "pipe", "pipe"],
261
+ })
262
+
263
+ let stdout = ""
264
+ let stderr = ""
265
+
266
+ child.stdout.on("data", (data) => {
267
+ stdout += data.toString()
268
+ })
269
+
270
+ child.stderr.on("data", (data) => {
271
+ stderr += data.toString()
272
+ })
273
+
274
+ child.on("close", (code) => {
275
+ if (code !== 0) {
276
+ const errorMsg = stderr.trim() || `Database "${dbName}" not found`
277
+ resolve({
278
+ valid: false,
279
+ error:
280
+ errorMsg.includes("not find") ||
281
+ errorMsg.includes("failed to find") ||
282
+ errorMsg.includes("Connection refused")
283
+ ? `"${dbName}" - Check if SpacetimeDB is running (spacetime start) and database exists (spacetime publish ${dbName})`
284
+ : errorMsg,
285
+ })
286
+ return
287
+ }
288
+
289
+ try {
290
+ const schema = JSON.parse(stdout) as {
291
+ tables?: unknown[]
292
+ reducers?: unknown[]
293
+ }
294
+ resolve({
295
+ valid: true,
296
+ tables: schema.tables?.length ?? 0,
297
+ reducers: schema.reducers?.length ?? 0,
298
+ })
299
+ } catch {
300
+ resolve({ valid: true })
301
+ }
302
+ })
303
+
304
+ child.on("error", (err) => {
305
+ resolve({
306
+ valid: false,
307
+ error: `Failed to run spacetime CLI: ${err.message}. Is SpacetimeDB installed?`,
308
+ })
309
+ })
310
+ })
311
+ }
312
+
313
+ program.parse()
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "allowJs": true,
5
+ "esModuleInterop": true,
6
+ "forceConsistentCasingInFileNames": true,
7
+ "resolveJsonModule": true,
8
+ "skipLibCheck": true,
9
+ "module": "ESNext",
10
+ "noEmit": true,
11
+ "moduleResolution": "bundler",
12
+ "target": "ES2022",
13
+ "lib": ["ESNext"],
14
+ "types": ["@types/bun"]
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }