spindb 0.34.0 → 0.34.5

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
@@ -105,6 +105,18 @@ spindb connect cache # Open redis-cli
105
105
  spindb connect cache --iredis # Enhanced shell
106
106
  ```
107
107
 
108
+ ### InfluxDB
109
+
110
+ ```bash
111
+ spindb create tsdata --engine influxdb --start
112
+ spindb run tsdata ./seed.lp # Seed with line protocol
113
+ spindb run tsdata -c "SHOW TABLES" # Run inline SQL
114
+ spindb run tsdata ./queries.sql # Run SQL file
115
+ spindb connect tsdata # Interactive SQL console
116
+ ```
117
+
118
+ > InfluxDB supports two file formats: `.lp` (line protocol) for writing data, `.sql` for queries.
119
+
108
120
  ### Enhanced Shells & Visual Tools
109
121
 
110
122
  ```bash
@@ -261,7 +273,7 @@ See [DEPLOY.md](DEPLOY.md) for comprehensive deployment documentation.
261
273
  - **Local only** - Databases bind to `127.0.0.1`. Remote connection support planned for v1.1.
262
274
  - **ClickHouse Windows** - Not supported (hostdb doesn't build for Windows).
263
275
  - **FerretDB Windows** - Not supported (postgresql-documentdb has startup issues on Windows).
264
- - **Qdrant, Meilisearch, CouchDB & InfluxDB** - Use REST API instead of CLI shell. Access via HTTP at the configured port.
276
+ - **Qdrant, Meilisearch, CouchDB** - Use REST API instead of CLI shell. Access via HTTP at the configured port.
265
277
 
266
278
  ---
267
279
 
@@ -2,14 +2,24 @@ import { Command } from 'commander'
2
2
  import { existsSync } from 'fs'
3
3
  import { resolve, basename } from 'path'
4
4
  import chalk from 'chalk'
5
- import { sqliteRegistry } from '../../engines/sqlite/registry'
6
5
  import { containerManager } from '../../core/container-manager'
7
- import { deriveContainerName } from '../../engines/sqlite/scanner'
6
+ import {
7
+ detectEngineFromPath,
8
+ getRegistryForEngine,
9
+ deriveContainerName,
10
+ formatAllExtensions,
11
+ } from '../../engines/file-based-utils'
8
12
  import { uiSuccess, uiError } from '../ui/theme'
13
+ import type { Engine } from '../../types'
9
14
 
10
15
  export const attachCommand = new Command('attach')
11
- .description('Register an existing SQLite database with SpinDB')
12
- .argument('<path>', 'Path to SQLite database file')
16
+ .description(
17
+ 'Register an existing file-based database with SpinDB (SQLite or DuckDB)',
18
+ )
19
+ .argument(
20
+ '<path>',
21
+ 'Path to database file (.sqlite, .db, .sqlite3, .duckdb, .ddb)',
22
+ )
13
23
  .option('-n, --name <name>', 'Container name (defaults to filename)')
14
24
  .option('--json', 'Output as JSON')
15
25
  .action(
@@ -20,6 +30,20 @@ export const attachCommand = new Command('attach')
20
30
  try {
21
31
  const absolutePath = resolve(path)
22
32
 
33
+ // Detect engine from file extension
34
+ const engine = detectEngineFromPath(absolutePath)
35
+ if (!engine) {
36
+ const msg = `Unrecognized file extension. Expected one of: ${formatAllExtensions()}`
37
+ if (options.json) {
38
+ console.log(JSON.stringify({ success: false, error: msg }))
39
+ } else {
40
+ console.error(uiError(msg))
41
+ }
42
+ process.exit(1)
43
+ }
44
+
45
+ const registry = getRegistryForEngine(engine)
46
+
23
47
  // Verify file exists
24
48
  if (!existsSync(absolutePath)) {
25
49
  if (options.json) {
@@ -33,8 +57,8 @@ export const attachCommand = new Command('attach')
33
57
  }
34
58
 
35
59
  // Check if already registered
36
- if (await sqliteRegistry.isPathRegistered(absolutePath)) {
37
- const entry = await sqliteRegistry.getByPath(absolutePath)
60
+ if (await registry.isPathRegistered(absolutePath)) {
61
+ const entry = await registry.getByPath(absolutePath)
38
62
  if (options.json) {
39
63
  console.log(
40
64
  JSON.stringify({
@@ -53,7 +77,11 @@ export const attachCommand = new Command('attach')
53
77
 
54
78
  // Determine container name
55
79
  const containerName =
56
- options.name || deriveContainerName(basename(absolutePath))
80
+ options.name ||
81
+ deriveContainerName(
82
+ basename(absolutePath),
83
+ engine as Engine.SQLite | Engine.DuckDB,
84
+ )
57
85
 
58
86
  // Check if container name exists
59
87
  if (await containerManager.exists(containerName)) {
@@ -73,7 +101,7 @@ export const attachCommand = new Command('attach')
73
101
  }
74
102
 
75
103
  // Register the file
76
- await sqliteRegistry.add({
104
+ await registry.add({
77
105
  name: containerName,
78
106
  filePath: absolutePath,
79
107
  created: new Date().toISOString(),
@@ -83,6 +111,7 @@ export const attachCommand = new Command('attach')
83
111
  console.log(
84
112
  JSON.stringify({
85
113
  success: true,
114
+ engine,
86
115
  name: containerName,
87
116
  filePath: absolutePath,
88
117
  }),
@@ -90,7 +119,7 @@ export const attachCommand = new Command('attach')
90
119
  } else {
91
120
  console.log(
92
121
  uiSuccess(
93
- `Registered "${basename(absolutePath)}" as "${containerName}"`,
122
+ `Registered "${basename(absolutePath)}" as "${containerName}" (${engine})`,
94
123
  ),
95
124
  )
96
125
  console.log()
@@ -44,6 +44,9 @@ function detectBackupType(filename: string): {
44
44
  case '.db':
45
45
  case '.sqlite3':
46
46
  return { engine: 'sqlite', format: 'Binary copy' }
47
+ case '.duckdb':
48
+ case '.ddb':
49
+ return { engine: 'duckdb', format: 'Binary copy' }
47
50
  case '.archive':
48
51
  return { engine: 'mongodb', format: 'BSON archive' }
49
52
  case '.rdb':
@@ -65,6 +68,8 @@ function isBackupFile(filename: string): boolean {
65
68
  '.sqlite',
66
69
  '.sqlite3',
67
70
  '.db',
71
+ '.duckdb',
72
+ '.ddb',
68
73
  '.archive',
69
74
  '.rdb',
70
75
  '.redis',
@@ -26,7 +26,7 @@ import { getEngine } from '../../engines'
26
26
  import { getEngineDefaults } from '../../config/defaults'
27
27
  import { promptContainerSelect } from '../ui/prompts'
28
28
  import { uiError, uiWarning, uiInfo, uiSuccess } from '../ui/theme'
29
- import { Engine } from '../../types'
29
+ import { Engine, isFileBasedEngine } from '../../types'
30
30
  import { configManager } from '../../core/config-manager'
31
31
  import { DBLAB_ENGINES, getDblabArgs } from '../../core/dblab-utils'
32
32
  import { downloadDblabCli } from './menu/shell-handlers'
@@ -86,9 +86,9 @@ export const connectCommand = new Command('connect')
86
86
 
87
87
  if (!containerName) {
88
88
  const containers = await containerManager.list()
89
- // SQLite containers are always "available" if file exists, server containers need to be running
89
+ // File-based containers are always "available" if file exists, server containers need to be running
90
90
  const connectable = containers.filter((c) => {
91
- if (c.engine === Engine.SQLite) {
91
+ if (isFileBasedEngine(c.engine)) {
92
92
  return existsSync(c.database)
93
93
  }
94
94
  return c.status === 'running'
@@ -131,11 +131,11 @@ export const connectCommand = new Command('connect')
131
131
  const database =
132
132
  options.database ?? config.database ?? engineDefaults.superuser
133
133
 
134
- // SQLite: check file exists instead of running status
135
- if (engineName === Engine.SQLite) {
134
+ // File-based engines: check file exists instead of running status
135
+ if (isFileBasedEngine(engineName)) {
136
136
  if (!existsSync(config.database)) {
137
137
  console.error(
138
- uiError(`SQLite database file not found: ${config.database}`),
138
+ uiError(`Database file not found: ${config.database}`),
139
139
  )
140
140
  process.exit(1)
141
141
  }
@@ -1,13 +1,15 @@
1
1
  import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
- import { sqliteRegistry } from '../../engines/sqlite/registry'
4
3
  import { containerManager } from '../../core/container-manager'
4
+ import { getRegistryForEngine } from '../../engines/file-based-utils'
5
5
  import { promptConfirm } from '../ui/prompts'
6
6
  import { uiSuccess, uiError, uiWarning } from '../ui/theme'
7
- import { Engine } from '../../types'
7
+ import { isFileBasedEngine } from '../../types'
8
8
 
9
9
  export const detachCommand = new Command('detach')
10
- .description('Unregister a SQLite database from SpinDB (keeps file on disk)')
10
+ .description(
11
+ 'Unregister a file-based database from SpinDB (keeps file on disk)',
12
+ )
11
13
  .argument('<name>', 'Container name')
12
14
  .option('-f, --force', 'Skip confirmation prompt')
13
15
  .option('--json', 'Output as JSON')
@@ -31,18 +33,22 @@ export const detachCommand = new Command('detach')
31
33
  process.exit(1)
32
34
  }
33
35
 
34
- // Verify it's a SQLite container
35
- if (config.engine !== Engine.SQLite) {
36
+ // Verify it's a file-based container
37
+ if (!isFileBasedEngine(config.engine)) {
36
38
  if (options.json) {
37
39
  console.log(
38
40
  JSON.stringify({
39
41
  success: false,
40
42
  error:
41
- 'Not a SQLite container. Use "spindb delete" for server databases.',
43
+ 'Not a file-based container. Use "spindb delete" for server databases.',
42
44
  }),
43
45
  )
44
46
  } else {
45
- console.error(uiError(`"${name}" is not a SQLite container`))
47
+ console.error(
48
+ uiError(
49
+ `"${name}" is not a file-based container (SQLite/DuckDB)`,
50
+ ),
51
+ )
46
52
  console.log(
47
53
  chalk.gray(
48
54
  ' Use "spindb delete" for server databases (PostgreSQL, MySQL)',
@@ -64,11 +70,12 @@ export const detachCommand = new Command('detach')
64
70
  }
65
71
  }
66
72
 
67
- const entry = await sqliteRegistry.get(name)
73
+ const registry = getRegistryForEngine(config.engine)
74
+ const entry = await registry.get(name)
68
75
  const filePath = entry?.filePath
69
76
 
70
77
  // Remove from registry only (not the file)
71
- await sqliteRegistry.remove(name)
78
+ await registry.remove(name)
72
79
 
73
80
  if (options.json) {
74
81
  console.log(
@@ -24,7 +24,7 @@ import { paths } from '../../config/paths'
24
24
  import { getSupportedEngines } from '../../config/engine-defaults'
25
25
  import { checkEngineDependencies } from '../../core/dependency-manager'
26
26
  import { header, uiSuccess } from '../ui/theme'
27
- import { Engine } from '../../types'
27
+ import { type Engine, isFileBasedEngine } from '../../types'
28
28
  import {
29
29
  findOutdatedContainers,
30
30
  migrateContainerVersion,
@@ -178,7 +178,7 @@ async function checkContainers(): Promise<HealthCheckResult> {
178
178
  }
179
179
 
180
180
  const details = Object.entries(byEngine).map(([engine, counts]) => {
181
- if (engine === Engine.SQLite) {
181
+ if (isFileBasedEngine(engine as Engine)) {
182
182
  return `${engine}: ${counts.running} exist, ${counts.stopped} missing`
183
183
  }
184
184
  return `${engine}: ${counts.running} running, ${counts.stopped} stopped`
@@ -0,0 +1,273 @@
1
+ import { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { existsSync } from 'fs'
4
+ import { resolve, basename } from 'path'
5
+ import { duckdbRegistry } from '../../engines/duckdb/registry'
6
+ import {
7
+ scanForUnregisteredDuckDBFiles,
8
+ deriveContainerName,
9
+ } from '../../engines/duckdb/scanner'
10
+ import {
11
+ isValidExtensionForEngine,
12
+ formatExtensionsForEngine,
13
+ } from '../../engines/file-based-utils'
14
+ import { containerManager } from '../../core/container-manager'
15
+ import { uiSuccess, uiError, uiInfo } from '../ui/theme'
16
+ import { Engine } from '../../types'
17
+ import { detachCommand } from './detach'
18
+
19
+ export const duckdbCommand = new Command('duckdb').description(
20
+ 'DuckDB-specific operations',
21
+ )
22
+
23
+ // duckdb scan
24
+ duckdbCommand
25
+ .command('scan')
26
+ .description('Scan folder for unregistered DuckDB files')
27
+ .option('-p, --path <dir>', 'Directory to scan (default: current directory)')
28
+ .option('--json', 'Output as JSON')
29
+ .action(async (options: { path?: string; json?: boolean }): Promise<void> => {
30
+ const dir = options.path ? resolve(options.path) : process.cwd()
31
+
32
+ if (!existsSync(dir)) {
33
+ if (options.json) {
34
+ console.log(
35
+ JSON.stringify({ error: 'Directory not found', directory: dir }),
36
+ )
37
+ } else {
38
+ console.error(uiError(`Directory not found: ${dir}`))
39
+ }
40
+ process.exit(1)
41
+ }
42
+
43
+ const unregistered = await scanForUnregisteredDuckDBFiles(dir)
44
+
45
+ if (options.json) {
46
+ console.log(JSON.stringify({ directory: dir, files: unregistered }))
47
+ return
48
+ }
49
+
50
+ if (unregistered.length === 0) {
51
+ console.log(uiInfo(`No unregistered DuckDB files found in ${dir}`))
52
+ return
53
+ }
54
+
55
+ console.log(
56
+ chalk.cyan(`Found ${unregistered.length} unregistered DuckDB file(s):`),
57
+ )
58
+ for (const file of unregistered) {
59
+ console.log(chalk.gray(` ${file.fileName}`))
60
+ }
61
+ console.log()
62
+ console.log(chalk.gray(' Register with: spindb attach <path>'))
63
+ })
64
+
65
+ // duckdb ignore
66
+ duckdbCommand
67
+ .command('ignore')
68
+ .description('Add folder to ignore list for CWD scanning')
69
+ .argument('[folder]', 'Folder path to ignore (default: current directory)')
70
+ .option('--json', 'Output as JSON')
71
+ .action(
72
+ async (
73
+ folder: string | undefined,
74
+ options: { json?: boolean },
75
+ ): Promise<void> => {
76
+ const absolutePath = resolve(folder || process.cwd())
77
+ await duckdbRegistry.addIgnoreFolder(absolutePath)
78
+
79
+ if (options.json) {
80
+ console.log(JSON.stringify({ success: true, folder: absolutePath }))
81
+ } else {
82
+ console.log(uiSuccess(`Added to ignore list: ${absolutePath}`))
83
+ }
84
+ },
85
+ )
86
+
87
+ // duckdb unignore
88
+ duckdbCommand
89
+ .command('unignore')
90
+ .description('Remove folder from ignore list')
91
+ .argument('[folder]', 'Folder path to unignore (default: current directory)')
92
+ .option('--json', 'Output as JSON')
93
+ .action(
94
+ async (
95
+ folder: string | undefined,
96
+ options: { json?: boolean },
97
+ ): Promise<void> => {
98
+ const absolutePath = resolve(folder || process.cwd())
99
+ const removed = await duckdbRegistry.removeIgnoreFolder(absolutePath)
100
+
101
+ if (options.json) {
102
+ console.log(JSON.stringify({ success: removed, folder: absolutePath }))
103
+ } else {
104
+ if (removed) {
105
+ console.log(uiSuccess(`Removed from ignore list: ${absolutePath}`))
106
+ } else {
107
+ console.log(uiInfo(`Folder was not in ignore list: ${absolutePath}`))
108
+ }
109
+ }
110
+ },
111
+ )
112
+
113
+ // duckdb ignored (list ignored folders)
114
+ duckdbCommand
115
+ .command('ignored')
116
+ .description('List ignored folders')
117
+ .option('--json', 'Output as JSON')
118
+ .action(async (options: { json?: boolean }): Promise<void> => {
119
+ const folders = await duckdbRegistry.listIgnoredFolders()
120
+
121
+ if (options.json) {
122
+ console.log(JSON.stringify({ folders }))
123
+ return
124
+ }
125
+
126
+ if (folders.length === 0) {
127
+ console.log(uiInfo('No folders are being ignored'))
128
+ return
129
+ }
130
+
131
+ console.log(chalk.cyan('Ignored folders:'))
132
+ for (const folder of folders) {
133
+ console.log(chalk.gray(` ${folder}`))
134
+ }
135
+ })
136
+
137
+ // duckdb attach (alias to top-level attach)
138
+ duckdbCommand
139
+ .command('attach')
140
+ .description(
141
+ 'Register an existing DuckDB database (alias for "spindb attach")',
142
+ )
143
+ .argument('<path>', 'Path to DuckDB database file')
144
+ .option('-n, --name <name>', 'Container name')
145
+ .option('--json', 'Output as JSON')
146
+ .action(
147
+ async (
148
+ path: string,
149
+ options: { name?: string; json?: boolean },
150
+ ): Promise<void> => {
151
+ try {
152
+ const absolutePath = resolve(path)
153
+
154
+ // Validate extension matches DuckDB
155
+ if (!isValidExtensionForEngine(absolutePath, Engine.DuckDB)) {
156
+ const msg = `File extension must be one of: ${formatExtensionsForEngine(Engine.DuckDB)}`
157
+ if (options.json) {
158
+ console.log(JSON.stringify({ success: false, error: msg }))
159
+ } else {
160
+ console.error(uiError(msg))
161
+ console.log(
162
+ chalk.gray(
163
+ ' For SQLite files, use: spindb sqlite attach <path>',
164
+ ),
165
+ )
166
+ }
167
+ process.exit(1)
168
+ }
169
+
170
+ if (!existsSync(absolutePath)) {
171
+ if (options.json) {
172
+ console.log(
173
+ JSON.stringify({ success: false, error: 'File not found' }),
174
+ )
175
+ } else {
176
+ console.error(uiError(`File not found: ${absolutePath}`))
177
+ }
178
+ process.exit(1)
179
+ }
180
+
181
+ if (await duckdbRegistry.isPathRegistered(absolutePath)) {
182
+ const entry = await duckdbRegistry.getByPath(absolutePath)
183
+ if (options.json) {
184
+ console.log(
185
+ JSON.stringify({
186
+ success: false,
187
+ error: 'Already registered',
188
+ existingName: entry?.name,
189
+ }),
190
+ )
191
+ } else {
192
+ console.error(
193
+ uiError(`File is already registered as "${entry?.name}"`),
194
+ )
195
+ }
196
+ process.exit(1)
197
+ }
198
+
199
+ const containerName =
200
+ options.name || deriveContainerName(basename(absolutePath))
201
+
202
+ if (await containerManager.exists(containerName)) {
203
+ if (options.json) {
204
+ console.log(
205
+ JSON.stringify({
206
+ success: false,
207
+ error: 'Container name already exists',
208
+ }),
209
+ )
210
+ } else {
211
+ console.error(
212
+ uiError(`Container "${containerName}" already exists`),
213
+ )
214
+ }
215
+ process.exit(1)
216
+ }
217
+
218
+ await duckdbRegistry.add({
219
+ name: containerName,
220
+ filePath: absolutePath,
221
+ created: new Date().toISOString(),
222
+ })
223
+
224
+ if (options.json) {
225
+ console.log(
226
+ JSON.stringify({
227
+ success: true,
228
+ name: containerName,
229
+ filePath: absolutePath,
230
+ }),
231
+ )
232
+ } else {
233
+ console.log(
234
+ uiSuccess(
235
+ `Registered "${basename(absolutePath)}" as "${containerName}"`,
236
+ ),
237
+ )
238
+ console.log()
239
+ console.log(chalk.gray(' Connect with:'))
240
+ console.log(chalk.cyan(` spindb connect ${containerName}`))
241
+ }
242
+ } catch (error) {
243
+ const e = error as Error
244
+ if (options.json) {
245
+ console.log(JSON.stringify({ success: false, error: e.message }))
246
+ } else {
247
+ console.error(uiError(e.message))
248
+ }
249
+ process.exit(1)
250
+ }
251
+ },
252
+ )
253
+
254
+ // duckdb detach (alias to top-level detach)
255
+ duckdbCommand
256
+ .command('detach')
257
+ .description('Unregister a DuckDB database (alias for "spindb detach")')
258
+ .argument('<name>', 'Container name')
259
+ .option('-f, --force', 'Skip confirmation')
260
+ .option('--json', 'Output as JSON')
261
+ .action(
262
+ async (
263
+ name: string,
264
+ options: { force?: boolean; json?: boolean },
265
+ ): Promise<void> => {
266
+ // Build args array
267
+ const args = ['node', 'detach', name]
268
+ if (options.force) args.push('-f')
269
+ if (options.json) args.push('--json')
270
+
271
+ await detachCommand.parseAsync(args, { from: 'node' })
272
+ },
273
+ )
@@ -15,12 +15,17 @@ import { containerManager } from '../../core/container-manager'
15
15
  import { processManager } from '../../core/process-manager'
16
16
  import { portManager } from '../../core/port-manager'
17
17
  import { getEngine } from '../../engines'
18
- import { sqliteRegistry } from '../../engines/sqlite/registry'
19
18
  import { paths } from '../../config/paths'
20
19
  import { promptContainerSelect } from '../ui/prompts'
21
20
  import { createSpinner } from '../ui/spinner'
22
21
  import { uiError, uiWarning, uiSuccess, uiInfo } from '../ui/theme'
23
- import { Engine } from '../../types'
22
+ import { Engine, isFileBasedEngine } from '../../types'
23
+ import {
24
+ FILE_BASED_EXTENSION_REGEX,
25
+ isValidExtensionForEngine,
26
+ formatExtensionsForEngine,
27
+ getRegistryForEngine,
28
+ } from '../../engines/file-based-utils'
24
29
 
25
30
  function isValidName(name: string): boolean {
26
31
  return /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)
@@ -32,8 +37,8 @@ async function promptEditAction(
32
37
  ): Promise<'name' | 'port' | 'config' | 'relocate' | null> {
33
38
  const choices = [{ name: 'Rename container', value: 'name' }]
34
39
 
35
- // SQLite: show relocate instead of port
36
- if (engine === Engine.SQLite) {
40
+ // File-based engines: show relocate instead of port
41
+ if (isFileBasedEngine(engine as Engine)) {
37
42
  choices.push({ name: 'Relocate database file', value: 'relocate' })
38
43
  } else {
39
44
  choices.push({ name: 'Change port', value: 'port' })
@@ -207,8 +212,11 @@ async function promptNewPort(currentPort: number): Promise<number | null> {
207
212
  return newPort
208
213
  }
209
214
 
210
- // Prompt for new file location (SQLite relocate)
211
- async function promptNewLocation(currentPath: string): Promise<string | null> {
215
+ // Prompt for new file location (file-based engine relocate)
216
+ async function promptNewLocation(
217
+ currentPath: string,
218
+ engine: Engine.SQLite | Engine.DuckDB,
219
+ ): Promise<string | null> {
212
220
  console.log()
213
221
  console.log(chalk.gray(` Current location: ${currentPath}`))
214
222
  console.log(
@@ -224,13 +232,8 @@ async function promptNewLocation(currentPath: string): Promise<string | null> {
224
232
  default: currentPath,
225
233
  validate: (input: string) => {
226
234
  if (!input.trim()) return 'Path is required'
227
- const resolvedPath = resolve(input).toLowerCase()
228
- if (
229
- !resolvedPath.endsWith('.sqlite') &&
230
- !resolvedPath.endsWith('.db') &&
231
- !resolvedPath.endsWith('.sqlite3')
232
- ) {
233
- return 'Path should end with .sqlite, .sqlite3, or .db'
235
+ if (!isValidExtensionForEngine(resolve(input), engine)) {
236
+ return `Path should end with ${formatExtensionsForEngine(engine)}`
234
237
  }
235
238
  return true
236
239
  },
@@ -272,7 +275,7 @@ export const editCommand = new Command('edit')
272
275
  .option('-p, --port <port>', 'New port number', parseInt)
273
276
  .option(
274
277
  '--relocate <path>',
275
- 'New file location for SQLite database (moves the file)',
278
+ 'New file location for file-based database (moves the file)',
276
279
  )
277
280
  .option(
278
281
  '--overwrite',
@@ -346,7 +349,10 @@ export const editCommand = new Command('edit')
346
349
  return
347
350
  }
348
351
  } else if (action === 'relocate') {
349
- const newLocation = await promptNewLocation(config.database)
352
+ const newLocation = await promptNewLocation(
353
+ config.database,
354
+ config.engine as Engine.SQLite | Engine.DuckDB,
355
+ )
350
356
  if (newLocation) {
351
357
  options.relocate = newLocation
352
358
  } else {
@@ -438,11 +444,13 @@ export const editCommand = new Command('edit')
438
444
  }
439
445
  }
440
446
 
441
- // Handle SQLite relocate
447
+ // Handle file-based engine relocate
442
448
  if (options.relocate) {
443
- if (config.engine !== Engine.SQLite) {
449
+ if (!isFileBasedEngine(config.engine)) {
444
450
  console.error(
445
- uiError('Relocate is only available for SQLite containers'),
451
+ uiError(
452
+ 'Relocate is only available for file-based containers (SQLite, DuckDB)',
453
+ ),
446
454
  )
447
455
  process.exit(1)
448
456
  }
@@ -461,7 +469,7 @@ export const editCommand = new Command('edit')
461
469
  }
462
470
 
463
471
  // Check if path looks like a file (has db extension) or directory
464
- const hasDbExtension = /\.(sqlite3?|db)$/i.test(expandedPath)
472
+ const hasDbExtension = FILE_BASED_EXTENSION_REGEX.test(expandedPath)
465
473
 
466
474
  // Treat as directory if:
467
475
  // - ends with /
@@ -553,11 +561,13 @@ export const editCommand = new Command('edit')
553
561
  }
554
562
  }
555
563
 
556
- // Update the container config and SQLite registry
564
+ // Update the container config and file-based registry
557
565
  await containerManager.updateConfig(containerName, {
558
566
  database: newPath,
559
567
  })
560
- await sqliteRegistry.update(containerName, { filePath: newPath })
568
+ await getRegistryForEngine(config.engine).update(containerName, {
569
+ filePath: newPath,
570
+ })
561
571
 
562
572
  // Now safe to delete source file for cross-device moves
563
573
  if (needsSourceCleanup && existsSync(originalPath)) {