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.
@@ -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) {
@@ -1,6 +1,7 @@
1
1
  import inquirer from 'inquirer'
2
2
  import chalk from 'chalk'
3
- import { listEngines } from '@/engines'
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: engines.map((e) => ({
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(engine: string): Promise<string> {
57
- const engines = listEngines()
58
- const selectedEngine = engines.find((e) => e.name === engine)
59
- const versions =
60
- selectedEngine?.supportedVersions || defaults.supportedPostgresVersions
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: 'Select version:',
67
- choices: versions.map((v, i) => ({
68
- name: i === versions.length - 1 ? `${v} ${chalk.green('(latest)')}` : v,
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 interface CreateOptions {
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(): Promise<CreateOptions> {
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
  }
@@ -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
- 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()
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
  }
@@ -1,13 +1,13 @@
1
- export interface PlatformMappings {
1
+ export type PlatformMappings = {
2
2
  [key: string]: string
3
3
  }
4
4
 
5
- export interface PortRange {
5
+ export type PortRange = {
6
6
  start: number
7
7
  end: number
8
8
  }
9
9
 
10
- export interface Defaults {
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 major version to full version (e.g., "16" -> "16.4.0")
31
+ * Convert version to full version format (e.g., "16" -> "16.6.0", "16.9" -> "16.9.0")
32
32
  */
33
- getFullVersion(majorVersion: string): string {
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.15.0',
36
- '15': '15.10.0',
37
- '16': '16.6.0',
38
- '17': '17.2.0',
37
+ '14': '14.20.0',
38
+ '15': '15.15.0',
39
+ '16': '16.11.0',
40
+ '17': '17.7.0',
39
41
  }
40
- return versionMap[majorVersion] || `${majorVersion}.0.0`
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
- const match = stdout.match(/postgres \(PostgreSQL\) (\d+)/)
189
- if (match && match[1] === version) {
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
- // 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}`)) {
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 ${stdout.trim()}`,
227
+ `Version mismatch: expected ${version}, got ${reportedVersion}`,
198
228
  )
199
229
  } catch (error) {
200
230
  const err = error as Error