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.
- package/bin/sql-kite.js +2 -0
- package/package.json +49 -0
- package/src/commands/delete.js +43 -0
- package/src/commands/import-server.js +50 -0
- package/src/commands/import.js +280 -0
- package/src/commands/init.js +193 -0
- package/src/commands/list.js +33 -0
- package/src/commands/new.js +69 -0
- package/src/commands/open.js +23 -0
- package/src/commands/ports.js +50 -0
- package/src/commands/start.js +128 -0
- package/src/commands/stop.js +71 -0
- package/src/index.js +73 -0
- package/src/utils/db-init.js +20 -0
- package/src/utils/meta-migration.js +259 -0
- package/src/utils/paths.js +71 -0
- package/src/utils/port-finder.js +239 -0
- package/src/utils/port-registry.js +233 -0
|
@@ -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
|
+
}
|