spindb 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -7
- package/cli/commands/connect.ts +115 -14
- package/cli/commands/create.ts +113 -1
- package/cli/commands/doctor.ts +319 -0
- package/cli/commands/edit.ts +203 -5
- package/cli/commands/info.ts +79 -26
- package/cli/commands/list.ts +64 -9
- package/cli/commands/menu/backup-handlers.ts +28 -13
- package/cli/commands/menu/container-handlers.ts +410 -120
- package/cli/commands/menu/index.ts +5 -1
- package/cli/commands/menu/shell-handlers.ts +105 -21
- package/cli/commands/menu/sql-handlers.ts +16 -4
- package/cli/commands/menu/update-handlers.ts +278 -0
- package/cli/commands/run.ts +27 -11
- package/cli/commands/url.ts +17 -9
- package/cli/constants.ts +1 -0
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +165 -14
- package/config/engine-defaults.ts +14 -0
- package/config/os-dependencies.ts +66 -0
- package/config/paths.ts +8 -0
- package/core/container-manager.ts +119 -11
- package/core/dependency-manager.ts +18 -0
- package/engines/index.ts +4 -0
- package/engines/sqlite/index.ts +597 -0
- package/engines/sqlite/registry.ts +185 -0
- package/package.json +3 -2
- package/types/index.ts +26 -0
package/cli/ui/prompts.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import inquirer from 'inquirer'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import ora from 'ora'
|
|
4
|
+
import { existsSync, statSync } from 'fs'
|
|
5
|
+
import { resolve, join } from 'path'
|
|
6
|
+
import { homedir } from 'os'
|
|
4
7
|
import { listEngines, getEngine } from '../../engines'
|
|
5
8
|
import { defaults, getEngineDefaults } from '../../config/defaults'
|
|
6
9
|
import { installPostgresBinaries } from '../../engines/postgresql/binary-manager'
|
|
@@ -204,35 +207,69 @@ export async function promptConfirm(
|
|
|
204
207
|
|
|
205
208
|
/**
|
|
206
209
|
* Prompt for container selection from a list
|
|
210
|
+
* @param containers - List of containers to choose from
|
|
211
|
+
* @param message - Prompt message
|
|
212
|
+
* @param options - Optional settings
|
|
213
|
+
* @param options.includeBack - Include a back option (returns null when selected)
|
|
207
214
|
*/
|
|
208
215
|
export async function promptContainerSelect(
|
|
209
216
|
containers: ContainerConfig[],
|
|
210
217
|
message: string = 'Select container:',
|
|
218
|
+
options: { includeBack?: boolean } = {},
|
|
211
219
|
): Promise<string | null> {
|
|
212
220
|
if (containers.length === 0) {
|
|
213
221
|
return null
|
|
214
222
|
}
|
|
215
223
|
|
|
224
|
+
type Choice = { name: string; value: string; short?: string }
|
|
225
|
+
const choices: Choice[] = containers.map((c) => ({
|
|
226
|
+
name: `${c.name} ${chalk.gray(`(${getEngineIcon(c.engine)} ${c.engine} ${c.version}, port ${c.port})`)} ${
|
|
227
|
+
c.status === 'running'
|
|
228
|
+
? chalk.green('● running')
|
|
229
|
+
: chalk.gray('○ stopped')
|
|
230
|
+
}`,
|
|
231
|
+
value: c.name,
|
|
232
|
+
short: c.name,
|
|
233
|
+
}))
|
|
234
|
+
|
|
235
|
+
if (options.includeBack) {
|
|
236
|
+
choices.push({ name: `${chalk.blue('←')} Back`, value: '__back__' })
|
|
237
|
+
}
|
|
238
|
+
|
|
216
239
|
const { container } = await inquirer.prompt<{ container: string }>([
|
|
217
240
|
{
|
|
218
241
|
type: 'list',
|
|
219
242
|
name: 'container',
|
|
220
243
|
message,
|
|
221
|
-
choices
|
|
222
|
-
name: `${c.name} ${chalk.gray(`(${getEngineIcon(c.engine)} ${c.engine} ${c.version}, port ${c.port})`)} ${
|
|
223
|
-
c.status === 'running'
|
|
224
|
-
? chalk.green('● running')
|
|
225
|
-
: chalk.gray('○ stopped')
|
|
226
|
-
}`,
|
|
227
|
-
value: c.name,
|
|
228
|
-
short: c.name,
|
|
229
|
-
})),
|
|
244
|
+
choices,
|
|
230
245
|
},
|
|
231
246
|
])
|
|
232
247
|
|
|
248
|
+
if (container === '__back__') {
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
|
|
233
252
|
return container
|
|
234
253
|
}
|
|
235
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Sanitize a string to be a valid database name
|
|
257
|
+
* Replaces invalid characters with underscores
|
|
258
|
+
*/
|
|
259
|
+
function sanitizeDatabaseName(name: string): string {
|
|
260
|
+
// Replace invalid characters with underscores
|
|
261
|
+
let sanitized = name.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
262
|
+
// Ensure it starts with a letter or underscore
|
|
263
|
+
if (sanitized && !/^[a-zA-Z_]/.test(sanitized)) {
|
|
264
|
+
sanitized = '_' + sanitized
|
|
265
|
+
}
|
|
266
|
+
// Collapse multiple underscores
|
|
267
|
+
sanitized = sanitized.replace(/_+/g, '_')
|
|
268
|
+
// Trim trailing underscores
|
|
269
|
+
sanitized = sanitized.replace(/_+$/, '')
|
|
270
|
+
return sanitized
|
|
271
|
+
}
|
|
272
|
+
|
|
236
273
|
/**
|
|
237
274
|
* Prompt for database name
|
|
238
275
|
* @param defaultName - Default value for the database name
|
|
@@ -246,12 +283,15 @@ export async function promptDatabaseName(
|
|
|
246
283
|
const label =
|
|
247
284
|
engine === 'mysql' ? 'Database (schema) name:' : 'Database name:'
|
|
248
285
|
|
|
286
|
+
// Sanitize the default name to ensure it's valid
|
|
287
|
+
const sanitizedDefault = defaultName ? sanitizeDatabaseName(defaultName) : undefined
|
|
288
|
+
|
|
249
289
|
const { database } = await inquirer.prompt<{ database: string }>([
|
|
250
290
|
{
|
|
251
291
|
type: 'input',
|
|
252
292
|
name: 'database',
|
|
253
293
|
message: label,
|
|
254
|
-
default:
|
|
294
|
+
default: sanitizedDefault,
|
|
255
295
|
validate: (input: string) => {
|
|
256
296
|
if (!input) return 'Database name is required'
|
|
257
297
|
// PostgreSQL database naming rules (also valid for MySQL)
|
|
@@ -362,6 +402,111 @@ export type CreateOptions = {
|
|
|
362
402
|
version: string
|
|
363
403
|
port: number
|
|
364
404
|
database: string
|
|
405
|
+
path?: string // SQLite file path
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Prompt for SQLite database file location
|
|
410
|
+
* Similar to the relocate logic in container-handlers.ts
|
|
411
|
+
*/
|
|
412
|
+
export async function promptSqlitePath(
|
|
413
|
+
containerName: string,
|
|
414
|
+
): Promise<string | undefined> {
|
|
415
|
+
const defaultPath = `./${containerName}.sqlite`
|
|
416
|
+
|
|
417
|
+
console.log(chalk.gray(' SQLite databases are stored as files in your project directory.'))
|
|
418
|
+
console.log(chalk.gray(` Default: ${defaultPath}`))
|
|
419
|
+
console.log()
|
|
420
|
+
|
|
421
|
+
const { useDefault } = await inquirer.prompt<{ useDefault: string }>([
|
|
422
|
+
{
|
|
423
|
+
type: 'list',
|
|
424
|
+
name: 'useDefault',
|
|
425
|
+
message: 'Where should the database file be created?',
|
|
426
|
+
choices: [
|
|
427
|
+
{ name: `Use default location (${defaultPath})`, value: 'default' },
|
|
428
|
+
{ name: 'Specify custom path', value: 'custom' },
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
])
|
|
432
|
+
|
|
433
|
+
if (useDefault === 'default') {
|
|
434
|
+
return undefined // Use default
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const { inputPath } = await inquirer.prompt<{ inputPath: string }>([
|
|
438
|
+
{
|
|
439
|
+
type: 'input',
|
|
440
|
+
name: 'inputPath',
|
|
441
|
+
message: 'File path:',
|
|
442
|
+
default: defaultPath,
|
|
443
|
+
validate: (input: string) => {
|
|
444
|
+
if (!input) return 'Path is required'
|
|
445
|
+
return true
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
])
|
|
449
|
+
|
|
450
|
+
// Expand ~ to home directory
|
|
451
|
+
let expandedPath = inputPath
|
|
452
|
+
if (inputPath === '~') {
|
|
453
|
+
expandedPath = homedir()
|
|
454
|
+
} else if (inputPath.startsWith('~/')) {
|
|
455
|
+
expandedPath = join(homedir(), inputPath.slice(2))
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Convert relative paths to absolute
|
|
459
|
+
if (!expandedPath.startsWith('/')) {
|
|
460
|
+
expandedPath = resolve(process.cwd(), expandedPath)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Check if path looks like a file (has db extension) or directory
|
|
464
|
+
const hasDbExtension = /\.(sqlite3?|db)$/i.test(expandedPath)
|
|
465
|
+
|
|
466
|
+
// Treat as directory if:
|
|
467
|
+
// - ends with /
|
|
468
|
+
// - exists and is a directory
|
|
469
|
+
// - doesn't have a database file extension (assume it's a directory path)
|
|
470
|
+
const isDirectory =
|
|
471
|
+
expandedPath.endsWith('/') ||
|
|
472
|
+
(existsSync(expandedPath) && statSync(expandedPath).isDirectory()) ||
|
|
473
|
+
!hasDbExtension
|
|
474
|
+
|
|
475
|
+
let finalPath: string
|
|
476
|
+
if (isDirectory) {
|
|
477
|
+
// Remove trailing slash if present, then append filename
|
|
478
|
+
const dirPath = expandedPath.endsWith('/')
|
|
479
|
+
? expandedPath.slice(0, -1)
|
|
480
|
+
: expandedPath
|
|
481
|
+
finalPath = join(dirPath, `${containerName}.sqlite`)
|
|
482
|
+
} else {
|
|
483
|
+
finalPath = expandedPath
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Check if file already exists
|
|
487
|
+
if (existsSync(finalPath)) {
|
|
488
|
+
console.log(chalk.yellow(` Warning: File already exists: ${finalPath}`))
|
|
489
|
+
const { overwrite } = await inquirer.prompt<{ overwrite: string }>([
|
|
490
|
+
{
|
|
491
|
+
type: 'list',
|
|
492
|
+
name: 'overwrite',
|
|
493
|
+
message: 'A file already exists at this location. What would you like to do?',
|
|
494
|
+
choices: [
|
|
495
|
+
{ name: 'Choose a different path', value: 'different' },
|
|
496
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
])
|
|
500
|
+
|
|
501
|
+
if (overwrite === 'cancel') {
|
|
502
|
+
throw new Error('Creation cancelled')
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Recursively prompt again
|
|
506
|
+
return promptSqlitePath(containerName)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return finalPath
|
|
365
510
|
}
|
|
366
511
|
|
|
367
512
|
/**
|
|
@@ -375,11 +520,17 @@ export async function promptCreateOptions(): Promise<CreateOptions> {
|
|
|
375
520
|
const name = await promptContainerName()
|
|
376
521
|
const database = await promptDatabaseName(name, engine) // Default to container name
|
|
377
522
|
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
523
|
+
// SQLite is file-based, no port needed but needs path
|
|
524
|
+
let port = 0
|
|
525
|
+
let path: string | undefined
|
|
526
|
+
if (engine === 'sqlite') {
|
|
527
|
+
path = await promptSqlitePath(name)
|
|
528
|
+
} else {
|
|
529
|
+
const engineDefaults = getEngineDefaults(engine)
|
|
530
|
+
port = await promptPort(engineDefaults.defaultPort)
|
|
531
|
+
}
|
|
381
532
|
|
|
382
|
-
return { name, engine, version, port, database }
|
|
533
|
+
return { name, engine, version, port, database, path }
|
|
383
534
|
}
|
|
384
535
|
|
|
385
536
|
/**
|
|
@@ -59,6 +59,20 @@ export const engineDefaults: Record<string, EngineDefaults> = {
|
|
|
59
59
|
clientTools: ['mysql', 'mysqldump', 'mysqlpump'],
|
|
60
60
|
maxConnections: 200, // Higher than default 151 for parallel builds
|
|
61
61
|
},
|
|
62
|
+
sqlite: {
|
|
63
|
+
defaultVersion: '3',
|
|
64
|
+
defaultPort: 0, // File-based, no port
|
|
65
|
+
portRange: { start: 0, end: 0 }, // N/A
|
|
66
|
+
supportedVersions: ['3'],
|
|
67
|
+
latestVersion: '3',
|
|
68
|
+
superuser: '', // No authentication
|
|
69
|
+
connectionScheme: 'sqlite',
|
|
70
|
+
logFileName: '', // No log file
|
|
71
|
+
pidFileName: '', // No PID file (no server process)
|
|
72
|
+
dataSubdir: '', // File is the data
|
|
73
|
+
clientTools: ['sqlite3'],
|
|
74
|
+
maxConnections: 0, // N/A - file-based
|
|
75
|
+
},
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
/**
|
|
@@ -289,6 +289,42 @@ const mysqlDependencies: EngineDependencies = {
|
|
|
289
289
|
],
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
// =============================================================================
|
|
293
|
+
// SQLite Dependencies
|
|
294
|
+
// =============================================================================
|
|
295
|
+
|
|
296
|
+
const sqliteDependencies: EngineDependencies = {
|
|
297
|
+
engine: 'sqlite',
|
|
298
|
+
displayName: 'SQLite',
|
|
299
|
+
dependencies: [
|
|
300
|
+
{
|
|
301
|
+
name: 'sqlite3',
|
|
302
|
+
binary: 'sqlite3',
|
|
303
|
+
description: 'SQLite command-line interface',
|
|
304
|
+
packages: {
|
|
305
|
+
brew: { package: 'sqlite' },
|
|
306
|
+
apt: { package: 'sqlite3' },
|
|
307
|
+
yum: { package: 'sqlite' },
|
|
308
|
+
dnf: { package: 'sqlite' },
|
|
309
|
+
pacman: { package: 'sqlite' },
|
|
310
|
+
},
|
|
311
|
+
manualInstall: {
|
|
312
|
+
darwin: [
|
|
313
|
+
'Install Homebrew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
|
|
314
|
+
'Then run: brew install sqlite',
|
|
315
|
+
'Note: macOS includes sqlite3 by default in /usr/bin/sqlite3',
|
|
316
|
+
],
|
|
317
|
+
linux: [
|
|
318
|
+
'Debian/Ubuntu: sudo apt install sqlite3',
|
|
319
|
+
'CentOS/RHEL: sudo yum install sqlite',
|
|
320
|
+
'Fedora: sudo dnf install sqlite',
|
|
321
|
+
'Arch: sudo pacman -S sqlite',
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
}
|
|
327
|
+
|
|
292
328
|
// =============================================================================
|
|
293
329
|
// Optional Tools (engine-agnostic)
|
|
294
330
|
// =============================================================================
|
|
@@ -381,6 +417,35 @@ export const mycliDependency: Dependency = {
|
|
|
381
417
|
},
|
|
382
418
|
}
|
|
383
419
|
|
|
420
|
+
/**
|
|
421
|
+
* litecli - SQLite CLI with auto-completion and syntax highlighting
|
|
422
|
+
* https://github.com/dbcli/litecli
|
|
423
|
+
*/
|
|
424
|
+
export const litecliDependency: Dependency = {
|
|
425
|
+
name: 'litecli',
|
|
426
|
+
binary: 'litecli',
|
|
427
|
+
description:
|
|
428
|
+
'SQLite CLI with intelligent auto-completion and syntax highlighting',
|
|
429
|
+
packages: {
|
|
430
|
+
brew: { package: 'litecli' },
|
|
431
|
+
apt: { package: 'litecli' },
|
|
432
|
+
dnf: { package: 'litecli' },
|
|
433
|
+
yum: { package: 'litecli' },
|
|
434
|
+
pacman: { package: 'litecli' },
|
|
435
|
+
},
|
|
436
|
+
manualInstall: {
|
|
437
|
+
darwin: [
|
|
438
|
+
'Install with Homebrew: brew install litecli',
|
|
439
|
+
'Or with pip: pip install litecli',
|
|
440
|
+
],
|
|
441
|
+
linux: [
|
|
442
|
+
'Debian/Ubuntu: sudo apt install litecli',
|
|
443
|
+
'Fedora: sudo dnf install litecli',
|
|
444
|
+
'Or with pip: pip install litecli',
|
|
445
|
+
],
|
|
446
|
+
},
|
|
447
|
+
}
|
|
448
|
+
|
|
384
449
|
// =============================================================================
|
|
385
450
|
// Registry
|
|
386
451
|
// =============================================================================
|
|
@@ -391,6 +456,7 @@ export const mycliDependency: Dependency = {
|
|
|
391
456
|
export const engineDependencies: EngineDependencies[] = [
|
|
392
457
|
postgresqlDependencies,
|
|
393
458
|
mysqlDependencies,
|
|
459
|
+
sqliteDependencies,
|
|
394
460
|
]
|
|
395
461
|
|
|
396
462
|
/**
|
package/config/paths.ts
CHANGED
|
@@ -114,4 +114,12 @@ export const paths = {
|
|
|
114
114
|
getEngineContainersPath(engine: string): string {
|
|
115
115
|
return join(this.containers, engine)
|
|
116
116
|
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get path for SQLite registry file
|
|
120
|
+
* SQLite uses a registry (not container directories) since databases are stored externally
|
|
121
|
+
*/
|
|
122
|
+
getSqliteRegistryPath(): string {
|
|
123
|
+
return join(this.root, 'sqlite-registry.json')
|
|
124
|
+
},
|
|
117
125
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync } from 'fs'
|
|
2
|
-
import { mkdir, readdir, readFile, writeFile, rm, cp } from 'fs/promises'
|
|
2
|
+
import { mkdir, readdir, readFile, writeFile, rm, cp, unlink } from 'fs/promises'
|
|
3
3
|
import { paths } from '../config/paths'
|
|
4
4
|
import { processManager } from './process-manager'
|
|
5
5
|
import { portManager } from './port-manager'
|
|
6
6
|
import { getEngineDefaults, getSupportedEngines } from '../config/defaults'
|
|
7
7
|
import { getEngine } from '../engines'
|
|
8
|
+
import { sqliteRegistry } from '../engines/sqlite/registry'
|
|
8
9
|
import type { ContainerConfig } from '../types'
|
|
9
10
|
import { Engine } from '../types'
|
|
10
11
|
|
|
@@ -74,6 +75,11 @@ export class ContainerManager {
|
|
|
74
75
|
const { engine } = options || {}
|
|
75
76
|
|
|
76
77
|
if (engine) {
|
|
78
|
+
// SQLite uses registry instead of filesystem
|
|
79
|
+
if (engine === 'sqlite') {
|
|
80
|
+
return this.getSqliteConfig(name)
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
// Look in specific engine directory
|
|
78
84
|
const configPath = paths.getContainerConfigPath(name, { engine })
|
|
79
85
|
if (!existsSync(configPath)) {
|
|
@@ -84,8 +90,14 @@ export class ContainerManager {
|
|
|
84
90
|
return this.migrateConfig(config)
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
// Search
|
|
88
|
-
const
|
|
93
|
+
// Search SQLite registry first
|
|
94
|
+
const sqliteConfig = await this.getSqliteConfig(name)
|
|
95
|
+
if (sqliteConfig) {
|
|
96
|
+
return sqliteConfig
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Search all engine directories (excluding sqlite which uses registry)
|
|
100
|
+
const engines = getSupportedEngines().filter((e) => e !== 'sqlite')
|
|
89
101
|
for (const eng of engines) {
|
|
90
102
|
const configPath = paths.getContainerConfigPath(name, { engine: eng })
|
|
91
103
|
if (existsSync(configPath)) {
|
|
@@ -98,6 +110,29 @@ export class ContainerManager {
|
|
|
98
110
|
return null
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Get SQLite container config from registry
|
|
115
|
+
*/
|
|
116
|
+
private async getSqliteConfig(name: string): Promise<ContainerConfig | null> {
|
|
117
|
+
const entry = await sqliteRegistry.get(name)
|
|
118
|
+
if (!entry) {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Convert registry entry to ContainerConfig format
|
|
123
|
+
const fileExists = existsSync(entry.filePath)
|
|
124
|
+
return {
|
|
125
|
+
name: entry.name,
|
|
126
|
+
engine: Engine.SQLite,
|
|
127
|
+
version: '3',
|
|
128
|
+
port: 0,
|
|
129
|
+
database: entry.filePath, // For SQLite, database field stores file path
|
|
130
|
+
databases: [entry.filePath],
|
|
131
|
+
created: entry.created,
|
|
132
|
+
status: fileExists ? 'running' : 'stopped', // "running" = file exists
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
101
136
|
/**
|
|
102
137
|
* Migrate old container configs to include databases array
|
|
103
138
|
* Ensures primary database is always in the databases array
|
|
@@ -165,12 +200,21 @@ export class ContainerManager {
|
|
|
165
200
|
const { engine } = options || {}
|
|
166
201
|
|
|
167
202
|
if (engine) {
|
|
203
|
+
// SQLite uses registry
|
|
204
|
+
if (engine === 'sqlite') {
|
|
205
|
+
return sqliteRegistry.exists(name)
|
|
206
|
+
}
|
|
168
207
|
const configPath = paths.getContainerConfigPath(name, { engine })
|
|
169
208
|
return existsSync(configPath)
|
|
170
209
|
}
|
|
171
210
|
|
|
172
|
-
// Check
|
|
173
|
-
|
|
211
|
+
// Check SQLite registry first
|
|
212
|
+
if (await sqliteRegistry.exists(name)) {
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check all engine directories (excluding sqlite)
|
|
217
|
+
const engines = getSupportedEngines().filter((e) => e !== 'sqlite')
|
|
174
218
|
for (const eng of engines) {
|
|
175
219
|
const configPath = paths.getContainerConfigPath(name, { engine: eng })
|
|
176
220
|
if (existsSync(configPath)) {
|
|
@@ -185,14 +229,31 @@ export class ContainerManager {
|
|
|
185
229
|
* List all containers across all engines
|
|
186
230
|
*/
|
|
187
231
|
async list(): Promise<ContainerConfig[]> {
|
|
188
|
-
const
|
|
232
|
+
const containers: ContainerConfig[] = []
|
|
189
233
|
|
|
234
|
+
// List SQLite containers from registry
|
|
235
|
+
const sqliteEntries = await sqliteRegistry.list()
|
|
236
|
+
for (const entry of sqliteEntries) {
|
|
237
|
+
const fileExists = existsSync(entry.filePath)
|
|
238
|
+
containers.push({
|
|
239
|
+
name: entry.name,
|
|
240
|
+
engine: Engine.SQLite,
|
|
241
|
+
version: '3',
|
|
242
|
+
port: 0,
|
|
243
|
+
database: entry.filePath,
|
|
244
|
+
databases: [entry.filePath],
|
|
245
|
+
created: entry.created,
|
|
246
|
+
status: fileExists ? 'running' : 'stopped', // "running" = file exists
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// List server-based containers (PostgreSQL, MySQL)
|
|
251
|
+
const containersDir = paths.containers
|
|
190
252
|
if (!existsSync(containersDir)) {
|
|
191
|
-
return
|
|
253
|
+
return containers
|
|
192
254
|
}
|
|
193
255
|
|
|
194
|
-
const
|
|
195
|
-
const engines = getSupportedEngines()
|
|
256
|
+
const engines = getSupportedEngines().filter((e) => e !== 'sqlite')
|
|
196
257
|
|
|
197
258
|
for (const engine of engines) {
|
|
198
259
|
const engineDir = paths.getEngineContainersPath(engine)
|
|
@@ -236,7 +297,23 @@ export class ContainerManager {
|
|
|
236
297
|
|
|
237
298
|
const { engine } = config
|
|
238
299
|
|
|
239
|
-
//
|
|
300
|
+
// SQLite: delete file, remove from registry, and clean up container directory
|
|
301
|
+
if (engine === Engine.SQLite) {
|
|
302
|
+
const entry = await sqliteRegistry.get(name)
|
|
303
|
+
if (entry && existsSync(entry.filePath)) {
|
|
304
|
+
await unlink(entry.filePath)
|
|
305
|
+
}
|
|
306
|
+
await sqliteRegistry.remove(name)
|
|
307
|
+
|
|
308
|
+
// Also remove the container directory (created by containerManager.create)
|
|
309
|
+
const containerPath = paths.getContainerPath(name, { engine })
|
|
310
|
+
if (existsSync(containerPath)) {
|
|
311
|
+
await rm(containerPath, { recursive: true, force: true })
|
|
312
|
+
}
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Server databases: check if running first
|
|
240
317
|
const running = await processManager.isRunning(name, { engine })
|
|
241
318
|
if (running && !force) {
|
|
242
319
|
throw new Error(
|
|
@@ -335,7 +412,38 @@ export class ContainerManager {
|
|
|
335
412
|
throw new Error(`Container "${newName}" already exists`)
|
|
336
413
|
}
|
|
337
414
|
|
|
338
|
-
//
|
|
415
|
+
// SQLite: rename in registry and handle container directory
|
|
416
|
+
if (engine === Engine.SQLite) {
|
|
417
|
+
const entry = await sqliteRegistry.get(oldName)
|
|
418
|
+
if (!entry) {
|
|
419
|
+
throw new Error(`SQLite container "${oldName}" not found in registry`)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Remove old entry and add new one with updated name
|
|
423
|
+
await sqliteRegistry.remove(oldName)
|
|
424
|
+
await sqliteRegistry.add({
|
|
425
|
+
name: newName,
|
|
426
|
+
filePath: entry.filePath,
|
|
427
|
+
created: entry.created,
|
|
428
|
+
lastVerified: entry.lastVerified,
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
// Rename container directory if it exists (created by containerManager.create)
|
|
432
|
+
const oldContainerPath = paths.getContainerPath(oldName, { engine })
|
|
433
|
+
const newContainerPath = paths.getContainerPath(newName, { engine })
|
|
434
|
+
if (existsSync(oldContainerPath)) {
|
|
435
|
+
await cp(oldContainerPath, newContainerPath, { recursive: true })
|
|
436
|
+
await rm(oldContainerPath, { recursive: true, force: true })
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Return updated config
|
|
440
|
+
return {
|
|
441
|
+
...sourceConfig,
|
|
442
|
+
name: newName,
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Server databases: check container is not running
|
|
339
447
|
const running = await processManager.isRunning(oldName, { engine })
|
|
340
448
|
if (running) {
|
|
341
449
|
throw new Error(`Container "${oldName}" is running. Stop it first`)
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
usqlDependency,
|
|
19
19
|
pgcliDependency,
|
|
20
20
|
mycliDependency,
|
|
21
|
+
litecliDependency,
|
|
21
22
|
} from '../config/os-dependencies'
|
|
22
23
|
import { platformService } from './platform-service'
|
|
23
24
|
import { configManager } from './config-manager'
|
|
@@ -398,3 +399,20 @@ export function getMycliManualInstructions(
|
|
|
398
399
|
): string[] {
|
|
399
400
|
return getManualInstallInstructions(mycliDependency, platform)
|
|
400
401
|
}
|
|
402
|
+
|
|
403
|
+
export async function isLitecliInstalled(): Promise<boolean> {
|
|
404
|
+
const status = await checkDependency(litecliDependency)
|
|
405
|
+
return status.installed
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export async function installLitecli(
|
|
409
|
+
packageManager: DetectedPackageManager,
|
|
410
|
+
): Promise<InstallResult> {
|
|
411
|
+
return installDependency(litecliDependency, packageManager)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function getLitecliManualInstructions(
|
|
415
|
+
platform: Platform = getCurrentPlatform(),
|
|
416
|
+
): string[] {
|
|
417
|
+
return getManualInstallInstructions(litecliDependency, platform)
|
|
418
|
+
}
|
package/engines/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { postgresqlEngine } from './postgresql'
|
|
2
2
|
import { mysqlEngine } from './mysql'
|
|
3
|
+
import { sqliteEngine } from './sqlite'
|
|
3
4
|
import type { BaseEngine } from './base-engine'
|
|
4
5
|
import type { EngineInfo } from '../types'
|
|
5
6
|
|
|
@@ -14,6 +15,9 @@ export const engines: Record<string, BaseEngine> = {
|
|
|
14
15
|
// MySQL and aliases
|
|
15
16
|
mysql: mysqlEngine,
|
|
16
17
|
mariadb: mysqlEngine, // MariaDB is MySQL-compatible
|
|
18
|
+
// SQLite and aliases
|
|
19
|
+
sqlite: sqliteEngine,
|
|
20
|
+
lite: sqliteEngine,
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
/**
|