spindb 0.5.3 → 0.5.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.
- package/README.md +78 -2
- package/cli/commands/backup.ts +263 -0
- package/cli/commands/config.ts +144 -66
- package/cli/commands/connect.ts +336 -111
- package/cli/commands/engines.ts +2 -10
- package/cli/commands/info.ts +3 -3
- package/cli/commands/list.ts +43 -5
- package/cli/commands/menu.ts +447 -33
- package/cli/commands/restore.ts +4 -1
- package/cli/index.ts +2 -0
- package/cli/ui/prompts.ts +99 -6
- package/cli/ui/theme.ts +12 -1
- package/config/os-dependencies.ts +92 -0
- package/core/binary-manager.ts +12 -19
- package/core/config-manager.ts +133 -37
- package/core/container-manager.ts +76 -2
- package/core/dependency-manager.ts +140 -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 +68 -1
- package/package.json +1 -1
- package/types/index.ts +20 -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
|
|
@@ -48,9 +48,10 @@ spindb connect mydb
|
|
|
48
48
|
| `spindb info [name]` | Show container details (or all containers) |
|
|
49
49
|
| `spindb start [name]` | Start a container |
|
|
50
50
|
| `spindb stop [name]` | Stop a container |
|
|
51
|
-
| `spindb connect [name]` | Connect with psql/mysql shell |
|
|
51
|
+
| `spindb connect [name]` | Connect with psql/mysql shell (`--pgcli`/`--mycli` for enhanced) |
|
|
52
52
|
| `spindb url [name]` | Output connection string |
|
|
53
53
|
| `spindb edit [name]` | Edit container properties (rename, port) |
|
|
54
|
+
| `spindb backup [name]` | Create a database backup |
|
|
54
55
|
| `spindb restore [name] [backup]` | Restore a backup file |
|
|
55
56
|
| `spindb clone [source] [target]` | Clone a container |
|
|
56
57
|
| `spindb delete [name]` | Delete a container |
|
|
@@ -107,6 +108,44 @@ Data is stored in `~/.spindb/`:
|
|
|
107
108
|
└── config.json
|
|
108
109
|
```
|
|
109
110
|
|
|
111
|
+
## How Data Persists
|
|
112
|
+
|
|
113
|
+
SpinDB runs database servers as **native processes** on your machine—no Docker or virtualization involved. When you start a container, SpinDB launches the actual `postgres` or `mysqld` binary, which listens on localhost at your configured port.
|
|
114
|
+
|
|
115
|
+
### What happens when you start a container
|
|
116
|
+
|
|
117
|
+
1. SpinDB runs the database server binary (e.g., `pg_ctl start`)
|
|
118
|
+
2. The server binds to `127.0.0.1` on your configured port
|
|
119
|
+
3. A **PID file** is created to track the running process
|
|
120
|
+
4. Logs are written to the container's log file
|
|
121
|
+
|
|
122
|
+
### What happens when you stop a container
|
|
123
|
+
|
|
124
|
+
1. SpinDB sends a shutdown signal to the database process
|
|
125
|
+
2. The server flushes any pending writes to disk
|
|
126
|
+
3. The **PID file is removed**
|
|
127
|
+
4. Your data remains safely in the data directory
|
|
128
|
+
|
|
129
|
+
### Where your data lives
|
|
130
|
+
|
|
131
|
+
All database files persist in the container's `data/` directory:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
~/.spindb/containers/postgresql/mydb/
|
|
135
|
+
├── container.json # Container configuration (port, version, etc.)
|
|
136
|
+
├── postgres.log # Server logs
|
|
137
|
+
└── data/ # ← Your actual database files live here
|
|
138
|
+
├── base/ # Table data
|
|
139
|
+
├── pg_wal/ # Transaction logs
|
|
140
|
+
└── ...
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The `data/` directory is a standard PostgreSQL/MySQL data directory. Stopping and starting a container doesn't affect your data—only the PID file changes.
|
|
144
|
+
|
|
145
|
+
### How SpinDB knows if a container is running
|
|
146
|
+
|
|
147
|
+
SpinDB checks for the PID file and verifies the process is still alive. No PID file (or dead process) = stopped container.
|
|
148
|
+
|
|
110
149
|
## Client Tools
|
|
111
150
|
|
|
112
151
|
SpinDB bundles the PostgreSQL **server** but not client tools. For `connect` and `restore` commands, you need client tools installed.
|
|
@@ -205,6 +244,18 @@ spindb start test-branch
|
|
|
205
244
|
# Interactive shell (auto-detects engine)
|
|
206
245
|
spindb connect mydb
|
|
207
246
|
|
|
247
|
+
# Use pgcli/mycli for enhanced shell (dropdown auto-completion)
|
|
248
|
+
spindb connect mydb --pgcli # PostgreSQL
|
|
249
|
+
spindb connect mydb --mycli # MySQL
|
|
250
|
+
|
|
251
|
+
# Install pgcli/mycli and connect in one command
|
|
252
|
+
spindb connect mydb --install-pgcli
|
|
253
|
+
spindb connect mydb --install-mycli
|
|
254
|
+
|
|
255
|
+
# Use usql for universal SQL client (works with both engines)
|
|
256
|
+
spindb connect mydb --tui
|
|
257
|
+
spindb connect mydb --install-tui
|
|
258
|
+
|
|
208
259
|
# Or use connection string directly
|
|
209
260
|
psql postgresql://postgres@localhost:5432/mydb
|
|
210
261
|
mysql -u root -h 127.0.0.1 -P 3306 mydb
|
|
@@ -254,6 +305,31 @@ psql $(spindb url mydb)
|
|
|
254
305
|
spindb url mydb --copy
|
|
255
306
|
```
|
|
256
307
|
|
|
308
|
+
### Backup databases
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
# Backup with default settings (SQL format, auto-generated filename)
|
|
312
|
+
spindb backup mydb
|
|
313
|
+
|
|
314
|
+
# Backup specific database in container
|
|
315
|
+
spindb backup mydb --database my_app_db
|
|
316
|
+
|
|
317
|
+
# Custom filename and output directory
|
|
318
|
+
spindb backup mydb --name my-backup --output ./backups/
|
|
319
|
+
|
|
320
|
+
# Choose format: sql (plain text) or dump (compressed/binary)
|
|
321
|
+
spindb backup mydb --format sql # Plain SQL (.sql)
|
|
322
|
+
spindb backup mydb --format dump # PostgreSQL custom format (.dump) / MySQL gzipped (.sql.gz)
|
|
323
|
+
|
|
324
|
+
# Shorthand flags
|
|
325
|
+
spindb backup mydb --sql # Same as --format sql
|
|
326
|
+
spindb backup mydb --dump # Same as --format dump
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Backup formats:**
|
|
330
|
+
- **SQL format** (`.sql`) - Plain text, universal, can be read/edited
|
|
331
|
+
- **Dump format** - PostgreSQL uses custom format (`.dump`), MySQL uses gzipped SQL (`.sql.gz`)
|
|
332
|
+
|
|
257
333
|
### Edit containers
|
|
258
334
|
|
|
259
335
|
```bash
|
|
@@ -0,0 +1,263 @@
|
|
|
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('-o, --output <path>', 'Output directory (defaults to current directory)')
|
|
55
|
+
.option('--format <format>', 'Output format: sql or dump')
|
|
56
|
+
.option('--sql', 'Output as plain SQL (shorthand for --format sql)')
|
|
57
|
+
.option('--dump', 'Output as dump format (shorthand for --format dump)')
|
|
58
|
+
.action(
|
|
59
|
+
async (
|
|
60
|
+
containerArg: string | undefined,
|
|
61
|
+
options: {
|
|
62
|
+
database?: string
|
|
63
|
+
name?: string
|
|
64
|
+
output?: string
|
|
65
|
+
format?: string
|
|
66
|
+
sql?: boolean
|
|
67
|
+
dump?: boolean
|
|
68
|
+
},
|
|
69
|
+
) => {
|
|
70
|
+
try {
|
|
71
|
+
let containerName = containerArg
|
|
72
|
+
|
|
73
|
+
// Interactive selection if no container provided
|
|
74
|
+
if (!containerName) {
|
|
75
|
+
const containers = await containerManager.list()
|
|
76
|
+
const running = containers.filter((c) => c.status === 'running')
|
|
77
|
+
|
|
78
|
+
if (running.length === 0) {
|
|
79
|
+
if (containers.length === 0) {
|
|
80
|
+
console.log(
|
|
81
|
+
warning('No containers found. Create one with: spindb create'),
|
|
82
|
+
)
|
|
83
|
+
} else {
|
|
84
|
+
console.log(
|
|
85
|
+
warning(
|
|
86
|
+
'No running containers. Start one first with: spindb start',
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const selected = await promptContainerSelect(
|
|
94
|
+
running,
|
|
95
|
+
'Select container to backup:',
|
|
96
|
+
)
|
|
97
|
+
if (!selected) return
|
|
98
|
+
containerName = selected
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get container config
|
|
102
|
+
const config = await containerManager.getConfig(containerName)
|
|
103
|
+
if (!config) {
|
|
104
|
+
console.error(error(`Container "${containerName}" not found`))
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { engine: engineName } = config
|
|
109
|
+
|
|
110
|
+
// Check if running
|
|
111
|
+
const running = await processManager.isRunning(containerName, {
|
|
112
|
+
engine: engineName,
|
|
113
|
+
})
|
|
114
|
+
if (!running) {
|
|
115
|
+
console.error(
|
|
116
|
+
error(
|
|
117
|
+
`Container "${containerName}" is not running. Start it first.`,
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
process.exit(1)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get engine
|
|
124
|
+
const engine = getEngine(engineName)
|
|
125
|
+
|
|
126
|
+
// Check for required client tools
|
|
127
|
+
const depsSpinner = createSpinner('Checking required tools...')
|
|
128
|
+
depsSpinner.start()
|
|
129
|
+
|
|
130
|
+
let missingDeps = await getMissingDependencies(config.engine)
|
|
131
|
+
if (missingDeps.length > 0) {
|
|
132
|
+
depsSpinner.warn(
|
|
133
|
+
`Missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// Offer to install
|
|
137
|
+
const installed = await promptInstallDependencies(
|
|
138
|
+
missingDeps[0].binary,
|
|
139
|
+
config.engine,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if (!installed) {
|
|
143
|
+
process.exit(1)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Verify installation worked
|
|
147
|
+
missingDeps = await getMissingDependencies(config.engine)
|
|
148
|
+
if (missingDeps.length > 0) {
|
|
149
|
+
console.error(
|
|
150
|
+
error(
|
|
151
|
+
`Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
process.exit(1)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(chalk.green(' ✓ All required tools are now available'))
|
|
158
|
+
console.log()
|
|
159
|
+
} else {
|
|
160
|
+
depsSpinner.succeed('Required tools available')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Determine which database to backup
|
|
164
|
+
let databaseName = options.database
|
|
165
|
+
|
|
166
|
+
if (!databaseName) {
|
|
167
|
+
// Get list of databases in container
|
|
168
|
+
const databases = config.databases || [config.database]
|
|
169
|
+
|
|
170
|
+
if (databases.length > 1) {
|
|
171
|
+
// Interactive mode: prompt for database selection
|
|
172
|
+
databaseName = await promptDatabaseSelect(
|
|
173
|
+
databases,
|
|
174
|
+
'Select database to backup:',
|
|
175
|
+
)
|
|
176
|
+
} else {
|
|
177
|
+
// Single database: use it
|
|
178
|
+
databaseName = databases[0]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Determine format
|
|
183
|
+
let format: 'sql' | 'dump' = 'sql' // Default to SQL
|
|
184
|
+
|
|
185
|
+
if (options.sql) {
|
|
186
|
+
format = 'sql'
|
|
187
|
+
} else if (options.dump) {
|
|
188
|
+
format = 'dump'
|
|
189
|
+
} else if (options.format) {
|
|
190
|
+
if (options.format !== 'sql' && options.format !== 'dump') {
|
|
191
|
+
console.error(error('Format must be "sql" or "dump"'))
|
|
192
|
+
process.exit(1)
|
|
193
|
+
}
|
|
194
|
+
format = options.format as 'sql' | 'dump'
|
|
195
|
+
} else if (!containerArg) {
|
|
196
|
+
// Interactive mode: prompt for format
|
|
197
|
+
format = await promptBackupFormat(engineName)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Determine filename
|
|
201
|
+
const defaultFilename = generateDefaultFilename(containerName, databaseName)
|
|
202
|
+
let filename = options.name || defaultFilename
|
|
203
|
+
|
|
204
|
+
// In interactive mode with no name provided, optionally prompt for custom name
|
|
205
|
+
if (!containerArg && !options.name) {
|
|
206
|
+
filename = await promptBackupFilename(defaultFilename)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Build full output path
|
|
210
|
+
const extension = getExtension(format, engineName)
|
|
211
|
+
const outputDir = options.output || process.cwd()
|
|
212
|
+
const outputPath = join(outputDir, `${filename}${extension}`)
|
|
213
|
+
|
|
214
|
+
// Create backup
|
|
215
|
+
const backupSpinner = createSpinner(
|
|
216
|
+
`Creating ${format === 'sql' ? 'SQL' : 'dump'} backup of "${databaseName}"...`,
|
|
217
|
+
)
|
|
218
|
+
backupSpinner.start()
|
|
219
|
+
|
|
220
|
+
const result = await engine.backup(config, outputPath, {
|
|
221
|
+
database: databaseName,
|
|
222
|
+
format,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
backupSpinner.succeed('Backup created successfully')
|
|
226
|
+
|
|
227
|
+
// Show result
|
|
228
|
+
console.log()
|
|
229
|
+
console.log(success('Backup complete'))
|
|
230
|
+
console.log()
|
|
231
|
+
console.log(chalk.gray(' File:'), chalk.cyan(result.path))
|
|
232
|
+
console.log(chalk.gray(' Size:'), chalk.white(formatBytes(result.size)))
|
|
233
|
+
console.log(chalk.gray(' Format:'), chalk.white(result.format))
|
|
234
|
+
console.log()
|
|
235
|
+
} catch (err) {
|
|
236
|
+
const e = err as Error
|
|
237
|
+
|
|
238
|
+
// Check if this is a missing tool error
|
|
239
|
+
const missingToolPatterns = [
|
|
240
|
+
'pg_dump not found',
|
|
241
|
+
'mysqldump not found',
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
const matchingPattern = missingToolPatterns.find((p) =>
|
|
245
|
+
e.message.includes(p),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if (matchingPattern) {
|
|
249
|
+
const missingTool = matchingPattern.replace(' not found', '')
|
|
250
|
+
const installed = await promptInstallDependencies(missingTool)
|
|
251
|
+
if (installed) {
|
|
252
|
+
console.log(
|
|
253
|
+
chalk.yellow(' Please re-run your command to continue.'),
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
process.exit(1)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.error(error(e.message))
|
|
260
|
+
process.exit(1)
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
)
|
package/cli/commands/config.ts
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import { existsSync } from 'fs'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
configManager,
|
|
6
|
+
POSTGRESQL_TOOLS,
|
|
7
|
+
MYSQL_TOOLS,
|
|
8
|
+
ENHANCED_SHELLS,
|
|
9
|
+
ALL_TOOLS,
|
|
10
|
+
} from '../../core/config-manager'
|
|
5
11
|
import { error, success, header } from '../ui/theme'
|
|
6
12
|
import { createSpinner } from '../ui/spinner'
|
|
7
13
|
import type { BinaryTool } from '../../types'
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Helper to display a tool's config
|
|
17
|
+
*/
|
|
18
|
+
function displayToolConfig(
|
|
19
|
+
tool: BinaryTool,
|
|
20
|
+
binaryConfig: { path: string; version?: string; source: string } | undefined,
|
|
21
|
+
): void {
|
|
22
|
+
if (binaryConfig) {
|
|
23
|
+
const sourceLabel =
|
|
24
|
+
binaryConfig.source === 'system'
|
|
25
|
+
? chalk.blue('system')
|
|
26
|
+
: binaryConfig.source === 'custom'
|
|
27
|
+
? chalk.yellow('custom')
|
|
28
|
+
: chalk.green('bundled')
|
|
29
|
+
const versionLabel = binaryConfig.version
|
|
30
|
+
? chalk.gray(` (v${binaryConfig.version})`)
|
|
31
|
+
: ''
|
|
32
|
+
console.log(
|
|
33
|
+
` ${chalk.cyan(tool.padEnd(15))} ${binaryConfig.path}${versionLabel} [${sourceLabel}]`,
|
|
34
|
+
)
|
|
35
|
+
} else {
|
|
36
|
+
console.log(
|
|
37
|
+
` ${chalk.cyan(tool.padEnd(15))} ${chalk.gray('not detected')}`,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
15
41
|
|
|
16
42
|
export const configCommand = new Command('config')
|
|
17
43
|
.description('Manage spindb configuration')
|
|
@@ -26,37 +52,36 @@ export const configCommand = new Command('config')
|
|
|
26
52
|
console.log(header('SpinDB Configuration'))
|
|
27
53
|
console.log()
|
|
28
54
|
|
|
29
|
-
|
|
30
|
-
console.log(chalk.
|
|
31
|
-
|
|
32
|
-
for (const tool of
|
|
33
|
-
|
|
34
|
-
if (binaryConfig) {
|
|
35
|
-
const sourceLabel =
|
|
36
|
-
binaryConfig.source === 'system'
|
|
37
|
-
? chalk.blue('system')
|
|
38
|
-
: binaryConfig.source === 'custom'
|
|
39
|
-
? chalk.yellow('custom')
|
|
40
|
-
: chalk.green('bundled')
|
|
41
|
-
const versionLabel = binaryConfig.version
|
|
42
|
-
? chalk.gray(` (v${binaryConfig.version})`)
|
|
43
|
-
: ''
|
|
44
|
-
console.log(
|
|
45
|
-
` ${chalk.cyan(tool.padEnd(15))} ${binaryConfig.path}${versionLabel} [${sourceLabel}]`,
|
|
46
|
-
)
|
|
47
|
-
} else {
|
|
48
|
-
console.log(
|
|
49
|
-
` ${chalk.cyan(tool.padEnd(15))} ${chalk.gray('not configured')}`,
|
|
50
|
-
)
|
|
51
|
-
}
|
|
55
|
+
// PostgreSQL tools
|
|
56
|
+
console.log(chalk.bold(' 🐘 PostgreSQL Tools:'))
|
|
57
|
+
console.log(chalk.gray(' ' + '─'.repeat(60)))
|
|
58
|
+
for (const tool of POSTGRESQL_TOOLS) {
|
|
59
|
+
displayToolConfig(tool, config.binaries[tool])
|
|
52
60
|
}
|
|
61
|
+
console.log()
|
|
62
|
+
|
|
63
|
+
// MySQL tools
|
|
64
|
+
console.log(chalk.bold(' 🐬 MySQL Tools:'))
|
|
65
|
+
console.log(chalk.gray(' ' + '─'.repeat(60)))
|
|
66
|
+
for (const tool of MYSQL_TOOLS) {
|
|
67
|
+
displayToolConfig(tool, config.binaries[tool])
|
|
68
|
+
}
|
|
69
|
+
console.log()
|
|
53
70
|
|
|
71
|
+
// Enhanced shells
|
|
72
|
+
console.log(chalk.bold(' ✨ Enhanced Shells (optional):'))
|
|
73
|
+
console.log(chalk.gray(' ' + '─'.repeat(60)))
|
|
74
|
+
for (const tool of ENHANCED_SHELLS) {
|
|
75
|
+
displayToolConfig(tool, config.binaries[tool])
|
|
76
|
+
}
|
|
54
77
|
console.log()
|
|
55
78
|
|
|
56
79
|
if (config.updatedAt) {
|
|
80
|
+
const isStale = await configManager.isStale()
|
|
81
|
+
const staleWarning = isStale ? chalk.yellow(' (stale - run config detect to refresh)') : ''
|
|
57
82
|
console.log(
|
|
58
83
|
chalk.gray(
|
|
59
|
-
` Last updated: ${new Date(config.updatedAt).toLocaleString()}`,
|
|
84
|
+
` Last updated: ${new Date(config.updatedAt).toLocaleString()}${staleWarning}`,
|
|
60
85
|
),
|
|
61
86
|
)
|
|
62
87
|
console.log()
|
|
@@ -70,55 +95,108 @@ export const configCommand = new Command('config')
|
|
|
70
95
|
)
|
|
71
96
|
.addCommand(
|
|
72
97
|
new Command('detect')
|
|
73
|
-
.description('Auto-detect
|
|
98
|
+
.description('Auto-detect all database tools on your system')
|
|
74
99
|
.action(async () => {
|
|
75
100
|
try {
|
|
76
101
|
console.log()
|
|
77
|
-
console.log(header('Detecting
|
|
102
|
+
console.log(header('Detecting Database Tools'))
|
|
78
103
|
console.log()
|
|
79
104
|
|
|
80
|
-
const spinner = createSpinner(
|
|
81
|
-
'Searching for PostgreSQL client tools...',
|
|
82
|
-
)
|
|
105
|
+
const spinner = createSpinner('Searching for database tools...')
|
|
83
106
|
spinner.start()
|
|
84
107
|
|
|
85
108
|
// Clear existing configs to force re-detection
|
|
86
109
|
await configManager.clearAllBinaries()
|
|
87
110
|
|
|
88
|
-
const
|
|
111
|
+
const result = await configManager.initialize()
|
|
89
112
|
|
|
90
113
|
spinner.succeed('Detection complete')
|
|
91
114
|
console.log()
|
|
92
115
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
// Helper to display category results
|
|
117
|
+
async function displayCategory(
|
|
118
|
+
title: string,
|
|
119
|
+
icon: string,
|
|
120
|
+
found: BinaryTool[],
|
|
121
|
+
missing: BinaryTool[],
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
console.log(chalk.bold(` ${icon} ${title}:`))
|
|
124
|
+
|
|
125
|
+
if (found.length > 0) {
|
|
126
|
+
for (const tool of found) {
|
|
127
|
+
const config = await configManager.getBinaryConfig(tool)
|
|
128
|
+
if (config) {
|
|
129
|
+
const versionLabel = config.version
|
|
130
|
+
? chalk.gray(` (v${config.version})`)
|
|
131
|
+
: ''
|
|
132
|
+
console.log(
|
|
133
|
+
` ${chalk.green('✓')} ${chalk.cyan(tool.padEnd(15))} ${config.path}${versionLabel}`,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (missing.length > 0) {
|
|
140
|
+
for (const tool of missing) {
|
|
101
141
|
console.log(
|
|
102
|
-
`
|
|
142
|
+
` ${chalk.gray('○')} ${chalk.gray(tool.padEnd(15))} not found`,
|
|
103
143
|
)
|
|
104
144
|
}
|
|
105
145
|
}
|
|
146
|
+
|
|
106
147
|
console.log()
|
|
107
148
|
}
|
|
108
149
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
150
|
+
await displayCategory(
|
|
151
|
+
'PostgreSQL Tools',
|
|
152
|
+
'🐘',
|
|
153
|
+
result.postgresql.found,
|
|
154
|
+
result.postgresql.missing,
|
|
155
|
+
)
|
|
156
|
+
await displayCategory(
|
|
157
|
+
'MySQL Tools',
|
|
158
|
+
'🐬',
|
|
159
|
+
result.mysql.found,
|
|
160
|
+
result.mysql.missing,
|
|
161
|
+
)
|
|
162
|
+
await displayCategory(
|
|
163
|
+
'Enhanced Shells (optional)',
|
|
164
|
+
'✨',
|
|
165
|
+
result.enhanced.found,
|
|
166
|
+
result.enhanced.missing,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Show install hints for missing required tools
|
|
170
|
+
if (
|
|
171
|
+
result.postgresql.missing.length > 0 ||
|
|
172
|
+
result.mysql.missing.length > 0
|
|
173
|
+
) {
|
|
174
|
+
console.log(chalk.gray(' Install missing tools:'))
|
|
175
|
+
if (result.postgresql.missing.length > 0) {
|
|
176
|
+
console.log(
|
|
177
|
+
chalk.gray(' PostgreSQL: brew install postgresql@17'),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
if (result.mysql.missing.length > 0) {
|
|
181
|
+
console.log(chalk.gray(' MySQL: brew install mysql'))
|
|
113
182
|
}
|
|
114
183
|
console.log()
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Show enhanced shell hints
|
|
187
|
+
if (result.enhanced.missing.length > 0) {
|
|
188
|
+
console.log(chalk.gray(' Optional enhanced shells:'))
|
|
189
|
+
if (result.enhanced.missing.includes('pgcli')) {
|
|
190
|
+
console.log(chalk.gray(' pgcli: brew install pgcli'))
|
|
191
|
+
}
|
|
192
|
+
if (result.enhanced.missing.includes('mycli')) {
|
|
193
|
+
console.log(chalk.gray(' mycli: brew install mycli'))
|
|
194
|
+
}
|
|
195
|
+
if (result.enhanced.missing.includes('usql')) {
|
|
196
|
+
console.log(
|
|
197
|
+
chalk.gray(' usql: brew tap xo/xo && brew install usql'),
|
|
198
|
+
)
|
|
199
|
+
}
|
|
122
200
|
console.log()
|
|
123
201
|
}
|
|
124
202
|
} catch (err) {
|
|
@@ -131,14 +209,14 @@ export const configCommand = new Command('config')
|
|
|
131
209
|
.addCommand(
|
|
132
210
|
new Command('set')
|
|
133
211
|
.description('Set a custom binary path')
|
|
134
|
-
.argument('<tool>',
|
|
212
|
+
.argument('<tool>', 'Tool name (psql, mysql, pgcli, etc.)')
|
|
135
213
|
.argument('<path>', 'Path to the binary')
|
|
136
214
|
.action(async (tool: string, path: string) => {
|
|
137
215
|
try {
|
|
138
216
|
// Validate tool name
|
|
139
|
-
if (!
|
|
217
|
+
if (!ALL_TOOLS.includes(tool as BinaryTool)) {
|
|
140
218
|
console.error(error(`Invalid tool: ${tool}`))
|
|
141
|
-
console.log(chalk.gray(` Valid tools: ${
|
|
219
|
+
console.log(chalk.gray(` Valid tools: ${ALL_TOOLS.join(', ')}`))
|
|
142
220
|
process.exit(1)
|
|
143
221
|
}
|
|
144
222
|
|
|
@@ -164,13 +242,13 @@ export const configCommand = new Command('config')
|
|
|
164
242
|
.addCommand(
|
|
165
243
|
new Command('unset')
|
|
166
244
|
.description('Remove a custom binary path')
|
|
167
|
-
.argument('<tool>',
|
|
245
|
+
.argument('<tool>', 'Tool name (psql, mysql, pgcli, etc.)')
|
|
168
246
|
.action(async (tool: string) => {
|
|
169
247
|
try {
|
|
170
248
|
// Validate tool name
|
|
171
|
-
if (!
|
|
249
|
+
if (!ALL_TOOLS.includes(tool as BinaryTool)) {
|
|
172
250
|
console.error(error(`Invalid tool: ${tool}`))
|
|
173
|
-
console.log(chalk.gray(` Valid tools: ${
|
|
251
|
+
console.log(chalk.gray(` Valid tools: ${ALL_TOOLS.join(', ')}`))
|
|
174
252
|
process.exit(1)
|
|
175
253
|
}
|
|
176
254
|
|
|
@@ -186,13 +264,13 @@ export const configCommand = new Command('config')
|
|
|
186
264
|
.addCommand(
|
|
187
265
|
new Command('path')
|
|
188
266
|
.description('Show the path for a specific tool')
|
|
189
|
-
.argument('<tool>',
|
|
267
|
+
.argument('<tool>', 'Tool name (psql, mysql, pgcli, etc.)')
|
|
190
268
|
.action(async (tool: string) => {
|
|
191
269
|
try {
|
|
192
270
|
// Validate tool name
|
|
193
|
-
if (!
|
|
271
|
+
if (!ALL_TOOLS.includes(tool as BinaryTool)) {
|
|
194
272
|
console.error(error(`Invalid tool: ${tool}`))
|
|
195
|
-
console.log(chalk.gray(` Valid tools: ${
|
|
273
|
+
console.log(chalk.gray(` Valid tools: ${ALL_TOOLS.join(', ')}`))
|
|
196
274
|
process.exit(1)
|
|
197
275
|
}
|
|
198
276
|
|