spindb 0.34.3 → 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/cli/commands/attach.ts +38 -9
- package/cli/commands/backups.ts +5 -0
- package/cli/commands/connect.ts +6 -6
- package/cli/commands/detach.ts +16 -9
- package/cli/commands/doctor.ts +2 -2
- package/cli/commands/duckdb.ts +273 -0
- package/cli/commands/edit.ts +31 -21
- package/cli/commands/info.ts +26 -16
- package/cli/commands/list.ts +44 -26
- package/cli/commands/menu/update-handlers.ts +2 -2
- package/cli/commands/sqlite.ts +21 -0
- package/cli/index.ts +2 -0
- package/engines/duckdb/scanner.ts +22 -0
- package/engines/file-based-utils.ts +262 -0
- package/engines/sqlite/scanner.ts +11 -88
- package/package.json +1 -1
package/cli/commands/attach.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
12
|
-
|
|
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
|
|
37
|
-
const entry = await
|
|
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 ||
|
|
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
|
|
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()
|
package/cli/commands/backups.ts
CHANGED
|
@@ -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',
|
package/cli/commands/connect.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
135
|
-
if (engineName
|
|
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(`
|
|
138
|
+
uiError(`Database file not found: ${config.database}`),
|
|
139
139
|
)
|
|
140
140
|
process.exit(1)
|
|
141
141
|
}
|
package/cli/commands/detach.ts
CHANGED
|
@@ -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 {
|
|
7
|
+
import { isFileBasedEngine } from '../../types'
|
|
8
8
|
|
|
9
9
|
export const detachCommand = new Command('detach')
|
|
10
|
-
.description(
|
|
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
|
|
35
|
-
if (config.engine
|
|
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
|
|
43
|
+
'Not a file-based container. Use "spindb delete" for server databases.',
|
|
42
44
|
}),
|
|
43
45
|
)
|
|
44
46
|
} else {
|
|
45
|
-
console.error(
|
|
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
|
|
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
|
|
78
|
+
await registry.remove(name)
|
|
72
79
|
|
|
73
80
|
if (options.json) {
|
|
74
81
|
console.log(
|
package/cli/commands/doctor.ts
CHANGED
|
@@ -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
|
|
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
|
+
)
|
package/cli/commands/edit.ts
CHANGED
|
@@ -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
|
-
//
|
|
36
|
-
if (engine
|
|
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 (
|
|
211
|
-
async function promptNewLocation(
|
|
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
|
-
|
|
228
|
-
|
|
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
|
|
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(
|
|
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
|
|
447
|
+
// Handle file-based engine relocate
|
|
442
448
|
if (options.relocate) {
|
|
443
|
-
if (config.engine
|
|
449
|
+
if (!isFileBasedEngine(config.engine)) {
|
|
444
450
|
console.error(
|
|
445
|
-
uiError(
|
|
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 =
|
|
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
|
|
564
|
+
// Update the container config and file-based registry
|
|
557
565
|
await containerManager.updateConfig(containerName, {
|
|
558
566
|
database: newPath,
|
|
559
567
|
})
|
|
560
|
-
await
|
|
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)) {
|