spindb 0.34.3 β 0.35.2
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 +4 -4
- package/cli/commands/attach.ts +38 -9
- package/cli/commands/backups.ts +5 -0
- package/cli/commands/connect.ts +6 -6
- package/cli/commands/create.ts +22 -1
- 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/engines.ts +51 -21
- package/cli/commands/info.ts +26 -16
- package/cli/commands/list.ts +44 -26
- package/cli/commands/menu/container-handlers.ts +17 -1
- package/cli/commands/menu/engine-handlers.ts +48 -29
- package/cli/commands/menu/update-handlers.ts +2 -2
- package/cli/commands/sqlite.ts +21 -0
- package/cli/index.ts +2 -0
- package/cli/ui/theme.ts +5 -2
- package/config/engines.json +2 -2
- package/core/base-binary-manager.ts +6 -2
- package/core/base-document-binary-manager.ts +5 -2
- package/core/base-embedded-binary-manager.ts +5 -2
- package/core/base-server-binary-manager.ts +5 -2
- package/core/hostdb-client.ts +157 -22
- package/core/hostdb-metadata.ts +67 -43
- package/engines/clickhouse/binary-urls.ts +1 -1
- package/engines/cockroachdb/binary-urls.ts +9 -7
- package/engines/cockroachdb/hostdb-releases.ts +18 -106
- package/engines/cockroachdb/version-maps.ts +1 -1
- package/engines/couchdb/binary-urls.ts +1 -1
- package/engines/duckdb/binary-urls.ts +1 -1
- package/engines/duckdb/index.ts +4 -74
- package/engines/duckdb/scanner.ts +22 -0
- package/engines/ferretdb/README.md +76 -38
- package/engines/ferretdb/backup.ts +18 -10
- package/engines/ferretdb/binary-manager.ts +233 -35
- package/engines/ferretdb/binary-urls.ts +69 -24
- package/engines/ferretdb/index.ts +424 -213
- package/engines/ferretdb/restore.ts +23 -16
- package/engines/ferretdb/version-maps.ts +36 -8
- package/engines/file-based-utils.ts +262 -0
- package/engines/index.ts +3 -4
- package/engines/influxdb/binary-urls.ts +1 -1
- package/engines/mariadb/binary-urls.ts +2 -2
- package/engines/meilisearch/binary-urls.ts +1 -1
- package/engines/mysql/binary-urls.ts +2 -2
- package/engines/postgresql/binary-urls.ts +1 -1
- package/engines/qdrant/binary-urls.ts +1 -1
- package/engines/questdb/binary-manager.ts +16 -9
- package/engines/questdb/binary-urls.ts +9 -10
- package/engines/questdb/hostdb-releases.ts +19 -97
- package/engines/questdb/version-maps.ts +2 -2
- package/engines/redis/binary-urls.ts +1 -8
- package/engines/sqlite/binary-urls.ts +1 -1
- package/engines/sqlite/index.ts +4 -74
- package/engines/sqlite/scanner.ts +11 -88
- package/engines/surrealdb/binary-urls.ts +9 -7
- package/engines/surrealdb/hostdb-releases.ts +18 -106
- package/engines/surrealdb/version-maps.ts +1 -1
- package/engines/typedb/binary-urls.ts +10 -8
- package/engines/typedb/hostdb-releases.ts +18 -113
- package/engines/typedb/version-maps.ts +1 -1
- package/engines/valkey/binary-urls.ts +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ SpinDB supports **18 database engines** across **5 platform architectures**βal
|
|
|
38
38
|
| πͺΆ **SQLite** | Embedded SQL | β
| β
| β
| β
| β
|
|
|
39
39
|
| π¦ **DuckDB** | Embedded OLAP | β
| β
| β
| β
| β
|
|
|
40
40
|
| π **MongoDB** | Document Store | β
| β
| β
| β
| β
|
|
|
41
|
-
| π¦ **FerretDB** | Document Store | β
| β
| β
| β
|
|
|
41
|
+
| π¦ **FerretDB** | Document Store | β
| β
| β
| β
| β οΈ |
|
|
42
42
|
| π΄ **Redis** | Key-Value | β
| β
| β
| β
| β
|
|
|
43
43
|
| π· **Valkey** | Key-Value | β
| β
| β
| β
| β
|
|
|
44
44
|
| π **ClickHouse** | Columnar OLAP | β
| β
| β
| β
| β |
|
|
@@ -51,9 +51,9 @@ SpinDB supports **18 database engines** across **5 platform architectures**βal
|
|
|
51
51
|
| π€ **TypeDB** | Knowledge Graph | β
| β
| β
| β
| β
|
|
|
52
52
|
| π **InfluxDB** | Time-Series | β
| β
| β
| β
| β
|
|
|
53
53
|
|
|
54
|
-
**
|
|
54
|
+
**89 combinations. One CLI. Zero configuration.**
|
|
55
55
|
|
|
56
|
-
> ClickHouse
|
|
56
|
+
> ClickHouse is available on Windows via WSL. FerretDB v1 is natively supported on Windows (uses plain PostgreSQL backend); v2 requires macOS/Linux.
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
@@ -272,7 +272,7 @@ See [DEPLOY.md](DEPLOY.md) for comprehensive deployment documentation.
|
|
|
272
272
|
|
|
273
273
|
- **Local only** - Databases bind to `127.0.0.1`. Remote connection support planned for v1.1.
|
|
274
274
|
- **ClickHouse Windows** - Not supported (hostdb doesn't build for Windows).
|
|
275
|
-
- **FerretDB Windows** -
|
|
275
|
+
- **FerretDB Windows** - v1 supported natively (plain PostgreSQL backend). v2 not supported (postgresql-documentdb has startup issues); use WSL for v2.
|
|
276
276
|
- **Qdrant, Meilisearch, CouchDB** - Use REST API instead of CLI shell. Access via HTTP at the configured port.
|
|
277
277
|
|
|
278
278
|
---
|
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/create.ts
CHANGED
|
@@ -22,7 +22,11 @@ import { startWithRetry } from '../../core/start-with-retry'
|
|
|
22
22
|
import { TransactionManager } from '../../core/transaction-manager'
|
|
23
23
|
import { isValidDatabaseName, exitWithError } from '../../core/error-handler'
|
|
24
24
|
import { resolve } from 'path'
|
|
25
|
-
import { Engine } from '../../types'
|
|
25
|
+
import { Engine, Platform } from '../../types'
|
|
26
|
+
import {
|
|
27
|
+
FERRETDB_VERSION_MAP,
|
|
28
|
+
isV1 as isFerretDBv1,
|
|
29
|
+
} from '../../engines/ferretdb/version-maps'
|
|
26
30
|
import type { BaseEngine } from '../../engines/base-engine'
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -535,6 +539,23 @@ export const createCommand = new Command('create')
|
|
|
535
539
|
database = answers.database
|
|
536
540
|
}
|
|
537
541
|
|
|
542
|
+
// FerretDB: force v1 on Windows (v2 requires postgresql-documentdb, not available on Windows)
|
|
543
|
+
// Runs after both CLI and interactive paths have resolved engine + version
|
|
544
|
+
if (
|
|
545
|
+
engine === Engine.FerretDB &&
|
|
546
|
+
!isFerretDBv1(version) &&
|
|
547
|
+
platformService.getPlatformInfo().platform === Platform.Win32
|
|
548
|
+
) {
|
|
549
|
+
version = FERRETDB_VERSION_MAP['1']
|
|
550
|
+
if (!options.json) {
|
|
551
|
+
console.log(
|
|
552
|
+
chalk.yellow(
|
|
553
|
+
` FerretDB v2 is not supported on Windows β using v1 (${version})`,
|
|
554
|
+
),
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
538
559
|
// Redis/Valkey use numbered databases (0-15), default to "0"
|
|
539
560
|
// Other engines default to container name (with hyphens replaced by underscores for SQL compatibility)
|
|
540
561
|
if (engine === Engine.Redis || engine === Engine.Valkey) {
|
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
|
+
)
|