spindb 0.8.1 → 0.9.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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  **Local databases without the Docker baggage.**
8
8
 
9
- Spin up PostgreSQL and MySQL instances for local development. No Docker daemon, no container networking, no volume mounts. Just databases running on localhost, ready in seconds.
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,14 @@ 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
+ # SQLite with custom path
231
+ spindb create mydb --engine sqlite --path ./data/app.sqlite
202
232
  ```
203
233
 
204
234
  Create and restore in one command:
@@ -213,10 +243,11 @@ spindb create mydb --from "postgresql://user:pass@host:5432/production"
213
243
 
214
244
  | Option | Description |
215
245
  |--------|-------------|
216
- | `--engine`, `-e` | Database engine (`postgresql`, `mysql`) |
246
+ | `--engine`, `-e` | Database engine (`postgresql`, `mysql`, `sqlite`) |
217
247
  | `--version`, `-v` | Engine version |
218
- | `--port`, `-p` | Port number |
248
+ | `--port`, `-p` | Port number (not applicable for SQLite) |
219
249
  | `--database`, `-d` | Primary database name |
250
+ | `--path` | File path for SQLite databases |
220
251
  | `--max-connections` | Maximum database connections (default: 200) |
221
252
  | `--from` | Restore from backup file or connection string |
222
253
  | `--no-start` | Create without starting |
@@ -333,11 +364,12 @@ spindb clone source-db new-db
333
364
  spindb start new-db
334
365
  ```
335
366
 
336
- #### `edit` - Rename, change port, or edit database config
367
+ #### `edit` - Rename, change port, relocate, or edit database config
337
368
 
338
369
  ```bash
339
370
  spindb edit mydb --name newname # Must be stopped
340
371
  spindb edit mydb --port 5433
372
+ spindb edit mydb --relocate ~/new/path # Move SQLite database file
341
373
  spindb edit mydb --set-config max_connections=300 # PostgreSQL config
342
374
  spindb edit mydb # Interactive mode
343
375
  ```
@@ -405,6 +437,43 @@ spindb version --check # Check for updates
405
437
  spindb self-update
406
438
  ```
407
439
 
440
+ #### `doctor` - System health check
441
+
442
+ ```bash
443
+ spindb doctor # Interactive health check
444
+ spindb doctor --json # JSON output for scripting
445
+ ```
446
+
447
+ Checks performed:
448
+ - Configuration file validity and binary cache freshness
449
+ - Container status across all engines
450
+ - SQLite registry for orphaned entries (files deleted outside SpinDB)
451
+ - Database tool availability
452
+
453
+ Example output:
454
+
455
+ ```
456
+ SpinDB Health Check
457
+ ═══════════════════
458
+
459
+ ✓ Configuration
460
+ └─ Configuration valid, 12 tools cached
461
+
462
+ ✓ Containers
463
+ └─ 4 container(s)
464
+ postgresql: 2 running, 0 stopped
465
+ mysql: 0 running, 1 stopped
466
+ sqlite: 1 exist, 0 missing
467
+
468
+ ⚠ SQLite Registry
469
+ └─ 1 orphaned entry found
470
+ "old-project" → /path/to/missing.sqlite
471
+
472
+ ? What would you like to do?
473
+ ❯ Remove orphaned entries from registry
474
+ Skip (do nothing)
475
+ ```
476
+
408
477
  ---
409
478
 
410
479
  ## Enhanced CLI Tools
@@ -415,7 +484,7 @@ SpinDB supports enhanced database shells that provide features like auto-complet
415
484
  |--------|----------|----------|-----------|
416
485
  | PostgreSQL | `psql` | `pgcli` | `usql` |
417
486
  | MySQL | `mysql` | `mycli` | `usql` |
418
- | SQLite (planned) | `sqlite3` | `litecli` | `usql` |
487
+ | SQLite | `sqlite3` | `litecli` | `usql` |
419
488
  | Redis (planned) | `redis-cli` | `iredis` | - |
420
489
  | MongoDB (planned) | `mongosh` | - | - |
421
490
 
@@ -456,8 +525,13 @@ spindb connect mydb --install-tui # usql
456
525
  │ ├── container.json
457
526
  │ ├── data/
458
527
  │ └── mysql.log
528
+ ├── sqlite-registry.json # Tracks SQLite file locations
459
529
  ├── logs/ # Error logs
460
530
  └── config.json # Tool paths cache
531
+
532
+ # SQLite databases are stored in project directories, not ~/.spindb/
533
+ ./myproject/
534
+ └── mydb.sqlite # Created with: spindb create mydb -e sqlite
461
535
  ```
462
536
 
463
537
  ### How Data Persists
@@ -521,7 +595,6 @@ See [TODO.md](TODO.md) for the full roadmap.
521
595
  - Secrets management (macOS Keychain)
522
596
 
523
597
  ### v1.2 - Additional Engines
524
- - SQLite (file-based, no server)
525
598
  - Redis (in-memory key-value)
526
599
  - MongoDB (document database)
527
600
  - MariaDB as standalone engine
@@ -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
- const running = containers.filter((c) => c.status === 'running')
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 (running.length === 0) {
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
- running,
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
- const running = await processManager.isRunning(containerName, {
96
- engine: engineName,
97
- })
98
- if (!running) {
99
- console.error(
100
- error(
101
- `Container "${containerName}" is not running. Start it first.`,
102
- ),
103
- )
104
- process.exit(1)
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 (usePgcli) {
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'))
@@ -21,6 +21,96 @@ import { platformService } from '../../core/platform-service'
21
21
  import { startWithRetry } from '../../core/start-with-retry'
22
22
  import { TransactionManager } from '../../core/transaction-manager'
23
23
  import { Engine } from '../../types'
24
+ import type { BaseEngine } from '../../engines/base-engine'
25
+ import { resolve } from 'path'
26
+
27
+ /**
28
+ * Simplified SQLite container creation flow
29
+ * SQLite is file-based, so no port, start/stop, or server management needed
30
+ */
31
+ async function createSqliteContainer(
32
+ containerName: string,
33
+ dbEngine: BaseEngine,
34
+ version: string,
35
+ options: { path?: string; from?: string | null },
36
+ ): Promise<void> {
37
+ const { path: filePath, from: restoreLocation } = options
38
+
39
+ // Check dependencies
40
+ const depsSpinner = createSpinner('Checking required tools...')
41
+ depsSpinner.start()
42
+
43
+ const missingDeps = await getMissingDependencies('sqlite')
44
+ if (missingDeps.length > 0) {
45
+ depsSpinner.warn(`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`)
46
+ const installed = await promptInstallDependencies(missingDeps[0].binary, 'sqlite')
47
+ if (!installed) {
48
+ process.exit(1)
49
+ }
50
+ } else {
51
+ depsSpinner.succeed('Required tools available')
52
+ }
53
+
54
+ // Check if container already exists
55
+ while (await containerManager.exists(containerName)) {
56
+ console.log(chalk.yellow(` Container "${containerName}" already exists.`))
57
+ containerName = await promptContainerName()
58
+ }
59
+
60
+ // Determine file path
61
+ const defaultPath = `./${containerName}.sqlite`
62
+ const absolutePath = resolve(filePath || defaultPath)
63
+
64
+ // Check if file already exists
65
+ if (existsSync(absolutePath)) {
66
+ console.error(error(`File already exists: ${absolutePath}`))
67
+ process.exit(1)
68
+ }
69
+
70
+ const createSpinnerInstance = createSpinner('Creating SQLite database...')
71
+ createSpinnerInstance.start()
72
+
73
+ try {
74
+ // Initialize the SQLite database file and register in registry
75
+ await dbEngine.initDataDir(containerName, version, { path: absolutePath })
76
+ createSpinnerInstance.succeed('SQLite database created')
77
+ } catch (err) {
78
+ createSpinnerInstance.fail('Failed to create SQLite database')
79
+ throw err
80
+ }
81
+
82
+ // Handle --from restore
83
+ if (restoreLocation) {
84
+ const config = await containerManager.getConfig(containerName)
85
+ if (config) {
86
+ const format = await dbEngine.detectBackupFormat(restoreLocation)
87
+ const restoreSpinner = createSpinner(`Restoring from ${format.description}...`)
88
+ restoreSpinner.start()
89
+
90
+ try {
91
+ await dbEngine.restore(config, restoreLocation)
92
+ restoreSpinner.succeed('Backup restored successfully')
93
+ } catch (err) {
94
+ restoreSpinner.fail('Failed to restore backup')
95
+ throw err
96
+ }
97
+ }
98
+ }
99
+
100
+ // Display success
101
+ console.log()
102
+ console.log(chalk.green(' ✓ SQLite database ready'))
103
+ console.log()
104
+ console.log(chalk.gray(' File path:'))
105
+ console.log(chalk.cyan(` ${absolutePath}`))
106
+ console.log()
107
+ console.log(chalk.gray(' Connection string:'))
108
+ console.log(chalk.cyan(` sqlite:///${absolutePath}`))
109
+ console.log()
110
+ console.log(chalk.gray(' Connect with:'))
111
+ console.log(chalk.cyan(` spindb connect ${containerName}`))
112
+ console.log()
113
+ }
24
114
 
25
115
  function detectLocationType(location: string): {
26
116
  type: 'connection' | 'file' | 'not_found'
@@ -37,7 +127,15 @@ function detectLocationType(location: string): {
37
127
  return { type: 'connection', inferredEngine: Engine.MySQL }
38
128
  }
39
129
 
130
+ if (location.startsWith('sqlite://')) {
131
+ return { type: 'connection', inferredEngine: Engine.SQLite }
132
+ }
133
+
40
134
  if (existsSync(location)) {
135
+ // Check if it's a SQLite file
136
+ if (location.endsWith('.sqlite') || location.endsWith('.db') || location.endsWith('.sqlite3')) {
137
+ return { type: 'file', inferredEngine: Engine.SQLite }
138
+ }
41
139
  return { type: 'file' }
42
140
  }
43
141
 
@@ -47,10 +145,14 @@ function detectLocationType(location: string): {
47
145
  export const createCommand = new Command('create')
48
146
  .description('Create a new database container')
49
147
  .argument('[name]', 'Container name')
50
- .option('-e, --engine <engine>', 'Database engine (postgresql, mysql)')
148
+ .option('-e, --engine <engine>', 'Database engine (postgresql, mysql, sqlite)')
51
149
  .option('-v, --version <version>', 'Database version')
52
150
  .option('-d, --database <database>', 'Database name')
53
151
  .option('-p, --port <port>', 'Port number')
152
+ .option(
153
+ '--path <path>',
154
+ 'Path for SQLite database file (default: ./<name>.sqlite)',
155
+ )
54
156
  .option(
55
157
  '--max-connections <number>',
56
158
  'Maximum number of database connections (default: 200)',
@@ -68,6 +170,7 @@ export const createCommand = new Command('create')
68
170
  version?: string
69
171
  database?: string
70
172
  port?: string
173
+ path?: string
71
174
  maxConnections?: string
72
175
  start: boolean
73
176
  from?: string
@@ -140,6 +243,15 @@ export const createCommand = new Command('create')
140
243
 
141
244
  const dbEngine = getEngine(engine)
142
245
 
246
+ // SQLite has a simplified flow (no port, no start/stop)
247
+ if (engine === Engine.SQLite) {
248
+ await createSqliteContainer(containerName, dbEngine, version, {
249
+ path: options.path,
250
+ from: restoreLocation,
251
+ })
252
+ return
253
+ }
254
+
143
255
  const depsSpinner = createSpinner('Checking required tools...')
144
256
  depsSpinner.start()
145
257