@rulebricks/cli 1.9.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.
Files changed (93) hide show
  1. package/README.md +62 -0
  2. package/dist/commands/clone.d.ts +6 -0
  3. package/dist/commands/clone.js +60 -0
  4. package/dist/commands/deploy.d.ts +8 -0
  5. package/dist/commands/deploy.js +409 -0
  6. package/dist/commands/destroy.d.ts +8 -0
  7. package/dist/commands/destroy.js +298 -0
  8. package/dist/commands/init.d.ts +7 -0
  9. package/dist/commands/init.js +201 -0
  10. package/dist/commands/logs.d.ts +9 -0
  11. package/dist/commands/logs.js +222 -0
  12. package/dist/commands/open.d.ts +7 -0
  13. package/dist/commands/open.js +139 -0
  14. package/dist/commands/status.d.ts +5 -0
  15. package/dist/commands/status.js +125 -0
  16. package/dist/commands/upgrade.d.ts +7 -0
  17. package/dist/commands/upgrade.js +239 -0
  18. package/dist/components/DNSWaitScreen.d.ts +9 -0
  19. package/dist/components/DNSWaitScreen.js +73 -0
  20. package/dist/components/Wizard/WizardContext.d.ts +176 -0
  21. package/dist/components/Wizard/WizardContext.js +346 -0
  22. package/dist/components/Wizard/index.d.ts +2 -0
  23. package/dist/components/Wizard/index.js +2 -0
  24. package/dist/components/Wizard/steps/CloudProviderStep.d.ts +6 -0
  25. package/dist/components/Wizard/steps/CloudProviderStep.js +210 -0
  26. package/dist/components/Wizard/steps/CredentialsStep.d.ts +6 -0
  27. package/dist/components/Wizard/steps/CredentialsStep.js +22 -0
  28. package/dist/components/Wizard/steps/DatabaseStep.d.ts +6 -0
  29. package/dist/components/Wizard/steps/DatabaseStep.js +80 -0
  30. package/dist/components/Wizard/steps/DeploymentModeStep.d.ts +5 -0
  31. package/dist/components/Wizard/steps/DeploymentModeStep.js +26 -0
  32. package/dist/components/Wizard/steps/DomainStep.d.ts +6 -0
  33. package/dist/components/Wizard/steps/DomainStep.js +126 -0
  34. package/dist/components/Wizard/steps/FeatureConfigStep.d.ts +6 -0
  35. package/dist/components/Wizard/steps/FeatureConfigStep.js +765 -0
  36. package/dist/components/Wizard/steps/FeaturesStep.d.ts +6 -0
  37. package/dist/components/Wizard/steps/FeaturesStep.js +119 -0
  38. package/dist/components/Wizard/steps/ReviewStep.d.ts +6 -0
  39. package/dist/components/Wizard/steps/ReviewStep.js +56 -0
  40. package/dist/components/Wizard/steps/SMTPStep.d.ts +6 -0
  41. package/dist/components/Wizard/steps/SMTPStep.js +191 -0
  42. package/dist/components/Wizard/steps/SupabaseCredentialsStep.d.ts +6 -0
  43. package/dist/components/Wizard/steps/SupabaseCredentialsStep.js +76 -0
  44. package/dist/components/Wizard/steps/TierStep.d.ts +6 -0
  45. package/dist/components/Wizard/steps/TierStep.js +29 -0
  46. package/dist/components/Wizard/steps/VersionStep.d.ts +6 -0
  47. package/dist/components/Wizard/steps/VersionStep.js +113 -0
  48. package/dist/components/Wizard/steps/index.d.ts +12 -0
  49. package/dist/components/Wizard/steps/index.js +12 -0
  50. package/dist/components/common/AppShell.d.ts +31 -0
  51. package/dist/components/common/AppShell.js +31 -0
  52. package/dist/components/common/Box.d.ts +20 -0
  53. package/dist/components/common/Box.js +20 -0
  54. package/dist/components/common/Logo.d.ts +7 -0
  55. package/dist/components/common/Logo.js +22 -0
  56. package/dist/components/common/Spinner.d.ts +12 -0
  57. package/dist/components/common/Spinner.js +28 -0
  58. package/dist/components/common/index.d.ts +6 -0
  59. package/dist/components/common/index.js +5 -0
  60. package/dist/index.d.ts +2 -0
  61. package/dist/index.js +202 -0
  62. package/dist/lib/cloudCli.d.ts +156 -0
  63. package/dist/lib/cloudCli.js +691 -0
  64. package/dist/lib/config.d.ts +91 -0
  65. package/dist/lib/config.js +278 -0
  66. package/dist/lib/dns.d.ts +41 -0
  67. package/dist/lib/dns.js +235 -0
  68. package/dist/lib/dockerHub.d.ts +57 -0
  69. package/dist/lib/dockerHub.js +128 -0
  70. package/dist/lib/helm.d.ts +53 -0
  71. package/dist/lib/helm.js +209 -0
  72. package/dist/lib/helmValues.d.ts +17 -0
  73. package/dist/lib/helmValues.js +693 -0
  74. package/dist/lib/kubernetes.d.ts +161 -0
  75. package/dist/lib/kubernetes.js +755 -0
  76. package/dist/lib/terraform.d.ts +44 -0
  77. package/dist/lib/terraform.js +230 -0
  78. package/dist/lib/theme.d.ts +81 -0
  79. package/dist/lib/theme.js +115 -0
  80. package/dist/lib/validation.d.ts +47 -0
  81. package/dist/lib/validation.js +164 -0
  82. package/dist/lib/versions.d.ts +69 -0
  83. package/dist/lib/versions.js +139 -0
  84. package/dist/types/index.d.ts +718 -0
  85. package/dist/types/index.js +556 -0
  86. package/email-templates/email_change.html +325 -0
  87. package/email-templates/invite.html +383 -0
  88. package/email-templates/password_change.html +414 -0
  89. package/email-templates/verify.html +396 -0
  90. package/package.json +78 -0
  91. package/terraform/aws/main.tf +327 -0
  92. package/terraform/azure/main.tf +326 -0
  93. package/terraform/gcp/main.tf +369 -0
@@ -0,0 +1,691 @@
1
+ /**
2
+ * Cloud CLI detection and dynamic resource listing
3
+ *
4
+ * Detects installed cloud CLIs (AWS, GCP, Azure), checks authentication status,
5
+ * and provides functions to list regions and buckets dynamically.
6
+ */
7
+ import { exec } from "child_process";
8
+ import { promisify } from "util";
9
+ import { CLOUD_REGIONS } from "../types/index.js";
10
+ const execAsync = promisify(exec);
11
+ // Timeout for CLI commands (in ms)
12
+ const CLI_TIMEOUT = 15000;
13
+ /**
14
+ * Sort regions by priority order defined in CLOUD_REGIONS.
15
+ * Priority regions come first (in their defined order), followed by
16
+ * any additional regions sorted alphabetically.
17
+ */
18
+ function sortRegionsByPriority(regions, provider) {
19
+ const priorityOrder = CLOUD_REGIONS[provider];
20
+ const prioritySet = new Set(priorityOrder);
21
+ // Separate priority regions from others
22
+ const priorityRegions = priorityOrder.filter((r) => regions.includes(r));
23
+ const otherRegions = regions.filter((r) => !prioritySet.has(r)).sort();
24
+ return [...priorityRegions, ...otherRegions];
25
+ }
26
+ /**
27
+ * Execute a CLI command with timeout
28
+ */
29
+ async function execCommand(command, timeout = CLI_TIMEOUT) {
30
+ try {
31
+ const result = await execAsync(command, { timeout });
32
+ return result;
33
+ }
34
+ catch (error) {
35
+ if (error && typeof error === "object" && "stdout" in error) {
36
+ // Command executed but returned non-zero exit code
37
+ const execError = error;
38
+ return {
39
+ stdout: execError.stdout || "",
40
+ stderr: execError.stderr || execError.message || "Command failed",
41
+ };
42
+ }
43
+ throw error;
44
+ }
45
+ }
46
+ // ============================================================================
47
+ // AWS CLI
48
+ // ============================================================================
49
+ /**
50
+ * Check if AWS CLI is installed and authenticated
51
+ */
52
+ export async function checkAwsCli() {
53
+ const status = {
54
+ provider: "aws",
55
+ installed: false,
56
+ authenticated: false,
57
+ };
58
+ try {
59
+ // Check if AWS CLI is installed
60
+ const versionResult = await execCommand("aws --version");
61
+ if (versionResult.stderr && !versionResult.stdout) {
62
+ status.error = "AWS CLI not found";
63
+ return status;
64
+ }
65
+ status.installed = true;
66
+ // Extract version (e.g., "aws-cli/2.13.0 Python/3.11.4 ...")
67
+ const versionMatch = versionResult.stdout.match(/aws-cli\/([\d.]+)/);
68
+ status.version = versionMatch ? versionMatch[1] : undefined;
69
+ // Check authentication by getting caller identity
70
+ const identityResult = await execCommand("aws sts get-caller-identity --output json");
71
+ if (identityResult.stderr &&
72
+ identityResult.stderr.includes("Unable to locate credentials")) {
73
+ status.error =
74
+ 'Not authenticated - run "aws configure" or set credentials';
75
+ return status;
76
+ }
77
+ if (identityResult.stderr &&
78
+ identityResult.stderr.includes("ExpiredToken")) {
79
+ status.error = "Session expired - refresh your credentials";
80
+ return status;
81
+ }
82
+ try {
83
+ const identity = JSON.parse(identityResult.stdout);
84
+ status.authenticated = true;
85
+ status.identity = identity.Account
86
+ ? `Account: ${identity.Account}`
87
+ : undefined;
88
+ }
89
+ catch {
90
+ status.error = "Failed to parse identity response";
91
+ }
92
+ }
93
+ catch (error) {
94
+ status.error = error instanceof Error ? error.message : "Unknown error";
95
+ }
96
+ return status;
97
+ }
98
+ /**
99
+ * List available AWS regions
100
+ */
101
+ export async function listAwsRegions() {
102
+ try {
103
+ const result = await execCommand('aws ec2 describe-regions --query "Regions[].RegionName" --output json');
104
+ if (result.stderr && !result.stdout) {
105
+ return getStaticAwsRegions();
106
+ }
107
+ const regions = JSON.parse(result.stdout);
108
+ return sortRegionsByPriority(regions, "aws");
109
+ }
110
+ catch {
111
+ return getStaticAwsRegions();
112
+ }
113
+ }
114
+ /**
115
+ * List S3 buckets
116
+ */
117
+ export async function listS3Buckets() {
118
+ try {
119
+ const result = await execCommand('aws s3api list-buckets --query "Buckets[].Name" --output json');
120
+ if (result.stderr && !result.stdout) {
121
+ return [];
122
+ }
123
+ const buckets = JSON.parse(result.stdout);
124
+ return buckets.sort();
125
+ }
126
+ catch {
127
+ return [];
128
+ }
129
+ }
130
+ /**
131
+ * Static fallback for AWS regions
132
+ */
133
+ function getStaticAwsRegions() {
134
+ return [
135
+ "us-east-1",
136
+ "us-east-2",
137
+ "us-west-1",
138
+ "us-west-2",
139
+ "ap-south-1",
140
+ "ap-northeast-1",
141
+ "ap-northeast-2",
142
+ "ap-northeast-3",
143
+ "ap-southeast-1",
144
+ "ap-southeast-2",
145
+ "ca-central-1",
146
+ "eu-central-1",
147
+ "eu-west-1",
148
+ "eu-west-2",
149
+ "eu-west-3",
150
+ "eu-north-1",
151
+ "sa-east-1",
152
+ ];
153
+ }
154
+ /**
155
+ * List EKS clusters in a specific region
156
+ */
157
+ export async function listEksClusters(region) {
158
+ try {
159
+ const result = await execCommand(`aws eks list-clusters --region ${region} --output json`);
160
+ if (result.stderr && !result.stdout) {
161
+ return [];
162
+ }
163
+ const response = JSON.parse(result.stdout);
164
+ return (response.clusters || []).sort();
165
+ }
166
+ catch {
167
+ return [];
168
+ }
169
+ }
170
+ // ============================================================================
171
+ // GCP CLI (gcloud)
172
+ // ============================================================================
173
+ /**
174
+ * Check if gcloud CLI is installed and authenticated
175
+ */
176
+ export async function checkGcloudCli() {
177
+ const status = {
178
+ provider: "gcp",
179
+ installed: false,
180
+ authenticated: false,
181
+ };
182
+ try {
183
+ // Check if gcloud is installed
184
+ const versionResult = await execCommand("gcloud --version");
185
+ if (versionResult.stderr && !versionResult.stdout) {
186
+ status.error = "gcloud CLI not found";
187
+ return status;
188
+ }
189
+ status.installed = true;
190
+ // Extract version (e.g., "Google Cloud SDK 440.0.0")
191
+ const versionMatch = versionResult.stdout.match(/Google Cloud SDK ([\d.]+)/);
192
+ status.version = versionMatch ? versionMatch[1] : undefined;
193
+ // Check authentication and active project
194
+ const configResult = await execCommand('gcloud config list --format="json"');
195
+ try {
196
+ const config = JSON.parse(configResult.stdout);
197
+ const account = config.core?.account;
198
+ const project = config.core?.project;
199
+ if (!account) {
200
+ status.error = 'Not authenticated - run "gcloud auth login"';
201
+ return status;
202
+ }
203
+ status.authenticated = true;
204
+ status.identity = project ? `Project: ${project}` : `Account: ${account}`;
205
+ if (!project) {
206
+ status.error =
207
+ 'No default project set - run "gcloud config set project PROJECT_ID"';
208
+ }
209
+ }
210
+ catch {
211
+ status.error = "Failed to parse gcloud config";
212
+ }
213
+ }
214
+ catch (error) {
215
+ status.error = error instanceof Error ? error.message : "Unknown error";
216
+ }
217
+ return status;
218
+ }
219
+ /**
220
+ * Get the active GCP project ID
221
+ */
222
+ export async function getGcpProjectId() {
223
+ try {
224
+ const result = await execCommand("gcloud config get-value project");
225
+ const projectId = result.stdout.trim();
226
+ return projectId && projectId !== "(unset)" ? projectId : null;
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ }
232
+ /**
233
+ * List available GCP regions
234
+ */
235
+ export async function listGcpRegions() {
236
+ try {
237
+ const result = await execCommand('gcloud compute regions list --format="json(name)"');
238
+ if (result.stderr && !result.stdout) {
239
+ return getStaticGcpRegions();
240
+ }
241
+ const regions = JSON.parse(result.stdout);
242
+ const regionNames = regions.map((r) => r.name);
243
+ return sortRegionsByPriority(regionNames, "gcp");
244
+ }
245
+ catch {
246
+ return getStaticGcpRegions();
247
+ }
248
+ }
249
+ /**
250
+ * List GCS buckets
251
+ */
252
+ export async function listGcsBuckets() {
253
+ try {
254
+ const result = await execCommand('gcloud storage buckets list --format="json(name)"');
255
+ if (result.stderr && !result.stdout) {
256
+ return [];
257
+ }
258
+ const buckets = JSON.parse(result.stdout);
259
+ // Bucket names come as "gs://bucket-name", strip the prefix
260
+ return buckets
261
+ .map((b) => b.name.replace("gs://", "").replace(/\/$/, ""))
262
+ .sort();
263
+ }
264
+ catch {
265
+ return [];
266
+ }
267
+ }
268
+ /**
269
+ * Static fallback for GCP regions
270
+ */
271
+ function getStaticGcpRegions() {
272
+ return [
273
+ "us-central1",
274
+ "us-east1",
275
+ "us-east4",
276
+ "us-west1",
277
+ "us-west2",
278
+ "us-west3",
279
+ "us-west4",
280
+ "northamerica-northeast1",
281
+ "northamerica-northeast2",
282
+ "southamerica-east1",
283
+ "southamerica-west1",
284
+ "europe-central2",
285
+ "europe-north1",
286
+ "europe-west1",
287
+ "europe-west2",
288
+ "europe-west3",
289
+ "europe-west4",
290
+ "europe-west6",
291
+ "europe-southwest1",
292
+ "asia-east1",
293
+ "asia-east2",
294
+ "asia-northeast1",
295
+ "asia-northeast2",
296
+ "asia-northeast3",
297
+ "asia-south1",
298
+ "asia-south2",
299
+ "asia-southeast1",
300
+ "asia-southeast2",
301
+ "australia-southeast1",
302
+ "australia-southeast2",
303
+ ];
304
+ }
305
+ /**
306
+ * List GKE clusters in a specific region
307
+ * Note: GKE supports both regional and zonal clusters. We search for regional clusters.
308
+ */
309
+ export async function listGkeClusters(region) {
310
+ try {
311
+ // List clusters in the specified region (includes both regional and zonal clusters in that region)
312
+ const result = await execCommand(`gcloud container clusters list --region ${region} --format="json(name)" 2>/dev/null || gcloud container clusters list --filter="location~^${region}" --format="json(name)"`);
313
+ if (result.stderr && !result.stdout) {
314
+ return [];
315
+ }
316
+ const clusters = JSON.parse(result.stdout);
317
+ return clusters.map((c) => c.name).sort();
318
+ }
319
+ catch {
320
+ return [];
321
+ }
322
+ }
323
+ // ============================================================================
324
+ // Azure CLI
325
+ // ============================================================================
326
+ /**
327
+ * Check if Azure CLI is installed and authenticated
328
+ */
329
+ export async function checkAzureCli() {
330
+ const status = {
331
+ provider: "azure",
332
+ installed: false,
333
+ authenticated: false,
334
+ };
335
+ try {
336
+ // Check if az is installed
337
+ const versionResult = await execCommand("az --version");
338
+ if (versionResult.stderr && !versionResult.stdout) {
339
+ status.error = "Azure CLI not found";
340
+ return status;
341
+ }
342
+ status.installed = true;
343
+ // Extract version (e.g., "azure-cli 2.51.0")
344
+ const versionMatch = versionResult.stdout.match(/azure-cli\s+([\d.]+)/);
345
+ status.version = versionMatch ? versionMatch[1] : undefined;
346
+ // Check authentication
347
+ const accountResult = await execCommand("az account show --output json");
348
+ if (accountResult.stderr && accountResult.stderr.includes("Please run")) {
349
+ status.error = 'Not authenticated - run "az login"';
350
+ return status;
351
+ }
352
+ try {
353
+ const account = JSON.parse(accountResult.stdout);
354
+ status.authenticated = true;
355
+ status.identity = account.name
356
+ ? `Subscription: ${account.name}`
357
+ : undefined;
358
+ }
359
+ catch {
360
+ status.error = "Failed to parse account info";
361
+ }
362
+ }
363
+ catch (error) {
364
+ status.error = error instanceof Error ? error.message : "Unknown error";
365
+ }
366
+ return status;
367
+ }
368
+ /**
369
+ * Get the active Azure subscription ID
370
+ */
371
+ export async function getAzureSubscriptionId() {
372
+ try {
373
+ const result = await execCommand("az account show --query id --output tsv");
374
+ return result.stdout.trim() || null;
375
+ }
376
+ catch {
377
+ return null;
378
+ }
379
+ }
380
+ /**
381
+ * List available Azure regions (locations)
382
+ */
383
+ export async function listAzureRegions() {
384
+ try {
385
+ const result = await execCommand('az account list-locations --query "[].name" --output json');
386
+ if (result.stderr && !result.stdout) {
387
+ return getStaticAzureRegions();
388
+ }
389
+ const regions = JSON.parse(result.stdout);
390
+ return sortRegionsByPriority(regions, "azure");
391
+ }
392
+ catch {
393
+ return getStaticAzureRegions();
394
+ }
395
+ }
396
+ /**
397
+ * List Azure storage accounts (containers require a storage account)
398
+ */
399
+ export async function listAzureStorageAccounts() {
400
+ try {
401
+ const result = await execCommand('az storage account list --query "[].name" --output json');
402
+ if (result.stderr && !result.stdout) {
403
+ return [];
404
+ }
405
+ const accounts = JSON.parse(result.stdout);
406
+ return accounts.sort();
407
+ }
408
+ catch {
409
+ return [];
410
+ }
411
+ }
412
+ /**
413
+ * List Azure blob containers in a storage account
414
+ */
415
+ export async function listAzureBlobContainers(storageAccount) {
416
+ try {
417
+ const result = await execCommand(`az storage container list --account-name ${storageAccount} --auth-mode login --query "[].name" --output json`);
418
+ if (result.stderr && !result.stdout) {
419
+ return [];
420
+ }
421
+ const containers = JSON.parse(result.stdout);
422
+ return containers.sort();
423
+ }
424
+ catch {
425
+ return [];
426
+ }
427
+ }
428
+ /**
429
+ * Static fallback for Azure regions
430
+ */
431
+ function getStaticAzureRegions() {
432
+ return [
433
+ "eastus",
434
+ "eastus2",
435
+ "centralus",
436
+ "northcentralus",
437
+ "southcentralus",
438
+ "westus",
439
+ "westus2",
440
+ "westus3",
441
+ "canadacentral",
442
+ "canadaeast",
443
+ "brazilsouth",
444
+ "northeurope",
445
+ "westeurope",
446
+ "uksouth",
447
+ "ukwest",
448
+ "francecentral",
449
+ "germanywestcentral",
450
+ "switzerlandnorth",
451
+ "norwayeast",
452
+ "eastasia",
453
+ "southeastasia",
454
+ "japaneast",
455
+ "japanwest",
456
+ "koreacentral",
457
+ "australiaeast",
458
+ "australiasoutheast",
459
+ "australiacentral",
460
+ "centralindia",
461
+ "southindia",
462
+ "westindia",
463
+ "uaenorth",
464
+ "southafricanorth",
465
+ ];
466
+ }
467
+ /**
468
+ * List AKS clusters, optionally filtered by resource group
469
+ */
470
+ export async function listAksClusters(resourceGroup) {
471
+ try {
472
+ const rgFilter = resourceGroup ? ` --resource-group ${resourceGroup}` : "";
473
+ const result = await execCommand(`az aks list${rgFilter} --query "[].name" --output json`);
474
+ if (result.stderr && !result.stdout) {
475
+ return [];
476
+ }
477
+ const clusters = JSON.parse(result.stdout);
478
+ return clusters.sort();
479
+ }
480
+ catch {
481
+ return [];
482
+ }
483
+ }
484
+ // ============================================================================
485
+ // Aggregated Functions
486
+ // ============================================================================
487
+ /**
488
+ * Check all cloud CLIs in parallel
489
+ */
490
+ export async function checkAllCloudClis() {
491
+ const [aws, gcp, azure] = await Promise.all([
492
+ checkAwsCli(),
493
+ checkGcloudCli(),
494
+ checkAzureCli(),
495
+ ]);
496
+ const anyInstalled = aws.installed || gcp.installed || azure.installed;
497
+ const anyAvailable = aws.authenticated || gcp.authenticated || azure.authenticated;
498
+ return { aws, gcp, azure, anyAvailable, anyInstalled };
499
+ }
500
+ /**
501
+ * List regions for a specific provider
502
+ */
503
+ export async function listRegions(provider) {
504
+ switch (provider) {
505
+ case "aws":
506
+ return listAwsRegions();
507
+ case "gcp":
508
+ return listGcpRegions();
509
+ case "azure":
510
+ return listAzureRegions();
511
+ default:
512
+ return [];
513
+ }
514
+ }
515
+ /**
516
+ * List buckets/storage for a specific provider
517
+ */
518
+ export async function listBuckets(provider) {
519
+ switch (provider) {
520
+ case "aws":
521
+ return listS3Buckets();
522
+ case "gcp":
523
+ return listGcsBuckets();
524
+ case "azure":
525
+ return listAzureStorageAccounts();
526
+ default:
527
+ return [];
528
+ }
529
+ }
530
+ /**
531
+ * List Kubernetes clusters for a specific provider
532
+ */
533
+ export async function listClusters(provider, region, options) {
534
+ switch (provider) {
535
+ case "aws":
536
+ return listEksClusters(region);
537
+ case "gcp":
538
+ return listGkeClusters(region);
539
+ case "azure":
540
+ return listAksClusters(options?.azureResourceGroup);
541
+ default:
542
+ return [];
543
+ }
544
+ }
545
+ /**
546
+ * Get installation URLs for cloud CLIs
547
+ */
548
+ export const CLI_INSTALL_URLS = {
549
+ aws: {
550
+ name: "AWS CLI",
551
+ url: "https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html",
552
+ installCmd: "brew install awscli",
553
+ },
554
+ gcp: {
555
+ name: "Google Cloud SDK",
556
+ url: "https://cloud.google.com/sdk/docs/install",
557
+ installCmd: "brew install --cask google-cloud-sdk",
558
+ },
559
+ azure: {
560
+ name: "Azure CLI",
561
+ url: "https://docs.microsoft.com/en-us/cli/azure/install-azure-cli",
562
+ installCmd: "brew install azure-cli",
563
+ },
564
+ };
565
+ /**
566
+ * Get login commands for cloud CLIs
567
+ */
568
+ export const CLI_LOGIN_COMMANDS = {
569
+ aws: "aws configure",
570
+ gcp: "gcloud auth login",
571
+ azure: "az login",
572
+ };
573
+ /**
574
+ * Check if Terraform is installed
575
+ */
576
+ export async function checkTerraform() {
577
+ try {
578
+ const result = await execCommand("terraform --version");
579
+ if (result.stderr && !result.stdout) {
580
+ return { installed: false, error: "Terraform not found" };
581
+ }
582
+ // Extract version (e.g., "Terraform v1.5.0")
583
+ const versionMatch = result.stdout.match(/Terraform v([\d.]+)/);
584
+ return {
585
+ installed: true,
586
+ version: versionMatch ? versionMatch[1] : undefined,
587
+ };
588
+ }
589
+ catch {
590
+ return { installed: false, error: "Terraform not found" };
591
+ }
592
+ }
593
+ /**
594
+ * Terraform installation info
595
+ */
596
+ export const TERRAFORM_INSTALL_INFO = {
597
+ name: "Terraform",
598
+ url: "https://developer.hashicorp.com/terraform/downloads",
599
+ installCmd: "brew install terraform",
600
+ };
601
+ // ============================================================================
602
+ // Region-filtered bucket listing
603
+ // ============================================================================
604
+ /**
605
+ * List S3 buckets in a specific region
606
+ * Note: S3 buckets are global, but we filter by region
607
+ */
608
+ export async function listS3BucketsInRegion(region) {
609
+ try {
610
+ // First get all buckets
611
+ const bucketsResult = await execCommand('aws s3api list-buckets --query "Buckets[].Name" --output json');
612
+ if (bucketsResult.stderr && !bucketsResult.stdout) {
613
+ return [];
614
+ }
615
+ const allBuckets = JSON.parse(bucketsResult.stdout);
616
+ // Filter by region - check each bucket's region
617
+ const bucketsInRegion = [];
618
+ for (const bucket of allBuckets) {
619
+ try {
620
+ const locationResult = await execCommand(`aws s3api get-bucket-location --bucket ${bucket} --output json`, 5000);
621
+ if (locationResult.stdout) {
622
+ const location = JSON.parse(locationResult.stdout);
623
+ // null means us-east-1, otherwise it's the region name
624
+ const bucketRegion = location.LocationConstraint || "us-east-1";
625
+ if (bucketRegion === region) {
626
+ bucketsInRegion.push(bucket);
627
+ }
628
+ }
629
+ }
630
+ catch {
631
+ // Skip buckets we can't access
632
+ }
633
+ }
634
+ return bucketsInRegion.sort();
635
+ }
636
+ catch {
637
+ return [];
638
+ }
639
+ }
640
+ /**
641
+ * List GCS buckets in a specific region
642
+ */
643
+ export async function listGcsBucketsInRegion(region) {
644
+ try {
645
+ // GCS locations can be multi-region (US, EU, ASIA) or single region
646
+ // We'll match on the region name (case-insensitive)
647
+ const result = await execCommand(`gcloud storage buckets list --format="json(name,location)"`);
648
+ if (result.stderr && !result.stdout) {
649
+ return [];
650
+ }
651
+ const buckets = JSON.parse(result.stdout);
652
+ return buckets
653
+ .filter((b) => b.location.toLowerCase() === region.toLowerCase())
654
+ .map((b) => b.name.replace("gs://", "").replace(/\/$/, ""))
655
+ .sort();
656
+ }
657
+ catch {
658
+ return [];
659
+ }
660
+ }
661
+ /**
662
+ * List Azure storage accounts in a specific region
663
+ */
664
+ export async function listAzureStorageAccountsInRegion(region) {
665
+ try {
666
+ const result = await execCommand(`az storage account list --query "[?primaryLocation=='${region}'].name" --output json`);
667
+ if (result.stderr && !result.stdout) {
668
+ return [];
669
+ }
670
+ const accounts = JSON.parse(result.stdout);
671
+ return accounts.sort();
672
+ }
673
+ catch {
674
+ return [];
675
+ }
676
+ }
677
+ /**
678
+ * List buckets/storage for a specific provider in a specific region
679
+ */
680
+ export async function listBucketsInRegion(provider, region) {
681
+ switch (provider) {
682
+ case "aws":
683
+ return listS3BucketsInRegion(region);
684
+ case "gcp":
685
+ return listGcsBucketsInRegion(region);
686
+ case "azure":
687
+ return listAzureStorageAccountsInRegion(region);
688
+ default:
689
+ return [];
690
+ }
691
+ }