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.
@@ -15,8 +15,12 @@ import {
15
15
  packageManagers,
16
16
  getEngineDependencies,
17
17
  getUniqueDependencies,
18
+ usqlDependency,
19
+ pgcliDependency,
20
+ mycliDependency,
18
21
  } from '../config/os-dependencies'
19
22
  import { platformService } from './platform-service'
23
+ import { configManager } from './config-manager'
20
24
 
21
25
  const execAsync = promisify(exec)
22
26
 
@@ -265,6 +269,10 @@ export async function installDependency(
265
269
  execWithInheritedStdio(cmd)
266
270
  }
267
271
 
272
+ // Refresh config cache after package manager interaction
273
+ // This ensures newly installed tools are detected with correct versions
274
+ await configManager.refreshAllBinaries()
275
+
268
276
  // Verify installation
269
277
  const status = await checkDependency(dependency)
270
278
  if (!status.installed) {
@@ -421,3 +429,135 @@ export async function getAllDependencyReports(): Promise<
421
429
  )
422
430
  return reports
423
431
  }
432
+
433
+ // =============================================================================
434
+ // usql (Enhanced Shell) Support
435
+ // =============================================================================
436
+
437
+ /**
438
+ * Check if usql is installed
439
+ */
440
+ export async function isUsqlInstalled(): Promise<boolean> {
441
+ const status = await checkDependency(usqlDependency)
442
+ return status.installed
443
+ }
444
+
445
+ /**
446
+ * Get usql dependency status
447
+ */
448
+ export async function getUsqlStatus(): Promise<DependencyStatus> {
449
+ return checkDependency(usqlDependency)
450
+ }
451
+
452
+ /**
453
+ * Install usql using the detected package manager
454
+ */
455
+ export async function installUsql(
456
+ packageManager: DetectedPackageManager,
457
+ ): Promise<InstallResult> {
458
+ return installDependency(usqlDependency, packageManager)
459
+ }
460
+
461
+ /**
462
+ * Get usql manual installation instructions
463
+ */
464
+ export function getUsqlManualInstructions(
465
+ platform: Platform = getCurrentPlatform(),
466
+ ): string[] {
467
+ return getManualInstallInstructions(usqlDependency, platform)
468
+ }
469
+
470
+ /**
471
+ * Get the usql dependency definition
472
+ */
473
+ export function getUsqlDependency(): Dependency {
474
+ return usqlDependency
475
+ }
476
+
477
+ // =============================================================================
478
+ // pgcli (PostgreSQL Enhanced Shell) Support
479
+ // =============================================================================
480
+
481
+ /**
482
+ * Check if pgcli is installed
483
+ */
484
+ export async function isPgcliInstalled(): Promise<boolean> {
485
+ const status = await checkDependency(pgcliDependency)
486
+ return status.installed
487
+ }
488
+
489
+ /**
490
+ * Get pgcli dependency status
491
+ */
492
+ export async function getPgcliStatus(): Promise<DependencyStatus> {
493
+ return checkDependency(pgcliDependency)
494
+ }
495
+
496
+ /**
497
+ * Install pgcli using the detected package manager
498
+ */
499
+ export async function installPgcli(
500
+ packageManager: DetectedPackageManager,
501
+ ): Promise<InstallResult> {
502
+ return installDependency(pgcliDependency, packageManager)
503
+ }
504
+
505
+ /**
506
+ * Get pgcli manual installation instructions
507
+ */
508
+ export function getPgcliManualInstructions(
509
+ platform: Platform = getCurrentPlatform(),
510
+ ): string[] {
511
+ return getManualInstallInstructions(pgcliDependency, platform)
512
+ }
513
+
514
+ /**
515
+ * Get the pgcli dependency definition
516
+ */
517
+ export function getPgcliDependency(): Dependency {
518
+ return pgcliDependency
519
+ }
520
+
521
+ // =============================================================================
522
+ // mycli (MySQL Enhanced Shell) Support
523
+ // =============================================================================
524
+
525
+ /**
526
+ * Check if mycli is installed
527
+ */
528
+ export async function isMycliInstalled(): Promise<boolean> {
529
+ const status = await checkDependency(mycliDependency)
530
+ return status.installed
531
+ }
532
+
533
+ /**
534
+ * Get mycli dependency status
535
+ */
536
+ export async function getMycliStatus(): Promise<DependencyStatus> {
537
+ return checkDependency(mycliDependency)
538
+ }
539
+
540
+ /**
541
+ * Install mycli using the detected package manager
542
+ */
543
+ export async function installMycli(
544
+ packageManager: DetectedPackageManager,
545
+ ): Promise<InstallResult> {
546
+ return installDependency(mycliDependency, packageManager)
547
+ }
548
+
549
+ /**
550
+ * Get mycli manual installation instructions
551
+ */
552
+ export function getMycliManualInstructions(
553
+ platform: Platform = getCurrentPlatform(),
554
+ ): string[] {
555
+ return getManualInstallInstructions(mycliDependency, platform)
556
+ }
557
+
558
+ /**
559
+ * Get the mycli dependency definition
560
+ */
561
+ export function getMycliDependency(): Dependency {
562
+ return mycliDependency
563
+ }
@@ -2,6 +2,8 @@ import type {
2
2
  ContainerConfig,
3
3
  ProgressCallback,
4
4
  BackupFormat,
5
+ BackupOptions,
6
+ BackupResult,
5
7
  RestoreResult,
6
8
  DumpResult,
7
9
  StatusResult,
@@ -131,4 +133,22 @@ export abstract class BaseEngine {
131
133
  connectionString: string,
132
134
  outputPath: string,
133
135
  ): Promise<DumpResult>
136
+
137
+ /**
138
+ * Get the size of a database in bytes
139
+ * Returns null if the container is not running or size cannot be determined
140
+ */
141
+ abstract getDatabaseSize(container: ContainerConfig): Promise<number | null>
142
+
143
+ /**
144
+ * Create a backup of a database
145
+ * @param container - The container configuration
146
+ * @param outputPath - Path to write the backup file
147
+ * @param options - Backup options including database name and format
148
+ */
149
+ abstract backup(
150
+ container: ContainerConfig,
151
+ outputPath: string,
152
+ options: BackupOptions,
153
+ ): Promise<BackupResult>
134
154
  }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * MySQL Backup
3
+ *
4
+ * Creates database backups in SQL or compressed (.dump = gzipped SQL) format using mysqldump.
5
+ */
6
+
7
+ import { spawn } from 'child_process'
8
+ import { createWriteStream } from 'fs'
9
+ import { stat } from 'fs/promises'
10
+ import { createGzip } from 'zlib'
11
+ import { pipeline } from 'stream/promises'
12
+ import { getMysqldumpPath } from './binary-detection'
13
+ import { getEngineDefaults } from '../../config/defaults'
14
+ import type { ContainerConfig, BackupOptions, BackupResult } from '../../types'
15
+
16
+ const engineDef = getEngineDefaults('mysql')
17
+
18
+ /**
19
+ * Create a backup of a MySQL database
20
+ *
21
+ * CLI equivalent:
22
+ * - SQL format: mysqldump -h 127.0.0.1 -P {port} -u root --result-file={outputPath} {database}
23
+ * - Dump format: mysqldump -h 127.0.0.1 -P {port} -u root {database} | gzip > {outputPath}
24
+ */
25
+ export async function createBackup(
26
+ container: ContainerConfig,
27
+ outputPath: string,
28
+ options: BackupOptions,
29
+ ): Promise<BackupResult> {
30
+ const { port } = container
31
+ const { database, format } = options
32
+
33
+ const mysqldump = await getMysqldumpPath()
34
+ if (!mysqldump) {
35
+ throw new Error(
36
+ 'mysqldump not found. Install MySQL client tools:\n' +
37
+ ' macOS: brew install mysql-client\n' +
38
+ ' Ubuntu/Debian: sudo apt install mysql-client',
39
+ )
40
+ }
41
+
42
+ if (format === 'sql') {
43
+ return createSqlBackup(mysqldump, port, database, outputPath)
44
+ } else {
45
+ return createCompressedBackup(mysqldump, port, database, outputPath)
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Create a plain SQL backup
51
+ */
52
+ async function createSqlBackup(
53
+ mysqldump: string,
54
+ port: number,
55
+ database: string,
56
+ outputPath: string,
57
+ ): Promise<BackupResult> {
58
+ return new Promise((resolve, reject) => {
59
+ const args = [
60
+ '-h',
61
+ '127.0.0.1',
62
+ '-P',
63
+ String(port),
64
+ '-u',
65
+ engineDef.superuser,
66
+ '--result-file',
67
+ outputPath,
68
+ database,
69
+ ]
70
+
71
+ const proc = spawn(mysqldump, args, {
72
+ stdio: ['pipe', 'pipe', 'pipe'],
73
+ })
74
+
75
+ let stderr = ''
76
+
77
+ proc.stderr?.on('data', (data: Buffer) => {
78
+ stderr += data.toString()
79
+ })
80
+
81
+ proc.on('error', (err: NodeJS.ErrnoException) => {
82
+ reject(err)
83
+ })
84
+
85
+ proc.on('close', async (code) => {
86
+ if (code === 0) {
87
+ const stats = await stat(outputPath)
88
+ resolve({
89
+ path: outputPath,
90
+ format: 'sql',
91
+ size: stats.size,
92
+ })
93
+ } else {
94
+ const errorMessage = stderr || `mysqldump exited with code ${code}`
95
+ reject(new Error(errorMessage))
96
+ }
97
+ })
98
+ })
99
+ }
100
+
101
+ /**
102
+ * Create a compressed (gzipped) backup
103
+ * Uses Node's zlib for compression instead of relying on system gzip
104
+ */
105
+ async function createCompressedBackup(
106
+ mysqldump: string,
107
+ port: number,
108
+ database: string,
109
+ outputPath: string,
110
+ ): Promise<BackupResult> {
111
+ return new Promise((resolve, reject) => {
112
+ const args = [
113
+ '-h',
114
+ '127.0.0.1',
115
+ '-P',
116
+ String(port),
117
+ '-u',
118
+ engineDef.superuser,
119
+ database,
120
+ ]
121
+
122
+ const proc = spawn(mysqldump, args, {
123
+ stdio: ['pipe', 'pipe', 'pipe'],
124
+ })
125
+
126
+ const gzip = createGzip()
127
+ const output = createWriteStream(outputPath)
128
+
129
+ let stderr = ''
130
+
131
+ proc.stderr?.on('data', (data: Buffer) => {
132
+ stderr += data.toString()
133
+ })
134
+
135
+ // Pipe mysqldump stdout -> gzip -> file
136
+ pipeline(proc.stdout!, gzip, output)
137
+ .then(async () => {
138
+ const stats = await stat(outputPath)
139
+ resolve({
140
+ path: outputPath,
141
+ format: 'compressed',
142
+ size: stats.size,
143
+ })
144
+ })
145
+ .catch(reject)
146
+
147
+ proc.on('error', (err: NodeJS.ErrnoException) => {
148
+ reject(err)
149
+ })
150
+
151
+ proc.on('close', (code) => {
152
+ if (code !== 0) {
153
+ const errorMessage = stderr || `mysqldump exited with code ${code}`
154
+ reject(new Error(errorMessage))
155
+ }
156
+ // If code is 0, the pipeline promise will resolve
157
+ })
158
+ })
159
+ }
@@ -33,10 +33,13 @@ import {
33
33
  restoreBackup,
34
34
  parseConnectionString,
35
35
  } from './restore'
36
+ import { createBackup } from './backup'
36
37
  import type {
37
38
  ContainerConfig,
38
39
  ProgressCallback,
39
40
  BackupFormat,
41
+ BackupOptions,
42
+ BackupResult,
40
43
  RestoreResult,
41
44
  DumpResult,
42
45
  StatusResult,
@@ -734,6 +737,31 @@ export class MySQLEngine extends BaseEngine {
734
737
  }
735
738
  }
736
739
 
740
+ /**
741
+ * Get the size of the container's database in bytes
742
+ * Uses information_schema.tables to sum data_length + index_length
743
+ * Returns null if container is not running or query fails
744
+ */
745
+ async getDatabaseSize(container: ContainerConfig): Promise<number | null> {
746
+ const { port, database } = container
747
+ const db = database || 'mysql'
748
+
749
+ try {
750
+ const mysql = await getMysqlClientPath()
751
+ if (!mysql) return null
752
+
753
+ // Query information_schema for total data + index size
754
+ const { stdout } = await execAsync(
755
+ `"${mysql}" -h 127.0.0.1 -P ${port} -u ${engineDef.superuser} -N -e "SELECT COALESCE(SUM(data_length + index_length), 0) FROM information_schema.tables WHERE table_schema = '${db}'"`,
756
+ )
757
+ const size = parseInt(stdout.trim(), 10)
758
+ return isNaN(size) ? null : size
759
+ } catch {
760
+ // Container not running or query failed
761
+ return null
762
+ }
763
+ }
764
+
737
765
  /**
738
766
  * Create a dump from a remote database using a connection string
739
767
  * CLI wrapper: mysqldump -h {host} -P {port} -u {user} -p{pass} {db} > {file}
@@ -803,6 +831,17 @@ export class MySQLEngine extends BaseEngine {
803
831
  })
804
832
  })
805
833
  }
834
+
835
+ /**
836
+ * Create a backup of a MySQL database
837
+ */
838
+ async backup(
839
+ container: ContainerConfig,
840
+ outputPath: string,
841
+ options: BackupOptions,
842
+ ): Promise<BackupResult> {
843
+ return createBackup(container, outputPath, options)
844
+ }
806
845
  }
807
846
 
808
847
  export const mysqlEngine = new MySQLEngine()
@@ -7,6 +7,7 @@
7
7
  import { spawn } from 'child_process'
8
8
  import { createReadStream } from 'fs'
9
9
  import { open } from 'fs/promises'
10
+ import { createGunzip } from 'zlib'
10
11
  import { getMysqlClientPath } from './binary-detection'
11
12
  import { validateRestoreCompatibility } from './version-validator'
12
13
  import { getEngineDefaults } from '../../config/defaults'
@@ -200,6 +201,7 @@ export async function restoreBackup(
200
201
 
201
202
  // Restore using mysql client
202
203
  // CLI: mysql -h 127.0.0.1 -P {port} -u root {db} < {file}
204
+ // For compressed files: gunzip -c {file} | mysql ...
203
205
  return new Promise((resolve, reject) => {
204
206
  const args = ['-h', '127.0.0.1', '-P', String(port), '-u', user, database]
205
207
 
@@ -207,9 +209,21 @@ export async function restoreBackup(
207
209
  stdio: ['pipe', 'pipe', 'pipe'],
208
210
  })
209
211
 
210
- // Pipe backup file to stdin
212
+ // Pipe backup file to stdin, decompressing if necessary
211
213
  const fileStream = createReadStream(backupPath)
212
- fileStream.pipe(proc.stdin)
214
+
215
+ if (format.format === 'compressed') {
216
+ // Decompress gzipped file before piping to mysql
217
+ const gunzip = createGunzip()
218
+ fileStream.pipe(gunzip).pipe(proc.stdin)
219
+
220
+ // Handle gunzip errors
221
+ gunzip.on('error', (err) => {
222
+ reject(new Error(`Failed to decompress backup file: ${err.message}`))
223
+ })
224
+ } else {
225
+ fileStream.pipe(proc.stdin)
226
+ }
213
227
 
214
228
  let stdout = ''
215
229
  let stderr = ''
@@ -0,0 +1,93 @@
1
+ /**
2
+ * PostgreSQL Backup
3
+ *
4
+ * Creates database backups in SQL or custom (.dump) format using pg_dump.
5
+ */
6
+
7
+ import { spawn } from 'child_process'
8
+ import { stat } from 'fs/promises'
9
+ import { configManager } from '../../core/config-manager'
10
+ import { defaults } from '../../config/defaults'
11
+ import type { ContainerConfig, BackupOptions, BackupResult } from '../../types'
12
+
13
+ /**
14
+ * Get pg_dump path from config, with helpful error message
15
+ */
16
+ async function getPgDumpPath(): Promise<string> {
17
+ const pgDumpPath = await configManager.getBinaryPath('pg_dump')
18
+ if (!pgDumpPath) {
19
+ throw new Error(
20
+ 'pg_dump not found. Install PostgreSQL client tools:\n' +
21
+ ' macOS: brew install libpq && brew link --force libpq\n' +
22
+ ' Ubuntu/Debian: apt install postgresql-client\n\n' +
23
+ 'Or configure manually: spindb config set pg_dump /path/to/pg_dump',
24
+ )
25
+ }
26
+ return pgDumpPath
27
+ }
28
+
29
+ /**
30
+ * Create a backup of a PostgreSQL database
31
+ *
32
+ * CLI equivalent:
33
+ * - SQL format: pg_dump -Fp -h 127.0.0.1 -p {port} -U postgres -d {database} -f {outputPath}
34
+ * - Dump format: pg_dump -Fc -h 127.0.0.1 -p {port} -U postgres -d {database} -f {outputPath}
35
+ */
36
+ export async function createBackup(
37
+ container: ContainerConfig,
38
+ outputPath: string,
39
+ options: BackupOptions,
40
+ ): Promise<BackupResult> {
41
+ const { port } = container
42
+ const { database, format } = options
43
+
44
+ const pgDumpPath = await getPgDumpPath()
45
+
46
+ // -Fp = plain SQL format, -Fc = custom format
47
+ const formatFlag = format === 'sql' ? '-Fp' : '-Fc'
48
+
49
+ return new Promise((resolve, reject) => {
50
+ const args = [
51
+ '-h',
52
+ '127.0.0.1',
53
+ '-p',
54
+ String(port),
55
+ '-U',
56
+ defaults.superuser,
57
+ '-d',
58
+ database,
59
+ formatFlag,
60
+ '-f',
61
+ outputPath,
62
+ ]
63
+
64
+ const proc = spawn(pgDumpPath, args, {
65
+ stdio: ['pipe', 'pipe', 'pipe'],
66
+ })
67
+
68
+ let stderr = ''
69
+
70
+ proc.stderr?.on('data', (data: Buffer) => {
71
+ stderr += data.toString()
72
+ })
73
+
74
+ proc.on('error', (err: NodeJS.ErrnoException) => {
75
+ reject(err)
76
+ })
77
+
78
+ proc.on('close', async (code) => {
79
+ if (code === 0) {
80
+ // Get file size
81
+ const stats = await stat(outputPath)
82
+ resolve({
83
+ path: outputPath,
84
+ format: format === 'sql' ? 'sql' : 'custom',
85
+ size: stats.size,
86
+ })
87
+ } else {
88
+ const errorMessage = stderr || `pg_dump exited with code ${code}`
89
+ reject(new Error(errorMessage))
90
+ }
91
+ })
92
+ })
93
+ }
@@ -12,12 +12,17 @@ import {
12
12
  getBinaryUrl,
13
13
  SUPPORTED_MAJOR_VERSIONS,
14
14
  fetchAvailableVersions,
15
+ getLatestVersion,
16
+ FALLBACK_VERSION_MAP,
15
17
  } from './binary-urls'
16
18
  import { detectBackupFormat, restoreBackup } from './restore'
19
+ import { createBackup } from './backup'
17
20
  import type {
18
21
  ContainerConfig,
19
22
  ProgressCallback,
20
23
  BackupFormat,
24
+ BackupOptions,
25
+ BackupResult,
21
26
  RestoreResult,
22
27
  DumpResult,
23
28
  StatusResult,
@@ -50,14 +55,42 @@ export class PostgreSQLEngine extends BaseEngine {
50
55
  }
51
56
  }
52
57
 
58
+ /**
59
+ * Resolve a version string to a full version.
60
+ * If given a major version like '17', resolves to '17.7.0'.
61
+ * If already a full version like '17.7.0', returns as-is.
62
+ */
63
+ resolveFullVersion(version: string): string {
64
+ // Check if already a full version (has at least one dot with numbers after)
65
+ if (/^\d+\.\d+/.test(version)) {
66
+ return version
67
+ }
68
+ // It's a major version, resolve using fallback map (sync, no network)
69
+ return FALLBACK_VERSION_MAP[version] || `${version}.0.0`
70
+ }
71
+
72
+ /**
73
+ * Resolve version asynchronously (tries network first for latest)
74
+ */
75
+ async resolveFullVersionAsync(version: string): Promise<string> {
76
+ // Check if already a full version
77
+ if (/^\d+\.\d+/.test(version)) {
78
+ return version
79
+ }
80
+ // Resolve from network/cache
81
+ return getLatestVersion(version)
82
+ }
83
+
53
84
  /**
54
85
  * Get binary path for current platform
86
+ * Uses full version for directory naming (e.g., postgresql-17.7.0-darwin-arm64)
55
87
  */
56
88
  getBinaryPath(version: string): string {
89
+ const fullVersion = this.resolveFullVersion(version)
57
90
  const { platform: p, arch: a } = this.getPlatformInfo()
58
91
  return paths.getBinaryPath({
59
92
  engine: 'postgresql',
60
- version,
93
+ version: fullVersion,
61
94
  platform: p,
62
95
  arch: a,
63
96
  })
@@ -339,6 +372,29 @@ export class PostgreSQLEngine extends BaseEngine {
339
372
  }
340
373
  }
341
374
 
375
+ /**
376
+ * Get the size of the container's database in bytes
377
+ * Uses pg_database_size() to get accurate data size
378
+ * Returns null if container is not running or query fails
379
+ */
380
+ async getDatabaseSize(container: ContainerConfig): Promise<number | null> {
381
+ const { port, database } = container
382
+ const db = database || 'postgres'
383
+
384
+ try {
385
+ const psqlPath = await this.getPsqlPath()
386
+ // Query pg_database_size for the specific database
387
+ const { stdout } = await execAsync(
388
+ `"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -t -A -c "SELECT pg_database_size('${db}')"`,
389
+ )
390
+ const size = parseInt(stdout.trim(), 10)
391
+ return isNaN(size) ? null : size
392
+ } catch {
393
+ // Container not running or query failed
394
+ return null
395
+ }
396
+ }
397
+
342
398
  /**
343
399
  * Create a dump from a remote database using a connection string
344
400
  * @param connectionString PostgreSQL connection string (e.g., postgresql://user:pass@host:port/dbname)
@@ -390,6 +446,17 @@ export class PostgreSQLEngine extends BaseEngine {
390
446
  })
391
447
  })
392
448
  }
449
+
450
+ /**
451
+ * Create a backup of a PostgreSQL database
452
+ */
453
+ async backup(
454
+ container: ContainerConfig,
455
+ outputPath: string,
456
+ options: BackupOptions,
457
+ ): Promise<BackupResult> {
458
+ return createBackup(container, outputPath, options)
459
+ }
393
460
  }
394
461
 
395
462
  export const postgresqlEngine = new PostgreSQLEngine()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spindb",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Spin up local database containers without Docker. A DBngin-like CLI for PostgreSQL and MySQL.",
5
5
  "type": "module",
6
6
  "bin": {
package/types/index.ts CHANGED
@@ -4,6 +4,7 @@ export type ContainerConfig = {
4
4
  version: string
5
5
  port: number
6
6
  database: string
7
+ databases?: string[]
7
8
  created: string
8
9
  status: 'created' | 'running' | 'stopped'
9
10
  clonedFrom?: string
@@ -56,6 +57,17 @@ export type RestoreResult = {
56
57
  code?: number
57
58
  }
58
59
 
60
+ export type BackupOptions = {
61
+ database: string
62
+ format: 'sql' | 'dump'
63
+ }
64
+
65
+ export type BackupResult = {
66
+ path: string
67
+ format: string
68
+ size: number
69
+ }
70
+
59
71
  export type DumpResult = {
60
72
  filePath: string
61
73
  stdout?: string
@@ -85,6 +97,10 @@ export type BinaryTool =
85
97
  | 'mysqlpump'
86
98
  | 'mysqld'
87
99
  | 'mysqladmin'
100
+ // Enhanced shells (optional)
101
+ | 'pgcli'
102
+ | 'mycli'
103
+ | 'usql'
88
104
 
89
105
  /**
90
106
  * Source of a binary - bundled (downloaded by spindb) or system (found on PATH)
@@ -118,6 +134,10 @@ export type SpinDBConfig = {
118
134
  mysqlpump?: BinaryConfig
119
135
  mysqld?: BinaryConfig
120
136
  mysqladmin?: BinaryConfig
137
+ // Enhanced shells (optional)
138
+ pgcli?: BinaryConfig
139
+ mycli?: BinaryConfig
140
+ usql?: BinaryConfig
121
141
  }
122
142
  // Default settings
123
143
  defaults?: {