sql-kite 1.0.0

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.
@@ -0,0 +1,69 @@
1
+ import { mkdirSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import {
6
+ ensureSqlKiteDirs,
7
+ getProjectPath,
8
+ getProjectDbPath,
9
+ getProjectMetaPath,
10
+ projectExists,
11
+ validateProjectName
12
+ } from '../utils/paths.js';
13
+ import { initUserDb, initMetaDb } from '../utils/db-init.js';
14
+
15
+ export async function newCommand(name) {
16
+ ensureSqlKiteDirs();
17
+
18
+ // Validate project name to prevent path traversal
19
+ const validation = validateProjectName(name);
20
+ if (!validation.valid) {
21
+ console.log(chalk.red(`✗ ${validation.error}`));
22
+ process.exit(1);
23
+ }
24
+
25
+ if (projectExists(validation.sanitized)) {
26
+ console.log(chalk.red(`✗ Project "${validation.sanitized}" already exists`));
27
+ process.exit(1);
28
+ }
29
+
30
+ const spinner = ora(`Creating project "${validation.sanitized}"...`).start();
31
+
32
+ try {
33
+ const projectPath = getProjectPath(validation.sanitized);
34
+ const studioPath = join(projectPath, '.studio');
35
+ const migrationsPath = join(projectPath, 'migrations');
36
+ const snapshotsPath = join(projectPath, 'snapshots');
37
+ const studioSnapshotsPath = join(studioPath, 'snapshots');
38
+
39
+ // Create directories
40
+ mkdirSync(projectPath, { recursive: true });
41
+ mkdirSync(studioPath, { recursive: true });
42
+ mkdirSync(migrationsPath, { recursive: true });
43
+ mkdirSync(snapshotsPath, { recursive: true });
44
+ mkdirSync(studioSnapshotsPath, { recursive: true }); // For automatic branch snapshots
45
+
46
+ // Initialize databases
47
+ initUserDb(getProjectDbPath(validation.sanitized));
48
+ initMetaDb(getProjectMetaPath(validation.sanitized));
49
+
50
+ // Create config
51
+ const config = {
52
+ name: validation.sanitized,
53
+ created_at: new Date().toISOString(),
54
+ version: '1.0.0'
55
+ };
56
+ writeFileSync(
57
+ join(projectPath, 'config.json'),
58
+ JSON.stringify(config, null, 2)
59
+ );
60
+
61
+ spinner.succeed(chalk.green(`✓ Project "${validation.sanitized}" created successfully`));
62
+ console.log(chalk.dim(` Location: ${projectPath}`));
63
+ console.log(chalk.dim(`\n Run: ${chalk.cyan(`npm run sql-kite start ${validation.sanitized}`)}`));
64
+ } catch (error) {
65
+ spinner.fail(chalk.red('✗ Failed to create project'));
66
+ console.error(error);
67
+ process.exit(1);
68
+ }
69
+ }
@@ -0,0 +1,23 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import chalk from 'chalk';
3
+ import open from 'open';
4
+ import { getProjectServerInfoPath, projectExists } from '../utils/paths.js';
5
+
6
+ export async function openCommand(name) {
7
+ if (!projectExists(name)) {
8
+ console.log(chalk.red(`✗ Project "${name}" does not exist`));
9
+ process.exit(1);
10
+ }
11
+
12
+ const serverInfoPath = getProjectServerInfoPath(name);
13
+
14
+ if (!existsSync(serverInfoPath)) {
15
+ console.log(chalk.yellow(`⚠ Project "${name}" is not running`));
16
+ console.log(chalk.dim(` Run: ${chalk.cyan(`npm run sql-kite start ${name}`)}`));
17
+ return;
18
+ }
19
+
20
+ const serverInfo = JSON.parse(readFileSync(serverInfoPath, 'utf-8'));
21
+ console.log(chalk.cyan(`Opening http://localhost:${serverInfo.port}...`));
22
+ await open(`http://localhost:${serverInfo.port}`);
23
+ }
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk';
2
+ import { getPortStatus, cleanupStalePorts } from '../utils/port-finder.js';
3
+
4
+ export async function portsCommand(options = {}) {
5
+ if (options.cleanup) {
6
+ console.log(chalk.cyan('Cleaning up stale port allocations...'));
7
+ const cleaned = cleanupStalePorts();
8
+ console.log(chalk.green(`✓ Cleaned ${cleaned} stale allocation(s)`));
9
+ return;
10
+ }
11
+
12
+ const status = getPortStatus();
13
+
14
+ console.log(chalk.bold('\n📊 SQL Kite Port Registry Status\n'));
15
+
16
+ if (status.total_allocations === 0) {
17
+ console.log(chalk.dim(' No ports currently allocated'));
18
+ console.log(chalk.dim(' All projects are stopped\n'));
19
+ return;
20
+ }
21
+ //hi
22
+
23
+ console.log(chalk.cyan(` Total allocations: ${status.total_allocations}\n`));
24
+
25
+ // Sort by port number
26
+ const allocations = Object.entries(status.allocations).sort(
27
+ (a, b) => a[1].port - b[1].port
28
+ );
29
+
30
+ console.log(chalk.bold(' Project Port Allocated At PID'));
31
+ console.log(chalk.dim(' ─────────────────────────────────────────────────────────────'));
32
+
33
+ for (const [projectName, info] of allocations) {
34
+ const allocatedTime = new Date(info.allocated_at).toLocaleString();
35
+ const projectDisplay = projectName.padEnd(20);
36
+ const portDisplay = info.port.toString().padEnd(7);
37
+ const pidDisplay = info.pid.toString().padEnd(7);
38
+
39
+ console.log(` ${chalk.green(projectDisplay)} ${chalk.cyan(portDisplay)} ${allocatedTime} ${pidDisplay}`);
40
+ }
41
+
42
+ console.log('');
43
+
44
+ if (status.last_cleanup) {
45
+ const lastCleanup = new Date(status.last_cleanup).toLocaleString();
46
+ console.log(chalk.dim(` Last cleanup: ${lastCleanup}`));
47
+ }
48
+
49
+ console.log(chalk.dim('\n Run with --cleanup to remove stale allocations\n'));
50
+ }
@@ -0,0 +1,128 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import http from 'http';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import open from 'open';
9
+ import {
10
+ getProjectPath,
11
+ getProjectServerInfoPath,
12
+ projectExists
13
+ } from '../utils/paths.js';
14
+ import { findFreePort } from '../utils/port-finder.js';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+
18
+ // Helper function to check if server is ready
19
+ async function waitForServer(port, maxAttempts = 30) {
20
+ for (let i = 0; i < maxAttempts; i++) {
21
+ try {
22
+ await new Promise((resolve, reject) => {
23
+ const req = http.get(`http://localhost:${port}/api/project`, (res) => {
24
+ if (res.statusCode === 200) {
25
+ resolve();
26
+ } else {
27
+ reject(new Error(`Server returned ${res.statusCode}`));
28
+ }
29
+ });
30
+ req.on('error', reject);
31
+ req.setTimeout(1000, () => {
32
+ req.destroy();
33
+ reject(new Error('Timeout'));
34
+ });
35
+ });
36
+ return true;
37
+ } catch (e) {
38
+ // Server not ready yet, wait and retry
39
+ await new Promise(resolve => setTimeout(resolve, 500));
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+
45
+ export async function startCommand(name) {
46
+ if (!projectExists(name)) {
47
+ console.log(chalk.red(`✗ Project "${name}" does not exist`));
48
+ console.log(chalk.dim(` Run: ${chalk.cyan(`npm run sql-kite new ${name}`)}`));
49
+ process.exit(1);
50
+ }
51
+
52
+ // Check if studio is built
53
+ const studioPath = join(__dirname, '../../../studio/out');
54
+ if (!existsSync(studioPath)) {
55
+ console.log(chalk.red(`✗ Studio UI not built yet`));
56
+ console.log(chalk.dim(` Run: ${chalk.cyan(`cd packages/studio && npm run build`)}`));
57
+ process.exit(1);
58
+ }
59
+
60
+ const serverInfoPath = getProjectServerInfoPath(name);
61
+
62
+ // Check if already running
63
+ if (existsSync(serverInfoPath)) {
64
+ try {
65
+ const serverInfo = JSON.parse(readFileSync(serverInfoPath, 'utf-8'));
66
+ console.log(chalk.yellow(`⚠ Project "${name}" is already running`));
67
+ console.log(chalk.dim(` URL: ${chalk.cyan(`http://localhost:${serverInfo.port}`)}`));
68
+ await open(`http://localhost:${serverInfo.port}`);
69
+ return;
70
+ } catch (e) {
71
+ // Server info corrupted, continue with start
72
+ }
73
+ }
74
+
75
+ const spinner = ora(`Starting project "${name}"...`).start();
76
+
77
+ try {
78
+ // Find and reserve a port for this project
79
+ const port = await findFreePort(3000, name);
80
+ const projectPath = getProjectPath(name);
81
+
82
+ // Path to server package
83
+ const serverPath = join(__dirname, '../../../server/src/index.js');
84
+
85
+ // Spawn server process
86
+ const serverProcess = spawn('node', [serverPath], {
87
+ detached: true,
88
+ stdio: 'ignore',
89
+ env: {
90
+ ...process.env,
91
+ PROJECT_NAME: name,
92
+ PROJECT_PATH: projectPath,
93
+ PORT: port.toString()
94
+ }
95
+ });
96
+
97
+ serverProcess.unref();
98
+
99
+ // Write server info
100
+ const serverInfo = {
101
+ pid: serverProcess.pid,
102
+ port,
103
+ started_at: new Date().toISOString()
104
+ };
105
+ writeFileSync(serverInfoPath, JSON.stringify(serverInfo, null, 2));
106
+
107
+ // Wait for server to be ready
108
+ spinner.text = `Waiting for server to start...`;
109
+ const serverReady = await waitForServer(port);
110
+
111
+ if (!serverReady) {
112
+ spinner.fail(chalk.red('✗ Server failed to start in time'));
113
+ console.log(chalk.yellow('⚠ Server might still be starting. Check logs for errors.'));
114
+ process.exit(1);
115
+ }
116
+
117
+ spinner.succeed(chalk.green(`✓ Project "${name}" started`));
118
+ console.log(chalk.dim(` URL: ${chalk.cyan(`http://localhost:${port}`)}`));
119
+ console.log(chalk.dim(` PID: ${serverProcess.pid}`));
120
+
121
+ // Open browser
122
+ await open(`http://localhost:${port}`);
123
+ } catch (error) {
124
+ spinner.fail(chalk.red('✗ Failed to start project'));
125
+ console.error(error);
126
+ process.exit(1);
127
+ }
128
+ }
@@ -0,0 +1,71 @@
1
+ import { existsSync, readFileSync, unlinkSync } from 'fs';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getProjectServerInfoPath, projectExists } from '../utils/paths.js';
5
+ import { releasePort } from '../utils/port-finder.js';
6
+
7
+ export async function stopCommand(name) {
8
+ if (!projectExists(name)) {
9
+ console.log(chalk.red(`✗ Project "${name}" does not exist`));
10
+ process.exit(1);
11
+ }
12
+
13
+ const serverInfoPath = getProjectServerInfoPath(name);
14
+
15
+ if (!existsSync(serverInfoPath)) {
16
+ console.log(chalk.yellow(`⚠ Project "${name}" is not running`));
17
+ return;
18
+ }
19
+
20
+ const spinner = ora(`Stopping project "${name}"...`).start();
21
+
22
+ try {
23
+ const serverInfo = JSON.parse(readFileSync(serverInfoPath, 'utf-8'));
24
+
25
+ // Send SIGTERM signal
26
+ try {
27
+ process.kill(serverInfo.pid, 'SIGTERM');
28
+ } catch (killError) {
29
+ // Process might already be dead, that's okay
30
+ if (killError.code !== 'ESRCH') {
31
+ throw killError;
32
+ }
33
+ }
34
+
35
+ // Wait for process to terminate (up to 5 seconds)
36
+ let processTerminated = false;
37
+ for (let i = 0; i < 50; i++) {
38
+ await new Promise(resolve => setTimeout(resolve, 100));
39
+ try {
40
+ // process.kill with signal 0 checks if process exists without killing it
41
+ process.kill(serverInfo.pid, 0);
42
+ } catch (e) {
43
+ // Process no longer exists
44
+ processTerminated = true;
45
+ break;
46
+ }
47
+ }
48
+
49
+ if (!processTerminated) {
50
+ // Force kill if graceful shutdown timed out
51
+ try {
52
+ process.kill(serverInfo.pid, 'SIGKILL');
53
+ await new Promise(resolve => setTimeout(resolve, 500));
54
+ } catch (e) {
55
+ // Process might already be gone
56
+ }
57
+ }
58
+
59
+ // Remove server info file
60
+ unlinkSync(serverInfoPath);
61
+
62
+ // Release port from registry
63
+ releasePort(name);
64
+
65
+ spinner.succeed(chalk.green(`✓ Project "${name}" stopped`));
66
+ } catch (error) {
67
+ spinner.fail(chalk.red('✗ Failed to stop project'));
68
+ console.error(error);
69
+ process.exit(1);
70
+ }
71
+ }
package/src/index.js ADDED
@@ -0,0 +1,73 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { newCommand } from './commands/new.js';
4
+ import { startCommand } from './commands/start.js';
5
+ import { stopCommand } from './commands/stop.js';
6
+ import { listCommand } from './commands/list.js';
7
+ import { deleteCommand } from './commands/delete.js';
8
+ import { openCommand } from './commands/open.js';
9
+ import { portsCommand } from './commands/ports.js';
10
+ import importCommand from './commands/import.js';
11
+ import importServerCommand from './commands/import-server.js';
12
+ import { initCommand } from './commands/init.js';
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name('sql-kite')
18
+ .description('SQL Kite - Local SQLite database platform with Studio UI')
19
+ .version('1.0.0');
20
+
21
+ program
22
+ .command('new <name>')
23
+ .description('Create a new database project')
24
+ .action(newCommand);
25
+
26
+ program
27
+ .command('init')
28
+ .description('Scaffold database integration layer in app project')
29
+ .action(initCommand);
30
+
31
+ program
32
+ .command('start <name>')
33
+ .description('Start the database server and open Studio')
34
+ .action(startCommand);
35
+
36
+ program
37
+ .command('stop <name>')
38
+ .description('Stop the database server')
39
+ .action(stopCommand);
40
+
41
+ program
42
+ .command('open <name>')
43
+ .description('Open Studio in browser (if server is running)')
44
+ .action(openCommand);
45
+
46
+ program
47
+ .command('import <database-path>')
48
+ .description('Import an existing SQLite database into a managed project')
49
+ .action(importCommand);
50
+
51
+ program
52
+ .command('import-server')
53
+ .description('Start import server (for completing pending imports)')
54
+ .option('-p, --port <port>', 'Port to run server on', '3000')
55
+ .action((options) => importServerCommand(parseInt(options.port)));
56
+
57
+ program
58
+ .command('list')
59
+ .description('List all database projects')
60
+ .action(listCommand);
61
+
62
+ program
63
+ .command('delete <name>')
64
+ .description('Delete a database project')
65
+ .action(deleteCommand);
66
+
67
+ program
68
+ .command('ports')
69
+ .description('View and manage port allocations')
70
+ .option('--cleanup', 'Clean up stale port allocations')
71
+ .action(portsCommand);
72
+
73
+ program.parse();
@@ -0,0 +1,20 @@
1
+ import Database from 'better-sqlite3';
2
+ import { migrateMetaDb } from './meta-migration.js';
3
+
4
+ export function initUserDb(dbPath) {
5
+ const db = new Database(dbPath);
6
+
7
+ // User's database starts empty
8
+ // They create tables via Studio
9
+
10
+ db.close();
11
+ }
12
+
13
+ export function initMetaDb(metaPath) {
14
+ // Just ensure the file exists, then run migration
15
+ const db = new Database(metaPath);
16
+ db.close();
17
+
18
+ // Run migration to latest schema
19
+ migrateMetaDb(metaPath);
20
+ }
@@ -0,0 +1,259 @@
1
+ import Database from 'better-sqlite3';
2
+
3
+ /**
4
+ * Migrate meta database to latest schema
5
+ * Handles upgrading existing projects to support branches
6
+ */
7
+ export function migrateMetaDb(metaPath) {
8
+ const db = new Database(metaPath);
9
+
10
+ // Create settings table first if it doesn't exist
11
+ db.exec(`
12
+ CREATE TABLE IF NOT EXISTS settings (
13
+ key TEXT PRIMARY KEY,
14
+ value TEXT NOT NULL
15
+ );
16
+ `);
17
+
18
+ // Get current schema version
19
+ let version = 0;
20
+ try {
21
+ const result = db.prepare(`
22
+ SELECT value FROM settings WHERE key = 'schema_version'
23
+ `).get();
24
+ version = result ? parseInt(result.value) : 0;
25
+ } catch (e) {
26
+ version = 0;
27
+ }
28
+
29
+ console.log(`Meta DB current version: ${version}`);
30
+
31
+ // Migration v0 -> v1: Add branch support
32
+ if (version < 1) {
33
+ console.log('Migrating meta DB to v1 (branch support)...');
34
+
35
+ // Create branches table
36
+ db.exec(`
37
+ CREATE TABLE IF NOT EXISTS branches (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ name TEXT NOT NULL UNIQUE,
40
+ db_file TEXT NOT NULL,
41
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
42
+ created_from TEXT,
43
+ description TEXT
44
+ );
45
+ `);
46
+
47
+ // Migrate events table
48
+ try {
49
+ const eventsInfo = db.pragma('table_info(events)');
50
+ if (eventsInfo.length > 0) {
51
+ const hasBranchColumn = eventsInfo.some(col => col.name === 'branch');
52
+
53
+ if (!hasBranchColumn) {
54
+ console.log(' Migrating events table...');
55
+ db.exec(`ALTER TABLE events RENAME TO events_old;`);
56
+
57
+ db.exec(`
58
+ CREATE TABLE events (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ branch TEXT NOT NULL DEFAULT 'main',
61
+ type TEXT NOT NULL,
62
+ data TEXT NOT NULL,
63
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
64
+ );
65
+ `);
66
+
67
+ db.exec(`
68
+ INSERT INTO events (id, branch, type, data, created_at)
69
+ SELECT id, 'main', type, data, created_at FROM events_old;
70
+ `);
71
+
72
+ db.exec(`DROP TABLE events_old;`);
73
+ }
74
+ } else {
75
+ // No events table yet, create it
76
+ db.exec(`
77
+ CREATE TABLE events (
78
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
79
+ branch TEXT NOT NULL DEFAULT 'main',
80
+ type TEXT NOT NULL,
81
+ data TEXT NOT NULL,
82
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
83
+ );
84
+ `);
85
+ }
86
+ } catch (e) {
87
+ // Events table doesn't exist, create it
88
+ db.exec(`
89
+ CREATE TABLE IF NOT EXISTS events (
90
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
91
+ branch TEXT NOT NULL DEFAULT 'main',
92
+ type TEXT NOT NULL,
93
+ data TEXT NOT NULL,
94
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
95
+ );
96
+ `);
97
+ }
98
+
99
+ // Migrate migrations table
100
+ try {
101
+ const migrationsInfo = db.pragma('table_info(migrations)');
102
+ if (migrationsInfo.length > 0) {
103
+ const hasBranchColumn = migrationsInfo.some(col => col.name === 'branch');
104
+
105
+ if (!hasBranchColumn) {
106
+ console.log(' Migrating migrations table...');
107
+ db.exec(`ALTER TABLE migrations RENAME TO migrations_old;`);
108
+
109
+ db.exec(`
110
+ CREATE TABLE migrations (
111
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
112
+ branch TEXT NOT NULL DEFAULT 'main',
113
+ filename TEXT NOT NULL,
114
+ applied_at TEXT DEFAULT CURRENT_TIMESTAMP,
115
+ UNIQUE(branch, filename)
116
+ );
117
+ `);
118
+
119
+ db.exec(`
120
+ INSERT INTO migrations (id, branch, filename, applied_at)
121
+ SELECT id, 'main', filename, applied_at FROM migrations_old;
122
+ `);
123
+
124
+ db.exec(`DROP TABLE migrations_old;`);
125
+ }
126
+ } else {
127
+ // No migrations table yet, create it
128
+ db.exec(`
129
+ CREATE TABLE migrations (
130
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
131
+ branch TEXT NOT NULL DEFAULT 'main',
132
+ filename TEXT NOT NULL,
133
+ applied_at TEXT DEFAULT CURRENT_TIMESTAMP,
134
+ UNIQUE(branch, filename)
135
+ );
136
+ `);
137
+ }
138
+ } catch (e) {
139
+ // Migrations table doesn't exist, create it
140
+ db.exec(`
141
+ CREATE TABLE IF NOT EXISTS migrations (
142
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
143
+ branch TEXT NOT NULL DEFAULT 'main',
144
+ filename TEXT NOT NULL,
145
+ applied_at TEXT DEFAULT CURRENT_TIMESTAMP,
146
+ UNIQUE(branch, filename)
147
+ );
148
+ `);
149
+ }
150
+
151
+ // Migrate snapshots table
152
+ try {
153
+ const snapshotsInfo = db.pragma('table_info(snapshots)');
154
+ if (snapshotsInfo.length > 0) {
155
+ const hasBranchColumn = snapshotsInfo.some(col => col.name === 'branch');
156
+ const hasNameColumn = snapshotsInfo.some(col => col.name === 'name');
157
+
158
+ if (!hasBranchColumn || !hasNameColumn) {
159
+ console.log(' Migrating snapshots table...');
160
+ db.exec(`ALTER TABLE snapshots RENAME TO snapshots_old;`);
161
+
162
+ db.exec(`
163
+ CREATE TABLE snapshots (
164
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
165
+ branch TEXT NOT NULL DEFAULT 'main',
166
+ filename TEXT NOT NULL UNIQUE,
167
+ name TEXT NOT NULL DEFAULT '',
168
+ size INTEGER,
169
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
170
+ description TEXT
171
+ );
172
+ `);
173
+
174
+ db.exec(`
175
+ INSERT INTO snapshots (id, branch, filename, name, size, created_at)
176
+ SELECT id, 'main', filename, filename, size, created_at FROM snapshots_old;
177
+ `);
178
+
179
+ db.exec(`DROP TABLE snapshots_old;`);
180
+ }
181
+ } else {
182
+ // No snapshots table yet, create it
183
+ db.exec(`
184
+ CREATE TABLE snapshots (
185
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
186
+ branch TEXT NOT NULL DEFAULT 'main',
187
+ filename TEXT NOT NULL UNIQUE,
188
+ name TEXT NOT NULL DEFAULT '',
189
+ size INTEGER,
190
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
191
+ description TEXT
192
+ );
193
+ `);
194
+ }
195
+ } catch (e) {
196
+ // Snapshots table doesn't exist, create it
197
+ db.exec(`
198
+ CREATE TABLE IF NOT EXISTS snapshots (
199
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
200
+ branch TEXT NOT NULL DEFAULT 'main',
201
+ filename TEXT NOT NULL UNIQUE,
202
+ name TEXT NOT NULL DEFAULT '',
203
+ size INTEGER,
204
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
205
+ description TEXT
206
+ );
207
+ `);
208
+ }
209
+
210
+ // Create default 'main' branch if not exists
211
+ const hasBranches = db.prepare('SELECT COUNT(*) as count FROM branches').get();
212
+ if (hasBranches.count === 0) {
213
+ console.log(' Creating default main branch...');
214
+ db.prepare(`
215
+ INSERT INTO branches (name, db_file, created_from, description)
216
+ VALUES ('main', 'db.sqlite', NULL, 'Default branch')
217
+ `).run();
218
+ }
219
+
220
+ // Set main as current branch
221
+ db.prepare(`
222
+ INSERT OR REPLACE INTO settings (key, value) VALUES ('current_branch', 'main')
223
+ `).run();
224
+
225
+ // Update schema version
226
+ db.prepare(`
227
+ INSERT OR REPLACE INTO settings (key, value) VALUES ('schema_version', '1')
228
+ `).run();
229
+
230
+ console.log('✓ Meta DB migrated to v1');
231
+ }
232
+
233
+ // Migration v1 -> v2: Add type column to snapshots
234
+ if (version < 2) {
235
+ console.log('Migrating meta DB to v2 (snapshot types)...');
236
+
237
+ // Add type column to snapshots if it doesn't exist
238
+ try {
239
+ const snapshotsInfo = db.pragma('table_info(snapshots)');
240
+ const hasTypeColumn = snapshotsInfo.some(col => col.name === 'type');
241
+
242
+ if (!hasTypeColumn) {
243
+ console.log(' Adding type column to snapshots table...');
244
+ db.exec(`ALTER TABLE snapshots ADD COLUMN type TEXT DEFAULT 'manual';`);
245
+ }
246
+ } catch (e) {
247
+ console.log(' Error adding type column:', e.message);
248
+ }
249
+
250
+ // Update schema version
251
+ db.prepare(`
252
+ INSERT OR REPLACE INTO settings (key, value) VALUES ('schema_version', '2')
253
+ `).run();
254
+
255
+ console.log('✓ Meta DB migrated to v2');
256
+ }
257
+
258
+ db.close();
259
+ }