spindb 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -7
- package/cli/commands/clone.ts +6 -0
- package/cli/commands/connect.ts +115 -14
- package/cli/commands/create.ts +170 -8
- package/cli/commands/doctor.ts +320 -0
- package/cli/commands/edit.ts +209 -9
- package/cli/commands/engines.ts +34 -3
- package/cli/commands/info.ts +81 -26
- package/cli/commands/list.ts +64 -9
- package/cli/commands/logs.ts +9 -3
- package/cli/commands/menu/backup-handlers.ts +52 -21
- package/cli/commands/menu/container-handlers.ts +433 -127
- package/cli/commands/menu/engine-handlers.ts +128 -4
- package/cli/commands/menu/index.ts +5 -1
- package/cli/commands/menu/shell-handlers.ts +105 -21
- package/cli/commands/menu/sql-handlers.ts +16 -4
- package/cli/commands/menu/update-handlers.ts +278 -0
- package/cli/commands/restore.ts +83 -23
- package/cli/commands/run.ts +27 -11
- package/cli/commands/url.ts +17 -9
- package/cli/constants.ts +1 -0
- package/cli/helpers.ts +41 -1
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +148 -7
- package/config/engine-defaults.ts +14 -0
- package/config/os-dependencies.ts +66 -0
- package/config/paths.ts +8 -0
- package/core/container-manager.ts +191 -32
- package/core/dependency-manager.ts +18 -0
- package/core/error-handler.ts +31 -0
- package/core/port-manager.ts +2 -0
- package/core/process-manager.ts +25 -3
- package/engines/index.ts +4 -0
- package/engines/mysql/backup.ts +53 -36
- package/engines/mysql/index.ts +48 -5
- package/engines/postgresql/index.ts +6 -0
- package/engines/sqlite/index.ts +606 -0
- package/engines/sqlite/registry.ts +185 -0
- package/package.json +1 -1
- package/types/index.ts +26 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**Local databases without the Docker baggage.**
|
|
8
8
|
|
|
9
|
-
Spin up PostgreSQL and
|
|
9
|
+
Spin up PostgreSQL, MySQL, and SQLite instances for local development. No Docker daemon, no container networking, no volume mounts. Just databases running on localhost, ready in seconds.
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -176,11 +176,37 @@ spindb deps check --engine mysql
|
|
|
176
176
|
|
|
177
177
|
**Linux users:** MariaDB works as a drop-in replacement for MySQL. If you have MariaDB installed, SpinDB will detect and use it automatically. In a future release, MariaDB will be available as its own engine with support for MariaDB-specific features.
|
|
178
178
|
|
|
179
|
+
#### SQLite
|
|
180
|
+
|
|
181
|
+
| | |
|
|
182
|
+
|---|---|
|
|
183
|
+
| Version | 3 (system) |
|
|
184
|
+
| Default port | N/A (file-based) |
|
|
185
|
+
| Data location | Project directory (CWD) |
|
|
186
|
+
| Binary source | System installation |
|
|
187
|
+
|
|
188
|
+
SQLite is a file-based database—no server process, no ports. Databases are stored in your project directory by default, not `~/.spindb/`. SpinDB tracks registered SQLite databases in a registry file.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# Create in current directory
|
|
192
|
+
spindb create mydb --engine sqlite
|
|
193
|
+
|
|
194
|
+
# Create with custom path
|
|
195
|
+
spindb create mydb --engine sqlite --path ./data/mydb.sqlite
|
|
196
|
+
|
|
197
|
+
# Connect to it
|
|
198
|
+
spindb connect mydb
|
|
199
|
+
|
|
200
|
+
# Use litecli for enhanced experience
|
|
201
|
+
spindb connect mydb --litecli
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Note:** Unlike server databases, SQLite databases don't need to be "started" or "stopped"—they're always available as long as the file exists.
|
|
205
|
+
|
|
179
206
|
### Planned Engines
|
|
180
207
|
|
|
181
208
|
| Engine | Type | Status |
|
|
182
209
|
|--------|------|--------|
|
|
183
|
-
| SQLite | File-based | Planned for v1.2 |
|
|
184
210
|
| Redis | In-memory key-value | Planned for v1.2 |
|
|
185
211
|
| MongoDB | Document database | Planned for v1.2 |
|
|
186
212
|
|
|
@@ -195,10 +221,17 @@ spindb deps check --engine mysql
|
|
|
195
221
|
```bash
|
|
196
222
|
spindb create mydb # PostgreSQL (default)
|
|
197
223
|
spindb create mydb --engine mysql # MySQL
|
|
224
|
+
spindb create mydb --engine sqlite # SQLite (file-based)
|
|
198
225
|
spindb create mydb --version 16 # Specific PostgreSQL version
|
|
199
226
|
spindb create mydb --port 5433 # Custom port
|
|
200
227
|
spindb create mydb --database my_app # Custom database name
|
|
201
228
|
spindb create mydb --no-start # Create without starting
|
|
229
|
+
|
|
230
|
+
# Create, start, and connect in one command
|
|
231
|
+
spindb create mydb --start --connect
|
|
232
|
+
|
|
233
|
+
# SQLite with custom path
|
|
234
|
+
spindb create mydb --engine sqlite --path ./data/app.sqlite
|
|
202
235
|
```
|
|
203
236
|
|
|
204
237
|
Create and restore in one command:
|
|
@@ -213,13 +246,16 @@ spindb create mydb --from "postgresql://user:pass@host:5432/production"
|
|
|
213
246
|
|
|
214
247
|
| Option | Description |
|
|
215
248
|
|--------|-------------|
|
|
216
|
-
| `--engine`, `-e` | Database engine (`postgresql`, `mysql`) |
|
|
249
|
+
| `--engine`, `-e` | Database engine (`postgresql`, `mysql`, `sqlite`) |
|
|
217
250
|
| `--version`, `-v` | Engine version |
|
|
218
|
-
| `--port`, `-p` | Port number |
|
|
251
|
+
| `--port`, `-p` | Port number (not applicable for SQLite) |
|
|
219
252
|
| `--database`, `-d` | Primary database name |
|
|
253
|
+
| `--path` | File path for SQLite databases |
|
|
220
254
|
| `--max-connections` | Maximum database connections (default: 200) |
|
|
221
255
|
| `--from` | Restore from backup file or connection string |
|
|
256
|
+
| `--start` | Start container after creation (skip prompt) |
|
|
222
257
|
| `--no-start` | Create without starting |
|
|
258
|
+
| `--connect` | Open a shell connection after creation |
|
|
223
259
|
|
|
224
260
|
</details>
|
|
225
261
|
|
|
@@ -333,11 +369,12 @@ spindb clone source-db new-db
|
|
|
333
369
|
spindb start new-db
|
|
334
370
|
```
|
|
335
371
|
|
|
336
|
-
#### `edit` - Rename, change port, or edit database config
|
|
372
|
+
#### `edit` - Rename, change port, relocate, or edit database config
|
|
337
373
|
|
|
338
374
|
```bash
|
|
339
375
|
spindb edit mydb --name newname # Must be stopped
|
|
340
376
|
spindb edit mydb --port 5433
|
|
377
|
+
spindb edit mydb --relocate ~/new/path # Move SQLite database file
|
|
341
378
|
spindb edit mydb --set-config max_connections=300 # PostgreSQL config
|
|
342
379
|
spindb edit mydb # Interactive mode
|
|
343
380
|
```
|
|
@@ -368,10 +405,12 @@ ENGINE VERSION SOURCE SIZE
|
|
|
368
405
|
🐘 postgresql 17.7 darwin-arm64 45.2 MB
|
|
369
406
|
🐘 postgresql 16.8 darwin-arm64 44.8 MB
|
|
370
407
|
🐬 mysql 8.0.35 system (system-installed)
|
|
408
|
+
🪶 sqlite 3.43.2 system (system-installed)
|
|
371
409
|
────────────────────────────────────────────────────────
|
|
372
410
|
|
|
373
411
|
PostgreSQL: 2 version(s), 90.0 MB
|
|
374
412
|
MySQL: system-installed at /opt/homebrew/bin/mysqld
|
|
413
|
+
SQLite: system-installed at /usr/bin/sqlite3
|
|
375
414
|
```
|
|
376
415
|
|
|
377
416
|
#### `deps` - Manage client tools
|
|
@@ -405,6 +444,43 @@ spindb version --check # Check for updates
|
|
|
405
444
|
spindb self-update
|
|
406
445
|
```
|
|
407
446
|
|
|
447
|
+
#### `doctor` - System health check
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
spindb doctor # Interactive health check
|
|
451
|
+
spindb doctor --json # JSON output for scripting
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Checks performed:
|
|
455
|
+
- Configuration file validity and binary cache freshness
|
|
456
|
+
- Container status across all engines
|
|
457
|
+
- SQLite registry for orphaned entries (files deleted outside SpinDB)
|
|
458
|
+
- Database tool availability
|
|
459
|
+
|
|
460
|
+
Example output:
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
SpinDB Health Check
|
|
464
|
+
═══════════════════
|
|
465
|
+
|
|
466
|
+
✓ Configuration
|
|
467
|
+
└─ Configuration valid, 12 tools cached
|
|
468
|
+
|
|
469
|
+
✓ Containers
|
|
470
|
+
└─ 4 container(s)
|
|
471
|
+
postgresql: 2 running, 0 stopped
|
|
472
|
+
mysql: 0 running, 1 stopped
|
|
473
|
+
sqlite: 1 exist, 0 missing
|
|
474
|
+
|
|
475
|
+
⚠ SQLite Registry
|
|
476
|
+
└─ 1 orphaned entry found
|
|
477
|
+
"old-project" → /path/to/missing.sqlite
|
|
478
|
+
|
|
479
|
+
? What would you like to do?
|
|
480
|
+
❯ Remove orphaned entries from registry
|
|
481
|
+
Skip (do nothing)
|
|
482
|
+
```
|
|
483
|
+
|
|
408
484
|
---
|
|
409
485
|
|
|
410
486
|
## Enhanced CLI Tools
|
|
@@ -415,7 +491,7 @@ SpinDB supports enhanced database shells that provide features like auto-complet
|
|
|
415
491
|
|--------|----------|----------|-----------|
|
|
416
492
|
| PostgreSQL | `psql` | `pgcli` | `usql` |
|
|
417
493
|
| MySQL | `mysql` | `mycli` | `usql` |
|
|
418
|
-
| SQLite
|
|
494
|
+
| SQLite | `sqlite3` | `litecli` | `usql` |
|
|
419
495
|
| Redis (planned) | `redis-cli` | `iredis` | - |
|
|
420
496
|
| MongoDB (planned) | `mongosh` | - | - |
|
|
421
497
|
|
|
@@ -456,8 +532,13 @@ spindb connect mydb --install-tui # usql
|
|
|
456
532
|
│ ├── container.json
|
|
457
533
|
│ ├── data/
|
|
458
534
|
│ └── mysql.log
|
|
535
|
+
├── sqlite-registry.json # Tracks SQLite file locations
|
|
459
536
|
├── logs/ # Error logs
|
|
460
537
|
└── config.json # Tool paths cache
|
|
538
|
+
|
|
539
|
+
# SQLite databases are stored in project directories, not ~/.spindb/
|
|
540
|
+
./myproject/
|
|
541
|
+
└── mydb.sqlite # Created with: spindb create mydb -e sqlite
|
|
461
542
|
```
|
|
462
543
|
|
|
463
544
|
### How Data Persists
|
|
@@ -521,7 +602,6 @@ See [TODO.md](TODO.md) for the full roadmap.
|
|
|
521
602
|
- Secrets management (macOS Keychain)
|
|
522
603
|
|
|
523
604
|
### v1.2 - Additional Engines
|
|
524
|
-
- SQLite (file-based, no server)
|
|
525
605
|
- Redis (in-memory key-value)
|
|
526
606
|
- MongoDB (document database)
|
|
527
607
|
- MariaDB as standalone engine
|
package/cli/commands/clone.ts
CHANGED
|
@@ -71,6 +71,12 @@ export const cloneCommand = new Command('clone')
|
|
|
71
71
|
targetName = await promptContainerName(`${sourceName}-copy`)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Check if target container already exists
|
|
75
|
+
if (await containerManager.exists(targetName, { engine: sourceConfig.engine })) {
|
|
76
|
+
console.error(error(`Container "${targetName}" already exists`))
|
|
77
|
+
process.exit(1)
|
|
78
|
+
}
|
|
79
|
+
|
|
74
80
|
const cloneSpinner = createSpinner(
|
|
75
81
|
`Cloning ${sourceName} to ${targetName}...`,
|
|
76
82
|
)
|
package/cli/commands/connect.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { spawn } from 'child_process'
|
|
3
|
+
import { existsSync } from 'fs'
|
|
3
4
|
import chalk from 'chalk'
|
|
4
5
|
import { containerManager } from '../../core/container-manager'
|
|
5
6
|
import { processManager } from '../../core/process-manager'
|
|
@@ -7,18 +8,22 @@ import {
|
|
|
7
8
|
isUsqlInstalled,
|
|
8
9
|
isPgcliInstalled,
|
|
9
10
|
isMycliInstalled,
|
|
11
|
+
isLitecliInstalled,
|
|
10
12
|
detectPackageManager,
|
|
11
13
|
installUsql,
|
|
12
14
|
installPgcli,
|
|
13
15
|
installMycli,
|
|
16
|
+
installLitecli,
|
|
14
17
|
getUsqlManualInstructions,
|
|
15
18
|
getPgcliManualInstructions,
|
|
16
19
|
getMycliManualInstructions,
|
|
20
|
+
getLitecliManualInstructions,
|
|
17
21
|
} from '../../core/dependency-manager'
|
|
18
22
|
import { getEngine } from '../../engines'
|
|
19
23
|
import { getEngineDefaults } from '../../config/defaults'
|
|
20
24
|
import { promptContainerSelect } from '../ui/prompts'
|
|
21
25
|
import { error, warning, info, success } from '../ui/theme'
|
|
26
|
+
import { Engine } from '../../types'
|
|
22
27
|
|
|
23
28
|
export const connectCommand = new Command('connect')
|
|
24
29
|
.alias('shell')
|
|
@@ -37,6 +42,11 @@ export const connectCommand = new Command('connect')
|
|
|
37
42
|
'Use mycli for enhanced MySQL shell (dropdown auto-completion)',
|
|
38
43
|
)
|
|
39
44
|
.option('--install-mycli', 'Install mycli if not present, then connect')
|
|
45
|
+
.option(
|
|
46
|
+
'--litecli',
|
|
47
|
+
'Use litecli for enhanced SQLite shell (auto-completion, syntax highlighting)',
|
|
48
|
+
)
|
|
49
|
+
.option('--install-litecli', 'Install litecli if not present, then connect')
|
|
40
50
|
.action(
|
|
41
51
|
async (
|
|
42
52
|
name: string | undefined,
|
|
@@ -48,6 +58,8 @@ export const connectCommand = new Command('connect')
|
|
|
48
58
|
installPgcli?: boolean
|
|
49
59
|
mycli?: boolean
|
|
50
60
|
installMycli?: boolean
|
|
61
|
+
litecli?: boolean
|
|
62
|
+
installLitecli?: boolean
|
|
51
63
|
},
|
|
52
64
|
) => {
|
|
53
65
|
try {
|
|
@@ -55,9 +67,15 @@ export const connectCommand = new Command('connect')
|
|
|
55
67
|
|
|
56
68
|
if (!containerName) {
|
|
57
69
|
const containers = await containerManager.list()
|
|
58
|
-
|
|
70
|
+
// SQLite containers are always "available" if file exists, server containers need to be running
|
|
71
|
+
const connectable = containers.filter((c) => {
|
|
72
|
+
if (c.engine === Engine.SQLite) {
|
|
73
|
+
return existsSync(c.database)
|
|
74
|
+
}
|
|
75
|
+
return c.status === 'running'
|
|
76
|
+
})
|
|
59
77
|
|
|
60
|
-
if (
|
|
78
|
+
if (connectable.length === 0) {
|
|
61
79
|
if (containers.length === 0) {
|
|
62
80
|
console.log(
|
|
63
81
|
warning('No containers found. Create one with: spindb create'),
|
|
@@ -73,7 +91,7 @@ export const connectCommand = new Command('connect')
|
|
|
73
91
|
}
|
|
74
92
|
|
|
75
93
|
const selected = await promptContainerSelect(
|
|
76
|
-
|
|
94
|
+
connectable,
|
|
77
95
|
'Select container to connect to:',
|
|
78
96
|
)
|
|
79
97
|
if (!selected) return
|
|
@@ -92,16 +110,29 @@ export const connectCommand = new Command('connect')
|
|
|
92
110
|
const database =
|
|
93
111
|
options.database ?? config.database ?? engineDefaults.superuser
|
|
94
112
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
// SQLite: check file exists instead of running status
|
|
114
|
+
if (engineName === Engine.SQLite) {
|
|
115
|
+
if (!existsSync(config.database)) {
|
|
116
|
+
console.error(
|
|
117
|
+
error(
|
|
118
|
+
`SQLite database file not found: ${config.database}`,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
process.exit(1)
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Server databases need to be running
|
|
125
|
+
const running = await processManager.isRunning(containerName, {
|
|
126
|
+
engine: engineName,
|
|
127
|
+
})
|
|
128
|
+
if (!running) {
|
|
129
|
+
console.error(
|
|
130
|
+
error(
|
|
131
|
+
`Container "${containerName}" is not running. Start it first.`,
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
|
105
136
|
}
|
|
106
137
|
|
|
107
138
|
const engine = getEngine(engineName)
|
|
@@ -275,13 +306,74 @@ export const connectCommand = new Command('connect')
|
|
|
275
306
|
}
|
|
276
307
|
}
|
|
277
308
|
|
|
309
|
+
const useLitecli = options.litecli || options.installLitecli
|
|
310
|
+
if (useLitecli) {
|
|
311
|
+
if (engineName !== Engine.SQLite) {
|
|
312
|
+
console.error(error('litecli is only available for SQLite containers'))
|
|
313
|
+
if (engineName === 'postgresql') {
|
|
314
|
+
console.log(chalk.gray('For PostgreSQL, use: spindb connect --pgcli'))
|
|
315
|
+
} else if (engineName === 'mysql') {
|
|
316
|
+
console.log(chalk.gray('For MySQL, use: spindb connect --mycli'))
|
|
317
|
+
}
|
|
318
|
+
process.exit(1)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const litecliInstalled = await isLitecliInstalled()
|
|
322
|
+
|
|
323
|
+
if (!litecliInstalled) {
|
|
324
|
+
if (options.installLitecli) {
|
|
325
|
+
console.log(info('Installing litecli for enhanced SQLite shell...'))
|
|
326
|
+
const pm = await detectPackageManager()
|
|
327
|
+
if (pm) {
|
|
328
|
+
const result = await installLitecli(pm)
|
|
329
|
+
if (result.success) {
|
|
330
|
+
console.log(success('litecli installed successfully!'))
|
|
331
|
+
console.log()
|
|
332
|
+
} else {
|
|
333
|
+
console.error(
|
|
334
|
+
error(`Failed to install litecli: ${result.error}`),
|
|
335
|
+
)
|
|
336
|
+
console.log()
|
|
337
|
+
console.log(chalk.gray('Manual installation:'))
|
|
338
|
+
for (const instruction of getLitecliManualInstructions()) {
|
|
339
|
+
console.log(chalk.cyan(` ${instruction}`))
|
|
340
|
+
}
|
|
341
|
+
process.exit(1)
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
console.error(error('No supported package manager found'))
|
|
345
|
+
console.log()
|
|
346
|
+
console.log(chalk.gray('Manual installation:'))
|
|
347
|
+
for (const instruction of getLitecliManualInstructions()) {
|
|
348
|
+
console.log(chalk.cyan(` ${instruction}`))
|
|
349
|
+
}
|
|
350
|
+
process.exit(1)
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
console.error(error('litecli is not installed'))
|
|
354
|
+
console.log()
|
|
355
|
+
console.log(chalk.gray('Install litecli for enhanced SQLite shell:'))
|
|
356
|
+
console.log(chalk.cyan(' spindb connect --install-litecli'))
|
|
357
|
+
console.log()
|
|
358
|
+
console.log(chalk.gray('Or install manually:'))
|
|
359
|
+
for (const instruction of getLitecliManualInstructions()) {
|
|
360
|
+
console.log(chalk.cyan(` ${instruction}`))
|
|
361
|
+
}
|
|
362
|
+
process.exit(1)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
278
367
|
console.log(info(`Connecting to ${containerName}:${database}...`))
|
|
279
368
|
console.log()
|
|
280
369
|
|
|
281
370
|
let clientCmd: string
|
|
282
371
|
let clientArgs: string[]
|
|
283
372
|
|
|
284
|
-
if (
|
|
373
|
+
if (useLitecli) {
|
|
374
|
+
clientCmd = 'litecli'
|
|
375
|
+
clientArgs = [config.database]
|
|
376
|
+
} else if (usePgcli) {
|
|
285
377
|
clientCmd = 'pgcli'
|
|
286
378
|
clientArgs = [connectionString]
|
|
287
379
|
} else if (useMycli) {
|
|
@@ -298,6 +390,9 @@ export const connectCommand = new Command('connect')
|
|
|
298
390
|
} else if (useUsql) {
|
|
299
391
|
clientCmd = 'usql'
|
|
300
392
|
clientArgs = [connectionString]
|
|
393
|
+
} else if (engineName === Engine.SQLite) {
|
|
394
|
+
clientCmd = 'sqlite3'
|
|
395
|
+
clientArgs = [config.database]
|
|
301
396
|
} else if (engineName === 'mysql') {
|
|
302
397
|
clientCmd = 'mysql'
|
|
303
398
|
clientArgs = [
|
|
@@ -339,6 +434,12 @@ export const connectCommand = new Command('connect')
|
|
|
339
434
|
} else if (clientCmd === 'mycli') {
|
|
340
435
|
console.log(chalk.gray(' Install mycli:'))
|
|
341
436
|
console.log(chalk.cyan(' brew install mycli'))
|
|
437
|
+
} else if (clientCmd === 'litecli') {
|
|
438
|
+
console.log(chalk.gray(' Install litecli:'))
|
|
439
|
+
console.log(chalk.cyan(' brew install litecli'))
|
|
440
|
+
} else if (clientCmd === 'sqlite3') {
|
|
441
|
+
console.log(chalk.gray(' sqlite3 comes with macOS.'))
|
|
442
|
+
console.log(chalk.gray(' If not available, check your PATH.'))
|
|
342
443
|
} else if (engineName === 'mysql') {
|
|
343
444
|
console.log(chalk.gray(' On macOS with Homebrew:'))
|
|
344
445
|
console.log(chalk.cyan(' brew install mysql-client'))
|
package/cli/commands/create.ts
CHANGED
|
@@ -20,7 +20,109 @@ import { getMissingDependencies } from '../../core/dependency-manager'
|
|
|
20
20
|
import { platformService } from '../../core/platform-service'
|
|
21
21
|
import { startWithRetry } from '../../core/start-with-retry'
|
|
22
22
|
import { TransactionManager } from '../../core/transaction-manager'
|
|
23
|
+
import { isValidDatabaseName } from '../../core/error-handler'
|
|
23
24
|
import { Engine } from '../../types'
|
|
25
|
+
import type { BaseEngine } from '../../engines/base-engine'
|
|
26
|
+
import { resolve } from 'path'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Simplified SQLite container creation flow
|
|
30
|
+
* SQLite is file-based, so no port, start/stop, or server management needed
|
|
31
|
+
*/
|
|
32
|
+
async function createSqliteContainer(
|
|
33
|
+
containerName: string,
|
|
34
|
+
dbEngine: BaseEngine,
|
|
35
|
+
version: string,
|
|
36
|
+
options: { path?: string; from?: string | null; connect?: boolean },
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const { path: filePath, from: restoreLocation, connect } = options
|
|
39
|
+
|
|
40
|
+
// Check dependencies
|
|
41
|
+
const depsSpinner = createSpinner('Checking required tools...')
|
|
42
|
+
depsSpinner.start()
|
|
43
|
+
|
|
44
|
+
const missingDeps = await getMissingDependencies('sqlite')
|
|
45
|
+
if (missingDeps.length > 0) {
|
|
46
|
+
depsSpinner.warn(`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`)
|
|
47
|
+
const installed = await promptInstallDependencies(missingDeps[0].binary, 'sqlite')
|
|
48
|
+
if (!installed) {
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
depsSpinner.succeed('Required tools available')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if container already exists
|
|
56
|
+
while (await containerManager.exists(containerName)) {
|
|
57
|
+
console.log(chalk.yellow(` Container "${containerName}" already exists.`))
|
|
58
|
+
containerName = await promptContainerName()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Determine file path
|
|
62
|
+
const defaultPath = `./${containerName}.sqlite`
|
|
63
|
+
const absolutePath = resolve(filePath || defaultPath)
|
|
64
|
+
|
|
65
|
+
// Check if file already exists
|
|
66
|
+
if (existsSync(absolutePath)) {
|
|
67
|
+
console.error(error(`File already exists: ${absolutePath}`))
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const createSpinnerInstance = createSpinner('Creating SQLite database...')
|
|
72
|
+
createSpinnerInstance.start()
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Initialize the SQLite database file and register in registry
|
|
76
|
+
await dbEngine.initDataDir(containerName, version, { path: absolutePath })
|
|
77
|
+
createSpinnerInstance.succeed('SQLite database created')
|
|
78
|
+
} catch (err) {
|
|
79
|
+
createSpinnerInstance.fail('Failed to create SQLite database')
|
|
80
|
+
throw err
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle --from restore
|
|
84
|
+
if (restoreLocation) {
|
|
85
|
+
const config = await containerManager.getConfig(containerName)
|
|
86
|
+
if (config) {
|
|
87
|
+
const format = await dbEngine.detectBackupFormat(restoreLocation)
|
|
88
|
+
const restoreSpinner = createSpinner(`Restoring from ${format.description}...`)
|
|
89
|
+
restoreSpinner.start()
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await dbEngine.restore(config, restoreLocation)
|
|
93
|
+
restoreSpinner.succeed('Backup restored successfully')
|
|
94
|
+
} catch (err) {
|
|
95
|
+
restoreSpinner.fail('Failed to restore backup')
|
|
96
|
+
throw err
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Display success
|
|
102
|
+
console.log()
|
|
103
|
+
console.log(chalk.green(' ✓ SQLite database ready'))
|
|
104
|
+
console.log()
|
|
105
|
+
console.log(chalk.gray(' File path:'))
|
|
106
|
+
console.log(chalk.cyan(` ${absolutePath}`))
|
|
107
|
+
console.log()
|
|
108
|
+
console.log(chalk.gray(' Connection string:'))
|
|
109
|
+
console.log(chalk.cyan(` sqlite:///${absolutePath}`))
|
|
110
|
+
console.log()
|
|
111
|
+
|
|
112
|
+
// Connect if requested
|
|
113
|
+
if (connect) {
|
|
114
|
+
const config = await containerManager.getConfig(containerName)
|
|
115
|
+
if (config) {
|
|
116
|
+
console.log(chalk.gray(' Opening shell...'))
|
|
117
|
+
console.log()
|
|
118
|
+
await dbEngine.connect(config)
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.gray(' Connect with:'))
|
|
122
|
+
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
123
|
+
console.log()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
24
126
|
|
|
25
127
|
function detectLocationType(location: string): {
|
|
26
128
|
type: 'connection' | 'file' | 'not_found'
|
|
@@ -37,7 +139,16 @@ function detectLocationType(location: string): {
|
|
|
37
139
|
return { type: 'connection', inferredEngine: Engine.MySQL }
|
|
38
140
|
}
|
|
39
141
|
|
|
142
|
+
if (location.startsWith('sqlite://')) {
|
|
143
|
+
return { type: 'connection', inferredEngine: Engine.SQLite }
|
|
144
|
+
}
|
|
145
|
+
|
|
40
146
|
if (existsSync(location)) {
|
|
147
|
+
// Check if it's a SQLite file (case-insensitive)
|
|
148
|
+
const lowerLocation = location.toLowerCase()
|
|
149
|
+
if (lowerLocation.endsWith('.sqlite') || lowerLocation.endsWith('.db') || lowerLocation.endsWith('.sqlite3')) {
|
|
150
|
+
return { type: 'file', inferredEngine: Engine.SQLite }
|
|
151
|
+
}
|
|
41
152
|
return { type: 'file' }
|
|
42
153
|
}
|
|
43
154
|
|
|
@@ -47,15 +158,21 @@ function detectLocationType(location: string): {
|
|
|
47
158
|
export const createCommand = new Command('create')
|
|
48
159
|
.description('Create a new database container')
|
|
49
160
|
.argument('[name]', 'Container name')
|
|
50
|
-
.option('-e, --engine <engine>', 'Database engine (postgresql, mysql)')
|
|
161
|
+
.option('-e, --engine <engine>', 'Database engine (postgresql, mysql, sqlite)')
|
|
51
162
|
.option('-v, --version <version>', 'Database version')
|
|
52
163
|
.option('-d, --database <database>', 'Database name')
|
|
53
164
|
.option('-p, --port <port>', 'Port number')
|
|
165
|
+
.option(
|
|
166
|
+
'--path <path>',
|
|
167
|
+
'Path for SQLite database file (default: ./<name>.sqlite)',
|
|
168
|
+
)
|
|
54
169
|
.option(
|
|
55
170
|
'--max-connections <number>',
|
|
56
171
|
'Maximum number of database connections (default: 200)',
|
|
57
172
|
)
|
|
173
|
+
.option('--start', 'Start the container after creation (skip prompt)')
|
|
58
174
|
.option('--no-start', 'Do not start the container after creation')
|
|
175
|
+
.option('--connect', 'Open a shell connection after creation')
|
|
59
176
|
.option(
|
|
60
177
|
'--from <location>',
|
|
61
178
|
'Restore from a dump file or connection string after creation',
|
|
@@ -68,8 +185,10 @@ export const createCommand = new Command('create')
|
|
|
68
185
|
version?: string
|
|
69
186
|
database?: string
|
|
70
187
|
port?: string
|
|
188
|
+
path?: string
|
|
71
189
|
maxConnections?: string
|
|
72
|
-
start
|
|
190
|
+
start?: boolean
|
|
191
|
+
connect?: boolean
|
|
73
192
|
from?: string
|
|
74
193
|
},
|
|
75
194
|
) => {
|
|
@@ -135,11 +254,41 @@ export const createCommand = new Command('create')
|
|
|
135
254
|
|
|
136
255
|
database = database ?? containerName
|
|
137
256
|
|
|
257
|
+
// Validate database name to prevent SQL injection
|
|
258
|
+
if (!isValidDatabaseName(database)) {
|
|
259
|
+
console.error(
|
|
260
|
+
error(
|
|
261
|
+
'Database name must start with a letter and contain only letters, numbers, hyphens, and underscores',
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
process.exit(1)
|
|
265
|
+
}
|
|
266
|
+
|
|
138
267
|
console.log(header('Creating Database Container'))
|
|
139
268
|
console.log()
|
|
140
269
|
|
|
141
270
|
const dbEngine = getEngine(engine)
|
|
142
271
|
|
|
272
|
+
// SQLite has a simplified flow (no port, no start/stop)
|
|
273
|
+
if (engine === Engine.SQLite) {
|
|
274
|
+
await createSqliteContainer(containerName, dbEngine, version, {
|
|
275
|
+
path: options.path,
|
|
276
|
+
from: restoreLocation,
|
|
277
|
+
connect: options.connect,
|
|
278
|
+
})
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// For server databases, validate --connect with --no-start
|
|
283
|
+
if (options.connect && options.start === false) {
|
|
284
|
+
console.error(
|
|
285
|
+
error(
|
|
286
|
+
'Cannot use --no-start with --connect (connection requires running container)',
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
process.exit(1)
|
|
290
|
+
}
|
|
291
|
+
|
|
143
292
|
const depsSpinner = createSpinner('Checking required tools...')
|
|
144
293
|
depsSpinner.start()
|
|
145
294
|
|
|
@@ -272,9 +421,12 @@ export const createCommand = new Command('create')
|
|
|
272
421
|
throw err
|
|
273
422
|
}
|
|
274
423
|
|
|
275
|
-
// --from requires start, --no-start skips, otherwise ask user
|
|
424
|
+
// --from requires start, --start forces start, --no-start skips, otherwise ask user
|
|
425
|
+
// --connect implies --start for server databases
|
|
276
426
|
let shouldStart = false
|
|
277
|
-
if (restoreLocation) {
|
|
427
|
+
if (restoreLocation || options.connect) {
|
|
428
|
+
shouldStart = true
|
|
429
|
+
} else if (options.start === true) {
|
|
278
430
|
shouldStart = true
|
|
279
431
|
} else if (options.start === false) {
|
|
280
432
|
shouldStart = false
|
|
@@ -430,7 +582,7 @@ export const createCommand = new Command('create')
|
|
|
430
582
|
createDatabase: false,
|
|
431
583
|
})
|
|
432
584
|
|
|
433
|
-
if (result.code === 0
|
|
585
|
+
if (result.code === 0) {
|
|
434
586
|
restoreSpinner.succeed('Backup restored successfully')
|
|
435
587
|
} else {
|
|
436
588
|
restoreSpinner.warn('Restore completed with warnings')
|
|
@@ -461,7 +613,17 @@ export const createCommand = new Command('create')
|
|
|
461
613
|
)
|
|
462
614
|
console.log()
|
|
463
615
|
|
|
464
|
-
if (shouldStart) {
|
|
616
|
+
if (options.connect && shouldStart) {
|
|
617
|
+
// --connect flag: open shell directly
|
|
618
|
+
const copied =
|
|
619
|
+
await platformService.copyToClipboard(connectionString)
|
|
620
|
+
if (copied) {
|
|
621
|
+
console.log(chalk.gray(' Connection string copied to clipboard'))
|
|
622
|
+
}
|
|
623
|
+
console.log(chalk.gray(' Opening shell...'))
|
|
624
|
+
console.log()
|
|
625
|
+
await dbEngine.connect(finalConfig, database)
|
|
626
|
+
} else if (shouldStart) {
|
|
465
627
|
console.log(chalk.gray(' Connect with:'))
|
|
466
628
|
console.log(chalk.cyan(` spindb connect ${containerName}`))
|
|
467
629
|
|
|
@@ -470,12 +632,12 @@ export const createCommand = new Command('create')
|
|
|
470
632
|
if (copied) {
|
|
471
633
|
console.log(chalk.gray(' Connection string copied to clipboard'))
|
|
472
634
|
}
|
|
635
|
+
console.log()
|
|
473
636
|
} else {
|
|
474
637
|
console.log(chalk.gray(' Start the container:'))
|
|
475
638
|
console.log(chalk.cyan(` spindb start ${containerName}`))
|
|
639
|
+
console.log()
|
|
476
640
|
}
|
|
477
|
-
|
|
478
|
-
console.log()
|
|
479
641
|
}
|
|
480
642
|
} catch (err) {
|
|
481
643
|
const e = err as Error
|