spindb 0.5.4 → 0.6.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 +46 -4
- package/cli/commands/backup.ts +269 -0
- package/cli/commands/config.ts +200 -67
- package/cli/commands/connect.ts +29 -9
- package/cli/commands/engines.ts +1 -9
- package/cli/commands/list.ts +41 -4
- package/cli/commands/menu.ts +289 -19
- package/cli/commands/restore.ts +3 -0
- package/cli/commands/self-update.ts +109 -0
- package/cli/commands/version.ts +55 -0
- package/cli/index.ts +84 -1
- package/cli/ui/prompts.ts +89 -1
- package/cli/ui/theme.ts +11 -0
- package/core/config-manager.ts +123 -37
- package/core/container-manager.ts +78 -2
- package/core/dependency-manager.ts +5 -0
- package/core/update-manager.ts +194 -0
- package/engines/base-engine.ts +20 -0
- package/engines/mysql/backup.ts +159 -0
- package/engines/mysql/index.ts +39 -0
- package/engines/mysql/restore.ts +16 -2
- package/engines/postgresql/backup.ts +93 -0
- package/engines/postgresql/index.ts +37 -0
- package/package.json +1 -1
- package/types/index.ts +26 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Spin up local PostgreSQL and MySQL databases without Docker. A lightweight alter
|
|
|
10
10
|
- **Interactive menu** - Arrow-key navigation for all operations
|
|
11
11
|
- **Auto port management** - Automatically finds available ports
|
|
12
12
|
- **Clone containers** - Duplicate databases with all data
|
|
13
|
-
- **Backup restore** -
|
|
13
|
+
- **Backup & restore** - Create and restore pg_dump/mysqldump backups
|
|
14
14
|
- **Custom database names** - Specify database name separate from container name
|
|
15
15
|
- **Engine management** - View installed PostgreSQL versions and free up disk space
|
|
16
16
|
- **Dynamic version selection** - Fetches available PostgreSQL versions from Maven Central
|
|
@@ -18,11 +18,23 @@ Spin up local PostgreSQL and MySQL databases without Docker. A lightweight alter
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
#
|
|
21
|
+
# Install globally (recommended)
|
|
22
|
+
npm install -g spindb
|
|
23
|
+
|
|
24
|
+
# Or run directly with pnpx (no install needed)
|
|
22
25
|
pnpx spindb
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Updating
|
|
29
|
+
|
|
30
|
+
SpinDB checks for updates automatically and will notify you when a new version is available:
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
```bash
|
|
33
|
+
# Update to latest version
|
|
34
|
+
spindb self-update
|
|
35
|
+
|
|
36
|
+
# Or check manually
|
|
37
|
+
spindb version --check
|
|
26
38
|
```
|
|
27
39
|
|
|
28
40
|
## Quick Start
|
|
@@ -51,6 +63,7 @@ spindb connect mydb
|
|
|
51
63
|
| `spindb connect [name]` | Connect with psql/mysql shell (`--pgcli`/`--mycli` for enhanced) |
|
|
52
64
|
| `spindb url [name]` | Output connection string |
|
|
53
65
|
| `spindb edit [name]` | Edit container properties (rename, port) |
|
|
66
|
+
| `spindb backup [name]` | Create a database backup |
|
|
54
67
|
| `spindb restore [name] [backup]` | Restore a backup file |
|
|
55
68
|
| `spindb clone [source] [target]` | Clone a container |
|
|
56
69
|
| `spindb delete [name]` | Delete a container |
|
|
@@ -60,6 +73,10 @@ spindb connect mydb
|
|
|
60
73
|
| `spindb config detect` | Auto-detect database tools |
|
|
61
74
|
| `spindb deps check` | Check status of client tools |
|
|
62
75
|
| `spindb deps install` | Install missing client tools |
|
|
76
|
+
| `spindb version` | Show current version |
|
|
77
|
+
| `spindb version --check` | Check for available updates |
|
|
78
|
+
| `spindb self-update` | Update to latest version |
|
|
79
|
+
| `spindb config update-check [on\|off]` | Enable/disable update notifications |
|
|
63
80
|
|
|
64
81
|
## Supported Engines
|
|
65
82
|
|
|
@@ -304,6 +321,31 @@ psql $(spindb url mydb)
|
|
|
304
321
|
spindb url mydb --copy
|
|
305
322
|
```
|
|
306
323
|
|
|
324
|
+
### Backup databases
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
# Backup with default settings (SQL format, auto-generated filename)
|
|
328
|
+
spindb backup mydb
|
|
329
|
+
|
|
330
|
+
# Backup specific database in container
|
|
331
|
+
spindb backup mydb --database my_app_db
|
|
332
|
+
|
|
333
|
+
# Custom filename and output directory
|
|
334
|
+
spindb backup mydb --name my-backup --output ./backups/
|
|
335
|
+
|
|
336
|
+
# Choose format: sql (plain text) or dump (compressed/binary)
|
|
337
|
+
spindb backup mydb --format sql # Plain SQL (.sql)
|
|
338
|
+
spindb backup mydb --format dump # PostgreSQL custom format (.dump) / MySQL gzipped (.sql.gz)
|
|
339
|
+
|
|
340
|
+
# Shorthand flags
|
|
341
|
+
spindb backup mydb --sql # Same as --format sql
|
|
342
|
+
spindb backup mydb --dump # Same as --format dump
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Backup formats:**
|
|
346
|
+
- **SQL format** (`.sql`) - Plain text, universal, can be read/edited
|
|
347
|
+
- **Dump format** - PostgreSQL uses custom format (`.dump`), MySQL uses gzipped SQL (`.sql.gz`)
|
|
348
|
+
|
|
307
349
|
### Edit containers
|
|
308
350
|
|
|
309
351
|
```bash
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import { containerManager } from '../../core/container-manager'
|
|
5
|
+
import { processManager } from '../../core/process-manager'
|
|
6
|
+
import { getEngine } from '../../engines'
|
|
7
|
+
import {
|
|
8
|
+
promptContainerSelect,
|
|
9
|
+
promptDatabaseSelect,
|
|
10
|
+
promptBackupFormat,
|
|
11
|
+
promptBackupFilename,
|
|
12
|
+
promptInstallDependencies,
|
|
13
|
+
} from '../ui/prompts'
|
|
14
|
+
import { createSpinner } from '../ui/spinner'
|
|
15
|
+
import { success, error, warning, formatBytes } from '../ui/theme'
|
|
16
|
+
import { getMissingDependencies } from '../../core/dependency-manager'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate a timestamp string for backup filenames
|
|
20
|
+
* Format: YYYY-MM-DDTHHMMSS (ISO 8601 without colons for filesystem compatibility)
|
|
21
|
+
*/
|
|
22
|
+
function generateTimestamp(): string {
|
|
23
|
+
const now = new Date()
|
|
24
|
+
return now.toISOString().replace(/:/g, '').split('.')[0]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate default backup filename
|
|
29
|
+
*/
|
|
30
|
+
function generateDefaultFilename(
|
|
31
|
+
containerName: string,
|
|
32
|
+
database: string,
|
|
33
|
+
): string {
|
|
34
|
+
const timestamp = generateTimestamp()
|
|
35
|
+
return `${containerName}-${database}-backup-${timestamp}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get file extension for backup format
|
|
40
|
+
*/
|
|
41
|
+
function getExtension(format: 'sql' | 'dump', engine: string): string {
|
|
42
|
+
if (format === 'sql') {
|
|
43
|
+
return '.sql'
|
|
44
|
+
}
|
|
45
|
+
// MySQL dump is gzipped SQL, PostgreSQL dump is custom format
|
|
46
|
+
return engine === 'mysql' ? '.sql.gz' : '.dump'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const backupCommand = new Command('backup')
|
|
50
|
+
.description('Create a backup of a database')
|
|
51
|
+
.argument('[container]', 'Container name')
|
|
52
|
+
.option('-d, --database <name>', 'Database to backup')
|
|
53
|
+
.option('-n, --name <name>', 'Custom backup filename (without extension)')
|
|
54
|
+
.option(
|
|
55
|
+
'-o, --output <path>',
|
|
56
|
+
'Output directory (defaults to current directory)',
|
|
57
|
+
)
|
|
58
|
+
.option('--format <format>', 'Output format: sql or dump')
|
|
59
|
+
.option('--sql', 'Output as plain SQL (shorthand for --format sql)')
|
|
60
|
+
.option('--dump', 'Output as dump format (shorthand for --format dump)')
|
|
61
|
+
.action(
|
|
62
|
+
async (
|
|
63
|
+
containerArg: string | undefined,
|
|
64
|
+
options: {
|
|
65
|
+
database?: string
|
|
66
|
+
name?: string
|
|
67
|
+
output?: string
|
|
68
|
+
format?: string
|
|
69
|
+
sql?: boolean
|
|
70
|
+
dump?: boolean
|
|
71
|
+
},
|
|
72
|
+
) => {
|
|
73
|
+
try {
|
|
74
|
+
let containerName = containerArg
|
|
75
|
+
|
|
76
|
+
// Interactive selection if no container provided
|
|
77
|
+
if (!containerName) {
|
|
78
|
+
const containers = await containerManager.list()
|
|
79
|
+
const running = containers.filter((c) => c.status === 'running')
|
|
80
|
+
|
|
81
|
+
if (running.length === 0) {
|
|
82
|
+
if (containers.length === 0) {
|
|
83
|
+
console.log(
|
|
84
|
+
warning('No containers found. Create one with: spindb create'),
|
|
85
|
+
)
|
|
86
|
+
} else {
|
|
87
|
+
console.log(
|
|
88
|
+
warning(
|
|
89
|
+
'No running containers. Start one first with: spindb start',
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const selected = await promptContainerSelect(
|
|
97
|
+
running,
|
|
98
|
+
'Select container to backup:',
|
|
99
|
+
)
|
|
100
|
+
if (!selected) return
|
|
101
|
+
containerName = selected
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Get container config
|
|
105
|
+
const config = await containerManager.getConfig(containerName)
|
|
106
|
+
if (!config) {
|
|
107
|
+
console.error(error(`Container "${containerName}" not found`))
|
|
108
|
+
process.exit(1)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { engine: engineName } = config
|
|
112
|
+
|
|
113
|
+
// Check if running
|
|
114
|
+
const running = await processManager.isRunning(containerName, {
|
|
115
|
+
engine: engineName,
|
|
116
|
+
})
|
|
117
|
+
if (!running) {
|
|
118
|
+
console.error(
|
|
119
|
+
error(
|
|
120
|
+
`Container "${containerName}" is not running. Start it first.`,
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
process.exit(1)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Get engine
|
|
127
|
+
const engine = getEngine(engineName)
|
|
128
|
+
|
|
129
|
+
// Check for required client tools
|
|
130
|
+
const depsSpinner = createSpinner('Checking required tools...')
|
|
131
|
+
depsSpinner.start()
|
|
132
|
+
|
|
133
|
+
let missingDeps = await getMissingDependencies(config.engine)
|
|
134
|
+
if (missingDeps.length > 0) {
|
|
135
|
+
depsSpinner.warn(
|
|
136
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// Offer to install
|
|
140
|
+
const installed = await promptInstallDependencies(
|
|
141
|
+
missingDeps[0].binary,
|
|
142
|
+
config.engine,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if (!installed) {
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Verify installation worked
|
|
150
|
+
missingDeps = await getMissingDependencies(config.engine)
|
|
151
|
+
if (missingDeps.length > 0) {
|
|
152
|
+
console.error(
|
|
153
|
+
error(
|
|
154
|
+
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
process.exit(1)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
161
|
+
console.log()
|
|
162
|
+
} else {
|
|
163
|
+
depsSpinner.succeed('Required tools available')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Determine which database to backup
|
|
167
|
+
let databaseName = options.database
|
|
168
|
+
|
|
169
|
+
if (!databaseName) {
|
|
170
|
+
// Get list of databases in container
|
|
171
|
+
const databases = config.databases || [config.database]
|
|
172
|
+
|
|
173
|
+
if (databases.length > 1) {
|
|
174
|
+
// Interactive mode: prompt for database selection
|
|
175
|
+
databaseName = await promptDatabaseSelect(
|
|
176
|
+
databases,
|
|
177
|
+
'Select database to backup:',
|
|
178
|
+
)
|
|
179
|
+
} else {
|
|
180
|
+
// Single database: use it
|
|
181
|
+
databaseName = databases[0]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Determine format
|
|
186
|
+
let format: 'sql' | 'dump' = 'sql' // Default to SQL
|
|
187
|
+
|
|
188
|
+
if (options.sql) {
|
|
189
|
+
format = 'sql'
|
|
190
|
+
} else if (options.dump) {
|
|
191
|
+
format = 'dump'
|
|
192
|
+
} else if (options.format) {
|
|
193
|
+
if (options.format !== 'sql' && options.format !== 'dump') {
|
|
194
|
+
console.error(error('Format must be "sql" or "dump"'))
|
|
195
|
+
process.exit(1)
|
|
196
|
+
}
|
|
197
|
+
format = options.format as 'sql' | 'dump'
|
|
198
|
+
} else if (!containerArg) {
|
|
199
|
+
// Interactive mode: prompt for format
|
|
200
|
+
format = await promptBackupFormat(engineName)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Determine filename
|
|
204
|
+
const defaultFilename = generateDefaultFilename(
|
|
205
|
+
containerName,
|
|
206
|
+
databaseName,
|
|
207
|
+
)
|
|
208
|
+
let filename = options.name || defaultFilename
|
|
209
|
+
|
|
210
|
+
// In interactive mode with no name provided, optionally prompt for custom name
|
|
211
|
+
if (!containerArg && !options.name) {
|
|
212
|
+
filename = await promptBackupFilename(defaultFilename)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Build full output path
|
|
216
|
+
const extension = getExtension(format, engineName)
|
|
217
|
+
const outputDir = options.output || process.cwd()
|
|
218
|
+
const outputPath = join(outputDir, `${filename}${extension}`)
|
|
219
|
+
|
|
220
|
+
// Create backup
|
|
221
|
+
const backupSpinner = createSpinner(
|
|
222
|
+
`Creating ${format === 'sql' ? 'SQL' : 'dump'} backup of "${databaseName}"...`,
|
|
223
|
+
)
|
|
224
|
+
backupSpinner.start()
|
|
225
|
+
|
|
226
|
+
const result = await engine.backup(config, outputPath, {
|
|
227
|
+
database: databaseName,
|
|
228
|
+
format,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
backupSpinner.succeed('Backup created successfully')
|
|
232
|
+
|
|
233
|
+
// Show result
|
|
234
|
+
console.log()
|
|
235
|
+
console.log(success('Backup complete'))
|
|
236
|
+
console.log()
|
|
237
|
+
console.log(chalk.gray(' File:'), chalk.cyan(result.path))
|
|
238
|
+
console.log(
|
|
239
|
+
chalk.gray(' Size:'),
|
|
240
|
+
chalk.white(formatBytes(result.size)),
|
|
241
|
+
)
|
|
242
|
+
console.log(chalk.gray(' Format:'), chalk.white(result.format))
|
|
243
|
+
console.log()
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const e = err as Error
|
|
246
|
+
|
|
247
|
+
// Check if this is a missing tool error
|
|
248
|
+
const missingToolPatterns = ['pg_dump not found', 'mysqldump not found']
|
|
249
|
+
|
|
250
|
+
const matchingPattern = missingToolPatterns.find((p) =>
|
|
251
|
+
e.message.includes(p),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if (matchingPattern) {
|
|
255
|
+
const missingTool = matchingPattern.replace(' not found', '')
|
|
256
|
+
const installed = await promptInstallDependencies(missingTool)
|
|
257
|
+
if (installed) {
|
|
258
|
+
console.log(
|
|
259
|
+
chalk.yellow(' Please re-run your command to continue.'),
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
process.exit(1)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.error(error(e.message))
|
|
266
|
+
process.exit(1)
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
)
|