sealos-cli 0.1.0

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.
@@ -0,0 +1,881 @@
1
+ import { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { createDatabaseClient } from '../../lib/api-client.ts'
4
+ import { type ApiErrorBody, mapApiError } from '../../lib/errors.ts'
5
+ import { outputJson, outputTable } from '../../lib/output.ts'
6
+ import { withAuth, withErrorHandling } from '../../lib/with-auth.ts'
7
+
8
+ const SUPPORTED_DATABASE_TYPES = [
9
+ 'postgresql',
10
+ 'mongodb',
11
+ 'apecloud-mysql',
12
+ 'mysql',
13
+ 'redis',
14
+ 'kafka',
15
+ 'qdrant',
16
+ 'nebula',
17
+ 'weaviate',
18
+ 'milvus',
19
+ 'pulsar',
20
+ 'clickhouse'
21
+ ] as const
22
+
23
+ const SUPPORTED_LOG_DB_TYPES = [
24
+ 'postgresql',
25
+ 'mongodb',
26
+ 'mysql',
27
+ 'redis'
28
+ ] as const
29
+
30
+ const SUPPORTED_LOG_TYPES = [
31
+ 'runtimeLog',
32
+ 'slowQuery',
33
+ 'errorLog'
34
+ ] as const
35
+
36
+ type DatabaseType = typeof SUPPORTED_DATABASE_TYPES[number]
37
+ type LogDbType = typeof SUPPORTED_LOG_DB_TYPES[number]
38
+ type LogType = typeof SUPPORTED_LOG_TYPES[number]
39
+
40
+ function collectOption (value: string, previous: string[]): string[] {
41
+ return [...previous, value]
42
+ }
43
+
44
+ function parseKeyValueArgs (pairs: string[]): Record<string, string> {
45
+ const values: Record<string, string> = {}
46
+ for (const pair of pairs) {
47
+ const index = pair.indexOf('=')
48
+ if (index === -1) {
49
+ throw new Error(`Invalid KEY=VALUE format: "${pair}"`)
50
+ }
51
+ values[pair.slice(0, index)] = pair.slice(index + 1)
52
+ }
53
+ return values
54
+ }
55
+
56
+ function normalizeDatabaseType (type: string): DatabaseType {
57
+ const normalized = type.trim().toLowerCase()
58
+ const aliases: Record<string, DatabaseType> = {
59
+ postgres: 'postgresql',
60
+ postgresql: 'postgresql',
61
+ mongo: 'mongodb',
62
+ mongodb: 'mongodb'
63
+ }
64
+
65
+ const resolved = aliases[normalized] || (normalized as DatabaseType)
66
+ if (!SUPPORTED_DATABASE_TYPES.includes(resolved)) {
67
+ throw new Error(`Unsupported database type "${type}"`)
68
+ }
69
+
70
+ return resolved
71
+ }
72
+
73
+ function normalizeLogDbType (type: string): LogDbType {
74
+ const normalized = normalizeDatabaseType(type)
75
+ if (!SUPPORTED_LOG_DB_TYPES.includes(normalized as LogDbType)) {
76
+ throw new Error(`Logs API only supports db types: ${SUPPORTED_LOG_DB_TYPES.join(', ')}`)
77
+ }
78
+
79
+ return normalized as LogDbType
80
+ }
81
+
82
+ function normalizeLogType (type: string): LogType {
83
+ const normalized = type.trim() as LogType
84
+ if (!SUPPORTED_LOG_TYPES.includes(normalized)) {
85
+ throw new Error(`Unsupported log type "${type}". Use one of: ${SUPPORTED_LOG_TYPES.join(', ')}`)
86
+ }
87
+
88
+ return normalized
89
+ }
90
+
91
+ function parseNumericValue (value: string, field: string): number {
92
+ const normalized = value.trim().toLowerCase()
93
+ let raw = normalized
94
+
95
+ if (field === 'cpu' && normalized.endsWith('c')) {
96
+ raw = normalized.slice(0, -1)
97
+ }
98
+
99
+ if ((field === 'memory' || field === 'storage') && /gi?$|gb$|g$/i.test(normalized)) {
100
+ raw = normalized.replace(/gi?$|gb$|g$/i, '')
101
+ }
102
+
103
+ const parsed = Number(raw)
104
+ if (!Number.isFinite(parsed)) {
105
+ throw new Error(`Invalid ${field} value "${value}"`)
106
+ }
107
+
108
+ return parsed
109
+ }
110
+
111
+ function parseIntegerValue (value: string, field: string): number {
112
+ const parsed = parseNumericValue(value, field)
113
+ if (!Number.isInteger(parsed)) {
114
+ throw new Error(`${field} must be an integer`)
115
+ }
116
+ return parsed
117
+ }
118
+
119
+ function buildQuota (options: { cpu?: string; memory?: string; storage?: string; replicas?: string }): Record<string, number> {
120
+ const quota: Record<string, number> = {}
121
+
122
+ if (options.cpu !== undefined) quota.cpu = parseNumericValue(options.cpu, 'cpu')
123
+ if (options.memory !== undefined) quota.memory = parseNumericValue(options.memory, 'memory')
124
+ if (options.storage !== undefined) quota.storage = parseNumericValue(options.storage, 'storage')
125
+ if (options.replicas !== undefined) quota.replicas = parseIntegerValue(options.replicas, 'replicas')
126
+
127
+ return quota
128
+ }
129
+
130
+ function buildAutoBackup (options: {
131
+ backupStart?: boolean
132
+ backupType?: string
133
+ backupWeek: string[]
134
+ backupHour?: string
135
+ backupMinute?: string
136
+ backupSaveTime?: string
137
+ backupSaveType?: string
138
+ }): Record<string, unknown> | undefined {
139
+ const autoBackup: Record<string, unknown> = {}
140
+
141
+ if (options.backupStart) autoBackup.start = true
142
+ if (options.backupType) autoBackup.type = options.backupType
143
+ if (options.backupWeek.length > 0) autoBackup.week = options.backupWeek
144
+ if (options.backupHour) autoBackup.hour = options.backupHour
145
+ if (options.backupMinute) autoBackup.minute = options.backupMinute
146
+ if (options.backupSaveTime) autoBackup.saveTime = parseIntegerValue(options.backupSaveTime, 'backup-save-time')
147
+ if (options.backupSaveType) autoBackup.saveType = options.backupSaveType
148
+
149
+ return Object.keys(autoBackup).length > 0 ? autoBackup : undefined
150
+ }
151
+
152
+ function formatValue (value: unknown): string {
153
+ if (value === undefined || value === null || value === '') return '-'
154
+ return String(value)
155
+ }
156
+
157
+ function summarizeVersions (versions: string[]): string {
158
+ if (versions.length <= 3) return versions.join(', ')
159
+ return `${versions.slice(0, 3).join(', ')} ... (${versions.length} total)`
160
+ }
161
+
162
+ function extractVersionsMap (payload: any): Record<string, string[]> {
163
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
164
+ if (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
165
+ return payload.data as Record<string, string[]>
166
+ }
167
+
168
+ return payload as Record<string, string[]>
169
+ }
170
+
171
+ throw new Error('Unexpected versions response shape')
172
+ }
173
+
174
+ function printDatabaseDetail (database: any): void {
175
+ console.log(chalk.bold(`\n ${database.name}\n`))
176
+ console.log(` ${chalk.dim('Type:')} ${formatValue(database.type)}`)
177
+ console.log(` ${chalk.dim('Version:')} ${formatValue(database.version)}`)
178
+ console.log(` ${chalk.dim('Status:')} ${formatValue(database.status)}`)
179
+ console.log(` ${chalk.dim('Created:')} ${formatValue(database.createdAt)}`)
180
+ console.log(` ${chalk.dim('Termination policy:')} ${formatValue(database.terminationPolicy)}`)
181
+ console.log(` ${chalk.dim('UID:')} ${formatValue(database.uid)}`)
182
+ console.log(` ${chalk.dim('Resource type:')} ${formatValue(database.resourceType)}`)
183
+
184
+ if (database.quota) {
185
+ console.log(`\n ${chalk.dim('Resources:')}`)
186
+ console.log(` CPU: ${formatValue(database.quota.cpu)} core(s)`)
187
+ console.log(` Memory: ${formatValue(database.quota.memory)} GB`)
188
+ console.log(` Storage: ${formatValue(database.quota.storage)} GB`)
189
+ console.log(` Replicas: ${formatValue(database.quota.replicas)}`)
190
+ }
191
+
192
+ if (database.connection) {
193
+ const privateReady = database.connection.privateConnection != null
194
+ const publicReady = database.connection.publicConnection != null
195
+ console.log(`\n ${chalk.dim('Connectivity:')}`)
196
+ console.log(` Private: ${privateReady ? 'available' : 'not ready'}`)
197
+ console.log(` Public: ${publicReady ? 'enabled' : 'disabled'}`)
198
+ }
199
+
200
+ if (database.autoBackup) {
201
+ console.log(`\n ${chalk.dim('Auto backup:')}`)
202
+ console.log(` Enabled: ${database.autoBackup.start ? 'yes' : 'no'}`)
203
+ if (database.autoBackup.type) {
204
+ console.log(` Schedule: ${database.autoBackup.type}`)
205
+ }
206
+ if (database.autoBackup.week?.length) {
207
+ console.log(` Weekdays: ${database.autoBackup.week.join(', ')}`)
208
+ }
209
+ if (database.autoBackup.hour || database.autoBackup.minute) {
210
+ console.log(` Time: ${formatValue(database.autoBackup.hour)}:${formatValue(database.autoBackup.minute)}`)
211
+ }
212
+ if (database.autoBackup.saveTime || database.autoBackup.saveType) {
213
+ console.log(` Retention: ${formatValue(database.autoBackup.saveTime)} ${formatValue(database.autoBackup.saveType)}`)
214
+ }
215
+ }
216
+
217
+ const params = database.parameterConfig ? Object.entries(database.parameterConfig).filter(([, value]) => value !== undefined && value !== null && value !== '') : []
218
+ if (params.length > 0) {
219
+ console.log(`\n ${chalk.dim('Parameters:')}`)
220
+ const rows: string[][] = [[chalk.bold('Key'), chalk.bold('Value')]]
221
+ for (const [key, value] of params) {
222
+ rows.push([key, formatValue(value)])
223
+ }
224
+ outputTable(rows)
225
+ }
226
+
227
+ if (database.pods?.length > 0) {
228
+ console.log(`\n ${chalk.dim('Pods:')}`)
229
+ const rows: string[][] = [[chalk.bold('Name'), chalk.bold('Status')]]
230
+ for (const pod of database.pods) {
231
+ rows.push([formatValue(pod.name), formatValue(pod.status)])
232
+ }
233
+ outputTable(rows)
234
+ }
235
+ }
236
+
237
+ function printConnectionDetail (connection: any): void {
238
+ const rows: string[][] = [[chalk.bold('Field'), chalk.bold('Value')]]
239
+ const privateConnection = connection?.privateConnection
240
+ const publicConnection = connection?.publicConnection
241
+
242
+ if (privateConnection && typeof privateConnection === 'object') {
243
+ rows.push(['Private endpoint', formatValue(privateConnection.endpoint)])
244
+ rows.push(['Private host', formatValue(privateConnection.host)])
245
+ rows.push(['Private port', formatValue(privateConnection.port)])
246
+ rows.push(['Username', formatValue(privateConnection.username)])
247
+ rows.push(['Password', formatValue(privateConnection.password)])
248
+ rows.push(['Connection string', formatValue(privateConnection.connectionString)])
249
+ }
250
+
251
+ if (publicConnection !== undefined && publicConnection !== null) {
252
+ rows.push(['Public connection', formatValue(publicConnection)])
253
+ }
254
+
255
+ if (rows.length === 1) {
256
+ console.log('Connection information is not available yet.')
257
+ return
258
+ }
259
+
260
+ outputTable(rows)
261
+ }
262
+
263
+ export function createDatabaseCommand (): Command {
264
+ const dbCmd = new Command('database')
265
+ .alias('db')
266
+ .description('Manage databases')
267
+
268
+ dbCmd
269
+ .command('list')
270
+ .description('List databases')
271
+ .option('-o, --output <format>', 'Output format (json|table)', 'table')
272
+ .action(withAuth({ spinnerText: 'Loading databases...' }, async (ctx, options: { output: string }) => {
273
+ const client = createDatabaseClient()
274
+ const { data, error, response } = await client.GET('/databases', {
275
+ headers: ctx.auth
276
+ })
277
+
278
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
279
+
280
+ ctx.spinner.stop()
281
+
282
+ if (options.output === 'json') {
283
+ outputJson(data)
284
+ return
285
+ }
286
+
287
+ if (data.length === 0) {
288
+ console.log('No databases found.')
289
+ return
290
+ }
291
+
292
+ const rows: string[][] = [[
293
+ chalk.bold('Name'),
294
+ chalk.bold('Type'),
295
+ chalk.bold('Version'),
296
+ chalk.bold('Status'),
297
+ chalk.bold('CPU'),
298
+ chalk.bold('Memory'),
299
+ chalk.bold('Storage'),
300
+ chalk.bold('Replicas')
301
+ ]]
302
+
303
+ for (const database of data) {
304
+ rows.push([
305
+ formatValue(database.name),
306
+ formatValue(database.type),
307
+ formatValue(database.version),
308
+ formatValue(database.status),
309
+ formatValue(database.quota?.cpu),
310
+ formatValue(database.quota?.memory),
311
+ formatValue(database.quota?.storage),
312
+ formatValue(database.quota?.replicas)
313
+ ])
314
+ }
315
+
316
+ outputTable(rows)
317
+ }))
318
+
319
+ dbCmd
320
+ .command('versions')
321
+ .description('List supported database versions (public endpoint)')
322
+ .option('-o, --output <format>', 'Output format (json|table)', 'table')
323
+ .option('--host <host>', 'Sealos region host for public version lookup, e.g. https://gzg.sealos.run')
324
+ .option('--type <type>', 'Filter versions by database type')
325
+ .action(withErrorHandling({ spinnerText: 'Loading versions...' }, async (ctx, options: { output: string; host?: string; type?: string }) => {
326
+ const client = createDatabaseClient({ baseUrl: options.host })
327
+ const { data, error, response } = await client.GET('/databases/versions')
328
+
329
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
330
+
331
+ ctx.spinner.stop()
332
+
333
+ if (options.output === 'json') {
334
+ if (!options.type) {
335
+ outputJson(data)
336
+ return
337
+ }
338
+
339
+ const versionsMap = extractVersionsMap(data)
340
+ const databaseType = normalizeDatabaseType(options.type)
341
+ outputJson({ [databaseType]: versionsMap[databaseType] ?? [] })
342
+ return
343
+ }
344
+
345
+ const versionsMap = extractVersionsMap(data)
346
+ if (options.type) {
347
+ const databaseType = normalizeDatabaseType(options.type)
348
+ const versions = versionsMap[databaseType] ?? []
349
+ if (versions.length === 0) {
350
+ console.log(`No versions found for database type "${databaseType}".`)
351
+ return
352
+ }
353
+
354
+ const rows: string[][] = [[chalk.bold('Type'), chalk.bold('Version')]]
355
+ for (const version of versions) {
356
+ rows.push([databaseType, version])
357
+ }
358
+ outputTable(rows)
359
+ return
360
+ }
361
+
362
+ const rows: string[][] = [[chalk.bold('Type'), chalk.bold('Versions')]]
363
+ for (const [type, versions] of Object.entries(versionsMap)) {
364
+ rows.push([type, summarizeVersions(versions)])
365
+ }
366
+ outputTable(rows)
367
+ }))
368
+
369
+ dbCmd
370
+ .command('create <type>')
371
+ .description('Create a database')
372
+ .requiredOption('--name <name>', 'Database name')
373
+ .option('--version <version>', 'Database version')
374
+ .option('--cpu <cpu>', 'CPU cores per replica', '1')
375
+ .option('--memory <memory>', 'Memory in GB per replica', '1')
376
+ .option('--storage <storage>', 'Storage in GB per replica', '3')
377
+ .option('--replicas <replicas>', 'Replica count', '1')
378
+ .option('--termination-policy <policy>', 'Termination policy (delete|wipeout)')
379
+ .option('--backup-start', 'Enable automatic backups')
380
+ .option('--backup-type <type>', 'Automatic backup frequency (day|hour|week)')
381
+ .option('--backup-week <day>', 'Weekday for weekly backups', collectOption, [] as string[])
382
+ .option('--backup-hour <hour>', 'Backup hour (00-23)')
383
+ .option('--backup-minute <minute>', 'Backup minute (00-59)')
384
+ .option('--backup-save-time <count>', 'Retention count')
385
+ .option('--backup-save-type <type>', 'Retention unit (days|hours|weeks|months)')
386
+ .option('--param <KEY=VALUE>', 'Database parameter override', collectOption, [] as string[])
387
+ .option('-o, --output <format>', 'Output format (json|table)', 'table')
388
+ .action(withAuth({
389
+ spinnerText: 'Creating database...'
390
+ }, async (
391
+ ctx,
392
+ type: string,
393
+ options: {
394
+ name: string
395
+ version?: string
396
+ cpu: string
397
+ memory: string
398
+ storage: string
399
+ replicas: string
400
+ terminationPolicy?: string
401
+ backupStart?: boolean
402
+ backupType?: string
403
+ backupWeek: string[]
404
+ backupHour?: string
405
+ backupMinute?: string
406
+ backupSaveTime?: string
407
+ backupSaveType?: string
408
+ param: string[]
409
+ output: string
410
+ }
411
+ ) => {
412
+ const client = createDatabaseClient()
413
+ const quota = {
414
+ cpu: parseNumericValue(options.cpu, 'cpu'),
415
+ memory: parseNumericValue(options.memory, 'memory'),
416
+ storage: parseNumericValue(options.storage, 'storage'),
417
+ replicas: parseIntegerValue(options.replicas, 'replicas')
418
+ }
419
+ const autoBackup = buildAutoBackup(options)
420
+ const parameterConfig = options.param.length > 0 ? parseKeyValueArgs(options.param) : undefined
421
+
422
+ const body = {
423
+ name: options.name,
424
+ type: normalizeDatabaseType(type),
425
+ quota
426
+ }
427
+
428
+ if (options.version) Object.assign(body, { version: options.version })
429
+ if (options.terminationPolicy) Object.assign(body, { terminationPolicy: options.terminationPolicy })
430
+ if (autoBackup) Object.assign(body, { autoBackup })
431
+ if (parameterConfig) Object.assign(body, { parameterConfig })
432
+
433
+ const { data, error, response } = await client.POST('/databases', {
434
+ headers: ctx.auth,
435
+ body: body as any
436
+ })
437
+
438
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
439
+
440
+ if (options.output === 'json') {
441
+ ctx.spinner.stop()
442
+ outputJson(data)
443
+ return
444
+ }
445
+
446
+ ctx.spinner.succeed(`Database "${data.name}" created successfully`)
447
+ console.log(chalk.dim(` Provisioning status: ${data.status}`))
448
+ console.log(chalk.dim(` Next: sealos database get ${data.name}`))
449
+ }))
450
+
451
+ dbCmd
452
+ .command('get <name>')
453
+ .alias('describe')
454
+ .description('Get database details')
455
+ .option('-o, --output <format>', 'Output format (json|table)', 'table')
456
+ .action(withAuth({ spinnerText: 'Loading database...' }, async (ctx, name: string, options: { output: string }) => {
457
+ const client = createDatabaseClient()
458
+ const { data, error, response } = await client.GET('/databases/{databaseName}', {
459
+ headers: ctx.auth,
460
+ params: {
461
+ path: { databaseName: name }
462
+ }
463
+ })
464
+
465
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
466
+
467
+ ctx.spinner.stop()
468
+
469
+ if (options.output === 'json') {
470
+ outputJson(data)
471
+ return
472
+ }
473
+
474
+ printDatabaseDetail(data)
475
+ }))
476
+
477
+ dbCmd
478
+ .command('connection <name>')
479
+ .description('Show database connection details')
480
+ .option('-o, --output <format>', 'Output format (json|table)', 'table')
481
+ .action(withAuth({ spinnerText: 'Loading connection details...' }, async (ctx, name: string, options: { output: string }) => {
482
+ const client = createDatabaseClient()
483
+ const { data, error, response } = await client.GET('/databases/{databaseName}', {
484
+ headers: ctx.auth,
485
+ params: {
486
+ path: { databaseName: name }
487
+ }
488
+ })
489
+
490
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
491
+
492
+ ctx.spinner.stop()
493
+
494
+ if (options.output === 'json') {
495
+ outputJson(data.connection ?? null)
496
+ return
497
+ }
498
+
499
+ printConnectionDetail(data.connection)
500
+ }))
501
+
502
+ dbCmd
503
+ .command('update <name>')
504
+ .description('Update database resources')
505
+ .option('--cpu <cpu>', 'CPU cores per replica')
506
+ .option('--memory <memory>', 'Memory in GB per replica')
507
+ .option('--storage <storage>', 'Storage in GB per replica')
508
+ .option('--replicas <replicas>', 'Replica count')
509
+ .action(withAuth({
510
+ spinnerText: 'Updating database...'
511
+ }, async (
512
+ ctx,
513
+ name: string,
514
+ options: { cpu?: string; memory?: string; storage?: string; replicas?: string }
515
+ ) => {
516
+ const quota = buildQuota(options)
517
+ if (Object.keys(quota).length === 0) {
518
+ throw new Error('Provide at least one of --cpu, --memory, --storage, or --replicas.')
519
+ }
520
+
521
+ const client = createDatabaseClient()
522
+ const { error, response } = await client.PATCH('/databases/{databaseName}', {
523
+ headers: ctx.auth,
524
+ params: {
525
+ path: { databaseName: name }
526
+ },
527
+ body: { quota }
528
+ })
529
+
530
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
531
+
532
+ ctx.spinner.succeed(`Database "${name}" update requested`)
533
+ }))
534
+
535
+ dbCmd
536
+ .command('start <name>')
537
+ .description('Start a database')
538
+ .action(withAuth({ spinnerText: 'Starting database...' }, async (ctx, name: string) => {
539
+ const client = createDatabaseClient()
540
+ const { error, response } = await client.POST('/databases/{databaseName}/start', {
541
+ headers: ctx.auth,
542
+ params: {
543
+ path: { databaseName: name }
544
+ }
545
+ })
546
+
547
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
548
+ ctx.spinner.succeed(`Database "${name}" start requested`)
549
+ }))
550
+
551
+ dbCmd
552
+ .command('pause <name>')
553
+ .alias('stop')
554
+ .description('Pause a database')
555
+ .action(withAuth({ spinnerText: 'Pausing database...' }, async (ctx, name: string) => {
556
+ const client = createDatabaseClient()
557
+ const { error, response } = await client.POST('/databases/{databaseName}/pause', {
558
+ headers: ctx.auth,
559
+ params: {
560
+ path: { databaseName: name }
561
+ }
562
+ })
563
+
564
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
565
+ ctx.spinner.succeed(`Database "${name}" pause requested`)
566
+ }))
567
+
568
+ dbCmd
569
+ .command('restart <name>')
570
+ .description('Restart a database')
571
+ .action(withAuth({ spinnerText: 'Restarting database...' }, async (ctx, name: string) => {
572
+ const client = createDatabaseClient()
573
+ const { error, response } = await client.POST('/databases/{databaseName}/restart', {
574
+ headers: ctx.auth,
575
+ params: {
576
+ path: { databaseName: name }
577
+ }
578
+ })
579
+
580
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
581
+ ctx.spinner.succeed(`Database "${name}" restart requested`)
582
+ }))
583
+
584
+ dbCmd
585
+ .command('delete <name>')
586
+ .description('Delete a database')
587
+ .option('-f, --force', 'Delete without confirmation')
588
+ .action(withAuth({ spinnerText: 'Deleting database...' }, async (ctx, name: string) => {
589
+ const client = createDatabaseClient()
590
+ const { error, response } = await client.DELETE('/databases/{databaseName}', {
591
+ headers: ctx.auth,
592
+ params: {
593
+ path: { databaseName: name }
594
+ }
595
+ })
596
+
597
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
598
+ ctx.spinner.succeed(`Database "${name}" delete requested`)
599
+ }))
600
+
601
+ dbCmd
602
+ .command('backups <name>')
603
+ .description('List backups for a database')
604
+ .option('-o, --output <format>', 'Output format (json|table)', 'table')
605
+ .action(withAuth({ spinnerText: 'Loading backups...' }, async (ctx, name: string, options: { output: string }) => {
606
+ const client = createDatabaseClient()
607
+ const { data, error, response } = await client.GET('/databases/{databaseName}/backups', {
608
+ headers: ctx.auth,
609
+ params: {
610
+ path: { databaseName: name }
611
+ }
612
+ })
613
+
614
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
615
+
616
+ ctx.spinner.stop()
617
+
618
+ if (options.output === 'json') {
619
+ outputJson(data)
620
+ return
621
+ }
622
+
623
+ if (data.length === 0) {
624
+ console.log('No backups found.')
625
+ return
626
+ }
627
+
628
+ const rows: string[][] = [[
629
+ chalk.bold('Name'),
630
+ chalk.bold('Status'),
631
+ chalk.bold('Created'),
632
+ chalk.bold('Description')
633
+ ]]
634
+
635
+ for (const backup of data) {
636
+ rows.push([
637
+ formatValue(backup.name),
638
+ formatValue(backup.status),
639
+ formatValue(backup.createdAt),
640
+ formatValue(backup.description)
641
+ ])
642
+ }
643
+
644
+ outputTable(rows)
645
+ }))
646
+
647
+ dbCmd
648
+ .command('backup <name>')
649
+ .description('Create a database backup')
650
+ .option('--name <backupName>', 'Backup name')
651
+ .option('--description <description>', 'Backup description')
652
+ .action(withAuth({
653
+ spinnerText: 'Creating backup...'
654
+ }, async (
655
+ ctx,
656
+ name: string,
657
+ options: { name?: string; description?: string }
658
+ ) => {
659
+ const client = createDatabaseClient()
660
+ const body: Record<string, string> = {}
661
+ if (options.name) body.name = options.name
662
+ if (options.description) body.description = options.description
663
+
664
+ const { error, response } = await client.POST('/databases/{databaseName}/backups', {
665
+ headers: ctx.auth,
666
+ params: {
667
+ path: { databaseName: name }
668
+ },
669
+ body
670
+ })
671
+
672
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
673
+ ctx.spinner.succeed(`Backup requested for database "${name}"`)
674
+ }))
675
+
676
+ dbCmd
677
+ .command('backup-delete <databaseName> <backupName>')
678
+ .description('Delete a database backup')
679
+ .action(withAuth({
680
+ spinnerText: 'Deleting backup...'
681
+ }, async (ctx, databaseName: string, backupName: string) => {
682
+ const client = createDatabaseClient()
683
+ const { error, response } = await client.DELETE('/databases/{databaseName}/backups/{backupName}', {
684
+ headers: ctx.auth,
685
+ params: {
686
+ path: { databaseName, backupName }
687
+ }
688
+ })
689
+
690
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
691
+ ctx.spinner.succeed(`Backup "${backupName}" deleted`)
692
+ }))
693
+
694
+ dbCmd
695
+ .command('restore <databaseName>')
696
+ .description('Restore a database from a backup')
697
+ .requiredOption('--from <backupName>', 'Backup name to restore from')
698
+ .option('--name <name>', 'Name for the restored database')
699
+ .option('--replicas <replicas>', 'Replica count for the restored database')
700
+ .action(withAuth({
701
+ spinnerText: 'Restoring database...'
702
+ }, async (
703
+ ctx,
704
+ databaseName: string,
705
+ options: { from: string; name?: string; replicas?: string }
706
+ ) => {
707
+ const client = createDatabaseClient()
708
+ const body: Record<string, unknown> = {}
709
+ if (options.name) body.name = options.name
710
+ if (options.replicas) body.replicas = parseIntegerValue(options.replicas, 'replicas')
711
+
712
+ const { error, response } = await client.POST('/databases/{databaseName}/backups/{backupName}/restore', {
713
+ headers: ctx.auth,
714
+ params: {
715
+ path: { databaseName, backupName: options.from }
716
+ },
717
+ body
718
+ })
719
+
720
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
721
+ ctx.spinner.succeed(`Restore requested from backup "${options.from}"`)
722
+ }))
723
+
724
+ dbCmd
725
+ .command('enable-public <name>')
726
+ .description('Enable public access for a database')
727
+ .action(withAuth({ spinnerText: 'Enabling public access...' }, async (ctx, name: string) => {
728
+ const client = createDatabaseClient()
729
+ const { error, response } = await client.POST('/databases/{databaseName}/enable-public', {
730
+ headers: ctx.auth,
731
+ params: {
732
+ path: { databaseName: name }
733
+ }
734
+ })
735
+
736
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
737
+ ctx.spinner.succeed(`Public access enabled for "${name}"`)
738
+ }))
739
+
740
+ dbCmd
741
+ .command('disable-public <name>')
742
+ .description('Disable public access for a database')
743
+ .action(withAuth({ spinnerText: 'Disabling public access...' }, async (ctx, name: string) => {
744
+ const client = createDatabaseClient()
745
+ const { error, response } = await client.POST('/databases/{databaseName}/disable-public', {
746
+ headers: ctx.auth,
747
+ params: {
748
+ path: { databaseName: name }
749
+ }
750
+ })
751
+
752
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
753
+ ctx.spinner.succeed(`Public access disabled for "${name}"`)
754
+ }))
755
+
756
+ dbCmd
757
+ .command('logs <podName>')
758
+ .description('Get parsed database logs for a pod')
759
+ .requiredOption('--db-type <type>', 'Database type used by the log service')
760
+ .requiredOption('--log-type <type>', 'Log type used by the log service')
761
+ .requiredOption('--log-path <path>', 'Log path to read. Use "log-files" first to discover valid paths')
762
+ .option('--page <page>', 'Page number', '1')
763
+ .option('--page-size <pageSize>', 'Page size', '200')
764
+ .option('-o, --output <format>', 'Output format (plain|json|table)', 'plain')
765
+ .action(withAuth({
766
+ spinnerText: 'Loading logs...'
767
+ }, async (
768
+ ctx,
769
+ podName: string,
770
+ options: {
771
+ dbType: string
772
+ logType: string
773
+ logPath: string
774
+ page: string
775
+ pageSize: string
776
+ output: string
777
+ }
778
+ ) => {
779
+ const client = createDatabaseClient()
780
+ const { data, error, response } = await client.GET('/logs', {
781
+ headers: ctx.auth,
782
+ params: {
783
+ query: {
784
+ podName,
785
+ dbType: normalizeLogDbType(options.dbType),
786
+ logType: normalizeLogType(options.logType),
787
+ logPath: options.logPath,
788
+ page: parseIntegerValue(options.page, 'page'),
789
+ pageSize: parseIntegerValue(options.pageSize, 'page-size')
790
+ }
791
+ }
792
+ })
793
+
794
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
795
+
796
+ ctx.spinner.stop()
797
+
798
+ if (options.output === 'json') {
799
+ outputJson(data)
800
+ return
801
+ }
802
+
803
+ if (options.output === 'table') {
804
+ const rows: string[][] = [[chalk.bold('Timestamp'), chalk.bold('Level'), chalk.bold('Content')]]
805
+ for (const log of data.data.logs) {
806
+ rows.push([formatValue(log.timestamp), formatValue(log.level), formatValue(log.content)])
807
+ }
808
+ outputTable(rows)
809
+ return
810
+ }
811
+
812
+ for (const log of data.data.logs) {
813
+ console.log(`${log.timestamp} [${log.level}] ${log.content}`)
814
+ }
815
+ console.log(chalk.dim(`\npage=${data.data.metadata.page} total=${data.data.metadata.total} hasMore=${String(data.data.metadata.hasMore)}`))
816
+ }))
817
+
818
+ dbCmd
819
+ .command('log-files <podName>')
820
+ .description('List database log files for a pod')
821
+ .requiredOption('--db-type <type>', 'Database type used by the log service')
822
+ .requiredOption('--log-type <type>', 'Log type used by the log service')
823
+ .option('-o, --output <format>', 'Output format (json|table)', 'table')
824
+ .action(withAuth({
825
+ spinnerText: 'Loading log files...'
826
+ }, async (
827
+ ctx,
828
+ podName: string,
829
+ options: { dbType: string; logType: string; output: string }
830
+ ) => {
831
+ const client = createDatabaseClient()
832
+ const { data, error, response } = await client.GET('/logs/files', {
833
+ headers: ctx.auth,
834
+ params: {
835
+ query: {
836
+ podName,
837
+ dbType: normalizeLogDbType(options.dbType),
838
+ logType: normalizeLogType(options.logType)
839
+ }
840
+ }
841
+ })
842
+
843
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
844
+
845
+ ctx.spinner.stop()
846
+
847
+ if (options.output === 'json') {
848
+ outputJson(data)
849
+ return
850
+ }
851
+
852
+ if (data.data.length === 0) {
853
+ console.log('No log files found.')
854
+ return
855
+ }
856
+
857
+ const rows: string[][] = [[
858
+ chalk.bold('Name'),
859
+ chalk.bold('Path'),
860
+ chalk.bold('Kind'),
861
+ chalk.bold('Size'),
862
+ chalk.bold('Updated'),
863
+ chalk.bold('Processed')
864
+ ]]
865
+
866
+ for (const file of data.data) {
867
+ rows.push([
868
+ formatValue(file.name),
869
+ formatValue(file.path),
870
+ formatValue(file.kind),
871
+ formatValue(file.size),
872
+ formatValue(file.updateTime),
873
+ formatValue(file.processed)
874
+ ])
875
+ }
876
+
877
+ outputTable(rows)
878
+ }))
879
+
880
+ return dbCmd
881
+ }