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
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
2
|
import inquirer from 'inquirer'
|
|
3
|
-
import { existsSync } from 'fs'
|
|
3
|
+
import { existsSync, renameSync, statSync, mkdirSync, copyFileSync, unlinkSync } from 'fs'
|
|
4
|
+
import { dirname, basename, join, resolve } from 'path'
|
|
5
|
+
import { homedir } from 'os'
|
|
4
6
|
import { containerManager } from '../../../core/container-manager'
|
|
5
7
|
import { getMissingDependencies } from '../../../core/dependency-manager'
|
|
6
8
|
import { platformService } from '../../../core/platform-service'
|
|
7
9
|
import { portManager } from '../../../core/port-manager'
|
|
8
10
|
import { processManager } from '../../../core/process-manager'
|
|
9
11
|
import { getEngine } from '../../../engines'
|
|
12
|
+
import { sqliteRegistry } from '../../../engines/sqlite/registry'
|
|
10
13
|
import { defaults } from '../../../config/defaults'
|
|
11
14
|
import { paths } from '../../../config/paths'
|
|
12
15
|
import {
|
|
@@ -26,24 +29,25 @@ import {
|
|
|
26
29
|
connectionBox,
|
|
27
30
|
formatBytes,
|
|
28
31
|
} from '../../ui/theme'
|
|
29
|
-
import { getEngineIcon } from '../../constants'
|
|
30
32
|
import { handleOpenShell, handleCopyConnectionString } from './shell-handlers'
|
|
31
33
|
import { handleRunSql, handleViewLogs } from './sql-handlers'
|
|
32
|
-
import {
|
|
33
|
-
import { type MenuChoice } from './shared'
|
|
34
|
+
import { Engine } from '../../../types'
|
|
35
|
+
import { type MenuChoice, pressEnterToContinue } from './shared'
|
|
34
36
|
|
|
35
37
|
export async function handleCreate(): Promise<void> {
|
|
36
38
|
console.log()
|
|
37
39
|
const answers = await promptCreateOptions()
|
|
38
40
|
let { name: containerName } = answers
|
|
39
|
-
const { engine, version, port, database } = answers
|
|
41
|
+
const { engine, version, port, database, path: sqlitePath } = answers
|
|
40
42
|
|
|
41
43
|
console.log()
|
|
42
44
|
console.log(header('Creating Database Container'))
|
|
43
45
|
console.log()
|
|
44
46
|
|
|
45
47
|
const dbEngine = getEngine(engine)
|
|
48
|
+
const isSQLite = engine === 'sqlite'
|
|
46
49
|
|
|
50
|
+
// Check dependencies (all engines need this)
|
|
47
51
|
const depsSpinner = createSpinner('Checking required tools...')
|
|
48
52
|
depsSpinner.start()
|
|
49
53
|
|
|
@@ -78,22 +82,26 @@ export async function handleCreate(): Promise<void> {
|
|
|
78
82
|
depsSpinner.succeed('Required tools available')
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
// Server databases: check port and binaries
|
|
86
|
+
let portAvailable = true
|
|
87
|
+
if (!isSQLite) {
|
|
88
|
+
portAvailable = await portManager.isPortAvailable(port)
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
const binarySpinner = createSpinner(
|
|
91
|
+
`Checking ${dbEngine.displayName} ${version} binaries...`,
|
|
92
|
+
)
|
|
93
|
+
binarySpinner.start()
|
|
94
|
+
|
|
95
|
+
const isInstalled = await dbEngine.isBinaryInstalled(version)
|
|
96
|
+
if (isInstalled) {
|
|
97
|
+
binarySpinner.succeed(`${dbEngine.displayName} ${version} binaries ready (cached)`)
|
|
98
|
+
} else {
|
|
99
|
+
binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
|
|
100
|
+
await dbEngine.ensureBinaries(version, ({ message }) => {
|
|
101
|
+
binarySpinner.text = message
|
|
102
|
+
})
|
|
103
|
+
binarySpinner.succeed(`${dbEngine.displayName} ${version} binaries downloaded`)
|
|
104
|
+
}
|
|
97
105
|
}
|
|
98
106
|
|
|
99
107
|
while (await containerManager.exists(containerName)) {
|
|
@@ -113,17 +121,62 @@ export async function handleCreate(): Promise<void> {
|
|
|
113
121
|
|
|
114
122
|
createSpinnerInstance.succeed('Container created')
|
|
115
123
|
|
|
116
|
-
const initSpinner = createSpinner(
|
|
124
|
+
const initSpinner = createSpinner(
|
|
125
|
+
isSQLite ? 'Creating database file...' : 'Initializing database cluster...',
|
|
126
|
+
)
|
|
117
127
|
initSpinner.start()
|
|
118
128
|
|
|
119
129
|
await dbEngine.initDataDir(containerName, version, {
|
|
120
130
|
superuser: defaults.superuser,
|
|
131
|
+
path: sqlitePath, // SQLite file path (undefined for server databases)
|
|
121
132
|
})
|
|
122
133
|
|
|
123
|
-
initSpinner.succeed('Database cluster initialized')
|
|
134
|
+
initSpinner.succeed(isSQLite ? 'Database file created' : 'Database cluster initialized')
|
|
124
135
|
|
|
136
|
+
// SQLite: show file path, no start needed
|
|
137
|
+
if (isSQLite) {
|
|
138
|
+
const config = await containerManager.getConfig(containerName)
|
|
139
|
+
if (config) {
|
|
140
|
+
const connectionString = dbEngine.getConnectionString(config)
|
|
141
|
+
console.log()
|
|
142
|
+
console.log(success('Database Created'))
|
|
143
|
+
console.log()
|
|
144
|
+
console.log(chalk.gray(` Container: ${containerName}`))
|
|
145
|
+
console.log(chalk.gray(` Engine: ${dbEngine.displayName} ${version}`))
|
|
146
|
+
console.log(chalk.gray(` File: ${config.database}`))
|
|
147
|
+
console.log()
|
|
148
|
+
console.log(success(`Available at ${config.database}`))
|
|
149
|
+
console.log()
|
|
150
|
+
console.log(chalk.gray(' Connection string:'))
|
|
151
|
+
console.log(chalk.cyan(` ${connectionString}`))
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const copied = await platformService.copyToClipboard(connectionString)
|
|
155
|
+
if (copied) {
|
|
156
|
+
console.log(chalk.gray(' ✓ Connection string copied to clipboard'))
|
|
157
|
+
} else {
|
|
158
|
+
console.log(chalk.gray(' (Could not copy to clipboard)'))
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
console.log(chalk.gray(' (Could not copy to clipboard)'))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log()
|
|
165
|
+
|
|
166
|
+
await inquirer.prompt([
|
|
167
|
+
{
|
|
168
|
+
type: 'input',
|
|
169
|
+
name: 'continue',
|
|
170
|
+
message: chalk.gray('Press Enter to return to the main menu...'),
|
|
171
|
+
},
|
|
172
|
+
])
|
|
173
|
+
}
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Server databases: start and create database
|
|
125
178
|
if (portAvailable) {
|
|
126
|
-
const startSpinner = createSpinner(
|
|
179
|
+
const startSpinner = createSpinner(`Starting ${dbEngine.displayName}...`)
|
|
127
180
|
startSpinner.start()
|
|
128
181
|
|
|
129
182
|
const config = await containerManager.getConfig(containerName)
|
|
@@ -132,7 +185,7 @@ export async function handleCreate(): Promise<void> {
|
|
|
132
185
|
await containerManager.updateConfig(containerName, { status: 'running' })
|
|
133
186
|
}
|
|
134
187
|
|
|
135
|
-
startSpinner.succeed(
|
|
188
|
+
startSpinner.succeed(`${dbEngine.displayName} started`)
|
|
136
189
|
|
|
137
190
|
if (config && database !== 'postgres') {
|
|
138
191
|
const dbSpinner = createSpinner(`Creating database "${database}"...`)
|
|
@@ -149,11 +202,11 @@ export async function handleCreate(): Promise<void> {
|
|
|
149
202
|
console.log(success('Database Created'))
|
|
150
203
|
console.log()
|
|
151
204
|
console.log(chalk.gray(` Container: ${containerName}`))
|
|
152
|
-
console.log(chalk.gray(` Engine: ${dbEngine.
|
|
205
|
+
console.log(chalk.gray(` Engine: ${dbEngine.displayName} ${version}`))
|
|
153
206
|
console.log(chalk.gray(` Database: ${database}`))
|
|
154
207
|
console.log(chalk.gray(` Port: ${port}`))
|
|
155
208
|
console.log()
|
|
156
|
-
console.log(success(`
|
|
209
|
+
console.log(success(`Running on port ${port}`))
|
|
157
210
|
console.log()
|
|
158
211
|
console.log(chalk.gray(' Connection string:'))
|
|
159
212
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
@@ -233,58 +286,86 @@ export async function handleList(
|
|
|
233
286
|
console.log()
|
|
234
287
|
console.log(
|
|
235
288
|
chalk.gray(' ') +
|
|
236
|
-
chalk.bold.white('NAME'.padEnd(
|
|
237
|
-
chalk.bold.white('ENGINE'.padEnd(
|
|
238
|
-
chalk.bold.white('VERSION'.padEnd(
|
|
239
|
-
chalk.bold.white('PORT'.padEnd(
|
|
240
|
-
chalk.bold.white('SIZE'.padEnd(
|
|
289
|
+
chalk.bold.white('NAME'.padEnd(16)) +
|
|
290
|
+
chalk.bold.white('ENGINE'.padEnd(11)) +
|
|
291
|
+
chalk.bold.white('VERSION'.padEnd(8)) +
|
|
292
|
+
chalk.bold.white('PORT'.padEnd(6)) +
|
|
293
|
+
chalk.bold.white('SIZE'.padEnd(9)) +
|
|
241
294
|
chalk.bold.white('STATUS'),
|
|
242
295
|
)
|
|
243
|
-
console.log(chalk.gray(' ' + '─'.repeat(
|
|
296
|
+
console.log(chalk.gray(' ' + '─'.repeat(58)))
|
|
244
297
|
|
|
245
298
|
for (let i = 0; i < containers.length; i++) {
|
|
246
299
|
const container = containers[i]
|
|
247
300
|
const size = sizes[i]
|
|
301
|
+
const isSQLite = container.engine === Engine.SQLite
|
|
248
302
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
303
|
+
// SQLite uses available/missing, server databases use running/stopped
|
|
304
|
+
const statusDisplay = isSQLite
|
|
305
|
+
? (container.status === 'running'
|
|
306
|
+
? chalk.blue('● available')
|
|
307
|
+
: chalk.gray('○ missing'))
|
|
308
|
+
: (container.status === 'running'
|
|
309
|
+
? chalk.green('● running')
|
|
310
|
+
: chalk.gray('○ stopped'))
|
|
253
311
|
|
|
254
312
|
const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
|
|
255
313
|
|
|
314
|
+
// Truncate name if too long
|
|
315
|
+
const displayName = container.name.length > 15
|
|
316
|
+
? container.name.slice(0, 14) + '…'
|
|
317
|
+
: container.name
|
|
318
|
+
|
|
319
|
+
// SQLite shows dash instead of port
|
|
320
|
+
const portDisplay = isSQLite ? '—' : String(container.port)
|
|
321
|
+
|
|
256
322
|
console.log(
|
|
257
323
|
chalk.gray(' ') +
|
|
258
|
-
chalk.cyan(
|
|
259
|
-
chalk.white(container.engine.padEnd(
|
|
260
|
-
chalk.yellow(container.version.padEnd(
|
|
261
|
-
chalk.green(
|
|
262
|
-
chalk.magenta(sizeDisplay.padEnd(
|
|
324
|
+
chalk.cyan(displayName.padEnd(16)) +
|
|
325
|
+
chalk.white(container.engine.padEnd(11)) +
|
|
326
|
+
chalk.yellow(container.version.padEnd(8)) +
|
|
327
|
+
chalk.green(portDisplay.padEnd(6)) +
|
|
328
|
+
chalk.magenta(sizeDisplay.padEnd(9)) +
|
|
263
329
|
statusDisplay,
|
|
264
330
|
)
|
|
265
331
|
}
|
|
266
332
|
|
|
267
333
|
console.log()
|
|
268
334
|
|
|
269
|
-
|
|
270
|
-
const
|
|
335
|
+
// Separate counts for server databases and SQLite
|
|
336
|
+
const serverContainers = containers.filter((c) => c.engine !== Engine.SQLite)
|
|
337
|
+
const sqliteContainers = containers.filter((c) => c.engine === Engine.SQLite)
|
|
338
|
+
|
|
339
|
+
const running = serverContainers.filter((c) => c.status === 'running').length
|
|
340
|
+
const stopped = serverContainers.filter((c) => c.status !== 'running').length
|
|
341
|
+
const available = sqliteContainers.filter((c) => c.status === 'running').length
|
|
342
|
+
const missing = sqliteContainers.filter((c) => c.status !== 'running').length
|
|
343
|
+
|
|
344
|
+
const parts: string[] = []
|
|
345
|
+
if (serverContainers.length > 0) {
|
|
346
|
+
parts.push(`${running} running, ${stopped} stopped`)
|
|
347
|
+
}
|
|
348
|
+
if (sqliteContainers.length > 0) {
|
|
349
|
+
parts.push(`${available} SQLite available${missing > 0 ? `, ${missing} missing` : ''}`)
|
|
350
|
+
}
|
|
351
|
+
|
|
271
352
|
console.log(
|
|
272
353
|
chalk.gray(
|
|
273
|
-
` ${containers.length} container(s): ${
|
|
354
|
+
` ${containers.length} container(s): ${parts.join('; ')}`,
|
|
274
355
|
),
|
|
275
356
|
)
|
|
276
357
|
|
|
277
358
|
console.log()
|
|
278
359
|
const containerChoices = [
|
|
279
|
-
...containers.map((c
|
|
280
|
-
|
|
281
|
-
const
|
|
360
|
+
...containers.map((c) => {
|
|
361
|
+
// Simpler selector - table already shows details
|
|
362
|
+
const statusLabel =
|
|
363
|
+
c.engine === Engine.SQLite
|
|
364
|
+
? (c.status === 'running' ? chalk.blue('● available') : chalk.gray('○ missing'))
|
|
365
|
+
: (c.status === 'running' ? chalk.green('● running') : chalk.gray('○ stopped'))
|
|
366
|
+
|
|
282
367
|
return {
|
|
283
|
-
name: `${c.name} ${
|
|
284
|
-
c.status === 'running'
|
|
285
|
-
? chalk.green('● running')
|
|
286
|
-
: chalk.gray('○ stopped')
|
|
287
|
-
}`,
|
|
368
|
+
name: `${c.name} ${statusLabel}`,
|
|
288
369
|
value: c.name,
|
|
289
370
|
short: c.name,
|
|
290
371
|
}
|
|
@@ -323,71 +404,116 @@ export async function showContainerSubmenu(
|
|
|
323
404
|
return
|
|
324
405
|
}
|
|
325
406
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
407
|
+
// SQLite: Check file existence instead of running status
|
|
408
|
+
const isSQLite = config.engine === Engine.SQLite
|
|
409
|
+
let isRunning: boolean
|
|
410
|
+
let status: string
|
|
411
|
+
let locationInfo: string
|
|
412
|
+
|
|
413
|
+
if (isSQLite) {
|
|
414
|
+
const fileExists = existsSync(config.database)
|
|
415
|
+
isRunning = fileExists // For SQLite, "running" means "file exists"
|
|
416
|
+
status = fileExists ? 'available' : 'missing'
|
|
417
|
+
locationInfo = `at ${config.database}`
|
|
418
|
+
} else {
|
|
419
|
+
isRunning = await processManager.isRunning(containerName, {
|
|
420
|
+
engine: config.engine,
|
|
421
|
+
})
|
|
422
|
+
status = isRunning ? 'running' : 'stopped'
|
|
423
|
+
locationInfo = `on port ${config.port}`
|
|
424
|
+
}
|
|
330
425
|
|
|
331
426
|
console.clear()
|
|
332
427
|
console.log(header(containerName))
|
|
333
428
|
console.log()
|
|
334
429
|
console.log(
|
|
335
430
|
chalk.gray(
|
|
336
|
-
` ${config.engine} ${config.version}
|
|
431
|
+
` ${config.engine} ${config.version} ${locationInfo} - ${status}`,
|
|
337
432
|
),
|
|
338
433
|
)
|
|
339
434
|
console.log()
|
|
340
435
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
:
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
436
|
+
// Build action choices based on engine type
|
|
437
|
+
const actionChoices: MenuChoice[] = []
|
|
438
|
+
|
|
439
|
+
// Start/Stop buttons only for server databases (not SQLite)
|
|
440
|
+
if (!isSQLite) {
|
|
441
|
+
if (!isRunning) {
|
|
442
|
+
actionChoices.push({
|
|
443
|
+
name: `${chalk.green('▶')} Start container`,
|
|
444
|
+
value: 'start',
|
|
445
|
+
})
|
|
446
|
+
} else {
|
|
447
|
+
actionChoices.push({
|
|
448
|
+
name: `${chalk.red('■')} Stop container`,
|
|
449
|
+
value: 'stop',
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Open shell - always enabled for SQLite (if file exists), server databases need to be running
|
|
455
|
+
const canOpenShell = isSQLite ? existsSync(config.database) : isRunning
|
|
456
|
+
actionChoices.push({
|
|
457
|
+
name: canOpenShell
|
|
458
|
+
? `${chalk.blue('⌘')} Open shell`
|
|
459
|
+
: chalk.gray('⌘ Open shell'),
|
|
460
|
+
value: 'shell',
|
|
461
|
+
disabled: canOpenShell ? false : (isSQLite ? 'Database file missing' : 'Start container first'),
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
// Run SQL - always enabled for SQLite (if file exists), server databases need to be running
|
|
465
|
+
const canRunSql = isSQLite ? existsSync(config.database) : isRunning
|
|
466
|
+
actionChoices.push({
|
|
467
|
+
name: canRunSql
|
|
468
|
+
? `${chalk.yellow('▷')} Run SQL file`
|
|
469
|
+
: chalk.gray('▷ Run SQL file'),
|
|
470
|
+
value: 'run-sql',
|
|
471
|
+
disabled: canRunSql ? false : (isSQLite ? 'Database file missing' : 'Start container first'),
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
// Edit container - SQLite can always edit (no running state), server databases must be stopped
|
|
475
|
+
const canEdit = isSQLite ? true : !isRunning
|
|
476
|
+
actionChoices.push({
|
|
477
|
+
name: canEdit
|
|
478
|
+
? `${chalk.white('⚙')} Edit container`
|
|
479
|
+
: chalk.gray('⚙ Edit container'),
|
|
480
|
+
value: 'edit',
|
|
481
|
+
disabled: canEdit ? false : 'Stop container first',
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
// Clone container - SQLite can always clone, server databases must be stopped
|
|
485
|
+
const canClone = isSQLite ? true : !isRunning
|
|
486
|
+
actionChoices.push({
|
|
487
|
+
name: canClone
|
|
488
|
+
? `${chalk.cyan('⧉')} Clone container`
|
|
489
|
+
: chalk.gray('⧉ Clone container'),
|
|
490
|
+
value: 'clone',
|
|
491
|
+
disabled: canClone ? false : 'Stop container first',
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
actionChoices.push(
|
|
379
495
|
{ name: `${chalk.magenta('⎘')} Copy connection string`, value: 'copy' },
|
|
380
|
-
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
// View logs - not available for SQLite (no log file)
|
|
499
|
+
if (!isSQLite) {
|
|
500
|
+
actionChoices.push({
|
|
381
501
|
name: `${chalk.gray('☰')} View logs`,
|
|
382
502
|
value: 'logs',
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Delete container - SQLite can always delete, server databases must be stopped
|
|
507
|
+
const canDelete = isSQLite ? true : !isRunning
|
|
508
|
+
actionChoices.push({
|
|
509
|
+
name: canDelete
|
|
510
|
+
? `${chalk.red('✕')} Delete container`
|
|
511
|
+
: chalk.gray('✕ Delete container'),
|
|
512
|
+
value: 'delete',
|
|
513
|
+
disabled: canDelete ? false : 'Stop container first',
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
actionChoices.push(
|
|
391
517
|
new inquirer.Separator(),
|
|
392
518
|
{
|
|
393
519
|
name: `${chalk.blue('←')} Back to containers`,
|
|
@@ -397,7 +523,7 @@ export async function showContainerSubmenu(
|
|
|
397
523
|
name: `${chalk.blue('⌂')} Back to main menu`,
|
|
398
524
|
value: 'main',
|
|
399
525
|
},
|
|
400
|
-
|
|
526
|
+
)
|
|
401
527
|
|
|
402
528
|
const { action } = await inquirer.prompt<{ action: string }>([
|
|
403
529
|
{
|
|
@@ -464,7 +590,10 @@ export async function showContainerSubmenu(
|
|
|
464
590
|
|
|
465
591
|
export async function handleStart(): Promise<void> {
|
|
466
592
|
const containers = await containerManager.list()
|
|
467
|
-
|
|
593
|
+
// Filter for stopped containers, excluding SQLite (no server process to start)
|
|
594
|
+
const stopped = containers.filter(
|
|
595
|
+
(c) => c.status !== 'running' && c.engine !== Engine.SQLite,
|
|
596
|
+
)
|
|
468
597
|
|
|
469
598
|
if (stopped.length === 0) {
|
|
470
599
|
console.log(warning('All containers are already running'))
|
|
@@ -474,6 +603,7 @@ export async function handleStart(): Promise<void> {
|
|
|
474
603
|
const containerName = await promptContainerSelect(
|
|
475
604
|
stopped,
|
|
476
605
|
'Select container to start:',
|
|
606
|
+
{ includeBack: true },
|
|
477
607
|
)
|
|
478
608
|
if (!containerName) return
|
|
479
609
|
|
|
@@ -511,7 +641,10 @@ export async function handleStart(): Promise<void> {
|
|
|
511
641
|
|
|
512
642
|
export async function handleStop(): Promise<void> {
|
|
513
643
|
const containers = await containerManager.list()
|
|
514
|
-
|
|
644
|
+
// Filter for running containers, excluding SQLite (no server process to stop)
|
|
645
|
+
const running = containers.filter(
|
|
646
|
+
(c) => c.status === 'running' && c.engine !== Engine.SQLite,
|
|
647
|
+
)
|
|
515
648
|
|
|
516
649
|
if (running.length === 0) {
|
|
517
650
|
console.log(warning('No running containers'))
|
|
@@ -521,6 +654,7 @@ export async function handleStop(): Promise<void> {
|
|
|
521
654
|
const containerName = await promptContainerSelect(
|
|
522
655
|
running,
|
|
523
656
|
'Select container to stop:',
|
|
657
|
+
{ includeBack: true },
|
|
524
658
|
)
|
|
525
659
|
if (!containerName) return
|
|
526
660
|
|
|
@@ -627,29 +761,41 @@ async function handleEditContainer(
|
|
|
627
761
|
return null
|
|
628
762
|
}
|
|
629
763
|
|
|
764
|
+
const isSQLite = config.engine === Engine.SQLite
|
|
765
|
+
|
|
630
766
|
console.clear()
|
|
631
767
|
console.log(header(`Edit: ${containerName}`))
|
|
632
768
|
console.log()
|
|
633
769
|
|
|
634
|
-
const editChoices = [
|
|
770
|
+
const editChoices: Array<{ name: string; value: string } | inquirer.Separator> = [
|
|
635
771
|
{
|
|
636
772
|
name: `Name: ${chalk.white(containerName)}`,
|
|
637
773
|
value: 'name',
|
|
638
774
|
},
|
|
639
|
-
|
|
775
|
+
]
|
|
776
|
+
|
|
777
|
+
// SQLite: show relocate option with file path; others: show port
|
|
778
|
+
if (isSQLite) {
|
|
779
|
+
editChoices.push({
|
|
780
|
+
name: `Location: ${chalk.white(config.database)}`,
|
|
781
|
+
value: 'relocate',
|
|
782
|
+
})
|
|
783
|
+
} else {
|
|
784
|
+
editChoices.push({
|
|
640
785
|
name: `Port: ${chalk.white(String(config.port))}`,
|
|
641
786
|
value: 'port',
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
}
|
|
652
|
-
|
|
787
|
+
})
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
editChoices.push(new inquirer.Separator())
|
|
791
|
+
editChoices.push({
|
|
792
|
+
name: `${chalk.blue('←')} Back to container`,
|
|
793
|
+
value: 'back',
|
|
794
|
+
})
|
|
795
|
+
editChoices.push({
|
|
796
|
+
name: `${chalk.blue('⌂')} Back to main menu`,
|
|
797
|
+
value: 'main',
|
|
798
|
+
})
|
|
653
799
|
|
|
654
800
|
const { field } = await inquirer.prompt<{ field: string }>([
|
|
655
801
|
{
|
|
@@ -746,6 +892,150 @@ async function handleEditContainer(
|
|
|
746
892
|
return await handleEditContainer(containerName)
|
|
747
893
|
}
|
|
748
894
|
|
|
895
|
+
if (field === 'relocate') {
|
|
896
|
+
const currentFileName = basename(config.database)
|
|
897
|
+
|
|
898
|
+
const { inputPath } = await inquirer.prompt<{ inputPath: string }>([
|
|
899
|
+
{
|
|
900
|
+
type: 'input',
|
|
901
|
+
name: 'inputPath',
|
|
902
|
+
message: 'New file path:',
|
|
903
|
+
default: config.database,
|
|
904
|
+
validate: (input: string) => {
|
|
905
|
+
if (!input) return 'Path is required'
|
|
906
|
+
return true
|
|
907
|
+
},
|
|
908
|
+
},
|
|
909
|
+
])
|
|
910
|
+
|
|
911
|
+
// Expand ~ to home directory
|
|
912
|
+
let expandedPath = inputPath
|
|
913
|
+
if (inputPath === '~') {
|
|
914
|
+
expandedPath = homedir()
|
|
915
|
+
} else if (inputPath.startsWith('~/')) {
|
|
916
|
+
expandedPath = join(homedir(), inputPath.slice(2))
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Convert relative paths to absolute
|
|
920
|
+
if (!expandedPath.startsWith('/')) {
|
|
921
|
+
expandedPath = resolve(process.cwd(), expandedPath)
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Check if path looks like a file (has db extension) or directory
|
|
925
|
+
const hasDbExtension = /\.(sqlite3?|db)$/i.test(expandedPath)
|
|
926
|
+
|
|
927
|
+
// Treat as directory if:
|
|
928
|
+
// - ends with /
|
|
929
|
+
// - exists and is a directory
|
|
930
|
+
// - doesn't have a database file extension (assume it's a directory path)
|
|
931
|
+
const isDirectory = expandedPath.endsWith('/') ||
|
|
932
|
+
(existsSync(expandedPath) && statSync(expandedPath).isDirectory()) ||
|
|
933
|
+
!hasDbExtension
|
|
934
|
+
|
|
935
|
+
let finalPath: string
|
|
936
|
+
if (isDirectory) {
|
|
937
|
+
// Remove trailing slash if present, then append filename
|
|
938
|
+
const dirPath = expandedPath.endsWith('/') ? expandedPath.slice(0, -1) : expandedPath
|
|
939
|
+
finalPath = join(dirPath, currentFileName)
|
|
940
|
+
} else {
|
|
941
|
+
finalPath = expandedPath
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (finalPath === config.database) {
|
|
945
|
+
console.log(info('Location unchanged'))
|
|
946
|
+
return await handleEditContainer(containerName)
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Check if source file exists
|
|
950
|
+
if (!existsSync(config.database)) {
|
|
951
|
+
console.log(error(`Source file not found: ${config.database}`))
|
|
952
|
+
return await handleEditContainer(containerName)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Check if destination already exists
|
|
956
|
+
if (existsSync(finalPath)) {
|
|
957
|
+
console.log(error(`Destination file already exists: ${finalPath}`))
|
|
958
|
+
return await handleEditContainer(containerName)
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Check if destination directory exists
|
|
962
|
+
const destDir = dirname(finalPath)
|
|
963
|
+
if (!existsSync(destDir)) {
|
|
964
|
+
console.log(warning(`Directory does not exist: ${destDir}`))
|
|
965
|
+
const { createDir } = await inquirer.prompt<{ createDir: string }>([
|
|
966
|
+
{
|
|
967
|
+
type: 'list',
|
|
968
|
+
name: 'createDir',
|
|
969
|
+
message: 'Create this directory?',
|
|
970
|
+
choices: [
|
|
971
|
+
{ name: 'Yes, create it', value: 'yes' },
|
|
972
|
+
{ name: 'No, cancel', value: 'no' },
|
|
973
|
+
],
|
|
974
|
+
},
|
|
975
|
+
])
|
|
976
|
+
|
|
977
|
+
if (createDir !== 'yes') {
|
|
978
|
+
return await handleEditContainer(containerName)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
try {
|
|
982
|
+
mkdirSync(destDir, { recursive: true })
|
|
983
|
+
console.log(success(`Created directory: ${destDir}`))
|
|
984
|
+
} catch (err) {
|
|
985
|
+
console.log(error(`Failed to create directory: ${(err as Error).message}`))
|
|
986
|
+
return await handleEditContainer(containerName)
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const spinner = createSpinner('Moving database file...')
|
|
991
|
+
spinner.start()
|
|
992
|
+
|
|
993
|
+
try {
|
|
994
|
+
// Try rename first (fast, same filesystem)
|
|
995
|
+
try {
|
|
996
|
+
renameSync(config.database, finalPath)
|
|
997
|
+
} catch (renameErr) {
|
|
998
|
+
const e = renameErr as NodeJS.ErrnoException
|
|
999
|
+
// EXDEV = cross-device link, need to copy+delete
|
|
1000
|
+
if (e.code === 'EXDEV') {
|
|
1001
|
+
try {
|
|
1002
|
+
// Copy file preserving mode/permissions
|
|
1003
|
+
copyFileSync(config.database, finalPath)
|
|
1004
|
+
// Only delete source after successful copy
|
|
1005
|
+
unlinkSync(config.database)
|
|
1006
|
+
} catch (copyErr) {
|
|
1007
|
+
// Clean up partial target on failure
|
|
1008
|
+
if (existsSync(finalPath)) {
|
|
1009
|
+
try {
|
|
1010
|
+
unlinkSync(finalPath)
|
|
1011
|
+
} catch {
|
|
1012
|
+
// Ignore cleanup errors
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
throw copyErr
|
|
1016
|
+
}
|
|
1017
|
+
} else {
|
|
1018
|
+
throw renameErr
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Update the container config and SQLite registry
|
|
1023
|
+
await containerManager.updateConfig(containerName, { database: finalPath })
|
|
1024
|
+
await sqliteRegistry.update(containerName, { filePath: finalPath })
|
|
1025
|
+
spinner.succeed(`Moved database to ${finalPath}`)
|
|
1026
|
+
|
|
1027
|
+
// Wait for user to see success message before refreshing
|
|
1028
|
+
await pressEnterToContinue()
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
spinner.fail('Failed to move database file')
|
|
1031
|
+
console.log(error((err as Error).message))
|
|
1032
|
+
await pressEnterToContinue()
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Continue editing (will fetch fresh config)
|
|
1036
|
+
return await handleEditContainer(containerName)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
749
1039
|
return containerName
|
|
750
1040
|
}
|
|
751
1041
|
|