spindb 0.13.4 → 0.15.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 CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  **The first npm CLI for running local databases without Docker.**
9
9
 
10
- Spin up PostgreSQL, MySQL, SQLite, MongoDB, and Redis instances for local development. No Docker daemon, no container networking, no volume mounts. Just databases running on localhost, ready in seconds.
10
+ Spin up PostgreSQL, MySQL, MariaDB, SQLite, MongoDB, and Redis instances for local development. No Docker daemon, no container networking, no volume mounts. Just databases running on localhost, ready in seconds.
11
11
 
12
12
  ---
13
13
 
@@ -139,20 +139,40 @@ You'll get an interactive menu with arrow-key navigation:
139
139
  | Versions | 14, 15, 16, 17, 18 |
140
140
  | Default port | 5432 |
141
141
  | Default user | `postgres` |
142
- | Binary source | [zonky.io](https://github.com/zonkyio/embedded-postgres-binaries) (macOS/Linux), [EDB](https://www.enterprisedb.com/) (Windows) |
142
+ | Binary source | [hostdb](https://github.com/robertjbass/hostdb) (macOS/Linux), [EDB](https://www.enterprisedb.com/) (Windows) |
143
143
 
144
144
  SpinDB downloads PostgreSQL server binaries automatically:
145
- - **macOS/Linux:** Pre-compiled binaries from the zonky.io project, hosted on Maven Central
145
+ - **macOS/Linux:** Pre-compiled binaries from [hostdb](https://github.com/robertjbass/hostdb) on GitHub Releases
146
146
  - **Windows:** Official binaries from EnterpriseDB (EDB)
147
147
 
148
- **Why download binaries instead of using system PostgreSQL?** The [zonky.io project](https://github.com/zonkyio/embedded-postgres-binaries) provides pre-configured, portable PostgreSQL binaries—just extract and run. This lets you run PostgreSQL 14 for one project and 18 for another, side-by-side, without conflicts. No other database engine has an equivalent portable distribution.
148
+ **Why download binaries instead of using system PostgreSQL?** The hostdb project provides pre-configured, portable PostgreSQL binaries—just extract and run. This lets you run PostgreSQL 14 for one project and 18 for another, side-by-side, without conflicts.
149
149
 
150
- **Client tools required:** You still need `psql`, `pg_dump`, and `pg_restore` installed on your system for some operations (connecting, backups, restores). SpinDB can install these for you:
150
+ **Client tools included:** PostgreSQL binaries include `psql`, `pg_dump`, and `pg_restore` for all operations.
151
+
152
+ #### MariaDB
153
+
154
+ | | |
155
+ |---|---|
156
+ | Versions | 10.11, 11.4, 11.8 |
157
+ | Default port | 3307 |
158
+ | Default user | `root` |
159
+ | Binary source | [hostdb](https://github.com/robertjbass/hostdb) |
160
+
161
+ SpinDB downloads MariaDB server binaries automatically from [hostdb](https://github.com/robertjbass/hostdb) on GitHub Releases—just like PostgreSQL. This provides multi-version support and works across all platforms.
151
162
 
152
163
  ```bash
153
- spindb deps install --engine postgresql
164
+ # Create a MariaDB container
165
+ spindb create mydb --engine mariadb
166
+
167
+ # Or using the alias
168
+ spindb create mydb -e maria
169
+
170
+ # Check what's available
171
+ spindb deps check --engine mariadb
154
172
  ```
155
173
 
174
+ MariaDB is MySQL-compatible, so most MySQL tools and clients work seamlessly. If you need MySQL-specific features, use the `mysql` engine instead.
175
+
156
176
  #### MySQL
157
177
 
158
178
  | | |
@@ -181,7 +201,7 @@ winget install Oracle.MySQL
181
201
  spindb deps check --engine mysql
182
202
  ```
183
203
 
184
- **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.
204
+ **Linux users:** MariaDB is also available as a standalone engine with downloadable binaries. Use `spindb create mydb --engine mariadb` for the dedicated MariaDB engine.
185
205
 
186
206
  #### SQLite
187
207
 
@@ -302,6 +322,7 @@ spindb connect myredis --iredis
302
322
 
303
323
  ```bash
304
324
  spindb create mydb # PostgreSQL (default)
325
+ spindb create mydb --engine mariadb # MariaDB
305
326
  spindb create mydb --engine mysql # MySQL
306
327
  spindb create mydb --engine sqlite # SQLite (file-based)
307
328
  spindb create mydb --db-version 16 # Specific PostgreSQL version
@@ -328,8 +349,8 @@ spindb create mydb --from "postgresql://user:pass@host:5432/production"
328
349
 
329
350
  | Option | Description |
330
351
  |--------|-------------|
331
- | `--engine`, `-e` | Database engine (`postgresql`, `mysql`, `sqlite`, `mongodb`, `redis`) |
332
- | `--db-version` | Engine version (e.g., 17 for PostgreSQL, 8 for Redis) |
352
+ | `--engine`, `-e` | Database engine (`postgresql`, `mariadb`, `mysql`, `sqlite`, `mongodb`, `redis`) |
353
+ | `--db-version` | Engine version (e.g., 17 for PostgreSQL, 11.8 for MariaDB, 8 for Redis) |
333
354
  | `--port`, `-p` | Port number (not applicable for SQLite) |
334
355
  | `--database`, `-d` | Primary database name (Redis uses 0-15) |
335
356
  | `--path` | File path for SQLite databases |
@@ -442,6 +463,7 @@ spindb backup mydb --dump
442
463
 
443
464
  Format by engine:
444
465
  - PostgreSQL: `.sql` (plain SQL) / `.dump` (pg_dump custom)
466
+ - MariaDB: `.sql` (plain SQL) / `.sql.gz` (compressed SQL)
445
467
  - MySQL: `.sql` (plain SQL) / `.sql.gz` (compressed SQL)
446
468
  - SQLite: `.sql` (plain SQL) / `.sqlite` (binary copy)
447
469
  - MongoDB: `.bson` (BSON dump) / `.archive` (compressed archive)
@@ -532,6 +554,18 @@ Each engine has specific backup formats and restore behaviors:
532
554
 
533
555
  </details>
534
556
 
557
+ <details>
558
+ <summary>MariaDB</summary>
559
+
560
+ | Format | Extension | Tool | Notes |
561
+ |--------|-----------|------|-------|
562
+ | SQL | `.sql` | mariadb-dump | Plain text SQL |
563
+ | Compressed | `.sql.gz` | mariadb-dump + gzip | Gzip compressed SQL |
564
+
565
+ **Restore behavior:** Creates new database or replaces existing. Pipes to `mariadb` client.
566
+
567
+ </details>
568
+
535
569
  <details>
536
570
  <summary>MySQL</summary>
537
571
 
@@ -734,6 +768,7 @@ SpinDB supports enhanced database shells that provide features like auto-complet
734
768
  | Engine | Standard | Enhanced | Universal |
735
769
  |--------|----------|----------|-----------|
736
770
  | PostgreSQL | `psql` | `pgcli` | `usql` |
771
+ | MariaDB | `mariadb` | `mycli` | `usql` |
737
772
  | MySQL | `mysql` | `mycli` | `usql` |
738
773
  | SQLite | `sqlite3` | `litecli` | `usql` |
739
774
  | MongoDB | `mongosh` | - | `usql` |
@@ -824,12 +859,13 @@ Each database engine has its own persistence mechanism:
824
859
  | Engine | Mechanism | Durability |
825
860
  |--------|-----------|------------|
826
861
  | PostgreSQL | Write-Ahead Logging (WAL) | Every commit is immediately durable |
862
+ | MariaDB | InnoDB transaction logs | Every commit is immediately durable |
827
863
  | MySQL | InnoDB transaction logs | Every commit is immediately durable |
828
864
  | SQLite | File-based transactions | Every commit is immediately durable |
829
865
  | MongoDB | WiredTiger with journaling | Writes journaled before acknowledged |
830
866
  | Redis | RDB snapshots | Periodic snapshots (see below) |
831
867
 
832
- **PostgreSQL, MySQL, MongoDB:** These engines use transaction logs or journaling. Every committed write is guaranteed to survive a crash or unexpected shutdown.
868
+ **PostgreSQL, MariaDB, MySQL, MongoDB:** These engines use transaction logs or journaling. Every committed write is guaranteed to survive a crash or unexpected shutdown.
833
869
 
834
870
  **SQLite:** As a file-based database, SQLite writes directly to disk on each commit. No server process means no risk of losing in-flight data.
835
871
 
@@ -843,41 +879,42 @@ This means Redis may lose up to ~60 seconds of writes on an unexpected crash. Fo
843
879
  ### Binary Sources
844
880
 
845
881
  **PostgreSQL:** Server binaries are downloaded automatically:
846
- - **macOS/Linux:** From [zonky.io/embedded-postgres-binaries](https://github.com/zonkyio/embedded-postgres-binaries), hosted on Maven Central
847
- - **Windows:** From [EnterpriseDB (EDB)](https://www.enterprisedb.com/download-postgresql-binaries), official PostgreSQL distributions
882
+ - **All platforms (macOS/Linux/Windows for macOS/Linux via hostdb, Windows via EDB):** From [hostdb](https://github.com/robertjbass/hostdb) on GitHub Releases (macOS/Linux) or [EnterpriseDB (EDB)](https://www.enterprisedb.com/download-postgresql-binaries) (Windows)
883
+
884
+ **MariaDB:** Server binaries are downloaded automatically from [hostdb](https://github.com/robertjbass/hostdb) on GitHub Releases for all platforms.
848
885
 
849
886
  **MySQL/MongoDB/Redis:** Uses your system installation. SpinDB detects binaries from Homebrew (macOS), apt/pacman (Linux), or Chocolatey/winget/Scoop (Windows).
850
887
 
851
- ### Why Precompiled Binaries for PostgreSQL, but System Installs for Others?
888
+ ### Why Precompiled Binaries for PostgreSQL and MariaDB, but System Installs for Others?
852
889
 
853
890
  This isn't a preference—it's a practical reality of what's available.
854
891
 
855
- **PostgreSQL has an excellent embedded binary distribution.** The [zonky.io](https://github.com/zonkyio/embedded-postgres-binaries) project maintains minimal, self-contained PostgreSQL server binaries specifically designed for embedding:
892
+ **PostgreSQL and MariaDB have excellent embedded binary distributions.** The [hostdb](https://github.com/robertjbass/hostdb) project provides pre-compiled, portable database binaries:
856
893
 
857
894
  - Cross-platform (macOS Intel/ARM, Linux x64/ARM, Windows)
858
- - Hosted on Maven Central (highly reliable CDN)
859
- - ~45 MB per version
860
- - Actively maintained with new PostgreSQL releases
895
+ - Hosted on GitHub Releases (highly reliable CDN)
896
+ - ~45-100 MB per version
897
+ - Actively maintained with new database releases
861
898
 
862
- This makes multi-version support trivial: need PostgreSQL 14 for a legacy project and 18 for a new one? SpinDB downloads both, and they run side-by-side without conflicts.
899
+ This makes multi-version support trivial: need PostgreSQL 14 for a legacy project and 18 for a new one? Need MariaDB 11.8? SpinDB downloads them all, and they run side-by-side without conflicts.
863
900
 
864
- **No equivalent exists for MySQL, MongoDB, or Redis.** None of these databases have a comparable embedded binary project:
901
+ **No equivalent exists for MySQL, MongoDB, or Redis (yet).** None of these databases have a comparable embedded binary distribution:
865
902
 
866
- - **MySQL:** Oracle distributes MySQL as large installers with system dependencies, not embeddable binaries. There's no "zonky.io for MySQL."
903
+ - **MySQL:** Oracle distributes MySQL as large installers with system dependencies, not embeddable binaries.
867
904
  - **MongoDB:** Server binaries are several hundred MB and aren't designed for portable distribution.
868
905
  - **Redis:** While Redis is small (~6-12 MB), there's no official portable distribution. Community Windows ports exist, but macOS/Linux rely on system packages.
869
906
 
870
907
  For these databases, system packages (Homebrew, apt, choco) are the most reliable option. They handle dependencies, platform quirks, and security updates. SpinDB simply orchestrates what's already installed.
871
908
 
872
- **Does this limit multi-version support?** Yes, for MySQL/MongoDB/Redis you get whatever version your package manager provides. In practice, this is rarely a problem—developers seldom need multiple versions of these databases simultaneously. If zonky.io-style distributions emerged for other databases, SpinDB could adopt them.
909
+ **Does this limit multi-version support?** Yes, for MySQL/MongoDB/Redis you get whatever version your package manager provides. In practice, this is rarely a problem—developers seldom need multiple versions of these databases simultaneously. As hostdb expands to support more databases, SpinDB will adopt them for multi-version support.
873
910
 
874
911
  ---
875
912
 
876
913
  ## Limitations
877
914
 
878
- - **Client tools required** - `psql`, `mysql`, `mongosh`, and `redis-cli` must be installed separately for some operations (connecting, backups, restores)
915
+ - **Client tools required** - `mysql`, `mongosh`, and `redis-cli` must be installed separately for some operations (connecting, backups, restores) for system-installed engines
879
916
  - **Local only** - Databases bind to `127.0.0.1`; remote connections planned for v1.1
880
- - **Single version for MySQL/MongoDB/Redis** - Unlike PostgreSQL, MySQL, MongoDB, and Redis use system installations, so you're limited to one version per machine (see [Why Precompiled Binaries for PostgreSQL?](#why-precompiled-binaries-for-postgresql-but-system-installs-for-others))
917
+ - **Single version for MySQL/MongoDB/Redis** - Unlike PostgreSQL and MariaDB, MySQL, MongoDB, and Redis use system installations, so you're limited to one version per machine (see [Why Precompiled Binaries for PostgreSQL and MariaDB?](#why-precompiled-binaries-for-postgresql-and-mariadb-but-system-installs-for-others))
881
918
  - **Redis remote dump not supported** - Redis doesn't support creating containers from remote connection strings. Use backup/restore for data migration.
882
919
 
883
920
  ---
@@ -892,7 +929,6 @@ See [TODO.md](TODO.md) for the full roadmap.
892
929
  - Secrets management (macOS Keychain)
893
930
 
894
931
  ### v1.2 - Additional Engines
895
- - MariaDB as standalone engine
896
932
  - CockroachDB (distributed SQL)
897
933
 
898
934
  ### v1.3 - Advanced Features
@@ -900,6 +936,9 @@ See [TODO.md](TODO.md) for the full roadmap.
900
936
  - Scheduled backups
901
937
  - Import from Docker
902
938
 
939
+ ### Future Infrastructure
940
+ - **hostdb npm package**: Available database versions will be published as an npm package from [hostdb](https://github.com/robertjbass/hostdb) and imported into SpinDB, eliminating the need to manually sync version-maps.ts with releases.json
941
+
903
942
  ### Possible Future Engines
904
943
 
905
944
  These engines are under consideration but not yet on the roadmap. Community interest and feasibility will determine priority:
@@ -3,6 +3,7 @@ import chalk from 'chalk'
3
3
  import { rm } from 'fs/promises'
4
4
  import inquirer from 'inquirer'
5
5
  import { containerManager } from '../../core/container-manager'
6
+ import { processManager } from '../../core/process-manager'
6
7
  import { getEngine } from '../../engines'
7
8
  import { binaryManager } from '../../core/binary-manager'
8
9
  import { paths } from '../../config/paths'
@@ -281,30 +282,46 @@ async function deleteEngine(
281
282
  process.exit(1)
282
283
  }
283
284
 
284
- // Check if any containers are using this engine version
285
+ // Check if any containers are using this engine version (for warning only)
285
286
  const containers = await containerManager.list()
286
287
  const usingContainers = containers.filter(
287
288
  (c) => c.engine === engineName && c.version === engineVersion,
288
289
  )
289
290
 
290
- if (usingContainers.length > 0) {
291
- console.error(
292
- uiError(
293
- `Cannot delete: ${usingContainers.length} container(s) are using ${engineName} ${engineVersion}`,
294
- ),
295
- )
296
- console.log(
297
- chalk.gray(
298
- ` Containers: ${usingContainers.map((c) => c.name).join(', ')}`,
299
- ),
300
- )
301
- console.log()
302
- console.log(chalk.gray(' Delete these containers first, then try again.'))
303
- process.exit(1)
304
- }
291
+ // Check for running containers using this engine
292
+ const runningContainers = usingContainers.filter((c) => c.status === 'running')
305
293
 
306
- // Confirm deletion
294
+ // Confirm deletion (warn about containers)
307
295
  if (!options.yes) {
296
+ if (usingContainers.length > 0) {
297
+ const runningCount = runningContainers.length
298
+ const stoppedCount = usingContainers.length - runningCount
299
+
300
+ if (runningCount > 0) {
301
+ console.log(
302
+ uiWarning(
303
+ `${runningCount} running container(s) will be stopped: ${runningContainers.map((c) => c.name).join(', ')}`,
304
+ ),
305
+ )
306
+ }
307
+ if (stoppedCount > 0) {
308
+ const stoppedContainers = usingContainers.filter(
309
+ (c) => c.status !== 'running',
310
+ )
311
+ console.log(
312
+ chalk.gray(
313
+ ` ${stoppedCount} stopped container(s) will be orphaned: ${stoppedContainers.map((c) => c.name).join(', ')}`,
314
+ ),
315
+ )
316
+ }
317
+ console.log(
318
+ chalk.gray(
319
+ ' You can re-download the engine later to use these containers.',
320
+ ),
321
+ )
322
+ console.log()
323
+ }
324
+
308
325
  const confirmed = await promptConfirm(
309
326
  `Delete ${engineName} ${engineVersion}? This cannot be undone.`,
310
327
  false,
@@ -316,6 +333,74 @@ async function deleteEngine(
316
333
  }
317
334
  }
318
335
 
336
+ // Stop any running containers first (while we still have the binary)
337
+ if (runningContainers.length > 0) {
338
+ const stopSpinner = createSpinner(
339
+ `Stopping ${runningContainers.length} running container(s)...`,
340
+ )
341
+ stopSpinner.start()
342
+
343
+ const engine = getEngine(Engine.PostgreSQL)
344
+ const failedToStop: string[] = []
345
+
346
+ for (const container of runningContainers) {
347
+ stopSpinner.text = `Stopping ${container.name}...`
348
+ try {
349
+ await engine.stop(container)
350
+ await containerManager.updateConfig(container.name, {
351
+ status: 'stopped',
352
+ })
353
+ } catch (error) {
354
+ // Log the original failure before attempting fallback
355
+ const err = error as Error
356
+ console.error(
357
+ chalk.gray(
358
+ ` Failed to stop ${container.name} via engine.stop: ${err.message}`,
359
+ ),
360
+ )
361
+ // Try fallback kill
362
+ const killed = await processManager.killProcess(container.name, {
363
+ engine: container.engine,
364
+ })
365
+ if (killed) {
366
+ await containerManager.updateConfig(container.name, {
367
+ status: 'stopped',
368
+ })
369
+ } else {
370
+ failedToStop.push(container.name)
371
+ }
372
+ }
373
+ }
374
+
375
+ if (failedToStop.length > 0) {
376
+ stopSpinner.warn(
377
+ `Could not stop ${failedToStop.length} container(s): ${failedToStop.join(', ')}`,
378
+ )
379
+ console.log(
380
+ chalk.yellow(
381
+ ' These containers may still be running. Deleting the engine could leave them in a broken state.',
382
+ ),
383
+ )
384
+
385
+ if (!options.yes) {
386
+ const continueAnyway = await promptConfirm(
387
+ 'Continue with engine deletion anyway?',
388
+ false,
389
+ )
390
+ if (!continueAnyway) {
391
+ console.log(uiWarning('Deletion cancelled'))
392
+ return
393
+ }
394
+ } else {
395
+ console.log(chalk.yellow(' Proceeding with deletion (--yes specified)'))
396
+ }
397
+ } else {
398
+ stopSpinner.succeed(
399
+ `Stopped ${runningContainers.length} container(s)`,
400
+ )
401
+ }
402
+ }
403
+
319
404
  // Delete the engine
320
405
  const spinner = createSpinner(`Deleting ${engineName} ${engineVersion}...`)
321
406
  spinner.start()
@@ -11,6 +11,7 @@ import { getEngineIcon, ENGINE_ICONS } from '../../constants'
11
11
  import {
12
12
  getInstalledEngines,
13
13
  type InstalledPostgresEngine,
14
+ type InstalledMariadbEngine,
14
15
  type InstalledMysqlEngine,
15
16
  type InstalledSqliteEngine,
16
17
  type InstalledMongodbEngine,
@@ -54,6 +55,9 @@ export async function handleEngines(): Promise<void> {
54
55
  const pgEngines = engines.filter(
55
56
  (e): e is InstalledPostgresEngine => e.engine === 'postgresql',
56
57
  )
58
+ const mariadbEngines = engines.filter(
59
+ (e): e is InstalledMariadbEngine => e.engine === 'mariadb',
60
+ )
57
61
  const mysqlEngines = engines.filter(
58
62
  (e): e is InstalledMysqlEngine => e.engine === 'mysql',
59
63
  )
@@ -68,6 +72,7 @@ export async function handleEngines(): Promise<void> {
68
72
  )
69
73
 
70
74
  const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
75
+ const totalMariadbSize = mariadbEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
71
76
 
72
77
  const COL_ENGINE = 14
73
78
  const COL_VERSION = 12
@@ -97,6 +102,20 @@ export async function handleEngines(): Promise<void> {
97
102
  )
98
103
  }
99
104
 
105
+ for (const engine of mariadbEngines) {
106
+ const icon = getEngineIcon(engine.engine)
107
+ const platformInfo = `${engine.platform}-${engine.arch}`
108
+ const engineDisplay = `${icon} ${engine.engine}`
109
+
110
+ console.log(
111
+ chalk.gray(' ') +
112
+ chalk.cyan(padToWidth(engineDisplay, COL_ENGINE)) +
113
+ chalk.yellow(engine.version.padEnd(COL_VERSION)) +
114
+ chalk.gray(platformInfo.padEnd(COL_SOURCE)) +
115
+ chalk.white(formatBytes(engine.sizeBytes)),
116
+ )
117
+ }
118
+
100
119
  for (const mysqlEngine of mysqlEngines) {
101
120
  const icon = ENGINE_ICONS.mysql
102
121
  const displayName = mysqlEngine.isMariaDB ? 'mariadb' : 'mysql'
@@ -113,8 +132,7 @@ export async function handleEngines(): Promise<void> {
113
132
 
114
133
  if (sqliteEngine) {
115
134
  const icon = ENGINE_ICONS.sqlite
116
- // double space to align with other engines
117
- const engineDisplay = `${icon} sqlite`
135
+ const engineDisplay = `${icon} sqlite`
118
136
 
119
137
  console.log(
120
138
  chalk.gray(' ') +
@@ -161,6 +179,13 @@ export async function handleEngines(): Promise<void> {
161
179
  ),
162
180
  )
163
181
  }
182
+ if (mariadbEngines.length > 0) {
183
+ console.log(
184
+ chalk.gray(
185
+ ` MariaDB: ${mariadbEngines.length} version(s), ${formatBytes(totalMariadbSize)}`,
186
+ ),
187
+ )
188
+ }
164
189
  if (mysqlEngines.length > 0) {
165
190
  const versionCount = mysqlEngines.length
166
191
  const versionText = versionCount === 1 ? 'version' : 'versions'
@@ -198,6 +223,13 @@ export async function handleEngines(): Promise<void> {
198
223
  })
199
224
  }
200
225
 
226
+ for (const e of mariadbEngines) {
227
+ choices.push({
228
+ name: `${chalk.red('✕')} Delete ${e.engine} ${e.version} ${chalk.gray(`(${formatBytes(e.sizeBytes)})`)}`,
229
+ value: `delete:${e.path}:${e.engine}:${e.version}`,
230
+ })
231
+ }
232
+
201
233
  for (const mysqlEngine of mysqlEngines) {
202
234
  const displayName = mysqlEngine.isMariaDB ? 'MariaDB' : 'MySQL'
203
235
  // Show formula name if it's a versioned install
@@ -21,6 +21,7 @@ import {
21
21
  getIredisManualInstructions,
22
22
  } from '../../../core/dependency-manager'
23
23
  import { platformService } from '../../../core/platform-service'
24
+ import { configManager } from '../../../core/config-manager'
24
25
  import { getEngine } from '../../../engines'
25
26
  import { createSpinner } from '../../ui/spinner'
26
27
  import { uiError, uiWarning, uiInfo, uiSuccess } from '../../ui/theme'
@@ -118,6 +119,12 @@ export async function handleOpenShell(containerName: string): Promise<void> {
118
119
  engineSpecificInstalled = mycliInstalled
119
120
  engineSpecificValue = 'mycli'
120
121
  engineSpecificInstallValue = 'install-mycli'
122
+ } else if (config.engine === 'mariadb') {
123
+ defaultShellName = 'mariadb'
124
+ engineSpecificCli = 'mycli'
125
+ engineSpecificInstalled = mycliInstalled
126
+ engineSpecificValue = 'mycli'
127
+ engineSpecificInstallValue = 'install-mycli'
121
128
  } else if (config.engine === 'mongodb') {
122
129
  defaultShellName = 'mongosh'
123
130
  // mongosh IS the enhanced shell for MongoDB (no separate enhanced CLI like pgcli/mycli)
@@ -425,6 +432,20 @@ async function launchShell(
425
432
  config.database,
426
433
  ]
427
434
  installHint = 'brew install mysql-client'
435
+ } else if (config.engine === 'mariadb') {
436
+ // MariaDB uses downloaded binaries, not system PATH - get the actual path
437
+ const mariadbPath = await configManager.getBinaryPath('mariadb')
438
+ shellCmd = mariadbPath || 'mariadb'
439
+ shellArgs = [
440
+ '-u',
441
+ 'root',
442
+ '-h',
443
+ '127.0.0.1',
444
+ '-P',
445
+ String(config.port),
446
+ config.database,
447
+ ]
448
+ installHint = 'spindb engines download mariadb'
428
449
  } else if (config.engine === 'mongodb') {
429
450
  shellCmd = 'mongosh'
430
451
  shellArgs = [connectionString]
@@ -5,9 +5,10 @@ import { processManager } from '../../core/process-manager'
5
5
  import { startWithRetry } from '../../core/start-with-retry'
6
6
  import { getEngine } from '../../engines'
7
7
  import { getEngineDefaults } from '../../config/defaults'
8
- import { promptContainerSelect } from '../ui/prompts'
8
+ import { promptContainerSelect, promptConfirm } from '../ui/prompts'
9
9
  import { createSpinner } from '../ui/spinner'
10
10
  import { uiError, uiWarning } from '../ui/theme'
11
+ import { Engine } from '../../types'
11
12
 
12
13
  export const startCommand = new Command('start')
13
14
  .description('Start a container')
@@ -61,6 +62,51 @@ export const startCommand = new Command('start')
61
62
  const engineDefaults = getEngineDefaults(engineName)
62
63
  const engine = getEngine(engineName)
63
64
 
65
+ // For PostgreSQL, check if the engine binary is installed
66
+ if (engineName === Engine.PostgreSQL) {
67
+ const isInstalled = await engine.isBinaryInstalled(config.version)
68
+ if (!isInstalled) {
69
+ console.log(
70
+ uiWarning(
71
+ `PostgreSQL ${config.version} engine is not installed (required by "${containerName}")`,
72
+ ),
73
+ )
74
+ const confirmed = await promptConfirm(
75
+ `Download PostgreSQL ${config.version} now?`,
76
+ true,
77
+ )
78
+ if (!confirmed) {
79
+ console.log(
80
+ chalk.gray(
81
+ ` Run "spindb engines download postgresql ${config.version}" to download manually.`,
82
+ ),
83
+ )
84
+ return
85
+ }
86
+
87
+ const downloadSpinner = createSpinner(
88
+ `Downloading PostgreSQL ${config.version}...`,
89
+ )
90
+ downloadSpinner.start()
91
+
92
+ try {
93
+ await engine.ensureBinaries(config.version, ({ stage, message }) => {
94
+ if (stage === 'cached') {
95
+ downloadSpinner.text = `PostgreSQL ${config.version} ready`
96
+ } else {
97
+ downloadSpinner.text = message
98
+ }
99
+ })
100
+ downloadSpinner.succeed(`PostgreSQL ${config.version} downloaded`)
101
+ } catch (downloadError) {
102
+ downloadSpinner.fail(
103
+ `Failed to download PostgreSQL ${config.version} for "${containerName}"`,
104
+ )
105
+ throw downloadError
106
+ }
107
+ }
108
+ }
109
+
64
110
  const spinner = createSpinner(`Starting ${containerName}...`)
65
111
  spinner.start()
66
112
 
@@ -1,10 +1,12 @@
1
1
  import { Command } from 'commander'
2
+ import chalk from 'chalk'
2
3
  import { containerManager } from '../../core/container-manager'
3
4
  import { processManager } from '../../core/process-manager'
4
5
  import { getEngine } from '../../engines'
5
6
  import { promptContainerSelect } from '../ui/prompts'
6
7
  import { createSpinner } from '../ui/spinner'
7
8
  import { uiSuccess, uiError, uiWarning } from '../ui/theme'
9
+ import { Engine } from '../../types'
8
10
 
9
11
  export const stopCommand = new Command('stop')
10
12
  .description('Stop a container')
@@ -35,7 +37,46 @@ export const stopCommand = new Command('stop')
35
37
  spinner?.start()
36
38
 
37
39
  const engine = getEngine(container.engine)
38
- await engine.stop(container)
40
+
41
+ // For PostgreSQL, check if engine binary is installed
42
+ let usedFallback = false
43
+ let stopFailed = false
44
+ if (container.engine === Engine.PostgreSQL) {
45
+ const isInstalled = await engine.isBinaryInstalled(container.version)
46
+ if (!isInstalled) {
47
+ if (spinner) {
48
+ spinner.text = `Stopping ${container.name} (engine missing, using fallback)...`
49
+ }
50
+ const killed = await processManager.killProcess(container.name, {
51
+ engine: container.engine,
52
+ })
53
+ if (!killed) {
54
+ spinner?.fail(`Failed to stop "${container.name}"`)
55
+ console.log(
56
+ chalk.gray(
57
+ ` The PostgreSQL ${container.version} engine is not installed.`,
58
+ ),
59
+ )
60
+ console.log(
61
+ chalk.gray(
62
+ ` Run "spindb engines download postgresql ${container.version.split('.')[0]}" to reinstall.`,
63
+ ),
64
+ )
65
+ stopFailed = true
66
+ } else {
67
+ usedFallback = true
68
+ }
69
+ }
70
+ }
71
+
72
+ if (stopFailed) {
73
+ continue
74
+ }
75
+
76
+ if (!usedFallback) {
77
+ await engine.stop(container)
78
+ }
79
+
39
80
  await containerManager.updateConfig(container.name, {
40
81
  status: 'stopped',
41
82
  })
@@ -98,7 +139,40 @@ export const stopCommand = new Command('stop')
98
139
  : createSpinner(`Stopping ${containerName}...`)
99
140
  spinner?.start()
100
141
 
101
- await engine.stop(config)
142
+ // For PostgreSQL, check if engine binary is installed
143
+ // If not, use fallback process kill
144
+ let usedFallback = false
145
+ if (config.engine === Engine.PostgreSQL) {
146
+ const isInstalled = await engine.isBinaryInstalled(config.version)
147
+ if (!isInstalled) {
148
+ if (spinner) {
149
+ spinner.text = `Stopping ${containerName} (engine missing, using fallback)...`
150
+ }
151
+ const killed = await processManager.killProcess(containerName, {
152
+ engine: config.engine,
153
+ })
154
+ if (!killed) {
155
+ spinner?.fail(`Failed to stop "${containerName}"`)
156
+ console.log(
157
+ chalk.gray(
158
+ ` The PostgreSQL ${config.version} engine is not installed.`,
159
+ ),
160
+ )
161
+ console.log(
162
+ chalk.gray(
163
+ ` Run "spindb engines download postgresql ${config.version.split('.')[0]}" to reinstall.`,
164
+ ),
165
+ )
166
+ process.exit(1)
167
+ }
168
+ usedFallback = true
169
+ }
170
+ }
171
+
172
+ if (!usedFallback) {
173
+ await engine.stop(config)
174
+ }
175
+
102
176
  await containerManager.updateConfig(containerName, {
103
177
  status: 'stopped',
104
178
  })
package/cli/constants.ts CHANGED
@@ -1,7 +1,10 @@
1
+ // Note: Some emojis render narrower than others in terminals.
2
+ // Add extra space after narrow emojis (🦭, 🪶) for visual alignment.
1
3
  export const ENGINE_ICONS: Record<string, string> = {
2
4
  postgresql: '🐘',
3
5
  mysql: '🐬',
4
- sqlite: '🪶',
6
+ mariadb: '🦭 ', // Extra space - seal emoji renders narrow
7
+ sqlite: '🪶 ', // Extra space - feather emoji renders narrow
5
8
  mongodb: '🍃',
6
9
  redis: '🔴',
7
10
  }