spindb 0.9.1 → 0.9.3
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 +5 -8
- package/cli/commands/attach.ts +108 -0
- package/cli/commands/backup.ts +13 -11
- package/cli/commands/clone.ts +14 -10
- package/cli/commands/config.ts +29 -29
- package/cli/commands/connect.ts +51 -39
- package/cli/commands/create.ts +65 -32
- package/cli/commands/delete.ts +8 -8
- package/cli/commands/deps.ts +17 -15
- package/cli/commands/detach.ts +100 -0
- package/cli/commands/doctor.ts +27 -13
- package/cli/commands/edit.ts +120 -57
- package/cli/commands/engines.ts +17 -15
- package/cli/commands/info.ts +8 -6
- package/cli/commands/list.ts +127 -18
- package/cli/commands/logs.ts +15 -11
- package/cli/commands/menu/backup-handlers.ts +52 -47
- package/cli/commands/menu/container-handlers.ts +164 -79
- package/cli/commands/menu/engine-handlers.ts +21 -11
- package/cli/commands/menu/index.ts +4 -4
- package/cli/commands/menu/shell-handlers.ts +34 -31
- package/cli/commands/menu/sql-handlers.ts +22 -16
- package/cli/commands/menu/update-handlers.ts +19 -17
- package/cli/commands/restore.ts +22 -20
- package/cli/commands/run.ts +20 -18
- package/cli/commands/self-update.ts +5 -5
- package/cli/commands/sqlite.ts +247 -0
- package/cli/commands/start.ts +11 -9
- package/cli/commands/stop.ts +9 -9
- package/cli/commands/url.ts +12 -9
- package/cli/helpers.ts +9 -4
- package/cli/index.ts +6 -0
- package/cli/ui/prompts.ts +12 -5
- package/cli/ui/spinner.ts +4 -4
- package/cli/ui/theme.ts +4 -4
- package/config/paths.ts +0 -8
- package/core/binary-manager.ts +5 -1
- package/core/config-manager.ts +32 -0
- package/core/container-manager.ts +5 -5
- package/core/platform-service.ts +3 -3
- package/core/start-with-retry.ts +6 -6
- package/core/transaction-manager.ts +6 -6
- package/engines/mysql/backup.ts +37 -13
- package/engines/mysql/index.ts +11 -11
- package/engines/mysql/restore.ts +4 -4
- package/engines/mysql/version-validator.ts +2 -2
- package/engines/postgresql/binary-manager.ts +17 -17
- package/engines/postgresql/index.ts +7 -2
- package/engines/postgresql/restore.ts +2 -2
- package/engines/postgresql/version-validator.ts +2 -2
- package/engines/sqlite/index.ts +30 -15
- package/engines/sqlite/registry.ts +64 -33
- package/engines/sqlite/scanner.ts +99 -0
- package/package.json +4 -3
- package/types/index.ts +21 -1
|
@@ -2,7 +2,7 @@ import { exec } from 'child_process'
|
|
|
2
2
|
import { promisify } from 'util'
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import { createSpinner } from '../../cli/ui/spinner'
|
|
5
|
-
import {
|
|
5
|
+
import { uiWarning, uiError, uiSuccess } from '../../cli/ui/theme'
|
|
6
6
|
import {
|
|
7
7
|
detectPackageManager as detectPM,
|
|
8
8
|
installEngineDependencies,
|
|
@@ -311,7 +311,7 @@ export async function installPostgresBinaries(): Promise<boolean> {
|
|
|
311
311
|
const packageManager = await detectPM()
|
|
312
312
|
if (!packageManager) {
|
|
313
313
|
spinner.fail('No supported package manager found')
|
|
314
|
-
console.log(
|
|
314
|
+
console.log(uiError('Please install PostgreSQL client tools manually:'))
|
|
315
315
|
|
|
316
316
|
// Show platform-specific instructions from the registry
|
|
317
317
|
const platform = getCurrentPlatform()
|
|
@@ -348,21 +348,21 @@ export async function installPostgresBinaries(): Promise<boolean> {
|
|
|
348
348
|
|
|
349
349
|
if (allSuccess) {
|
|
350
350
|
console.log()
|
|
351
|
-
console.log(
|
|
351
|
+
console.log(uiSuccess('PostgreSQL client tools installed successfully'))
|
|
352
352
|
return true
|
|
353
353
|
} else {
|
|
354
354
|
const failed = results.filter((r) => !r.success)
|
|
355
355
|
console.log()
|
|
356
|
-
console.log(
|
|
356
|
+
console.log(uiError('Some installations failed:'))
|
|
357
357
|
for (const f of failed) {
|
|
358
|
-
console.log(
|
|
358
|
+
console.log(uiError(` ${f.dependency.name}: ${f.error}`))
|
|
359
359
|
}
|
|
360
360
|
return false
|
|
361
361
|
}
|
|
362
362
|
} catch (error: unknown) {
|
|
363
363
|
console.log()
|
|
364
|
-
console.log(
|
|
365
|
-
console.log(
|
|
364
|
+
console.log(uiError('Failed to install PostgreSQL client tools'))
|
|
365
|
+
console.log(uiWarning('Please install manually'))
|
|
366
366
|
if (error instanceof Error) {
|
|
367
367
|
console.log(chalk.gray(`Error details: ${error.message}`))
|
|
368
368
|
}
|
|
@@ -408,7 +408,7 @@ export async function updatePostgresClientTools(): Promise<boolean> {
|
|
|
408
408
|
|
|
409
409
|
spinner.succeed('PostgreSQL client tools updated')
|
|
410
410
|
console.log(
|
|
411
|
-
|
|
411
|
+
uiSuccess(
|
|
412
412
|
`Client tools successfully linked to PostgreSQL ${latestMajor}`,
|
|
413
413
|
),
|
|
414
414
|
)
|
|
@@ -418,13 +418,13 @@ export async function updatePostgresClientTools(): Promise<boolean> {
|
|
|
418
418
|
// For other package managers, use the standard update
|
|
419
419
|
await execWithTimeout(packageManager.updateCommand('postgresql'), 120000)
|
|
420
420
|
spinner.succeed('PostgreSQL client tools updated')
|
|
421
|
-
console.log(
|
|
421
|
+
console.log(uiSuccess('Update completed successfully'))
|
|
422
422
|
return true
|
|
423
423
|
}
|
|
424
424
|
} catch (error: unknown) {
|
|
425
425
|
spinner.fail('Update failed')
|
|
426
|
-
console.log(
|
|
427
|
-
console.log(
|
|
426
|
+
console.log(uiError('Failed to update PostgreSQL client tools'))
|
|
427
|
+
console.log(uiWarning('Please update manually:'))
|
|
428
428
|
|
|
429
429
|
if (packageManager.name === 'brew') {
|
|
430
430
|
const olderVersions = ['14', '15', '16'].filter((v) => v !== latestMajor)
|
|
@@ -473,12 +473,12 @@ export async function updatePostgresBinaries(): Promise<boolean> {
|
|
|
473
473
|
try {
|
|
474
474
|
await execWithTimeout(packageManager.updateCommand('postgresql'), 120000) // 2 minute timeout
|
|
475
475
|
updateSpinner.succeed('PostgreSQL client tools updated')
|
|
476
|
-
console.log(
|
|
476
|
+
console.log(uiSuccess('Update completed successfully'))
|
|
477
477
|
return true
|
|
478
478
|
} catch (error: unknown) {
|
|
479
479
|
updateSpinner.fail('Update failed')
|
|
480
|
-
console.log(
|
|
481
|
-
console.log(
|
|
480
|
+
console.log(uiError('Failed to update PostgreSQL client tools'))
|
|
481
|
+
console.log(uiWarning('Please update manually:'))
|
|
482
482
|
console.log(` ${packageManager.updateCommand('postgresql')}`)
|
|
483
483
|
if (error instanceof Error) {
|
|
484
484
|
console.log(chalk.gray(`Error details: ${error.message}`))
|
|
@@ -505,7 +505,7 @@ export async function ensurePostgresBinary(
|
|
|
505
505
|
return { success: false, info: null, action: 'install_required' }
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
-
console.log(
|
|
508
|
+
console.log(uiWarning(`${binary} not found on your system`))
|
|
509
509
|
const success = await installPostgresBinaries()
|
|
510
510
|
if (!success) {
|
|
511
511
|
return { success: false, info: null, action: 'install_failed' }
|
|
@@ -527,13 +527,13 @@ export async function ensurePostgresBinary(
|
|
|
527
527
|
}
|
|
528
528
|
|
|
529
529
|
console.log(
|
|
530
|
-
|
|
530
|
+
uiWarning(
|
|
531
531
|
`Your ${binary} version (${info.version}) is incompatible with the dump file`,
|
|
532
532
|
),
|
|
533
533
|
)
|
|
534
534
|
if (info.requiredVersion) {
|
|
535
535
|
console.log(
|
|
536
|
-
|
|
536
|
+
uiWarning(`Required version: ${info.requiredVersion} or compatible`),
|
|
537
537
|
)
|
|
538
538
|
}
|
|
539
539
|
|
|
@@ -155,8 +155,13 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
155
155
|
|
|
156
156
|
// Configure max_connections after initdb creates postgresql.conf
|
|
157
157
|
const maxConnections =
|
|
158
|
-
(options.maxConnections as number) ||
|
|
159
|
-
|
|
158
|
+
(options.maxConnections as number) ||
|
|
159
|
+
getEngineDefaults('postgresql').maxConnections
|
|
160
|
+
await this.setConfigValue(
|
|
161
|
+
dataDir,
|
|
162
|
+
'max_connections',
|
|
163
|
+
String(maxConnections),
|
|
164
|
+
)
|
|
160
165
|
|
|
161
166
|
return dataDir
|
|
162
167
|
}
|
|
@@ -219,8 +219,8 @@ export async function restoreBackup(
|
|
|
219
219
|
format: detectedFormat,
|
|
220
220
|
...result,
|
|
221
221
|
}
|
|
222
|
-
} catch (
|
|
223
|
-
const e =
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const e = error as Error & { stdout?: string; stderr?: string }
|
|
224
224
|
// pg_restore often returns non-zero even on partial success
|
|
225
225
|
return {
|
|
226
226
|
format: detectedFormat,
|
|
@@ -135,11 +135,11 @@ export async function parseDumpVersion(
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
} catch (
|
|
138
|
+
} catch (error) {
|
|
139
139
|
logDebug('Failed to parse dump version', {
|
|
140
140
|
dumpPath,
|
|
141
141
|
format,
|
|
142
|
-
error:
|
|
142
|
+
error: error instanceof Error ? error.message : String(error),
|
|
143
143
|
})
|
|
144
144
|
}
|
|
145
145
|
|
package/engines/sqlite/index.ts
CHANGED
|
@@ -446,15 +446,17 @@ export class SQLiteEngine extends BaseEngine {
|
|
|
446
446
|
throw new Error('sqlite3 not found')
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
|
|
450
|
-
|
|
449
|
+
try {
|
|
450
|
+
// Pipe .dump output to file (avoids shell injection)
|
|
451
|
+
await this.dumpToFile(sqlite3, filePath, outputPath)
|
|
451
452
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
453
|
+
return { filePath: outputPath }
|
|
454
|
+
} finally {
|
|
455
|
+
// Clean up temp file if we downloaded it (even on error)
|
|
456
|
+
if (tempFile && existsSync(tempFile)) {
|
|
457
|
+
await unlink(tempFile)
|
|
458
|
+
}
|
|
455
459
|
}
|
|
456
|
-
|
|
457
|
-
return { filePath: outputPath }
|
|
458
460
|
}
|
|
459
461
|
|
|
460
462
|
/**
|
|
@@ -538,15 +540,28 @@ export class SQLiteEngine extends BaseEngine {
|
|
|
538
540
|
* Download a file from HTTP/HTTPS URL
|
|
539
541
|
*/
|
|
540
542
|
private async downloadFile(url: string, destPath: string): Promise<void> {
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
543
|
+
const controller = new AbortController()
|
|
544
|
+
const timeoutMs = 5 * 60 * 1000 // 5 minutes
|
|
545
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
const response = await fetch(url, { signal: controller.signal })
|
|
549
|
+
if (!response.ok) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`Failed to download: ${response.status} ${response.statusText}`,
|
|
552
|
+
)
|
|
553
|
+
}
|
|
547
554
|
|
|
548
|
-
|
|
549
|
-
|
|
555
|
+
const buffer = await response.arrayBuffer()
|
|
556
|
+
await writeFile(destPath, Buffer.from(buffer))
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
559
|
+
throw new Error('Download timed out after 5 minutes')
|
|
560
|
+
}
|
|
561
|
+
throw error
|
|
562
|
+
} finally {
|
|
563
|
+
clearTimeout(timeout)
|
|
564
|
+
}
|
|
550
565
|
}
|
|
551
566
|
|
|
552
567
|
/**
|
|
@@ -4,57 +4,38 @@
|
|
|
4
4
|
* Unlike PostgreSQL/MySQL which store containers in ~/.spindb/containers/,
|
|
5
5
|
* SQLite databases are stored in user project directories. This registry
|
|
6
6
|
* tracks the file paths of all SQLite databases managed by SpinDB.
|
|
7
|
+
*
|
|
8
|
+
* The registry is now stored in ~/.spindb/config.json under registry.sqlite
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { existsSync } from 'fs'
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { paths } from '../../config/paths'
|
|
13
|
-
import type { SQLiteRegistry, SQLiteRegistryEntry } from '../../types'
|
|
12
|
+
import { configManager } from '../../core/config-manager'
|
|
13
|
+
import type { SQLiteEngineRegistry, SQLiteRegistryEntry } from '../../types'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* SQLite Registry Manager
|
|
17
|
-
* Manages the
|
|
17
|
+
* Manages the registry that tracks external SQLite database files
|
|
18
|
+
* Data is stored in config.json under registry.sqlite
|
|
18
19
|
*/
|
|
19
20
|
class SQLiteRegistryManager {
|
|
20
|
-
private registryPath: string
|
|
21
|
-
|
|
22
|
-
constructor() {
|
|
23
|
-
this.registryPath = paths.getSqliteRegistryPath()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
21
|
/**
|
|
27
|
-
* Load the registry from
|
|
28
|
-
* Returns an empty registry if
|
|
22
|
+
* Load the registry from config.json
|
|
23
|
+
* Returns an empty registry if none exists
|
|
29
24
|
*/
|
|
30
|
-
async load(): Promise<
|
|
31
|
-
|
|
32
|
-
return { version: 1, entries: [] }
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
const content = await readFile(this.registryPath, 'utf8')
|
|
36
|
-
return JSON.parse(content) as SQLiteRegistry
|
|
37
|
-
} catch {
|
|
38
|
-
// If file is corrupted, return empty registry
|
|
39
|
-
return { version: 1, entries: [] }
|
|
40
|
-
}
|
|
25
|
+
async load(): Promise<SQLiteEngineRegistry> {
|
|
26
|
+
return configManager.getSqliteRegistry()
|
|
41
27
|
}
|
|
42
28
|
|
|
43
29
|
/**
|
|
44
|
-
* Save the registry to
|
|
45
|
-
* Creates the parent directory if it doesn't exist
|
|
30
|
+
* Save the registry to config.json
|
|
46
31
|
*/
|
|
47
|
-
async save(registry:
|
|
48
|
-
|
|
49
|
-
if (!existsSync(dir)) {
|
|
50
|
-
await mkdir(dir, { recursive: true })
|
|
51
|
-
}
|
|
52
|
-
await writeFile(this.registryPath, JSON.stringify(registry, null, 2))
|
|
32
|
+
async save(registry: SQLiteEngineRegistry): Promise<void> {
|
|
33
|
+
await configManager.saveSqliteRegistry(registry)
|
|
53
34
|
}
|
|
54
35
|
|
|
55
36
|
/**
|
|
56
37
|
* Add a new entry to the registry
|
|
57
|
-
* @throws Error if a container with the same name already exists
|
|
38
|
+
* @throws Error if a container with the same name or file path already exists
|
|
58
39
|
*/
|
|
59
40
|
async add(entry: SQLiteRegistryEntry): Promise<void> {
|
|
60
41
|
const registry = await this.load()
|
|
@@ -64,6 +45,13 @@ class SQLiteRegistryManager {
|
|
|
64
45
|
throw new Error(`SQLite container "${entry.name}" already exists`)
|
|
65
46
|
}
|
|
66
47
|
|
|
48
|
+
// Check for duplicate file path
|
|
49
|
+
if (registry.entries.some((e) => e.filePath === entry.filePath)) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`SQLite container for path "${entry.filePath}" already exists`,
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
67
55
|
registry.entries.push(entry)
|
|
68
56
|
await this.save(registry)
|
|
69
57
|
}
|
|
@@ -179,6 +167,49 @@ class SQLiteRegistryManager {
|
|
|
179
167
|
const registry = await this.load()
|
|
180
168
|
return registry.entries.find((e) => e.filePath === filePath) || null
|
|
181
169
|
}
|
|
170
|
+
|
|
171
|
+
// ============================================================
|
|
172
|
+
// Folder Ignore Methods
|
|
173
|
+
// ============================================================
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if a folder is in the ignore list
|
|
177
|
+
*/
|
|
178
|
+
async isFolderIgnored(folderPath: string): Promise<boolean> {
|
|
179
|
+
const registry = await this.load()
|
|
180
|
+
return folderPath in registry.ignoreFolders
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Add a folder to the ignore list
|
|
185
|
+
*/
|
|
186
|
+
async addIgnoreFolder(folderPath: string): Promise<void> {
|
|
187
|
+
const registry = await this.load()
|
|
188
|
+
registry.ignoreFolders[folderPath] = true
|
|
189
|
+
await this.save(registry)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Remove a folder from the ignore list
|
|
194
|
+
* Returns true if the folder was in the list and removed, false otherwise
|
|
195
|
+
*/
|
|
196
|
+
async removeIgnoreFolder(folderPath: string): Promise<boolean> {
|
|
197
|
+
const registry = await this.load()
|
|
198
|
+
if (folderPath in registry.ignoreFolders) {
|
|
199
|
+
delete registry.ignoreFolders[folderPath]
|
|
200
|
+
await this.save(registry)
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* List all ignored folders
|
|
208
|
+
*/
|
|
209
|
+
async listIgnoredFolders(): Promise<string[]> {
|
|
210
|
+
const registry = await this.load()
|
|
211
|
+
return Object.keys(registry.ignoreFolders)
|
|
212
|
+
}
|
|
182
213
|
}
|
|
183
214
|
|
|
184
215
|
// Export singleton instance
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Scanner
|
|
3
|
+
*
|
|
4
|
+
* Scans directories for unregistered SQLite database files.
|
|
5
|
+
* Used to detect SQLite databases in the current working directory
|
|
6
|
+
* that are not yet registered with SpinDB.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readdir } from 'fs/promises'
|
|
10
|
+
import { existsSync } from 'fs'
|
|
11
|
+
import { resolve } from 'path'
|
|
12
|
+
import { sqliteRegistry } from './registry'
|
|
13
|
+
|
|
14
|
+
export type UnregisteredFile = {
|
|
15
|
+
fileName: string
|
|
16
|
+
absolutePath: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Scan a directory for unregistered SQLite files
|
|
21
|
+
* Returns files with .sqlite, .sqlite3, or .db extensions
|
|
22
|
+
* that are not already in the registry
|
|
23
|
+
*
|
|
24
|
+
* @param directory Directory to scan (defaults to CWD)
|
|
25
|
+
* @returns Array of unregistered SQLite files
|
|
26
|
+
*/
|
|
27
|
+
export async function scanForUnregisteredSqliteFiles(
|
|
28
|
+
directory: string = process.cwd(),
|
|
29
|
+
): Promise<UnregisteredFile[]> {
|
|
30
|
+
const absoluteDir = resolve(directory)
|
|
31
|
+
|
|
32
|
+
// Check if folder is ignored
|
|
33
|
+
if (await sqliteRegistry.isFolderIgnored(absoluteDir)) {
|
|
34
|
+
return []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check if directory exists
|
|
38
|
+
if (!existsSync(absoluteDir)) {
|
|
39
|
+
return []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Get all files in directory
|
|
44
|
+
const entries = await readdir(absoluteDir, { withFileTypes: true })
|
|
45
|
+
|
|
46
|
+
// Filter for SQLite files
|
|
47
|
+
const sqliteFiles = entries
|
|
48
|
+
.filter((e) => e.isFile())
|
|
49
|
+
.filter((e) => /\.(sqlite3?|db)$/i.test(e.name))
|
|
50
|
+
.map((e) => ({
|
|
51
|
+
fileName: e.name,
|
|
52
|
+
absolutePath: resolve(absoluteDir, e.name),
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
// Filter out already registered files
|
|
56
|
+
const unregistered: UnregisteredFile[] = []
|
|
57
|
+
for (const file of sqliteFiles) {
|
|
58
|
+
if (!(await sqliteRegistry.isPathRegistered(file.absolutePath))) {
|
|
59
|
+
unregistered.push(file)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return unregistered
|
|
64
|
+
} catch {
|
|
65
|
+
// If we can't read the directory, return empty
|
|
66
|
+
return []
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Derive a valid container name from a filename
|
|
72
|
+
* Removes extension and converts to valid container name format:
|
|
73
|
+
* - Must start with a letter
|
|
74
|
+
* - Can contain letters, numbers, hyphens, underscores
|
|
75
|
+
*
|
|
76
|
+
* @param fileName The SQLite filename (e.g., "my-database.sqlite")
|
|
77
|
+
* @returns A valid container name (e.g., "my-database")
|
|
78
|
+
*/
|
|
79
|
+
export function deriveContainerName(fileName: string): string {
|
|
80
|
+
// Remove extension
|
|
81
|
+
const base = fileName.replace(/\.(sqlite3?|db)$/i, '')
|
|
82
|
+
|
|
83
|
+
// Convert to valid container name (alphanumeric, hyphens, underscores)
|
|
84
|
+
// Replace invalid chars with hyphens
|
|
85
|
+
let name = base.replace(/[^a-zA-Z0-9_-]/g, '-')
|
|
86
|
+
|
|
87
|
+
// Ensure starts with letter
|
|
88
|
+
if (!/^[a-zA-Z]/.test(name)) {
|
|
89
|
+
name = 'db-' + name
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Remove consecutive hyphens
|
|
93
|
+
name = name.replace(/-+/g, '-')
|
|
94
|
+
|
|
95
|
+
// Trim leading/trailing hyphens
|
|
96
|
+
name = name.replace(/^-+|-+$/g, '')
|
|
97
|
+
|
|
98
|
+
return name || 'sqlite-db'
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spindb",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Spin up local database containers without Docker. A DBngin-like CLI for PostgreSQL and MySQL.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "tsx cli/bin.ts",
|
|
11
11
|
"dev": "tsx watch cli/bin.ts",
|
|
12
|
-
"test": "pnpm test:pg && pnpm test:mysql",
|
|
12
|
+
"test": "pnpm test:pg && pnpm test:mysql && pnpm test:sqlite",
|
|
13
13
|
"test:pg": "node --import tsx --test tests/integration/postgresql.test.ts",
|
|
14
14
|
"test:mysql": "node --import tsx --test tests/integration/mysql.test.ts",
|
|
15
|
+
"test:sqlite": "node --import tsx --test tests/integration/sqlite.test.ts",
|
|
15
16
|
"test:unit": "node --import tsx --test tests/unit/*.test.ts",
|
|
16
|
-
"test:integration": "pnpm test:pg && pnpm test:mysql",
|
|
17
|
+
"test:integration": "pnpm test:pg && pnpm test:mysql && pnpm test:sqlite",
|
|
17
18
|
"test:all": "pnpm test:unit && pnpm test:integration",
|
|
18
19
|
"format": "prettier --write .",
|
|
19
20
|
"lint": "tsc --noEmit && eslint .",
|
package/types/index.ts
CHANGED
|
@@ -149,6 +149,8 @@ export type SpinDBConfig = {
|
|
|
149
149
|
litecli?: BinaryConfig
|
|
150
150
|
usql?: BinaryConfig
|
|
151
151
|
}
|
|
152
|
+
// Engine registries (for file-based databases like SQLite)
|
|
153
|
+
registry?: EngineRegistries
|
|
152
154
|
// Default settings
|
|
153
155
|
defaults?: {
|
|
154
156
|
engine?: Engine
|
|
@@ -177,7 +179,25 @@ export type SQLiteRegistryEntry = {
|
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
/**
|
|
180
|
-
* SQLite registry stored
|
|
182
|
+
* SQLite engine registry stored in config.json under registry.sqlite
|
|
183
|
+
* Includes entries and folder ignore list for CWD scanning
|
|
184
|
+
*/
|
|
185
|
+
export type SQLiteEngineRegistry = {
|
|
186
|
+
version: 1
|
|
187
|
+
entries: SQLiteRegistryEntry[]
|
|
188
|
+
ignoreFolders: Record<string, true> // O(1) lookup for ignored folders
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Engine registries stored in config.json
|
|
193
|
+
* Currently only SQLite uses this (file-based databases)
|
|
194
|
+
*/
|
|
195
|
+
export type EngineRegistries = {
|
|
196
|
+
sqlite?: SQLiteEngineRegistry
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @deprecated Use SQLiteEngineRegistry instead - now stored in config.json
|
|
181
201
|
*/
|
|
182
202
|
export type SQLiteRegistry = {
|
|
183
203
|
version: 1
|