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
|
@@ -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
|
+
})
|