spindb 0.9.1 → 0.9.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.
Files changed (55) hide show
  1. package/README.md +5 -8
  2. package/cli/commands/attach.ts +108 -0
  3. package/cli/commands/backup.ts +13 -11
  4. package/cli/commands/clone.ts +14 -10
  5. package/cli/commands/config.ts +29 -29
  6. package/cli/commands/connect.ts +51 -39
  7. package/cli/commands/create.ts +65 -32
  8. package/cli/commands/delete.ts +8 -8
  9. package/cli/commands/deps.ts +17 -15
  10. package/cli/commands/detach.ts +100 -0
  11. package/cli/commands/doctor.ts +27 -13
  12. package/cli/commands/edit.ts +120 -57
  13. package/cli/commands/engines.ts +17 -15
  14. package/cli/commands/info.ts +8 -6
  15. package/cli/commands/list.ts +127 -18
  16. package/cli/commands/logs.ts +15 -11
  17. package/cli/commands/menu/backup-handlers.ts +52 -47
  18. package/cli/commands/menu/container-handlers.ts +164 -79
  19. package/cli/commands/menu/engine-handlers.ts +21 -11
  20. package/cli/commands/menu/index.ts +4 -4
  21. package/cli/commands/menu/shell-handlers.ts +34 -31
  22. package/cli/commands/menu/sql-handlers.ts +22 -16
  23. package/cli/commands/menu/update-handlers.ts +19 -17
  24. package/cli/commands/restore.ts +22 -20
  25. package/cli/commands/run.ts +20 -18
  26. package/cli/commands/self-update.ts +5 -5
  27. package/cli/commands/sqlite.ts +247 -0
  28. package/cli/commands/start.ts +11 -9
  29. package/cli/commands/stop.ts +9 -9
  30. package/cli/commands/url.ts +12 -9
  31. package/cli/helpers.ts +9 -4
  32. package/cli/index.ts +6 -0
  33. package/cli/ui/prompts.ts +12 -5
  34. package/cli/ui/spinner.ts +4 -4
  35. package/cli/ui/theme.ts +4 -4
  36. package/config/paths.ts +0 -8
  37. package/core/binary-manager.ts +5 -1
  38. package/core/config-manager.ts +32 -0
  39. package/core/container-manager.ts +5 -5
  40. package/core/platform-service.ts +3 -3
  41. package/core/start-with-retry.ts +6 -6
  42. package/core/transaction-manager.ts +6 -6
  43. package/engines/mysql/backup.ts +37 -13
  44. package/engines/mysql/index.ts +11 -11
  45. package/engines/mysql/restore.ts +4 -4
  46. package/engines/mysql/version-validator.ts +2 -2
  47. package/engines/postgresql/binary-manager.ts +17 -17
  48. package/engines/postgresql/index.ts +7 -2
  49. package/engines/postgresql/restore.ts +2 -2
  50. package/engines/postgresql/version-validator.ts +2 -2
  51. package/engines/sqlite/index.ts +30 -15
  52. package/engines/sqlite/registry.ts +64 -33
  53. package/engines/sqlite/scanner.ts +99 -0
  54. package/package.json +4 -3
  55. package/types/index.ts +21 -1
package/README.md CHANGED
@@ -653,16 +653,13 @@ rm -rf ~/.spindb
653
653
 
654
654
  ## Contributing
655
655
 
656
- See [CLAUDE.md](CLAUDE.md) for development setup and architecture documentation.
656
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and distribution info.
657
657
 
658
- ### Running Tests
658
+ See [ARCHITECTURE.md](ARCHITECTURE.md) for project architecture and comprehensive CLI command examples.
659
659
 
660
- ```bash
661
- pnpm test # All tests
662
- pnpm test:unit # Unit tests only
663
- pnpm test:pg # PostgreSQL integration
664
- pnpm test:mysql # MySQL integration
665
- ```
660
+ See [CLAUDE.md](CLAUDE.md) for AI-assisted development context.
661
+
662
+ See [ENGINES.md](ENGINES.md) for detailed engine documentation (backup formats, planned engines, etc.).
666
663
 
667
664
  ---
668
665
 
@@ -0,0 +1,108 @@
1
+ import { Command } from 'commander'
2
+ import { existsSync } from 'fs'
3
+ import { resolve, basename } from 'path'
4
+ import chalk from 'chalk'
5
+ import { sqliteRegistry } from '../../engines/sqlite/registry'
6
+ import { containerManager } from '../../core/container-manager'
7
+ import { deriveContainerName } from '../../engines/sqlite/scanner'
8
+ import { uiSuccess, uiError } from '../ui/theme'
9
+
10
+ export const attachCommand = new Command('attach')
11
+ .description('Register an existing SQLite database with SpinDB')
12
+ .argument('<path>', 'Path to SQLite database file')
13
+ .option('-n, --name <name>', 'Container name (defaults to filename)')
14
+ .option('--json', 'Output as JSON')
15
+ .action(
16
+ async (
17
+ path: string,
18
+ options: { name?: string; json?: boolean },
19
+ ): Promise<void> => {
20
+ try {
21
+ const absolutePath = resolve(path)
22
+
23
+ // Verify file exists
24
+ if (!existsSync(absolutePath)) {
25
+ if (options.json) {
26
+ console.log(
27
+ JSON.stringify({ success: false, error: 'File not found' }),
28
+ )
29
+ } else {
30
+ console.error(uiError(`File not found: ${absolutePath}`))
31
+ }
32
+ process.exit(1)
33
+ }
34
+
35
+ // Check if already registered
36
+ if (await sqliteRegistry.isPathRegistered(absolutePath)) {
37
+ const entry = await sqliteRegistry.getByPath(absolutePath)
38
+ if (options.json) {
39
+ console.log(
40
+ JSON.stringify({
41
+ success: false,
42
+ error: 'Already registered',
43
+ existingName: entry?.name,
44
+ }),
45
+ )
46
+ } else {
47
+ console.error(
48
+ uiError(`File is already registered as "${entry?.name}"`),
49
+ )
50
+ }
51
+ process.exit(1)
52
+ }
53
+
54
+ // Determine container name
55
+ const containerName =
56
+ options.name || deriveContainerName(basename(absolutePath))
57
+
58
+ // Check if container name exists
59
+ if (await containerManager.exists(containerName)) {
60
+ if (options.json) {
61
+ console.log(
62
+ JSON.stringify({
63
+ success: false,
64
+ error: 'Container name already exists',
65
+ }),
66
+ )
67
+ } else {
68
+ console.error(uiError(`Container "${containerName}" already exists`))
69
+ }
70
+ process.exit(1)
71
+ }
72
+
73
+ // Register the file
74
+ await sqliteRegistry.add({
75
+ name: containerName,
76
+ filePath: absolutePath,
77
+ created: new Date().toISOString(),
78
+ })
79
+
80
+ if (options.json) {
81
+ console.log(
82
+ JSON.stringify({
83
+ success: true,
84
+ name: containerName,
85
+ filePath: absolutePath,
86
+ }),
87
+ )
88
+ } else {
89
+ console.log(
90
+ uiSuccess(
91
+ `Registered "${basename(absolutePath)}" as "${containerName}"`,
92
+ ),
93
+ )
94
+ console.log()
95
+ console.log(chalk.gray(' Connect with:'))
96
+ console.log(chalk.cyan(` spindb connect ${containerName}`))
97
+ }
98
+ } catch (error) {
99
+ const e = error as Error
100
+ if (options.json) {
101
+ console.log(JSON.stringify({ success: false, error: e.message }))
102
+ } else {
103
+ console.error(uiError(e.message))
104
+ }
105
+ process.exit(1)
106
+ }
107
+ },
108
+ )
@@ -12,7 +12,7 @@ import {
12
12
  promptInstallDependencies,
13
13
  } from '../ui/prompts'
14
14
  import { createSpinner } from '../ui/spinner'
15
- import { success, error, warning, formatBytes } from '../ui/theme'
15
+ import { uiSuccess, uiError, uiWarning, formatBytes } from '../ui/theme'
16
16
  import { getMissingDependencies } from '../../core/dependency-manager'
17
17
 
18
18
  function generateTimestamp(): string {
@@ -70,11 +70,13 @@ export const backupCommand = new Command('backup')
70
70
  if (running.length === 0) {
71
71
  if (containers.length === 0) {
72
72
  console.log(
73
- warning('No containers found. Create one with: spindb create'),
73
+ uiWarning(
74
+ 'No containers found. Create one with: spindb create',
75
+ ),
74
76
  )
75
77
  } else {
76
78
  console.log(
77
- warning(
79
+ uiWarning(
78
80
  'No running containers. Start one first with: spindb start',
79
81
  ),
80
82
  )
@@ -92,7 +94,7 @@ export const backupCommand = new Command('backup')
92
94
 
93
95
  const config = await containerManager.getConfig(containerName)
94
96
  if (!config) {
95
- console.error(error(`Container "${containerName}" not found`))
97
+ console.error(uiError(`Container "${containerName}" not found`))
96
98
  process.exit(1)
97
99
  }
98
100
 
@@ -103,7 +105,7 @@ export const backupCommand = new Command('backup')
103
105
  })
104
106
  if (!running) {
105
107
  console.error(
106
- error(
108
+ uiError(
107
109
  `Container "${containerName}" is not running. Start it first.`,
108
110
  ),
109
111
  )
@@ -133,7 +135,7 @@ export const backupCommand = new Command('backup')
133
135
  missingDeps = await getMissingDependencies(config.engine)
134
136
  if (missingDeps.length > 0) {
135
137
  console.error(
136
- error(
138
+ uiError(
137
139
  `Still missing tools: ${missingDeps.map((d) => d.name).join(', ')}`,
138
140
  ),
139
141
  )
@@ -169,7 +171,7 @@ export const backupCommand = new Command('backup')
169
171
  format = 'dump'
170
172
  } else if (options.format) {
171
173
  if (options.format !== 'sql' && options.format !== 'dump') {
172
- console.error(error('Format must be "sql" or "dump"'))
174
+ console.error(uiError('Format must be "sql" or "dump"'))
173
175
  process.exit(1)
174
176
  }
175
177
  format = options.format as 'sql' | 'dump'
@@ -204,7 +206,7 @@ export const backupCommand = new Command('backup')
204
206
  backupSpinner.succeed('Backup created successfully')
205
207
 
206
208
  console.log()
207
- console.log(success('Backup complete'))
209
+ console.log(uiSuccess('Backup complete'))
208
210
  console.log()
209
211
  console.log(chalk.gray(' File:'), chalk.cyan(result.path))
210
212
  console.log(
@@ -213,8 +215,8 @@ export const backupCommand = new Command('backup')
213
215
  )
214
216
  console.log(chalk.gray(' Format:'), chalk.white(result.format))
215
217
  console.log()
216
- } catch (err) {
217
- const e = err as Error
218
+ } catch (error) {
219
+ const e = error as Error
218
220
 
219
221
  const missingToolPatterns = ['pg_dump not found', 'mysqldump not found']
220
222
 
@@ -233,7 +235,7 @@ export const backupCommand = new Command('backup')
233
235
  process.exit(1)
234
236
  }
235
237
 
236
- console.error(error(e.message))
238
+ console.error(uiError(e.message))
237
239
  process.exit(1)
238
240
  }
239
241
  },
@@ -5,7 +5,7 @@ import { processManager } from '../../core/process-manager'
5
5
  import { getEngine } from '../../engines'
6
6
  import { promptContainerSelect, promptContainerName } from '../ui/prompts'
7
7
  import { createSpinner } from '../ui/spinner'
8
- import { error, warning, connectionBox } from '../ui/theme'
8
+ import { uiError, uiWarning, connectionBox } from '../ui/theme'
9
9
 
10
10
  export const cloneCommand = new Command('clone')
11
11
  .description('Clone a container with all its data')
@@ -22,14 +22,14 @@ export const cloneCommand = new Command('clone')
22
22
 
23
23
  if (containers.length === 0) {
24
24
  console.log(
25
- warning('No containers found. Create one with: spindb create'),
25
+ uiWarning('No containers found. Create one with: spindb create'),
26
26
  )
27
27
  return
28
28
  }
29
29
 
30
30
  if (stopped.length === 0) {
31
31
  console.log(
32
- warning(
32
+ uiWarning(
33
33
  'All containers are running. Stop a container first to clone it.',
34
34
  ),
35
35
  )
@@ -51,7 +51,7 @@ export const cloneCommand = new Command('clone')
51
51
 
52
52
  const sourceConfig = await containerManager.getConfig(sourceName)
53
53
  if (!sourceConfig) {
54
- console.error(error(`Container "${sourceName}" not found`))
54
+ console.error(uiError(`Container "${sourceName}" not found`))
55
55
  process.exit(1)
56
56
  }
57
57
 
@@ -60,7 +60,7 @@ export const cloneCommand = new Command('clone')
60
60
  })
61
61
  if (running) {
62
62
  console.error(
63
- error(
63
+ uiError(
64
64
  `Container "${sourceName}" is running. Stop it first to clone.`,
65
65
  ),
66
66
  )
@@ -72,8 +72,12 @@ export const cloneCommand = new Command('clone')
72
72
  }
73
73
 
74
74
  // Check if target container already exists
75
- if (await containerManager.exists(targetName, { engine: sourceConfig.engine })) {
76
- console.error(error(`Container "${targetName}" already exists`))
75
+ if (
76
+ await containerManager.exists(targetName, {
77
+ engine: sourceConfig.engine,
78
+ })
79
+ ) {
80
+ console.error(uiError(`Container "${targetName}" already exists`))
77
81
  process.exit(1)
78
82
  }
79
83
 
@@ -95,9 +99,9 @@ export const cloneCommand = new Command('clone')
95
99
  console.log(chalk.gray(' Start the cloned container:'))
96
100
  console.log(chalk.cyan(` spindb start ${targetName}`))
97
101
  console.log()
98
- } catch (err) {
99
- const e = err as Error
100
- console.error(error(e.message))
102
+ } catch (error) {
103
+ const e = error as Error
104
+ console.error(uiError(e.message))
101
105
  process.exit(1)
102
106
  }
103
107
  })
@@ -9,7 +9,7 @@ import {
9
9
  ALL_TOOLS,
10
10
  } from '../../core/config-manager'
11
11
  import { updateManager } from '../../core/update-manager'
12
- import { error, success, header, info } from '../ui/theme'
12
+ import { uiError, uiSuccess, header, uiInfo } from '../ui/theme'
13
13
  import { createSpinner } from '../ui/spinner'
14
14
  import type { BinaryTool } from '../../types'
15
15
 
@@ -95,9 +95,9 @@ export const configCommand = new Command('config')
95
95
  )
96
96
  console.log()
97
97
  }
98
- } catch (err) {
99
- const e = err as Error
100
- console.error(error(e.message))
98
+ } catch (error) {
99
+ const e = error as Error
100
+ console.error(uiError(e.message))
101
101
  process.exit(1)
102
102
  }
103
103
  }),
@@ -208,9 +208,9 @@ export const configCommand = new Command('config')
208
208
  }
209
209
  console.log()
210
210
  }
211
- } catch (err) {
212
- const e = err as Error
213
- console.error(error(e.message))
211
+ } catch (error) {
212
+ const e = error as Error
213
+ console.error(uiError(e.message))
214
214
  process.exit(1)
215
215
  }
216
216
  }),
@@ -224,14 +224,14 @@ export const configCommand = new Command('config')
224
224
  try {
225
225
  // Validate tool name
226
226
  if (!ALL_TOOLS.includes(tool as BinaryTool)) {
227
- console.error(error(`Invalid tool: ${tool}`))
227
+ console.error(uiError(`Invalid tool: ${tool}`))
228
228
  console.log(chalk.gray(` Valid tools: ${ALL_TOOLS.join(', ')}`))
229
229
  process.exit(1)
230
230
  }
231
231
 
232
232
  // Validate path exists
233
233
  if (!existsSync(path)) {
234
- console.error(error(`File not found: ${path}`))
234
+ console.error(uiError(`File not found: ${path}`))
235
235
  process.exit(1)
236
236
  }
237
237
 
@@ -240,10 +240,10 @@ export const configCommand = new Command('config')
240
240
  const config = await configManager.getBinaryConfig(tool as BinaryTool)
241
241
  const versionLabel = config?.version ? ` (v${config.version})` : ''
242
242
 
243
- console.log(success(`Set ${tool} to: ${path}${versionLabel}`))
244
- } catch (err) {
245
- const e = err as Error
246
- console.error(error(e.message))
243
+ console.log(uiSuccess(`Set ${tool} to: ${path}${versionLabel}`))
244
+ } catch (error) {
245
+ const e = error as Error
246
+ console.error(uiError(e.message))
247
247
  process.exit(1)
248
248
  }
249
249
  }),
@@ -256,16 +256,16 @@ export const configCommand = new Command('config')
256
256
  try {
257
257
  // Validate tool name
258
258
  if (!ALL_TOOLS.includes(tool as BinaryTool)) {
259
- console.error(error(`Invalid tool: ${tool}`))
259
+ console.error(uiError(`Invalid tool: ${tool}`))
260
260
  console.log(chalk.gray(` Valid tools: ${ALL_TOOLS.join(', ')}`))
261
261
  process.exit(1)
262
262
  }
263
263
 
264
264
  await configManager.clearBinaryPath(tool as BinaryTool)
265
- console.log(success(`Removed ${tool} configuration`))
266
- } catch (err) {
267
- const e = err as Error
268
- console.error(error(e.message))
265
+ console.log(uiSuccess(`Removed ${tool} configuration`))
266
+ } catch (error) {
267
+ const e = error as Error
268
+ console.error(uiError(e.message))
269
269
  process.exit(1)
270
270
  }
271
271
  }),
@@ -278,7 +278,7 @@ export const configCommand = new Command('config')
278
278
  try {
279
279
  // Validate tool name
280
280
  if (!ALL_TOOLS.includes(tool as BinaryTool)) {
281
- console.error(error(`Invalid tool: ${tool}`))
281
+ console.error(uiError(`Invalid tool: ${tool}`))
282
282
  console.log(chalk.gray(` Valid tools: ${ALL_TOOLS.join(', ')}`))
283
283
  process.exit(1)
284
284
  }
@@ -287,15 +287,15 @@ export const configCommand = new Command('config')
287
287
  if (path) {
288
288
  console.log(path)
289
289
  } else {
290
- console.error(error(`${tool} not found`))
290
+ console.error(uiError(`${tool} not found`))
291
291
  console.log(
292
292
  chalk.gray(` Run 'spindb config detect' to auto-detect tools`),
293
293
  )
294
294
  process.exit(1)
295
295
  }
296
- } catch (err) {
297
- const e = err as Error
298
- console.error(error(e.message))
296
+ } catch (error) {
297
+ const e = error as Error
298
+ console.error(uiError(e.message))
299
299
  process.exit(1)
300
300
  }
301
301
  }),
@@ -328,7 +328,7 @@ export const configCommand = new Command('config')
328
328
  }
329
329
 
330
330
  if (state !== 'on' && state !== 'off') {
331
- console.error(error('Invalid state. Use "on" or "off"'))
331
+ console.error(uiError('Invalid state. Use "on" or "off"'))
332
332
  process.exit(1)
333
333
  }
334
334
 
@@ -336,18 +336,18 @@ export const configCommand = new Command('config')
336
336
  await updateManager.setAutoCheckEnabled(enabled)
337
337
 
338
338
  if (enabled) {
339
- console.log(success('Update checks enabled on startup'))
339
+ console.log(uiSuccess('Update checks enabled on startup'))
340
340
  } else {
341
- console.log(info('Update checks disabled on startup'))
341
+ console.log(uiInfo('Update checks disabled on startup'))
342
342
  console.log(
343
343
  chalk.gray(
344
344
  ' You can still manually check with: spindb version --check',
345
345
  ),
346
346
  )
347
347
  }
348
- } catch (err) {
349
- const e = err as Error
350
- console.error(error(e.message))
348
+ } catch (error) {
349
+ const e = error as Error
350
+ console.error(uiError(e.message))
351
351
  process.exit(1)
352
352
  }
353
353
  }),