@parix/cli 0.1.5

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 (66) hide show
  1. package/README.md +123 -0
  2. package/dist/cli.cjs +30 -0
  3. package/dist/cli.d.cts +2 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +28 -0
  6. package/dist/commands/api.cjs +81 -0
  7. package/dist/commands/api.d.cts +2 -0
  8. package/dist/commands/api.d.ts +2 -0
  9. package/dist/commands/api.js +78 -0
  10. package/dist/commands/auth.cjs +128 -0
  11. package/dist/commands/auth.d.cts +2 -0
  12. package/dist/commands/auth.d.ts +2 -0
  13. package/dist/commands/auth.js +125 -0
  14. package/dist/commands/database.cjs +259 -0
  15. package/dist/commands/database.d.cts +2 -0
  16. package/dist/commands/database.d.ts +2 -0
  17. package/dist/commands/database.js +256 -0
  18. package/dist/commands/tb.cjs +242 -0
  19. package/dist/commands/tb.d.cts +2 -0
  20. package/dist/commands/tb.d.ts +2 -0
  21. package/dist/commands/tb.js +239 -0
  22. package/dist/lib/api-client.cjs +44 -0
  23. package/dist/lib/api-client.d.cts +10 -0
  24. package/dist/lib/api-client.d.ts +10 -0
  25. package/dist/lib/api-client.js +40 -0
  26. package/dist/lib/config.cjs +24 -0
  27. package/dist/lib/config.d.cts +10 -0
  28. package/dist/lib/config.d.ts +10 -0
  29. package/dist/lib/config.js +18 -0
  30. package/dist/lib/loopback-server.cjs +158 -0
  31. package/dist/lib/loopback-server.d.cts +20 -0
  32. package/dist/lib/loopback-server.d.ts +20 -0
  33. package/dist/lib/loopback-server.js +155 -0
  34. package/dist/lib/oauth-api.cjs +73 -0
  35. package/dist/lib/oauth-api.d.cts +31 -0
  36. package/dist/lib/oauth-api.d.ts +31 -0
  37. package/dist/lib/oauth-api.js +68 -0
  38. package/dist/lib/oauth-session.cjs +108 -0
  39. package/dist/lib/oauth-session.d.cts +32 -0
  40. package/dist/lib/oauth-session.d.ts +32 -0
  41. package/dist/lib/oauth-session.js +103 -0
  42. package/dist/lib/oauth.cjs +58 -0
  43. package/dist/lib/oauth.d.cts +19 -0
  44. package/dist/lib/oauth.d.ts +19 -0
  45. package/dist/lib/oauth.js +49 -0
  46. package/dist/lib/open-url.cjs +38 -0
  47. package/dist/lib/open-url.d.cts +1 -0
  48. package/dist/lib/open-url.d.ts +1 -0
  49. package/dist/lib/open-url.js +32 -0
  50. package/dist/lib/output.cjs +20 -0
  51. package/dist/lib/output.d.cts +6 -0
  52. package/dist/lib/output.d.ts +6 -0
  53. package/dist/lib/output.js +15 -0
  54. package/dist/lib/parix-api.cjs +59 -0
  55. package/dist/lib/parix-api.d.cts +12 -0
  56. package/dist/lib/parix-api.d.ts +12 -0
  57. package/dist/lib/parix-api.js +55 -0
  58. package/dist/lib/session.cjs +80 -0
  59. package/dist/lib/session.d.cts +28 -0
  60. package/dist/lib/session.d.ts +28 -0
  61. package/dist/lib/session.js +74 -0
  62. package/dist/lib/tb-payloads.cjs +153 -0
  63. package/dist/lib/tb-payloads.d.cts +61 -0
  64. package/dist/lib/tb-payloads.d.ts +61 -0
  65. package/dist/lib/tb-payloads.js +144 -0
  66. package/package.json +74 -0
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDatabaseCommand = createDatabaseCommand;
4
+ const prompts_1 = require("@clack/prompts");
5
+ const commander_1 = require("commander");
6
+ const output_1 = require("../lib/output.cjs");
7
+ const parix_api_1 = require("../lib/parix-api.cjs");
8
+ function createDatabaseCommand() {
9
+ const database = new commander_1.Command('database')
10
+ .description('Manage Parix databases')
11
+ .addHelpText('after', [
12
+ '',
13
+ 'Examples:',
14
+ ' parix database catalog --base-url http://localhost:5173',
15
+ ' parix database create demo-ledger --provider gcp --region asia-southeast1',
16
+ ' parix database list',
17
+ ' parix database info id-123',
18
+ ' parix database remove id-123 --yes',
19
+ ].join('\n'));
20
+ database
21
+ .command('catalog')
22
+ .description('Show database creation catalog options')
23
+ .option('-b, --base-url <url>', 'Parix base URL')
24
+ .option('--json', 'Print raw JSON')
25
+ .action(async (options) => {
26
+ const response = await (0, parix_api_1.requestApiJson)({
27
+ baseUrl: options.baseUrl,
28
+ path: '/api/v1/catalog/create',
29
+ });
30
+ if (options.json) {
31
+ (0, output_1.printJson)(response);
32
+ return;
33
+ }
34
+ (0, output_1.printTable)({
35
+ columns: ['Provider', 'Regions'],
36
+ rows: response.catalog.providers.map((provider) => [
37
+ provider.slug,
38
+ provider.regions.map((region) => region.code).join(', '),
39
+ ]),
40
+ });
41
+ (0, prompts_1.note)('Use `--provider <slug>` and `--region <code>` from this table.', 'Providers');
42
+ (0, output_1.printTable)({
43
+ columns: ['Config ID', 'Label', 'Nodes'],
44
+ rows: response.catalog.clusterConfigs.map((config) => [config.id, config.label, String(config.nodeCount)]),
45
+ });
46
+ (0, output_1.printTable)({
47
+ columns: ['Storage ID', 'Label', 'Multiplier'],
48
+ rows: response.catalog.storageTiers.map((tier) => [tier.id, tier.label, `${tier.priceMultiplier}x`]),
49
+ });
50
+ (0, output_1.printTable)({
51
+ columns: ['Size ID', 'Label', 'vCPU', 'Memory GB'],
52
+ rows: response.catalog.clusterSizes.map((size) => [
53
+ size.id,
54
+ size.label,
55
+ String(size.vcpu),
56
+ String(size.memoryGb),
57
+ ]),
58
+ });
59
+ (0, prompts_1.outro)('Done');
60
+ });
61
+ database
62
+ .command('create')
63
+ .description('Create a new database')
64
+ .argument('<database>', 'Database name')
65
+ .option('-b, --base-url <url>', 'Parix base URL')
66
+ .option('--provider <slug>', 'Provider slug (for example: gcp, aws)')
67
+ .option('--region <code>', 'Region code')
68
+ .option('--cluster-config-id <id>', 'Cluster config id')
69
+ .option('--storage-tier-id <id>', 'Storage tier id')
70
+ .option('--cluster-size-id <id>', 'Cluster size id')
71
+ .option('--storage-gb <n>', 'Storage size in GB')
72
+ .option('--json', 'Print raw JSON')
73
+ .addHelpText('after', [
74
+ '',
75
+ 'Examples:',
76
+ ' parix database create payments-ledger --provider gcp --region asia-southeast1',
77
+ ' parix database create payments-ledger --provider aws --region ap-southeast-1 --cluster-config-id config-single --cluster-size-id size-hx-5',
78
+ ].join('\n'))
79
+ .action(async (databaseName, options) => {
80
+ const parsedStorageGb = options.storageGb ? Number.parseInt(options.storageGb, 10) : null;
81
+ if (options.storageGb &&
82
+ (typeof parsedStorageGb !== 'number' || !Number.isInteger(parsedStorageGb) || parsedStorageGb <= 0)) {
83
+ throw new Error('--storage-gb must be a positive integer.');
84
+ }
85
+ const response = await (0, parix_api_1.requestApiJson)({
86
+ baseUrl: options.baseUrl,
87
+ body: JSON.stringify({
88
+ clusterConfigId: options.clusterConfigId,
89
+ clusterSizeId: options.clusterSizeId,
90
+ name: databaseName,
91
+ provider: options.provider,
92
+ region: options.region,
93
+ selectedStorageGb: parsedStorageGb ?? undefined,
94
+ storageTierId: options.storageTierId,
95
+ }),
96
+ headers: {
97
+ 'content-type': 'application/json',
98
+ },
99
+ method: 'POST',
100
+ path: '/api/v1/databases',
101
+ });
102
+ if (options.json) {
103
+ (0, output_1.printJson)(response);
104
+ return;
105
+ }
106
+ if (!response.success) {
107
+ (0, prompts_1.note)([
108
+ `Billing: ${response.billing?.message ?? response.message}`,
109
+ `Checkout URL: ${response.billing?.checkoutUrl ?? '-'}`,
110
+ ].join('\n'), 'Provisioning blocked');
111
+ (0, prompts_1.outro)(response.message);
112
+ return;
113
+ }
114
+ (0, output_1.printKeyValues)([
115
+ ['Database ID', response.database?.id ?? '-'],
116
+ ['Database Name', response.database?.name ?? databaseName],
117
+ ['Profile ID', response.profile?.id ?? '-'],
118
+ ['Profile status', response.profile?.status ?? '-'],
119
+ ['Provisioning', response.provisioning?.status ?? '-'],
120
+ ['Workflow ID', response.job?.workflowId ?? '-'],
121
+ ]);
122
+ if (response.provisioning) {
123
+ (0, prompts_1.note)([`Status: ${response.provisioning.status}`, `Message: ${response.provisioning.message ?? '-'}`].join('\n'), 'Provisioning');
124
+ }
125
+ (0, prompts_1.outro)(response.message);
126
+ });
127
+ database
128
+ .command('list')
129
+ .description('List databases in the active organization')
130
+ .option('-b, --base-url <url>', 'Parix base URL')
131
+ .option('--search <text>', 'Filter database names')
132
+ .option('--limit <n>', 'Maximum number of rows to return')
133
+ .option('--json', 'Print raw JSON')
134
+ .addHelpText('after', [
135
+ '',
136
+ 'Examples:',
137
+ ' parix database list',
138
+ ' parix database list --search payments',
139
+ ' parix database list --json',
140
+ ].join('\n'))
141
+ .action(async (options) => {
142
+ const query = new URLSearchParams();
143
+ if (options.search) {
144
+ query.set('search', options.search);
145
+ }
146
+ if (options.limit) {
147
+ query.set('limit', options.limit);
148
+ }
149
+ const suffix = query.size > 0 ? `?${query.toString()}` : '';
150
+ const response = await (0, parix_api_1.requestApiJson)({
151
+ baseUrl: options.baseUrl,
152
+ path: `/api/v1/databases${suffix}`,
153
+ });
154
+ if (options.json) {
155
+ (0, output_1.printJson)(response);
156
+ return;
157
+ }
158
+ if (response.databases.length === 0) {
159
+ (0, prompts_1.outro)('No databases found');
160
+ return;
161
+ }
162
+ (0, output_1.printTable)({
163
+ columns: ['ID', 'Name', 'Region', 'Provider', 'Status', 'Updated'],
164
+ rows: response.databases.map((database) => [
165
+ database.id,
166
+ database.name,
167
+ database.region ?? '-',
168
+ database.provider ?? '-',
169
+ database.status,
170
+ database.updatedAt,
171
+ ]),
172
+ });
173
+ (0, prompts_1.note)(String(response.databases.length), 'Databases');
174
+ (0, prompts_1.outro)('Done');
175
+ });
176
+ database
177
+ .command('info')
178
+ .description('Show database details')
179
+ .argument('<database-id>', 'Database id')
180
+ .option('-b, --base-url <url>', 'Parix base URL')
181
+ .option('--json', 'Print raw JSON')
182
+ .addHelpText('after', ['', 'Examples:', ' parix database info db_123', ' parix database info db_123 --json'].join('\n'))
183
+ .action(async (databaseId, options) => {
184
+ const response = await (0, parix_api_1.requestApiJson)({
185
+ baseUrl: options.baseUrl,
186
+ path: `/api/v1/databases/${encodeURIComponent(databaseId)}`,
187
+ });
188
+ if (options.json) {
189
+ (0, output_1.printJson)(response);
190
+ return;
191
+ }
192
+ (0, output_1.printKeyValues)([
193
+ ['Database ID', response.database.id],
194
+ ['Database Name', response.database.name],
195
+ ['Provider', response.provider ?? '-'],
196
+ ['Region', response.region ?? '-'],
197
+ ['Gateway URL', response.gatewayUrl ?? '-'],
198
+ ['Status', response.decommissionState?.status ?? response.profile?.status ?? '-'],
199
+ ['Version', response.profile?.tigerbeetleCurrentVersion ?? response.profile?.tigerbeetleVersion ?? '-'],
200
+ ['Nodes', response.profile ? String(response.profile.selectedNodeCount) : '-'],
201
+ ['CPU', response.profile ? String(response.profile.selectedVcpu) : '-'],
202
+ ['Memory GB', response.profile ? String(response.profile.selectedMemoryGb) : '-'],
203
+ ['Storage GB', response.profile?.selectedStorageGb != null ? String(response.profile.selectedStorageGb) : '-'],
204
+ ['Updated', response.database.updatedAt],
205
+ ]);
206
+ if (response.metricsSummary) {
207
+ (0, prompts_1.note)([
208
+ `CPU: ${response.metricsSummary.cpuUsagePct ?? '-'}%`,
209
+ `Memory: ${response.metricsSummary.memoryUsagePct ?? '-'}%`,
210
+ `Disk: ${response.metricsSummary.diskUsagePct ?? '-'}%`,
211
+ `Gateway P95: ${response.metricsSummary.gatewayP95LatencyMs ?? '-'}ms`,
212
+ `Last collected: ${response.metricsSummary.lastCollectedAt ?? '-'}`,
213
+ ].join('\n'), 'Metrics');
214
+ }
215
+ if (response.latestProvisionJob) {
216
+ (0, prompts_1.note)([
217
+ `Provision job: ${response.latestProvisionJob.id}`,
218
+ `Provision status: ${response.latestProvisionJob.status}`,
219
+ `Workflow ID: ${response.latestProvisionJob.workflowId ?? '-'}`,
220
+ ].join('\n'), 'Latest provision job');
221
+ }
222
+ (0, prompts_1.outro)('Done');
223
+ });
224
+ database
225
+ .command('remove')
226
+ .description('Queue database deletion')
227
+ .argument('<database-id>', 'Database id')
228
+ .option('-b, --base-url <url>', 'Parix base URL')
229
+ .option('--json', 'Print raw JSON')
230
+ .option('-y, --yes', 'Skip confirmation prompt')
231
+ .addHelpText('after', ['', 'Examples:', ' parix database remove db_123', ' parix database remove db_123 --yes --json'].join('\n'))
232
+ .action(async (databaseId, options) => {
233
+ if (!options.yes) {
234
+ const confirmed = await (0, prompts_1.confirm)({
235
+ message: `Queue deletion for ${databaseId}?`,
236
+ });
237
+ if ((0, prompts_1.isCancel)(confirmed) || !confirmed) {
238
+ (0, prompts_1.outro)('Cancelled');
239
+ return;
240
+ }
241
+ }
242
+ const response = await (0, parix_api_1.requestApiJson)({
243
+ baseUrl: options.baseUrl,
244
+ method: 'DELETE',
245
+ path: `/api/v1/databases/${encodeURIComponent(databaseId)}`,
246
+ });
247
+ if (options.json) {
248
+ (0, output_1.printJson)(response);
249
+ return;
250
+ }
251
+ (0, output_1.printKeyValues)([
252
+ ['Database ID', response.databaseId],
253
+ ['Status', response.status],
254
+ ['Workflow ID', response.workflowId],
255
+ ]);
256
+ (0, prompts_1.outro)(response.message);
257
+ });
258
+ return database;
259
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createDatabaseCommand(): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createDatabaseCommand(): Command;
@@ -0,0 +1,256 @@
1
+ import { confirm, isCancel, note, outro } from '@clack/prompts';
2
+ import { Command } from 'commander';
3
+ import { printJson, printKeyValues, printTable } from "../lib/output.js";
4
+ import { requestApiJson } from "../lib/parix-api.js";
5
+ export function createDatabaseCommand() {
6
+ const database = new Command('database')
7
+ .description('Manage Parix databases')
8
+ .addHelpText('after', [
9
+ '',
10
+ 'Examples:',
11
+ ' parix database catalog --base-url http://localhost:5173',
12
+ ' parix database create demo-ledger --provider gcp --region asia-southeast1',
13
+ ' parix database list',
14
+ ' parix database info id-123',
15
+ ' parix database remove id-123 --yes',
16
+ ].join('\n'));
17
+ database
18
+ .command('catalog')
19
+ .description('Show database creation catalog options')
20
+ .option('-b, --base-url <url>', 'Parix base URL')
21
+ .option('--json', 'Print raw JSON')
22
+ .action(async (options) => {
23
+ const response = await requestApiJson({
24
+ baseUrl: options.baseUrl,
25
+ path: '/api/v1/catalog/create',
26
+ });
27
+ if (options.json) {
28
+ printJson(response);
29
+ return;
30
+ }
31
+ printTable({
32
+ columns: ['Provider', 'Regions'],
33
+ rows: response.catalog.providers.map((provider) => [
34
+ provider.slug,
35
+ provider.regions.map((region) => region.code).join(', '),
36
+ ]),
37
+ });
38
+ note('Use `--provider <slug>` and `--region <code>` from this table.', 'Providers');
39
+ printTable({
40
+ columns: ['Config ID', 'Label', 'Nodes'],
41
+ rows: response.catalog.clusterConfigs.map((config) => [config.id, config.label, String(config.nodeCount)]),
42
+ });
43
+ printTable({
44
+ columns: ['Storage ID', 'Label', 'Multiplier'],
45
+ rows: response.catalog.storageTiers.map((tier) => [tier.id, tier.label, `${tier.priceMultiplier}x`]),
46
+ });
47
+ printTable({
48
+ columns: ['Size ID', 'Label', 'vCPU', 'Memory GB'],
49
+ rows: response.catalog.clusterSizes.map((size) => [
50
+ size.id,
51
+ size.label,
52
+ String(size.vcpu),
53
+ String(size.memoryGb),
54
+ ]),
55
+ });
56
+ outro('Done');
57
+ });
58
+ database
59
+ .command('create')
60
+ .description('Create a new database')
61
+ .argument('<database>', 'Database name')
62
+ .option('-b, --base-url <url>', 'Parix base URL')
63
+ .option('--provider <slug>', 'Provider slug (for example: gcp, aws)')
64
+ .option('--region <code>', 'Region code')
65
+ .option('--cluster-config-id <id>', 'Cluster config id')
66
+ .option('--storage-tier-id <id>', 'Storage tier id')
67
+ .option('--cluster-size-id <id>', 'Cluster size id')
68
+ .option('--storage-gb <n>', 'Storage size in GB')
69
+ .option('--json', 'Print raw JSON')
70
+ .addHelpText('after', [
71
+ '',
72
+ 'Examples:',
73
+ ' parix database create payments-ledger --provider gcp --region asia-southeast1',
74
+ ' parix database create payments-ledger --provider aws --region ap-southeast-1 --cluster-config-id config-single --cluster-size-id size-hx-5',
75
+ ].join('\n'))
76
+ .action(async (databaseName, options) => {
77
+ const parsedStorageGb = options.storageGb ? Number.parseInt(options.storageGb, 10) : null;
78
+ if (options.storageGb &&
79
+ (typeof parsedStorageGb !== 'number' || !Number.isInteger(parsedStorageGb) || parsedStorageGb <= 0)) {
80
+ throw new Error('--storage-gb must be a positive integer.');
81
+ }
82
+ const response = await requestApiJson({
83
+ baseUrl: options.baseUrl,
84
+ body: JSON.stringify({
85
+ clusterConfigId: options.clusterConfigId,
86
+ clusterSizeId: options.clusterSizeId,
87
+ name: databaseName,
88
+ provider: options.provider,
89
+ region: options.region,
90
+ selectedStorageGb: parsedStorageGb ?? undefined,
91
+ storageTierId: options.storageTierId,
92
+ }),
93
+ headers: {
94
+ 'content-type': 'application/json',
95
+ },
96
+ method: 'POST',
97
+ path: '/api/v1/databases',
98
+ });
99
+ if (options.json) {
100
+ printJson(response);
101
+ return;
102
+ }
103
+ if (!response.success) {
104
+ note([
105
+ `Billing: ${response.billing?.message ?? response.message}`,
106
+ `Checkout URL: ${response.billing?.checkoutUrl ?? '-'}`,
107
+ ].join('\n'), 'Provisioning blocked');
108
+ outro(response.message);
109
+ return;
110
+ }
111
+ printKeyValues([
112
+ ['Database ID', response.database?.id ?? '-'],
113
+ ['Database Name', response.database?.name ?? databaseName],
114
+ ['Profile ID', response.profile?.id ?? '-'],
115
+ ['Profile status', response.profile?.status ?? '-'],
116
+ ['Provisioning', response.provisioning?.status ?? '-'],
117
+ ['Workflow ID', response.job?.workflowId ?? '-'],
118
+ ]);
119
+ if (response.provisioning) {
120
+ note([`Status: ${response.provisioning.status}`, `Message: ${response.provisioning.message ?? '-'}`].join('\n'), 'Provisioning');
121
+ }
122
+ outro(response.message);
123
+ });
124
+ database
125
+ .command('list')
126
+ .description('List databases in the active organization')
127
+ .option('-b, --base-url <url>', 'Parix base URL')
128
+ .option('--search <text>', 'Filter database names')
129
+ .option('--limit <n>', 'Maximum number of rows to return')
130
+ .option('--json', 'Print raw JSON')
131
+ .addHelpText('after', [
132
+ '',
133
+ 'Examples:',
134
+ ' parix database list',
135
+ ' parix database list --search payments',
136
+ ' parix database list --json',
137
+ ].join('\n'))
138
+ .action(async (options) => {
139
+ const query = new URLSearchParams();
140
+ if (options.search) {
141
+ query.set('search', options.search);
142
+ }
143
+ if (options.limit) {
144
+ query.set('limit', options.limit);
145
+ }
146
+ const suffix = query.size > 0 ? `?${query.toString()}` : '';
147
+ const response = await requestApiJson({
148
+ baseUrl: options.baseUrl,
149
+ path: `/api/v1/databases${suffix}`,
150
+ });
151
+ if (options.json) {
152
+ printJson(response);
153
+ return;
154
+ }
155
+ if (response.databases.length === 0) {
156
+ outro('No databases found');
157
+ return;
158
+ }
159
+ printTable({
160
+ columns: ['ID', 'Name', 'Region', 'Provider', 'Status', 'Updated'],
161
+ rows: response.databases.map((database) => [
162
+ database.id,
163
+ database.name,
164
+ database.region ?? '-',
165
+ database.provider ?? '-',
166
+ database.status,
167
+ database.updatedAt,
168
+ ]),
169
+ });
170
+ note(String(response.databases.length), 'Databases');
171
+ outro('Done');
172
+ });
173
+ database
174
+ .command('info')
175
+ .description('Show database details')
176
+ .argument('<database-id>', 'Database id')
177
+ .option('-b, --base-url <url>', 'Parix base URL')
178
+ .option('--json', 'Print raw JSON')
179
+ .addHelpText('after', ['', 'Examples:', ' parix database info db_123', ' parix database info db_123 --json'].join('\n'))
180
+ .action(async (databaseId, options) => {
181
+ const response = await requestApiJson({
182
+ baseUrl: options.baseUrl,
183
+ path: `/api/v1/databases/${encodeURIComponent(databaseId)}`,
184
+ });
185
+ if (options.json) {
186
+ printJson(response);
187
+ return;
188
+ }
189
+ printKeyValues([
190
+ ['Database ID', response.database.id],
191
+ ['Database Name', response.database.name],
192
+ ['Provider', response.provider ?? '-'],
193
+ ['Region', response.region ?? '-'],
194
+ ['Gateway URL', response.gatewayUrl ?? '-'],
195
+ ['Status', response.decommissionState?.status ?? response.profile?.status ?? '-'],
196
+ ['Version', response.profile?.tigerbeetleCurrentVersion ?? response.profile?.tigerbeetleVersion ?? '-'],
197
+ ['Nodes', response.profile ? String(response.profile.selectedNodeCount) : '-'],
198
+ ['CPU', response.profile ? String(response.profile.selectedVcpu) : '-'],
199
+ ['Memory GB', response.profile ? String(response.profile.selectedMemoryGb) : '-'],
200
+ ['Storage GB', response.profile?.selectedStorageGb != null ? String(response.profile.selectedStorageGb) : '-'],
201
+ ['Updated', response.database.updatedAt],
202
+ ]);
203
+ if (response.metricsSummary) {
204
+ note([
205
+ `CPU: ${response.metricsSummary.cpuUsagePct ?? '-'}%`,
206
+ `Memory: ${response.metricsSummary.memoryUsagePct ?? '-'}%`,
207
+ `Disk: ${response.metricsSummary.diskUsagePct ?? '-'}%`,
208
+ `Gateway P95: ${response.metricsSummary.gatewayP95LatencyMs ?? '-'}ms`,
209
+ `Last collected: ${response.metricsSummary.lastCollectedAt ?? '-'}`,
210
+ ].join('\n'), 'Metrics');
211
+ }
212
+ if (response.latestProvisionJob) {
213
+ note([
214
+ `Provision job: ${response.latestProvisionJob.id}`,
215
+ `Provision status: ${response.latestProvisionJob.status}`,
216
+ `Workflow ID: ${response.latestProvisionJob.workflowId ?? '-'}`,
217
+ ].join('\n'), 'Latest provision job');
218
+ }
219
+ outro('Done');
220
+ });
221
+ database
222
+ .command('remove')
223
+ .description('Queue database deletion')
224
+ .argument('<database-id>', 'Database id')
225
+ .option('-b, --base-url <url>', 'Parix base URL')
226
+ .option('--json', 'Print raw JSON')
227
+ .option('-y, --yes', 'Skip confirmation prompt')
228
+ .addHelpText('after', ['', 'Examples:', ' parix database remove db_123', ' parix database remove db_123 --yes --json'].join('\n'))
229
+ .action(async (databaseId, options) => {
230
+ if (!options.yes) {
231
+ const confirmed = await confirm({
232
+ message: `Queue deletion for ${databaseId}?`,
233
+ });
234
+ if (isCancel(confirmed) || !confirmed) {
235
+ outro('Cancelled');
236
+ return;
237
+ }
238
+ }
239
+ const response = await requestApiJson({
240
+ baseUrl: options.baseUrl,
241
+ method: 'DELETE',
242
+ path: `/api/v1/databases/${encodeURIComponent(databaseId)}`,
243
+ });
244
+ if (options.json) {
245
+ printJson(response);
246
+ return;
247
+ }
248
+ printKeyValues([
249
+ ['Database ID', response.databaseId],
250
+ ['Status', response.status],
251
+ ['Workflow ID', response.workflowId],
252
+ ]);
253
+ outro(response.message);
254
+ });
255
+ return database;
256
+ }