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 +7 -0
- package/package.json +23 -0
- package/src/cli.ts +313 -0
- package/tsconfig.json +18 -0
package/CHANGELOG.md
ADDED
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
|
+
}
|