spindb 0.1.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.
Files changed (41) hide show
  1. package/.claude/settings.local.json +20 -0
  2. package/.env.example +1 -0
  3. package/.prettierignore +4 -0
  4. package/.prettierrc +6 -0
  5. package/CLAUDE.md +162 -0
  6. package/README.md +204 -0
  7. package/TODO.md +66 -0
  8. package/bin/cli.js +7 -0
  9. package/eslint.config.js +18 -0
  10. package/package.json +52 -0
  11. package/seeds/mysql/sample-db.sql +22 -0
  12. package/seeds/postgres/sample-db.sql +27 -0
  13. package/src/bin/cli.ts +8 -0
  14. package/src/cli/commands/clone.ts +101 -0
  15. package/src/cli/commands/config.ts +215 -0
  16. package/src/cli/commands/connect.ts +106 -0
  17. package/src/cli/commands/create.ts +148 -0
  18. package/src/cli/commands/delete.ts +94 -0
  19. package/src/cli/commands/list.ts +69 -0
  20. package/src/cli/commands/menu.ts +675 -0
  21. package/src/cli/commands/restore.ts +161 -0
  22. package/src/cli/commands/start.ts +95 -0
  23. package/src/cli/commands/stop.ts +91 -0
  24. package/src/cli/index.ts +38 -0
  25. package/src/cli/ui/prompts.ts +197 -0
  26. package/src/cli/ui/spinner.ts +94 -0
  27. package/src/cli/ui/theme.ts +113 -0
  28. package/src/config/defaults.ts +49 -0
  29. package/src/config/paths.ts +53 -0
  30. package/src/core/binary-manager.ts +239 -0
  31. package/src/core/config-manager.ts +259 -0
  32. package/src/core/container-manager.ts +234 -0
  33. package/src/core/port-manager.ts +84 -0
  34. package/src/core/process-manager.ts +353 -0
  35. package/src/engines/base-engine.ts +103 -0
  36. package/src/engines/index.ts +46 -0
  37. package/src/engines/postgresql/binary-urls.ts +52 -0
  38. package/src/engines/postgresql/index.ts +298 -0
  39. package/src/engines/postgresql/restore.ts +173 -0
  40. package/src/types/index.ts +97 -0
  41. package/tsconfig.json +24 -0
@@ -0,0 +1,103 @@
1
+ import type {
2
+ ContainerConfig,
3
+ ProgressCallback,
4
+ BackupFormat,
5
+ RestoreResult,
6
+ StatusResult,
7
+ } from '@/types'
8
+
9
+ /**
10
+ * Base class for database engines
11
+ * All engines (PostgreSQL, MySQL, SQLite) should extend this class
12
+ */
13
+ export abstract class BaseEngine {
14
+ abstract name: string
15
+ abstract displayName: string
16
+ abstract defaultPort: number
17
+ abstract supportedVersions: string[]
18
+
19
+ /**
20
+ * Get the download URL for binaries
21
+ */
22
+ abstract getBinaryUrl(version: string, platform: string, arch: string): string
23
+
24
+ /**
25
+ * Verify that the binaries are working correctly
26
+ */
27
+ abstract verifyBinary(binPath: string): Promise<boolean>
28
+
29
+ /**
30
+ * Initialize a new data directory
31
+ */
32
+ abstract initDataDir(
33
+ containerName: string,
34
+ version: string,
35
+ options?: Record<string, unknown>,
36
+ ): Promise<string>
37
+
38
+ /**
39
+ * Start the database server
40
+ */
41
+ abstract start(
42
+ container: ContainerConfig,
43
+ onProgress?: ProgressCallback,
44
+ ): Promise<{ port: number; connectionString: string }>
45
+
46
+ /**
47
+ * Stop the database server
48
+ */
49
+ abstract stop(container: ContainerConfig): Promise<void>
50
+
51
+ /**
52
+ * Get the status of the database server
53
+ */
54
+ abstract status(container: ContainerConfig): Promise<StatusResult>
55
+
56
+ /**
57
+ * Detect the format of a backup file
58
+ */
59
+ abstract detectBackupFormat(filePath: string): Promise<BackupFormat>
60
+
61
+ /**
62
+ * Restore a backup to the database
63
+ */
64
+ abstract restore(
65
+ container: ContainerConfig,
66
+ backupPath: string,
67
+ options?: Record<string, unknown>,
68
+ ): Promise<RestoreResult>
69
+
70
+ /**
71
+ * Get the connection string for a container
72
+ */
73
+ abstract getConnectionString(
74
+ container: ContainerConfig,
75
+ database?: string,
76
+ ): string
77
+
78
+ /**
79
+ * Open an interactive shell/CLI connection
80
+ */
81
+ abstract connect(container: ContainerConfig, database?: string): Promise<void>
82
+
83
+ /**
84
+ * Create a new database within the container
85
+ */
86
+ abstract createDatabase(
87
+ container: ContainerConfig,
88
+ database: string,
89
+ ): Promise<void>
90
+
91
+ /**
92
+ * Check if binaries are installed
93
+ */
94
+ abstract isBinaryInstalled(version: string): Promise<boolean>
95
+
96
+ /**
97
+ * Ensure binaries are available, downloading if necessary
98
+ */
99
+ abstract ensureBinaries(
100
+ version: string,
101
+ onProgress?: ProgressCallback,
102
+ ): Promise<string>
103
+ }
@@ -0,0 +1,46 @@
1
+ import { postgresqlEngine } from '@/engines/postgresql'
2
+ import type { BaseEngine } from '@/engines/base-engine'
3
+ import type { EngineInfo } from '@/types'
4
+
5
+ /**
6
+ * Registry of available database engines
7
+ */
8
+ export const engines: Record<string, BaseEngine> = {
9
+ postgresql: postgresqlEngine,
10
+ postgres: postgresqlEngine, // Alias
11
+ pg: postgresqlEngine, // Alias
12
+ }
13
+
14
+ /**
15
+ * Get an engine by name
16
+ */
17
+ export function getEngine(name: string): BaseEngine {
18
+ const engine = engines[name.toLowerCase()]
19
+ if (!engine) {
20
+ const available = [...new Set(Object.values(engines))].map((e) => e.name)
21
+ throw new Error(
22
+ `Unknown engine "${name}". Available: ${available.join(', ')}`,
23
+ )
24
+ }
25
+ return engine
26
+ }
27
+
28
+ /**
29
+ * List all available engines
30
+ */
31
+ export function listEngines(): EngineInfo[] {
32
+ // Return unique engines (filter out aliases)
33
+ const seen = new Set<BaseEngine>()
34
+ return Object.entries(engines)
35
+ .filter(([, engine]) => {
36
+ if (seen.has(engine)) return false
37
+ seen.add(engine)
38
+ return true
39
+ })
40
+ .map(([, engine]) => ({
41
+ name: engine.name,
42
+ displayName: engine.displayName,
43
+ defaultPort: engine.defaultPort,
44
+ supportedVersions: engine.supportedVersions,
45
+ }))
46
+ }
@@ -0,0 +1,52 @@
1
+ import { defaults } from '@/config/defaults'
2
+
3
+ /**
4
+ * Map major versions to latest stable patch versions
5
+ */
6
+ export const VERSION_MAP: Record<string, string> = {
7
+ '14': '14.15.0',
8
+ '15': '15.10.0',
9
+ '16': '16.6.0',
10
+ '17': '17.2.0',
11
+ }
12
+
13
+ /**
14
+ * Get the zonky.io platform identifier
15
+ */
16
+ export function getZonkyPlatform(
17
+ platform: string,
18
+ arch: string,
19
+ ): string | undefined {
20
+ const key = `${platform}-${arch}`
21
+ return defaults.platformMappings[key]
22
+ }
23
+
24
+ /**
25
+ * Build the download URL for PostgreSQL binaries from zonky.io
26
+ */
27
+ export function getBinaryUrl(
28
+ version: string,
29
+ platform: string,
30
+ arch: string,
31
+ ): string {
32
+ const zonkyPlatform = getZonkyPlatform(platform, arch)
33
+ if (!zonkyPlatform) {
34
+ throw new Error(`Unsupported platform: ${platform}-${arch}`)
35
+ }
36
+
37
+ const fullVersion = VERSION_MAP[version]
38
+ if (!fullVersion) {
39
+ throw new Error(
40
+ `Unsupported PostgreSQL version: ${version}. Supported: ${Object.keys(VERSION_MAP).join(', ')}`,
41
+ )
42
+ }
43
+
44
+ return `https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-${zonkyPlatform}/${fullVersion}/embedded-postgres-binaries-${zonkyPlatform}-${fullVersion}.jar`
45
+ }
46
+
47
+ /**
48
+ * Get the full version string for a major version
49
+ */
50
+ export function getFullVersion(majorVersion: string): string | null {
51
+ return VERSION_MAP[majorVersion] || null
52
+ }
@@ -0,0 +1,298 @@
1
+ import { platform, arch } from 'os'
2
+ import { join } from 'path'
3
+ import { spawn, exec } from 'child_process'
4
+ import { promisify } from 'util'
5
+ import { BaseEngine } from '@/engines/base-engine'
6
+ import { binaryManager } from '@/core/binary-manager'
7
+ import { processManager } from '@/core/process-manager'
8
+ import { configManager } from '@/core/config-manager'
9
+ import { paths } from '@/config/paths'
10
+ import { defaults } from '@/config/defaults'
11
+ import { getBinaryUrl, VERSION_MAP } from './binary-urls'
12
+ import { detectBackupFormat, restoreBackup } from './restore'
13
+ import type {
14
+ ContainerConfig,
15
+ ProgressCallback,
16
+ BackupFormat,
17
+ RestoreResult,
18
+ StatusResult,
19
+ } from '@/types'
20
+
21
+ const execAsync = promisify(exec)
22
+
23
+ export class PostgreSQLEngine extends BaseEngine {
24
+ name = 'postgresql'
25
+ displayName = 'PostgreSQL'
26
+ defaultPort = 5432
27
+ supportedVersions = Object.keys(VERSION_MAP)
28
+
29
+ /**
30
+ * Get current platform info
31
+ */
32
+ getPlatformInfo(): { platform: string; arch: string } {
33
+ return {
34
+ platform: platform(),
35
+ arch: arch(),
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Get binary path for current platform
41
+ */
42
+ getBinaryPath(version: string): string {
43
+ const { platform: p, arch: a } = this.getPlatformInfo()
44
+ return paths.getBinaryPath('postgresql', version, p, a)
45
+ }
46
+
47
+ /**
48
+ * Get binary download URL
49
+ */
50
+ getBinaryUrl(version: string, plat: string, arc: string): string {
51
+ return getBinaryUrl(version, plat, arc)
52
+ }
53
+
54
+ /**
55
+ * Verify binary installation
56
+ */
57
+ async verifyBinary(binPath: string): Promise<boolean> {
58
+ const { platform: p, arch: a } = this.getPlatformInfo()
59
+ // Extract version from path
60
+ const parts = binPath.split('-')
61
+ const version = parts[1]
62
+ return binaryManager.verify(version, p, a)
63
+ }
64
+
65
+ /**
66
+ * Ensure PostgreSQL binaries are available
67
+ */
68
+ async ensureBinaries(
69
+ version: string,
70
+ onProgress?: ProgressCallback,
71
+ ): Promise<string> {
72
+ const { platform: p, arch: a } = this.getPlatformInfo()
73
+ return binaryManager.ensureInstalled(version, p, a, onProgress)
74
+ }
75
+
76
+ /**
77
+ * Check if binaries are installed
78
+ */
79
+ async isBinaryInstalled(version: string): Promise<boolean> {
80
+ const { platform: p, arch: a } = this.getPlatformInfo()
81
+ return binaryManager.isInstalled(version, p, a)
82
+ }
83
+
84
+ /**
85
+ * Initialize a new PostgreSQL data directory
86
+ */
87
+ async initDataDir(
88
+ containerName: string,
89
+ version: string,
90
+ options: Record<string, unknown> = {},
91
+ ): Promise<string> {
92
+ const binPath = this.getBinaryPath(version)
93
+ const initdbPath = join(binPath, 'bin', 'initdb')
94
+ const dataDir = paths.getContainerDataPath(containerName)
95
+
96
+ await processManager.initdb(initdbPath, dataDir, {
97
+ superuser: (options.superuser as string) || defaults.superuser,
98
+ })
99
+
100
+ return dataDir
101
+ }
102
+
103
+ /**
104
+ * Start PostgreSQL server
105
+ */
106
+ async start(
107
+ container: ContainerConfig,
108
+ onProgress?: ProgressCallback,
109
+ ): Promise<{ port: number; connectionString: string }> {
110
+ const { name, version, port } = container
111
+ const binPath = this.getBinaryPath(version)
112
+ const pgCtlPath = join(binPath, 'bin', 'pg_ctl')
113
+ const dataDir = paths.getContainerDataPath(name)
114
+ const logFile = paths.getContainerLogPath(name)
115
+
116
+ onProgress?.({ stage: 'starting', message: 'Starting PostgreSQL...' })
117
+
118
+ await processManager.start(pgCtlPath, dataDir, {
119
+ port,
120
+ logFile,
121
+ })
122
+
123
+ return {
124
+ port,
125
+ connectionString: this.getConnectionString(container),
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Stop PostgreSQL server
131
+ */
132
+ async stop(container: ContainerConfig): Promise<void> {
133
+ const { name, version } = container
134
+ const binPath = this.getBinaryPath(version)
135
+ const pgCtlPath = join(binPath, 'bin', 'pg_ctl')
136
+ const dataDir = paths.getContainerDataPath(name)
137
+
138
+ await processManager.stop(pgCtlPath, dataDir)
139
+ }
140
+
141
+ /**
142
+ * Get PostgreSQL server status
143
+ */
144
+ async status(container: ContainerConfig): Promise<StatusResult> {
145
+ const { name, version } = container
146
+ const binPath = this.getBinaryPath(version)
147
+ const pgCtlPath = join(binPath, 'bin', 'pg_ctl')
148
+ const dataDir = paths.getContainerDataPath(name)
149
+
150
+ return processManager.status(pgCtlPath, dataDir)
151
+ }
152
+
153
+ /**
154
+ * Detect backup format
155
+ */
156
+ async detectBackupFormat(filePath: string): Promise<BackupFormat> {
157
+ return detectBackupFormat(filePath)
158
+ }
159
+
160
+ /**
161
+ * Restore a backup
162
+ */
163
+ async restore(
164
+ container: ContainerConfig,
165
+ backupPath: string,
166
+ options: Record<string, unknown> = {},
167
+ ): Promise<RestoreResult> {
168
+ const { version, port } = container
169
+ const binPath = this.getBinaryPath(version)
170
+ const database = (options.database as string) || container.name
171
+
172
+ // First create the database if it doesn't exist
173
+ if (options.createDatabase !== false) {
174
+ await this.createDatabase(container, database)
175
+ }
176
+
177
+ return restoreBackup(binPath, backupPath, {
178
+ port,
179
+ database,
180
+ user: defaults.superuser,
181
+ ...(options as { format?: string }),
182
+ })
183
+ }
184
+
185
+ /**
186
+ * Get connection string
187
+ */
188
+ getConnectionString(container: ContainerConfig, database?: string): string {
189
+ const { port } = container
190
+ const db = database || 'postgres'
191
+ return `postgresql://${defaults.superuser}@localhost:${port}/${db}`
192
+ }
193
+
194
+ /**
195
+ * Get path to psql, using config manager to find it
196
+ */
197
+ async getPsqlPath(): Promise<string> {
198
+ const psqlPath = await configManager.getBinaryPath('psql')
199
+ if (!psqlPath) {
200
+ throw new Error(
201
+ 'psql not found. Install PostgreSQL client tools:\n' +
202
+ ' macOS: brew install libpq && brew link --force libpq\n' +
203
+ ' Ubuntu/Debian: apt install postgresql-client\n\n' +
204
+ 'Or configure manually: spindb config set psql /path/to/psql',
205
+ )
206
+ }
207
+ return psqlPath
208
+ }
209
+
210
+ /**
211
+ * Get path to pg_restore, using config manager to find it
212
+ */
213
+ async getPgRestorePath(): Promise<string> {
214
+ const pgRestorePath = await configManager.getBinaryPath('pg_restore')
215
+ if (!pgRestorePath) {
216
+ throw new Error(
217
+ 'pg_restore not found. Install PostgreSQL client tools:\n' +
218
+ ' macOS: brew install libpq && brew link --force libpq\n' +
219
+ ' Ubuntu/Debian: apt install postgresql-client\n\n' +
220
+ 'Or configure manually: spindb config set pg_restore /path/to/pg_restore',
221
+ )
222
+ }
223
+ return pgRestorePath
224
+ }
225
+
226
+ /**
227
+ * Get path to pg_dump, using config manager to find it
228
+ */
229
+ async getPgDumpPath(): Promise<string> {
230
+ const pgDumpPath = await configManager.getBinaryPath('pg_dump')
231
+ if (!pgDumpPath) {
232
+ throw new Error(
233
+ 'pg_dump not found. Install PostgreSQL client tools:\n' +
234
+ ' macOS: brew install libpq && brew link --force libpq\n' +
235
+ ' Ubuntu/Debian: apt install postgresql-client\n\n' +
236
+ 'Or configure manually: spindb config set pg_dump /path/to/pg_dump',
237
+ )
238
+ }
239
+ return pgDumpPath
240
+ }
241
+
242
+ /**
243
+ * Open psql interactive shell
244
+ */
245
+ async connect(container: ContainerConfig, database?: string): Promise<void> {
246
+ const { port } = container
247
+ const db = database || 'postgres'
248
+ const psqlPath = await this.getPsqlPath()
249
+
250
+ return new Promise((resolve, reject) => {
251
+ const proc = spawn(
252
+ psqlPath,
253
+ [
254
+ '-h',
255
+ '127.0.0.1',
256
+ '-p',
257
+ String(port),
258
+ '-U',
259
+ defaults.superuser,
260
+ '-d',
261
+ db,
262
+ ],
263
+ { stdio: 'inherit' },
264
+ )
265
+
266
+ proc.on('error', (err: NodeJS.ErrnoException) => {
267
+ reject(err)
268
+ })
269
+
270
+ proc.on('close', () => resolve())
271
+ })
272
+ }
273
+
274
+ /**
275
+ * Create a new database
276
+ */
277
+ async createDatabase(
278
+ container: ContainerConfig,
279
+ database: string,
280
+ ): Promise<void> {
281
+ const { port } = container
282
+ const psqlPath = await this.getPsqlPath()
283
+
284
+ try {
285
+ await execAsync(
286
+ `"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -c 'CREATE DATABASE "${database}"'`,
287
+ )
288
+ } catch (error) {
289
+ const err = error as Error
290
+ // Ignore "database already exists" error
291
+ if (!err.message.includes('already exists')) {
292
+ throw error
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ export const postgresqlEngine = new PostgreSQLEngine()
@@ -0,0 +1,173 @@
1
+ import { readFile } from 'fs/promises'
2
+ import { exec } from 'child_process'
3
+ import { promisify } from 'util'
4
+ import { configManager } from '@/core/config-manager'
5
+ import type { BackupFormat, RestoreResult } from '@/types'
6
+
7
+ const execAsync = promisify(exec)
8
+
9
+ /**
10
+ * Detect the format of a PostgreSQL backup file
11
+ */
12
+ export async function detectBackupFormat(
13
+ filePath: string,
14
+ ): Promise<BackupFormat> {
15
+ // Read the first few bytes to detect format
16
+ const file = await readFile(filePath)
17
+ const buffer = Buffer.alloc(16)
18
+
19
+ // Copy first bytes
20
+ file.copy(buffer, 0, 0, Math.min(16, file.length))
21
+
22
+ // Check for PostgreSQL custom format magic number
23
+ // Custom format starts with "PGDMP"
24
+ if (buffer.toString('ascii', 0, 5) === 'PGDMP') {
25
+ return {
26
+ format: 'custom',
27
+ description: 'PostgreSQL custom format (pg_dump -Fc)',
28
+ restoreCommand: 'pg_restore',
29
+ }
30
+ }
31
+
32
+ // Check for tar format (directory dumps are usually tar)
33
+ // Tar files have "ustar" at offset 257
34
+ if (file.length > 262) {
35
+ const tarMagic = file.toString('ascii', 257, 262)
36
+ if (tarMagic === 'ustar') {
37
+ return {
38
+ format: 'tar',
39
+ description: 'PostgreSQL tar format (pg_dump -Ft)',
40
+ restoreCommand: 'pg_restore',
41
+ }
42
+ }
43
+ }
44
+
45
+ // Check for gzip compression
46
+ if (buffer[0] === 0x1f && buffer[1] === 0x8b) {
47
+ return {
48
+ format: 'compressed',
49
+ description: 'Gzip compressed (likely SQL or custom format)',
50
+ restoreCommand: 'auto',
51
+ }
52
+ }
53
+
54
+ // Check if it looks like SQL (starts with common SQL statements)
55
+ const textStart = buffer.toString('utf8', 0, 16).toLowerCase()
56
+ if (
57
+ textStart.startsWith('--') ||
58
+ textStart.startsWith('/*') ||
59
+ textStart.startsWith('set ') ||
60
+ textStart.startsWith('create') ||
61
+ textStart.startsWith('drop') ||
62
+ textStart.startsWith('begin') ||
63
+ textStart.startsWith('pg_dump')
64
+ ) {
65
+ return {
66
+ format: 'sql',
67
+ description: 'Plain SQL format (pg_dump -Fp)',
68
+ restoreCommand: 'psql',
69
+ }
70
+ }
71
+
72
+ // Default to trying custom format
73
+ return {
74
+ format: 'unknown',
75
+ description: 'Unknown format - will attempt custom format restore',
76
+ restoreCommand: 'pg_restore',
77
+ }
78
+ }
79
+
80
+ export interface RestoreOptions {
81
+ port: number
82
+ database: string
83
+ user?: string
84
+ format?: string
85
+ }
86
+
87
+ /**
88
+ * Get psql path from config, with helpful error message
89
+ */
90
+ async function getPsqlPath(): Promise<string> {
91
+ const psqlPath = await configManager.getBinaryPath('psql')
92
+ if (!psqlPath) {
93
+ throw new Error(
94
+ 'psql not found. Install PostgreSQL client tools:\n' +
95
+ ' macOS: brew install libpq && brew link --force libpq\n' +
96
+ ' Ubuntu/Debian: apt install postgresql-client\n\n' +
97
+ 'Or configure manually: spindb config set psql /path/to/psql',
98
+ )
99
+ }
100
+ return psqlPath
101
+ }
102
+
103
+ /**
104
+ * Get pg_restore path from config, with helpful error message
105
+ */
106
+ async function getPgRestorePath(): Promise<string> {
107
+ const pgRestorePath = await configManager.getBinaryPath('pg_restore')
108
+ if (!pgRestorePath) {
109
+ throw new Error(
110
+ 'pg_restore not found. Install PostgreSQL client tools:\n' +
111
+ ' macOS: brew install libpq && brew link --force libpq\n' +
112
+ ' Ubuntu/Debian: apt install postgresql-client\n\n' +
113
+ 'Or configure manually: spindb config set pg_restore /path/to/pg_restore',
114
+ )
115
+ }
116
+ return pgRestorePath
117
+ }
118
+
119
+ /**
120
+ * Restore a backup to a PostgreSQL database
121
+ */
122
+ export async function restoreBackup(
123
+ _binPath: string, // Not used - using config manager instead
124
+ backupPath: string,
125
+ options: RestoreOptions,
126
+ ): Promise<RestoreResult> {
127
+ const { port, database, user = 'postgres', format } = options
128
+
129
+ const detectedFormat = format || (await detectBackupFormat(backupPath)).format
130
+
131
+ if (detectedFormat === 'sql') {
132
+ const psqlPath = await getPsqlPath()
133
+
134
+ const result = await execAsync(
135
+ `"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${user} -d ${database} -f "${backupPath}"`,
136
+ { maxBuffer: 50 * 1024 * 1024 }, // 50MB buffer for large dumps
137
+ )
138
+
139
+ return {
140
+ format: 'sql',
141
+ ...result,
142
+ }
143
+ } else {
144
+ const pgRestorePath = await getPgRestorePath()
145
+
146
+ try {
147
+ const formatFlag =
148
+ detectedFormat === 'custom'
149
+ ? '-Fc'
150
+ : detectedFormat === 'tar'
151
+ ? '-Ft'
152
+ : ''
153
+ const result = await execAsync(
154
+ `"${pgRestorePath}" -h 127.0.0.1 -p ${port} -U ${user} -d ${database} --no-owner --no-privileges ${formatFlag} "${backupPath}"`,
155
+ { maxBuffer: 50 * 1024 * 1024 },
156
+ )
157
+
158
+ return {
159
+ format: detectedFormat,
160
+ ...result,
161
+ }
162
+ } catch (err) {
163
+ const e = err as Error & { stdout?: string; stderr?: string }
164
+ // pg_restore often returns non-zero even on partial success
165
+ return {
166
+ format: detectedFormat,
167
+ stdout: e.stdout || '',
168
+ stderr: e.stderr || e.message,
169
+ code: 1,
170
+ }
171
+ }
172
+ }
173
+ }