spindb 0.23.5 → 0.24.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
@@ -7,7 +7,7 @@
7
7
 
8
8
  **One CLI for all your local databases.**
9
9
 
10
- SpinDB is a universal database management tool that combines a package manager, a unified API, and native client tooling for 12 different database engines—all from a single command-line interface. No Docker, no VMs, no platform-specific installers. Just databases, running natively on your machine.
10
+ SpinDB is a universal database management tool that combines a package manager, a unified API, and native client tooling for 13 different database engines—all from a single command-line interface. No Docker, no VMs, no platform-specific installers. Just databases, running natively on your machine.
11
11
 
12
12
  ```bash
13
13
  npm install -g spindb
@@ -48,7 +48,7 @@ One consistent interface across SQL databases, document stores, key-value stores
48
48
 
49
49
  ```bash
50
50
  # Same commands work for ANY database
51
- spindb create mydb --engine [postgresql|mysql|mariadb|mongodb|ferretdb|redis|valkey|clickhouse|sqlite|duckdb|qdrant|meilisearch]
51
+ spindb create mydb --engine [postgresql|mysql|mariadb|mongodb|ferretdb|redis|valkey|clickhouse|sqlite|duckdb|qdrant|meilisearch|couchdb]
52
52
  spindb start mydb
53
53
  spindb connect mydb
54
54
  spindb backup mydb
@@ -70,7 +70,7 @@ spindb run mydb -c "SELECT * FROM system.tables" # ClickHouse
70
70
 
71
71
  ## Platform Coverage
72
72
 
73
- SpinDB works across **12 database engines** and **5 platform architectures** with a **single, consistent API**.
73
+ SpinDB works across **13 database engines** and **5 platform architectures** with a **single, consistent API**.
74
74
 
75
75
  | Database | macOS ARM64 | macOS Intel | Linux x64 | Linux ARM64 | Windows x64 |
76
76
  |----------|:-----------:|:-----------:|:---------:|:-----------:|:-----------:|
@@ -86,8 +86,9 @@ SpinDB works across **12 database engines** and **5 platform architectures** wit
86
86
  | 🏠 **ClickHouse** | ✅ | ✅ | ✅ | ✅ | ❌ |
87
87
  | 🧭 **Qdrant** | ✅ | ✅ | ✅ | ✅ | ✅ |
88
88
  | 🔍 **Meilisearch** | ✅ | ✅ | ✅ | ✅ | ✅ |
89
+ | 🛋 **CouchDB** | ✅ | ✅ | ✅ | ✅ | ✅ |
89
90
 
90
- **58 combinations. One CLI. Zero configuration.**
91
+ **63 combinations. One CLI. Zero configuration.**
91
92
 
92
93
  ---
93
94
 
@@ -166,7 +167,7 @@ SpinDB runs databases as **native processes** with **isolated data directories**
166
167
  | Feature | SpinDB | Docker | DBngin | Postgres.app | XAMPP |
167
168
  |---------|--------|--------|--------|--------------|-------|
168
169
  | No Docker required | ✅ | ❌ | ✅ | ✅ | ✅ |
169
- | Multiple DB engines | ✅ 12 engines | ✅ Unlimited | ✅ 3 engines | ❌ PostgreSQL only | ⚠️ MySQL only |
170
+ | Multiple DB engines | ✅ 13 engines | ✅ Unlimited | ✅ 3 engines | ❌ PostgreSQL only | ⚠️ MySQL only |
170
171
  | CLI-first | ✅ | ✅ | ❌ GUI-first | ❌ GUI-first | ❌ GUI-first |
171
172
  | Multiple versions | ✅ | ✅ | ✅ | ✅ | ❌ |
172
173
  | Clone databases | ✅ | Manual | ✅ | ❌ | ❌ |
@@ -180,7 +181,7 @@ SpinDB runs databases as **native processes** with **isolated data directories**
180
181
 
181
182
  ## Supported Databases
182
183
 
183
- SpinDB supports **12 database engines** with **multiple versions** for each:
184
+ SpinDB supports **13 database engines** with **multiple versions** for each:
184
185
 
185
186
  | Engine | Type | Versions | Default Port | Query Language |
186
187
  |--------|------|----------|--------------|----------------|
@@ -196,10 +197,11 @@ SpinDB supports **12 database engines** with **multiple versions** for each:
196
197
  | 🏠 **ClickHouse** | Columnar OLAP | 25.12 | 9000 (TCP), 8123 (HTTP) | SQL (ClickHouse dialect) |
197
198
  | 🧭 **Qdrant** | Vector Search | 1 | 6333 (HTTP), 6334 (gRPC) | REST API |
198
199
  | 🔍 **Meilisearch** | Full-Text Search | 1 | 7700 | REST API |
200
+ | 🛋 **CouchDB** | Document Store | 3 | 5984 | REST API |
199
201
 
200
202
  ### Engine Categories
201
203
 
202
- **Server-Based Databases** (PostgreSQL, MySQL, MariaDB, MongoDB, FerretDB, Redis, Valkey, ClickHouse, Qdrant, Meilisearch):
204
+ **Server-Based Databases** (PostgreSQL, MySQL, MariaDB, MongoDB, FerretDB, Redis, Valkey, ClickHouse, Qdrant, Meilisearch, CouchDB):
203
205
  - Start/stop server processes
204
206
  - Bind to localhost ports
205
207
  - Data stored in `~/.spindb/containers/{engine}/{name}/`
@@ -601,6 +603,7 @@ SpinDB supports enhanced database shells with auto-completion, syntax highlighti
601
603
  | ClickHouse | `clickhouse-client` | - | `usql` |
602
604
  | Qdrant | REST API | - | - |
603
605
  | Meilisearch | REST API | - | - |
606
+ | CouchDB | REST API | - | - |
604
607
 
605
608
  Install and use in one command:
606
609
 
@@ -751,6 +754,7 @@ spindb restore mydb --from-url "postgresql://user:pass@prod-host:5432/production
751
754
  | ClickHouse | `clickhouse://` or `http://` | `clickhouse://default:pass@host:8123/db` |
752
755
  | Qdrant | `qdrant://` or `http://` | `http://host:6333?api_key=KEY` |
753
756
  | Meilisearch | `meilisearch://` or `http://` | `http://host:7700?api_key=KEY` |
757
+ | CouchDB | `couchdb://` or `http://` | `http://user:pass@host:5984/db` |
754
758
 
755
759
  ### Multi-Version Support
756
760
 
@@ -831,7 +835,7 @@ The following engines may be added based on community interest:
831
835
  - **Local only** - Databases bind to `127.0.0.1`. Remote connection support planned for v1.1.
832
836
  - **ClickHouse Windows** - Not supported (hostdb doesn't build for Windows).
833
837
  - **FerretDB Windows** - Not supported (postgresql-documentdb has startup issues on Windows).
834
- - **Qdrant & Meilisearch** - Use REST API instead of CLI shell. Access via HTTP at the configured port.
838
+ - **Qdrant, Meilisearch & CouchDB** - Use REST API instead of CLI shell. Access via HTTP at the configured port.
835
839
 
836
840
  ---
837
841
 
@@ -906,7 +910,7 @@ See [FEATURE.md](FEATURE.md) for adding new database engines.
906
910
 
907
911
  SpinDB is powered by:
908
912
 
909
- - **[hostdb](https://github.com/robertjbass/hostdb)** - Pre-compiled database binaries for 11 engines across all major platforms. Makes Docker-free multi-version database support possible.
913
+ - **[hostdb](https://github.com/robertjbass/hostdb)** - Pre-compiled database binaries for 14 engines across all major platforms. Makes Docker-free multi-version database support possible.
910
914
 
911
915
  ---
912
916
 
@@ -43,6 +43,7 @@ import {
43
43
  type InstalledValkeyEngine,
44
44
  type InstalledQdrantEngine,
45
45
  type InstalledMeilisearchEngine,
46
+ type InstalledCouchDBEngine,
46
47
  } from '../helpers'
47
48
  import { Engine, Platform } from '../../types'
48
49
  import {
@@ -60,6 +61,7 @@ import { clickhouseBinaryManager } from '../../engines/clickhouse/binary-manager
60
61
  import { qdrantBinaryManager } from '../../engines/qdrant/binary-manager'
61
62
  import { meilisearchBinaryManager } from '../../engines/meilisearch/binary-manager'
62
63
  import { ferretdbBinaryManager } from '../../engines/ferretdb/binary-manager'
64
+ import { couchdbBinaryManager } from '../../engines/couchdb/binary-manager'
63
65
  import {
64
66
  DEFAULT_DOCUMENTDB_VERSION,
65
67
  normalizeDocumentDBVersion,
@@ -457,6 +459,9 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
457
459
  const meilisearchEngines = engines.filter(
458
460
  (e): e is InstalledMeilisearchEngine => e.engine === 'meilisearch',
459
461
  )
462
+ const couchdbEngines = engines.filter(
463
+ (e): e is InstalledCouchDBEngine => e.engine === 'couchdb',
464
+ )
460
465
 
461
466
  // Calculate total size for PostgreSQL
462
467
  const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
@@ -621,6 +626,21 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
621
626
  )
622
627
  }
623
628
 
629
+ // CouchDB rows
630
+ for (const engine of couchdbEngines) {
631
+ const icon = ENGINE_ICONS.couchdb
632
+ const platformInfo = `${engine.platform}-${engine.arch}`
633
+ const engineDisplay = `${icon} couchdb`
634
+
635
+ console.log(
636
+ chalk.gray(' ') +
637
+ chalk.cyan(padWithEmoji(engineDisplay, 13)) +
638
+ chalk.yellow(engine.version.padEnd(12)) +
639
+ chalk.gray(platformInfo.padEnd(18)) +
640
+ chalk.white(formatBytes(engine.sizeBytes)),
641
+ )
642
+ }
643
+
624
644
  console.log(chalk.gray(' ' + '─'.repeat(55)))
625
645
 
626
646
  // Summary
@@ -716,6 +736,17 @@ async function listEngines(options: { json?: boolean }): Promise<void> {
716
736
  ),
717
737
  )
718
738
  }
739
+ if (couchdbEngines.length > 0) {
740
+ const totalCouchDBSize = couchdbEngines.reduce(
741
+ (acc, e) => acc + e.sizeBytes,
742
+ 0,
743
+ )
744
+ console.log(
745
+ chalk.gray(
746
+ ` CouchDB: ${couchdbEngines.length} version(s), ${formatBytes(totalCouchDBSize)}`,
747
+ ),
748
+ )
749
+ }
719
750
  console.log()
720
751
  }
721
752
 
@@ -1601,9 +1632,53 @@ enginesCommand
1601
1632
  return
1602
1633
  }
1603
1634
 
1635
+ if (['couchdb', 'couch'].includes(normalizedEngine)) {
1636
+ if (!version) {
1637
+ console.error(uiError('CouchDB requires a version (e.g., 3)'))
1638
+ process.exit(1)
1639
+ }
1640
+
1641
+ const engine = getEngine(Engine.CouchDB)
1642
+
1643
+ const spinner = createSpinner(`Checking CouchDB ${version} binaries...`)
1644
+ spinner.start()
1645
+
1646
+ let wasCached = false
1647
+ await engine.ensureBinaries(version, ({ stage, message }) => {
1648
+ if (stage === 'cached') {
1649
+ wasCached = true
1650
+ spinner.text = `CouchDB ${version} binaries ready (cached)`
1651
+ } else {
1652
+ spinner.text = message
1653
+ }
1654
+ })
1655
+
1656
+ if (wasCached) {
1657
+ spinner.succeed(`CouchDB ${version} binaries already installed`)
1658
+ } else {
1659
+ spinner.succeed(`CouchDB ${version} binaries downloaded`)
1660
+ }
1661
+
1662
+ // Show the path for reference
1663
+ const { platform: couchdbPlatform, arch: couchdbArch } =
1664
+ platformService.getPlatformInfo()
1665
+ const couchdbFullVersion = couchdbBinaryManager.getFullVersion(version)
1666
+ const binPath = paths.getBinaryPath({
1667
+ engine: 'couchdb',
1668
+ version: couchdbFullVersion,
1669
+ platform: couchdbPlatform,
1670
+ arch: couchdbArch,
1671
+ })
1672
+ console.log(chalk.gray(` Location: ${binPath}`))
1673
+
1674
+ // Skip client tools check for CouchDB - it's a REST API server
1675
+ // with no CLI client tools (uses HTTP protocols instead)
1676
+ return
1677
+ }
1678
+
1604
1679
  console.error(
1605
1680
  uiError(
1606
- `Unknown engine "${engineName}". Supported: postgresql, mysql, mariadb, sqlite, duckdb, mongodb, ferretdb, redis, valkey, clickhouse, qdrant, meilisearch`,
1681
+ `Unknown engine "${engineName}". Supported: postgresql, mysql, mariadb, sqlite, duckdb, mongodb, ferretdb, redis, valkey, clickhouse, qdrant, meilisearch, couchdb`,
1607
1682
  ),
1608
1683
  )
1609
1684
  process.exit(1)
@@ -165,6 +165,15 @@ function validateConnectionString(
165
165
  return 'Connection string must start with meilisearch://, http://, or https://'
166
166
  }
167
167
  break
168
+ case Engine.CouchDB:
169
+ if (
170
+ !input.startsWith('couchdb://') &&
171
+ !input.startsWith('http://') &&
172
+ !input.startsWith('https://')
173
+ ) {
174
+ return 'Connection string must start with couchdb://, http://, or https://'
175
+ }
176
+ break
168
177
  case Engine.SQLite:
169
178
  case Engine.DuckDB:
170
179
  return 'File-based engines do not support remote connection strings'
@@ -151,19 +151,29 @@ export async function handleCreate(): Promise<'main' | void> {
151
151
  )
152
152
  binarySpinner.start()
153
153
 
154
- const isInstalled = await dbEngine.isBinaryInstalled(version)
155
- if (isInstalled) {
156
- binarySpinner.succeed(
157
- `${dbEngine.displayName} ${version} binaries ready (cached)`,
158
- )
159
- } else {
160
- binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
161
- await dbEngine.ensureBinaries(version, ({ message }) => {
162
- binarySpinner.text = message
163
- })
164
- binarySpinner.succeed(
165
- `${dbEngine.displayName} ${version} binaries downloaded`,
166
- )
154
+ try {
155
+ const isInstalled = await dbEngine.isBinaryInstalled(version)
156
+ if (isInstalled) {
157
+ binarySpinner.succeed(
158
+ `${dbEngine.displayName} ${version} binaries ready (cached)`,
159
+ )
160
+ } else {
161
+ binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
162
+ await dbEngine.ensureBinaries(version, ({ message }) => {
163
+ binarySpinner.text = message
164
+ })
165
+ binarySpinner.succeed(
166
+ `${dbEngine.displayName} ${version} binaries downloaded`,
167
+ )
168
+ }
169
+ } catch (error) {
170
+ binarySpinner.fail(`Failed to download ${dbEngine.displayName} binaries`)
171
+ const e = error as Error
172
+ console.log()
173
+ console.log(uiError(e.message))
174
+ console.log()
175
+ await pressEnterToContinue()
176
+ return
167
177
  }
168
178
  }
169
179
 
@@ -221,19 +231,29 @@ export async function handleCreate(): Promise<'main' | void> {
221
231
  )
222
232
  binarySpinner.start()
223
233
 
224
- const isInstalled = await dbEngine.isBinaryInstalled(version)
225
- if (isInstalled) {
226
- binarySpinner.succeed(
227
- `${dbEngine.displayName} ${version} binaries ready (cached)`,
228
- )
229
- } else {
230
- binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
231
- await dbEngine.ensureBinaries(version, ({ message }) => {
232
- binarySpinner.text = message
233
- })
234
- binarySpinner.succeed(
235
- `${dbEngine.displayName} ${version} binaries downloaded`,
236
- )
234
+ try {
235
+ const isInstalled = await dbEngine.isBinaryInstalled(version)
236
+ if (isInstalled) {
237
+ binarySpinner.succeed(
238
+ `${dbEngine.displayName} ${version} binaries ready (cached)`,
239
+ )
240
+ } else {
241
+ binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
242
+ await dbEngine.ensureBinaries(version, ({ message }) => {
243
+ binarySpinner.text = message
244
+ })
245
+ binarySpinner.succeed(
246
+ `${dbEngine.displayName} ${version} binaries downloaded`,
247
+ )
248
+ }
249
+ } catch (error) {
250
+ binarySpinner.fail(`Failed to download ${dbEngine.displayName} binaries`)
251
+ const e = error as Error
252
+ console.log()
253
+ console.log(uiError(e.message))
254
+ console.log()
255
+ await pressEnterToContinue()
256
+ return
237
257
  }
238
258
  }
239
259
 
@@ -335,7 +355,7 @@ export async function handleCreate(): Promise<'main' | void> {
335
355
  // For other engines (MySQL, SQLite), allow creating a database named 'postgres'
336
356
  if (
337
357
  config &&
338
- !(config.engine === 'postgresql' && database === 'postgres')
358
+ !(config.engine === Engine.PostgreSQL && database === 'postgres')
339
359
  ) {
340
360
  const dbSpinner = createSpinner(`Creating database "${database}"...`)
341
361
  dbSpinner.start()
@@ -625,14 +645,14 @@ export async function showContainerSubmenu(
625
645
  })
626
646
 
627
647
  // Run SQL/script - always enabled for file-based DBs (if file exists), server databases need to be running
628
- // Qdrant and Meilisearch use REST API and don't support script files - hide the option entirely
629
- if (config.engine !== 'qdrant' && config.engine !== 'meilisearch') {
648
+ // REST API engines (Qdrant, Meilisearch, CouchDB) don't support script files - hide the option entirely
649
+ if (config.engine !== Engine.Qdrant && config.engine !== Engine.Meilisearch && config.engine !== Engine.CouchDB) {
630
650
  const canRunSql = isFileBasedDB ? existsSync(config.database) : isRunning
631
- // Engine-specific terminology: Redis/Valkey use commands, MongoDB uses scripts, others use SQL
651
+ // Engine-specific terminology: Redis/Valkey use commands, MongoDB/FerretDB use scripts, others use SQL
632
652
  const runScriptLabel =
633
- config.engine === 'redis' || config.engine === 'valkey'
653
+ config.engine === Engine.Redis || config.engine === Engine.Valkey
634
654
  ? 'Run command file'
635
- : config.engine === 'mongodb'
655
+ : config.engine === Engine.MongoDB || config.engine === Engine.FerretDB
636
656
  ? 'Run script file'
637
657
  : 'Run SQL file'
638
658
  actionChoices.push({
@@ -208,6 +208,13 @@ export async function handleOpenShell(containerName: string): Promise<void> {
208
208
  engineSpecificInstalled = false
209
209
  engineSpecificValue = null
210
210
  engineSpecificInstallValue = null
211
+ } else if (config.engine === 'couchdb') {
212
+ // CouchDB uses REST API, open Fauxton dashboard in browser
213
+ defaultShellName = 'Fauxton Dashboard'
214
+ engineSpecificCli = null
215
+ engineSpecificInstalled = false
216
+ engineSpecificValue = null
217
+ engineSpecificInstallValue = null
211
218
  } else {
212
219
  defaultShellName = 'psql'
213
220
  engineSpecificCli = 'pgcli'
@@ -259,8 +266,19 @@ export async function handleOpenShell(containerName: string): Promise<void> {
259
266
  name: `ℹ Show API info`,
260
267
  value: 'api-info',
261
268
  })
269
+ } else if (config.engine === 'couchdb') {
270
+ // CouchDB: Fauxton dashboard is built-in at /_utils
271
+ choices.push({
272
+ name: `◎ Open Fauxton Dashboard in browser`,
273
+ value: 'default',
274
+ })
275
+ // Always show API info option for CouchDB
276
+ choices.push({
277
+ name: `ℹ Show API info`,
278
+ value: 'api-info',
279
+ })
262
280
  } else {
263
- // Non-Qdrant/Meilisearch engines: show default shell option
281
+ // Non-Qdrant/Meilisearch/CouchDB engines: show default shell option
264
282
  choices.push({
265
283
  name: `>_ Use default shell (${defaultShellName})`,
266
284
  value: 'default',
@@ -291,14 +309,15 @@ export async function handleOpenShell(containerName: string): Promise<void> {
291
309
  }
292
310
  }
293
311
 
294
- // usql supports SQL databases (PostgreSQL, MySQL, SQLite) - skip for Redis, Valkey, MongoDB, FerretDB, Qdrant, and Meilisearch
312
+ // usql supports SQL databases (PostgreSQL, MySQL, SQLite) - skip for Redis, Valkey, MongoDB, FerretDB, Qdrant, Meilisearch, and CouchDB
295
313
  const isNonSqlEngine =
296
314
  config.engine === 'redis' ||
297
315
  config.engine === 'valkey' ||
298
316
  config.engine === 'mongodb' ||
299
317
  config.engine === 'ferretdb' ||
300
318
  config.engine === 'qdrant' ||
301
- config.engine === 'meilisearch'
319
+ config.engine === 'meilisearch' ||
320
+ config.engine === 'couchdb'
302
321
  if (!isNonSqlEngine) {
303
322
  if (usqlInstalled) {
304
323
  choices.push({
@@ -368,6 +387,19 @@ export async function handleOpenShell(containerName: string): Promise<void> {
368
387
  console.log(chalk.gray(` curl http://127.0.0.1:${config.port}/indexes`))
369
388
  console.log(chalk.gray(` curl http://127.0.0.1:${config.port}/health`))
370
389
  console.log(chalk.gray(` curl http://127.0.0.1:${config.port}/stats`))
390
+ } else if (config.engine === 'couchdb') {
391
+ console.log(chalk.cyan('CouchDB REST API:'))
392
+ console.log(chalk.white(` HTTP: http://127.0.0.1:${config.port}`))
393
+ console.log(chalk.white(` Fauxton: http://127.0.0.1:${config.port}/_utils`))
394
+ console.log()
395
+ console.log(chalk.cyan('Credentials:'))
396
+ console.log(chalk.white(` Username: admin`))
397
+ console.log(chalk.white(` Password: admin`))
398
+ console.log()
399
+ console.log(chalk.gray('Example curl commands:'))
400
+ console.log(chalk.gray(` curl http://127.0.0.1:${config.port}`))
401
+ console.log(chalk.gray(` curl http://127.0.0.1:${config.port}/_all_dbs`))
402
+ console.log(chalk.gray(` curl -X PUT http://127.0.0.1:${config.port}/mydb`))
371
403
  }
372
404
  console.log()
373
405
  await pressEnterToContinue()
@@ -814,6 +846,29 @@ async function launchShell(
814
846
  openInBrowser(dashboardUrl)
815
847
  await pressEnterToContinue()
816
848
  return
849
+ } else if (config.engine === 'couchdb') {
850
+ // CouchDB: Open Fauxton dashboard in browser (served at /_utils)
851
+ const dashboardUrl = `http://127.0.0.1:${config.port}/_utils`
852
+ console.log()
853
+ console.log(chalk.cyan('CouchDB Fauxton Dashboard'))
854
+ console.log(chalk.gray(` ${dashboardUrl}`))
855
+ console.log()
856
+ console.log(chalk.cyan('Credentials (if prompted):'))
857
+ console.log(chalk.white(` Username: admin`))
858
+ console.log(chalk.white(` Password: admin`))
859
+ console.log()
860
+
861
+ // Prompt before opening so user can see credentials
862
+ await escapeablePrompt([
863
+ {
864
+ type: 'input',
865
+ name: 'continue',
866
+ message: chalk.gray('Press Enter to open in browser...'),
867
+ },
868
+ ])
869
+
870
+ openInBrowser(dashboardUrl)
871
+ return
817
872
  } else {
818
873
  shellCmd = 'psql'
819
874
  shellArgs = [connectionString]
@@ -71,11 +71,12 @@ export async function handleRunSql(containerName: string): Promise<void> {
71
71
 
72
72
  // Document/search engines use "Script" terminology
73
73
  // MongoDB and FerretDB use JavaScript via mongosh
74
- // Qdrant and Meilisearch use REST API (JSON)
74
+ // Qdrant, Meilisearch, and CouchDB use REST API (JSON)
75
75
  case Engine.MongoDB:
76
76
  case Engine.FerretDB:
77
77
  case Engine.Qdrant:
78
78
  case Engine.Meilisearch:
79
+ case Engine.CouchDB:
79
80
  return { type: 'Script', lower: 'script' }
80
81
 
81
82
  // SQL engines use "SQL" terminology
package/cli/constants.ts CHANGED
@@ -12,6 +12,7 @@ export const ENGINE_ICONS: Record<string, string> = {
12
12
  clickhouse: '🏠',
13
13
  qdrant: '🧭',
14
14
  meilisearch: '🔍',
15
+ couchdb: '🛋',
15
16
  }
16
17
 
17
18
  // Visual width of each icon in terminal columns
@@ -30,6 +31,7 @@ export const ENGINE_ICON_WIDTHS: Record<string, number> = {
30
31
  clickhouse: 2,
31
32
  qdrant: 2,
32
33
  meilisearch: 2,
34
+ couchdb: 1, // 🛋 couch renders narrow
33
35
  }
34
36
 
35
37
  export const DEFAULT_ENGINE_ICON = '▣'
package/cli/helpers.ts CHANGED
@@ -184,6 +184,16 @@ export type InstalledFerretDBEngine = {
184
184
  source: 'downloaded'
185
185
  }
186
186
 
187
+ export type InstalledCouchDBEngine = {
188
+ engine: 'couchdb'
189
+ version: string
190
+ platform: string
191
+ arch: string
192
+ path: string
193
+ sizeBytes: number
194
+ source: 'downloaded'
195
+ }
196
+
187
197
  export type InstalledEngine =
188
198
  | InstalledPostgresEngine
189
199
  | InstalledMariadbEngine
@@ -197,6 +207,7 @@ export type InstalledEngine =
197
207
  | InstalledClickHouseEngine
198
208
  | InstalledQdrantEngine
199
209
  | InstalledMeilisearchEngine
210
+ | InstalledCouchDBEngine
200
211
 
201
212
  async function getPostgresVersion(binPath: string): Promise<string | null> {
202
213
  const ext = platformService.getExecutableExtension()
@@ -813,6 +824,58 @@ async function getInstalledMeilisearchEngines(): Promise<InstalledMeilisearchEng
813
824
  return engines
814
825
  }
815
826
 
827
+ // Get CouchDB version from binary path
828
+ // Note: CouchDB doesn't support --version flag, so we just verify the binary exists
829
+ // and return null to use the version from the directory name
830
+ async function getCouchDBVersion(binPath: string): Promise<string | null> {
831
+ const ext = platformService.getExecutableExtension()
832
+ const couchdbPath = join(binPath, 'bin', `couchdb${ext}`)
833
+ if (!existsSync(couchdbPath)) {
834
+ return null
835
+ }
836
+ // CouchDB is an Erlang app that tries to start when run with any args
837
+ // Just return null to use directory-parsed version
838
+ return null
839
+ }
840
+
841
+ // Get installed CouchDB engines from downloaded binaries
842
+ async function getInstalledCouchDBEngines(): Promise<InstalledCouchDBEngine[]> {
843
+ const binDir = paths.bin
844
+
845
+ if (!existsSync(binDir)) {
846
+ return []
847
+ }
848
+
849
+ const entries = await readdir(binDir, { withFileTypes: true })
850
+ const engines: InstalledCouchDBEngine[] = []
851
+
852
+ for (const entry of entries) {
853
+ if (!entry.isDirectory()) continue
854
+ if (!entry.name.startsWith('couchdb-')) continue
855
+
856
+ const parsed = parseEngineDirectory(entry.name, 'couchdb-', binDir)
857
+ if (!parsed) continue
858
+
859
+ const actualVersion =
860
+ (await getCouchDBVersion(parsed.path)) || parsed.version
861
+ const sizeBytes = await calculateDirectorySize(parsed.path)
862
+
863
+ engines.push({
864
+ engine: 'couchdb',
865
+ version: actualVersion,
866
+ platform: parsed.platform,
867
+ arch: parsed.arch,
868
+ path: parsed.path,
869
+ sizeBytes,
870
+ source: 'downloaded',
871
+ })
872
+ }
873
+
874
+ engines.sort((a, b) => compareVersions(b.version, a.version))
875
+
876
+ return engines
877
+ }
878
+
816
879
  // Get FerretDB version from binary path
817
880
  async function getFerretDBVersion(binPath: string): Promise<string | null> {
818
881
  const ext = platformService.getExecutableExtension()
@@ -899,6 +962,7 @@ const ENGINE_PREFIXES = [
899
962
  'clickhouse-',
900
963
  'qdrant-',
901
964
  'meilisearch-',
965
+ 'couchdb-',
902
966
  ] as const
903
967
 
904
968
  /**
@@ -941,6 +1005,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
941
1005
  clickhouseEngines,
942
1006
  qdrantEngines,
943
1007
  meilisearchEngines,
1008
+ couchdbEngines,
944
1009
  ] = await Promise.all([
945
1010
  getInstalledPostgresEngines(),
946
1011
  getInstalledMariadbEngines(),
@@ -954,6 +1019,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
954
1019
  getInstalledClickHouseEngines(),
955
1020
  getInstalledQdrantEngines(),
956
1021
  getInstalledMeilisearchEngines(),
1022
+ getInstalledCouchDBEngines(),
957
1023
  ])
958
1024
 
959
1025
  return [
@@ -969,6 +1035,7 @@ export async function getInstalledEngines(): Promise<InstalledEngine[]> {
969
1035
  ...clickhouseEngines,
970
1036
  ...qdrantEngines,
971
1037
  ...meilisearchEngines,
1038
+ ...couchdbEngines,
972
1039
  ]
973
1040
  }
974
1041
 
@@ -984,4 +1051,5 @@ export {
984
1051
  getInstalledClickHouseEngines,
985
1052
  getInstalledQdrantEngines,
986
1053
  getInstalledMeilisearchEngines,
1054
+ getInstalledCouchDBEngines,
987
1055
  }
package/cli/ui/prompts.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  installEngineDependencies,
16
16
  } from '../../core/dependency-manager'
17
17
  import { getEngineDependencies } from '../../config/os-dependencies'
18
- import { getEngineIcon } from '../constants'
18
+ import { getEngineIcon, getEngineIconPadded } from '../constants'
19
19
  import { type ContainerConfig, type Engine, type BackupFormatType } from '../../types'
20
20
 
21
21
  // Navigation sentinel values for menu navigation
@@ -221,7 +221,7 @@ export async function promptEngine(options?: {
221
221
  | inquirer.Separator
222
222
 
223
223
  const choices: Choice[] = engines.map((e) => ({
224
- name: `${getEngineIcon(e.name)} ${e.displayName} ${chalk.gray(`(versions: ${e.supportedVersions.join(', ')})`)}`,
224
+ name: `${getEngineIconPadded(e.name)} ${e.displayName} ${chalk.gray(`(versions: ${e.supportedVersions.join(', ')})`)}`,
225
225
  value: e.name,
226
226
  short: e.displayName,
227
227
  }))
@@ -233,6 +233,7 @@ export async function promptEngine(options?: {
233
233
  name: `${chalk.blue('⌂')} Back to main menu ${chalk.gray('(esc)')}`,
234
234
  value: MAIN_MENU_VALUE,
235
235
  })
236
+ choices.push(new inquirer.Separator())
236
237
  }
237
238
 
238
239
  const { engine } = await escapeablePrompt<{ engine: string }>([
@@ -22,6 +22,7 @@ import {
22
22
  type ClickHouseFormat,
23
23
  type QdrantFormat,
24
24
  type MeilisearchFormat,
25
+ type CouchDBFormat,
25
26
  type BackupFormatType,
26
27
  } from '../types'
27
28
 
@@ -53,6 +54,7 @@ export const BACKUP_FORMATS: {
53
54
  [Engine.ClickHouse]: EngineBackupFormats<ClickHouseFormat>
54
55
  [Engine.Qdrant]: EngineBackupFormats<QdrantFormat>
55
56
  [Engine.Meilisearch]: EngineBackupFormats<MeilisearchFormat>
57
+ [Engine.CouchDB]: EngineBackupFormats<CouchDBFormat>
56
58
  } = {
57
59
  [Engine.PostgreSQL]: {
58
60
  formats: {
@@ -252,6 +254,18 @@ export const BACKUP_FORMATS: {
252
254
  supportsFormatChoice: false, // Only snapshot format supported
253
255
  defaultFormat: 'snapshot',
254
256
  },
257
+ [Engine.CouchDB]: {
258
+ formats: {
259
+ json: {
260
+ extension: '.json',
261
+ label: '.json',
262
+ description: 'JSON backup - all documents exported as JSON',
263
+ spinnerLabel: 'JSON',
264
+ },
265
+ },
266
+ supportsFormatChoice: false, // Only JSON format supported
267
+ defaultFormat: 'json',
268
+ },
255
269
  }
256
270
 
257
271
  /**