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 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** - Restore pg_dump/mysqldump backups
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
- # Run directly with pnpx (no install needed)
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
- # Or install globally
25
- pnpm add -g spindb
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
+ )