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.
Files changed (36) hide show
  1. package/README.md +137 -8
  2. package/cli/commands/connect.ts +8 -4
  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/menu.ts +408 -153
  9. package/cli/commands/restore.ts +10 -24
  10. package/cli/commands/start.ts +25 -20
  11. package/cli/commands/url.ts +79 -0
  12. package/cli/index.ts +9 -3
  13. package/cli/ui/prompts.ts +8 -6
  14. package/config/engine-defaults.ts +24 -1
  15. package/config/os-dependencies.ts +59 -113
  16. package/config/paths.ts +7 -36
  17. package/core/binary-manager.ts +19 -6
  18. package/core/config-manager.ts +17 -5
  19. package/core/dependency-manager.ts +9 -15
  20. package/core/error-handler.ts +336 -0
  21. package/core/platform-service.ts +634 -0
  22. package/core/port-manager.ts +11 -3
  23. package/core/process-manager.ts +12 -2
  24. package/core/start-with-retry.ts +167 -0
  25. package/core/transaction-manager.ts +170 -0
  26. package/engines/mysql/binary-detection.ts +177 -100
  27. package/engines/mysql/index.ts +240 -131
  28. package/engines/mysql/restore.ts +257 -0
  29. package/engines/mysql/version-validator.ts +373 -0
  30. package/{core/postgres-binary-manager.ts → engines/postgresql/binary-manager.ts} +63 -23
  31. package/engines/postgresql/binary-urls.ts +5 -3
  32. package/engines/postgresql/index.ts +4 -3
  33. package/engines/postgresql/restore.ts +54 -5
  34. package/engines/postgresql/version-validator.ts +262 -0
  35. package/package.json +6 -2
  36. package/cli/commands/postgres-tools.ts +0 -216
@@ -0,0 +1,279 @@
1
+ import { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { containerManager } from '../../core/container-manager'
4
+ import { processManager } from '../../core/process-manager'
5
+ import { paths } from '../../config/paths'
6
+ import { getEngine } from '../../engines'
7
+ import { error, info, header } from '../ui/theme'
8
+ import type { ContainerConfig } from '../../types'
9
+
10
+ /**
11
+ * Engine icons
12
+ */
13
+ const engineIcons: Record<string, string> = {
14
+ postgresql: '🐘',
15
+ mysql: '🐬',
16
+ }
17
+
18
+ /**
19
+ * Format a date for display
20
+ */
21
+ function formatDate(dateString: string): string {
22
+ const date = new Date(dateString)
23
+ return date.toLocaleString()
24
+ }
25
+
26
+ /**
27
+ * Get actual running status (not just config status)
28
+ */
29
+ async function getActualStatus(
30
+ config: ContainerConfig,
31
+ ): Promise<'running' | 'stopped'> {
32
+ const running = await processManager.isRunning(config.name, {
33
+ engine: config.engine,
34
+ })
35
+ return running ? 'running' : 'stopped'
36
+ }
37
+
38
+ /**
39
+ * Display info for a single container
40
+ */
41
+ async function displayContainerInfo(
42
+ config: ContainerConfig,
43
+ options: { json?: boolean },
44
+ ): Promise<void> {
45
+ const actualStatus = await getActualStatus(config)
46
+ const engine = getEngine(config.engine)
47
+ const connectionString = engine.getConnectionString(config)
48
+ const dataDir = paths.getContainerDataPath(config.name, {
49
+ engine: config.engine,
50
+ })
51
+
52
+ if (options.json) {
53
+ console.log(
54
+ JSON.stringify(
55
+ {
56
+ ...config,
57
+ status: actualStatus,
58
+ connectionString,
59
+ dataDir,
60
+ },
61
+ null,
62
+ 2,
63
+ ),
64
+ )
65
+ return
66
+ }
67
+
68
+ const icon = engineIcons[config.engine] || '🗄️'
69
+ const statusDisplay =
70
+ actualStatus === 'running'
71
+ ? chalk.green('● running')
72
+ : chalk.gray('○ stopped')
73
+
74
+ console.log()
75
+ console.log(header(`Container: ${config.name}`))
76
+ console.log()
77
+ console.log(
78
+ chalk.gray(' ') +
79
+ chalk.white('Engine:'.padEnd(14)) +
80
+ chalk.cyan(`${icon} ${config.engine} ${config.version}`),
81
+ )
82
+ console.log(
83
+ chalk.gray(' ') + chalk.white('Status:'.padEnd(14)) + statusDisplay,
84
+ )
85
+ console.log(
86
+ chalk.gray(' ') +
87
+ chalk.white('Port:'.padEnd(14)) +
88
+ chalk.green(String(config.port)),
89
+ )
90
+ console.log(
91
+ chalk.gray(' ') +
92
+ chalk.white('Database:'.padEnd(14)) +
93
+ chalk.yellow(config.database),
94
+ )
95
+ console.log(
96
+ chalk.gray(' ') +
97
+ chalk.white('Created:'.padEnd(14)) +
98
+ chalk.gray(formatDate(config.created)),
99
+ )
100
+ console.log(
101
+ chalk.gray(' ') +
102
+ chalk.white('Data Dir:'.padEnd(14)) +
103
+ chalk.gray(dataDir),
104
+ )
105
+ if (config.clonedFrom) {
106
+ console.log(
107
+ chalk.gray(' ') +
108
+ chalk.white('Cloned From:'.padEnd(14)) +
109
+ chalk.gray(config.clonedFrom),
110
+ )
111
+ }
112
+ console.log()
113
+ console.log(chalk.gray(' ') + chalk.white('Connection String:'))
114
+ console.log(chalk.gray(' ') + chalk.cyan(connectionString))
115
+ console.log()
116
+ }
117
+
118
+ /**
119
+ * Display summary info for all containers
120
+ */
121
+ async function displayAllContainersInfo(
122
+ containers: ContainerConfig[],
123
+ options: { json?: boolean },
124
+ ): Promise<void> {
125
+ if (options.json) {
126
+ // Get actual status for all containers
127
+ const containersWithStatus = await Promise.all(
128
+ containers.map(async (config) => {
129
+ const actualStatus = await getActualStatus(config)
130
+ const engine = getEngine(config.engine)
131
+ const connectionString = engine.getConnectionString(config)
132
+ const dataDir = paths.getContainerDataPath(config.name, {
133
+ engine: config.engine,
134
+ })
135
+ return {
136
+ ...config,
137
+ status: actualStatus,
138
+ connectionString,
139
+ dataDir,
140
+ }
141
+ }),
142
+ )
143
+ console.log(JSON.stringify(containersWithStatus, null, 2))
144
+ return
145
+ }
146
+
147
+ console.log()
148
+ console.log(header('All Containers'))
149
+ console.log()
150
+
151
+ // Table header
152
+ console.log(
153
+ chalk.gray(' ') +
154
+ chalk.bold.white('NAME'.padEnd(18)) +
155
+ chalk.bold.white('ENGINE'.padEnd(14)) +
156
+ chalk.bold.white('VERSION'.padEnd(10)) +
157
+ chalk.bold.white('PORT'.padEnd(8)) +
158
+ chalk.bold.white('DATABASE'.padEnd(16)) +
159
+ chalk.bold.white('STATUS'),
160
+ )
161
+ console.log(chalk.gray(' ' + '─'.repeat(78)))
162
+
163
+ // Table rows
164
+ for (const container of containers) {
165
+ const actualStatus = await getActualStatus(container)
166
+ const statusDisplay =
167
+ actualStatus === 'running'
168
+ ? chalk.green('● running')
169
+ : chalk.gray('○ stopped')
170
+
171
+ const icon = engineIcons[container.engine] || '🗄️'
172
+ const engineDisplay = `${icon} ${container.engine}`
173
+
174
+ console.log(
175
+ chalk.gray(' ') +
176
+ chalk.cyan(container.name.padEnd(18)) +
177
+ chalk.white(engineDisplay.padEnd(13)) +
178
+ chalk.yellow(container.version.padEnd(10)) +
179
+ chalk.green(String(container.port).padEnd(8)) +
180
+ chalk.gray(container.database.padEnd(16)) +
181
+ statusDisplay,
182
+ )
183
+ }
184
+
185
+ console.log()
186
+
187
+ // Summary
188
+ const statusChecks = await Promise.all(
189
+ containers.map((c) => getActualStatus(c)),
190
+ )
191
+ const running = statusChecks.filter((s) => s === 'running').length
192
+ const stopped = statusChecks.filter((s) => s === 'stopped').length
193
+
194
+ console.log(
195
+ chalk.gray(
196
+ ` ${containers.length} container(s): ${running} running, ${stopped} stopped`,
197
+ ),
198
+ )
199
+ console.log()
200
+
201
+ // Connection strings
202
+ console.log(chalk.bold.white(' Connection Strings:'))
203
+ console.log(chalk.gray(' ' + '─'.repeat(78)))
204
+ for (const container of containers) {
205
+ const engine = getEngine(container.engine)
206
+ const connectionString = engine.getConnectionString(container)
207
+ console.log(
208
+ chalk.gray(' ') +
209
+ chalk.cyan(container.name.padEnd(18)) +
210
+ chalk.gray(connectionString),
211
+ )
212
+ }
213
+ console.log()
214
+ }
215
+
216
+ export const infoCommand = new Command('info')
217
+ .description('Show container details')
218
+ .argument('[name]', 'Container name (omit to show all)')
219
+ .option('--json', 'Output as JSON')
220
+ .action(async (name: string | undefined, options: { json?: boolean }) => {
221
+ try {
222
+ const containers = await containerManager.list()
223
+
224
+ if (containers.length === 0) {
225
+ console.log(info('No containers found. Create one with: spindb create'))
226
+ return
227
+ }
228
+
229
+ // If name provided, show single container
230
+ if (name) {
231
+ const config = await containerManager.getConfig(name)
232
+ if (!config) {
233
+ console.error(error(`Container "${name}" not found`))
234
+ process.exit(1)
235
+ }
236
+ await displayContainerInfo(config, options)
237
+ return
238
+ }
239
+
240
+ // If running interactively without name, ask if they want all or specific
241
+ if (!options.json && process.stdout.isTTY && containers.length > 1) {
242
+ const { choice } = await (
243
+ await import('inquirer')
244
+ ).default.prompt<{
245
+ choice: string
246
+ }>([
247
+ {
248
+ type: 'list',
249
+ name: 'choice',
250
+ message: 'Show info for:',
251
+ choices: [
252
+ { name: 'All containers', value: 'all' },
253
+ ...containers.map((c) => ({
254
+ name: `${c.name} ${chalk.gray(`(${engineIcons[c.engine] || '🗄️'} ${c.engine})`)}`,
255
+ value: c.name,
256
+ })),
257
+ ],
258
+ },
259
+ ])
260
+
261
+ if (choice === 'all') {
262
+ await displayAllContainersInfo(containers, options)
263
+ } else {
264
+ const config = await containerManager.getConfig(choice)
265
+ if (config) {
266
+ await displayContainerInfo(config, options)
267
+ }
268
+ }
269
+ return
270
+ }
271
+
272
+ // Non-interactive or only one container: show all
273
+ await displayAllContainersInfo(containers, options)
274
+ } catch (err) {
275
+ const e = err as Error
276
+ console.error(error(e.message))
277
+ process.exit(1)
278
+ }
279
+ })