spindb 0.6.0 → 0.7.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 +421 -294
- package/cli/commands/config.ts +7 -1
- package/cli/commands/connect.ts +1 -0
- package/cli/commands/create.ts +7 -7
- package/cli/commands/edit.ts +10 -0
- package/cli/commands/engines.ts +10 -188
- package/cli/commands/info.ts +7 -14
- package/cli/commands/list.ts +2 -9
- package/cli/commands/logs.ts +130 -0
- package/cli/commands/menu/backup-handlers.ts +798 -0
- package/cli/commands/menu/container-handlers.ts +832 -0
- package/cli/commands/menu/engine-handlers.ts +382 -0
- package/cli/commands/menu/index.ts +184 -0
- package/cli/commands/menu/shared.ts +26 -0
- package/cli/commands/menu/shell-handlers.ts +331 -0
- package/cli/commands/menu/sql-handlers.ts +197 -0
- package/cli/commands/menu/update-handlers.ts +94 -0
- package/cli/commands/run.ts +150 -0
- package/cli/commands/url.ts +19 -5
- package/cli/constants.ts +10 -0
- package/cli/helpers.ts +152 -0
- package/cli/index.ts +5 -2
- package/cli/ui/prompts.ts +3 -11
- package/config/defaults.ts +5 -29
- package/core/binary-manager.ts +2 -2
- package/core/container-manager.ts +3 -2
- package/core/dependency-manager.ts +0 -163
- package/core/error-handler.ts +0 -26
- package/core/platform-service.ts +60 -40
- package/core/start-with-retry.ts +3 -28
- package/core/transaction-manager.ts +0 -8
- package/engines/base-engine.ts +10 -0
- package/engines/mysql/binary-detection.ts +1 -1
- package/engines/mysql/index.ts +78 -2
- package/engines/postgresql/index.ts +49 -0
- package/package.json +1 -1
- package/types/index.ts +7 -4
- package/cli/commands/menu.ts +0 -2670
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import inquirer from 'inquirer'
|
|
3
|
+
import { rm } from 'fs/promises'
|
|
4
|
+
import { containerManager } from '../../../core/container-manager'
|
|
5
|
+
import { processManager } from '../../../core/process-manager'
|
|
6
|
+
import { createSpinner } from '../../ui/spinner'
|
|
7
|
+
import { header, error, warning, info, formatBytes } from '../../ui/theme'
|
|
8
|
+
import { promptConfirm } from '../../ui/prompts'
|
|
9
|
+
import { getEngineIcon, ENGINE_ICONS } from '../../constants'
|
|
10
|
+
import {
|
|
11
|
+
getInstalledEngines,
|
|
12
|
+
type InstalledPostgresEngine,
|
|
13
|
+
type InstalledMysqlEngine,
|
|
14
|
+
} from '../../helpers'
|
|
15
|
+
import {
|
|
16
|
+
getMysqlVersion,
|
|
17
|
+
getMysqlInstallInfo,
|
|
18
|
+
} from '../../../engines/mysql/binary-detection'
|
|
19
|
+
import { type MenuChoice } from './shared'
|
|
20
|
+
|
|
21
|
+
export async function handleEngines(): Promise<void> {
|
|
22
|
+
console.clear()
|
|
23
|
+
console.log(header('Installed Engines'))
|
|
24
|
+
console.log()
|
|
25
|
+
|
|
26
|
+
const engines = await getInstalledEngines()
|
|
27
|
+
|
|
28
|
+
if (engines.length === 0) {
|
|
29
|
+
console.log(info('No engines installed yet.'))
|
|
30
|
+
console.log(
|
|
31
|
+
chalk.gray(
|
|
32
|
+
' PostgreSQL engines are downloaded automatically when you create a container.',
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
console.log(
|
|
36
|
+
chalk.gray(
|
|
37
|
+
' MySQL requires system installation (brew install mysql or apt install mysql-server).',
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Separate PostgreSQL and MySQL
|
|
44
|
+
const pgEngines = engines.filter(
|
|
45
|
+
(e): e is InstalledPostgresEngine => e.engine === 'postgresql',
|
|
46
|
+
)
|
|
47
|
+
const mysqlEngine = engines.find(
|
|
48
|
+
(e): e is InstalledMysqlEngine => e.engine === 'mysql',
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Calculate total size for PostgreSQL
|
|
52
|
+
const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
|
|
53
|
+
|
|
54
|
+
// Table header
|
|
55
|
+
console.log()
|
|
56
|
+
console.log(
|
|
57
|
+
chalk.gray(' ') +
|
|
58
|
+
chalk.bold.white('ENGINE'.padEnd(14)) +
|
|
59
|
+
chalk.bold.white('VERSION'.padEnd(12)) +
|
|
60
|
+
chalk.bold.white('SOURCE'.padEnd(18)) +
|
|
61
|
+
chalk.bold.white('SIZE'),
|
|
62
|
+
)
|
|
63
|
+
console.log(chalk.gray(' ' + '─'.repeat(55)))
|
|
64
|
+
|
|
65
|
+
// PostgreSQL rows
|
|
66
|
+
for (const engine of pgEngines) {
|
|
67
|
+
const icon = getEngineIcon(engine.engine)
|
|
68
|
+
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
69
|
+
|
|
70
|
+
console.log(
|
|
71
|
+
chalk.gray(' ') +
|
|
72
|
+
chalk.cyan(`${icon} ${engine.engine}`.padEnd(13)) +
|
|
73
|
+
chalk.yellow(engine.version.padEnd(12)) +
|
|
74
|
+
chalk.gray(platformInfo.padEnd(18)) +
|
|
75
|
+
chalk.white(formatBytes(engine.sizeBytes)),
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MySQL row
|
|
80
|
+
if (mysqlEngine) {
|
|
81
|
+
const icon = ENGINE_ICONS.mysql
|
|
82
|
+
const displayName = mysqlEngine.isMariaDB ? 'mariadb' : 'mysql'
|
|
83
|
+
|
|
84
|
+
console.log(
|
|
85
|
+
chalk.gray(' ') +
|
|
86
|
+
chalk.cyan(`${icon} ${displayName}`.padEnd(13)) +
|
|
87
|
+
chalk.yellow(mysqlEngine.version.padEnd(12)) +
|
|
88
|
+
chalk.gray('system'.padEnd(18)) +
|
|
89
|
+
chalk.gray('(system-installed)'),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(chalk.gray(' ' + '─'.repeat(55)))
|
|
94
|
+
|
|
95
|
+
// Summary
|
|
96
|
+
console.log()
|
|
97
|
+
if (pgEngines.length > 0) {
|
|
98
|
+
console.log(
|
|
99
|
+
chalk.gray(
|
|
100
|
+
` PostgreSQL: ${pgEngines.length} version(s), ${formatBytes(totalPgSize)}`,
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
if (mysqlEngine) {
|
|
105
|
+
console.log(chalk.gray(` MySQL: system-installed at ${mysqlEngine.path}`))
|
|
106
|
+
}
|
|
107
|
+
console.log()
|
|
108
|
+
|
|
109
|
+
// Menu options - only allow deletion of PostgreSQL engines
|
|
110
|
+
const choices: MenuChoice[] = []
|
|
111
|
+
|
|
112
|
+
for (const e of pgEngines) {
|
|
113
|
+
choices.push({
|
|
114
|
+
name: `${chalk.red('✕')} Delete ${e.engine} ${e.version} ${chalk.gray(`(${formatBytes(e.sizeBytes)})`)}`,
|
|
115
|
+
value: `delete:${e.path}:${e.engine}:${e.version}`,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// MySQL info option (not disabled, shows info icon)
|
|
120
|
+
if (mysqlEngine) {
|
|
121
|
+
const displayName = mysqlEngine.isMariaDB ? 'MariaDB' : 'MySQL'
|
|
122
|
+
choices.push({
|
|
123
|
+
name: `${chalk.blue('ℹ')} ${displayName} ${mysqlEngine.version} ${chalk.gray('(system-installed)')}`,
|
|
124
|
+
value: `mysql-info:${mysqlEngine.path}`,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
choices.push(new inquirer.Separator())
|
|
129
|
+
choices.push({ name: `${chalk.blue('←')} Back to main menu`, value: 'back' })
|
|
130
|
+
|
|
131
|
+
const { action } = await inquirer.prompt<{ action: string }>([
|
|
132
|
+
{
|
|
133
|
+
type: 'list',
|
|
134
|
+
name: 'action',
|
|
135
|
+
message: 'Manage engines:',
|
|
136
|
+
choices,
|
|
137
|
+
pageSize: 15,
|
|
138
|
+
},
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
if (action === 'back') {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (action.startsWith('delete:')) {
|
|
146
|
+
const [, enginePath, engineName, engineVersion] = action.split(':')
|
|
147
|
+
await handleDeleteEngine(enginePath, engineName, engineVersion)
|
|
148
|
+
// Return to engines menu
|
|
149
|
+
await handleEngines()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (action.startsWith('mysql-info:')) {
|
|
153
|
+
const mysqldPath = action.replace('mysql-info:', '')
|
|
154
|
+
await handleMysqlInfo(mysqldPath)
|
|
155
|
+
// Return to engines menu
|
|
156
|
+
await handleEngines()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function handleDeleteEngine(
|
|
161
|
+
enginePath: string,
|
|
162
|
+
engineName: string,
|
|
163
|
+
engineVersion: string,
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
// Check if any container is using this engine version
|
|
166
|
+
const containers = await containerManager.list()
|
|
167
|
+
const usingContainers = containers.filter(
|
|
168
|
+
(c) => c.engine === engineName && c.version === engineVersion,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if (usingContainers.length > 0) {
|
|
172
|
+
console.log()
|
|
173
|
+
console.log(
|
|
174
|
+
error(
|
|
175
|
+
`Cannot delete: ${usingContainers.length} container(s) are using ${engineName} ${engineVersion}`,
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
console.log(
|
|
179
|
+
chalk.gray(
|
|
180
|
+
` Containers: ${usingContainers.map((c) => c.name).join(', ')}`,
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
console.log()
|
|
184
|
+
await inquirer.prompt([
|
|
185
|
+
{
|
|
186
|
+
type: 'input',
|
|
187
|
+
name: 'continue',
|
|
188
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
189
|
+
},
|
|
190
|
+
])
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const confirmed = await promptConfirm(
|
|
195
|
+
`Delete ${engineName} ${engineVersion}? This cannot be undone.`,
|
|
196
|
+
false,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if (!confirmed) {
|
|
200
|
+
console.log(warning('Deletion cancelled'))
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const spinner = createSpinner(`Deleting ${engineName} ${engineVersion}...`)
|
|
205
|
+
spinner.start()
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await rm(enginePath, { recursive: true, force: true })
|
|
209
|
+
spinner.succeed(`Deleted ${engineName} ${engineVersion}`)
|
|
210
|
+
} catch (err) {
|
|
211
|
+
const e = err as Error
|
|
212
|
+
spinner.fail(`Failed to delete: ${e.message}`)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function handleMysqlInfo(mysqldPath: string): Promise<void> {
|
|
217
|
+
console.clear()
|
|
218
|
+
|
|
219
|
+
// Get install info
|
|
220
|
+
const installInfo = await getMysqlInstallInfo(mysqldPath)
|
|
221
|
+
const displayName = installInfo.isMariaDB ? 'MariaDB' : 'MySQL'
|
|
222
|
+
|
|
223
|
+
// Get version
|
|
224
|
+
const version = await getMysqlVersion(mysqldPath)
|
|
225
|
+
|
|
226
|
+
console.log(header(`${displayName} Information`))
|
|
227
|
+
console.log()
|
|
228
|
+
|
|
229
|
+
// Check for containers using MySQL
|
|
230
|
+
const containers = await containerManager.list()
|
|
231
|
+
const mysqlContainers = containers.filter((c) => c.engine === 'mysql')
|
|
232
|
+
|
|
233
|
+
// Track running containers for uninstall instructions
|
|
234
|
+
const runningContainers: string[] = []
|
|
235
|
+
|
|
236
|
+
if (mysqlContainers.length > 0) {
|
|
237
|
+
console.log(
|
|
238
|
+
warning(
|
|
239
|
+
`${mysqlContainers.length} container(s) are using ${displayName}:`,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
console.log()
|
|
243
|
+
for (const c of mysqlContainers) {
|
|
244
|
+
const isRunning = await processManager.isRunning(c.name, {
|
|
245
|
+
engine: c.engine,
|
|
246
|
+
})
|
|
247
|
+
if (isRunning) {
|
|
248
|
+
runningContainers.push(c.name)
|
|
249
|
+
}
|
|
250
|
+
const status = isRunning
|
|
251
|
+
? chalk.green('● running')
|
|
252
|
+
: chalk.gray('○ stopped')
|
|
253
|
+
console.log(chalk.gray(` • ${c.name} ${status}`))
|
|
254
|
+
}
|
|
255
|
+
console.log()
|
|
256
|
+
console.log(
|
|
257
|
+
chalk.yellow(
|
|
258
|
+
' Uninstalling will break these containers. Delete them first.',
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
console.log()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Show installation details
|
|
265
|
+
console.log(chalk.white(' Installation Details:'))
|
|
266
|
+
console.log(chalk.gray(' ' + '─'.repeat(50)))
|
|
267
|
+
console.log(
|
|
268
|
+
chalk.gray(' ') +
|
|
269
|
+
chalk.white('Version:'.padEnd(18)) +
|
|
270
|
+
chalk.yellow(version || 'unknown'),
|
|
271
|
+
)
|
|
272
|
+
console.log(
|
|
273
|
+
chalk.gray(' ') +
|
|
274
|
+
chalk.white('Binary Path:'.padEnd(18)) +
|
|
275
|
+
chalk.gray(mysqldPath),
|
|
276
|
+
)
|
|
277
|
+
console.log(
|
|
278
|
+
chalk.gray(' ') +
|
|
279
|
+
chalk.white('Package Manager:'.padEnd(18)) +
|
|
280
|
+
chalk.cyan(installInfo.packageManager),
|
|
281
|
+
)
|
|
282
|
+
console.log(
|
|
283
|
+
chalk.gray(' ') +
|
|
284
|
+
chalk.white('Package Name:'.padEnd(18)) +
|
|
285
|
+
chalk.cyan(installInfo.packageName),
|
|
286
|
+
)
|
|
287
|
+
console.log()
|
|
288
|
+
|
|
289
|
+
// Uninstall instructions
|
|
290
|
+
console.log(chalk.white(' To uninstall:'))
|
|
291
|
+
console.log(chalk.gray(' ' + '─'.repeat(50)))
|
|
292
|
+
|
|
293
|
+
let stepNum = 1
|
|
294
|
+
|
|
295
|
+
// Step: Stop running containers first
|
|
296
|
+
if (runningContainers.length > 0) {
|
|
297
|
+
console.log(chalk.gray(` # ${stepNum}. Stop running SpinDB containers`))
|
|
298
|
+
console.log(chalk.cyan(' spindb stop <container-name>'))
|
|
299
|
+
console.log()
|
|
300
|
+
stepNum++
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Step: Delete SpinDB containers
|
|
304
|
+
if (mysqlContainers.length > 0) {
|
|
305
|
+
console.log(chalk.gray(` # ${stepNum}. Delete SpinDB containers`))
|
|
306
|
+
console.log(chalk.cyan(' spindb delete <container-name>'))
|
|
307
|
+
console.log()
|
|
308
|
+
stepNum++
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (installInfo.packageManager === 'homebrew') {
|
|
312
|
+
console.log(
|
|
313
|
+
chalk.gray(
|
|
314
|
+
` # ${stepNum}. Stop Homebrew service (if running separately)`,
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
console.log(chalk.cyan(` brew services stop ${installInfo.packageName}`))
|
|
318
|
+
console.log()
|
|
319
|
+
console.log(chalk.gray(` # ${stepNum + 1}. Uninstall the package`))
|
|
320
|
+
console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
|
|
321
|
+
} else if (installInfo.packageManager === 'apt') {
|
|
322
|
+
console.log(chalk.gray(` # ${stepNum}. Stop the system service`))
|
|
323
|
+
console.log(
|
|
324
|
+
chalk.cyan(
|
|
325
|
+
` sudo systemctl stop ${installInfo.isMariaDB ? 'mariadb' : 'mysql'}`,
|
|
326
|
+
),
|
|
327
|
+
)
|
|
328
|
+
console.log()
|
|
329
|
+
console.log(chalk.gray(` # ${stepNum + 1}. Disable auto-start on boot`))
|
|
330
|
+
console.log(
|
|
331
|
+
chalk.cyan(
|
|
332
|
+
` sudo systemctl disable ${installInfo.isMariaDB ? 'mariadb' : 'mysql'}`,
|
|
333
|
+
),
|
|
334
|
+
)
|
|
335
|
+
console.log()
|
|
336
|
+
console.log(chalk.gray(` # ${stepNum + 2}. Uninstall the package`))
|
|
337
|
+
console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
|
|
338
|
+
console.log()
|
|
339
|
+
console.log(chalk.gray(` # ${stepNum + 3}. Remove data files (optional)`))
|
|
340
|
+
console.log(
|
|
341
|
+
chalk.cyan(' sudo apt purge mysql-server mysql-client mysql-common'),
|
|
342
|
+
)
|
|
343
|
+
console.log(chalk.cyan(' sudo rm -rf /var/lib/mysql /etc/mysql'))
|
|
344
|
+
} else if (
|
|
345
|
+
installInfo.packageManager === 'yum' ||
|
|
346
|
+
installInfo.packageManager === 'dnf'
|
|
347
|
+
) {
|
|
348
|
+
console.log(chalk.gray(` # ${stepNum}. Stop the system service`))
|
|
349
|
+
console.log(
|
|
350
|
+
chalk.cyan(
|
|
351
|
+
` sudo systemctl stop ${installInfo.isMariaDB ? 'mariadb' : 'mysqld'}`,
|
|
352
|
+
),
|
|
353
|
+
)
|
|
354
|
+
console.log()
|
|
355
|
+
console.log(chalk.gray(` # ${stepNum + 1}. Uninstall the package`))
|
|
356
|
+
console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
|
|
357
|
+
} else if (installInfo.packageManager === 'pacman') {
|
|
358
|
+
console.log(chalk.gray(` # ${stepNum}. Stop the system service`))
|
|
359
|
+
console.log(
|
|
360
|
+
chalk.cyan(
|
|
361
|
+
` sudo systemctl stop ${installInfo.isMariaDB ? 'mariadb' : 'mysqld'}`,
|
|
362
|
+
),
|
|
363
|
+
)
|
|
364
|
+
console.log()
|
|
365
|
+
console.log(chalk.gray(` # ${stepNum + 1}. Uninstall the package`))
|
|
366
|
+
console.log(chalk.cyan(` ${installInfo.uninstallCommand}`))
|
|
367
|
+
} else {
|
|
368
|
+
console.log(chalk.gray(' Use your system package manager to uninstall.'))
|
|
369
|
+
console.log(chalk.gray(` The binary is located at: ${mysqldPath}`))
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
console.log()
|
|
373
|
+
|
|
374
|
+
// Wait for user
|
|
375
|
+
await inquirer.prompt([
|
|
376
|
+
{
|
|
377
|
+
type: 'input',
|
|
378
|
+
name: 'continue',
|
|
379
|
+
message: chalk.gray('Press Enter to go back...'),
|
|
380
|
+
},
|
|
381
|
+
])
|
|
382
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import inquirer from 'inquirer'
|
|
4
|
+
import { containerManager } from '../../../core/container-manager'
|
|
5
|
+
import { promptInstallDependencies } from '../../ui/prompts'
|
|
6
|
+
import { header, error } from '../../ui/theme'
|
|
7
|
+
import { getInstalledEngines } from '../../helpers'
|
|
8
|
+
import { type MenuChoice } from './shared'
|
|
9
|
+
import {
|
|
10
|
+
handleCreate,
|
|
11
|
+
handleList,
|
|
12
|
+
handleStart,
|
|
13
|
+
handleStop,
|
|
14
|
+
} from './container-handlers'
|
|
15
|
+
import {
|
|
16
|
+
handleBackup,
|
|
17
|
+
handleRestore,
|
|
18
|
+
handleClone,
|
|
19
|
+
} from './backup-handlers'
|
|
20
|
+
import { handleEngines } from './engine-handlers'
|
|
21
|
+
import { handleCheckUpdate } from './update-handlers'
|
|
22
|
+
|
|
23
|
+
async function showMainMenu(): Promise<void> {
|
|
24
|
+
console.clear()
|
|
25
|
+
console.log(header('SpinDB - Local Database Manager'))
|
|
26
|
+
console.log()
|
|
27
|
+
|
|
28
|
+
const containers = await containerManager.list()
|
|
29
|
+
const running = containers.filter((c) => c.status === 'running').length
|
|
30
|
+
const stopped = containers.filter((c) => c.status !== 'running').length
|
|
31
|
+
|
|
32
|
+
console.log(
|
|
33
|
+
chalk.gray(
|
|
34
|
+
` ${containers.length} container(s): ${running} running, ${stopped} stopped`,
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
console.log()
|
|
38
|
+
|
|
39
|
+
const canStart = stopped > 0
|
|
40
|
+
const canStop = running > 0
|
|
41
|
+
const canRestore = running > 0
|
|
42
|
+
const canClone = containers.length > 0
|
|
43
|
+
|
|
44
|
+
// Check if any engines are installed
|
|
45
|
+
const engines = await getInstalledEngines()
|
|
46
|
+
const hasEngines = engines.length > 0
|
|
47
|
+
|
|
48
|
+
// If containers exist, show List first; otherwise show Create first
|
|
49
|
+
const hasContainers = containers.length > 0
|
|
50
|
+
|
|
51
|
+
const choices: MenuChoice[] = [
|
|
52
|
+
...(hasContainers
|
|
53
|
+
? [
|
|
54
|
+
{ name: `${chalk.cyan('◉')} Containers`, value: 'list' },
|
|
55
|
+
{ name: `${chalk.green('+')} Create new container`, value: 'create' },
|
|
56
|
+
]
|
|
57
|
+
: [
|
|
58
|
+
{ name: `${chalk.green('+')} Create new container`, value: 'create' },
|
|
59
|
+
{ name: `${chalk.cyan('◉')} Containers`, value: 'list' },
|
|
60
|
+
]),
|
|
61
|
+
{
|
|
62
|
+
name: canStart
|
|
63
|
+
? `${chalk.green('▶')} Start a container`
|
|
64
|
+
: chalk.gray('▶ Start a container'),
|
|
65
|
+
value: 'start',
|
|
66
|
+
disabled: canStart ? false : 'No stopped containers',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: canStop
|
|
70
|
+
? `${chalk.red('■')} Stop a container`
|
|
71
|
+
: chalk.gray('■ Stop a container'),
|
|
72
|
+
value: 'stop',
|
|
73
|
+
disabled: canStop ? false : 'No running containers',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: canRestore
|
|
77
|
+
? `${chalk.magenta('↓')} Restore backup`
|
|
78
|
+
: chalk.gray('↓ Restore backup'),
|
|
79
|
+
value: 'restore',
|
|
80
|
+
disabled: canRestore ? false : 'No running containers',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: canRestore
|
|
84
|
+
? `${chalk.magenta('↑')} Backup database`
|
|
85
|
+
: chalk.gray('↑ Backup database'),
|
|
86
|
+
value: 'backup',
|
|
87
|
+
disabled: canRestore ? false : 'No running containers',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: canClone
|
|
91
|
+
? `${chalk.cyan('⧉')} Clone a container`
|
|
92
|
+
: chalk.gray('⧉ Clone a container'),
|
|
93
|
+
value: 'clone',
|
|
94
|
+
disabled: canClone ? false : 'No containers',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: hasEngines
|
|
98
|
+
? `${chalk.yellow('⚙')} List installed engines`
|
|
99
|
+
: chalk.gray('⚙ List installed engines'),
|
|
100
|
+
value: 'engines',
|
|
101
|
+
disabled: hasEngines ? false : 'No engines installed',
|
|
102
|
+
},
|
|
103
|
+
new inquirer.Separator(),
|
|
104
|
+
{ name: `${chalk.cyan('↑')} Check for updates`, value: 'check-update' },
|
|
105
|
+
{ name: `${chalk.gray('⏻')} Exit`, value: 'exit' },
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
const { action } = await inquirer.prompt<{ action: string }>([
|
|
109
|
+
{
|
|
110
|
+
type: 'list',
|
|
111
|
+
name: 'action',
|
|
112
|
+
message: 'What would you like to do?',
|
|
113
|
+
choices,
|
|
114
|
+
pageSize: 12,
|
|
115
|
+
},
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
switch (action) {
|
|
119
|
+
case 'create':
|
|
120
|
+
await handleCreate()
|
|
121
|
+
break
|
|
122
|
+
case 'list':
|
|
123
|
+
await handleList(showMainMenu)
|
|
124
|
+
break
|
|
125
|
+
case 'start':
|
|
126
|
+
await handleStart()
|
|
127
|
+
break
|
|
128
|
+
case 'stop':
|
|
129
|
+
await handleStop()
|
|
130
|
+
break
|
|
131
|
+
case 'restore':
|
|
132
|
+
await handleRestore()
|
|
133
|
+
break
|
|
134
|
+
case 'backup':
|
|
135
|
+
await handleBackup()
|
|
136
|
+
break
|
|
137
|
+
case 'clone':
|
|
138
|
+
await handleClone()
|
|
139
|
+
break
|
|
140
|
+
case 'engines':
|
|
141
|
+
await handleEngines()
|
|
142
|
+
break
|
|
143
|
+
case 'check-update':
|
|
144
|
+
await handleCheckUpdate()
|
|
145
|
+
break
|
|
146
|
+
case 'exit':
|
|
147
|
+
console.log(chalk.gray('\n Goodbye!\n'))
|
|
148
|
+
process.exit(0)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Return to menu after action
|
|
152
|
+
await showMainMenu()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const menuCommand = new Command('menu')
|
|
156
|
+
.description('Interactive menu for managing containers')
|
|
157
|
+
.action(async () => {
|
|
158
|
+
try {
|
|
159
|
+
await showMainMenu()
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const e = err as Error
|
|
162
|
+
|
|
163
|
+
// Check if this is a missing tool error
|
|
164
|
+
if (
|
|
165
|
+
e.message.includes('pg_restore not found') ||
|
|
166
|
+
e.message.includes('psql not found') ||
|
|
167
|
+
e.message.includes('pg_dump not found')
|
|
168
|
+
) {
|
|
169
|
+
const missingTool = e.message.includes('pg_restore')
|
|
170
|
+
? 'pg_restore'
|
|
171
|
+
: e.message.includes('pg_dump')
|
|
172
|
+
? 'pg_dump'
|
|
173
|
+
: 'psql'
|
|
174
|
+
const installed = await promptInstallDependencies(missingTool)
|
|
175
|
+
if (installed) {
|
|
176
|
+
console.log(chalk.yellow(' Please re-run spindb to continue.'))
|
|
177
|
+
}
|
|
178
|
+
process.exit(1)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.error(error(e.message))
|
|
182
|
+
process.exit(1)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import inquirer from 'inquirer'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Menu choice type for inquirer list prompts
|
|
6
|
+
*/
|
|
7
|
+
export type MenuChoice =
|
|
8
|
+
| {
|
|
9
|
+
name: string
|
|
10
|
+
value: string
|
|
11
|
+
disabled?: boolean | string
|
|
12
|
+
}
|
|
13
|
+
| inquirer.Separator
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Helper to pause and wait for user to press Enter
|
|
17
|
+
*/
|
|
18
|
+
export async function pressEnterToContinue(): Promise<void> {
|
|
19
|
+
await inquirer.prompt([
|
|
20
|
+
{
|
|
21
|
+
type: 'input',
|
|
22
|
+
name: 'continue',
|
|
23
|
+
message: chalk.gray('Press Enter to continue...'),
|
|
24
|
+
},
|
|
25
|
+
])
|
|
26
|
+
}
|