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.
Files changed (40) hide show
  1. package/README.md +87 -7
  2. package/cli/commands/clone.ts +6 -0
  3. package/cli/commands/connect.ts +115 -14
  4. package/cli/commands/create.ts +170 -8
  5. package/cli/commands/doctor.ts +320 -0
  6. package/cli/commands/edit.ts +209 -9
  7. package/cli/commands/engines.ts +34 -3
  8. package/cli/commands/info.ts +81 -26
  9. package/cli/commands/list.ts +64 -9
  10. package/cli/commands/logs.ts +9 -3
  11. package/cli/commands/menu/backup-handlers.ts +52 -21
  12. package/cli/commands/menu/container-handlers.ts +433 -127
  13. package/cli/commands/menu/engine-handlers.ts +128 -4
  14. package/cli/commands/menu/index.ts +5 -1
  15. package/cli/commands/menu/shell-handlers.ts +105 -21
  16. package/cli/commands/menu/sql-handlers.ts +16 -4
  17. package/cli/commands/menu/update-handlers.ts +278 -0
  18. package/cli/commands/restore.ts +83 -23
  19. package/cli/commands/run.ts +27 -11
  20. package/cli/commands/url.ts +17 -9
  21. package/cli/constants.ts +1 -0
  22. package/cli/helpers.ts +41 -1
  23. package/cli/index.ts +2 -0
  24. package/cli/ui/prompts.ts +148 -7
  25. package/config/engine-defaults.ts +14 -0
  26. package/config/os-dependencies.ts +66 -0
  27. package/config/paths.ts +8 -0
  28. package/core/container-manager.ts +191 -32
  29. package/core/dependency-manager.ts +18 -0
  30. package/core/error-handler.ts +31 -0
  31. package/core/port-manager.ts +2 -0
  32. package/core/process-manager.ts +25 -3
  33. package/engines/index.ts +4 -0
  34. package/engines/mysql/backup.ts +53 -36
  35. package/engines/mysql/index.ts +48 -5
  36. package/engines/postgresql/index.ts +6 -0
  37. package/engines/sqlite/index.ts +606 -0
  38. package/engines/sqlite/registry.ts +185 -0
  39. package/package.json +1 -1
  40. 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 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,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 (planned) | `sqlite3` | `litecli` | `usql` |
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
@@ -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
  )
@@ -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'))
@@ -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: boolean
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 || !result.stderr) {
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