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/commands/info.ts
CHANGED
|
@@ -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
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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(
|
|
211
|
+
chalk.green(portOrPath.padEnd(8)) +
|
|
159
212
|
chalk.gray(container.database.padEnd(16)) +
|
|
160
213
|
statusDisplay,
|
|
161
214
|
)
|
package/cli/commands/list.ts
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
119
|
+
chalk.white(padWithEmoji(engineDisplay, 14)) +
|
|
79
120
|
chalk.yellow(container.version.padEnd(10)) +
|
|
80
|
-
chalk.green(
|
|
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
|
|
89
|
-
const
|
|
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): ${
|
|
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
|
|
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(
|
|
80
|
+
binarySpinner.succeed(`${dbEngine.displayName} ${version} binaries ready (cached)`)
|
|
81
81
|
} else {
|
|
82
|
-
binarySpinner.text = `Downloading
|
|
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(
|
|
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(
|
|
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(
|
|
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('
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
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(
|
|
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
|
|