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,113 @@
1
+ import chalk from 'chalk'
2
+
3
+ /**
4
+ * Color theme for spindb CLI
5
+ */
6
+ export const theme = {
7
+ // Brand colors
8
+ primary: chalk.cyan,
9
+ secondary: chalk.gray,
10
+ accent: chalk.magenta,
11
+
12
+ // Status colors
13
+ success: chalk.green,
14
+ error: chalk.red,
15
+ warning: chalk.yellow,
16
+ info: chalk.blue,
17
+
18
+ // Text styles
19
+ bold: chalk.bold,
20
+ dim: chalk.dim,
21
+ italic: chalk.italic,
22
+
23
+ // Semantic helpers
24
+ containerName: chalk.cyan.bold,
25
+ version: chalk.yellow,
26
+ port: chalk.green,
27
+ path: chalk.gray,
28
+ command: chalk.cyan,
29
+
30
+ // Status badges
31
+ running: chalk.green.bold('● running'),
32
+ stopped: chalk.gray('○ stopped'),
33
+ created: chalk.blue('◐ created'),
34
+
35
+ // Icons
36
+ icons: {
37
+ success: chalk.green('✔'),
38
+ error: chalk.red('✖'),
39
+ warning: chalk.yellow('⚠'),
40
+ info: chalk.blue('ℹ'),
41
+ arrow: chalk.cyan('→'),
42
+ bullet: chalk.gray('•'),
43
+ database: '🗄️',
44
+ postgres: '🐘',
45
+ },
46
+ }
47
+
48
+ /**
49
+ * Format a header box
50
+ */
51
+ export function header(text: string): string {
52
+ const line = '─'.repeat(text.length + 4)
53
+ return `
54
+ ${chalk.cyan('┌' + line + '┐')}
55
+ ${chalk.cyan('│')} ${chalk.bold(text)} ${chalk.cyan('│')}
56
+ ${chalk.cyan('└' + line + '┘')}
57
+ `.trim()
58
+ }
59
+
60
+ /**
61
+ * Format a success message
62
+ */
63
+ export function success(message: string): string {
64
+ return `${theme.icons.success} ${message}`
65
+ }
66
+
67
+ /**
68
+ * Format an error message
69
+ */
70
+ export function error(message: string): string {
71
+ return `${theme.icons.error} ${chalk.red(message)}`
72
+ }
73
+
74
+ /**
75
+ * Format a warning message
76
+ */
77
+ export function warning(message: string): string {
78
+ return `${theme.icons.warning} ${chalk.yellow(message)}`
79
+ }
80
+
81
+ /**
82
+ * Format an info message
83
+ */
84
+ export function info(message: string): string {
85
+ return `${theme.icons.info} ${message}`
86
+ }
87
+
88
+ /**
89
+ * Format a key-value pair
90
+ */
91
+ export function keyValue(key: string, value: string): string {
92
+ return `${chalk.gray(key + ':')} ${value}`
93
+ }
94
+
95
+ /**
96
+ * Format a connection string box
97
+ */
98
+ export function connectionBox(
99
+ name: string,
100
+ connectionString: string,
101
+ port: number,
102
+ ): string {
103
+ return `
104
+ ${chalk.cyan('┌─────────────────────────────────────────┐')}
105
+ ${chalk.cyan('│')} ${theme.icons.success} Container ${chalk.bold(name)} is ready! ${chalk.cyan('│')}
106
+ ${chalk.cyan('│')} ${chalk.cyan('│')}
107
+ ${chalk.cyan('│')} ${chalk.gray('Connection string:')} ${chalk.cyan('│')}
108
+ ${chalk.cyan('│')} ${chalk.white(connectionString)} ${chalk.cyan('│')}
109
+ ${chalk.cyan('│')} ${chalk.cyan('│')}
110
+ ${chalk.cyan('│')} ${chalk.gray('Port:')} ${chalk.green(String(port))} ${chalk.cyan('│')}
111
+ ${chalk.cyan('└─────────────────────────────────────────┘')}
112
+ `.trim()
113
+ }
@@ -0,0 +1,49 @@
1
+ export interface PlatformMappings {
2
+ [key: string]: string
3
+ }
4
+
5
+ export interface PortRange {
6
+ start: number
7
+ end: number
8
+ }
9
+
10
+ export interface Defaults {
11
+ postgresVersion: string
12
+ port: number
13
+ portRange: PortRange
14
+ engine: string
15
+ supportedPostgresVersions: string[]
16
+ superuser: string
17
+ platformMappings: PlatformMappings
18
+ }
19
+
20
+ export const defaults: Defaults = {
21
+ // Default PostgreSQL version
22
+ postgresVersion: '16',
23
+
24
+ // Default port (standard PostgreSQL port)
25
+ port: 5432,
26
+
27
+ // Port range to scan if default is busy
28
+ portRange: {
29
+ start: 5432,
30
+ end: 5500,
31
+ },
32
+
33
+ // Default engine
34
+ engine: 'postgresql',
35
+
36
+ // Supported PostgreSQL versions
37
+ supportedPostgresVersions: ['14', '15', '16', '17'],
38
+
39
+ // Default superuser
40
+ superuser: 'postgres',
41
+
42
+ // Platform mappings for zonky.io binaries
43
+ platformMappings: {
44
+ 'darwin-arm64': 'darwin-arm64v8',
45
+ 'darwin-x64': 'darwin-amd64',
46
+ 'linux-arm64': 'linux-arm64v8',
47
+ 'linux-x64': 'linux-amd64',
48
+ },
49
+ }
@@ -0,0 +1,53 @@
1
+ import { homedir } from 'os'
2
+ import { join } from 'path'
3
+
4
+ const SPINDB_HOME = join(homedir(), '.spindb')
5
+
6
+ export const paths = {
7
+ // Root directory for all spindb data
8
+ root: SPINDB_HOME,
9
+
10
+ // Directory for downloaded database binaries
11
+ bin: join(SPINDB_HOME, 'bin'),
12
+
13
+ // Directory for container data
14
+ containers: join(SPINDB_HOME, 'containers'),
15
+
16
+ // Global config file
17
+ config: join(SPINDB_HOME, 'config.json'),
18
+
19
+ // Get path for a specific binary version
20
+ getBinaryPath(
21
+ engine: string,
22
+ version: string,
23
+ platform: string,
24
+ arch: string,
25
+ ): string {
26
+ return join(this.bin, `${engine}-${version}-${platform}-${arch}`)
27
+ },
28
+
29
+ // Get path for a specific container
30
+ getContainerPath(name: string): string {
31
+ return join(this.containers, name)
32
+ },
33
+
34
+ // Get path for container config
35
+ getContainerConfigPath(name: string): string {
36
+ return join(this.containers, name, 'container.json')
37
+ },
38
+
39
+ // Get path for container data directory
40
+ getContainerDataPath(name: string): string {
41
+ return join(this.containers, name, 'data')
42
+ },
43
+
44
+ // Get path for container log file
45
+ getContainerLogPath(name: string): string {
46
+ return join(this.containers, name, 'postgres.log')
47
+ },
48
+
49
+ // Get path for container PID file
50
+ getContainerPidPath(name: string): string {
51
+ return join(this.containers, name, 'data', 'postmaster.pid')
52
+ },
53
+ }
@@ -0,0 +1,239 @@
1
+ import { createWriteStream, existsSync } from 'fs'
2
+ import { mkdir, readdir, rm, chmod } from 'fs/promises'
3
+ import { join } from 'path'
4
+ import { pipeline } from 'stream/promises'
5
+ import { exec } from 'child_process'
6
+ import { promisify } from 'util'
7
+ import { paths } from '@/config/paths'
8
+ import { defaults } from '@/config/defaults'
9
+ import type { ProgressCallback, InstalledBinary } from '@/types'
10
+
11
+ const execAsync = promisify(exec)
12
+
13
+ export class BinaryManager {
14
+ /**
15
+ * Get the download URL for a PostgreSQL version
16
+ */
17
+ getDownloadUrl(version: string, platform: string, arch: string): string {
18
+ const platformKey = `${platform}-${arch}`
19
+ const zonkyPlatform = defaults.platformMappings[platformKey]
20
+
21
+ if (!zonkyPlatform) {
22
+ throw new Error(`Unsupported platform: ${platformKey}`)
23
+ }
24
+
25
+ // Zonky.io Maven Central URL pattern
26
+ const fullVersion = this.getFullVersion(version)
27
+ return `https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-${zonkyPlatform}/${fullVersion}/embedded-postgres-binaries-${zonkyPlatform}-${fullVersion}.jar`
28
+ }
29
+
30
+ /**
31
+ * Convert major version to full version (e.g., "16" -> "16.4.0")
32
+ */
33
+ getFullVersion(majorVersion: string): string {
34
+ const versionMap: Record<string, string> = {
35
+ '14': '14.15.0',
36
+ '15': '15.10.0',
37
+ '16': '16.6.0',
38
+ '17': '17.2.0',
39
+ }
40
+ return versionMap[majorVersion] || `${majorVersion}.0.0`
41
+ }
42
+
43
+ /**
44
+ * Check if binaries for a specific version are already installed
45
+ */
46
+ async isInstalled(
47
+ version: string,
48
+ platform: string,
49
+ arch: string,
50
+ ): Promise<boolean> {
51
+ const binPath = paths.getBinaryPath('postgresql', version, platform, arch)
52
+ const postgresPath = join(binPath, 'bin', 'postgres')
53
+ return existsSync(postgresPath)
54
+ }
55
+
56
+ /**
57
+ * List all installed PostgreSQL versions
58
+ */
59
+ async listInstalled(): Promise<InstalledBinary[]> {
60
+ const binDir = paths.bin
61
+ if (!existsSync(binDir)) {
62
+ return []
63
+ }
64
+
65
+ const entries = await readdir(binDir, { withFileTypes: true })
66
+ const installed: InstalledBinary[] = []
67
+
68
+ for (const entry of entries) {
69
+ if (entry.isDirectory() && entry.name.startsWith('postgresql-')) {
70
+ const parts = entry.name.split('-')
71
+ if (parts.length >= 4) {
72
+ installed.push({
73
+ engine: parts[0],
74
+ version: parts[1],
75
+ platform: parts[2],
76
+ arch: parts[3],
77
+ })
78
+ }
79
+ }
80
+ }
81
+
82
+ return installed
83
+ }
84
+
85
+ /**
86
+ * Download and extract PostgreSQL binaries
87
+ *
88
+ * The zonky.io JAR files are ZIP archives containing a .txz (tar.xz) file.
89
+ * We need to: 1) unzip the JAR, 2) extract the .txz inside
90
+ */
91
+ async download(
92
+ version: string,
93
+ platform: string,
94
+ arch: string,
95
+ onProgress?: ProgressCallback,
96
+ ): Promise<string> {
97
+ const url = this.getDownloadUrl(version, platform, arch)
98
+ const binPath = paths.getBinaryPath('postgresql', version, platform, arch)
99
+ const tempDir = join(paths.bin, `temp-${version}-${platform}-${arch}`)
100
+ const jarFile = join(tempDir, 'postgres.jar')
101
+
102
+ // Ensure directories exist
103
+ await mkdir(paths.bin, { recursive: true })
104
+ await mkdir(tempDir, { recursive: true })
105
+ await mkdir(binPath, { recursive: true })
106
+
107
+ try {
108
+ // Download the JAR file
109
+ onProgress?.({
110
+ stage: 'downloading',
111
+ message: 'Downloading PostgreSQL binaries...',
112
+ })
113
+
114
+ const response = await fetch(url)
115
+ if (!response.ok) {
116
+ throw new Error(
117
+ `Failed to download binaries: ${response.status} ${response.statusText}`,
118
+ )
119
+ }
120
+
121
+ const fileStream = createWriteStream(jarFile)
122
+ // @ts-expect-error - response.body is ReadableStream
123
+ await pipeline(response.body, fileStream)
124
+
125
+ // Extract the JAR (it's a ZIP file)
126
+ onProgress?.({
127
+ stage: 'extracting',
128
+ message: 'Extracting binaries (step 1/2)...',
129
+ })
130
+
131
+ await execAsync(`unzip -q -o "${jarFile}" -d "${tempDir}"`)
132
+
133
+ // Find and extract the .txz file inside
134
+ onProgress?.({
135
+ stage: 'extracting',
136
+ message: 'Extracting binaries (step 2/2)...',
137
+ })
138
+
139
+ const { stdout: findOutput } = await execAsync(
140
+ `find "${tempDir}" -name "*.txz" -o -name "*.tar.xz" | head -1`,
141
+ )
142
+ const txzFile = findOutput.trim()
143
+
144
+ if (!txzFile) {
145
+ throw new Error('Could not find .txz file in downloaded archive')
146
+ }
147
+
148
+ // Extract the tar.xz file (no strip-components since files are at root level)
149
+ await execAsync(`tar -xJf "${txzFile}" -C "${binPath}"`)
150
+
151
+ // Make binaries executable
152
+ const binDir = join(binPath, 'bin')
153
+ if (existsSync(binDir)) {
154
+ const binaries = await readdir(binDir)
155
+ for (const binary of binaries) {
156
+ await chmod(join(binDir, binary), 0o755)
157
+ }
158
+ }
159
+
160
+ // Verify the installation
161
+ onProgress?.({ stage: 'verifying', message: 'Verifying installation...' })
162
+ await this.verify(version, platform, arch)
163
+
164
+ return binPath
165
+ } finally {
166
+ // Clean up temp directory
167
+ await rm(tempDir, { recursive: true, force: true })
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Verify that PostgreSQL binaries are working
173
+ */
174
+ async verify(
175
+ version: string,
176
+ platform: string,
177
+ arch: string,
178
+ ): Promise<boolean> {
179
+ const binPath = paths.getBinaryPath('postgresql', version, platform, arch)
180
+ const postgresPath = join(binPath, 'bin', 'postgres')
181
+
182
+ if (!existsSync(postgresPath)) {
183
+ throw new Error(`PostgreSQL binary not found at ${postgresPath}`)
184
+ }
185
+
186
+ try {
187
+ const { stdout } = await execAsync(`"${postgresPath}" --version`)
188
+ const match = stdout.match(/postgres \(PostgreSQL\) (\d+)/)
189
+ if (match && match[1] === version) {
190
+ return true
191
+ }
192
+ // Version might be more specific (e.g., 16.4), so also check if it starts with the major version
193
+ if (stdout.includes(`PostgreSQL) ${version}`)) {
194
+ return true
195
+ }
196
+ throw new Error(
197
+ `Version mismatch: expected ${version}, got ${stdout.trim()}`,
198
+ )
199
+ } catch (error) {
200
+ const err = error as Error
201
+ throw new Error(`Failed to verify PostgreSQL binaries: ${err.message}`)
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Get the path to a specific binary (postgres, pg_ctl, psql, etc.)
207
+ */
208
+ getBinaryExecutable(
209
+ version: string,
210
+ platform: string,
211
+ arch: string,
212
+ binary: string,
213
+ ): string {
214
+ const binPath = paths.getBinaryPath('postgresql', version, platform, arch)
215
+ return join(binPath, 'bin', binary)
216
+ }
217
+
218
+ /**
219
+ * Ensure binaries are available, downloading if necessary
220
+ */
221
+ async ensureInstalled(
222
+ version: string,
223
+ platform: string,
224
+ arch: string,
225
+ onProgress?: ProgressCallback,
226
+ ): Promise<string> {
227
+ if (await this.isInstalled(version, platform, arch)) {
228
+ onProgress?.({
229
+ stage: 'cached',
230
+ message: 'Using cached PostgreSQL binaries',
231
+ })
232
+ return paths.getBinaryPath('postgresql', version, platform, arch)
233
+ }
234
+
235
+ return this.download(version, platform, arch, onProgress)
236
+ }
237
+ }
238
+
239
+ export const binaryManager = new BinaryManager()
@@ -0,0 +1,259 @@
1
+ import { existsSync } from 'fs'
2
+ import { readFile, writeFile, mkdir } from 'fs/promises'
3
+ import { exec } from 'child_process'
4
+ import { promisify } from 'util'
5
+ import { dirname } from 'path'
6
+ import { paths } from '@/config/paths'
7
+ import type {
8
+ SpinDBConfig,
9
+ BinaryConfig,
10
+ BinaryTool,
11
+ BinarySource,
12
+ } from '@/types'
13
+
14
+ const execAsync = promisify(exec)
15
+
16
+ const DEFAULT_CONFIG: SpinDBConfig = {
17
+ binaries: {},
18
+ }
19
+
20
+ export class ConfigManager {
21
+ private config: SpinDBConfig | null = null
22
+
23
+ /**
24
+ * Load config from disk, creating default if it doesn't exist
25
+ */
26
+ async load(): Promise<SpinDBConfig> {
27
+ if (this.config) {
28
+ return this.config
29
+ }
30
+
31
+ const configPath = paths.config
32
+
33
+ if (!existsSync(configPath)) {
34
+ // Create default config
35
+ this.config = { ...DEFAULT_CONFIG }
36
+ await this.save()
37
+ return this.config
38
+ }
39
+
40
+ try {
41
+ const content = await readFile(configPath, 'utf8')
42
+ this.config = JSON.parse(content) as SpinDBConfig
43
+ return this.config
44
+ } catch {
45
+ // If config is corrupted, reset to default
46
+ this.config = { ...DEFAULT_CONFIG }
47
+ await this.save()
48
+ return this.config
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Save config to disk
54
+ */
55
+ async save(): Promise<void> {
56
+ const configPath = paths.config
57
+
58
+ // Ensure directory exists
59
+ await mkdir(dirname(configPath), { recursive: true })
60
+
61
+ if (this.config) {
62
+ this.config.updatedAt = new Date().toISOString()
63
+ await writeFile(configPath, JSON.stringify(this.config, null, 2))
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get the path for a binary tool, detecting from system if not configured
69
+ */
70
+ async getBinaryPath(tool: BinaryTool): Promise<string | null> {
71
+ const config = await this.load()
72
+
73
+ // Check if we have a configured path
74
+ const binaryConfig = config.binaries[tool]
75
+ if (binaryConfig?.path) {
76
+ // Verify it still exists
77
+ if (existsSync(binaryConfig.path)) {
78
+ return binaryConfig.path
79
+ }
80
+ // Path no longer valid, clear it
81
+ delete config.binaries[tool]
82
+ await this.save()
83
+ }
84
+
85
+ // Try to detect from system
86
+ const systemPath = await this.detectSystemBinary(tool)
87
+ if (systemPath) {
88
+ await this.setBinaryPath(tool, systemPath, 'system')
89
+ return systemPath
90
+ }
91
+
92
+ return null
93
+ }
94
+
95
+ /**
96
+ * Set the path for a binary tool
97
+ */
98
+ async setBinaryPath(
99
+ tool: BinaryTool,
100
+ path: string,
101
+ source: BinarySource,
102
+ ): Promise<void> {
103
+ const config = await this.load()
104
+
105
+ // Get version if possible
106
+ let version: string | undefined
107
+ try {
108
+ const { stdout } = await execAsync(`"${path}" --version`)
109
+ const match = stdout.match(/\d+\.\d+/)
110
+ if (match) {
111
+ version = match[0]
112
+ }
113
+ } catch {
114
+ // Version detection failed, that's ok
115
+ }
116
+
117
+ config.binaries[tool] = {
118
+ tool,
119
+ path,
120
+ source,
121
+ version,
122
+ }
123
+
124
+ await this.save()
125
+ }
126
+
127
+ /**
128
+ * Get configuration for a specific binary
129
+ */
130
+ async getBinaryConfig(tool: BinaryTool): Promise<BinaryConfig | null> {
131
+ const config = await this.load()
132
+ return config.binaries[tool] || null
133
+ }
134
+
135
+ /**
136
+ * Detect a binary on the system PATH
137
+ */
138
+ async detectSystemBinary(tool: BinaryTool): Promise<string | null> {
139
+ try {
140
+ const { stdout } = await execAsync(`which ${tool}`)
141
+ const path = stdout.trim()
142
+ if (path && existsSync(path)) {
143
+ return path
144
+ }
145
+ } catch {
146
+ // which failed, binary not found
147
+ }
148
+
149
+ // Check common locations
150
+ const commonPaths = this.getCommonBinaryPaths(tool)
151
+ for (const path of commonPaths) {
152
+ if (existsSync(path)) {
153
+ return path
154
+ }
155
+ }
156
+
157
+ return null
158
+ }
159
+
160
+ /**
161
+ * Get common installation paths for PostgreSQL client tools
162
+ */
163
+ private getCommonBinaryPaths(tool: BinaryTool): string[] {
164
+ const paths: string[] = []
165
+
166
+ // Homebrew (macOS)
167
+ paths.push(`/opt/homebrew/bin/${tool}`)
168
+ paths.push(`/opt/homebrew/opt/libpq/bin/${tool}`)
169
+ paths.push(`/usr/local/bin/${tool}`)
170
+ paths.push(`/usr/local/opt/libpq/bin/${tool}`)
171
+
172
+ // Postgres.app (macOS)
173
+ paths.push(
174
+ `/Applications/Postgres.app/Contents/Versions/latest/bin/${tool}`,
175
+ )
176
+
177
+ // Linux common paths
178
+ paths.push(`/usr/bin/${tool}`)
179
+ paths.push(`/usr/lib/postgresql/16/bin/${tool}`)
180
+ paths.push(`/usr/lib/postgresql/15/bin/${tool}`)
181
+ paths.push(`/usr/lib/postgresql/14/bin/${tool}`)
182
+
183
+ return paths
184
+ }
185
+
186
+ /**
187
+ * Detect all available client tools on the system
188
+ */
189
+ async detectAllTools(): Promise<Map<BinaryTool, string>> {
190
+ const tools: BinaryTool[] = [
191
+ 'psql',
192
+ 'pg_dump',
193
+ 'pg_restore',
194
+ 'pg_basebackup',
195
+ ]
196
+ const found = new Map<BinaryTool, string>()
197
+
198
+ for (const tool of tools) {
199
+ const path = await this.detectSystemBinary(tool)
200
+ if (path) {
201
+ found.set(tool, path)
202
+ }
203
+ }
204
+
205
+ return found
206
+ }
207
+
208
+ /**
209
+ * Initialize config by detecting all available tools
210
+ */
211
+ async initialize(): Promise<{ found: BinaryTool[]; missing: BinaryTool[] }> {
212
+ const tools: BinaryTool[] = [
213
+ 'psql',
214
+ 'pg_dump',
215
+ 'pg_restore',
216
+ 'pg_basebackup',
217
+ ]
218
+ const found: BinaryTool[] = []
219
+ const missing: BinaryTool[] = []
220
+
221
+ for (const tool of tools) {
222
+ const path = await this.getBinaryPath(tool)
223
+ if (path) {
224
+ found.push(tool)
225
+ } else {
226
+ missing.push(tool)
227
+ }
228
+ }
229
+
230
+ return { found, missing }
231
+ }
232
+
233
+ /**
234
+ * Get the full config
235
+ */
236
+ async getConfig(): Promise<SpinDBConfig> {
237
+ return this.load()
238
+ }
239
+
240
+ /**
241
+ * Clear a binary configuration
242
+ */
243
+ async clearBinaryPath(tool: BinaryTool): Promise<void> {
244
+ const config = await this.load()
245
+ delete config.binaries[tool]
246
+ await this.save()
247
+ }
248
+
249
+ /**
250
+ * Clear all binary configurations (useful for re-detection)
251
+ */
252
+ async clearAllBinaries(): Promise<void> {
253
+ const config = await this.load()
254
+ config.binaries = {}
255
+ await this.save()
256
+ }
257
+ }
258
+
259
+ export const configManager = new ConfigManager()