spindb 0.5.2 → 0.5.3
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 +137 -8
- package/cli/commands/connect.ts +8 -4
- package/cli/commands/create.ts +106 -67
- package/cli/commands/deps.ts +19 -4
- package/cli/commands/edit.ts +245 -0
- package/cli/commands/engines.ts +434 -0
- package/cli/commands/info.ts +279 -0
- package/cli/commands/menu.ts +408 -153
- package/cli/commands/restore.ts +10 -24
- package/cli/commands/start.ts +25 -20
- package/cli/commands/url.ts +79 -0
- package/cli/index.ts +9 -3
- package/cli/ui/prompts.ts +8 -6
- package/config/engine-defaults.ts +24 -1
- package/config/os-dependencies.ts +59 -113
- package/config/paths.ts +7 -36
- package/core/binary-manager.ts +19 -6
- package/core/config-manager.ts +17 -5
- package/core/dependency-manager.ts +9 -15
- package/core/error-handler.ts +336 -0
- package/core/platform-service.ts +634 -0
- package/core/port-manager.ts +11 -3
- package/core/process-manager.ts +12 -2
- package/core/start-with-retry.ts +167 -0
- package/core/transaction-manager.ts +170 -0
- package/engines/mysql/binary-detection.ts +177 -100
- package/engines/mysql/index.ts +240 -131
- package/engines/mysql/restore.ts +257 -0
- package/engines/mysql/version-validator.ts +373 -0
- package/{core/postgres-binary-manager.ts → engines/postgresql/binary-manager.ts} +63 -23
- package/engines/postgresql/binary-urls.ts +5 -3
- package/engines/postgresql/index.ts +4 -3
- package/engines/postgresql/restore.ts +54 -5
- package/engines/postgresql/version-validator.ts +262 -0
- package/package.json +6 -2
- package/cli/commands/postgres-tools.ts +0 -216
package/core/config-manager.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { exec } from 'child_process'
|
|
|
4
4
|
import { promisify } from 'util'
|
|
5
5
|
import { dirname } from 'path'
|
|
6
6
|
import { paths } from '../config/paths'
|
|
7
|
+
import { logDebug, logWarning } from './error-handler'
|
|
7
8
|
import type {
|
|
8
9
|
SpinDBConfig,
|
|
9
10
|
BinaryConfig,
|
|
@@ -41,8 +42,12 @@ export class ConfigManager {
|
|
|
41
42
|
const content = await readFile(configPath, 'utf8')
|
|
42
43
|
this.config = JSON.parse(content) as SpinDBConfig
|
|
43
44
|
return this.config
|
|
44
|
-
} catch {
|
|
45
|
+
} catch (error) {
|
|
45
46
|
// If config is corrupted, reset to default
|
|
47
|
+
logWarning('Config file corrupted, resetting to default', {
|
|
48
|
+
configPath,
|
|
49
|
+
error: error instanceof Error ? error.message : String(error),
|
|
50
|
+
})
|
|
46
51
|
this.config = { ...DEFAULT_CONFIG }
|
|
47
52
|
await this.save()
|
|
48
53
|
return this.config
|
|
@@ -110,8 +115,12 @@ export class ConfigManager {
|
|
|
110
115
|
if (match) {
|
|
111
116
|
version = match[0]
|
|
112
117
|
}
|
|
113
|
-
} catch {
|
|
114
|
-
|
|
118
|
+
} catch (error) {
|
|
119
|
+
logDebug('Version detection failed', {
|
|
120
|
+
tool,
|
|
121
|
+
path,
|
|
122
|
+
error: error instanceof Error ? error.message : String(error),
|
|
123
|
+
})
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
config.binaries[tool] = {
|
|
@@ -142,8 +151,11 @@ export class ConfigManager {
|
|
|
142
151
|
if (path && existsSync(path)) {
|
|
143
152
|
return path
|
|
144
153
|
}
|
|
145
|
-
} catch {
|
|
146
|
-
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logDebug('which command failed for binary detection', {
|
|
156
|
+
tool,
|
|
157
|
+
error: error instanceof Error ? error.message : String(error),
|
|
158
|
+
})
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
// Check common locations
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getEngineDependencies,
|
|
17
17
|
getUniqueDependencies,
|
|
18
18
|
} from '../config/os-dependencies'
|
|
19
|
+
import { platformService } from './platform-service'
|
|
19
20
|
|
|
20
21
|
const execAsync = promisify(exec)
|
|
21
22
|
|
|
@@ -46,7 +47,7 @@ export type InstallResult = {
|
|
|
46
47
|
* Detect which package manager is available on the current system
|
|
47
48
|
*/
|
|
48
49
|
export async function detectPackageManager(): Promise<DetectedPackageManager | null> {
|
|
49
|
-
const platform =
|
|
50
|
+
const { platform } = platformService.getPlatformInfo()
|
|
50
51
|
|
|
51
52
|
// Filter to package managers available on this platform
|
|
52
53
|
const candidates = packageManagers.filter((pm) =>
|
|
@@ -73,7 +74,7 @@ export async function detectPackageManager(): Promise<DetectedPackageManager | n
|
|
|
73
74
|
* Get the current platform
|
|
74
75
|
*/
|
|
75
76
|
export function getCurrentPlatform(): Platform {
|
|
76
|
-
return
|
|
77
|
+
return platformService.getPlatformInfo().platform as Platform
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
// =============================================================================
|
|
@@ -87,21 +88,12 @@ export async function findBinary(
|
|
|
87
88
|
binary: string,
|
|
88
89
|
): Promise<{ path: string; version?: string } | null> {
|
|
89
90
|
try {
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
const path = stdout.trim().split('\n')[0]
|
|
93
|
-
|
|
91
|
+
// Use platformService to find the binary path
|
|
92
|
+
const path = await platformService.findToolPath(binary)
|
|
94
93
|
if (!path) return null
|
|
95
94
|
|
|
96
95
|
// Try to get version
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const { stdout: versionOutput } = await execAsync(`${binary} --version`)
|
|
100
|
-
const match = versionOutput.match(/(\d+\.\d+(\.\d+)?)/)
|
|
101
|
-
version = match ? match[1] : undefined
|
|
102
|
-
} catch {
|
|
103
|
-
// Version check failed, that's ok
|
|
104
|
-
}
|
|
96
|
+
const version = (await platformService.getToolVersion(path)) || undefined
|
|
105
97
|
|
|
106
98
|
return { path, version }
|
|
107
99
|
} catch {
|
|
@@ -216,7 +208,9 @@ function execWithInheritedStdio(command: string): void {
|
|
|
216
208
|
}
|
|
217
209
|
|
|
218
210
|
if (result.status !== 0) {
|
|
219
|
-
throw new Error(
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Command failed with exit code ${result.status}: ${cmdToRun}`,
|
|
213
|
+
)
|
|
220
214
|
}
|
|
221
215
|
}
|
|
222
216
|
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler
|
|
3
|
+
*
|
|
4
|
+
* Centralized error handling with proper logging and user feedback.
|
|
5
|
+
* - CLI commands log and exit (no blocking for scripts/CI)
|
|
6
|
+
* - Interactive menu uses "Press Enter to continue" pattern
|
|
7
|
+
* - All errors are logged to ~/.spindb/spindb.log for debugging
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { appendFileSync, existsSync, mkdirSync } from 'fs'
|
|
11
|
+
import { dirname, join } from 'path'
|
|
12
|
+
import chalk from 'chalk'
|
|
13
|
+
|
|
14
|
+
// Get SpinDB home directory without circular import
|
|
15
|
+
function getSpinDBRoot(): string {
|
|
16
|
+
const home = process.env.HOME || process.env.USERPROFILE || ''
|
|
17
|
+
return join(home, '.spindb')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
export type ErrorSeverity = 'fatal' | 'error' | 'warning' | 'info'
|
|
25
|
+
|
|
26
|
+
export type SpinDBErrorInfo = {
|
|
27
|
+
code: string
|
|
28
|
+
message: string
|
|
29
|
+
severity: ErrorSeverity
|
|
30
|
+
suggestion?: string
|
|
31
|
+
context?: Record<string, unknown>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Error Codes
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
export const ErrorCodes = {
|
|
39
|
+
// Port errors
|
|
40
|
+
PORT_IN_USE: 'PORT_IN_USE',
|
|
41
|
+
PORT_PERMISSION_DENIED: 'PORT_PERMISSION_DENIED',
|
|
42
|
+
PORT_RANGE_EXHAUSTED: 'PORT_RANGE_EXHAUSTED',
|
|
43
|
+
|
|
44
|
+
// Process errors
|
|
45
|
+
PROCESS_START_FAILED: 'PROCESS_START_FAILED',
|
|
46
|
+
PROCESS_STOP_TIMEOUT: 'PROCESS_STOP_TIMEOUT',
|
|
47
|
+
PROCESS_ALREADY_RUNNING: 'PROCESS_ALREADY_RUNNING',
|
|
48
|
+
PROCESS_NOT_RUNNING: 'PROCESS_NOT_RUNNING',
|
|
49
|
+
PID_FILE_CORRUPT: 'PID_FILE_CORRUPT',
|
|
50
|
+
PID_FILE_STALE: 'PID_FILE_STALE',
|
|
51
|
+
PID_FILE_READ_FAILED: 'PID_FILE_READ_FAILED',
|
|
52
|
+
|
|
53
|
+
// Restore errors
|
|
54
|
+
VERSION_MISMATCH: 'VERSION_MISMATCH',
|
|
55
|
+
RESTORE_PARTIAL_FAILURE: 'RESTORE_PARTIAL_FAILURE',
|
|
56
|
+
RESTORE_COMPLETE_FAILURE: 'RESTORE_COMPLETE_FAILURE',
|
|
57
|
+
BACKUP_FORMAT_UNKNOWN: 'BACKUP_FORMAT_UNKNOWN',
|
|
58
|
+
WRONG_ENGINE_DUMP: 'WRONG_ENGINE_DUMP',
|
|
59
|
+
|
|
60
|
+
// Container errors
|
|
61
|
+
CONTAINER_NOT_FOUND: 'CONTAINER_NOT_FOUND',
|
|
62
|
+
CONTAINER_ALREADY_EXISTS: 'CONTAINER_ALREADY_EXISTS',
|
|
63
|
+
CONTAINER_RUNNING: 'CONTAINER_RUNNING',
|
|
64
|
+
CONTAINER_CREATE_FAILED: 'CONTAINER_CREATE_FAILED',
|
|
65
|
+
INIT_FAILED: 'INIT_FAILED',
|
|
66
|
+
DATABASE_CREATE_FAILED: 'DATABASE_CREATE_FAILED',
|
|
67
|
+
|
|
68
|
+
// Dependency errors
|
|
69
|
+
DEPENDENCY_MISSING: 'DEPENDENCY_MISSING',
|
|
70
|
+
DEPENDENCY_VERSION_INCOMPATIBLE: 'DEPENDENCY_VERSION_INCOMPATIBLE',
|
|
71
|
+
|
|
72
|
+
// Rollback errors
|
|
73
|
+
ROLLBACK_FAILED: 'ROLLBACK_FAILED',
|
|
74
|
+
|
|
75
|
+
// Clipboard errors
|
|
76
|
+
CLIPBOARD_FAILED: 'CLIPBOARD_FAILED',
|
|
77
|
+
|
|
78
|
+
// General errors
|
|
79
|
+
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
|
80
|
+
FILE_NOT_FOUND: 'FILE_NOT_FOUND',
|
|
81
|
+
PERMISSION_DENIED: 'PERMISSION_DENIED',
|
|
82
|
+
} as const
|
|
83
|
+
|
|
84
|
+
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]
|
|
85
|
+
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// SpinDBError Class
|
|
88
|
+
// =============================================================================
|
|
89
|
+
|
|
90
|
+
export class SpinDBError extends Error {
|
|
91
|
+
public readonly code: string
|
|
92
|
+
public readonly severity: ErrorSeverity
|
|
93
|
+
public readonly suggestion?: string
|
|
94
|
+
public readonly context?: Record<string, unknown>
|
|
95
|
+
|
|
96
|
+
constructor(
|
|
97
|
+
code: string,
|
|
98
|
+
message: string,
|
|
99
|
+
severity: ErrorSeverity = 'error',
|
|
100
|
+
suggestion?: string,
|
|
101
|
+
context?: Record<string, unknown>,
|
|
102
|
+
) {
|
|
103
|
+
super(message)
|
|
104
|
+
this.name = 'SpinDBError'
|
|
105
|
+
this.code = code
|
|
106
|
+
this.severity = severity
|
|
107
|
+
this.suggestion = suggestion
|
|
108
|
+
this.context = context
|
|
109
|
+
|
|
110
|
+
// Capture proper stack trace
|
|
111
|
+
Error.captureStackTrace(this, SpinDBError)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create SpinDBError from an unknown error
|
|
116
|
+
*/
|
|
117
|
+
static from(
|
|
118
|
+
error: unknown,
|
|
119
|
+
code: string = ErrorCodes.UNKNOWN_ERROR,
|
|
120
|
+
suggestion?: string,
|
|
121
|
+
): SpinDBError {
|
|
122
|
+
if (error instanceof SpinDBError) {
|
|
123
|
+
return error
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
127
|
+
|
|
128
|
+
return new SpinDBError(code, message, 'error', suggestion, {
|
|
129
|
+
originalError: error instanceof Error ? error.stack : undefined,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// =============================================================================
|
|
135
|
+
// Logging Functions
|
|
136
|
+
// =============================================================================
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get the path to the log file
|
|
140
|
+
*/
|
|
141
|
+
function getLogPath(): string {
|
|
142
|
+
return join(getSpinDBRoot(), 'spindb.log')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Ensure the log directory exists
|
|
147
|
+
*/
|
|
148
|
+
function ensureLogDirectory(): void {
|
|
149
|
+
const logPath = getLogPath()
|
|
150
|
+
const logDir = dirname(logPath)
|
|
151
|
+
if (!existsSync(logDir)) {
|
|
152
|
+
mkdirSync(logDir, { recursive: true })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Append a structured log entry to the log file
|
|
158
|
+
*/
|
|
159
|
+
function appendToLogFile(entry: SpinDBErrorInfo): void {
|
|
160
|
+
try {
|
|
161
|
+
ensureLogDirectory()
|
|
162
|
+
const logPath = getLogPath()
|
|
163
|
+
const logEntry = {
|
|
164
|
+
timestamp: new Date().toISOString(),
|
|
165
|
+
...entry,
|
|
166
|
+
}
|
|
167
|
+
appendFileSync(logPath, JSON.stringify(logEntry) + '\n')
|
|
168
|
+
} catch {
|
|
169
|
+
// If we can't write to log file, don't fail the operation
|
|
170
|
+
// This could happen if ~/.spindb doesn't exist yet
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Format severity for console output
|
|
176
|
+
*/
|
|
177
|
+
function formatSeverity(severity: ErrorSeverity): string {
|
|
178
|
+
switch (severity) {
|
|
179
|
+
case 'fatal':
|
|
180
|
+
return chalk.red.bold('[FATAL]')
|
|
181
|
+
case 'error':
|
|
182
|
+
return chalk.red('[ERROR]')
|
|
183
|
+
case 'warning':
|
|
184
|
+
return chalk.yellow('[WARN]')
|
|
185
|
+
case 'info':
|
|
186
|
+
return chalk.blue('[INFO]')
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Log an error to console and log file
|
|
192
|
+
* This is for CLI commands - displays error and returns (no blocking)
|
|
193
|
+
*/
|
|
194
|
+
export function logError(error: SpinDBErrorInfo): void {
|
|
195
|
+
// Console output with colors
|
|
196
|
+
const prefix = formatSeverity(error.severity)
|
|
197
|
+
console.error(`${prefix} [${error.code}] ${error.message}`)
|
|
198
|
+
|
|
199
|
+
if (error.suggestion) {
|
|
200
|
+
console.error(chalk.yellow(` Suggestion: ${error.suggestion}`))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Also append to log file for headless debugging
|
|
204
|
+
appendToLogFile(error)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Log a SpinDBError instance
|
|
209
|
+
*/
|
|
210
|
+
export function logSpinDBError(error: SpinDBError): void {
|
|
211
|
+
logError({
|
|
212
|
+
code: error.code,
|
|
213
|
+
message: error.message,
|
|
214
|
+
severity: error.severity,
|
|
215
|
+
suggestion: error.suggestion,
|
|
216
|
+
context: error.context,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Log a warning (non-blocking, yellow output)
|
|
222
|
+
*/
|
|
223
|
+
export function logWarning(
|
|
224
|
+
message: string,
|
|
225
|
+
context?: Record<string, unknown>,
|
|
226
|
+
): void {
|
|
227
|
+
console.warn(chalk.yellow(` ⚠ ${message}`))
|
|
228
|
+
|
|
229
|
+
appendToLogFile({
|
|
230
|
+
code: 'WARNING',
|
|
231
|
+
message,
|
|
232
|
+
severity: 'warning',
|
|
233
|
+
context,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Log an info message
|
|
239
|
+
*/
|
|
240
|
+
export function logInfo(
|
|
241
|
+
message: string,
|
|
242
|
+
context?: Record<string, unknown>,
|
|
243
|
+
): void {
|
|
244
|
+
appendToLogFile({
|
|
245
|
+
code: 'INFO',
|
|
246
|
+
message,
|
|
247
|
+
severity: 'info',
|
|
248
|
+
context,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Log a debug message (only to file, not console)
|
|
254
|
+
*/
|
|
255
|
+
export function logDebug(
|
|
256
|
+
message: string,
|
|
257
|
+
context?: Record<string, unknown>,
|
|
258
|
+
): void {
|
|
259
|
+
appendToLogFile({
|
|
260
|
+
code: 'DEBUG',
|
|
261
|
+
message,
|
|
262
|
+
severity: 'info',
|
|
263
|
+
context,
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// =============================================================================
|
|
268
|
+
// Error Creation Helpers
|
|
269
|
+
// =============================================================================
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Create a port-in-use error with helpful suggestion
|
|
273
|
+
*/
|
|
274
|
+
export function createPortInUseError(port: number): SpinDBError {
|
|
275
|
+
return new SpinDBError(
|
|
276
|
+
ErrorCodes.PORT_IN_USE,
|
|
277
|
+
`Port ${port} is already in use`,
|
|
278
|
+
'error',
|
|
279
|
+
`Use a different port with -p flag, or stop the process using port ${port}`,
|
|
280
|
+
{ port },
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Create a container-not-found error
|
|
286
|
+
*/
|
|
287
|
+
export function createContainerNotFoundError(name: string): SpinDBError {
|
|
288
|
+
return new SpinDBError(
|
|
289
|
+
ErrorCodes.CONTAINER_NOT_FOUND,
|
|
290
|
+
`Container "${name}" not found`,
|
|
291
|
+
'error',
|
|
292
|
+
'Run "spindb list" to see available containers',
|
|
293
|
+
{ containerName: name },
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Create a version mismatch error for pg_restore
|
|
299
|
+
*/
|
|
300
|
+
export function createVersionMismatchError(
|
|
301
|
+
dumpVersion: string,
|
|
302
|
+
toolVersion: string,
|
|
303
|
+
): SpinDBError {
|
|
304
|
+
return new SpinDBError(
|
|
305
|
+
ErrorCodes.VERSION_MISMATCH,
|
|
306
|
+
`Backup was created with PostgreSQL ${dumpVersion}, but your pg_restore is version ${toolVersion}`,
|
|
307
|
+
'fatal',
|
|
308
|
+
`Install PostgreSQL ${dumpVersion} client tools: brew install postgresql@${dumpVersion}`,
|
|
309
|
+
{ dumpVersion, toolVersion },
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Create a dependency missing error
|
|
315
|
+
*/
|
|
316
|
+
export function createDependencyMissingError(
|
|
317
|
+
toolName: string,
|
|
318
|
+
engine: string,
|
|
319
|
+
): SpinDBError {
|
|
320
|
+
const suggestions: Record<string, string> = {
|
|
321
|
+
psql: 'brew install libpq && brew link --force libpq',
|
|
322
|
+
pg_dump: 'brew install libpq && brew link --force libpq',
|
|
323
|
+
pg_restore: 'brew install libpq && brew link --force libpq',
|
|
324
|
+
mysql: 'brew install mysql-client',
|
|
325
|
+
mysqldump: 'brew install mysql-client',
|
|
326
|
+
mysqld: 'brew install mysql',
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return new SpinDBError(
|
|
330
|
+
ErrorCodes.DEPENDENCY_MISSING,
|
|
331
|
+
`${toolName} not found`,
|
|
332
|
+
'error',
|
|
333
|
+
suggestions[toolName] || `Install ${engine} client tools`,
|
|
334
|
+
{ toolName, engine },
|
|
335
|
+
)
|
|
336
|
+
}
|