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,434 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { readdir, lstat, rm } from 'fs/promises'
|
|
4
|
+
import { existsSync } from 'fs'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import { exec } from 'child_process'
|
|
7
|
+
import { promisify } from 'util'
|
|
8
|
+
import inquirer from 'inquirer'
|
|
9
|
+
import { paths } from '../../config/paths'
|
|
10
|
+
import { containerManager } from '../../core/container-manager'
|
|
11
|
+
import { promptConfirm } from '../ui/prompts'
|
|
12
|
+
import { createSpinner } from '../ui/spinner'
|
|
13
|
+
import { error, warning, info } from '../ui/theme'
|
|
14
|
+
import {
|
|
15
|
+
getMysqldPath,
|
|
16
|
+
getMysqlVersion,
|
|
17
|
+
isMariaDB,
|
|
18
|
+
} from '../../engines/mysql/binary-detection'
|
|
19
|
+
|
|
20
|
+
const execAsync = promisify(exec)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Installed engine info for PostgreSQL (downloaded binaries)
|
|
24
|
+
*/
|
|
25
|
+
type InstalledPostgresEngine = {
|
|
26
|
+
engine: 'postgresql'
|
|
27
|
+
version: string
|
|
28
|
+
platform: string
|
|
29
|
+
arch: string
|
|
30
|
+
path: string
|
|
31
|
+
sizeBytes: number
|
|
32
|
+
source: 'downloaded'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Installed engine info for MySQL (system-installed)
|
|
37
|
+
*/
|
|
38
|
+
type InstalledMysqlEngine = {
|
|
39
|
+
engine: 'mysql'
|
|
40
|
+
version: string
|
|
41
|
+
path: string
|
|
42
|
+
source: 'system'
|
|
43
|
+
isMariaDB: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type InstalledEngine = InstalledPostgresEngine | InstalledMysqlEngine
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the actual PostgreSQL version from the binary
|
|
50
|
+
*/
|
|
51
|
+
async function getPostgresVersion(binPath: string): Promise<string | null> {
|
|
52
|
+
const postgresPath = join(binPath, 'bin', 'postgres')
|
|
53
|
+
if (!existsSync(postgresPath)) {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const { stdout } = await execAsync(`"${postgresPath}" --version`)
|
|
59
|
+
// Output: postgres (PostgreSQL) 17.7
|
|
60
|
+
const match = stdout.match(/\(PostgreSQL\)\s+([\d.]+)/)
|
|
61
|
+
return match ? match[1] : null
|
|
62
|
+
} catch {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get installed PostgreSQL engines from ~/.spindb/bin/
|
|
69
|
+
*/
|
|
70
|
+
async function getInstalledPostgresEngines(): Promise<
|
|
71
|
+
InstalledPostgresEngine[]
|
|
72
|
+
> {
|
|
73
|
+
const binDir = paths.bin
|
|
74
|
+
|
|
75
|
+
if (!existsSync(binDir)) {
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const entries = await readdir(binDir, { withFileTypes: true })
|
|
80
|
+
const engines: InstalledPostgresEngine[] = []
|
|
81
|
+
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
// Parse directory name: postgresql-17-darwin-arm64
|
|
85
|
+
const match = entry.name.match(/^(\w+)-(.+)-(\w+)-(\w+)$/)
|
|
86
|
+
if (match && match[1] === 'postgresql') {
|
|
87
|
+
const [, , majorVersion, platform, arch] = match
|
|
88
|
+
const dirPath = join(binDir, entry.name)
|
|
89
|
+
|
|
90
|
+
// Get actual version from the binary
|
|
91
|
+
const actualVersion =
|
|
92
|
+
(await getPostgresVersion(dirPath)) || majorVersion
|
|
93
|
+
|
|
94
|
+
// Get directory size
|
|
95
|
+
let sizeBytes = 0
|
|
96
|
+
try {
|
|
97
|
+
const files = await readdir(dirPath, { recursive: true })
|
|
98
|
+
for (const file of files) {
|
|
99
|
+
try {
|
|
100
|
+
const filePath = join(dirPath, file.toString())
|
|
101
|
+
const fileStat = await lstat(filePath)
|
|
102
|
+
if (fileStat.isFile()) {
|
|
103
|
+
sizeBytes += fileStat.size
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Skip files we can't stat
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Skip directories we can't read
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
engines.push({
|
|
114
|
+
engine: 'postgresql',
|
|
115
|
+
version: actualVersion,
|
|
116
|
+
platform,
|
|
117
|
+
arch,
|
|
118
|
+
path: dirPath,
|
|
119
|
+
sizeBytes,
|
|
120
|
+
source: 'downloaded',
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Sort by version descending
|
|
127
|
+
engines.sort((a, b) => compareVersions(b.version, a.version))
|
|
128
|
+
|
|
129
|
+
return engines
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Detect system-installed MySQL
|
|
134
|
+
*/
|
|
135
|
+
async function getInstalledMysqlEngine(): Promise<InstalledMysqlEngine | null> {
|
|
136
|
+
const mysqldPath = await getMysqldPath()
|
|
137
|
+
if (!mysqldPath) {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const version = await getMysqlVersion(mysqldPath)
|
|
142
|
+
if (!version) {
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const mariadb = await isMariaDB()
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
engine: 'mysql',
|
|
150
|
+
version,
|
|
151
|
+
path: mysqldPath,
|
|
152
|
+
source: 'system',
|
|
153
|
+
isMariaDB: mariadb,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get all installed engines (PostgreSQL + MySQL)
|
|
159
|
+
*/
|
|
160
|
+
async function getInstalledEngines(): Promise<InstalledEngine[]> {
|
|
161
|
+
const engines: InstalledEngine[] = []
|
|
162
|
+
|
|
163
|
+
// Get PostgreSQL engines
|
|
164
|
+
const pgEngines = await getInstalledPostgresEngines()
|
|
165
|
+
engines.push(...pgEngines)
|
|
166
|
+
|
|
167
|
+
// Get MySQL engine
|
|
168
|
+
const mysqlEngine = await getInstalledMysqlEngine()
|
|
169
|
+
if (mysqlEngine) {
|
|
170
|
+
engines.push(mysqlEngine)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return engines
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function compareVersions(a: string, b: string): number {
|
|
177
|
+
const partsA = a.split('.').map((p) => parseInt(p, 10) || 0)
|
|
178
|
+
const partsB = b.split('.').map((p) => parseInt(p, 10) || 0)
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
181
|
+
const numA = partsA[i] || 0
|
|
182
|
+
const numB = partsB[i] || 0
|
|
183
|
+
if (numA !== numB) return numA - numB
|
|
184
|
+
}
|
|
185
|
+
return 0
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function formatBytes(bytes: number): string {
|
|
189
|
+
if (bytes === 0) return '0 B'
|
|
190
|
+
const k = 1024
|
|
191
|
+
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
192
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
193
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Engine icons
|
|
198
|
+
*/
|
|
199
|
+
const engineIcons: Record<string, string> = {
|
|
200
|
+
postgresql: '🐘',
|
|
201
|
+
mysql: '🐬',
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* List subcommand action
|
|
206
|
+
*/
|
|
207
|
+
async function listEngines(options: { json?: boolean }): Promise<void> {
|
|
208
|
+
const engines = await getInstalledEngines()
|
|
209
|
+
|
|
210
|
+
if (options.json) {
|
|
211
|
+
console.log(JSON.stringify(engines, null, 2))
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (engines.length === 0) {
|
|
216
|
+
console.log(info('No engines installed yet.'))
|
|
217
|
+
console.log(
|
|
218
|
+
chalk.gray(
|
|
219
|
+
' PostgreSQL engines are downloaded automatically when you create a container.',
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
console.log(
|
|
223
|
+
chalk.gray(
|
|
224
|
+
' MySQL requires system installation (brew install mysql or apt install mysql-server).',
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Separate PostgreSQL and MySQL
|
|
231
|
+
const pgEngines = engines.filter(
|
|
232
|
+
(e): e is InstalledPostgresEngine => e.engine === 'postgresql',
|
|
233
|
+
)
|
|
234
|
+
const mysqlEngine = engines.find(
|
|
235
|
+
(e): e is InstalledMysqlEngine => e.engine === 'mysql',
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// Calculate total size for PostgreSQL
|
|
239
|
+
const totalPgSize = pgEngines.reduce((acc, e) => acc + e.sizeBytes, 0)
|
|
240
|
+
|
|
241
|
+
// Table header
|
|
242
|
+
console.log()
|
|
243
|
+
console.log(
|
|
244
|
+
chalk.gray(' ') +
|
|
245
|
+
chalk.bold.white('ENGINE'.padEnd(14)) +
|
|
246
|
+
chalk.bold.white('VERSION'.padEnd(12)) +
|
|
247
|
+
chalk.bold.white('SOURCE'.padEnd(18)) +
|
|
248
|
+
chalk.bold.white('SIZE'),
|
|
249
|
+
)
|
|
250
|
+
console.log(chalk.gray(' ' + '─'.repeat(55)))
|
|
251
|
+
|
|
252
|
+
// PostgreSQL rows
|
|
253
|
+
for (const engine of pgEngines) {
|
|
254
|
+
const icon = engineIcons[engine.engine] || '▣'
|
|
255
|
+
const platformInfo = `${engine.platform}-${engine.arch}`
|
|
256
|
+
|
|
257
|
+
console.log(
|
|
258
|
+
chalk.gray(' ') +
|
|
259
|
+
chalk.cyan(`${icon} ${engine.engine}`.padEnd(13)) +
|
|
260
|
+
chalk.yellow(engine.version.padEnd(12)) +
|
|
261
|
+
chalk.gray(platformInfo.padEnd(18)) +
|
|
262
|
+
chalk.white(formatBytes(engine.sizeBytes)),
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// MySQL row
|
|
267
|
+
if (mysqlEngine) {
|
|
268
|
+
const icon = engineIcons.mysql
|
|
269
|
+
const displayName = mysqlEngine.isMariaDB ? 'mariadb' : 'mysql'
|
|
270
|
+
|
|
271
|
+
console.log(
|
|
272
|
+
chalk.gray(' ') +
|
|
273
|
+
chalk.cyan(`${icon} ${displayName}`.padEnd(13)) +
|
|
274
|
+
chalk.yellow(mysqlEngine.version.padEnd(12)) +
|
|
275
|
+
chalk.gray('system'.padEnd(18)) +
|
|
276
|
+
chalk.gray('(system-installed)'),
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
console.log(chalk.gray(' ' + '─'.repeat(55)))
|
|
281
|
+
|
|
282
|
+
// Summary
|
|
283
|
+
console.log()
|
|
284
|
+
if (pgEngines.length > 0) {
|
|
285
|
+
console.log(
|
|
286
|
+
chalk.gray(
|
|
287
|
+
` PostgreSQL: ${pgEngines.length} version(s), ${formatBytes(totalPgSize)}`,
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
if (mysqlEngine) {
|
|
292
|
+
console.log(chalk.gray(` MySQL: system-installed at ${mysqlEngine.path}`))
|
|
293
|
+
}
|
|
294
|
+
console.log()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Delete subcommand action
|
|
299
|
+
*/
|
|
300
|
+
async function deleteEngine(
|
|
301
|
+
engine: string | undefined,
|
|
302
|
+
version: string | undefined,
|
|
303
|
+
options: { yes?: boolean },
|
|
304
|
+
): Promise<void> {
|
|
305
|
+
// Get PostgreSQL engines only (MySQL can't be deleted via spindb)
|
|
306
|
+
const pgEngines = await getInstalledPostgresEngines()
|
|
307
|
+
|
|
308
|
+
if (pgEngines.length === 0) {
|
|
309
|
+
console.log(warning('No deletable engines found.'))
|
|
310
|
+
console.log(
|
|
311
|
+
chalk.gray(
|
|
312
|
+
' MySQL is system-installed and cannot be deleted via spindb.',
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let engineName = engine
|
|
319
|
+
let engineVersion = version
|
|
320
|
+
|
|
321
|
+
// Interactive selection if not provided
|
|
322
|
+
if (!engineName || !engineVersion) {
|
|
323
|
+
const choices = pgEngines.map((e) => ({
|
|
324
|
+
name: `${engineIcons[e.engine]} ${e.engine} ${e.version} ${chalk.gray(`(${formatBytes(e.sizeBytes)})`)}`,
|
|
325
|
+
value: `${e.engine}:${e.version}:${e.path}`,
|
|
326
|
+
}))
|
|
327
|
+
|
|
328
|
+
const { selected } = await inquirer.prompt<{ selected: string }>([
|
|
329
|
+
{
|
|
330
|
+
type: 'list',
|
|
331
|
+
name: 'selected',
|
|
332
|
+
message: 'Select engine to delete:',
|
|
333
|
+
choices,
|
|
334
|
+
},
|
|
335
|
+
])
|
|
336
|
+
|
|
337
|
+
const [eng, ver] = selected.split(':')
|
|
338
|
+
engineName = eng
|
|
339
|
+
engineVersion = ver
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Find the engine
|
|
343
|
+
const targetEngine = pgEngines.find(
|
|
344
|
+
(e) => e.engine === engineName && e.version === engineVersion,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if (!targetEngine) {
|
|
348
|
+
console.error(error(`Engine "${engineName} ${engineVersion}" not found`))
|
|
349
|
+
process.exit(1)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Check if any containers are using this engine version
|
|
353
|
+
const containers = await containerManager.list()
|
|
354
|
+
const usingContainers = containers.filter(
|
|
355
|
+
(c) => c.engine === engineName && c.version === engineVersion,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if (usingContainers.length > 0) {
|
|
359
|
+
console.error(
|
|
360
|
+
error(
|
|
361
|
+
`Cannot delete: ${usingContainers.length} container(s) are using ${engineName} ${engineVersion}`,
|
|
362
|
+
),
|
|
363
|
+
)
|
|
364
|
+
console.log(
|
|
365
|
+
chalk.gray(
|
|
366
|
+
` Containers: ${usingContainers.map((c) => c.name).join(', ')}`,
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
console.log()
|
|
370
|
+
console.log(chalk.gray(' Delete these containers first, then try again.'))
|
|
371
|
+
process.exit(1)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Confirm deletion
|
|
375
|
+
if (!options.yes) {
|
|
376
|
+
const confirmed = await promptConfirm(
|
|
377
|
+
`Delete ${engineName} ${engineVersion}? This cannot be undone.`,
|
|
378
|
+
false,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if (!confirmed) {
|
|
382
|
+
console.log(warning('Deletion cancelled'))
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Delete the engine
|
|
388
|
+
const spinner = createSpinner(`Deleting ${engineName} ${engineVersion}...`)
|
|
389
|
+
spinner.start()
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
await rm(targetEngine.path, { recursive: true, force: true })
|
|
393
|
+
spinner.succeed(`Deleted ${engineName} ${engineVersion}`)
|
|
394
|
+
} catch (err) {
|
|
395
|
+
const e = err as Error
|
|
396
|
+
spinner.fail(`Failed to delete: ${e.message}`)
|
|
397
|
+
process.exit(1)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Main engines command
|
|
402
|
+
export const enginesCommand = new Command('engines')
|
|
403
|
+
.description('Manage installed database engines')
|
|
404
|
+
.option('--json', 'Output as JSON')
|
|
405
|
+
.action(async (options: { json?: boolean }) => {
|
|
406
|
+
try {
|
|
407
|
+
await listEngines(options)
|
|
408
|
+
} catch (err) {
|
|
409
|
+
const e = err as Error
|
|
410
|
+
console.error(error(e.message))
|
|
411
|
+
process.exit(1)
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// Delete subcommand
|
|
416
|
+
enginesCommand
|
|
417
|
+
.command('delete [engine] [version]')
|
|
418
|
+
.description('Delete an installed engine version')
|
|
419
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
420
|
+
.action(
|
|
421
|
+
async (
|
|
422
|
+
engine: string | undefined,
|
|
423
|
+
version: string | undefined,
|
|
424
|
+
options: { yes?: boolean },
|
|
425
|
+
) => {
|
|
426
|
+
try {
|
|
427
|
+
await deleteEngine(engine, version, options)
|
|
428
|
+
} catch (err) {
|
|
429
|
+
const e = err as Error
|
|
430
|
+
console.error(error(e.message))
|
|
431
|
+
process.exit(1)
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
)
|
|
@@ -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
|
+
})
|
package/cli/commands/list.ts
CHANGED
|
@@ -48,7 +48,7 @@ export const listCommand = new Command('list')
|
|
|
48
48
|
? chalk.green('● running')
|
|
49
49
|
: chalk.gray('○ stopped')
|
|
50
50
|
|
|
51
|
-
const engineIcon = engineIcons[container.engine] || '
|
|
51
|
+
const engineIcon = engineIcons[container.engine] || '▣'
|
|
52
52
|
const engineDisplay = `${engineIcon} ${container.engine}`
|
|
53
53
|
|
|
54
54
|
console.log(
|