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.
Files changed (38) hide show
  1. package/README.md +188 -9
  2. package/cli/commands/connect.ts +334 -105
  3. package/cli/commands/create.ts +106 -67
  4. package/cli/commands/deps.ts +19 -4
  5. package/cli/commands/edit.ts +245 -0
  6. package/cli/commands/engines.ts +434 -0
  7. package/cli/commands/info.ts +279 -0
  8. package/cli/commands/list.ts +1 -1
  9. package/cli/commands/menu.ts +664 -167
  10. package/cli/commands/restore.ts +11 -25
  11. package/cli/commands/start.ts +25 -20
  12. package/cli/commands/url.ts +79 -0
  13. package/cli/index.ts +9 -3
  14. package/cli/ui/prompts.ts +20 -12
  15. package/cli/ui/theme.ts +1 -1
  16. package/config/engine-defaults.ts +24 -1
  17. package/config/os-dependencies.ts +151 -113
  18. package/config/paths.ts +7 -36
  19. package/core/binary-manager.ts +12 -6
  20. package/core/config-manager.ts +17 -5
  21. package/core/dependency-manager.ts +144 -15
  22. package/core/error-handler.ts +336 -0
  23. package/core/platform-service.ts +634 -0
  24. package/core/port-manager.ts +11 -3
  25. package/core/process-manager.ts +12 -2
  26. package/core/start-with-retry.ts +167 -0
  27. package/core/transaction-manager.ts +170 -0
  28. package/engines/mysql/binary-detection.ts +177 -100
  29. package/engines/mysql/index.ts +240 -131
  30. package/engines/mysql/restore.ts +257 -0
  31. package/engines/mysql/version-validator.ts +373 -0
  32. package/{core/postgres-binary-manager.ts → engines/postgresql/binary-manager.ts} +63 -23
  33. package/engines/postgresql/binary-urls.ts +5 -3
  34. package/engines/postgresql/index.ts +35 -4
  35. package/engines/postgresql/restore.ts +54 -5
  36. package/engines/postgresql/version-validator.ts +262 -0
  37. package/package.json +6 -2
  38. 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
+ }