spindb 0.5.2 → 0.5.4
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 +188 -9
- package/cli/commands/connect.ts +334 -105
- 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/list.ts +1 -1
- package/cli/commands/menu.ts +664 -167
- package/cli/commands/restore.ts +11 -25
- 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 +20 -12
- package/cli/ui/theme.ts +1 -1
- package/config/engine-defaults.ts +24 -1
- package/config/os-dependencies.ts +151 -113
- package/config/paths.ts +7 -36
- package/core/binary-manager.ts +12 -6
- package/core/config-manager.ts +17 -5
- package/core/dependency-manager.ts +144 -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 +35 -4
- 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
|
@@ -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
|
+
}
|