@grada-run/grada 0.0.1 → 0.33.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 (242) hide show
  1. package/.github/workflows/deploy-docs.yml +37 -0
  2. package/.github/workflows/e2e.yml +73 -0
  3. package/.github/workflows/iac-validation.yml +303 -0
  4. package/.github/workflows/publish.yml +68 -0
  5. package/.github/workflows/sync-bedrock-models.yml +57 -0
  6. package/.github/workflows/test.yml +43 -0
  7. package/.muserules +31 -0
  8. package/LICENSE +21 -0
  9. package/README.md +193 -3
  10. package/apps/docs/.astro/collections/docs.schema.json +644 -0
  11. package/apps/docs/.astro/content-assets.mjs +4 -0
  12. package/apps/docs/.astro/content-modules.mjs +4 -0
  13. package/apps/docs/.astro/content.d.ts +179 -0
  14. package/apps/docs/.astro/data-store.json +1 -0
  15. package/apps/docs/.astro/dev.json +14 -0
  16. package/apps/docs/.astro/settings.json +5 -0
  17. package/apps/docs/.astro/types.d.ts +2 -0
  18. package/apps/docs/astro.config.mjs +97 -0
  19. package/apps/docs/package.json +17 -0
  20. package/apps/docs/src/content/docs/adrs/0001-s3-native-state-locking.md +37 -0
  21. package/apps/docs/src/content/docs/adrs/0002-eject-mechanism-pure-iac.md +39 -0
  22. package/apps/docs/src/content/docs/adrs/0003-sync-ai-context-strategy.md +48 -0
  23. package/apps/docs/src/content/docs/adrs/0004-iac-driven-diagnostic-context.md +37 -0
  24. package/apps/docs/src/content/docs/adrs/0005-ecs-fargate-alb-runtime-target.md +38 -0
  25. package/apps/docs/src/content/docs/adrs/0006-github-oidc-no-stored-keys.md +37 -0
  26. package/apps/docs/src/content/docs/adrs/0007-framework-detection-with-fallback.md +37 -0
  27. package/apps/docs/src/content/docs/adrs/0008-secrets-names-in-git-values-in-aws.md +37 -0
  28. package/apps/docs/src/content/docs/adrs/0009-regenerate-with-backup-on-rerun.md +37 -0
  29. package/apps/docs/src/content/docs/adrs/0010-advisory-only-security-scans.md +37 -0
  30. package/apps/docs/src/content/docs/cli/add.md +85 -0
  31. package/apps/docs/src/content/docs/cli/apply.md +35 -0
  32. package/apps/docs/src/content/docs/cli/db.md +200 -0
  33. package/apps/docs/src/content/docs/cli/destroy.md +35 -0
  34. package/apps/docs/src/content/docs/cli/diagnose.md +37 -0
  35. package/apps/docs/src/content/docs/cli/doctor.md +28 -0
  36. package/apps/docs/src/content/docs/cli/domain.md +57 -0
  37. package/apps/docs/src/content/docs/cli/drift.md +40 -0
  38. package/apps/docs/src/content/docs/cli/eject.md +33 -0
  39. package/apps/docs/src/content/docs/cli/exec.md +49 -0
  40. package/apps/docs/src/content/docs/cli/gc.md +37 -0
  41. package/apps/docs/src/content/docs/cli/init.md +72 -0
  42. package/apps/docs/src/content/docs/cli/logs.md +39 -0
  43. package/apps/docs/src/content/docs/cli/rollback.md +51 -0
  44. package/apps/docs/src/content/docs/cli/secrets.md +73 -0
  45. package/apps/docs/src/content/docs/cli/sleep.md +53 -0
  46. package/apps/docs/src/content/docs/cli/status.md +34 -0
  47. package/apps/docs/src/content/docs/cli/sync-ai.md +27 -0
  48. package/apps/docs/src/content/docs/guides/architecture.md +87 -0
  49. package/apps/docs/src/content/docs/guides/aws-credentials.md +72 -0
  50. package/apps/docs/src/content/docs/guides/background-workers.md +45 -0
  51. package/apps/docs/src/content/docs/guides/cicd-pipeline.md +64 -0
  52. package/apps/docs/src/content/docs/guides/database-connections.md +64 -0
  53. package/apps/docs/src/content/docs/guides/docker-compose.md +37 -0
  54. package/apps/docs/src/content/docs/guides/dockerfiles.md +46 -0
  55. package/apps/docs/src/content/docs/guides/ephemeral-pr-previews.md +39 -0
  56. package/apps/docs/src/content/docs/guides/examples.md +50 -0
  57. package/apps/docs/src/content/docs/guides/frameworks.md +88 -0
  58. package/apps/docs/src/content/docs/guides/headless.md +87 -0
  59. package/apps/docs/src/content/docs/guides/quickstart.md +52 -0
  60. package/apps/docs/src/content/docs/guides/rerun-init.md +43 -0
  61. package/apps/docs/src/content/docs/guides/secrets-management.md +83 -0
  62. package/apps/docs/src/content/docs/guides/understanding-your-bill.md +63 -0
  63. package/apps/docs/src/content/docs/index.mdx +103 -0
  64. package/apps/docs/src/content/docs/migrations/astro-vercel-to-aws.md +55 -0
  65. package/apps/docs/src/content/docs/migrations/heroku-procfile-to-aws.md +41 -0
  66. package/apps/docs/src/content/docs/migrations/nextjs-vercel-to-aws.md +51 -0
  67. package/apps/docs/src/content/docs/migrations/sveltekit-vercel-to-aws.md +63 -0
  68. package/apps/docs/src/content/docs/roadmap.md +99 -0
  69. package/apps/docs/src/content/docs/testing-strategy.md +37 -0
  70. package/apps/docs/src/content.config.ts +7 -0
  71. package/apps/docs/src/custom.css +14 -0
  72. package/apps/docs/tsconfig.json +6 -0
  73. package/bin/cli.js +140 -0
  74. package/package.json +107 -7
  75. package/scripts/sync-bedrock-models.js +22 -0
  76. package/scripts/test-iac.js +261 -0
  77. package/specs/add-redis-sqs-bedrock.md +128 -0
  78. package/specs/add-storage-dynamodb.md +106 -0
  79. package/specs/bedrock-model-catalog.md +131 -0
  80. package/specs/ci-pipeline.md +17 -0
  81. package/specs/cost-transparency.md +115 -0
  82. package/specs/custom-domains-and-ses.md +153 -0
  83. package/specs/database-suite-expansion.md +151 -0
  84. package/specs/db-connect.md +69 -0
  85. package/specs/db-lifecycle-migrations.md +159 -0
  86. package/specs/dependency-aware-init.md +176 -0
  87. package/specs/deployment-safety.md +170 -0
  88. package/specs/diagnose.md +16 -0
  89. package/specs/docs-hub.md +16 -0
  90. package/specs/dx-polish.md +46 -0
  91. package/specs/e2e-testing.md +50 -0
  92. package/specs/exec.md +25 -0
  93. package/specs/finops-cron-drift.md +161 -0
  94. package/specs/gc.md +26 -0
  95. package/specs/integration-suite.md +16 -0
  96. package/specs/logs.md +32 -0
  97. package/specs/rollback-live-polling.md +40 -0
  98. package/specs/secrets-pull-audit.md +51 -0
  99. package/specs/serverless-lambda-target.md +133 -0
  100. package/specs/status.md +31 -0
  101. package/specs/telemetry-and-spawn-hardening.md +69 -0
  102. package/specs/telemetry-hardening.md +35 -0
  103. package/specs/test-suite-deduplication.md +42 -0
  104. package/src/commands/add.js +1111 -0
  105. package/src/commands/apply.js +214 -0
  106. package/src/commands/db/backup.js +229 -0
  107. package/src/commands/db/connect.js +304 -0
  108. package/src/commands/db/enable-vector.js +344 -0
  109. package/src/commands/db/import.js +604 -0
  110. package/src/commands/db/migrate.js +477 -0
  111. package/src/commands/db/restore.js +361 -0
  112. package/src/commands/db.js +87 -0
  113. package/src/commands/destroy.js +217 -0
  114. package/src/commands/diagnose.js +460 -0
  115. package/src/commands/doctor.js +109 -0
  116. package/src/commands/domain.js +685 -0
  117. package/src/commands/drift.js +243 -0
  118. package/src/commands/eject.js +130 -0
  119. package/src/commands/exec.js +222 -0
  120. package/src/commands/gc.js +250 -0
  121. package/src/commands/init.js +649 -0
  122. package/src/commands/logs.js +256 -0
  123. package/src/commands/rollback.js +323 -0
  124. package/src/commands/secrets.js +485 -0
  125. package/src/commands/sleep.js +347 -0
  126. package/src/commands/status.js +309 -0
  127. package/src/commands/sync-ai.js +115 -0
  128. package/src/commands/wake.js +337 -0
  129. package/src/core/parser.js +128 -0
  130. package/src/core/telemetry.js +244 -0
  131. package/src/data/bedrock-models.json +896 -0
  132. package/src/utils/addons.js +126 -0
  133. package/src/utils/ai-rules.js +59 -0
  134. package/src/utils/args.js +91 -0
  135. package/src/utils/aws.js +178 -0
  136. package/src/utils/backup.js +69 -0
  137. package/src/utils/bedrock-catalog.js +511 -0
  138. package/src/utils/capabilities.js +500 -0
  139. package/src/utils/command.js +78 -0
  140. package/src/utils/db-tunnel.js +164 -0
  141. package/src/utils/detector.js +298 -0
  142. package/src/utils/dockerCompose.js +65 -0
  143. package/src/utils/domains.js +73 -0
  144. package/src/utils/ecs-runner.js +289 -0
  145. package/src/utils/ecs.js +92 -0
  146. package/src/utils/frameworks.js +55 -0
  147. package/src/utils/generator.js +527 -0
  148. package/src/utils/hcl.js +426 -0
  149. package/src/utils/lambda-ecr.js +185 -0
  150. package/src/utils/prompts.js +278 -0
  151. package/src/utils/rds.js +131 -0
  152. package/src/utils/resolvers.js +174 -0
  153. package/src/utils/sleep-state.js +140 -0
  154. package/src/utils/sleep-targets.js +139 -0
  155. package/src/utils/system.js +42 -0
  156. package/src/utils/terraform.js +70 -0
  157. package/src/utils/visualizer.js +381 -0
  158. package/src/utils/warnings.js +49 -0
  159. package/templates/README.md +150 -0
  160. package/templates/docker/django.Dockerfile +40 -0
  161. package/templates/docker/go.Dockerfile +23 -0
  162. package/templates/docker/nestjs.Dockerfile +33 -0
  163. package/templates/docker/nextjs.Dockerfile +55 -0
  164. package/templates/docker/node.Dockerfile +24 -0
  165. package/templates/docker/nuxt.Dockerfile +47 -0
  166. package/templates/docker/python.Dockerfile +38 -0
  167. package/templates/docker/rails.Dockerfile +59 -0
  168. package/templates/docker/static.Dockerfile +32 -0
  169. package/templates/docker/svelte.Dockerfile +52 -0
  170. package/templates/github/deploy-lambda.yml +120 -0
  171. package/templates/github/deploy.yml +138 -0
  172. package/templates/github/drift.yml +112 -0
  173. package/templates/github/preview-lambda.yml +86 -0
  174. package/templates/github/preview.yml +69 -0
  175. package/templates/github/teardown.yml +43 -0
  176. package/templates/terraform/addons/bedrock.tf +34 -0
  177. package/templates/terraform/addons/cron-lambda.tf +78 -0
  178. package/templates/terraform/addons/cron.tf +101 -0
  179. package/templates/terraform/addons/dynamodb.tf +73 -0
  180. package/templates/terraform/addons/redis.tf +64 -0
  181. package/templates/terraform/addons/s3.tf +143 -0
  182. package/templates/terraform/addons/ses.tf +73 -0
  183. package/templates/terraform/addons/sqs.tf +67 -0
  184. package/templates/terraform/backend.tf +22 -0
  185. package/templates/terraform/cloudfront-lambda.tf +80 -0
  186. package/templates/terraform/cloudfront.tf +80 -0
  187. package/templates/terraform/database-aurora-postgresql.tf +92 -0
  188. package/templates/terraform/database-mysql.tf +72 -0
  189. package/templates/terraform/database.tf +71 -0
  190. package/templates/terraform/main-lambda.tf +229 -0
  191. package/templates/terraform/main.tf +296 -0
  192. package/templates/terraform/network.tf +95 -0
  193. package/templates/terraform/oidc.tf +64 -0
  194. package/templates/terraform/secrets.tf +31 -0
  195. package/templates/terraform/worker.tf +69 -0
  196. package/tests/__snapshots__/generator.test.js.snap +9633 -0
  197. package/tests/add.test.js +2018 -0
  198. package/tests/ai.test.js +91 -0
  199. package/tests/apply.test.js +446 -0
  200. package/tests/args.test.js +86 -0
  201. package/tests/aws.test.js +244 -0
  202. package/tests/capabilities.test.js +304 -0
  203. package/tests/cli.test.js +29 -0
  204. package/tests/command.test.js +99 -0
  205. package/tests/commands-import.test.js +74 -0
  206. package/tests/db.test.js +2673 -0
  207. package/tests/destroy.test.js +365 -0
  208. package/tests/detector.test.js +79 -0
  209. package/tests/diagnose.test.js +765 -0
  210. package/tests/doctor.test.js +195 -0
  211. package/tests/domain.test.js +882 -0
  212. package/tests/drift.test.js +228 -0
  213. package/tests/e2e/helpers.js +122 -0
  214. package/tests/e2e/tier0.e2e.test.js +142 -0
  215. package/tests/e2e/tier1.live.e2e.test.js +118 -0
  216. package/tests/ecs.test.js +130 -0
  217. package/tests/eject.test.js +65 -0
  218. package/tests/exec.test.js +366 -0
  219. package/tests/gc.test.js +466 -0
  220. package/tests/generator.test.js +794 -0
  221. package/tests/headless.test.js +491 -0
  222. package/tests/helpers/clack.js +103 -0
  223. package/tests/helpers/console.js +41 -0
  224. package/tests/helpers/telemetry.js +37 -0
  225. package/tests/helpers/tmpdir.js +36 -0
  226. package/tests/lambda-ecr.test.js +185 -0
  227. package/tests/logs.test.js +436 -0
  228. package/tests/parser.test.js +160 -0
  229. package/tests/rds.test.js +244 -0
  230. package/tests/resolvers.test.js +279 -0
  231. package/tests/rollback.test.js +670 -0
  232. package/tests/secrets.test.js +729 -0
  233. package/tests/sleep-wake.test.js +998 -0
  234. package/tests/status.test.js +356 -0
  235. package/tests/system.test.js +70 -0
  236. package/tests/telemetry.test.js +520 -0
  237. package/tests/terraform.test.js +84 -0
  238. package/tests/visualizer.test.js +480 -0
  239. package/vitest.config.js +10 -0
  240. package/vitest.e2e.tier0.config.js +8 -0
  241. package/vitest.e2e.tier1.config.js +8 -0
  242. package/index.js +0 -2
@@ -0,0 +1,250 @@
1
+ import { ECRClient, DescribeRepositoriesCommand, DescribeImagesCommand, BatchDeleteImageCommand } from '@aws-sdk/client-ecr';
2
+ import { CloudWatchLogsClient, DescribeLogGroupsCommand, DeleteLogGroupCommand } from '@aws-sdk/client-cloudwatch-logs';
3
+ import { EC2Client, DescribeAddressesCommand, ReleaseAddressCommand } from '@aws-sdk/client-ec2';
4
+ import color from 'picocolors';
5
+ import { intro, outro, confirm, spinner, cancel } from '@clack/prompts';
6
+ import { trackEvent, flushTelemetry, trackSuccess, trackFailure } from '../core/telemetry.js';
7
+ import { parseFlags, normalizeOptions, normalizeArgv } from '../utils/args.js';
8
+ import { resolveRegion, resolveProjectName, resolveCwd, readTerraformComputeTarget } from '../utils/resolvers.js';
9
+ import { resolveClient } from '../utils/aws.js';
10
+ import { failProjectNotInitialized } from '../utils/command.js';
11
+
12
+ export const CONFIRM_MESSAGE = 'Are you sure you want to permanently delete these orphaned resources? (y/N)';
13
+
14
+ export function parseGcArgs(argv = []) {
15
+ const args = normalizeArgv(argv);
16
+ if (args[0] === 'gc') args.shift();
17
+ // NOTE: intentionally no --yes flag. Deletion always requires
18
+ // explicit interactive confirmation to prevent CI accidents.
19
+ const { options } = parseFlags(args, { string: ['region', 'project-name'] });
20
+ return options;
21
+ }
22
+
23
+ export function isUntaggedImageDetail(detail = {}) {
24
+ if (Array.isArray(detail.imageTags)) return detail.imageTags.length === 0;
25
+ if (typeof detail.imageTag === 'string') return detail.imageTag.trim().length === 0;
26
+ if (detail.imageTag == null && detail.imageDigest) return true;
27
+ return !detail.imageTags;
28
+ }
29
+
30
+ export function filterUntaggedImages(imageDetails = []) {
31
+ return (imageDetails || []).filter(isUntaggedImageDetail);
32
+ }
33
+
34
+ export function isUnattachedAddress(address = {}) {
35
+ return !address.AssociationId;
36
+ }
37
+
38
+ export function filterUnattachedAddresses(addresses = []) {
39
+ return (addresses || []).filter(isUnattachedAddress);
40
+ }
41
+
42
+ function matchesProjectRepo(repositoryName, projectName) {
43
+ return typeof repositoryName === 'string' && repositoryName.startsWith(`${projectName}-`);
44
+ }
45
+
46
+ export async function discoverOrphanedResources({ ecrClient, logsClient, ec2Client, projectName, logGroupPrefix, excludeLogGroup }) {
47
+ const untaggedImages = [];
48
+ const orphanedLogGroups = [];
49
+ const unattachedEips = [];
50
+
51
+ // Target 1: untagged ECR images in project-prefixed repos.
52
+ // Both DescribeRepositories and DescribeImages are paginated.
53
+ const repositories = [];
54
+ let reposNextToken;
55
+ do {
56
+ const reposResp = await ecrClient.send(new DescribeRepositoriesCommand({ nextToken: reposNextToken }));
57
+ for (const repo of reposResp.repositories || []) {
58
+ if (matchesProjectRepo(repo.repositoryName, projectName)) repositories.push(repo);
59
+ }
60
+ reposNextToken = reposResp.nextToken;
61
+ } while (reposNextToken);
62
+ for (const repo of repositories) {
63
+ let imagesNextToken;
64
+ do {
65
+ const imagesResp = await ecrClient.send(
66
+ new DescribeImagesCommand({ repositoryName: repo.repositoryName, nextToken: imagesNextToken })
67
+ );
68
+ for (const detail of filterUntaggedImages(imagesResp.imageDetails || [])) {
69
+ if (!detail.imageDigest) continue;
70
+ untaggedImages.push({
71
+ repositoryName: repo.repositoryName,
72
+ imageDigest: detail.imageDigest,
73
+ imageSizeInBytes: detail.imageSizeInBytes,
74
+ imagePushedAt: detail.imagePushedAt,
75
+ });
76
+ }
77
+ imagesNextToken = imagesResp.nextToken;
78
+ } while (imagesNextToken);
79
+ }
80
+
81
+ // Target 2: orphaned CloudWatch log groups for deleted preview environments.
82
+ // The live service log group is `/ecs/<project>` (Lambda: `/aws/lambda/<project>-fn`,
83
+ // no trailing dash after the project name); preview leftovers carry a
84
+ // `-pr-*` suffix, so a prefix scan isolates candidates.
85
+ const groupPrefix = logGroupPrefix || `/ecs/${projectName}-`;
86
+ let nextToken;
87
+ do {
88
+ const logsResp = await logsClient.send(
89
+ new DescribeLogGroupsCommand({ logGroupNamePrefix: groupPrefix, nextToken })
90
+ );
91
+ for (const group of logsResp.logGroups || []) {
92
+ // The Lambda live group (`/aws/lambda/<project>-fn`) matches the
93
+ // preview prefix scan, so it is excluded explicitly — it must
94
+ // never be proposed for deletion.
95
+ if (group.logGroupName && group.logGroupName !== excludeLogGroup) {
96
+ orphanedLogGroups.push({ logGroupName: group.logGroupName, storedBytes: group.storedBytes });
97
+ }
98
+ }
99
+ nextToken = logsResp.nextToken;
100
+ } while (nextToken);
101
+
102
+ // Target 3: unattached Elastic IPs (hourly charge when idle).
103
+ const addressesResp = await ec2Client.send(new DescribeAddressesCommand({}));
104
+ for (const address of filterUnattachedAddresses(addressesResp.Addresses || [])) {
105
+ unattachedEips.push({ PublicIp: address.PublicIp, AllocationId: address.AllocationId });
106
+ }
107
+
108
+ return {
109
+ untaggedImages,
110
+ orphanedLogGroups,
111
+ unattachedEips,
112
+ totalCount: untaggedImages.length + orphanedLogGroups.length + unattachedEips.length,
113
+ };
114
+ }
115
+
116
+ export const ECR_BATCH_DELETE_LIMIT = 100;
117
+
118
+ export function chunkArray(items = [], size = ECR_BATCH_DELETE_LIMIT) {
119
+ const chunks = [];
120
+ for (let i = 0; i < items.length; i += size) {
121
+ chunks.push(items.slice(i, i + size));
122
+ }
123
+ return chunks;
124
+ }
125
+
126
+ export async function deleteDiscoveredResources({ ecrClient, logsClient, ec2Client }, discovered) {
127
+ let deletedImages = 0;
128
+ let deletedLogGroups = 0;
129
+ let releasedEips = 0;
130
+
131
+ // Group untagged images by repository for batch deletion.
132
+ // BatchDeleteImage accepts at most 100 image IDs per request.
133
+ const byRepo = new Map();
134
+ for (const image of discovered.untaggedImages || []) {
135
+ if (!byRepo.has(image.repositoryName)) byRepo.set(image.repositoryName, []);
136
+ byRepo.get(image.repositoryName).push({ imageDigest: image.imageDigest });
137
+ }
138
+ for (const [repositoryName, imageIds] of byRepo) {
139
+ for (const batch of chunkArray(imageIds, ECR_BATCH_DELETE_LIMIT)) {
140
+ await ecrClient.send(new BatchDeleteImageCommand({ repositoryName, imageIds: batch }));
141
+ deletedImages += batch.length;
142
+ }
143
+ }
144
+
145
+ for (const group of discovered.orphanedLogGroups || []) {
146
+ await logsClient.send(new DeleteLogGroupCommand({ logGroupName: group.logGroupName }));
147
+ deletedLogGroups += 1;
148
+ }
149
+
150
+ for (const eip of discovered.unattachedEips || []) {
151
+ await ec2Client.send(new ReleaseAddressCommand({ AllocationId: eip.AllocationId }));
152
+ releasedEips += 1;
153
+ }
154
+
155
+ return { deletedImages, deletedLogGroups, releasedEips };
156
+ }
157
+
158
+ function printDiscovery(discovered) {
159
+ console.log('');
160
+ console.log(` ${color.bold('Untagged ECR images:')} ${discovered.untaggedImages.length}`);
161
+ for (const image of discovered.untaggedImages.slice(0, 10)) {
162
+ console.log(` ${color.dim('-')} ${image.repositoryName}@${String(image.imageDigest).slice(0, 19)}`);
163
+ }
164
+ if (discovered.untaggedImages.length > 10) {
165
+ console.log(` ${color.dim(`…and ${discovered.untaggedImages.length - 10} more`)}`);
166
+ }
167
+ console.log(` ${color.bold('Orphaned log groups:')} ${discovered.orphanedLogGroups.length}`);
168
+ for (const group of discovered.orphanedLogGroups.slice(0, 10)) {
169
+ console.log(` ${color.dim('-')} ${group.logGroupName}`);
170
+ }
171
+ if (discovered.orphanedLogGroups.length > 10) {
172
+ console.log(` ${color.dim(`…and ${discovered.orphanedLogGroups.length - 10} more`)}`);
173
+ }
174
+ console.log(` ${color.bold('Unattached Elastic IPs:')} ${discovered.unattachedEips.length}`);
175
+ for (const eip of discovered.unattachedEips) {
176
+ console.log(` ${color.dim('-')} ${eip.PublicIp || eip.AllocationId}`);
177
+ }
178
+ console.log('');
179
+ }
180
+
181
+ export async function runGc(input = {}) {
182
+ const options = normalizeOptions(input);
183
+ let cwd;
184
+ let region;
185
+ let projectName;
186
+ try {
187
+ cwd = resolveCwd(options);
188
+ region = resolveRegion(options, cwd);
189
+ projectName = resolveProjectName(options, cwd);
190
+ } catch {
191
+ return failProjectNotInitialized({ event: 'gc_run' });
192
+ }
193
+
194
+ const ecrClient = resolveClient(options.ecrClient, ECRClient, { region });
195
+ const logsClient = resolveClient(options.logsClient, CloudWatchLogsClient, { region });
196
+ const ec2Client = resolveClient(options.ec2Client, EC2Client, { region });
197
+
198
+ intro(color.bgCyan(color.black(' grada gc 🧹 ')));
199
+
200
+ const s = spinner();
201
+ s.start('Scanning for orphaned resources (dry run)...');
202
+
203
+ let discovered;
204
+ try {
205
+ const isLambda = readTerraformComputeTarget(cwd) === 'lambda';
206
+ discovered = await discoverOrphanedResources({
207
+ ecrClient,
208
+ logsClient,
209
+ ec2Client,
210
+ projectName,
211
+ logGroupPrefix: isLambda ? `/aws/lambda/${projectName}-` : undefined,
212
+ excludeLogGroup: isLambda ? `/aws/lambda/${projectName}-fn` : undefined,
213
+ });
214
+ } catch (error) {
215
+ s.stop(color.red('❌ Discovery failed.'));
216
+ console.log(color.red(`✖ ${error?.message || error}`));
217
+ await trackFailure('gc_run', { projectName, error_message: error?.message });
218
+ throw error;
219
+ }
220
+ s.stop('Discovery complete.');
221
+
222
+ console.log(color.bold(`\nDry run — orphaned resources for project ${color.cyan(projectName)}:`));
223
+ printDiscovery(discovered);
224
+ console.log(color.dim(`Total orphaned resources: ${discovered.totalCount}`));
225
+
226
+ if (discovered.totalCount === 0) {
227
+ outro(color.green('No orphaned resources found. ✅'));
228
+ await trackSuccess('gc_run', { projectName, deleted: false, total: 0 });
229
+ return { ...discovered, deleted: false };
230
+ }
231
+
232
+ // Safety: only a literal `true` from the interactive prompt proceeds.
233
+ // Anything else (false, undefined, cancelled symbol) aborts deletion.
234
+ const confirmed = await confirm({ message: color.yellow(CONFIRM_MESSAGE), initialValue: false });
235
+
236
+ if (confirmed !== true) {
237
+ cancel('Cancelled. No resources were deleted.');
238
+ await trackSuccess('gc_run', { projectName, deleted: false, total: discovered.totalCount, confirmed: false });
239
+ return { ...discovered, deleted: false, confirmed: false };
240
+ }
241
+
242
+ const del = spinner();
243
+ del.start('Deleting orphaned resources...');
244
+ const summary = await deleteDiscoveredResources({ ecrClient, logsClient, ec2Client }, discovered);
245
+ del.stop('Deletion complete.');
246
+
247
+ outro(color.green(`Deleted ${summary.deletedImages} image(s), ${summary.deletedLogGroups} log group(s), released ${summary.releasedEips} Elastic IP(s). ✅`));
248
+ await trackSuccess('gc_run', { projectName, deleted: true, ...summary, total: discovered.totalCount });
249
+ return { ...discovered, ...summary, deleted: true, confirmed: true };
250
+ }