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.
@@ -1,13 +1,14 @@
1
1
  import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
3
  import inquirer from 'inquirer'
4
+ import { existsSync } from 'fs'
4
5
  import { containerManager } from '../../core/container-manager'
5
6
  import { processManager } from '../../core/process-manager'
6
7
  import { paths } from '../../config/paths'
7
8
  import { getEngine } from '../../engines'
8
9
  import { error, info, header } from '../ui/theme'
9
10
  import { getEngineIcon } from '../constants'
10
- import type { ContainerConfig } from '../../types'
11
+ import { Engine, type ContainerConfig } from '../../types'
11
12
 
12
13
  function formatDate(dateString: string): string {
13
14
  const date = new Date(dateString)
@@ -16,7 +17,13 @@ function formatDate(dateString: string): string {
16
17
 
17
18
  async function getActualStatus(
18
19
  config: ContainerConfig,
19
- ): Promise<'running' | 'stopped'> {
20
+ ): Promise<'running' | 'stopped' | 'available' | 'missing'> {
21
+ // SQLite: check file existence instead of running status
22
+ if (config.engine === Engine.SQLite) {
23
+ const fileExists = existsSync(config.database)
24
+ return fileExists ? 'available' : 'missing'
25
+ }
26
+
20
27
  const running = await processManager.isRunning(config.name, {
21
28
  engine: config.engine,
22
29
  })
@@ -51,10 +58,21 @@ async function displayContainerInfo(
51
58
  }
52
59
 
53
60
  const icon = getEngineIcon(config.engine)
54
- const statusDisplay =
55
- actualStatus === 'running'
56
- ? chalk.green('● running')
57
- : chalk.gray('○ stopped')
61
+ const isSQLite = config.engine === Engine.SQLite
62
+
63
+ // Status display based on engine type
64
+ let statusDisplay: string
65
+ if (isSQLite) {
66
+ statusDisplay =
67
+ actualStatus === 'available'
68
+ ? chalk.blue('🔵 available')
69
+ : chalk.gray('⚪ missing')
70
+ } else {
71
+ statusDisplay =
72
+ actualStatus === 'running'
73
+ ? chalk.green('● running')
74
+ : chalk.gray('○ stopped')
75
+ }
58
76
 
59
77
  console.log()
60
78
  console.log(header(`Container: ${config.name}`))
@@ -67,26 +85,41 @@ async function displayContainerInfo(
67
85
  console.log(
68
86
  chalk.gray(' ') + chalk.white('Status:'.padEnd(14)) + statusDisplay,
69
87
  )
70
- console.log(
71
- chalk.gray(' ') +
72
- chalk.white('Port:'.padEnd(14)) +
73
- chalk.green(String(config.port)),
74
- )
75
- console.log(
76
- chalk.gray(' ') +
77
- chalk.white('Database:'.padEnd(14)) +
78
- chalk.yellow(config.database),
79
- )
88
+
89
+ // Show file path for SQLite, port for server databases
90
+ if (isSQLite) {
91
+ console.log(
92
+ chalk.gray(' ') +
93
+ chalk.white('File:'.padEnd(14)) +
94
+ chalk.green(config.database),
95
+ )
96
+ } else {
97
+ console.log(
98
+ chalk.gray(' ') +
99
+ chalk.white('Port:'.padEnd(14)) +
100
+ chalk.green(String(config.port)),
101
+ )
102
+ console.log(
103
+ chalk.gray(' ') +
104
+ chalk.white('Database:'.padEnd(14)) +
105
+ chalk.yellow(config.database),
106
+ )
107
+ }
108
+
80
109
  console.log(
81
110
  chalk.gray(' ') +
82
111
  chalk.white('Created:'.padEnd(14)) +
83
112
  chalk.gray(formatDate(config.created)),
84
113
  )
85
- console.log(
86
- chalk.gray(' ') +
87
- chalk.white('Data Dir:'.padEnd(14)) +
88
- chalk.gray(dataDir),
89
- )
114
+
115
+ // Don't show data dir for SQLite (file path is already shown)
116
+ if (!isSQLite) {
117
+ console.log(
118
+ chalk.gray(' ') +
119
+ chalk.white('Data Dir:'.padEnd(14)) +
120
+ chalk.gray(dataDir),
121
+ )
122
+ }
90
123
  if (config.clonedFrom) {
91
124
  console.log(
92
125
  chalk.gray(' ') +
@@ -142,20 +175,40 @@ async function displayAllContainersInfo(
142
175
 
143
176
  for (const container of containers) {
144
177
  const actualStatus = await getActualStatus(container)
145
- const statusDisplay =
146
- actualStatus === 'running'
147
- ? chalk.green('● running')
148
- : chalk.gray('○ stopped')
178
+ const isSQLite = container.engine === Engine.SQLite
179
+
180
+ // Status display based on engine type
181
+ let statusDisplay: string
182
+ if (isSQLite) {
183
+ statusDisplay =
184
+ actualStatus === 'available'
185
+ ? chalk.blue('🔵 available')
186
+ : chalk.gray('⚪ missing')
187
+ } else {
188
+ statusDisplay =
189
+ actualStatus === 'running'
190
+ ? chalk.green('● running')
191
+ : chalk.gray('○ stopped')
192
+ }
149
193
 
150
194
  const icon = getEngineIcon(container.engine)
151
195
  const engineDisplay = `${icon} ${container.engine}`
152
196
 
197
+ // Show truncated file path for SQLite instead of port
198
+ let portOrPath: string
199
+ if (isSQLite) {
200
+ const fileName = container.database.split('/').pop() || container.database
201
+ portOrPath = fileName.length > 7 ? fileName.slice(0, 6) + '…' : fileName
202
+ } else {
203
+ portOrPath = String(container.port)
204
+ }
205
+
153
206
  console.log(
154
207
  chalk.gray(' ') +
155
208
  chalk.cyan(container.name.padEnd(18)) +
156
209
  chalk.white(engineDisplay.padEnd(13)) +
157
210
  chalk.yellow(container.version.padEnd(10)) +
158
- chalk.green(String(container.port).padEnd(8)) +
211
+ chalk.green(portOrPath.padEnd(8)) +
159
212
  chalk.gray(container.database.padEnd(16)) +
160
213
  statusDisplay,
161
214
  )
@@ -4,11 +4,33 @@ import { containerManager } from '../../core/container-manager'
4
4
  import { getEngine } from '../../engines'
5
5
  import { info, error, formatBytes } from '../ui/theme'
6
6
  import { getEngineIcon } from '../constants'
7
+ import { Engine } from '../../types'
8
+ import { basename } from 'path'
7
9
  import type { ContainerConfig } from '../../types'
8
10
 
11
+ /**
12
+ * Pad string to width, accounting for emoji taking 2 display columns
13
+ */
14
+ function padWithEmoji(str: string, width: number): string {
15
+ // Count emojis (they take 2 display columns but count as 1-2 chars)
16
+ const emojiCount = (str.match(/[\u{1F300}-\u{1F9FF}]/gu) || []).length
17
+ return str.padEnd(width + emojiCount)
18
+ }
19
+
9
20
  async function getContainerSize(
10
21
  container: ContainerConfig,
11
22
  ): Promise<number | null> {
23
+ // SQLite can always get size (it's just file size)
24
+ if (container.engine === Engine.SQLite) {
25
+ try {
26
+ const engine = getEngine(container.engine)
27
+ return await engine.getDatabaseSize(container)
28
+ } catch {
29
+ return null
30
+ }
31
+ }
32
+
33
+ // Server databases need to be running
12
34
  if (container.status !== 'running') {
13
35
  return null
14
36
  }
@@ -62,22 +84,41 @@ export const listCommand = new Command('list')
62
84
  const container = containers[i]
63
85
  const size = sizes[i]
64
86
 
65
- const statusDisplay =
66
- container.status === 'running'
67
- ? chalk.green('● running')
68
- : chalk.gray('○ stopped')
87
+ // SQLite uses different status labels (blue/white icons)
88
+ let statusDisplay: string
89
+ if (container.engine === Engine.SQLite) {
90
+ statusDisplay =
91
+ container.status === 'running'
92
+ ? chalk.blue('🔵 available')
93
+ : chalk.gray('⚪ missing')
94
+ } else {
95
+ statusDisplay =
96
+ container.status === 'running'
97
+ ? chalk.green('● running')
98
+ : chalk.gray('○ stopped')
99
+ }
69
100
 
70
101
  const engineIcon = getEngineIcon(container.engine)
71
102
  const engineDisplay = `${engineIcon} ${container.engine}`
72
103
 
73
104
  const sizeDisplay = size !== null ? formatBytes(size) : chalk.gray('—')
74
105
 
106
+ // SQLite shows truncated file name instead of port
107
+ let portOrPath: string
108
+ if (container.engine === Engine.SQLite) {
109
+ const fileName = basename(container.database)
110
+ // Truncate if longer than 7 chars to fit in 8-char column
111
+ portOrPath = fileName.length > 7 ? fileName.slice(0, 6) + '…' : fileName
112
+ } else {
113
+ portOrPath = String(container.port)
114
+ }
115
+
75
116
  console.log(
76
117
  chalk.gray(' ') +
77
118
  chalk.cyan(container.name.padEnd(20)) +
78
- chalk.white(engineDisplay.padEnd(14)) +
119
+ chalk.white(padWithEmoji(engineDisplay, 14)) +
79
120
  chalk.yellow(container.version.padEnd(10)) +
80
- chalk.green(String(container.port).padEnd(8)) +
121
+ chalk.green(portOrPath.padEnd(8)) +
81
122
  chalk.magenta(sizeDisplay.padEnd(10)) +
82
123
  statusDisplay,
83
124
  )
@@ -85,11 +126,25 @@ export const listCommand = new Command('list')
85
126
 
86
127
  console.log()
87
128
 
88
- const running = containers.filter((c) => c.status === 'running').length
89
- const stopped = containers.filter((c) => c.status !== 'running').length
129
+ const serverContainers = containers.filter((c) => c.engine !== Engine.SQLite)
130
+ const sqliteContainers = containers.filter((c) => c.engine === Engine.SQLite)
131
+
132
+ const running = serverContainers.filter((c) => c.status === 'running').length
133
+ const stopped = serverContainers.filter((c) => c.status !== 'running').length
134
+ const available = sqliteContainers.filter((c) => c.status === 'running').length
135
+ const missing = sqliteContainers.filter((c) => c.status !== 'running').length
136
+
137
+ const parts: string[] = []
138
+ if (serverContainers.length > 0) {
139
+ parts.push(`${running} running, ${stopped} stopped`)
140
+ }
141
+ if (sqliteContainers.length > 0) {
142
+ parts.push(`${available} SQLite available${missing > 0 ? `, ${missing} missing` : ''}`)
143
+ }
144
+
90
145
  console.log(
91
146
  chalk.gray(
92
- ` ${containers.length} container(s): ${running} running, ${stopped} stopped`,
147
+ ` ${containers.length} container(s): ${parts.join('; ')}`,
93
148
  ),
94
149
  )
95
150
  console.log()
@@ -71,19 +71,19 @@ export async function handleCreateForRestore(): Promise<{
71
71
  }
72
72
 
73
73
  const binarySpinner = createSpinner(
74
- `Checking PostgreSQL ${version} binaries...`,
74
+ `Checking ${dbEngine.displayName} ${version} binaries...`,
75
75
  )
76
76
  binarySpinner.start()
77
77
 
78
78
  const isInstalled = await dbEngine.isBinaryInstalled(version)
79
79
  if (isInstalled) {
80
- binarySpinner.succeed(`PostgreSQL ${version} binaries ready (cached)`)
80
+ binarySpinner.succeed(`${dbEngine.displayName} ${version} binaries ready (cached)`)
81
81
  } else {
82
- binarySpinner.text = `Downloading PostgreSQL ${version} binaries...`
82
+ binarySpinner.text = `Downloading ${dbEngine.displayName} ${version} binaries...`
83
83
  await dbEngine.ensureBinaries(version, ({ message }) => {
84
84
  binarySpinner.text = message
85
85
  })
86
- binarySpinner.succeed(`PostgreSQL ${version} binaries downloaded`)
86
+ binarySpinner.succeed(`${dbEngine.displayName} ${version} binaries downloaded`)
87
87
  }
88
88
 
89
89
  while (await containerManager.exists(containerName)) {
@@ -112,7 +112,7 @@ export async function handleCreateForRestore(): Promise<{
112
112
 
113
113
  initSpinner.succeed('Database cluster initialized')
114
114
 
115
- const startSpinner = createSpinner('Starting PostgreSQL...')
115
+ const startSpinner = createSpinner(`Starting ${dbEngine.displayName}...`)
116
116
  startSpinner.start()
117
117
 
118
118
  const config = await containerManager.getConfig(containerName)
@@ -124,7 +124,7 @@ export async function handleCreateForRestore(): Promise<{
124
124
  await dbEngine.start(config)
125
125
  await containerManager.updateConfig(containerName, { status: 'running' })
126
126
 
127
- startSpinner.succeed('PostgreSQL started')
127
+ startSpinner.succeed(`${dbEngine.displayName} started`)
128
128
 
129
129
  if (database !== 'postgres') {
130
130
  const dbSpinner = createSpinner(`Creating database "${database}"...`)
@@ -259,11 +259,18 @@ export async function handleRestore(): Promise<void> {
259
259
  message: 'Connection string:',
260
260
  validate: (input: string) => {
261
261
  if (!input) return true
262
- if (
263
- !input.startsWith('postgresql://') &&
264
- !input.startsWith('postgres://')
265
- ) {
266
- return 'Connection string must start with postgresql:// or postgres://'
262
+ if (config.engine === 'mysql') {
263
+ if (!input.startsWith('mysql://')) {
264
+ return 'Connection string must start with mysql://'
265
+ }
266
+ } else {
267
+ // PostgreSQL
268
+ if (
269
+ !input.startsWith('postgresql://') &&
270
+ !input.startsWith('postgres://')
271
+ ) {
272
+ return 'Connection string must start with postgresql:// or postgres://'
273
+ }
267
274
  }
268
275
  return true
269
276
  },
@@ -300,15 +307,21 @@ export async function handleRestore(): Promise<void> {
300
307
 
301
308
  if (
302
309
  e.message.includes('pg_dump not found') ||
310
+ e.message.includes('mysqldump not found') ||
303
311
  e.message.includes('ENOENT')
304
312
  ) {
305
- const installed = await promptInstallDependencies('pg_dump')
313
+ const missingTool = e.message.includes('mysqldump')
314
+ ? 'mysqldump'
315
+ : 'pg_dump'
316
+ const toolEngine = missingTool === 'mysqldump' ? 'mysql' : 'postgresql'
317
+ const installed = await promptInstallDependencies(missingTool, toolEngine as Engine)
306
318
  if (installed) {
307
319
  continue
308
320
  }
309
321
  } else {
322
+ const dumpTool = config.engine === 'mysql' ? 'mysqldump' : 'pg_dump'
310
323
  console.log()
311
- console.log(error('pg_dump error:'))
324
+ console.log(error(`${dumpTool} error:`))
312
325
  console.log(chalk.gray(` ${e.message}`))
313
326
  console.log()
314
327
  }
@@ -592,6 +605,7 @@ export async function handleBackup(): Promise<void> {
592
605
  const containerName = await promptContainerSelect(
593
606
  running,
594
607
  'Select container to backup:',
608
+ { includeBack: true },
595
609
  )
596
610
  if (!containerName) return
597
611
 
@@ -715,6 +729,7 @@ export async function handleClone(): Promise<void> {
715
729
  const sourceName = await promptContainerSelect(
716
730
  stopped,
717
731
  'Select container to clone:',
732
+ { includeBack: true },
718
733
  )
719
734
  if (!sourceName) return
720
735