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
package/bin/sql-kite.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sql-kite",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SQL-Kite CLI — Local-first SQLite workspace with branches, migrations and snapshots.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sql-kite": "./bin/sql-kite.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node bin/sql-kite.js",
|
|
11
|
+
"start": "node bin/sql-kite.js"
|
|
12
|
+
},
|
|
13
|
+
"author": "D Krishna",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/Ananta-V/sql-kite"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/Ananta-V/sql-kite#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/Ananta-V/sql-kite/issues"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"sqlite",
|
|
25
|
+
"database",
|
|
26
|
+
"cli",
|
|
27
|
+
"migration",
|
|
28
|
+
"branching",
|
|
29
|
+
"snapshots",
|
|
30
|
+
"local-first",
|
|
31
|
+
"developer-tools"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"bin/",
|
|
38
|
+
"src/"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"better-sqlite3": "^9.2.2",
|
|
42
|
+
"chalk": "^5.3.0",
|
|
43
|
+
"commander": "^12.0.0",
|
|
44
|
+
"find-free-port": "^2.0.0",
|
|
45
|
+
"inquirer": "^9.2.12",
|
|
46
|
+
"open": "^10.0.3",
|
|
47
|
+
"ora": "^8.0.1"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { rmSync } from 'fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { getProjectPath, projectExists } from '../utils/paths.js';
|
|
6
|
+
import { stopCommand } from './stop.js';
|
|
7
|
+
|
|
8
|
+
export async function deleteCommand(name) {
|
|
9
|
+
if (!projectExists(name)) {
|
|
10
|
+
console.log(chalk.red(`✗ Project "${name}" does not exist`));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { confirm } = await inquirer.prompt([
|
|
15
|
+
{
|
|
16
|
+
type: 'confirm',
|
|
17
|
+
name: 'confirm',
|
|
18
|
+
message: `Delete project "${name}"? This cannot be undone.`,
|
|
19
|
+
default: false
|
|
20
|
+
}
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
if (!confirm) {
|
|
24
|
+
console.log(chalk.dim('Cancelled'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const spinner = ora(`Deleting project "${name}"...`).start();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Stop if running
|
|
32
|
+
await stopCommand(name).catch(() => {});
|
|
33
|
+
|
|
34
|
+
// Delete project folder
|
|
35
|
+
rmSync(getProjectPath(name), { recursive: true, force: true });
|
|
36
|
+
|
|
37
|
+
spinner.succeed(chalk.green(`✓ Project "${name}" deleted`));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
spinner.fail(chalk.red('✗ Failed to delete project'));
|
|
40
|
+
console.error(error);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'child_process'
|
|
4
|
+
import { join, dirname } from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
|
|
10
|
+
export default function importServerCommand(port = 3000) {
|
|
11
|
+
console.log(chalk.cyan('→ Starting import server...'))
|
|
12
|
+
console.log(chalk.dim(` Port: ${port}`))
|
|
13
|
+
console.log(chalk.dim(' Mode: Import-only'))
|
|
14
|
+
console.log('')
|
|
15
|
+
|
|
16
|
+
const serverPath = join(__dirname, '../../../server/src/index.js')
|
|
17
|
+
|
|
18
|
+
const serverProcess = spawn('node', [serverPath], {
|
|
19
|
+
stdio: 'inherit',
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
PORT: port,
|
|
23
|
+
IMPORT_MODE: 'true'
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
serverProcess.on('error', (error) => {
|
|
28
|
+
console.error(chalk.red('✗ Failed to start import server:'), error.message)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
serverProcess.on('exit', (code) => {
|
|
33
|
+
if (code !== 0) {
|
|
34
|
+
console.error(chalk.red(`✗ Import server exited with code ${code}`))
|
|
35
|
+
process.exit(code)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Handle Ctrl+C
|
|
40
|
+
process.on('SIGINT', () => {
|
|
41
|
+
console.log(chalk.yellow('\n→ Stopping import server...'))
|
|
42
|
+
serverProcess.kill('SIGINT')
|
|
43
|
+
process.exit(0)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
console.log(chalk.green('✓ Import server started'))
|
|
47
|
+
console.log(chalk.dim(` Open: http://localhost:${port}`))
|
|
48
|
+
console.log('')
|
|
49
|
+
console.log(chalk.dim('Press Ctrl+C to stop'))
|
|
50
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, statSync, accessSync, constants, writeFileSync, mkdirSync } from 'fs'
|
|
4
|
+
import { resolve, extname, basename, join, dirname } from 'path'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import Database from 'better-sqlite3'
|
|
7
|
+
import chalk from 'chalk'
|
|
8
|
+
import { spawn } from 'child_process'
|
|
9
|
+
import http from 'http'
|
|
10
|
+
import open from 'open'
|
|
11
|
+
import { findFreePort } from '../utils/port-finder.js'
|
|
12
|
+
import { ensureSqlKiteDirs, LOGS_DIR } from '../utils/paths.js'
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
|
|
16
|
+
export default async function importCommand(dbPath) {
|
|
17
|
+
ensureSqlKiteDirs()
|
|
18
|
+
if (!dbPath) {
|
|
19
|
+
console.error(chalk.red('✗ Error: Database path is required'))
|
|
20
|
+
console.log(chalk.dim('Usage: npm run sql-kite import <path-to-database>'))
|
|
21
|
+
console.log(chalk.dim(' Or: npm run sql-kite open <path-to-database>'))
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const absolutePath = resolve(dbPath)
|
|
26
|
+
|
|
27
|
+
// ========================================
|
|
28
|
+
// Step 1: Preflight checks
|
|
29
|
+
// ========================================
|
|
30
|
+
|
|
31
|
+
console.log(chalk.cyan('→ Running preflight checks...'))
|
|
32
|
+
|
|
33
|
+
// Check file exists
|
|
34
|
+
if (!existsSync(absolutePath)) {
|
|
35
|
+
console.error(chalk.red('✗ Error: File does not exist'))
|
|
36
|
+
console.error(chalk.dim(` Path: ${absolutePath}`))
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check it's a file, not directory
|
|
41
|
+
const stats = statSync(absolutePath)
|
|
42
|
+
if (stats.isDirectory()) {
|
|
43
|
+
console.error(chalk.red('✗ Error: Path is a directory, not a database file'))
|
|
44
|
+
console.error(chalk.dim(' Please provide a path to a .db file'))
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check for symlinks (security)
|
|
49
|
+
if (stats.isSymbolicLink()) {
|
|
50
|
+
console.error(chalk.red('✗ Error: Symlinks are not supported for security reasons'))
|
|
51
|
+
console.error(chalk.dim(' Please provide a direct path to the database file'))
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check file is readable
|
|
56
|
+
try {
|
|
57
|
+
accessSync(absolutePath, constants.R_OK)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error(chalk.red('✗ Error: File is not readable'))
|
|
60
|
+
console.error(chalk.dim(` Permission denied: ${absolutePath}`))
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Warn about file extension (don't block)
|
|
65
|
+
const ext = extname(absolutePath).toLowerCase()
|
|
66
|
+
if (ext !== '.db' && ext !== '.sqlite' && ext !== '.sqlite3') {
|
|
67
|
+
console.log(chalk.yellow('⚠ Warning: File extension is not .db, .sqlite, or .sqlite3'))
|
|
68
|
+
console.log(chalk.dim(` Found: ${ext || '(no extension)'}`))
|
|
69
|
+
console.log(chalk.dim(' Continuing anyway...'))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(chalk.green('✓ Preflight checks passed'))
|
|
73
|
+
|
|
74
|
+
// ========================================
|
|
75
|
+
// Step 2: Read-only probe
|
|
76
|
+
// ========================================
|
|
77
|
+
|
|
78
|
+
console.log(chalk.cyan('→ Validating SQLite database...'))
|
|
79
|
+
|
|
80
|
+
let db
|
|
81
|
+
try {
|
|
82
|
+
// Open in read-only mode (no WAL writes, no side effects)
|
|
83
|
+
db = new Database(absolutePath, { readonly: true, fileMustExist: true })
|
|
84
|
+
|
|
85
|
+
// Test basic query
|
|
86
|
+
try {
|
|
87
|
+
db.prepare('SELECT name FROM sqlite_master LIMIT 1').all()
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(chalk.red('✗ Error: File is not a valid SQLite database'))
|
|
90
|
+
console.error(chalk.dim(` ${err.message}`))
|
|
91
|
+
db.close()
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Get metadata
|
|
96
|
+
const userVersion = db.prepare('PRAGMA user_version').get().user_version
|
|
97
|
+
const journalMode = db.prepare('PRAGMA journal_mode').get().journal_mode
|
|
98
|
+
|
|
99
|
+
console.log(chalk.green('✓ Valid SQLite database detected'))
|
|
100
|
+
console.log(chalk.dim(` User version: ${userVersion}`))
|
|
101
|
+
console.log(chalk.dim(` Journal mode: ${journalMode}`))
|
|
102
|
+
|
|
103
|
+
// Get table count
|
|
104
|
+
const tableCount = db.prepare(`
|
|
105
|
+
SELECT COUNT(*) as count
|
|
106
|
+
FROM sqlite_master
|
|
107
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
108
|
+
`).get().count
|
|
109
|
+
|
|
110
|
+
console.log(chalk.dim(` Tables: ${tableCount}`))
|
|
111
|
+
|
|
112
|
+
db.close()
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (db) db.close()
|
|
115
|
+
console.error(chalk.red('✗ Error: Failed to open database'))
|
|
116
|
+
console.error(chalk.dim(` ${err.message}`))
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ========================================
|
|
121
|
+
// Step 3: Generate project name suggestion
|
|
122
|
+
// ========================================
|
|
123
|
+
|
|
124
|
+
const suggestedName = basename(absolutePath, extname(absolutePath))
|
|
125
|
+
.toLowerCase()
|
|
126
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
127
|
+
.replace(/-+/g, '-')
|
|
128
|
+
.replace(/^-|-$/g, '')
|
|
129
|
+
|
|
130
|
+
// ========================================
|
|
131
|
+
// Step 4: Import through Studio
|
|
132
|
+
// ========================================
|
|
133
|
+
|
|
134
|
+
console.log('')
|
|
135
|
+
console.log(chalk.green('✓ Database validated successfully'))
|
|
136
|
+
console.log('')
|
|
137
|
+
|
|
138
|
+
// Store import session data
|
|
139
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE
|
|
140
|
+
const sqlKiteDir = join(homeDir, '.sql-kite')
|
|
141
|
+
const sessionFile = join(sqlKiteDir, 'import-pending.json')
|
|
142
|
+
|
|
143
|
+
// Ensure .sql-kite directory exists
|
|
144
|
+
if (!existsSync(sqlKiteDir)) {
|
|
145
|
+
mkdirSync(sqlKiteDir, { recursive: true })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const importSession = {
|
|
149
|
+
sourcePath: absolutePath,
|
|
150
|
+
suggestedName,
|
|
151
|
+
validated: true,
|
|
152
|
+
timestamp: Date.now()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
writeFileSync(sessionFile, JSON.stringify(importSession, null, 2))
|
|
156
|
+
|
|
157
|
+
const defaultPort = 3000
|
|
158
|
+
const importUrl = (port) => `http://localhost:${port}`
|
|
159
|
+
|
|
160
|
+
async function getImportServerPort() {
|
|
161
|
+
try {
|
|
162
|
+
const mode = await new Promise((resolve, reject) => {
|
|
163
|
+
const req = http.get(`http://localhost:${defaultPort}/api/project`, (res) => {
|
|
164
|
+
let raw = ''
|
|
165
|
+
res.on('data', (chunk) => { raw += chunk })
|
|
166
|
+
res.on('end', () => {
|
|
167
|
+
try {
|
|
168
|
+
const data = JSON.parse(raw)
|
|
169
|
+
resolve(data.mode || 'project')
|
|
170
|
+
} catch (e) {
|
|
171
|
+
resolve('project')
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
req.on('error', reject)
|
|
176
|
+
req.setTimeout(500, () => {
|
|
177
|
+
req.destroy()
|
|
178
|
+
reject(new Error('Timeout'))
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
if (mode === 'import') {
|
|
183
|
+
return { port: defaultPort, alreadyRunning: true }
|
|
184
|
+
}
|
|
185
|
+
} catch (e) {
|
|
186
|
+
// Not running on default port
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const port = await findFreePort(defaultPort)
|
|
190
|
+
return { port, alreadyRunning: false }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function waitForImportServer(port, maxAttempts = 60) {
|
|
194
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
195
|
+
try {
|
|
196
|
+
await new Promise((resolve, reject) => {
|
|
197
|
+
const req = http.get(`http://localhost:${port}/api/project`, (res) => {
|
|
198
|
+
if (res.statusCode === 200) {
|
|
199
|
+
resolve()
|
|
200
|
+
} else {
|
|
201
|
+
reject(new Error(`Server returned ${res.statusCode}`))
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
req.on('error', reject)
|
|
205
|
+
req.setTimeout(1000, () => {
|
|
206
|
+
req.destroy()
|
|
207
|
+
reject(new Error('Timeout'))
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
return true
|
|
211
|
+
} catch (e) {
|
|
212
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log(chalk.bold('Import session ready!'))
|
|
219
|
+
console.log('')
|
|
220
|
+
console.log(chalk.cyan('→ Next steps:'))
|
|
221
|
+
console.log(chalk.dim(` 1. Launching import Studio...`))
|
|
222
|
+
console.log(chalk.dim(` 2. Complete the import wizard`))
|
|
223
|
+
console.log('')
|
|
224
|
+
console.log(chalk.dim(`Session saved to: ${sessionFile}`))
|
|
225
|
+
|
|
226
|
+
const studioPath = join(__dirname, '../../../studio/out')
|
|
227
|
+
if (!existsSync(studioPath)) {
|
|
228
|
+
console.log(chalk.red(`\n✗ Studio UI not built yet`))
|
|
229
|
+
console.log(chalk.dim(` Run: ${chalk.cyan(`cd packages/studio && npm run build`)}`))
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const { port, alreadyRunning } = await getImportServerPort()
|
|
235
|
+
|
|
236
|
+
if (!alreadyRunning) {
|
|
237
|
+
const serverPath = join(__dirname, '../../../server/src/index.js')
|
|
238
|
+
const logPath = join(LOGS_DIR, `import-server-${Date.now()}.log`)
|
|
239
|
+
const out = []
|
|
240
|
+
const serverProcess = spawn('node', [serverPath], {
|
|
241
|
+
detached: true,
|
|
242
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
243
|
+
env: {
|
|
244
|
+
...process.env,
|
|
245
|
+
PORT: port.toString(),
|
|
246
|
+
IMPORT_MODE: 'true'
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
serverProcess.stdout.on('data', (chunk) => {
|
|
251
|
+
out.push(chunk.toString())
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
serverProcess.stderr.on('data', (chunk) => {
|
|
255
|
+
out.push(chunk.toString())
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
serverProcess.unref()
|
|
259
|
+
|
|
260
|
+
const ready = await waitForImportServer(port)
|
|
261
|
+
if (!ready) {
|
|
262
|
+
if (out.length > 0) {
|
|
263
|
+
writeFileSync(logPath, out.join(''))
|
|
264
|
+
console.log(chalk.red('✗ Import server failed to start in time'))
|
|
265
|
+
console.log(chalk.dim(` Log: ${logPath}`))
|
|
266
|
+
} else {
|
|
267
|
+
console.log(chalk.red('✗ Import server failed to start in time'))
|
|
268
|
+
console.log(chalk.dim(' No logs captured. The server may not have started.'))
|
|
269
|
+
}
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log(chalk.cyan(`Opening ${importUrl(port)}...`))
|
|
275
|
+
await open(importUrl(port))
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.log(chalk.red('✗ Failed to launch import Studio'))
|
|
278
|
+
console.log(chalk.dim(error.message))
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
|
|
7
|
+
const ENGINE_DEV_TEMPLATE = `/**
|
|
8
|
+
* Development Database Engine
|
|
9
|
+
* Connects to SQL-Kite server via HTTP
|
|
10
|
+
* Used during development only
|
|
11
|
+
*
|
|
12
|
+
* ✅ LOCKED TO MAIN BRANCH
|
|
13
|
+
* The API is hardcoded to ONLY query the 'main' branch.
|
|
14
|
+
* You can switch branches in SQL-Kite Studio without affecting your app.
|
|
15
|
+
* Your app will always query main.
|
|
16
|
+
*
|
|
17
|
+
* Port Configuration:
|
|
18
|
+
* 1. Set SQL_KITE_PORT environment variable
|
|
19
|
+
* 2. Or update DEFAULT_PORT below
|
|
20
|
+
* 3. Port is shown when you run: sql-kite start <project>
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const DEFAULT_PORT = 3000;
|
|
24
|
+
|
|
25
|
+
// Auto-detect port from environment or use default
|
|
26
|
+
const getApiUrl = () => {
|
|
27
|
+
const port = process.env.SQL_KITE_PORT || DEFAULT_PORT;
|
|
28
|
+
return "http://localhost:" + port;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function runDevQuery(sql, params = []) {
|
|
32
|
+
try {
|
|
33
|
+
const apiUrl = getApiUrl();
|
|
34
|
+
const res = await fetch(apiUrl + "/api/query", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ sql, params }),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
|
|
42
|
+
if (!data.ok) {
|
|
43
|
+
throw new Error(data.error || "Query failed");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return data.result;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("Dev database error:", error);
|
|
49
|
+
console.error("Make sure SQL-Kite is running on port " + (process.env.SQL_KITE_PORT || DEFAULT_PORT));
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const ENGINE_LOCAL_TEMPLATE = `/**
|
|
56
|
+
* Local Database Engine
|
|
57
|
+
* Connects to local SQLite database using expo-sqlite
|
|
58
|
+
* Used in production builds
|
|
59
|
+
*
|
|
60
|
+
* IMPORTANT: Install expo-sqlite first:
|
|
61
|
+
* npx expo install expo-sqlite
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
import * as SQLite from "expo-sqlite";
|
|
65
|
+
|
|
66
|
+
const db = SQLite.openDatabase("main.db");
|
|
67
|
+
|
|
68
|
+
export function runLocalQuery(sql, params = []) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
db.transaction(tx => {
|
|
71
|
+
tx.executeSql(
|
|
72
|
+
sql,
|
|
73
|
+
params,
|
|
74
|
+
(_, result) => {
|
|
75
|
+
resolve(result.rows._array);
|
|
76
|
+
},
|
|
77
|
+
(_, error) => {
|
|
78
|
+
reject(error);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const INDEX_TEMPLATE = `/**
|
|
88
|
+
* Unified Database Layer
|
|
89
|
+
* Automatically switches between dev and production engines
|
|
90
|
+
*
|
|
91
|
+
* Usage in your app:
|
|
92
|
+
*
|
|
93
|
+
* import { runQuery } from '@/lib/database';
|
|
94
|
+
*
|
|
95
|
+
* const users = await runQuery(
|
|
96
|
+
* "SELECT * FROM users WHERE active = ?",
|
|
97
|
+
* [1]
|
|
98
|
+
* );
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
import { runDevQuery } from "./engine.dev";
|
|
102
|
+
import { runLocalQuery } from "./engine.local";
|
|
103
|
+
|
|
104
|
+
// In Expo, __DEV__ is available globally
|
|
105
|
+
const isDev = typeof __DEV__ !== 'undefined' ? __DEV__ : process.env.NODE_ENV === 'development';
|
|
106
|
+
|
|
107
|
+
export async function runQuery(sql, params = []) {
|
|
108
|
+
try {
|
|
109
|
+
if (isDev) {
|
|
110
|
+
// Development: Use HTTP connection to SQL-Kite
|
|
111
|
+
return await runDevQuery(sql, params);
|
|
112
|
+
} else {
|
|
113
|
+
// Production: Use local SQLite database
|
|
114
|
+
return await runLocalQuery(sql, params);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("Database Error:", error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Export individual engines for advanced use cases
|
|
123
|
+
export { runDevQuery, runLocalQuery };
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
export async function initCommand() {
|
|
127
|
+
const spinner = ora('Initializing database layer...').start();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Determine target directory
|
|
131
|
+
const targetDir = join(process.cwd(), 'lib', 'database');
|
|
132
|
+
const relativeDir = 'lib/database';
|
|
133
|
+
|
|
134
|
+
// Check if directory already exists
|
|
135
|
+
if (existsSync(targetDir)) {
|
|
136
|
+
spinner.stop();
|
|
137
|
+
|
|
138
|
+
const { overwrite } = await inquirer.prompt([
|
|
139
|
+
{
|
|
140
|
+
type: 'confirm',
|
|
141
|
+
name: 'overwrite',
|
|
142
|
+
message: chalk.yellow('Directory ' + relativeDir + ' already exists. Overwrite?'),
|
|
143
|
+
default: false
|
|
144
|
+
}
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
if (!overwrite) {
|
|
148
|
+
console.log(chalk.dim(' Cancelled.'));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
spinner.start('Creating database layer...');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Create directory
|
|
156
|
+
mkdirSync(targetDir, { recursive: true });
|
|
157
|
+
|
|
158
|
+
// Write files
|
|
159
|
+
writeFileSync(join(targetDir, 'engine.dev.js'), ENGINE_DEV_TEMPLATE);
|
|
160
|
+
writeFileSync(join(targetDir, 'engine.local.js'), ENGINE_LOCAL_TEMPLATE);
|
|
161
|
+
writeFileSync(join(targetDir, 'index.js'), INDEX_TEMPLATE);
|
|
162
|
+
|
|
163
|
+
spinner.succeed('Database layer created successfully!');
|
|
164
|
+
|
|
165
|
+
// Print instructions
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(chalk.bold('📁 Files created:'));
|
|
168
|
+
console.log(chalk.dim(' ' + relativeDir + '/index.js'));
|
|
169
|
+
console.log(chalk.dim(' ' + relativeDir + '/engine.dev.js'));
|
|
170
|
+
console.log(chalk.dim(' ' + relativeDir + '/engine.local.js'));
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log(chalk.bold('🚀 Usage:'));
|
|
173
|
+
console.log(chalk.cyan(" import { runQuery } from '@/lib/database';"));
|
|
174
|
+
console.log(chalk.cyan(' const users = await runQuery("SELECT * FROM users");'));
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log(chalk.bold('📖 Next steps:'));
|
|
177
|
+
console.log(chalk.dim(' 1. Start SQL-Kite: ' + chalk.cyan('npm run sql-kite start <project>')));
|
|
178
|
+
console.log(chalk.dim(' 2. Note the port (e.g., localhost:3001)'));
|
|
179
|
+
console.log(chalk.dim(' 3. Set port: ' + chalk.cyan('SQL_KITE_PORT=3001 in .env')));
|
|
180
|
+
console.log(chalk.dim(' 4. Use runQuery() in your app'));
|
|
181
|
+
console.log(chalk.dim(' 5. For production: ' + chalk.cyan('npx expo install expo-sqlite')));
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(chalk.bold('⚙️ Port configuration:'));
|
|
184
|
+
console.log(chalk.dim(' • AUTO: Set SQL_KITE_PORT env variable (recommended)'));
|
|
185
|
+
console.log(chalk.dim(' • MANUAL: Edit DEFAULT_PORT in engine.dev.js'));
|
|
186
|
+
console.log('');
|
|
187
|
+
|
|
188
|
+
} catch (error) {
|
|
189
|
+
spinner.fail('Failed to initialize database layer');
|
|
190
|
+
console.error(chalk.red(' Error: ' + error.message));
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readdirSync, existsSync, readFileSync } from 'fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { RUNTIME_DIR, getProjectServerInfoPath } from '../utils/paths.js';
|
|
4
|
+
|
|
5
|
+
export async function listCommand() {
|
|
6
|
+
if (!existsSync(RUNTIME_DIR)) {
|
|
7
|
+
console.log(chalk.dim('No projects yet'));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const projects = readdirSync(RUNTIME_DIR);
|
|
12
|
+
|
|
13
|
+
if (projects.length === 0) {
|
|
14
|
+
console.log(chalk.dim('No projects yet'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(chalk.bold('\nProjects:\n'));
|
|
19
|
+
|
|
20
|
+
projects.forEach(name => {
|
|
21
|
+
const serverInfoPath = getProjectServerInfoPath(name);
|
|
22
|
+
const isRunning = existsSync(serverInfoPath);
|
|
23
|
+
|
|
24
|
+
if (isRunning) {
|
|
25
|
+
const serverInfo = JSON.parse(readFileSync(serverInfoPath, 'utf-8'));
|
|
26
|
+
console.log(` ${chalk.green('●')} ${chalk.bold(name)} ${chalk.dim(`(port ${serverInfo.port})`)}`);
|
|
27
|
+
} else {
|
|
28
|
+
console.log(` ${chalk.gray('○')} ${name}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log();
|
|
33
|
+
}
|