sql-kite 1.0.6 → 1.0.8

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.
Files changed (89) hide show
  1. package/README.md +10 -2
  2. package/package.json +12 -4
  3. package/server/db/connections.js +114 -0
  4. package/server/db/meta-db.js +0 -0
  5. package/server/db/user-db.js +0 -0
  6. package/server/index.js +214 -0
  7. package/server/routes/branches.js +484 -0
  8. package/server/routes/compare.js +109 -0
  9. package/server/routes/export.js +201 -0
  10. package/server/routes/import.js +375 -0
  11. package/server/routes/migrations.js +332 -0
  12. package/server/routes/query.js +67 -0
  13. package/server/routes/schema.js +206 -0
  14. package/server/routes/snapshots.js +322 -0
  15. package/server/routes/tables.js +121 -0
  16. package/server/routes/timeline.js +108 -0
  17. package/server/server.js +0 -0
  18. package/src/commands/import-server.js +2 -5
  19. package/src/commands/import.js +5 -9
  20. package/src/commands/start.js +6 -7
  21. package/src/commands/stop.js +7 -2
  22. package/src/utils/paths.js +61 -1
  23. package/studio-out/404/index.html +1 -0
  24. package/studio-out/404.html +1 -0
  25. package/studio-out/__next.__PAGE__.txt +10 -0
  26. package/studio-out/__next._full.txt +20 -0
  27. package/studio-out/__next._head.txt +6 -0
  28. package/studio-out/__next._index.txt +6 -0
  29. package/studio-out/__next._tree.txt +3 -0
  30. package/studio-out/_next/static/LhecVBdPttfi1VZfXA-dL/_buildManifest.js +11 -0
  31. package/studio-out/_next/static/LhecVBdPttfi1VZfXA-dL/_clientMiddlewareManifest.json +1 -0
  32. package/studio-out/_next/static/LhecVBdPttfi1VZfXA-dL/_ssgManifest.js +1 -0
  33. package/studio-out/_next/static/chunks/118fc599da2f27aa.css +2 -0
  34. package/studio-out/_next/static/chunks/240f2fa81d4fb687.js +1 -0
  35. package/studio-out/_next/static/chunks/42c33ca704af9b68.js +1 -0
  36. package/studio-out/_next/static/chunks/99b69e65b599be96.js +5 -0
  37. package/studio-out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
  38. package/studio-out/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
  39. package/studio-out/_next/static/chunks/b20313408e970968.css +1 -0
  40. package/studio-out/_next/static/chunks/d104f42a7b0c57b2.js +2 -0
  41. package/studio-out/_next/static/chunks/d4aa9be9c80c98d6.js +1 -0
  42. package/studio-out/_next/static/chunks/f2f58a7e93290fbb.js +1 -0
  43. package/studio-out/_next/static/chunks/f547e106c8e2aa8e.js +1 -0
  44. package/studio-out/_next/static/chunks/f5cb054219e2eeb8.js +109 -0
  45. package/studio-out/_next/static/chunks/turbopack-1577480078e795df.js +4 -0
  46. package/studio-out/_not-found/__next._full.txt +15 -0
  47. package/studio-out/_not-found/__next._head.txt +6 -0
  48. package/studio-out/_not-found/__next._index.txt +6 -0
  49. package/studio-out/_not-found/__next._not-found/__PAGE__.txt +5 -0
  50. package/studio-out/_not-found/__next._not-found.txt +4 -0
  51. package/studio-out/_not-found/__next._tree.txt +2 -0
  52. package/studio-out/_not-found/index.html +1 -0
  53. package/studio-out/_not-found/index.txt +15 -0
  54. package/studio-out/favicon.ico +10 -0
  55. package/studio-out/index.html +37 -0
  56. package/studio-out/index.txt +20 -0
  57. package/studio-out/logo.svg +5 -0
  58. package/studio-out/snapshots/__next._full.txt +15 -0
  59. package/studio-out/snapshots/__next._head.txt +6 -0
  60. package/studio-out/snapshots/__next._index.txt +6 -0
  61. package/studio-out/snapshots/__next._tree.txt +2 -0
  62. package/studio-out/snapshots/__next.snapshots/__PAGE__.txt +5 -0
  63. package/studio-out/snapshots/__next.snapshots.txt +4 -0
  64. package/studio-out/snapshots/index.html +1 -0
  65. package/studio-out/snapshots/index.txt +15 -0
  66. package/studio-out/sql/__next._full.txt +15 -0
  67. package/studio-out/sql/__next._head.txt +6 -0
  68. package/studio-out/sql/__next._index.txt +6 -0
  69. package/studio-out/sql/__next._tree.txt +2 -0
  70. package/studio-out/sql/__next.sql/__PAGE__.txt +5 -0
  71. package/studio-out/sql/__next.sql.txt +4 -0
  72. package/studio-out/sql/index.html +1 -0
  73. package/studio-out/sql/index.txt +15 -0
  74. package/studio-out/tables/__next._full.txt +15 -0
  75. package/studio-out/tables/__next._head.txt +6 -0
  76. package/studio-out/tables/__next._index.txt +6 -0
  77. package/studio-out/tables/__next._tree.txt +2 -0
  78. package/studio-out/tables/__next.tables/__PAGE__.txt +5 -0
  79. package/studio-out/tables/__next.tables.txt +4 -0
  80. package/studio-out/tables/index.html +1 -0
  81. package/studio-out/tables/index.txt +15 -0
  82. package/studio-out/timeline/__next._full.txt +15 -0
  83. package/studio-out/timeline/__next._head.txt +6 -0
  84. package/studio-out/timeline/__next._index.txt +6 -0
  85. package/studio-out/timeline/__next._tree.txt +2 -0
  86. package/studio-out/timeline/__next.timeline/__PAGE__.txt +5 -0
  87. package/studio-out/timeline/__next.timeline.txt +4 -0
  88. package/studio-out/timeline/index.html +1 -0
  89. package/studio-out/timeline/index.txt +15 -0
@@ -0,0 +1,201 @@
1
+ import { join } from 'path';
2
+ import { existsSync, copyFileSync, unlinkSync, statSync, readFileSync, writeFileSync } from 'fs';
3
+ import Database from 'better-sqlite3';
4
+ import { getCurrentBranch } from '../db/connections.js';
5
+
6
+ /**
7
+ * Export Routes
8
+ *
9
+ * Handles exporting production database from main branch
10
+ */
11
+
12
+ export default async function exportRoutes(fastify, options) {
13
+
14
+ /**
15
+ * Get export status and pre-flight checks
16
+ */
17
+ fastify.get('/status', async (request, reply) => {
18
+ try {
19
+ const metaDb = fastify.getMetaDb();
20
+ const projectPath = fastify.projectPath;
21
+
22
+ // Check if main branch exists
23
+ const mainBranch = metaDb.prepare(`
24
+ SELECT name, db_file, created_at FROM branches WHERE name = 'main'
25
+ `).get();
26
+
27
+ const mainExists = !!mainBranch;
28
+ let databaseHealthy = false;
29
+ let tableCount = 0;
30
+ let totalRows = 0;
31
+ let lastModified = null;
32
+ let pendingMigrations = 0;
33
+
34
+ if (mainExists) {
35
+ // Resolve database path consistently with the rest of the codebase
36
+ const mainDbPath = join(projectPath, mainBranch.db_file);
37
+
38
+ if (existsSync(mainDbPath)) {
39
+ try {
40
+ // Open main database to check health (not readonly to handle WAL mode properly)
41
+ const mainDb = new Database(mainDbPath);
42
+
43
+ // Check integrity - PRAGMA returns object with integrity_check property
44
+ const integrityResult = mainDb.prepare('PRAGMA integrity_check').get();
45
+ // Access the property directly - it returns { integrity_check: 'ok' } when healthy
46
+ const integrityValue = integrityResult?.integrity_check;
47
+ databaseHealthy = integrityValue === 'ok';
48
+
49
+ // Get table count
50
+ const tables = mainDb.prepare(`
51
+ SELECT name FROM sqlite_master
52
+ WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
53
+ `).all();
54
+ tableCount = tables.length;
55
+
56
+ // Get total row count across tables
57
+ for (const table of tables) {
58
+ try {
59
+ const count = mainDb.prepare(`SELECT COUNT(*) as cnt FROM "${table.name}"`).get();
60
+ totalRows += count?.cnt || 0;
61
+ } catch (e) {
62
+ // Skip tables that can't be counted
63
+ }
64
+ }
65
+
66
+ mainDb.close();
67
+
68
+ // Get file modification time
69
+ const stat = statSync(mainDbPath);
70
+ lastModified = stat.mtime.toISOString();
71
+ } catch (error) {
72
+ console.error('Error checking main database health:', error);
73
+ databaseHealthy = false;
74
+ }
75
+ }
76
+
77
+ // Check for pending migrations
78
+ try {
79
+ const pendingMigrationCount = metaDb.prepare(`
80
+ SELECT COUNT(*) as cnt FROM migrations
81
+ WHERE status = 'pending' AND branch = 'main'
82
+ `).get();
83
+ pendingMigrations = pendingMigrationCount?.cnt || 0;
84
+ } catch (e) {
85
+ // Migrations table might not exist
86
+ pendingMigrations = 0;
87
+ }
88
+ }
89
+
90
+ return {
91
+ mainExists,
92
+ databaseHealthy,
93
+ tableCount,
94
+ totalRows,
95
+ lastModified,
96
+ pendingMigrations
97
+ };
98
+ } catch (error) {
99
+ console.error('Export status error:', error);
100
+ return reply.code(500).send({
101
+ error: error.message
102
+ });
103
+ }
104
+ });
105
+
106
+ /**
107
+ * Export the main branch database
108
+ *
109
+ * This creates a clean SQLite file:
110
+ * - WAL checkpoint performed
111
+ * - No WAL or SHM files
112
+ * - No branch metadata
113
+ * - No timeline
114
+ * - Just pure SQLite data
115
+ */
116
+ fastify.post('/database', async (request, reply) => {
117
+ const { fileName = 'production' } = request.body || {};
118
+ const metaDb = fastify.getMetaDb();
119
+ const projectPath = fastify.projectPath;
120
+
121
+ try {
122
+ // Get main branch info
123
+ const mainBranch = metaDb.prepare(`
124
+ SELECT name, db_file FROM branches WHERE name = 'main'
125
+ `).get();
126
+
127
+ if (!mainBranch) {
128
+ return reply.code(400).send({
129
+ error: 'Main branch does not exist. Cannot export.'
130
+ });
131
+ }
132
+
133
+ // Resolve database path consistently
134
+ const mainDbPath = join(projectPath, mainBranch.db_file);
135
+
136
+ if (!existsSync(mainDbPath)) {
137
+ return reply.code(400).send({
138
+ error: 'Main database file not found.'
139
+ });
140
+ }
141
+
142
+ // Create a temporary export path
143
+ const exportPath = join(projectPath, '.studio', `export_${Date.now()}.db`);
144
+
145
+ try {
146
+ // Open the main database
147
+ const sourceDb = new Database(mainDbPath);
148
+
149
+ // Perform WAL checkpoint to ensure all data is in main file
150
+ sourceDb.pragma('wal_checkpoint(TRUNCATE)');
151
+
152
+ // Use backup API for clean copy
153
+ await sourceDb.backup(exportPath);
154
+
155
+ sourceDb.close();
156
+
157
+ // Open the exported database and clean it up
158
+ const exportDb = new Database(exportPath);
159
+
160
+ // Vacuum to optimize
161
+ exportDb.exec('VACUUM');
162
+
163
+ // Ensure no WAL mode on export (use DELETE journal for compatibility)
164
+ exportDb.pragma('journal_mode = DELETE');
165
+
166
+ exportDb.close();
167
+
168
+ // Read the file and send as download
169
+ const fileBuffer = readFileSync(exportPath);
170
+
171
+ // Clean up temp file
172
+ try {
173
+ unlinkSync(exportPath);
174
+ } catch (e) {
175
+ console.error('Failed to clean up temp export file:', e);
176
+ }
177
+
178
+ // Send as binary download
179
+ reply
180
+ .header('Content-Type', 'application/x-sqlite3')
181
+ .header('Content-Disposition', `attachment; filename="${fileName}.db"`)
182
+ .header('Content-Length', fileBuffer.length)
183
+ .send(fileBuffer);
184
+
185
+ } catch (error) {
186
+ // Clean up temp file on error
187
+ try {
188
+ if (existsSync(exportPath)) {
189
+ unlinkSync(exportPath);
190
+ }
191
+ } catch (e) {}
192
+
193
+ throw error;
194
+ }
195
+
196
+ } catch (error) {
197
+ console.error('Export error:', error);
198
+ reply.code(500).send({ error: error.message });
199
+ }
200
+ });
201
+ }
@@ -0,0 +1,375 @@
1
+ import { existsSync, mkdirSync, copyFileSync, readFileSync, unlinkSync, writeFileSync, statSync } from 'fs'
2
+ import { join, dirname } from 'path'
3
+ import { spawn } from 'child_process'
4
+ import { fileURLToPath, pathToFileURL } from 'url'
5
+ import { homedir } from 'os'
6
+ import Database from 'better-sqlite3'
7
+
8
+ const __routes_dirname = dirname(fileURLToPath(import.meta.url))
9
+
10
+ // Resolve CLI utils - works in both layouts:
11
+ // monorepo: packages/server/src/routes/ -> ../../../cli/src/utils/
12
+ // npm: sql-kite/server/routes/ -> ../../src/utils/
13
+ const cliUtilsCandidates = [
14
+ join(__routes_dirname, '..', '..', 'src', 'utils'), // npm layout
15
+ join(__routes_dirname, '..', '..', '..', 'cli', 'src', 'utils') // monorepo layout
16
+ ]
17
+ let cliUtilsDir
18
+ for (const candidate of cliUtilsCandidates) {
19
+ if (existsSync(join(candidate, 'paths.js'))) {
20
+ cliUtilsDir = candidate
21
+ break
22
+ }
23
+ }
24
+ if (!cliUtilsDir) {
25
+ throw new Error('Could not find CLI utils directory. Tried: ' + cliUtilsCandidates.join(', '))
26
+ }
27
+ const { findFreePort } = await import(pathToFileURL(join(cliUtilsDir, 'port-finder.js')).href)
28
+ const { migrateMetaDb } = await import(pathToFileURL(join(cliUtilsDir, 'meta-migration.js')).href)
29
+ const { validateProjectName } = await import(pathToFileURL(join(cliUtilsDir, 'paths.js')).href)
30
+
31
+ export default async function importRoutes(fastify, options) {
32
+ const homeDir = homedir()
33
+ const projectsRoot = join(homeDir, '.sql-kite', 'runtime')
34
+ const sessionFile = join(homeDir, '.sql-kite', 'import-pending.json')
35
+
36
+ // Get pending import session
37
+ fastify.get('/pending', async (request, reply) => {
38
+ try {
39
+ if (!existsSync(sessionFile)) {
40
+ return { pending: false }
41
+ }
42
+
43
+ const session = JSON.parse(readFileSync(sessionFile, 'utf-8'))
44
+ return { pending: true, ...session }
45
+ } catch (error) {
46
+ fastify.log.error(error)
47
+ return { pending: false, error: error.message }
48
+ }
49
+ })
50
+
51
+ // Clear pending import session
52
+ fastify.delete('/pending', async (request, reply) => {
53
+ try {
54
+ if (existsSync(sessionFile)) {
55
+ unlinkSync(sessionFile)
56
+ }
57
+ return { success: true }
58
+ } catch (error) {
59
+ fastify.log.error(error)
60
+ return reply.code(500).send({ error: error.message })
61
+ }
62
+ })
63
+
64
+ // Create project structure
65
+ fastify.post('/create', async (request, reply) => {
66
+ const { projectName, sourcePath, importMode } = request.body
67
+
68
+ // Validate project name to prevent path traversal
69
+ const validation = validateProjectName(projectName)
70
+ if (!validation.valid) {
71
+ return reply.code(400).send({ error: validation.error })
72
+ }
73
+
74
+ const safeName = validation.sanitized
75
+
76
+ try {
77
+ const projectPath = join(projectsRoot, safeName)
78
+
79
+ // Check if project already exists
80
+ if (existsSync(projectPath)) {
81
+ return reply.code(400).send({ error: 'Project already exists' })
82
+ }
83
+
84
+ // Create project directories
85
+ mkdirSync(projectPath, { recursive: true })
86
+ mkdirSync(join(projectPath, 'migrations'), { recursive: true })
87
+ mkdirSync(join(projectPath, 'snapshots'), { recursive: true })
88
+ mkdirSync(join(projectPath, '.studio'), { recursive: true })
89
+ mkdirSync(join(projectPath, '.studio', 'snapshots'), { recursive: true })
90
+ mkdirSync(join(projectPath, '.studio', 'locks'), { recursive: true })
91
+
92
+ // Create config.json
93
+ const config = {
94
+ name: safeName,
95
+ created_at: new Date().toISOString(),
96
+ version: '1.0.0'
97
+ }
98
+ writeFileSync(
99
+ join(projectPath, 'config.json'),
100
+ JSON.stringify(config, null, 2)
101
+ )
102
+
103
+ // Initialize meta.db using the shared migration
104
+ const metaDbPath = join(projectPath, '.studio', 'meta.db')
105
+ migrateMetaDb(metaDbPath)
106
+
107
+ // Update main branch description and ensure current branch
108
+ const metaDb = new Database(metaDbPath)
109
+ metaDb.prepare(`
110
+ UPDATE branches
111
+ SET description = 'Imported database baseline'
112
+ WHERE name = 'main'
113
+ `).run()
114
+ metaDb.prepare(`
115
+ INSERT OR REPLACE INTO settings (key, value)
116
+ VALUES ('current_branch', 'main')
117
+ `).run()
118
+ metaDb.close()
119
+
120
+ return { success: true, projectPath }
121
+ } catch (error) {
122
+ fastify.log.error(error)
123
+ return reply.code(500).send({ error: error.message })
124
+ }
125
+ })
126
+
127
+ // Copy database with WAL checkpoint
128
+ fastify.post('/copy', async (request, reply) => {
129
+ const { projectName, sourcePath, importMode } = request.body
130
+
131
+ // Validate project name
132
+ const validation = validateProjectName(projectName)
133
+ if (!validation.valid) {
134
+ return reply.code(400).send({ error: validation.error })
135
+ }
136
+
137
+ try {
138
+ const projectPath = join(projectsRoot, validation.sanitized)
139
+ const targetPath = join(projectPath, 'db.sqlite')
140
+
141
+ if (importMode === 'copy') {
142
+ // Step 1: Open source DB and checkpoint WAL
143
+ let sourceDb
144
+ try {
145
+ sourceDb = new Database(sourcePath, { fileMustExist: true })
146
+
147
+ // Force WAL checkpoint to ensure all data is in main DB file
148
+ sourceDb.pragma('wal_checkpoint(FULL)')
149
+
150
+ sourceDb.close()
151
+ } catch (err) {
152
+ if (sourceDb) sourceDb.close()
153
+ throw new Error(`Failed to checkpoint source database: ${err.message}`)
154
+ }
155
+
156
+ // Step 2: Copy database files
157
+ try {
158
+ // Copy main DB file
159
+ copyFileSync(sourcePath, targetPath)
160
+
161
+ // Copy WAL file if exists
162
+ const walPath = sourcePath + '-wal'
163
+ if (existsSync(walPath)) {
164
+ copyFileSync(walPath, targetPath + '-wal')
165
+ }
166
+
167
+ // Copy SHM file if exists
168
+ const shmPath = sourcePath + '-shm'
169
+ if (existsSync(shmPath)) {
170
+ copyFileSync(shmPath, targetPath + '-shm')
171
+ }
172
+ } catch (err) {
173
+ throw new Error(`Failed to copy database files: ${err.message}`)
174
+ }
175
+
176
+ // Step 3: Verify copied database
177
+ let targetDb
178
+ try {
179
+ targetDb = new Database(targetPath)
180
+
181
+ // Test query to verify integrity
182
+ targetDb.prepare('SELECT COUNT(*) FROM sqlite_master').get()
183
+
184
+ // Enable WAL mode for the copied database
185
+ targetDb.pragma('journal_mode = WAL')
186
+
187
+ targetDb.close()
188
+ } catch (err) {
189
+ if (targetDb) targetDb.close()
190
+ throw new Error(`Failed to verify copied database: ${err.message}`)
191
+ }
192
+
193
+ } else if (importMode === 'link') {
194
+ if (!existsSync(sourcePath)) {
195
+ return reply.code(400).send({ error: 'Source database not found' })
196
+ }
197
+ }
198
+
199
+ // Update main branch db_file
200
+ const metaDbPath = join(projectPath, '.studio', 'meta.db')
201
+ const metaDb = new Database(metaDbPath)
202
+
203
+ const dbFile = importMode === 'link' ? sourcePath : 'db.sqlite'
204
+ metaDb.prepare(`
205
+ UPDATE branches
206
+ SET db_file = ?
207
+ WHERE name = 'main'
208
+ `).run(dbFile)
209
+
210
+ // Log event in meta.db
211
+ metaDb.prepare(`
212
+ INSERT INTO events (branch, type, data)
213
+ VALUES ('main', 'database_imported', ?)
214
+ `).run(JSON.stringify({ sourcePath, importMode, db_file: dbFile }))
215
+
216
+ metaDb.close()
217
+
218
+ return { success: true }
219
+ } catch (error) {
220
+ fastify.log.error(error)
221
+ return reply.code(500).send({ error: error.message })
222
+ }
223
+ })
224
+
225
+ // Create baseline snapshot
226
+ fastify.post('/snapshot', async (request, reply) => {
227
+ const { projectName } = request.body
228
+
229
+ // Validate project name
230
+ const validation = validateProjectName(projectName)
231
+ if (!validation.valid) {
232
+ return reply.code(400).send({ error: validation.error })
233
+ }
234
+
235
+ try {
236
+ const projectPath = join(projectsRoot, validation.sanitized)
237
+ const metaDbPath = join(projectPath, '.studio', 'meta.db')
238
+ const metaDb = new Database(metaDbPath)
239
+ const branchInfo = metaDb.prepare(`
240
+ SELECT db_file FROM branches WHERE name = 'main'
241
+ `).get()
242
+
243
+ if (!branchInfo) {
244
+ metaDb.close()
245
+ return reply.code(404).send({ error: 'Main branch not found' })
246
+ }
247
+
248
+ const sourceDbPath = join(projectPath, branchInfo.db_file)
249
+
250
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
251
+ const snapshotFilename = `imported-baseline-${timestamp}.db`
252
+ const snapshotDir = join(projectPath, '.studio', 'snapshots')
253
+
254
+ // Ensure snapshots directory exists
255
+ if (!existsSync(snapshotDir)) {
256
+ mkdirSync(snapshotDir, { recursive: true })
257
+ }
258
+
259
+ const snapshotPath = join(snapshotDir, snapshotFilename)
260
+
261
+ // Copy database to snapshot
262
+ let sourceDb
263
+ try {
264
+ sourceDb = new Database(sourceDbPath)
265
+ sourceDb.pragma('wal_checkpoint(FULL)')
266
+ sourceDb.close()
267
+ } catch (err) {
268
+ if (sourceDb) sourceDb.close()
269
+ throw err
270
+ }
271
+
272
+ copyFileSync(sourceDbPath, snapshotPath)
273
+ const stats = statSync(snapshotPath)
274
+
275
+ metaDb.prepare(`
276
+ INSERT INTO snapshots (branch, filename, name, size, description)
277
+ VALUES ('main', ?, 'Imported Baseline', ?, 'Baseline snapshot created during import')
278
+ `).run(snapshotFilename, stats.size)
279
+
280
+ metaDb.prepare(`
281
+ INSERT INTO events (branch, type, data)
282
+ VALUES ('main', 'snapshot_created', ?)
283
+ `).run(JSON.stringify({ filename: snapshotFilename }))
284
+
285
+ metaDb.close()
286
+
287
+ return { success: true, snapshotId: snapshotFilename }
288
+ } catch (error) {
289
+ fastify.log.error(error)
290
+ return reply.code(500).send({ error: error.message })
291
+ }
292
+ })
293
+
294
+ // Finalize import
295
+ fastify.post('/finalize', async (request, reply) => {
296
+ const { projectName } = request.body
297
+
298
+ // Validate project name
299
+ const validation = validateProjectName(projectName)
300
+ if (!validation.valid) {
301
+ return reply.code(400).send({ error: validation.error })
302
+ }
303
+ const safeName = validation.sanitized
304
+
305
+ try {
306
+ const projectPath = join(projectsRoot, safeName)
307
+ const metaDbPath = join(projectPath, '.studio', 'meta.db')
308
+ const metaDb = new Database(metaDbPath)
309
+
310
+ // Create baseline migration marker (NOT a SQL file)
311
+ metaDb.prepare(`
312
+ INSERT INTO migrations (branch, filename, applied_at)
313
+ VALUES ('main', 'baseline', datetime('now'))
314
+ `).run()
315
+
316
+ // Log finalization event
317
+ metaDb.prepare(`
318
+ INSERT INTO events (branch, type, data)
319
+ VALUES ('main', 'import_completed', ?)
320
+ `).run(JSON.stringify({ completed_at: new Date().toISOString() }))
321
+
322
+ metaDb.close()
323
+
324
+ // Create lock file for main branch
325
+ const locksDir = join(projectPath, '.studio', 'locks')
326
+ if (!existsSync(locksDir)) {
327
+ mkdirSync(locksDir, { recursive: true })
328
+ }
329
+ const lockPath = join(locksDir, 'main.lock')
330
+ if (!existsSync(lockPath)) {
331
+ writeFileSync(
332
+ lockPath,
333
+ JSON.stringify({
334
+ branch: 'main',
335
+ created_at: new Date().toISOString(),
336
+ reason: 'import'
337
+ }, null, 2)
338
+ )
339
+ }
340
+
341
+ // Start project server automatically
342
+ const serverInfoPath = join(projectPath, '.studio', 'server.json')
343
+ if (existsSync(serverInfoPath)) {
344
+ const serverInfo = JSON.parse(readFileSync(serverInfoPath, 'utf-8'))
345
+ return { success: true, server: { running: true, port: serverInfo.port } }
346
+ }
347
+
348
+ const port = await findFreePort(3000, safeName)
349
+ const serverPath = join(__routes_dirname, '..', 'index.js')
350
+ const serverProcess = spawn('node', [serverPath], {
351
+ detached: true,
352
+ stdio: 'ignore',
353
+ env: {
354
+ ...process.env,
355
+ PROJECT_NAME: safeName,
356
+ PROJECT_PATH: projectPath,
357
+ PORT: port.toString(),
358
+ IMPORT_MODE: 'false'
359
+ }
360
+ })
361
+ serverProcess.unref()
362
+
363
+ writeFileSync(serverInfoPath, JSON.stringify({
364
+ pid: serverProcess.pid,
365
+ port,
366
+ started_at: new Date().toISOString()
367
+ }, null, 2))
368
+
369
+ return { success: true, server: { running: true, port } }
370
+ } catch (error) {
371
+ fastify.log.error(error)
372
+ return reply.code(500).send({ error: error.message })
373
+ }
374
+ })
375
+ }