spindb 0.1.0 → 0.2.1
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 +24 -3
- package/package.json +10 -2
- package/src/cli/commands/create.ts +23 -3
- package/src/cli/commands/menu.ts +955 -142
- package/src/cli/commands/postgres-tools.ts +216 -0
- package/src/cli/commands/restore.ts +28 -0
- package/src/cli/index.ts +2 -0
- package/src/cli/ui/prompts.ts +111 -21
- package/src/cli/ui/theme.ts +54 -10
- package/src/config/defaults.ts +6 -3
- package/src/core/binary-manager.ts +42 -12
- package/src/core/container-manager.ts +53 -5
- package/src/core/port-manager.ts +76 -1
- package/src/core/postgres-binary-manager.ts +499 -0
- package/src/core/process-manager.ts +4 -4
- package/src/engines/base-engine.ts +22 -0
- package/src/engines/postgresql/binary-urls.ts +130 -12
- package/src/engines/postgresql/index.ts +40 -4
- package/src/engines/postgresql/restore.ts +20 -9
- package/src/types/index.ts +15 -13
- package/tsconfig.json +6 -3
- package/.claude/settings.local.json +0 -20
- package/.env.example +0 -1
- package/.prettierignore +0 -4
- package/.prettierrc +0 -6
- package/CLAUDE.md +0 -162
- package/TODO.md +0 -66
- package/eslint.config.js +0 -18
- package/seeds/mysql/sample-db.sql +0 -22
- package/seeds/postgres/sample-db.sql +0 -27
|
@@ -88,6 +88,14 @@ export abstract class BaseEngine {
|
|
|
88
88
|
database: string,
|
|
89
89
|
): Promise<void>
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Drop a database within the container
|
|
93
|
+
*/
|
|
94
|
+
abstract dropDatabase(
|
|
95
|
+
container: ContainerConfig,
|
|
96
|
+
database: string,
|
|
97
|
+
): Promise<void>
|
|
98
|
+
|
|
91
99
|
/**
|
|
92
100
|
* Check if binaries are installed
|
|
93
101
|
*/
|
|
@@ -100,4 +108,18 @@ export abstract class BaseEngine {
|
|
|
100
108
|
version: string,
|
|
101
109
|
onProgress?: ProgressCallback,
|
|
102
110
|
): Promise<string>
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Fetch all available versions from remote source (grouped by major version)
|
|
114
|
+
* Returns a map of major version -> array of full versions (sorted latest first)
|
|
115
|
+
* Falls back to hardcoded versions if network fails
|
|
116
|
+
*/
|
|
117
|
+
async fetchAvailableVersions(): Promise<Record<string, string[]>> {
|
|
118
|
+
// Default implementation returns supported versions as single-item arrays
|
|
119
|
+
const versions: Record<string, string[]> = {}
|
|
120
|
+
for (const v of this.supportedVersions) {
|
|
121
|
+
versions[v] = [v]
|
|
122
|
+
}
|
|
123
|
+
return versions
|
|
124
|
+
}
|
|
103
125
|
}
|
|
@@ -1,15 +1,126 @@
|
|
|
1
|
+
import { platform, arch } from 'os'
|
|
1
2
|
import { defaults } from '@/config/defaults'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Fallback map of major versions to stable patch versions
|
|
6
|
+
* Used when Maven repository is unreachable
|
|
5
7
|
*/
|
|
6
|
-
export const
|
|
7
|
-
'14': '14.
|
|
8
|
-
'15': '15.
|
|
9
|
-
'16': '16.
|
|
10
|
-
'17': '17.
|
|
8
|
+
export const FALLBACK_VERSION_MAP: Record<string, string> = {
|
|
9
|
+
'14': '14.20.0',
|
|
10
|
+
'15': '15.15.0',
|
|
11
|
+
'16': '16.11.0',
|
|
12
|
+
'17': '17.7.0',
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Supported major versions (in order of display)
|
|
17
|
+
*/
|
|
18
|
+
export const SUPPORTED_MAJOR_VERSIONS = ['14', '15', '16', '17']
|
|
19
|
+
|
|
20
|
+
// Cache for fetched versions
|
|
21
|
+
let cachedVersions: Record<string, string[]> | null = null
|
|
22
|
+
let cacheTimestamp = 0
|
|
23
|
+
const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fetch available versions from Maven repository
|
|
27
|
+
*/
|
|
28
|
+
export async function fetchAvailableVersions(): Promise<
|
|
29
|
+
Record<string, string[]>
|
|
30
|
+
> {
|
|
31
|
+
// Return cached versions if still valid
|
|
32
|
+
if (cachedVersions && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
|
|
33
|
+
return cachedVersions
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const zonkyPlatform = getZonkyPlatform(platform(), arch())
|
|
37
|
+
if (!zonkyPlatform) {
|
|
38
|
+
throw new Error(`Unsupported platform: ${platform()}-${arch()}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const url = `https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-${zonkyPlatform}/`
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5000) })
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`HTTP ${response.status}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const html = await response.text()
|
|
50
|
+
|
|
51
|
+
// Parse version directories from the HTML listing
|
|
52
|
+
// Format: <a href="14.15.0/">14.15.0/</a>
|
|
53
|
+
const versionRegex = /href="(\d+\.\d+\.\d+)\/"/g
|
|
54
|
+
const versions: string[] = []
|
|
55
|
+
let match
|
|
56
|
+
|
|
57
|
+
while ((match = versionRegex.exec(html)) !== null) {
|
|
58
|
+
versions.push(match[1])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Group versions by major version
|
|
62
|
+
const grouped: Record<string, string[]> = {}
|
|
63
|
+
for (const major of SUPPORTED_MAJOR_VERSIONS) {
|
|
64
|
+
grouped[major] = versions
|
|
65
|
+
.filter((v) => v.startsWith(`${major}.`))
|
|
66
|
+
.sort((a, b) => compareVersions(b, a)) // Sort descending (latest first)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cache the results
|
|
70
|
+
cachedVersions = grouped
|
|
71
|
+
cacheTimestamp = Date.now()
|
|
72
|
+
|
|
73
|
+
return grouped
|
|
74
|
+
} catch {
|
|
75
|
+
// Return fallback on any error
|
|
76
|
+
return getFallbackVersions()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get fallback versions when network is unavailable
|
|
82
|
+
*/
|
|
83
|
+
function getFallbackVersions(): Record<string, string[]> {
|
|
84
|
+
const grouped: Record<string, string[]> = {}
|
|
85
|
+
for (const major of SUPPORTED_MAJOR_VERSIONS) {
|
|
86
|
+
grouped[major] = [FALLBACK_VERSION_MAP[major]]
|
|
87
|
+
}
|
|
88
|
+
return grouped
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Compare two version strings (e.g., "16.11.0" vs "16.9.0")
|
|
93
|
+
* Returns positive if a > b, negative if a < b, 0 if equal
|
|
94
|
+
*/
|
|
95
|
+
function compareVersions(a: string, b: string): number {
|
|
96
|
+
const partsA = a.split('.').map(Number)
|
|
97
|
+
const partsB = b.split('.').map(Number)
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
100
|
+
const numA = partsA[i] || 0
|
|
101
|
+
const numB = partsB[i] || 0
|
|
102
|
+
if (numA !== numB) {
|
|
103
|
+
return numA - numB
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the latest version for a major version
|
|
111
|
+
*/
|
|
112
|
+
export async function getLatestVersion(major: string): Promise<string> {
|
|
113
|
+
const versions = await fetchAvailableVersions()
|
|
114
|
+
const majorVersions = versions[major]
|
|
115
|
+
if (majorVersions && majorVersions.length > 0) {
|
|
116
|
+
return majorVersions[0] // First is latest due to descending sort
|
|
117
|
+
}
|
|
118
|
+
return FALLBACK_VERSION_MAP[major] || `${major}.0.0`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Legacy export for backward compatibility
|
|
122
|
+
export const VERSION_MAP = FALLBACK_VERSION_MAP
|
|
123
|
+
|
|
13
124
|
/**
|
|
14
125
|
* Get the zonky.io platform identifier
|
|
15
126
|
*/
|
|
@@ -34,16 +145,23 @@ export function getBinaryUrl(
|
|
|
34
145
|
throw new Error(`Unsupported platform: ${platform}-${arch}`)
|
|
35
146
|
}
|
|
36
147
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
throw new Error(
|
|
40
|
-
`Unsupported PostgreSQL version: ${version}. Supported: ${Object.keys(VERSION_MAP).join(', ')}`,
|
|
41
|
-
)
|
|
42
|
-
}
|
|
148
|
+
// Use VERSION_MAP for major versions, otherwise treat as full version
|
|
149
|
+
const fullVersion = VERSION_MAP[version] || normalizeVersion(version)
|
|
43
150
|
|
|
44
151
|
return `https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-${zonkyPlatform}/${fullVersion}/embedded-postgres-binaries-${zonkyPlatform}-${fullVersion}.jar`
|
|
45
152
|
}
|
|
46
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Normalize version string to X.Y.Z format
|
|
156
|
+
*/
|
|
157
|
+
function normalizeVersion(version: string): string {
|
|
158
|
+
const parts = version.split('.')
|
|
159
|
+
if (parts.length === 2) {
|
|
160
|
+
return `${version}.0`
|
|
161
|
+
}
|
|
162
|
+
return version
|
|
163
|
+
}
|
|
164
|
+
|
|
47
165
|
/**
|
|
48
166
|
* Get the full version string for a major version
|
|
49
167
|
*/
|
|
@@ -8,7 +8,11 @@ import { processManager } from '@/core/process-manager'
|
|
|
8
8
|
import { configManager } from '@/core/config-manager'
|
|
9
9
|
import { paths } from '@/config/paths'
|
|
10
10
|
import { defaults } from '@/config/defaults'
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getBinaryUrl,
|
|
13
|
+
SUPPORTED_MAJOR_VERSIONS,
|
|
14
|
+
fetchAvailableVersions,
|
|
15
|
+
} from './binary-urls'
|
|
12
16
|
import { detectBackupFormat, restoreBackup } from './restore'
|
|
13
17
|
import type {
|
|
14
18
|
ContainerConfig,
|
|
@@ -24,7 +28,15 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
24
28
|
name = 'postgresql'
|
|
25
29
|
displayName = 'PostgreSQL'
|
|
26
30
|
defaultPort = 5432
|
|
27
|
-
supportedVersions =
|
|
31
|
+
supportedVersions = SUPPORTED_MAJOR_VERSIONS
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fetch all available versions from Maven (grouped by major version)
|
|
35
|
+
* Falls back to hardcoded versions if network fails
|
|
36
|
+
*/
|
|
37
|
+
async fetchAvailableVersions(): Promise<Record<string, string[]>> {
|
|
38
|
+
return fetchAvailableVersions()
|
|
39
|
+
}
|
|
28
40
|
|
|
29
41
|
/**
|
|
30
42
|
* Get current platform info
|
|
@@ -178,6 +190,7 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
178
190
|
port,
|
|
179
191
|
database,
|
|
180
192
|
user: defaults.superuser,
|
|
193
|
+
pgRestorePath: options.pgRestorePath as string, // Use custom path if provided
|
|
181
194
|
...(options as { format?: string }),
|
|
182
195
|
})
|
|
183
196
|
}
|
|
@@ -187,8 +200,8 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
187
200
|
*/
|
|
188
201
|
getConnectionString(container: ContainerConfig, database?: string): string {
|
|
189
202
|
const { port } = container
|
|
190
|
-
const db = database || 'postgres'
|
|
191
|
-
return `postgresql://${defaults.superuser}@
|
|
203
|
+
const db = database || container.database || 'postgres'
|
|
204
|
+
return `postgresql://${defaults.superuser}@127.0.0.1:${port}/${db}`
|
|
192
205
|
}
|
|
193
206
|
|
|
194
207
|
/**
|
|
@@ -293,6 +306,29 @@ export class PostgreSQLEngine extends BaseEngine {
|
|
|
293
306
|
}
|
|
294
307
|
}
|
|
295
308
|
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Drop a database
|
|
312
|
+
*/
|
|
313
|
+
async dropDatabase(
|
|
314
|
+
container: ContainerConfig,
|
|
315
|
+
database: string,
|
|
316
|
+
): Promise<void> {
|
|
317
|
+
const { port } = container
|
|
318
|
+
const psqlPath = await this.getPsqlPath()
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
await execAsync(
|
|
322
|
+
`"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -c 'DROP DATABASE IF EXISTS "${database}"'`,
|
|
323
|
+
)
|
|
324
|
+
} catch (error) {
|
|
325
|
+
const err = error as Error
|
|
326
|
+
// Ignore "database does not exist" error
|
|
327
|
+
if (!err.message.includes('does not exist')) {
|
|
328
|
+
throw error
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
296
332
|
}
|
|
297
333
|
|
|
298
334
|
export const postgresqlEngine = new PostgreSQLEngine()
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'fs/promises'
|
|
|
2
2
|
import { exec } from 'child_process'
|
|
3
3
|
import { promisify } from 'util'
|
|
4
4
|
import { configManager } from '@/core/config-manager'
|
|
5
|
+
import { findBinaryPathFresh } from '@/core/postgres-binary-manager'
|
|
5
6
|
import type { BackupFormat, RestoreResult } from '@/types'
|
|
6
7
|
|
|
7
8
|
const execAsync = promisify(exec)
|
|
@@ -77,11 +78,12 @@ export async function detectBackupFormat(
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
export
|
|
81
|
+
export type RestoreOptions = {
|
|
81
82
|
port: number
|
|
82
83
|
database: string
|
|
83
84
|
user?: string
|
|
84
85
|
format?: string
|
|
86
|
+
pgRestorePath?: string
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/**
|
|
@@ -101,19 +103,27 @@ async function getPsqlPath(): Promise<string> {
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/**
|
|
104
|
-
* Get pg_restore path from config, with helpful error message
|
|
106
|
+
* Get pg_restore path from config or system PATH, with helpful error message
|
|
105
107
|
*/
|
|
106
108
|
async function getPgRestorePath(): Promise<string> {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
// First try to get from config (in case user has set a custom path)
|
|
110
|
+
const configPath = await configManager.getBinaryPath('pg_restore')
|
|
111
|
+
if (configPath) {
|
|
112
|
+
return configPath
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fall back to finding it on the system PATH with cache refresh
|
|
116
|
+
const systemPath = await findBinaryPathFresh('pg_restore')
|
|
117
|
+
if (!systemPath) {
|
|
109
118
|
throw new Error(
|
|
110
119
|
'pg_restore not found. Install PostgreSQL client tools:\n' +
|
|
111
120
|
' macOS: brew install libpq && brew link --force libpq\n' +
|
|
112
|
-
' Ubuntu/Debian: apt install postgresql-client\n
|
|
121
|
+
' Ubuntu/Debian: apt install postgresql-client\n' +
|
|
122
|
+
' CentOS/RHEL/Fedora: yum install postgresql\n\n' +
|
|
113
123
|
'Or configure manually: spindb config set pg_restore /path/to/pg_restore',
|
|
114
124
|
)
|
|
115
125
|
}
|
|
116
|
-
return
|
|
126
|
+
return systemPath
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
/**
|
|
@@ -124,7 +134,7 @@ export async function restoreBackup(
|
|
|
124
134
|
backupPath: string,
|
|
125
135
|
options: RestoreOptions,
|
|
126
136
|
): Promise<RestoreResult> {
|
|
127
|
-
const { port, database, user = 'postgres', format } = options
|
|
137
|
+
const { port, database, user = 'postgres', format, pgRestorePath } = options
|
|
128
138
|
|
|
129
139
|
const detectedFormat = format || (await detectBackupFormat(backupPath)).format
|
|
130
140
|
|
|
@@ -141,7 +151,8 @@ export async function restoreBackup(
|
|
|
141
151
|
...result,
|
|
142
152
|
}
|
|
143
153
|
} else {
|
|
144
|
-
|
|
154
|
+
// Use custom path if provided, otherwise find it dynamically
|
|
155
|
+
const restorePath = pgRestorePath || (await getPgRestorePath())
|
|
145
156
|
|
|
146
157
|
try {
|
|
147
158
|
const formatFlag =
|
|
@@ -151,7 +162,7 @@ export async function restoreBackup(
|
|
|
151
162
|
? '-Ft'
|
|
152
163
|
: ''
|
|
153
164
|
const result = await execAsync(
|
|
154
|
-
`"${
|
|
165
|
+
`"${restorePath}" -h 127.0.0.1 -p ${port} -U ${user} -d ${database} --no-owner --no-privileges ${formatFlag} "${backupPath}"`,
|
|
155
166
|
{ maxBuffer: 50 * 1024 * 1024 },
|
|
156
167
|
)
|
|
157
168
|
|
package/src/types/index.ts
CHANGED
|
@@ -1,54 +1,56 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type ContainerConfig = {
|
|
2
2
|
name: string
|
|
3
3
|
engine: string
|
|
4
4
|
version: string
|
|
5
5
|
port: number
|
|
6
|
+
database: string
|
|
6
7
|
created: string
|
|
7
8
|
status: 'created' | 'running' | 'stopped'
|
|
8
9
|
clonedFrom?: string
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
export type ProgressCallback = (progress: {
|
|
13
|
+
stage: string
|
|
14
|
+
message: string
|
|
15
|
+
}) => void
|
|
14
16
|
|
|
15
|
-
export
|
|
17
|
+
export type InstalledBinary = {
|
|
16
18
|
engine: string
|
|
17
19
|
version: string
|
|
18
20
|
platform: string
|
|
19
21
|
arch: string
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
export
|
|
24
|
+
export type PortResult = {
|
|
23
25
|
port: number
|
|
24
26
|
isDefault: boolean
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export
|
|
29
|
+
export type ProcessResult = {
|
|
28
30
|
stdout: string
|
|
29
31
|
stderr: string
|
|
30
32
|
code?: number
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
export
|
|
35
|
+
export type StatusResult = {
|
|
34
36
|
running: boolean
|
|
35
37
|
message: string
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
export
|
|
40
|
+
export type BackupFormat = {
|
|
39
41
|
format: string
|
|
40
42
|
description: string
|
|
41
43
|
restoreCommand: string
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
export
|
|
46
|
+
export type RestoreResult = {
|
|
45
47
|
format: string
|
|
46
48
|
stdout?: string
|
|
47
49
|
stderr?: string
|
|
48
50
|
code?: number
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
export
|
|
53
|
+
export type EngineInfo = {
|
|
52
54
|
name: string
|
|
53
55
|
displayName: string
|
|
54
56
|
defaultPort: number
|
|
@@ -68,7 +70,7 @@ export type BinarySource = 'bundled' | 'system' | 'custom'
|
|
|
68
70
|
/**
|
|
69
71
|
* Configuration for a single binary tool
|
|
70
72
|
*/
|
|
71
|
-
export
|
|
73
|
+
export type BinaryConfig = {
|
|
72
74
|
tool: BinaryTool
|
|
73
75
|
path: string
|
|
74
76
|
source: BinarySource
|
|
@@ -78,7 +80,7 @@ export interface BinaryConfig {
|
|
|
78
80
|
/**
|
|
79
81
|
* Global spindb configuration stored in ~/.spindb/config.json
|
|
80
82
|
*/
|
|
81
|
-
export
|
|
83
|
+
export type SpinDBConfig = {
|
|
82
84
|
// Binary paths for client tools
|
|
83
85
|
binaries: {
|
|
84
86
|
psql?: BinaryConfig
|
package/tsconfig.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
-
"module": "
|
|
5
|
-
"moduleResolution": "
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
6
|
"lib": ["ES2022"],
|
|
7
7
|
"outDir": "./dist",
|
|
8
8
|
"rootDir": "./src",
|
|
9
9
|
"strict": true,
|
|
10
10
|
"esModuleInterop": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
11
12
|
"skipLibCheck": true,
|
|
12
13
|
"forceConsistentCasingInFileNames": true,
|
|
13
14
|
"resolveJsonModule": true,
|
|
@@ -17,7 +18,9 @@
|
|
|
17
18
|
"baseUrl": ".",
|
|
18
19
|
"paths": {
|
|
19
20
|
"@/*": ["./src/*"]
|
|
20
|
-
}
|
|
21
|
+
},
|
|
22
|
+
"allowImportingTsExtensions": true,
|
|
23
|
+
"noEmit": true
|
|
21
24
|
},
|
|
22
25
|
"include": ["src/**/*"],
|
|
23
26
|
"exclude": ["node_modules", "dist"]
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(lsof:*)",
|
|
5
|
-
"Bash(npm view:*)",
|
|
6
|
-
"Bash(pnpm install:*)",
|
|
7
|
-
"Bash(pnpm run start --help:*)",
|
|
8
|
-
"Bash(pnpm run start:*)",
|
|
9
|
-
"Bash(curl:*)",
|
|
10
|
-
"Bash(psql:*)",
|
|
11
|
-
"Bash(chmod:*)",
|
|
12
|
-
"Bash(node bin/cli.js:*)",
|
|
13
|
-
"Bash(pnpm add:*)",
|
|
14
|
-
"Bash(pnpm run format:*)",
|
|
15
|
-
"Bash(pnpm run lint:*)"
|
|
16
|
-
],
|
|
17
|
-
"deny": [],
|
|
18
|
-
"ask": []
|
|
19
|
-
}
|
|
20
|
-
}
|
package/.env.example
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# No environment variables needed for this project
|
package/.prettierignore
DELETED
package/.prettierrc
DELETED
package/CLAUDE.md
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md - Project Context for Claude Code
|
|
2
|
-
|
|
3
|
-
## Project Overview
|
|
4
|
-
|
|
5
|
-
SpinDB is a CLI tool for running local PostgreSQL databases without Docker. It's a lightweight alternative to DBngin, downloading and managing PostgreSQL binaries directly.
|
|
6
|
-
|
|
7
|
-
## Tech Stack
|
|
8
|
-
|
|
9
|
-
- **Runtime**: Node.js 18+ with TypeScript
|
|
10
|
-
- **Execution**: `tsx` for direct TypeScript execution (no build step for dev)
|
|
11
|
-
- **Package Manager**: pnpm (strictly - not npm/yarn)
|
|
12
|
-
- **CLI Framework**: Commander.js
|
|
13
|
-
- **Interactive UI**: Inquirer.js (prompts), Chalk (colors), Ora (spinners)
|
|
14
|
-
- **Module System**: ESM (`"type": "module"`)
|
|
15
|
-
- **Path Aliases**: `@/*` maps to `./src/*`
|
|
16
|
-
|
|
17
|
-
## Project Structure
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
src/
|
|
21
|
-
├── bin/cli.ts # Entry point (#!/usr/bin/env tsx)
|
|
22
|
-
├── cli/
|
|
23
|
-
│ ├── index.ts # Commander setup, routes to commands
|
|
24
|
-
│ ├── commands/ # CLI commands (create, start, stop, etc.)
|
|
25
|
-
│ │ ├── menu.ts # Interactive arrow-key menu (default when no args)
|
|
26
|
-
│ │ └── config.ts # Binary path configuration
|
|
27
|
-
│ └── ui/
|
|
28
|
-
│ ├── prompts.ts # Inquirer prompts
|
|
29
|
-
│ ├── spinner.ts # Ora spinner helpers
|
|
30
|
-
│ └── theme.ts # Chalk color theme
|
|
31
|
-
├── core/
|
|
32
|
-
│ ├── binary-manager.ts # Downloads PostgreSQL from zonky.io
|
|
33
|
-
│ ├── config-manager.ts # Manages ~/.spindb/config.json
|
|
34
|
-
│ ├── container-manager.ts # CRUD for containers
|
|
35
|
-
│ ├── port-manager.ts # Port availability checking
|
|
36
|
-
│ └── process-manager.ts # pg_ctl start/stop wrapper
|
|
37
|
-
├── config/
|
|
38
|
-
│ ├── paths.ts # ~/.spindb/ path definitions
|
|
39
|
-
│ └── defaults.ts # Default values, platform mappings
|
|
40
|
-
├── engines/
|
|
41
|
-
│ ├── base-engine.ts # Abstract base class
|
|
42
|
-
│ ├── index.ts # Engine registry
|
|
43
|
-
│ └── postgresql/
|
|
44
|
-
│ ├── index.ts # PostgreSQL engine implementation
|
|
45
|
-
│ ├── binary-urls.ts # Zonky.io URL builder
|
|
46
|
-
│ └── restore.ts # Backup detection and restore
|
|
47
|
-
└── types/index.ts # TypeScript interfaces
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Key Architecture Decisions
|
|
51
|
-
|
|
52
|
-
### Binary Source
|
|
53
|
-
PostgreSQL server binaries come from [zonky.io](https://github.com/zonkyio/embedded-postgres-binaries) (Maven Central). These only include server binaries (postgres, pg_ctl, initdb), NOT client tools (psql, pg_dump, pg_restore).
|
|
54
|
-
|
|
55
|
-
**Download flow:**
|
|
56
|
-
1. Download JAR from Maven Central
|
|
57
|
-
2. Unzip JAR (it's a ZIP file)
|
|
58
|
-
3. Extract `.txz` file inside
|
|
59
|
-
4. Extract tar.xz to `~/.spindb/bin/postgresql-{version}-{platform}-{arch}/`
|
|
60
|
-
|
|
61
|
-
### Client Tools
|
|
62
|
-
Client tools (psql, pg_restore) are detected from the system. The `config-manager.ts` handles:
|
|
63
|
-
- Auto-detection from PATH and common locations
|
|
64
|
-
- Caching paths in `~/.spindb/config.json`
|
|
65
|
-
- Manual override via `spindb config set`
|
|
66
|
-
|
|
67
|
-
### Data Storage
|
|
68
|
-
```
|
|
69
|
-
~/.spindb/
|
|
70
|
-
├── bin/ # Downloaded PostgreSQL binaries
|
|
71
|
-
├── containers/{name}/
|
|
72
|
-
│ ├── container.json # Container metadata
|
|
73
|
-
│ ├── data/ # PostgreSQL data directory
|
|
74
|
-
│ └── postgres.log # Server logs
|
|
75
|
-
└── config.json # Tool paths, settings
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### Interactive Menu
|
|
79
|
-
When `spindb` is run with no arguments, it shows an interactive menu (`src/cli/commands/menu.ts`) using Inquirer's list prompt. Users navigate with arrow keys.
|
|
80
|
-
|
|
81
|
-
## Common Tasks
|
|
82
|
-
|
|
83
|
-
### Running the CLI
|
|
84
|
-
```bash
|
|
85
|
-
pnpm run start # Opens interactive menu
|
|
86
|
-
pnpm run start create mydb # Run specific command
|
|
87
|
-
pnpm run start --help # Show help
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Testing Changes
|
|
91
|
-
No test suite yet. Manual testing:
|
|
92
|
-
```bash
|
|
93
|
-
pnpm run start create testdb -p 5433
|
|
94
|
-
pnpm run start list
|
|
95
|
-
pnpm run start connect testdb
|
|
96
|
-
pnpm run start delete testdb --force --yes
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### Adding a New Command
|
|
100
|
-
1. Create `src/cli/commands/{name}.ts`
|
|
101
|
-
2. Export a Commander `Command` instance
|
|
102
|
-
3. Import and add to `src/cli/index.ts`
|
|
103
|
-
4. Optionally add to interactive menu in `src/cli/commands/menu.ts`
|
|
104
|
-
|
|
105
|
-
## Important Implementation Details
|
|
106
|
-
|
|
107
|
-
### Platform Detection
|
|
108
|
-
```typescript
|
|
109
|
-
import { platform, arch } from 'os';
|
|
110
|
-
// platform() returns 'darwin' | 'linux'
|
|
111
|
-
// arch() returns 'arm64' | 'x64'
|
|
112
|
-
// Mapped to zonky.io names in defaults.ts platformMappings
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### Version Mapping
|
|
116
|
-
Major versions (14, 15, 16, 17) map to full versions in `src/engines/postgresql/binary-urls.ts`:
|
|
117
|
-
```typescript
|
|
118
|
-
const VERSION_MAP = {
|
|
119
|
-
'14': '14.15.0',
|
|
120
|
-
'15': '15.10.0',
|
|
121
|
-
'16': '16.6.0',
|
|
122
|
-
'17': '17.2.0'
|
|
123
|
-
};
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Port Management
|
|
127
|
-
- Default port: 5432
|
|
128
|
-
- If busy, scans 5432-5500 for available port
|
|
129
|
-
- Uses `net.createServer()` to test availability
|
|
130
|
-
|
|
131
|
-
### Process Management
|
|
132
|
-
Uses `pg_ctl` for start/stop:
|
|
133
|
-
```bash
|
|
134
|
-
pg_ctl start -D {dataDir} -l {logFile} -w -o "-p {port}"
|
|
135
|
-
pg_ctl stop -D {dataDir} -m fast -w
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
PID file location: `~/.spindb/containers/{name}/data/postmaster.pid`
|
|
139
|
-
|
|
140
|
-
## Known Limitations
|
|
141
|
-
|
|
142
|
-
1. **No client tools bundled** - psql/pg_restore must be installed separately
|
|
143
|
-
2. **macOS/Linux only** - No Windows support (zonky.io doesn't provide Windows binaries)
|
|
144
|
-
3. **No backup command** - pg_dump must be run manually with system tools
|
|
145
|
-
4. **No automatic updates** - Binary versions are hardcoded in VERSION_MAP
|
|
146
|
-
|
|
147
|
-
## Future Improvements (Not Implemented)
|
|
148
|
-
|
|
149
|
-
- [ ] Add `spindb backup` command (wrapper around pg_dump)
|
|
150
|
-
- [ ] Support MySQL/SQLite engines (architecture supports it)
|
|
151
|
-
- [ ] Add `spindb logs` command to tail postgres.log
|
|
152
|
-
- [ ] Add `spindb exec` for running SQL files
|
|
153
|
-
- [ ] Automatic binary version updates
|
|
154
|
-
- [ ] Windows support (would need different binary source)
|
|
155
|
-
|
|
156
|
-
## Code Style Notes
|
|
157
|
-
|
|
158
|
-
- No `.js` extensions in imports
|
|
159
|
-
- Use `@/` path alias for all internal imports
|
|
160
|
-
- Prefer `async/await` over callbacks
|
|
161
|
-
- Use Ora spinners for long-running operations
|
|
162
|
-
- Error messages should include actionable fix suggestions
|