spindb 0.1.0 → 0.2.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.
- package/.claude/settings.local.json +6 -1
- package/CLAUDE.md +42 -12
- package/README.md +24 -3
- package/TODO.md +12 -3
- package/eslint.config.js +7 -1
- package/package.json +1 -1
- 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
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { header, success, warning, error } from '@/cli/ui/theme'
|
|
4
|
+
import {
|
|
5
|
+
detectPackageManager,
|
|
6
|
+
getBinaryInfo,
|
|
7
|
+
installPostgresBinaries,
|
|
8
|
+
updatePostgresBinaries,
|
|
9
|
+
ensurePostgresBinary,
|
|
10
|
+
getPostgresVersion,
|
|
11
|
+
} from '@/core/postgres-binary-manager'
|
|
12
|
+
|
|
13
|
+
export const postgresToolsCommand = new Command('postgres-tools').description(
|
|
14
|
+
'Manage PostgreSQL client tools (psql, pg_restore, etc.)',
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
postgresToolsCommand
|
|
18
|
+
.command('check')
|
|
19
|
+
.description('Check PostgreSQL client tools status')
|
|
20
|
+
.option('--dump <path>', 'Check compatibility with a specific dump file')
|
|
21
|
+
.action(async (options: { dump?: string }) => {
|
|
22
|
+
console.log(header('PostgreSQL Tools Status'))
|
|
23
|
+
console.log()
|
|
24
|
+
|
|
25
|
+
// Check package manager
|
|
26
|
+
const packageManager = await detectPackageManager()
|
|
27
|
+
if (packageManager) {
|
|
28
|
+
console.log(success(`Package Manager: ${packageManager.name}`))
|
|
29
|
+
} else {
|
|
30
|
+
console.log(warning('Package Manager: Not found'))
|
|
31
|
+
}
|
|
32
|
+
console.log()
|
|
33
|
+
|
|
34
|
+
// Check binaries
|
|
35
|
+
const binaries = ['pg_restore', 'psql'] as const
|
|
36
|
+
|
|
37
|
+
for (const binary of binaries) {
|
|
38
|
+
const info = await getBinaryInfo(binary, options.dump)
|
|
39
|
+
|
|
40
|
+
if (!info) {
|
|
41
|
+
console.log(error(`${binary}: Not found`))
|
|
42
|
+
} else {
|
|
43
|
+
console.log(`${chalk.cyan(binary)}:`)
|
|
44
|
+
console.log(` Version: ${info.version}`)
|
|
45
|
+
console.log(` Path: ${info.path}`)
|
|
46
|
+
console.log(` Package Manager: ${info.packageManager || 'Unknown'}`)
|
|
47
|
+
|
|
48
|
+
if (options.dump) {
|
|
49
|
+
console.log(
|
|
50
|
+
` Compatible: ${info.isCompatible ? chalk.green('Yes') : chalk.red('No')}`,
|
|
51
|
+
)
|
|
52
|
+
if (info.requiredVersion) {
|
|
53
|
+
console.log(` Required Version: ${info.requiredVersion}+`)
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
console.log(` Status: ${chalk.green('Available')}`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
console.log()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (options.dump) {
|
|
63
|
+
const binaryCheck = await ensurePostgresBinary(
|
|
64
|
+
'pg_restore',
|
|
65
|
+
options.dump,
|
|
66
|
+
{
|
|
67
|
+
autoInstall: false,
|
|
68
|
+
autoUpdate: false,
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if (!binaryCheck.success) {
|
|
73
|
+
console.log(warning('Compatibility Issues Detected:'))
|
|
74
|
+
if (binaryCheck.action === 'install_required') {
|
|
75
|
+
console.log(error(' pg_restore is not installed'))
|
|
76
|
+
} else if (binaryCheck.action === 'update_required') {
|
|
77
|
+
console.log(
|
|
78
|
+
error(' pg_restore version is incompatible with the dump file'),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
console.log()
|
|
82
|
+
console.log(chalk.gray('Run: spindb postgres-tools install --auto-fix'))
|
|
83
|
+
console.log(chalk.gray('Or: spindb postgres-tools update --auto-fix'))
|
|
84
|
+
} else {
|
|
85
|
+
console.log(success('All tools are compatible with the dump file'))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
postgresToolsCommand
|
|
91
|
+
.command('install')
|
|
92
|
+
.description('Install PostgreSQL client tools')
|
|
93
|
+
.option('--auto-fix', 'Install and automatically fix compatibility issues')
|
|
94
|
+
.action(async (options: { autoFix?: boolean }) => {
|
|
95
|
+
console.log(header('Installing PostgreSQL Client Tools'))
|
|
96
|
+
console.log()
|
|
97
|
+
|
|
98
|
+
const installSuccess = await installPostgresBinaries()
|
|
99
|
+
|
|
100
|
+
if (installSuccess) {
|
|
101
|
+
console.log()
|
|
102
|
+
console.log(success('Installation completed successfully'))
|
|
103
|
+
|
|
104
|
+
if (options.autoFix) {
|
|
105
|
+
console.log()
|
|
106
|
+
console.log(chalk.gray('Verifying installation...'))
|
|
107
|
+
|
|
108
|
+
const pgRestoreCheck = await ensurePostgresBinary('pg_restore')
|
|
109
|
+
const psqlCheck = await ensurePostgresBinary('psql')
|
|
110
|
+
|
|
111
|
+
if (pgRestoreCheck.success && psqlCheck.success) {
|
|
112
|
+
console.log(success('All tools are working correctly'))
|
|
113
|
+
} else {
|
|
114
|
+
console.log(warning('Some tools may need additional configuration'))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
postgresToolsCommand
|
|
121
|
+
.command('update')
|
|
122
|
+
.description('Update PostgreSQL client tools')
|
|
123
|
+
.option('--auto-fix', 'Update and automatically fix compatibility issues')
|
|
124
|
+
.action(async (options: { autoFix?: boolean }) => {
|
|
125
|
+
console.log(header('Updating PostgreSQL Client Tools'))
|
|
126
|
+
console.log()
|
|
127
|
+
|
|
128
|
+
const updateSuccess = await updatePostgresBinaries()
|
|
129
|
+
|
|
130
|
+
if (updateSuccess) {
|
|
131
|
+
console.log()
|
|
132
|
+
console.log(success('Update completed successfully'))
|
|
133
|
+
|
|
134
|
+
if (options.autoFix) {
|
|
135
|
+
console.log()
|
|
136
|
+
console.log(chalk.gray('Verifying update...'))
|
|
137
|
+
|
|
138
|
+
const pgRestoreVersion = await getPostgresVersion('pg_restore')
|
|
139
|
+
const psqlVersion = await getPostgresVersion('psql')
|
|
140
|
+
|
|
141
|
+
if (pgRestoreVersion && psqlVersion) {
|
|
142
|
+
console.log(success(`pg_restore: ${pgRestoreVersion}`))
|
|
143
|
+
console.log(success(`psql: ${psqlVersion}`))
|
|
144
|
+
} else {
|
|
145
|
+
console.log(warning('Could not verify versions'))
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
postgresToolsCommand
|
|
152
|
+
.command('fix')
|
|
153
|
+
.description('Fix compatibility issues with a dump file')
|
|
154
|
+
.argument('<dump-path>', 'Path to the dump file')
|
|
155
|
+
.action(async (dumpPath: string) => {
|
|
156
|
+
console.log(header('Fixing Compatibility Issues'))
|
|
157
|
+
console.log()
|
|
158
|
+
console.log(chalk.gray(`Dump file: ${dumpPath}`))
|
|
159
|
+
console.log()
|
|
160
|
+
|
|
161
|
+
const binaryCheck = await ensurePostgresBinary('pg_restore', dumpPath, {
|
|
162
|
+
autoInstall: true,
|
|
163
|
+
autoUpdate: true,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
if (!binaryCheck.success) {
|
|
167
|
+
console.log(error('Failed to fix compatibility issues automatically'))
|
|
168
|
+
console.log()
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
binaryCheck.action === 'install_required' ||
|
|
172
|
+
binaryCheck.action === 'install_failed'
|
|
173
|
+
) {
|
|
174
|
+
console.log(warning('Manual installation required:'))
|
|
175
|
+
console.log(
|
|
176
|
+
chalk.gray(' macOS: brew install libpq && brew link --force libpq'),
|
|
177
|
+
)
|
|
178
|
+
console.log(
|
|
179
|
+
chalk.gray(' Ubuntu/Debian: sudo apt install postgresql-client'),
|
|
180
|
+
)
|
|
181
|
+
console.log(
|
|
182
|
+
chalk.gray(' CentOS/RHEL/Fedora: sudo yum install postgresql'),
|
|
183
|
+
)
|
|
184
|
+
} else if (
|
|
185
|
+
binaryCheck.action === 'update_required' ||
|
|
186
|
+
binaryCheck.action === 'update_failed'
|
|
187
|
+
) {
|
|
188
|
+
console.log(warning('Manual update required:'))
|
|
189
|
+
console.log(
|
|
190
|
+
chalk.gray(' macOS: brew upgrade libpq && brew link --force libpq'),
|
|
191
|
+
)
|
|
192
|
+
console.log(
|
|
193
|
+
chalk.gray(
|
|
194
|
+
' Ubuntu/Debian: sudo apt update && sudo apt upgrade postgresql-client',
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
console.log(
|
|
198
|
+
chalk.gray(' CentOS/RHEL/Fedora: sudo yum update postgresql'),
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
process.exit(1)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(success('Compatibility issues fixed successfully'))
|
|
206
|
+
|
|
207
|
+
if (binaryCheck.info) {
|
|
208
|
+
console.log()
|
|
209
|
+
console.log(chalk.gray('Current status:'))
|
|
210
|
+
console.log(` pg_restore version: ${binaryCheck.info.version}`)
|
|
211
|
+
console.log(` Path: ${binaryCheck.info.path}`)
|
|
212
|
+
if (binaryCheck.info.requiredVersion) {
|
|
213
|
+
console.log(` Required version: ${binaryCheck.info.requiredVersion}+`)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
})
|
|
@@ -7,6 +7,8 @@ import { getEngine } from '@/engines'
|
|
|
7
7
|
import { promptContainerSelect, promptDatabaseName } from '@/cli/ui/prompts'
|
|
8
8
|
import { createSpinner } from '@/cli/ui/spinner'
|
|
9
9
|
import { success, error, warning } from '@/cli/ui/theme'
|
|
10
|
+
import { platform } from 'os'
|
|
11
|
+
import { spawn } from 'child_process'
|
|
10
12
|
|
|
11
13
|
export const restoreCommand = new Command('restore')
|
|
12
14
|
.description('Restore a backup to a container')
|
|
@@ -146,6 +148,32 @@ export const restoreCommand = new Command('restore')
|
|
|
146
148
|
console.log()
|
|
147
149
|
console.log(chalk.gray(' Connection string:'))
|
|
148
150
|
console.log(chalk.cyan(` ${connectionString}`))
|
|
151
|
+
|
|
152
|
+
// Copy connection string to clipboard using platform-specific command
|
|
153
|
+
try {
|
|
154
|
+
const cmd = platform() === 'darwin' ? 'pbcopy' : 'xclip'
|
|
155
|
+
const args =
|
|
156
|
+
platform() === 'darwin' ? [] : ['-selection', 'clipboard']
|
|
157
|
+
|
|
158
|
+
await new Promise<void>((resolve, reject) => {
|
|
159
|
+
const proc = spawn(cmd, args, {
|
|
160
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
161
|
+
})
|
|
162
|
+
proc.stdin?.write(connectionString)
|
|
163
|
+
proc.stdin?.end()
|
|
164
|
+
proc.on('close', (code) => {
|
|
165
|
+
if (code === 0) resolve()
|
|
166
|
+
else
|
|
167
|
+
reject(new Error(`Clipboard command exited with code ${code}`))
|
|
168
|
+
})
|
|
169
|
+
proc.on('error', reject)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
console.log(chalk.gray(' Connection string copied to clipboard'))
|
|
173
|
+
} catch {
|
|
174
|
+
console.log(chalk.gray(' (Could not copy to clipboard)'))
|
|
175
|
+
}
|
|
176
|
+
|
|
149
177
|
console.log()
|
|
150
178
|
console.log(chalk.gray(' Connect with:'))
|
|
151
179
|
console.log(
|
package/src/cli/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { connectCommand } from '@/cli/commands/connect'
|
|
|
9
9
|
import { cloneCommand } from '@/cli/commands/clone'
|
|
10
10
|
import { menuCommand } from '@/cli/commands/menu'
|
|
11
11
|
import { configCommand } from '@/cli/commands/config'
|
|
12
|
+
import { postgresToolsCommand } from '@/cli/commands/postgres-tools'
|
|
12
13
|
|
|
13
14
|
export async function run(): Promise<void> {
|
|
14
15
|
program
|
|
@@ -26,6 +27,7 @@ export async function run(): Promise<void> {
|
|
|
26
27
|
program.addCommand(cloneCommand)
|
|
27
28
|
program.addCommand(menuCommand)
|
|
28
29
|
program.addCommand(configCommand)
|
|
30
|
+
program.addCommand(postgresToolsCommand)
|
|
29
31
|
|
|
30
32
|
// If no arguments provided, show interactive menu
|
|
31
33
|
if (process.argv.length <= 2) {
|
package/src/cli/ui/prompts.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import inquirer from 'inquirer'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
|
-
import
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import { listEngines, getEngine } from '@/engines'
|
|
4
5
|
import { defaults } from '@/config/defaults'
|
|
5
6
|
import type { ContainerConfig } from '@/types'
|
|
6
7
|
|
|
@@ -34,16 +35,26 @@ export async function promptContainerName(
|
|
|
34
35
|
export async function promptEngine(): Promise<string> {
|
|
35
36
|
const engines = listEngines()
|
|
36
37
|
|
|
38
|
+
// Build choices from available engines plus coming soon engines
|
|
39
|
+
const choices = [
|
|
40
|
+
...engines.map((e) => ({
|
|
41
|
+
name: `🐘 ${e.displayName} ${chalk.gray(`(versions: ${e.supportedVersions.join(', ')})`)}`,
|
|
42
|
+
value: e.name,
|
|
43
|
+
short: e.displayName,
|
|
44
|
+
})),
|
|
45
|
+
{
|
|
46
|
+
name: chalk.gray('🐬 MySQL (coming soon)'),
|
|
47
|
+
value: 'mysql',
|
|
48
|
+
disabled: 'Coming soon',
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
|
|
37
52
|
const { engine } = await inquirer.prompt<{ engine: string }>([
|
|
38
53
|
{
|
|
39
54
|
type: 'list',
|
|
40
55
|
name: 'engine',
|
|
41
56
|
message: 'Select database engine:',
|
|
42
|
-
choices
|
|
43
|
-
name: `${e.displayName} ${chalk.gray(`(versions: ${e.supportedVersions.join(', ')})`)}`,
|
|
44
|
-
value: e.name,
|
|
45
|
-
short: e.displayName,
|
|
46
|
-
})),
|
|
57
|
+
choices,
|
|
47
58
|
},
|
|
48
59
|
])
|
|
49
60
|
|
|
@@ -52,23 +63,89 @@ export async function promptEngine(): Promise<string> {
|
|
|
52
63
|
|
|
53
64
|
/**
|
|
54
65
|
* Prompt for PostgreSQL version
|
|
66
|
+
* Two-step selection: first major version, then specific minor version
|
|
55
67
|
*/
|
|
56
|
-
export async function promptVersion(
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
export async function promptVersion(engineName: string): Promise<string> {
|
|
69
|
+
const engine = getEngine(engineName)
|
|
70
|
+
const majorVersions = engine.supportedVersions
|
|
71
|
+
|
|
72
|
+
// Fetch available versions with a loading indicator
|
|
73
|
+
const spinner = ora({
|
|
74
|
+
text: 'Fetching available versions...',
|
|
75
|
+
color: 'cyan',
|
|
76
|
+
}).start()
|
|
77
|
+
|
|
78
|
+
let availableVersions: Record<string, string[]>
|
|
79
|
+
try {
|
|
80
|
+
availableVersions = await engine.fetchAvailableVersions()
|
|
81
|
+
spinner.stop()
|
|
82
|
+
} catch {
|
|
83
|
+
spinner.stop()
|
|
84
|
+
// Fall back to major versions only
|
|
85
|
+
availableVersions = {}
|
|
86
|
+
for (const v of majorVersions) {
|
|
87
|
+
availableVersions[v] = []
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Step 1: Select major version
|
|
92
|
+
type Choice = {
|
|
93
|
+
name: string
|
|
94
|
+
value: string
|
|
95
|
+
short?: string
|
|
96
|
+
}
|
|
97
|
+
const majorChoices: Choice[] = []
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < majorVersions.length; i++) {
|
|
100
|
+
const major = majorVersions[i]
|
|
101
|
+
const fullVersions = availableVersions[major] || []
|
|
102
|
+
const versionCount = fullVersions.length
|
|
103
|
+
const isLatestMajor = i === majorVersions.length - 1
|
|
104
|
+
|
|
105
|
+
const countLabel =
|
|
106
|
+
versionCount > 0 ? chalk.gray(`(${versionCount} versions)`) : ''
|
|
107
|
+
const label = isLatestMajor
|
|
108
|
+
? `PostgreSQL ${major} ${countLabel} ${chalk.green('← latest')}`
|
|
109
|
+
: `PostgreSQL ${major} ${countLabel}`
|
|
110
|
+
|
|
111
|
+
majorChoices.push({
|
|
112
|
+
name: label,
|
|
113
|
+
value: major,
|
|
114
|
+
short: `PostgreSQL ${major}`,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { majorVersion } = await inquirer.prompt<{ majorVersion: string }>([
|
|
119
|
+
{
|
|
120
|
+
type: 'list',
|
|
121
|
+
name: 'majorVersion',
|
|
122
|
+
message: 'Select major version:',
|
|
123
|
+
choices: majorChoices,
|
|
124
|
+
default: majorVersions[majorVersions.length - 1], // Default to latest major
|
|
125
|
+
},
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
// Step 2: Select specific version within the major version
|
|
129
|
+
const minorVersions = availableVersions[majorVersion] || []
|
|
130
|
+
|
|
131
|
+
if (minorVersions.length === 0) {
|
|
132
|
+
// No versions fetched, return major version (will use fallback)
|
|
133
|
+
return majorVersion
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const minorChoices: Choice[] = minorVersions.map((v, i) => ({
|
|
137
|
+
name: i === 0 ? `${v} ${chalk.green('← latest')}` : v,
|
|
138
|
+
value: v,
|
|
139
|
+
short: v,
|
|
140
|
+
}))
|
|
61
141
|
|
|
62
142
|
const { version } = await inquirer.prompt<{ version: string }>([
|
|
63
143
|
{
|
|
64
144
|
type: 'list',
|
|
65
145
|
name: 'version',
|
|
66
|
-
message:
|
|
67
|
-
choices:
|
|
68
|
-
|
|
69
|
-
value: v,
|
|
70
|
-
})),
|
|
71
|
-
default: versions[versions.length - 1], // Default to latest
|
|
146
|
+
message: `Select PostgreSQL ${majorVersion} version:`,
|
|
147
|
+
choices: minorChoices,
|
|
148
|
+
default: minorVersions[0], // Default to latest
|
|
72
149
|
},
|
|
73
150
|
])
|
|
74
151
|
|
|
@@ -169,6 +246,13 @@ export async function promptDatabaseName(
|
|
|
169
246
|
default: defaultName,
|
|
170
247
|
validate: (input: string) => {
|
|
171
248
|
if (!input) return 'Database name is required'
|
|
249
|
+
// PostgreSQL database naming rules
|
|
250
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(input)) {
|
|
251
|
+
return 'Database name must start with a letter or underscore and contain only letters, numbers, underscores, and hyphens'
|
|
252
|
+
}
|
|
253
|
+
if (input.length > 63) {
|
|
254
|
+
return 'Database name must be 63 characters or less'
|
|
255
|
+
}
|
|
172
256
|
return true
|
|
173
257
|
},
|
|
174
258
|
},
|
|
@@ -177,21 +261,27 @@ export async function promptDatabaseName(
|
|
|
177
261
|
return database
|
|
178
262
|
}
|
|
179
263
|
|
|
180
|
-
export
|
|
264
|
+
export type CreateOptions = {
|
|
181
265
|
name: string
|
|
182
266
|
engine: string
|
|
183
267
|
version: string
|
|
268
|
+
port: number
|
|
269
|
+
database: string
|
|
184
270
|
}
|
|
185
271
|
|
|
186
272
|
/**
|
|
187
273
|
* Full interactive create flow
|
|
188
274
|
*/
|
|
189
|
-
export async function promptCreateOptions(
|
|
275
|
+
export async function promptCreateOptions(
|
|
276
|
+
defaultPort: number = defaults.port,
|
|
277
|
+
): Promise<CreateOptions> {
|
|
190
278
|
console.log(chalk.cyan('\n 🗄️ Create New Database Container\n'))
|
|
191
279
|
|
|
192
|
-
const name = await promptContainerName()
|
|
193
280
|
const engine = await promptEngine()
|
|
194
281
|
const version = await promptVersion(engine)
|
|
282
|
+
const name = await promptContainerName()
|
|
283
|
+
const database = await promptDatabaseName(name) // Default to container name
|
|
284
|
+
const port = await promptPort(defaultPort)
|
|
195
285
|
|
|
196
|
-
return { name, engine, version }
|
|
286
|
+
return { name, engine, version, port, database }
|
|
197
287
|
}
|
package/src/cli/ui/theme.ts
CHANGED
|
@@ -92,6 +92,50 @@ export function keyValue(key: string, value: string): string {
|
|
|
92
92
|
return `${chalk.gray(key + ':')} ${value}`
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Strip ANSI escape codes to get actual string length
|
|
97
|
+
*/
|
|
98
|
+
function stripAnsi(str: string): string {
|
|
99
|
+
// eslint-disable-next-line no-control-regex
|
|
100
|
+
return str.replace(/\x1B\[[0-9;]*m/g, '')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Pad a string (accounting for ANSI codes) to a specific visible width
|
|
105
|
+
*/
|
|
106
|
+
function padToWidth(str: string, width: number): string {
|
|
107
|
+
const visibleLength = stripAnsi(str).length
|
|
108
|
+
const padding = Math.max(0, width - visibleLength)
|
|
109
|
+
return str + ' '.repeat(padding)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a box with dynamic width based on content
|
|
114
|
+
*/
|
|
115
|
+
export function box(lines: string[], padding: number = 2): string {
|
|
116
|
+
// Calculate max visible width
|
|
117
|
+
const maxWidth = Math.max(...lines.map((line) => stripAnsi(line).length))
|
|
118
|
+
const innerWidth = maxWidth + padding * 2
|
|
119
|
+
const horizontalLine = '─'.repeat(innerWidth)
|
|
120
|
+
|
|
121
|
+
const boxLines = [chalk.cyan('┌' + horizontalLine + '┐')]
|
|
122
|
+
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const paddedLine = padToWidth(line, maxWidth)
|
|
125
|
+
boxLines.push(
|
|
126
|
+
chalk.cyan('│') +
|
|
127
|
+
' '.repeat(padding) +
|
|
128
|
+
paddedLine +
|
|
129
|
+
' '.repeat(padding) +
|
|
130
|
+
chalk.cyan('│'),
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
boxLines.push(chalk.cyan('└' + horizontalLine + '┘'))
|
|
135
|
+
|
|
136
|
+
return boxLines.join('\n')
|
|
137
|
+
}
|
|
138
|
+
|
|
95
139
|
/**
|
|
96
140
|
* Format a connection string box
|
|
97
141
|
*/
|
|
@@ -100,14 +144,14 @@ export function connectionBox(
|
|
|
100
144
|
connectionString: string,
|
|
101
145
|
port: number,
|
|
102
146
|
): string {
|
|
103
|
-
|
|
104
|
-
${chalk.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
147
|
+
const lines = [
|
|
148
|
+
`${theme.icons.success} Container ${chalk.bold(name)} is ready!`,
|
|
149
|
+
'',
|
|
150
|
+
chalk.gray('Connection string:'),
|
|
151
|
+
chalk.white(connectionString),
|
|
152
|
+
'',
|
|
153
|
+
`${chalk.gray('Port:')} ${chalk.green(String(port))}`,
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
return box(lines)
|
|
113
157
|
}
|
package/src/config/defaults.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type PlatformMappings = {
|
|
2
2
|
[key: string]: string
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export type PortRange = {
|
|
6
6
|
start: number
|
|
7
7
|
end: number
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export type Defaults = {
|
|
11
11
|
postgresVersion: string
|
|
12
12
|
port: number
|
|
13
13
|
portRange: PortRange
|
|
@@ -17,6 +17,9 @@ export interface Defaults {
|
|
|
17
17
|
platformMappings: PlatformMappings
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// TODO - make defaults configurable via env vars or config file
|
|
21
|
+
// TODO - make defaults generic so it supports multiple engines
|
|
22
|
+
// TODO - consider using a configuration file or environment variables for overrides
|
|
20
23
|
export const defaults: Defaults = {
|
|
21
24
|
// Default PostgreSQL version
|
|
22
25
|
postgresVersion: '16',
|
|
@@ -28,16 +28,30 @@ export class BinaryManager {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Convert
|
|
31
|
+
* Convert version to full version format (e.g., "16" -> "16.6.0", "16.9" -> "16.9.0")
|
|
32
32
|
*/
|
|
33
|
-
getFullVersion(
|
|
33
|
+
getFullVersion(version: string): string {
|
|
34
|
+
// Map major versions to latest stable patch versions
|
|
35
|
+
// Updated from: https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-darwin-arm64v8/
|
|
34
36
|
const versionMap: Record<string, string> = {
|
|
35
|
-
'14': '14.
|
|
36
|
-
'15': '15.
|
|
37
|
-
'16': '16.
|
|
38
|
-
'17': '17.
|
|
37
|
+
'14': '14.20.0',
|
|
38
|
+
'15': '15.15.0',
|
|
39
|
+
'16': '16.11.0',
|
|
40
|
+
'17': '17.7.0',
|
|
39
41
|
}
|
|
40
|
-
|
|
42
|
+
|
|
43
|
+
// If it's a major version only, use the map
|
|
44
|
+
if (versionMap[version]) {
|
|
45
|
+
return versionMap[version]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Normalize to X.Y.Z format
|
|
49
|
+
const parts = version.split('.')
|
|
50
|
+
if (parts.length === 2) {
|
|
51
|
+
return `${version}.0`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return version
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/**
|
|
@@ -185,16 +199,32 @@ export class BinaryManager {
|
|
|
185
199
|
|
|
186
200
|
try {
|
|
187
201
|
const { stdout } = await execAsync(`"${postgresPath}" --version`)
|
|
188
|
-
|
|
189
|
-
|
|
202
|
+
// Extract version from output like "postgres (PostgreSQL) 16.9"
|
|
203
|
+
const match = stdout.match(/postgres \(PostgreSQL\) ([\d.]+)/)
|
|
204
|
+
if (!match) {
|
|
205
|
+
throw new Error(`Could not parse version from: ${stdout.trim()}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const reportedVersion = match[1]
|
|
209
|
+
// Normalize both versions for comparison (16.9.0 -> 16.9, 16 -> 16)
|
|
210
|
+
const normalizeVersion = (v: string) => v.replace(/\.0$/, '')
|
|
211
|
+
const expectedNormalized = normalizeVersion(version)
|
|
212
|
+
const reportedNormalized = normalizeVersion(reportedVersion)
|
|
213
|
+
|
|
214
|
+
// Check if versions match (after normalization)
|
|
215
|
+
if (reportedNormalized === expectedNormalized) {
|
|
190
216
|
return true
|
|
191
217
|
}
|
|
192
|
-
|
|
193
|
-
if (
|
|
218
|
+
|
|
219
|
+
// Also accept if major versions match (e.g., expected "16", got "16.9")
|
|
220
|
+
const expectedMajor = version.split('.')[0]
|
|
221
|
+
const reportedMajor = reportedVersion.split('.')[0]
|
|
222
|
+
if (expectedMajor === reportedMajor && version === expectedMajor) {
|
|
194
223
|
return true
|
|
195
224
|
}
|
|
225
|
+
|
|
196
226
|
throw new Error(
|
|
197
|
-
`Version mismatch: expected ${version}, got ${
|
|
227
|
+
`Version mismatch: expected ${version}, got ${reportedVersion}`,
|
|
198
228
|
)
|
|
199
229
|
} catch (error) {
|
|
200
230
|
const err = error as Error
|