spindb 0.7.0 → 0.7.5

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