spindb 0.5.2 → 0.5.4

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 (38) hide show
  1. package/README.md +188 -9
  2. package/cli/commands/connect.ts +334 -105
  3. package/cli/commands/create.ts +106 -67
  4. package/cli/commands/deps.ts +19 -4
  5. package/cli/commands/edit.ts +245 -0
  6. package/cli/commands/engines.ts +434 -0
  7. package/cli/commands/info.ts +279 -0
  8. package/cli/commands/list.ts +1 -1
  9. package/cli/commands/menu.ts +664 -167
  10. package/cli/commands/restore.ts +11 -25
  11. package/cli/commands/start.ts +25 -20
  12. package/cli/commands/url.ts +79 -0
  13. package/cli/index.ts +9 -3
  14. package/cli/ui/prompts.ts +20 -12
  15. package/cli/ui/theme.ts +1 -1
  16. package/config/engine-defaults.ts +24 -1
  17. package/config/os-dependencies.ts +151 -113
  18. package/config/paths.ts +7 -36
  19. package/core/binary-manager.ts +12 -6
  20. package/core/config-manager.ts +17 -5
  21. package/core/dependency-manager.ts +144 -15
  22. package/core/error-handler.ts +336 -0
  23. package/core/platform-service.ts +634 -0
  24. package/core/port-manager.ts +11 -3
  25. package/core/process-manager.ts +12 -2
  26. package/core/start-with-retry.ts +167 -0
  27. package/core/transaction-manager.ts +170 -0
  28. package/engines/mysql/binary-detection.ts +177 -100
  29. package/engines/mysql/index.ts +240 -131
  30. package/engines/mysql/restore.ts +257 -0
  31. package/engines/mysql/version-validator.ts +373 -0
  32. package/{core/postgres-binary-manager.ts → engines/postgresql/binary-manager.ts} +63 -23
  33. package/engines/postgresql/binary-urls.ts +5 -3
  34. package/engines/postgresql/index.ts +35 -4
  35. package/engines/postgresql/restore.ts +54 -5
  36. package/engines/postgresql/version-validator.ts +262 -0
  37. package/package.json +6 -2
  38. package/cli/commands/postgres-tools.ts +0 -216
@@ -0,0 +1,634 @@
1
+ /**
2
+ * Platform Service
3
+ *
4
+ * Centralizes all OS-specific detection and behavior, similar to how
5
+ * the engine abstraction handles database-specific behavior.
6
+ *
7
+ * This enables:
8
+ * - Consistent platform detection across the codebase
9
+ * - Easy mocking for unit tests
10
+ * - Simple addition of new platforms (e.g., Windows)
11
+ */
12
+
13
+ import { homedir, platform as osPlatform, arch as osArch } from 'os'
14
+ import { execSync, exec, spawn } from 'child_process'
15
+ import { promisify } from 'util'
16
+ import { existsSync } from 'fs'
17
+
18
+ const execAsync = promisify(exec)
19
+
20
+ // =============================================================================
21
+ // Types
22
+ // =============================================================================
23
+
24
+ export type Platform = 'darwin' | 'linux' | 'win32'
25
+ export type Architecture = 'arm64' | 'x64'
26
+
27
+ export type PlatformInfo = {
28
+ platform: Platform
29
+ arch: Architecture
30
+ homeDir: string
31
+ isWSL: boolean
32
+ isSudo: boolean
33
+ sudoUser: string | null
34
+ }
35
+
36
+ export type ClipboardConfig = {
37
+ copyCommand: string
38
+ copyArgs: string[]
39
+ pasteCommand: string
40
+ pasteArgs: string[]
41
+ available: boolean
42
+ }
43
+
44
+ export type WhichCommandConfig = {
45
+ command: string
46
+ args: string[]
47
+ }
48
+
49
+ export type PackageManagerInfo = {
50
+ id: string
51
+ name: string
52
+ checkCommand: string
53
+ installTemplate: string
54
+ updateCommand: string
55
+ }
56
+
57
+ // =============================================================================
58
+ // Abstract Base Class
59
+ // =============================================================================
60
+
61
+ export abstract class BasePlatformService {
62
+ protected cachedPlatformInfo: PlatformInfo | null = null
63
+
64
+ /**
65
+ * Get platform information
66
+ */
67
+ abstract getPlatformInfo(): PlatformInfo
68
+
69
+ /**
70
+ * Get clipboard configuration for this platform
71
+ */
72
+ abstract getClipboardConfig(): ClipboardConfig
73
+
74
+ /**
75
+ * Get the "which" command equivalent for this platform
76
+ */
77
+ abstract getWhichCommand(): WhichCommandConfig
78
+
79
+ /**
80
+ * Get common search paths for a tool on this platform
81
+ */
82
+ abstract getSearchPaths(tool: string): string[]
83
+
84
+ /**
85
+ * Detect available package manager
86
+ */
87
+ abstract detectPackageManager(): Promise<PackageManagerInfo | null>
88
+
89
+ /**
90
+ * Get the zonky.io platform identifier for PostgreSQL binaries
91
+ */
92
+ abstract getZonkyPlatform(): string | null
93
+
94
+ /**
95
+ * Copy text to clipboard
96
+ */
97
+ async copyToClipboard(text: string): Promise<boolean> {
98
+ const config = this.getClipboardConfig()
99
+ if (!config.available) return false
100
+
101
+ try {
102
+ await new Promise<void>((resolve, reject) => {
103
+ const proc = spawn(config.copyCommand, config.copyArgs, {
104
+ stdio: ['pipe', 'inherit', 'inherit'],
105
+ })
106
+ proc.stdin?.write(text)
107
+ proc.stdin?.end()
108
+ proc.on('close', (code) => {
109
+ if (code === 0) resolve()
110
+ else reject(new Error(`Clipboard command exited with code ${code}`))
111
+ })
112
+ proc.on('error', reject)
113
+ })
114
+ return true
115
+ } catch {
116
+ return false
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Check if a tool is installed and return its path
122
+ */
123
+ async findToolPath(toolName: string): Promise<string | null> {
124
+ const whichConfig = this.getWhichCommand()
125
+
126
+ // First try the which/where command
127
+ try {
128
+ const { stdout } = await execAsync(`${whichConfig.command} ${toolName}`)
129
+ const path = stdout.trim().split('\n')[0]
130
+ if (path && existsSync(path)) {
131
+ return path
132
+ }
133
+ } catch {
134
+ // Not found via which, continue to search paths
135
+ }
136
+
137
+ // Search common installation paths
138
+ const searchPaths = this.getSearchPaths(toolName)
139
+ for (const dir of searchPaths) {
140
+ const fullPath = this.buildToolPath(dir, toolName)
141
+ if (existsSync(fullPath)) {
142
+ return fullPath
143
+ }
144
+ }
145
+
146
+ return null
147
+ }
148
+
149
+ /**
150
+ * Build the full path to a tool in a directory
151
+ */
152
+ protected abstract buildToolPath(dir: string, toolName: string): string
153
+
154
+ /**
155
+ * Get tool version by running --version
156
+ */
157
+ async getToolVersion(toolPath: string): Promise<string | null> {
158
+ try {
159
+ const { stdout } = await execAsync(`"${toolPath}" --version`)
160
+ const match = stdout.match(/(\d+\.\d+(\.\d+)?)/)
161
+ return match ? match[1] : null
162
+ } catch {
163
+ return null
164
+ }
165
+ }
166
+ }
167
+
168
+ // =============================================================================
169
+ // Darwin (macOS) Implementation
170
+ // =============================================================================
171
+
172
+ class DarwinPlatformService extends BasePlatformService {
173
+ getPlatformInfo(): PlatformInfo {
174
+ if (this.cachedPlatformInfo) return this.cachedPlatformInfo
175
+
176
+ const sudoUser = process.env.SUDO_USER || null
177
+ let homeDir: string
178
+
179
+ if (sudoUser) {
180
+ // Running under sudo - get original user's home
181
+ try {
182
+ const result = execSync(`getent passwd ${sudoUser}`, {
183
+ encoding: 'utf-8',
184
+ })
185
+ const parts = result.trim().split(':')
186
+ homeDir =
187
+ parts.length >= 6 && parts[5] ? parts[5] : `/Users/${sudoUser}`
188
+ } catch {
189
+ homeDir = `/Users/${sudoUser}`
190
+ }
191
+ } else {
192
+ homeDir = homedir()
193
+ }
194
+
195
+ this.cachedPlatformInfo = {
196
+ platform: 'darwin',
197
+ arch: osArch() as Architecture,
198
+ homeDir,
199
+ isWSL: false,
200
+ isSudo: !!sudoUser,
201
+ sudoUser,
202
+ }
203
+
204
+ return this.cachedPlatformInfo
205
+ }
206
+
207
+ getClipboardConfig(): ClipboardConfig {
208
+ return {
209
+ copyCommand: 'pbcopy',
210
+ copyArgs: [],
211
+ pasteCommand: 'pbpaste',
212
+ pasteArgs: [],
213
+ available: true, // pbcopy is always available on macOS
214
+ }
215
+ }
216
+
217
+ getWhichCommand(): WhichCommandConfig {
218
+ return {
219
+ command: 'which',
220
+ args: [],
221
+ }
222
+ }
223
+
224
+ getSearchPaths(tool: string): string[] {
225
+ const paths: string[] = []
226
+
227
+ // MySQL-specific paths
228
+ if (
229
+ tool === 'mysqld' ||
230
+ tool === 'mysql' ||
231
+ tool === 'mysqladmin' ||
232
+ tool === 'mysqldump'
233
+ ) {
234
+ paths.push(
235
+ // Homebrew (Apple Silicon)
236
+ '/opt/homebrew/bin',
237
+ '/opt/homebrew/opt/mysql/bin',
238
+ '/opt/homebrew/opt/mysql@8.0/bin',
239
+ '/opt/homebrew/opt/mysql@8.4/bin',
240
+ '/opt/homebrew/opt/mysql@5.7/bin',
241
+ // Homebrew (Intel)
242
+ '/usr/local/bin',
243
+ '/usr/local/opt/mysql/bin',
244
+ '/usr/local/opt/mysql@8.0/bin',
245
+ '/usr/local/opt/mysql@8.4/bin',
246
+ '/usr/local/opt/mysql@5.7/bin',
247
+ // Official MySQL installer
248
+ '/usr/local/mysql/bin',
249
+ )
250
+ }
251
+
252
+ // PostgreSQL-specific paths
253
+ if (
254
+ tool === 'psql' ||
255
+ tool === 'pg_dump' ||
256
+ tool === 'pg_restore' ||
257
+ tool === 'pg_basebackup'
258
+ ) {
259
+ paths.push(
260
+ // Homebrew (Apple Silicon)
261
+ '/opt/homebrew/bin',
262
+ '/opt/homebrew/opt/postgresql/bin',
263
+ '/opt/homebrew/opt/postgresql@17/bin',
264
+ '/opt/homebrew/opt/postgresql@16/bin',
265
+ '/opt/homebrew/opt/postgresql@15/bin',
266
+ '/opt/homebrew/opt/postgresql@14/bin',
267
+ // Homebrew (Intel)
268
+ '/usr/local/bin',
269
+ '/usr/local/opt/postgresql/bin',
270
+ '/usr/local/opt/postgresql@17/bin',
271
+ '/usr/local/opt/postgresql@16/bin',
272
+ '/usr/local/opt/postgresql@15/bin',
273
+ '/usr/local/opt/postgresql@14/bin',
274
+ // Postgres.app
275
+ '/Applications/Postgres.app/Contents/Versions/latest/bin',
276
+ )
277
+ }
278
+
279
+ // Generic paths
280
+ paths.push('/usr/local/bin', '/usr/bin')
281
+
282
+ return paths
283
+ }
284
+
285
+ async detectPackageManager(): Promise<PackageManagerInfo | null> {
286
+ try {
287
+ await execAsync('brew --version')
288
+ return {
289
+ id: 'brew',
290
+ name: 'Homebrew',
291
+ checkCommand: 'brew --version',
292
+ installTemplate: 'brew install {package}',
293
+ updateCommand: 'brew update',
294
+ }
295
+ } catch {
296
+ return null
297
+ }
298
+ }
299
+
300
+ getZonkyPlatform(): string | null {
301
+ const arch = osArch()
302
+ if (arch === 'arm64') return 'darwin-arm64v8'
303
+ if (arch === 'x64') return 'darwin-amd64'
304
+ return null
305
+ }
306
+
307
+ protected buildToolPath(dir: string, toolName: string): string {
308
+ return `${dir}/${toolName}`
309
+ }
310
+ }
311
+
312
+ // =============================================================================
313
+ // Linux Implementation
314
+ // =============================================================================
315
+
316
+ class LinuxPlatformService extends BasePlatformService {
317
+ getPlatformInfo(): PlatformInfo {
318
+ if (this.cachedPlatformInfo) return this.cachedPlatformInfo
319
+
320
+ const sudoUser = process.env.SUDO_USER || null
321
+ let homeDir: string
322
+
323
+ if (sudoUser) {
324
+ try {
325
+ const result = execSync(`getent passwd ${sudoUser}`, {
326
+ encoding: 'utf-8',
327
+ })
328
+ const parts = result.trim().split(':')
329
+ homeDir = parts.length >= 6 && parts[5] ? parts[5] : `/home/${sudoUser}`
330
+ } catch {
331
+ homeDir = `/home/${sudoUser}`
332
+ }
333
+ } else {
334
+ homeDir = homedir()
335
+ }
336
+
337
+ // Check if running in WSL
338
+ let isWSL = false
339
+ try {
340
+ const uname = execSync('uname -r', { encoding: 'utf-8' })
341
+ isWSL = uname.toLowerCase().includes('microsoft')
342
+ } catch {
343
+ // Not WSL
344
+ }
345
+
346
+ this.cachedPlatformInfo = {
347
+ platform: 'linux',
348
+ arch: osArch() as Architecture,
349
+ homeDir,
350
+ isWSL,
351
+ isSudo: !!sudoUser,
352
+ sudoUser,
353
+ }
354
+
355
+ return this.cachedPlatformInfo
356
+ }
357
+
358
+ getClipboardConfig(): ClipboardConfig {
359
+ // Check if xclip is available
360
+ let available = false
361
+ try {
362
+ execSync('which xclip', { encoding: 'utf-8' })
363
+ available = true
364
+ } catch {
365
+ // xclip not installed
366
+ }
367
+
368
+ return {
369
+ copyCommand: 'xclip',
370
+ copyArgs: ['-selection', 'clipboard'],
371
+ pasteCommand: 'xclip',
372
+ pasteArgs: ['-selection', 'clipboard', '-o'],
373
+ available,
374
+ }
375
+ }
376
+
377
+ getWhichCommand(): WhichCommandConfig {
378
+ return {
379
+ command: 'which',
380
+ args: [],
381
+ }
382
+ }
383
+
384
+ getSearchPaths(tool: string): string[] {
385
+ const paths: string[] = []
386
+
387
+ // MySQL-specific paths
388
+ if (
389
+ tool === 'mysqld' ||
390
+ tool === 'mysql' ||
391
+ tool === 'mysqladmin' ||
392
+ tool === 'mysqldump'
393
+ ) {
394
+ paths.push(
395
+ '/usr/bin',
396
+ '/usr/sbin',
397
+ '/usr/local/bin',
398
+ '/usr/local/mysql/bin',
399
+ )
400
+ }
401
+
402
+ // PostgreSQL-specific paths
403
+ if (
404
+ tool === 'psql' ||
405
+ tool === 'pg_dump' ||
406
+ tool === 'pg_restore' ||
407
+ tool === 'pg_basebackup'
408
+ ) {
409
+ paths.push(
410
+ '/usr/bin',
411
+ '/usr/local/bin',
412
+ '/usr/lib/postgresql/17/bin',
413
+ '/usr/lib/postgresql/16/bin',
414
+ '/usr/lib/postgresql/15/bin',
415
+ '/usr/lib/postgresql/14/bin',
416
+ )
417
+ }
418
+
419
+ // Generic paths
420
+ paths.push('/usr/bin', '/usr/local/bin')
421
+
422
+ return paths
423
+ }
424
+
425
+ async detectPackageManager(): Promise<PackageManagerInfo | null> {
426
+ // Try apt first (Debian/Ubuntu)
427
+ try {
428
+ await execAsync('apt --version')
429
+ return {
430
+ id: 'apt',
431
+ name: 'APT',
432
+ checkCommand: 'apt --version',
433
+ installTemplate: 'sudo apt install -y {package}',
434
+ updateCommand: 'sudo apt update',
435
+ }
436
+ } catch {
437
+ // Not apt
438
+ }
439
+
440
+ // Try dnf (Fedora/RHEL 8+)
441
+ try {
442
+ await execAsync('dnf --version')
443
+ return {
444
+ id: 'dnf',
445
+ name: 'DNF',
446
+ checkCommand: 'dnf --version',
447
+ installTemplate: 'sudo dnf install -y {package}',
448
+ updateCommand: 'sudo dnf check-update',
449
+ }
450
+ } catch {
451
+ // Not dnf
452
+ }
453
+
454
+ // Try yum (RHEL/CentOS 7)
455
+ try {
456
+ await execAsync('yum --version')
457
+ return {
458
+ id: 'yum',
459
+ name: 'YUM',
460
+ checkCommand: 'yum --version',
461
+ installTemplate: 'sudo yum install -y {package}',
462
+ updateCommand: 'sudo yum check-update',
463
+ }
464
+ } catch {
465
+ // Not yum
466
+ }
467
+
468
+ // Try pacman (Arch)
469
+ try {
470
+ await execAsync('pacman --version')
471
+ return {
472
+ id: 'pacman',
473
+ name: 'Pacman',
474
+ checkCommand: 'pacman --version',
475
+ installTemplate: 'sudo pacman -S --noconfirm {package}',
476
+ updateCommand: 'sudo pacman -Sy',
477
+ }
478
+ } catch {
479
+ // Not pacman
480
+ }
481
+
482
+ return null
483
+ }
484
+
485
+ getZonkyPlatform(): string | null {
486
+ const arch = osArch()
487
+ if (arch === 'arm64') return 'linux-arm64v8'
488
+ if (arch === 'x64') return 'linux-amd64'
489
+ return null
490
+ }
491
+
492
+ protected buildToolPath(dir: string, toolName: string): string {
493
+ return `${dir}/${toolName}`
494
+ }
495
+ }
496
+
497
+ // =============================================================================
498
+ // Windows Implementation (Stub for future support)
499
+ // =============================================================================
500
+
501
+ class Win32PlatformService extends BasePlatformService {
502
+ getPlatformInfo(): PlatformInfo {
503
+ if (this.cachedPlatformInfo) return this.cachedPlatformInfo
504
+
505
+ this.cachedPlatformInfo = {
506
+ platform: 'win32',
507
+ arch: osArch() as Architecture,
508
+ homeDir: homedir(),
509
+ isWSL: false,
510
+ isSudo: false,
511
+ sudoUser: null,
512
+ }
513
+
514
+ return this.cachedPlatformInfo
515
+ }
516
+
517
+ getClipboardConfig(): ClipboardConfig {
518
+ return {
519
+ copyCommand: 'clip',
520
+ copyArgs: [],
521
+ pasteCommand: 'powershell',
522
+ pasteArgs: ['-command', 'Get-Clipboard'],
523
+ available: true,
524
+ }
525
+ }
526
+
527
+ getWhichCommand(): WhichCommandConfig {
528
+ return {
529
+ command: 'where',
530
+ args: [],
531
+ }
532
+ }
533
+
534
+ getSearchPaths(tool: string): string[] {
535
+ const paths: string[] = []
536
+
537
+ // MySQL-specific paths
538
+ if (
539
+ tool === 'mysqld' ||
540
+ tool === 'mysql' ||
541
+ tool === 'mysqladmin' ||
542
+ tool === 'mysqldump'
543
+ ) {
544
+ paths.push(
545
+ 'C:\\Program Files\\MySQL\\MySQL Server 8.0\\bin',
546
+ 'C:\\Program Files\\MySQL\\MySQL Server 8.4\\bin',
547
+ 'C:\\Program Files\\MySQL\\MySQL Server 5.7\\bin',
548
+ )
549
+ }
550
+
551
+ // PostgreSQL-specific paths
552
+ if (
553
+ tool === 'psql' ||
554
+ tool === 'pg_dump' ||
555
+ tool === 'pg_restore' ||
556
+ tool === 'pg_basebackup'
557
+ ) {
558
+ paths.push(
559
+ 'C:\\Program Files\\PostgreSQL\\17\\bin',
560
+ 'C:\\Program Files\\PostgreSQL\\16\\bin',
561
+ 'C:\\Program Files\\PostgreSQL\\15\\bin',
562
+ 'C:\\Program Files\\PostgreSQL\\14\\bin',
563
+ )
564
+ }
565
+
566
+ return paths
567
+ }
568
+
569
+ async detectPackageManager(): Promise<PackageManagerInfo | null> {
570
+ // Try chocolatey
571
+ try {
572
+ await execAsync('choco --version')
573
+ return {
574
+ id: 'choco',
575
+ name: 'Chocolatey',
576
+ checkCommand: 'choco --version',
577
+ installTemplate: 'choco install -y {package}',
578
+ updateCommand: 'choco upgrade all',
579
+ }
580
+ } catch {
581
+ // Not chocolatey
582
+ }
583
+
584
+ // Try winget
585
+ try {
586
+ await execAsync('winget --version')
587
+ return {
588
+ id: 'winget',
589
+ name: 'Windows Package Manager',
590
+ checkCommand: 'winget --version',
591
+ installTemplate: 'winget install {package}',
592
+ updateCommand: 'winget upgrade --all',
593
+ }
594
+ } catch {
595
+ // Not winget
596
+ }
597
+
598
+ return null
599
+ }
600
+
601
+ getZonkyPlatform(): string | null {
602
+ // zonky.io doesn't provide Windows binaries
603
+ return null
604
+ }
605
+
606
+ protected buildToolPath(dir: string, toolName: string): string {
607
+ return `${dir}\\${toolName}.exe`
608
+ }
609
+ }
610
+
611
+ // =============================================================================
612
+ // Factory and Singleton
613
+ // =============================================================================
614
+
615
+ /**
616
+ * Create the appropriate platform service for the current OS
617
+ */
618
+ export function createPlatformService(): BasePlatformService {
619
+ const platform = osPlatform()
620
+
621
+ switch (platform) {
622
+ case 'darwin':
623
+ return new DarwinPlatformService()
624
+ case 'linux':
625
+ return new LinuxPlatformService()
626
+ case 'win32':
627
+ return new Win32PlatformService()
628
+ default:
629
+ throw new Error(`Unsupported platform: ${platform}`)
630
+ }
631
+ }
632
+
633
+ // Export singleton instance for convenience
634
+ export const platformService = createPlatformService()
@@ -5,6 +5,7 @@ import { existsSync } from 'fs'
5
5
  import { readdir, readFile } from 'fs/promises'
6
6
  import { defaults, getSupportedEngines } from '../config/defaults'
7
7
  import { paths } from '../config/paths'
8
+ import { logDebug } from './error-handler'
8
9
  import type { ContainerConfig, PortResult } from '../types'
9
10
 
10
11
  const execAsync = promisify(exec)
@@ -83,7 +84,11 @@ export class PortManager {
83
84
  try {
84
85
  const { stdout } = await execAsync(`lsof -i :${port} -P -n | head -5`)
85
86
  return stdout.trim()
86
- } catch {
87
+ } catch (error) {
88
+ logDebug('Could not determine port user', {
89
+ port,
90
+ error: error instanceof Error ? error.message : String(error),
91
+ })
87
92
  return null
88
93
  }
89
94
  }
@@ -116,8 +121,11 @@ export class PortManager {
116
121
  const content = await readFile(configPath, 'utf8')
117
122
  const config = JSON.parse(content) as ContainerConfig
118
123
  ports.push(config.port)
119
- } catch {
120
- // Skip invalid configs
124
+ } catch (error) {
125
+ logDebug('Skipping invalid container config', {
126
+ configPath,
127
+ error: error instanceof Error ? error.message : String(error),
128
+ })
121
129
  }
122
130
  }
123
131
  }
@@ -3,6 +3,7 @@ import { promisify } from 'util'
3
3
  import { existsSync } from 'fs'
4
4
  import { readFile } from 'fs/promises'
5
5
  import { paths } from '../config/paths'
6
+ import { logDebug } from './error-handler'
6
7
  import type { ProcessResult, StatusResult } from '../types'
7
8
 
8
9
  const execAsync = promisify(exec)
@@ -221,7 +222,12 @@ export class ProcessManager {
221
222
  // Check if process is still running
222
223
  process.kill(pid, 0)
223
224
  return true
224
- } catch {
225
+ } catch (error) {
226
+ logDebug('PID file check failed', {
227
+ containerName,
228
+ engine: options.engine,
229
+ error: error instanceof Error ? error.message : String(error),
230
+ })
225
231
  return false
226
232
  }
227
233
  }
@@ -242,7 +248,11 @@ export class ProcessManager {
242
248
  try {
243
249
  const content = await readFile(pidFile, 'utf8')
244
250
  return parseInt(content.split('\n')[0], 10)
245
- } catch {
251
+ } catch (error) {
252
+ logDebug('Failed to read PID file', {
253
+ pidFile,
254
+ error: error instanceof Error ? error.message : String(error),
255
+ })
246
256
  return null
247
257
  }
248
258
  }