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 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
@@ -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
+ )
@@ -1,17 +1,43 @@
1
1
  import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
3
  import { existsSync } from 'fs'
4
- import { configManager } from '../../core/config-manager'
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
- const VALID_TOOLS: BinaryTool[] = [
10
- 'psql',
11
- 'pg_dump',
12
- 'pg_restore',
13
- 'pg_basebackup',
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
- console.log(chalk.bold(' Binary Paths:'))
30
- console.log(chalk.gray(' ' + '─'.repeat(50)))
31
-
32
- for (const tool of VALID_TOOLS) {
33
- const binaryConfig = config.binaries[tool]
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 PostgreSQL client tools on your system')
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 PostgreSQL Tools'))
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 { found, missing } = await configManager.initialize()
111
+ const result = await configManager.initialize()
89
112
 
90
113
  spinner.succeed('Detection complete')
91
114
  console.log()
92
115
 
93
- if (found.length > 0) {
94
- console.log(chalk.bold(' Found:'))
95
- for (const tool of found) {
96
- const config = await configManager.getBinaryConfig(tool)
97
- if (config) {
98
- const versionLabel = config.version
99
- ? chalk.gray(` (v${config.version})`)
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
- ` ${chalk.green('')} ${chalk.cyan(tool.padEnd(15))} ${config.path}${versionLabel}`,
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
- if (missing.length > 0) {
110
- console.log(chalk.bold(' Not found:'))
111
- for (const tool of missing) {
112
- console.log(` ${chalk.red('✗')} ${chalk.cyan(tool)}`)
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
- console.log(chalk.gray(' Install missing tools:'))
116
- console.log(
117
- chalk.gray(
118
- ' macOS: brew install libpq && brew link --force libpq',
119
- ),
120
- )
121
- console.log(chalk.gray(' Ubuntu: apt install postgresql-client'))
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>', `Tool name (${VALID_TOOLS.join(', ')})`)
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 (!VALID_TOOLS.includes(tool as BinaryTool)) {
217
+ if (!ALL_TOOLS.includes(tool as BinaryTool)) {
140
218
  console.error(error(`Invalid tool: ${tool}`))
141
- console.log(chalk.gray(` Valid tools: ${VALID_TOOLS.join(', ')}`))
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>', `Tool name (${VALID_TOOLS.join(', ')})`)
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 (!VALID_TOOLS.includes(tool as BinaryTool)) {
249
+ if (!ALL_TOOLS.includes(tool as BinaryTool)) {
172
250
  console.error(error(`Invalid tool: ${tool}`))
173
- console.log(chalk.gray(` Valid tools: ${VALID_TOOLS.join(', ')}`))
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>', `Tool name (${VALID_TOOLS.join(', ')})`)
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 (!VALID_TOOLS.includes(tool as BinaryTool)) {
271
+ if (!ALL_TOOLS.includes(tool as BinaryTool)) {
194
272
  console.error(error(`Invalid tool: ${tool}`))
195
- console.log(chalk.gray(` Valid tools: ${VALID_TOOLS.join(', ')}`))
273
+ console.log(chalk.gray(` Valid tools: ${ALL_TOOLS.join(', ')}`))
196
274
  process.exit(1)
197
275
  }
198
276