@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.47

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 (212) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/HEALTH.md +468 -0
  3. package/infrastructure/README.md +51 -0
  4. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  5. package/infrastructure/__tests__/template-generation.test.js +687 -0
  6. package/infrastructure/create-frigg-infrastructure.js +1 -1
  7. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  8. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  9. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  10. package/infrastructure/domains/database/aurora-builder.js +809 -0
  11. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  12. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  13. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  14. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  15. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  16. package/infrastructure/domains/database/migration-builder.js +695 -0
  17. package/infrastructure/domains/database/migration-builder.test.js +294 -0
  18. package/infrastructure/domains/database/migration-resolver.js +163 -0
  19. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  20. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  21. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  22. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  23. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  24. package/infrastructure/domains/health/application/ports/index.js +26 -0
  25. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  26. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  27. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  28. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  29. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  30. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  31. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  32. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  33. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  34. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  35. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  36. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  37. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  38. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  39. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  40. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  41. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  42. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  43. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  44. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  45. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  46. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  47. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  48. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  49. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  50. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  51. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  52. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  53. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  54. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  55. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  56. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  57. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  58. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  59. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  60. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  61. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  62. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  63. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  64. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  65. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  66. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  67. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  68. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  69. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  70. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  71. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  72. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  73. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  74. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  75. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  76. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  77. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  78. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  79. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  80. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  81. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  82. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  83. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  84. package/infrastructure/domains/integration/integration-builder.js +397 -0
  85. package/infrastructure/domains/integration/integration-builder.test.js +593 -0
  86. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  87. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  88. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  89. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  90. package/infrastructure/domains/networking/vpc-builder.js +1829 -0
  91. package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
  92. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  93. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  94. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  95. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  96. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  97. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  98. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  99. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  100. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  101. package/infrastructure/domains/security/kms-builder.js +366 -0
  102. package/infrastructure/domains/security/kms-builder.test.js +374 -0
  103. package/infrastructure/domains/security/kms-discovery.js +80 -0
  104. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  105. package/infrastructure/domains/security/kms-resolver.js +96 -0
  106. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  107. package/infrastructure/domains/shared/base-builder.js +112 -0
  108. package/infrastructure/domains/shared/base-resolver.js +186 -0
  109. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  110. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  111. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  112. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  113. package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
  114. package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
  115. package/infrastructure/domains/shared/environment-builder.js +119 -0
  116. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  117. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
  118. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
  119. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  120. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  121. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  122. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  123. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  124. package/infrastructure/domains/shared/resource-discovery.js +192 -0
  125. package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
  126. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  127. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  128. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  129. package/infrastructure/domains/shared/types/index.js +46 -0
  130. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  131. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  132. package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
  133. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  134. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  135. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  136. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  137. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  138. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
  139. package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
  140. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  141. package/infrastructure/esbuild.config.js +53 -0
  142. package/infrastructure/infrastructure-composer.js +87 -0
  143. package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
  144. package/infrastructure/scripts/build-prisma-layer.js +553 -0
  145. package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
  146. package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
  147. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  148. package/layers/prisma/nodejs/package.json +8 -0
  149. package/management-ui/server/utils/cliIntegration.js +1 -1
  150. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  151. package/package.json +11 -11
  152. package/frigg-cli/.eslintrc.js +0 -141
  153. package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
  154. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
  155. package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
  156. package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
  157. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
  158. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
  159. package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
  160. package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
  161. package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
  162. package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
  163. package/frigg-cli/__tests__/utils/test-setup.js +0 -287
  164. package/frigg-cli/build-command/index.js +0 -65
  165. package/frigg-cli/db-setup-command/index.js +0 -193
  166. package/frigg-cli/deploy-command/index.js +0 -175
  167. package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
  168. package/frigg-cli/generate-command/azure-generator.js +0 -43
  169. package/frigg-cli/generate-command/gcp-generator.js +0 -47
  170. package/frigg-cli/generate-command/index.js +0 -332
  171. package/frigg-cli/generate-command/terraform-generator.js +0 -555
  172. package/frigg-cli/generate-iam-command.js +0 -118
  173. package/frigg-cli/index.js +0 -75
  174. package/frigg-cli/index.test.js +0 -158
  175. package/frigg-cli/init-command/backend-first-handler.js +0 -756
  176. package/frigg-cli/init-command/index.js +0 -93
  177. package/frigg-cli/init-command/template-handler.js +0 -143
  178. package/frigg-cli/install-command/backend-js.js +0 -33
  179. package/frigg-cli/install-command/commit-changes.js +0 -16
  180. package/frigg-cli/install-command/environment-variables.js +0 -127
  181. package/frigg-cli/install-command/environment-variables.test.js +0 -136
  182. package/frigg-cli/install-command/index.js +0 -54
  183. package/frigg-cli/install-command/install-package.js +0 -13
  184. package/frigg-cli/install-command/integration-file.js +0 -30
  185. package/frigg-cli/install-command/logger.js +0 -12
  186. package/frigg-cli/install-command/template.js +0 -90
  187. package/frigg-cli/install-command/validate-package.js +0 -75
  188. package/frigg-cli/jest.config.js +0 -124
  189. package/frigg-cli/package.json +0 -54
  190. package/frigg-cli/start-command/index.js +0 -149
  191. package/frigg-cli/start-command/start-command.test.js +0 -297
  192. package/frigg-cli/test/init-command.test.js +0 -180
  193. package/frigg-cli/test/npm-registry.test.js +0 -319
  194. package/frigg-cli/ui-command/index.js +0 -154
  195. package/frigg-cli/utils/app-resolver.js +0 -319
  196. package/frigg-cli/utils/backend-path.js +0 -25
  197. package/frigg-cli/utils/database-validator.js +0 -161
  198. package/frigg-cli/utils/error-messages.js +0 -257
  199. package/frigg-cli/utils/npm-registry.js +0 -167
  200. package/frigg-cli/utils/prisma-runner.js +0 -280
  201. package/frigg-cli/utils/process-manager.js +0 -199
  202. package/frigg-cli/utils/repo-detection.js +0 -405
  203. package/infrastructure/aws-discovery.js +0 -1176
  204. package/infrastructure/aws-discovery.test.js +0 -1220
  205. package/infrastructure/serverless-template.js +0 -2094
  206. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  207. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  208. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  209. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  210. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  211. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  212. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -0,0 +1,1419 @@
1
+ # Specification: Cleanup Command for Orphaned Resources
2
+
3
+ **Version**: 1.0.0
4
+ **Status**: Draft
5
+ **Created**: 2025-10-27
6
+ **Author**: Claude Code (following user requirements)
7
+
8
+ ## Overview
9
+
10
+ Create a new `frigg cleanup --orphaned` command to safely delete duplicate orphaned resources that are not part of the current CloudFormation stack template. This command helps users clean up leftover resources from previous deployments or failed stacks.
11
+
12
+ ## Business Context
13
+
14
+ ### Problem Statement
15
+
16
+ After implementing logical ID mapping and deduplication for `frigg repair --import`, we discovered that many stacks have duplicate orphaned resources:
17
+
18
+ - Resources with CloudFormation tags (indicating they were once managed)
19
+ - NOT referenced in the current build template (indicating they're no longer needed)
20
+ - Example: 3 VPCs all tagged `FriggVPC`, but only 1 is in the current template
21
+
22
+ These duplicate resources:
23
+ - Cost money (especially VPCs, NAT Gateways, Elastic IPs)
24
+ - Clutter AWS accounts
25
+ - Cause confusion during troubleshooting
26
+ - Are potential security risks (orphaned security groups with open rules)
27
+
28
+ ### User Story
29
+
30
+ > As a DevOps engineer managing Frigg applications
31
+ > I want to safely delete duplicate orphaned resources
32
+ > So that I can reduce costs, improve account hygiene, and eliminate security risks
33
+ > While avoiding accidental deletion of resources that are still in use
34
+
35
+ ### Real-World Example
36
+
37
+ From `quo-integrations-dev` stack:
38
+ - **16 orphaned resources detected**
39
+ - **5 will be imported** (in build template)
40
+ - **11 are duplicates** that should be cleaned up:
41
+ - 2 extra VPCs ($36/month each for NAT Gateway)
42
+ - 7 extra Subnets ($0 but clutter)
43
+ - 2 extra SecurityGroups (potential security risk)
44
+
45
+ **Estimated monthly savings**: ~$72 for just the NAT Gateways
46
+
47
+ ## Requirements
48
+
49
+ ### Functional Requirements
50
+
51
+ #### FR-1: Command Interface
52
+
53
+ ```bash
54
+ # Dry-run mode (default) - show what would be deleted
55
+ frigg cleanup --orphaned
56
+ frigg cleanup --orphaned --dry-run
57
+
58
+ # Execute deletion
59
+ frigg cleanup --orphaned --execute
60
+
61
+ # Target specific stack
62
+ frigg cleanup --orphaned --stack quo-integrations-dev --execute
63
+
64
+ # Auto-confirm (skip confirmation prompts)
65
+ frigg cleanup --orphaned --execute --yes
66
+
67
+ # Clean up specific resource types only
68
+ frigg cleanup --orphaned --resource-type AWS::EC2::VPC --execute
69
+ frigg cleanup --orphaned --resource-type AWS::EC2::Subnet --execute
70
+
71
+ # Filter by logical ID pattern
72
+ frigg cleanup --orphaned --logical-id "Frigg*" --execute
73
+
74
+ # JSON output for scripting
75
+ frigg cleanup --orphaned --output json
76
+ ```
77
+
78
+ #### FR-2: Safety Features
79
+
80
+ **CRITICAL SAFETY REQUIREMENTS**:
81
+
82
+ 1. **Dry-run by default**: Unless `--execute` is specified, only show what would be deleted
83
+ 2. **Dependency checking**: Detect and warn about dependencies before deletion
84
+ 3. **Confirmation prompts**: Require explicit user confirmation before deletion
85
+ 4. **Deletion order**: Delete resources in correct order (subnets before VPCs, etc.)
86
+ 5. **Rollback protection**: Create snapshots/backups where possible
87
+ 6. **Audit logging**: Log all deletion attempts and results
88
+
89
+ **Dependency Detection Examples**:
90
+
91
+ ```javascript
92
+ // VPC dependency check
93
+ if (resourceType === 'AWS::EC2::VPC') {
94
+ const dependencies = [
95
+ await checkForSubnets(vpcId),
96
+ await checkForSecurityGroups(vpcId),
97
+ await checkForNATGateways(vpcId),
98
+ await checkForInternetGateways(vpcId),
99
+ await checkForVPCEndpoints(vpcId),
100
+ await checkForRouteTables(vpcId),
101
+ await checkForNetworkACLs(vpcId),
102
+ ];
103
+
104
+ if (dependencies.some(d => d.hasResources)) {
105
+ throw new Error('Cannot delete VPC: has dependent resources');
106
+ }
107
+ }
108
+
109
+ // Security Group dependency check
110
+ if (resourceType === 'AWS::EC2::SecurityGroup') {
111
+ const usage = [
112
+ await checkForEC2Instances(sgId),
113
+ await checkForRDSInstances(sgId),
114
+ await checkForLambdaFunctions(sgId),
115
+ await checkForLoadBalancers(sgId),
116
+ ];
117
+
118
+ if (usage.some(u => u.hasResources)) {
119
+ throw new Error('Cannot delete SecurityGroup: in use by other resources');
120
+ }
121
+ }
122
+ ```
123
+
124
+ #### FR-3: Deletion Order
125
+
126
+ Resources must be deleted in dependency order:
127
+
128
+ **Phase 1 - Detach Dependencies**:
129
+ 1. VPC Endpoints
130
+ 2. NAT Gateway attachments
131
+ 3. Internet Gateway attachments
132
+ 4. Route table associations
133
+
134
+ **Phase 2 - Delete Dependent Resources**:
135
+ 1. NAT Gateways
136
+ 2. Internet Gateways
137
+ 3. Route Tables (non-default)
138
+ 4. Network ACLs (non-default)
139
+ 5. Subnets
140
+ 6. Security Groups (non-default)
141
+
142
+ **Phase 3 - Delete Core Resources**:
143
+ 1. VPCs
144
+
145
+ **Implementation**:
146
+
147
+ ```javascript
148
+ const DELETION_ORDER = [
149
+ { type: 'AWS::EC2::VPCEndpoint', phase: 1 },
150
+ { type: 'AWS::EC2::NatGateway', phase: 2 },
151
+ { type: 'AWS::EC2::InternetGateway', phase: 2 },
152
+ { type: 'AWS::EC2::RouteTable', phase: 2 },
153
+ { type: 'AWS::EC2::NetworkAcl', phase: 2 },
154
+ { type: 'AWS::EC2::Subnet', phase: 2 },
155
+ { type: 'AWS::EC2::SecurityGroup', phase: 2 },
156
+ { type: 'AWS::EC2::VPC', phase: 3 },
157
+ ];
158
+ ```
159
+
160
+ #### FR-4: Terminal Output
161
+
162
+ **Dry-Run Mode** (default):
163
+
164
+ ```bash
165
+ $ frigg cleanup --orphaned
166
+
167
+ 🧹 Frigg Cleanup - Orphaned Resources
168
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
169
+
170
+ Stack: quo-integrations-dev
171
+ Region: us-east-1
172
+ Mode: DRY-RUN (no resources will be deleted)
173
+
174
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
175
+
176
+ 📋 Analyzing duplicate orphaned resources...
177
+
178
+ Found 11 duplicate resources not in build template:
179
+
180
+ VPCs (2):
181
+ ⚠️ vpc-0e2351eac99adcb83 (FriggVPC)
182
+ • Tags: quo-integrations-dev, Stage: dev
183
+ • Cost: ~$36/month (NAT Gateway)
184
+ • Dependencies: 3 subnets, 1 security group
185
+ • Status: Can be deleted (after dependencies)
186
+
187
+ ⚠️ vpc-020a0365610c05f0b (FriggVPC)
188
+ • Tags: quo-integrations-dev, Stage: dev
189
+ • Cost: ~$36/month (NAT Gateway)
190
+ • Dependencies: 2 subnets, 1 security group
191
+ • Status: Can be deleted (after dependencies)
192
+
193
+ Subnets (7):
194
+ ✓ subnet-0123456789abcdef0 (FriggPrivateSubnet1)
195
+ • Parent VPC: vpc-0e2351eac99adcb83
196
+ • Status: Can be deleted
197
+
198
+ ✓ subnet-0123456789abcdef1 (FriggPrivateSubnet2)
199
+ • Parent VPC: vpc-0e2351eac99adcb83
200
+ • Status: Can be deleted
201
+
202
+ ... (5 more subnets)
203
+
204
+ SecurityGroups (2):
205
+ ⚠️ sg-0123456789abcdef0 (FriggLambdaSecurityGroup)
206
+ • VPC: vpc-0e2351eac99adcb83
207
+ • Rules: 2 ingress, 1 egress
208
+ • In use by: 0 resources
209
+ • Status: Can be deleted
210
+
211
+ ⚠️ sg-0123456789abcdef1 (FriggLambdaSecurityGroup)
212
+ • VPC: vpc-020a0365610c05f0b
213
+ • Rules: 2 ingress, 1 egress
214
+ • In use by: 0 resources
215
+ • Status: Can be deleted
216
+
217
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
218
+
219
+ 📊 CLEANUP SUMMARY
220
+
221
+ Total resources: 11
222
+ • VPCs: 2 (can delete)
223
+ • Subnets: 7 (can delete)
224
+ • SecurityGroups: 2 (can delete)
225
+
226
+ Estimated monthly savings: $72
227
+
228
+ Deletion order:
229
+ Phase 1: Remove attachments (0 resources)
230
+ Phase 2: Delete dependent resources (9 resources)
231
+ Phase 3: Delete core resources (2 VPCs)
232
+
233
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
234
+
235
+ ⚠️ SAFETY WARNINGS
236
+
237
+ • This operation cannot be easily undone
238
+ • Resources will be permanently deleted from AWS
239
+ • Verify no applications depend on these resources
240
+ • Consider taking backups if needed
241
+
242
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
243
+
244
+ To delete these resources, run:
245
+ frigg cleanup --orphaned --execute
246
+
247
+ To review individual resources:
248
+ frigg cleanup --orphaned --output json | jq .
249
+ ```
250
+
251
+ **Execute Mode** (with confirmation):
252
+
253
+ ```bash
254
+ $ frigg cleanup --orphaned --execute
255
+
256
+ 🧹 Frigg Cleanup - Orphaned Resources
257
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
258
+
259
+ Stack: quo-integrations-dev
260
+ Region: us-east-1
261
+ Mode: EXECUTE (resources WILL be deleted)
262
+
263
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
264
+
265
+ ⚠️ WARNING: You are about to DELETE 11 AWS resources
266
+
267
+ This action:
268
+ • Cannot be easily undone
269
+ • Will permanently delete resources from AWS
270
+ • May affect running applications if dependencies exist
271
+ • Will save approximately $72/month
272
+
273
+ Resources to delete:
274
+ • 2 VPCs
275
+ • 7 Subnets
276
+ • 2 SecurityGroups
277
+
278
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
279
+
280
+ Type 'delete quo-integrations-dev' to confirm: delete quo-integrations-dev
281
+
282
+ ✓ Confirmation received. Starting deletion...
283
+
284
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
285
+
286
+ Phase 1: Detaching dependencies...
287
+ (no resources in this phase)
288
+
289
+ Phase 2: Deleting dependent resources...
290
+ [1/9] Deleting subnet-0123456789abcdef0... ✓
291
+ [2/9] Deleting subnet-0123456789abcdef1... ✓
292
+ [3/9] Deleting subnet-0123456789abcdef2... ✓
293
+ [4/9] Deleting subnet-0123456789abcdef3... ✓
294
+ [5/9] Deleting subnet-0123456789abcdef4... ✓
295
+ [6/9] Deleting subnet-0123456789abcdef5... ✓
296
+ [7/9] Deleting subnet-0123456789abcdef6... ✓
297
+ [8/9] Deleting sg-0123456789abcdef0... ✓
298
+ [9/9] Deleting sg-0123456789abcdef1... ✓
299
+
300
+ Phase 3: Deleting core resources...
301
+ [1/2] Deleting vpc-0e2351eac99adcb83... ✓ (20s)
302
+ [2/2] Deleting vpc-020a0365610c05f0b... ✓ (18s)
303
+
304
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
305
+
306
+ ✅ CLEANUP COMPLETE
307
+
308
+ Successfully deleted: 11 resources
309
+ • VPCs: 2
310
+ • Subnets: 7
311
+ • SecurityGroups: 2
312
+
313
+ Failed: 0 resources
314
+
315
+ Estimated monthly savings: $72
316
+
317
+ Total time: 45s
318
+
319
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
320
+
321
+ 💡 Next steps:
322
+ 1. Run 'frigg doctor' to verify health score improved
323
+ 2. Run 'frigg repair --import' to import remaining resources
324
+ ```
325
+
326
+ **Execute Mode** (with --yes flag):
327
+
328
+ ```bash
329
+ $ frigg cleanup --orphaned --execute --yes
330
+
331
+ 🧹 Frigg Cleanup - Orphaned Resources
332
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
333
+
334
+ ⚠️ Auto-confirm enabled (--yes flag)
335
+
336
+ Deleting 11 resources without confirmation...
337
+
338
+ Phase 2: Deleting dependent resources...
339
+ [1/9] Deleting subnet-0123456789abcdef0... ✓
340
+ ... (progress shown)
341
+
342
+ ✅ CLEANUP COMPLETE
343
+ ```
344
+
345
+ #### FR-5: Error Handling
346
+
347
+ **Dependency Errors**:
348
+
349
+ ```bash
350
+ $ frigg cleanup --orphaned --execute
351
+
352
+ Phase 3: Deleting core resources...
353
+ [1/2] Deleting vpc-0e2351eac99adcb83... ✗ FAILED
354
+
355
+ ❌ Error: Cannot delete VPC vpc-0e2351eac99adcb83
356
+ Reason: DependencyViolation - The vpc has dependencies and cannot be deleted
357
+
358
+ Remaining dependencies:
359
+ • Internet Gateway: igw-0123456789abcdef0 (still attached)
360
+ • NAT Gateway: nat-0123456789abcdef0 (still exists)
361
+
362
+ 💡 Run cleanup again to retry after dependency cleanup
363
+ ```
364
+
365
+ **Permission Errors**:
366
+
367
+ ```bash
368
+ ❌ Error: Access Denied
369
+ Reason: User lacks permission to delete VPCs
370
+
371
+ Required IAM permissions:
372
+ • ec2:DeleteVpc
373
+ • ec2:DeleteSubnet
374
+ • ec2:DeleteSecurityGroup
375
+
376
+ 💡 Update your IAM policy and try again
377
+ ```
378
+
379
+ **Resource In Use**:
380
+
381
+ ```bash
382
+ ⚠️ Cannot delete sg-0123456789abcdef0
383
+
384
+ Reason: Security group is in use by:
385
+ • Lambda function: quo-integrations-dev-handler (12345678)
386
+ • RDS instance: quo-integrations-db (i-12345678)
387
+
388
+ ❌ This resource will NOT be deleted
389
+
390
+ 💡 Remove references from these resources first
391
+ ```
392
+
393
+ #### FR-6: JSON Output
394
+
395
+ For scripting and automation:
396
+
397
+ ```json
398
+ {
399
+ "stack": {
400
+ "name": "quo-integrations-dev",
401
+ "region": "us-east-1"
402
+ },
403
+ "mode": "dry-run",
404
+ "resources": {
405
+ "total": 11,
406
+ "canDelete": 11,
407
+ "blocked": 0,
408
+ "byType": {
409
+ "AWS::EC2::VPC": 2,
410
+ "AWS::EC2::Subnet": 7,
411
+ "AWS::EC2::SecurityGroup": 2
412
+ }
413
+ },
414
+ "duplicateResources": [
415
+ {
416
+ "physicalId": "vpc-0e2351eac99adcb83",
417
+ "resourceType": "AWS::EC2::VPC",
418
+ "logicalId": "FriggVPC",
419
+ "tags": {
420
+ "aws:cloudformation:stack-name": "quo-integrations-dev",
421
+ "aws:cloudformation:logical-id": "FriggVPC",
422
+ "Stage": "dev"
423
+ },
424
+ "dependencies": {
425
+ "subnets": ["subnet-0123456789abcdef0", "subnet-0123456789abcdef1", "subnet-0123456789abcdef2"],
426
+ "securityGroups": ["sg-0123456789abcdef0"],
427
+ "hasBlockingDependencies": false
428
+ },
429
+ "estimatedMonthlyCost": 36.00,
430
+ "deletionPhase": 3,
431
+ "canDelete": true,
432
+ "blockingReason": null
433
+ }
434
+ // ... 10 more resources
435
+ ],
436
+ "deletionPlan": {
437
+ "phase1": [],
438
+ "phase2": [
439
+ {
440
+ "physicalId": "subnet-0123456789abcdef0",
441
+ "resourceType": "AWS::EC2::Subnet",
442
+ "order": 1
443
+ }
444
+ // ... 8 more
445
+ ],
446
+ "phase3": [
447
+ {
448
+ "physicalId": "vpc-0e2351eac99adcb83",
449
+ "resourceType": "AWS::EC2::VPC",
450
+ "order": 1
451
+ },
452
+ {
453
+ "physicalId": "vpc-020a0365610c05f0b",
454
+ "resourceType": "AWS::EC2::VPC",
455
+ "order": 2
456
+ }
457
+ ]
458
+ },
459
+ "estimatedSavings": {
460
+ "monthly": 72.00,
461
+ "annual": 864.00
462
+ },
463
+ "warnings": [
464
+ "This operation cannot be easily undone",
465
+ "Resources will be permanently deleted from AWS",
466
+ "Verify no applications depend on these resources"
467
+ ],
468
+ "timestamp": "2025-10-27T10:30:00Z"
469
+ }
470
+ ```
471
+
472
+ ### Non-Functional Requirements
473
+
474
+ #### NFR-1: Performance
475
+
476
+ - Dependency checking must complete within 30 seconds for 50 resources
477
+ - Deletion progress updates every 2 seconds
478
+ - Support parallel deletion where safe (independent resources)
479
+
480
+ #### NFR-2: Reliability
481
+
482
+ - Idempotent operations (safe to retry after failure)
483
+ - Continue on non-fatal errors
484
+ - Comprehensive error logging
485
+
486
+ #### NFR-3: Security
487
+
488
+ - Require explicit confirmation for destructive operations
489
+ - Log all deletion attempts to CloudWatch/audit log
490
+ - Support AWS CloudTrail integration
491
+ - Never expose credentials in logs
492
+
493
+ #### NFR-4: Usability
494
+
495
+ - Clear, informative error messages
496
+ - Progress indicators for long operations
497
+ - Cost estimates to help decision-making
498
+ - Helpful next-step suggestions
499
+
500
+ ## Design
501
+
502
+ ### Architecture
503
+
504
+ Following **Hexagonal Architecture** principles:
505
+
506
+ ```
507
+ ┌─────────────────────────────────────────────────────────────────┐
508
+ │ Adapter Layer (CLI Command) │
509
+ │ cleanup-command/index.js │
510
+ │ - Parse command line options │
511
+ │ - Handle confirmation prompts │
512
+ │ - Format output (terminal, JSON) │
513
+ │ - Display progress indicators │
514
+ └────────────────┬────────────────────────────────────────────────┘
515
+ │ calls
516
+ ┌────────────────▼────────────────────────────────────────────────┐
517
+ │ Application Layer (Use Case) │
518
+ │ CleanupOrphanedResourcesUseCase (NEW) │
519
+ │ - Orchestrate cleanup workflow │
520
+ │ - Identify duplicate resources │
521
+ │ - Check dependencies │
522
+ │ - Plan deletion order │
523
+ │ - Execute deletions with rollback │
524
+ └────────────────┬────────────────────────────────────────────────┘
525
+ │ calls
526
+ ┌────────────────▼────────────────────────────────────────────────┐
527
+ │ Domain Layer (Services) │
528
+ │ OrphanedResourceCategorizerService (REUSE) │
529
+ │ - Identify duplicate orphaned resources │
530
+ │ │
531
+ │ ResourceDependencyAnalyzer (NEW) │
532
+ │ - Analyze resource dependencies │
533
+ │ - Determine deletion order │
534
+ │ - Check for blocking dependencies │
535
+ │ │
536
+ │ ResourceDeletionPlanner (NEW) │
537
+ │ - Create deletion plan with phases │
538
+ │ - Calculate cost savings │
539
+ │ - Generate warnings │
540
+ └────────────────┬────────────────────────────────────────────────┘
541
+ │ uses
542
+ ┌────────────────▼────────────────────────────────────────────────┐
543
+ │ Infrastructure Layer (Repositories) │
544
+ │ ResourceDeleterRepository (NEW) │
545
+ │ - Execute AWS API calls for resource deletion │
546
+ │ - Check resource dependencies via AWS APIs │
547
+ │ - Handle AWS-specific errors │
548
+ │ │
549
+ │ AuditLogRepository (NEW) │
550
+ │ - Log deletion attempts and results │
551
+ │ - Store audit trail │
552
+ └─────────────────────────────────────────────────────────────────┘
553
+ ```
554
+
555
+ ### Component Details
556
+
557
+ #### 1. ResourceDependencyAnalyzer (NEW)
558
+
559
+ **Location**: `packages/devtools/infrastructure/domains/health/domain/services/resource-dependency-analyzer.js`
560
+
561
+ **Responsibilities**:
562
+ - Analyze dependencies between resources
563
+ - Determine safe deletion order
564
+ - Detect blocking dependencies
565
+
566
+ **Public Methods**:
567
+
568
+ ```javascript
569
+ class ResourceDependencyAnalyzer {
570
+ constructor({ ec2Client, elbClient, rdsClient, lambdaClient }) {
571
+ this.ec2 = ec2Client;
572
+ this.elb = elbClient;
573
+ this.rds = rdsClient;
574
+ this.lambda = lambdaClient;
575
+ }
576
+
577
+ /**
578
+ * Analyze dependencies for a list of resources
579
+ *
580
+ * @param {Array} resources - Resources to analyze
581
+ * @returns {Promise<Object>} Dependency analysis
582
+ */
583
+ async analyzeDependencies(resources) {
584
+ const analysis = {
585
+ canDeleteAll: true,
586
+ blockedResources: [],
587
+ dependencies: {},
588
+ };
589
+
590
+ for (const resource of resources) {
591
+ const deps = await this._checkResourceDependencies(resource);
592
+
593
+ analysis.dependencies[resource.physicalId] = deps;
594
+
595
+ if (deps.hasBlockingDependencies) {
596
+ analysis.canDeleteAll = false;
597
+ analysis.blockedResources.push({
598
+ resource,
599
+ blockingDependencies: deps.blocking,
600
+ });
601
+ }
602
+ }
603
+
604
+ return analysis;
605
+ }
606
+
607
+ /**
608
+ * Determine deletion order based on dependencies
609
+ *
610
+ * @param {Array} resources - Resources to order
611
+ * @returns {Object} Deletion plan with phases
612
+ */
613
+ determineDeletionOrder(resources) {
614
+ const phases = {
615
+ phase1: [], // Detach dependencies
616
+ phase2: [], // Delete dependent resources
617
+ phase3: [], // Delete core resources
618
+ };
619
+
620
+ for (const resource of resources) {
621
+ const phase = this._getDeletionPhase(resource.resourceType);
622
+ phases[`phase${phase}`].push(resource);
623
+ }
624
+
625
+ // Sort each phase by priority
626
+ phases.phase1.sort(this._compareDeletionPriority);
627
+ phases.phase2.sort(this._compareDeletionPriority);
628
+ phases.phase3.sort(this._compareDeletionPriority);
629
+
630
+ return phases;
631
+ }
632
+
633
+ /**
634
+ * Check dependencies for a specific resource
635
+ * @private
636
+ */
637
+ async _checkResourceDependencies(resource) {
638
+ switch (resource.resourceType) {
639
+ case 'AWS::EC2::VPC':
640
+ return await this._checkVpcDependencies(resource.physicalId);
641
+ case 'AWS::EC2::Subnet':
642
+ return await this._checkSubnetDependencies(resource.physicalId);
643
+ case 'AWS::EC2::SecurityGroup':
644
+ return await this._checkSecurityGroupDependencies(resource.physicalId);
645
+ default:
646
+ return { hasBlockingDependencies: false, blocking: [], dependent: [] };
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Check VPC dependencies
652
+ * @private
653
+ */
654
+ async _checkVpcDependencies(vpcId) {
655
+ const [subnets, securityGroups, natGateways, igws, endpoints] = await Promise.all([
656
+ this.ec2.describeSubnets({ Filters: [{ Name: 'vpc-id', Values: [vpcId] }] }),
657
+ this.ec2.describeSecurityGroups({ Filters: [{ Name: 'vpc-id', Values: [vpcId] }] }),
658
+ this.ec2.describeNatGateways({ Filter: [{ Name: 'vpc-id', Values: [vpcId] }] }),
659
+ this.ec2.describeInternetGateways({ Filters: [{ Name: 'attachment.vpc-id', Values: [vpcId] }] }),
660
+ this.ec2.describeVpcEndpoints({ Filters: [{ Name: 'vpc-id', Values: [vpcId] }] }),
661
+ ]);
662
+
663
+ const blocking = [];
664
+
665
+ // Filter out default resources that can be deleted with VPC
666
+ const customSubnets = subnets.Subnets.filter(s => !s.DefaultForAz);
667
+ const customSGs = securityGroups.SecurityGroups.filter(sg => sg.GroupName !== 'default');
668
+
669
+ if (customSubnets.length > 0) {
670
+ blocking.push({ type: 'subnets', count: customSubnets.length, ids: customSubnets.map(s => s.SubnetId) });
671
+ }
672
+ if (customSGs.length > 0) {
673
+ blocking.push({ type: 'security_groups', count: customSGs.length, ids: customSGs.map(sg => sg.GroupId) });
674
+ }
675
+ if (natGateways.NatGateways?.length > 0) {
676
+ blocking.push({ type: 'nat_gateways', count: natGateways.NatGateways.length });
677
+ }
678
+ if (igws.InternetGateways?.length > 0) {
679
+ blocking.push({ type: 'internet_gateways', count: igws.InternetGateways.length });
680
+ }
681
+ if (endpoints.VpcEndpoints?.length > 0) {
682
+ blocking.push({ type: 'vpc_endpoints', count: endpoints.VpcEndpoints.length });
683
+ }
684
+
685
+ return {
686
+ hasBlockingDependencies: blocking.length > 0,
687
+ blocking,
688
+ dependent: [],
689
+ };
690
+ }
691
+
692
+ /**
693
+ * Check security group dependencies
694
+ * @private
695
+ */
696
+ async _checkSecurityGroupDependencies(sgId) {
697
+ // Check if security group is in use by any resources
698
+ const [ec2Instances, rdsInstances, lambdaFunctions, loadBalancers] = await Promise.all([
699
+ this.ec2.describeInstances({ Filters: [{ Name: 'instance.group-id', Values: [sgId] }] }),
700
+ this.rds.describeDBInstances(),
701
+ this.lambda.listFunctions(),
702
+ this.elb.describeLoadBalancers(),
703
+ ]);
704
+
705
+ const blocking = [];
706
+
707
+ // Check EC2 instances
708
+ const instances = ec2Instances.Reservations.flatMap(r => r.Instances);
709
+ if (instances.length > 0) {
710
+ blocking.push({
711
+ type: 'ec2_instances',
712
+ count: instances.length,
713
+ ids: instances.map(i => i.InstanceId),
714
+ });
715
+ }
716
+
717
+ // Check RDS instances
718
+ const rdsUsingThisSG = rdsInstances.DBInstances.filter(db =>
719
+ db.VpcSecurityGroups?.some(sg => sg.VpcSecurityGroupId === sgId)
720
+ );
721
+ if (rdsUsingThisSG.length > 0) {
722
+ blocking.push({
723
+ type: 'rds_instances',
724
+ count: rdsUsingThisSG.length,
725
+ ids: rdsUsingThisSG.map(db => db.DBInstanceIdentifier),
726
+ });
727
+ }
728
+
729
+ // Check Lambda functions
730
+ const lambdaUsingThisSG = lambdaFunctions.Functions.filter(fn =>
731
+ fn.VpcConfig?.SecurityGroupIds?.includes(sgId)
732
+ );
733
+ if (lambdaUsingThisSG.length > 0) {
734
+ blocking.push({
735
+ type: 'lambda_functions',
736
+ count: lambdaUsingThisSG.length,
737
+ ids: lambdaUsingThisSG.map(fn => fn.FunctionName),
738
+ });
739
+ }
740
+
741
+ return {
742
+ hasBlockingDependencies: blocking.length > 0,
743
+ blocking,
744
+ dependent: [],
745
+ };
746
+ }
747
+
748
+ /**
749
+ * Get deletion phase for resource type
750
+ * @private
751
+ */
752
+ _getDeletionPhase(resourceType) {
753
+ const phaseMap = {
754
+ 'AWS::EC2::VPCEndpoint': 1,
755
+ 'AWS::EC2::NatGateway': 2,
756
+ 'AWS::EC2::InternetGateway': 2,
757
+ 'AWS::EC2::RouteTable': 2,
758
+ 'AWS::EC2::NetworkAcl': 2,
759
+ 'AWS::EC2::Subnet': 2,
760
+ 'AWS::EC2::SecurityGroup': 2,
761
+ 'AWS::EC2::VPC': 3,
762
+ };
763
+
764
+ return phaseMap[resourceType] || 2;
765
+ }
766
+
767
+ /**
768
+ * Compare deletion priority
769
+ * @private
770
+ */
771
+ _compareDeletionPriority(a, b) {
772
+ const priorityMap = {
773
+ 'AWS::EC2::VPCEndpoint': 1,
774
+ 'AWS::EC2::NatGateway': 2,
775
+ 'AWS::EC2::InternetGateway': 3,
776
+ 'AWS::EC2::RouteTable': 4,
777
+ 'AWS::EC2::Subnet': 5,
778
+ 'AWS::EC2::SecurityGroup': 6,
779
+ 'AWS::EC2::VPC': 7,
780
+ };
781
+
782
+ return (priorityMap[a.resourceType] || 99) - (priorityMap[b.resourceType] || 99);
783
+ }
784
+ }
785
+ ```
786
+
787
+ #### 2. ResourceDeletionPlanner (NEW)
788
+
789
+ **Location**: `packages/devtools/infrastructure/domains/health/domain/services/resource-deletion-planner.js`
790
+
791
+ **Responsibilities**:
792
+ - Create detailed deletion plan
793
+ - Calculate cost savings
794
+ - Generate warnings
795
+
796
+ **Public Methods**:
797
+
798
+ ```javascript
799
+ class ResourceDeletionPlanner {
800
+ /**
801
+ * Create deletion plan with phases and cost estimates
802
+ *
803
+ * @param {Object} params
804
+ * @param {Array} params.resources - Resources to delete
805
+ * @param {Object} params.dependencyAnalysis - Dependency analysis result
806
+ * @returns {Object} Deletion plan
807
+ */
808
+ createDeletionPlan({ resources, dependencyAnalysis }) {
809
+ // 1. Filter out blocked resources
810
+ const deletableResources = resources.filter(
811
+ (r) => !dependencyAnalysis.blockedResources.some(
812
+ (b) => b.resource.physicalId === r.physicalId
813
+ )
814
+ );
815
+
816
+ // 2. Determine deletion order
817
+ const deletionPhases = this._organizeDeletionPhases(deletableResources);
818
+
819
+ // 3. Calculate cost savings
820
+ const costSavings = this._calculateCostSavings(deletableResources);
821
+
822
+ // 4. Generate warnings
823
+ const warnings = this._generateWarnings(deletableResources, dependencyAnalysis);
824
+
825
+ return {
826
+ totalResources: resources.length,
827
+ deletableCount: deletableResources.length,
828
+ blockedCount: dependencyAnalysis.blockedResources.length,
829
+ phases: deletionPhases,
830
+ costSavings,
831
+ warnings,
832
+ blockedResources: dependencyAnalysis.blockedResources,
833
+ };
834
+ }
835
+
836
+ /**
837
+ * Calculate estimated monthly cost savings
838
+ * @private
839
+ */
840
+ _calculateCostSavings(resources) {
841
+ let monthlyCost = 0;
842
+
843
+ for (const resource of resources) {
844
+ switch (resource.resourceType) {
845
+ case 'AWS::EC2::VPC':
846
+ // Assume NAT Gateway cost (most expensive part of VPC)
847
+ monthlyCost += 36; // $32.40 for NAT + $0.045/GB
848
+ break;
849
+ case 'AWS::EC2::NatGateway':
850
+ monthlyCost += 36;
851
+ break;
852
+ case 'AWS::EC2::EIP':
853
+ monthlyCost += 3.65; // $0.005/hour
854
+ break;
855
+ // Other resources have minimal direct costs
856
+ }
857
+ }
858
+
859
+ return {
860
+ monthly: monthlyCost,
861
+ annual: monthlyCost * 12,
862
+ };
863
+ }
864
+
865
+ /**
866
+ * Generate warnings for deletion
867
+ * @private
868
+ */
869
+ _generateWarnings(resources, dependencyAnalysis) {
870
+ const warnings = [
871
+ 'This operation cannot be easily undone',
872
+ 'Resources will be permanently deleted from AWS',
873
+ 'Verify no applications depend on these resources',
874
+ ];
875
+
876
+ // Add specific warnings based on resource types
877
+ if (resources.some(r => r.resourceType === 'AWS::EC2::VPC')) {
878
+ warnings.push('Deleting VPCs will also delete associated default resources');
879
+ }
880
+
881
+ if (dependencyAnalysis.blockedResources.length > 0) {
882
+ warnings.push(
883
+ `${dependencyAnalysis.blockedResources.length} resources cannot be deleted due to dependencies`
884
+ );
885
+ }
886
+
887
+ return warnings;
888
+ }
889
+ }
890
+ ```
891
+
892
+ #### 3. CleanupOrphanedResourcesUseCase (NEW)
893
+
894
+ **Location**: `packages/devtools/infrastructure/domains/health/application/use-cases/cleanup-orphaned-resources-use-case.js`
895
+
896
+ **Responsibilities**:
897
+ - Orchestrate complete cleanup workflow
898
+ - Coordinate categorizer, analyzer, planner, and deleter
899
+ - Handle errors and rollback
900
+
901
+ **Public Methods**:
902
+
903
+ ```javascript
904
+ class CleanupOrphanedResourcesUseCase {
905
+ constructor({
906
+ orphanedResourceCategorizerService,
907
+ resourceDependencyAnalyzer,
908
+ resourceDeletionPlanner,
909
+ resourceDeleterRepository,
910
+ auditLogRepository,
911
+ }) {
912
+ this.categorizerService = orphanedResourceCategorizerService;
913
+ this.dependencyAnalyzer = resourceDependencyAnalyzer;
914
+ this.deletionPlanner = resourceDeletionPlanner;
915
+ this.deleterRepo = resourceDeleterRepository;
916
+ this.auditRepo = auditLogRepository;
917
+ }
918
+
919
+ /**
920
+ * Execute cleanup workflow
921
+ *
922
+ * @param {Object} params
923
+ * @param {StackIdentifier} params.stackIdentifier
924
+ * @param {string} params.buildTemplatePath
925
+ * @param {boolean} params.dryRun - If true, only plan, don't execute
926
+ * @param {string} params.resourceTypeFilter - Optional resource type filter
927
+ * @param {string} params.logicalIdPattern - Optional logical ID pattern
928
+ * @returns {Promise<Object>} Cleanup result
929
+ */
930
+ async execute({
931
+ stackIdentifier,
932
+ buildTemplatePath,
933
+ dryRun = true,
934
+ resourceTypeFilter = null,
935
+ logicalIdPattern = null,
936
+ }) {
937
+ // 1. Get duplicate orphaned resources
938
+ const categorization = await this.categorizerService.categorize({
939
+ orphanedResources: [], // Will be fetched internally
940
+ stackIdentifier,
941
+ buildTemplatePath,
942
+ });
943
+
944
+ let duplicates = categorization.duplicates || [];
945
+
946
+ // 2. Apply filters
947
+ if (resourceTypeFilter) {
948
+ duplicates = duplicates.filter((r) => r.resourceType === resourceTypeFilter);
949
+ }
950
+ if (logicalIdPattern) {
951
+ const regex = new RegExp(logicalIdPattern.replace('*', '.*'));
952
+ duplicates = duplicates.filter((r) => regex.test(r.logicalId));
953
+ }
954
+
955
+ if (duplicates.length === 0) {
956
+ return {
957
+ success: true,
958
+ message: 'No duplicate orphaned resources found',
959
+ deletedCount: 0,
960
+ skippedCount: 0,
961
+ };
962
+ }
963
+
964
+ // 3. Analyze dependencies
965
+ const dependencyAnalysis = await this.dependencyAnalyzer.analyzeDependencies(duplicates);
966
+
967
+ // 4. Create deletion plan
968
+ const deletionPlan = this.deletionPlanner.createDeletionPlan({
969
+ resources: duplicates,
970
+ dependencyAnalysis,
971
+ });
972
+
973
+ // 5. If dry-run, return plan without executing
974
+ if (dryRun) {
975
+ return {
976
+ dryRun: true,
977
+ deletionPlan,
978
+ message: 'Dry-run complete. No resources were deleted.',
979
+ };
980
+ }
981
+
982
+ // 6. Execute deletions
983
+ const deletionResult = await this._executeDeletionPlan(
984
+ deletionPlan,
985
+ stackIdentifier
986
+ );
987
+
988
+ // 7. Log to audit trail
989
+ await this.auditRepo.logCleanupOperation({
990
+ stackIdentifier,
991
+ deletionPlan,
992
+ result: deletionResult,
993
+ timestamp: new Date().toISOString(),
994
+ });
995
+
996
+ return {
997
+ success: true,
998
+ dryRun: false,
999
+ deletedCount: deletionResult.successCount,
1000
+ failedCount: deletionResult.failedCount,
1001
+ skippedCount: deletionPlan.blockedCount,
1002
+ deletionResult,
1003
+ costSavings: deletionPlan.costSavings,
1004
+ };
1005
+ }
1006
+
1007
+ /**
1008
+ * Execute deletion plan phase by phase
1009
+ * @private
1010
+ */
1011
+ async _executeDeletionPlan(deletionPlan, stackIdentifier) {
1012
+ const results = {
1013
+ successCount: 0,
1014
+ failedCount: 0,
1015
+ deleted: [],
1016
+ failed: [],
1017
+ };
1018
+
1019
+ // Execute in order: phase1, phase2, phase3
1020
+ for (const phase of ['phase1', 'phase2', 'phase3']) {
1021
+ const resources = deletionPlan.phases[phase];
1022
+
1023
+ for (const resource of resources) {
1024
+ try {
1025
+ await this.deleterRepo.deleteResource({
1026
+ resourceType: resource.resourceType,
1027
+ physicalId: resource.physicalId,
1028
+ region: stackIdentifier.region,
1029
+ });
1030
+
1031
+ results.successCount++;
1032
+ results.deleted.push({
1033
+ physicalId: resource.physicalId,
1034
+ resourceType: resource.resourceType,
1035
+ });
1036
+ } catch (error) {
1037
+ results.failedCount++;
1038
+ results.failed.push({
1039
+ physicalId: resource.physicalId,
1040
+ resourceType: resource.resourceType,
1041
+ error: error.message,
1042
+ });
1043
+ }
1044
+
1045
+ // Small delay between deletions to avoid throttling
1046
+ await this._delay(500);
1047
+ }
1048
+
1049
+ // Delay between phases to allow AWS to clean up
1050
+ if (resources.length > 0) {
1051
+ await this._delay(5000);
1052
+ }
1053
+ }
1054
+
1055
+ return results;
1056
+ }
1057
+
1058
+ _delay(ms) {
1059
+ return new Promise((resolve) => setTimeout(resolve, ms));
1060
+ }
1061
+ }
1062
+ ```
1063
+
1064
+ #### 4. ResourceDeleterRepository (NEW)
1065
+
1066
+ **Location**: `packages/devtools/infrastructure/domains/health/infrastructure/repositories/resource-deleter-repository.js`
1067
+
1068
+ **Responsibilities**:
1069
+ - Execute AWS API calls for resource deletion
1070
+ - Handle AWS-specific errors
1071
+
1072
+ **Public Methods**:
1073
+
1074
+ ```javascript
1075
+ class ResourceDeleterRepository {
1076
+ constructor({ ec2Client, region }) {
1077
+ this.ec2 = ec2Client;
1078
+ this.region = region;
1079
+ }
1080
+
1081
+ /**
1082
+ * Delete a resource from AWS
1083
+ *
1084
+ * @param {Object} params
1085
+ * @param {string} params.resourceType - CloudFormation resource type
1086
+ * @param {string} params.physicalId - Physical resource ID
1087
+ * @param {string} params.region - AWS region
1088
+ * @returns {Promise<Object>} Deletion result
1089
+ */
1090
+ async deleteResource({ resourceType, physicalId, region }) {
1091
+ try {
1092
+ switch (resourceType) {
1093
+ case 'AWS::EC2::VPC':
1094
+ return await this._deleteVpc(physicalId);
1095
+ case 'AWS::EC2::Subnet':
1096
+ return await this._deleteSubnet(physicalId);
1097
+ case 'AWS::EC2::SecurityGroup':
1098
+ return await this._deleteSecurityGroup(physicalId);
1099
+ // Add more resource types as needed
1100
+ default:
1101
+ throw new Error(`Unsupported resource type: ${resourceType}`);
1102
+ }
1103
+ } catch (error) {
1104
+ throw new Error(
1105
+ `Failed to delete ${resourceType} ${physicalId}: ${error.message}`
1106
+ );
1107
+ }
1108
+ }
1109
+
1110
+ /**
1111
+ * Delete VPC
1112
+ * @private
1113
+ */
1114
+ async _deleteVpc(vpcId) {
1115
+ await this.ec2.deleteVpc({ VpcId: vpcId });
1116
+ return { success: true, physicalId: vpcId };
1117
+ }
1118
+
1119
+ /**
1120
+ * Delete Subnet
1121
+ * @private
1122
+ */
1123
+ async _deleteSubnet(subnetId) {
1124
+ await this.ec2.deleteSubnet({ SubnetId: subnetId });
1125
+ return { success: true, physicalId: subnetId };
1126
+ }
1127
+
1128
+ /**
1129
+ * Delete Security Group
1130
+ * @private
1131
+ */
1132
+ async _deleteSecurityGroup(sgId) {
1133
+ await this.ec2.deleteSecurityGroup({ GroupId: sgId });
1134
+ return { success: true, physicalId: sgId };
1135
+ }
1136
+ }
1137
+ ```
1138
+
1139
+ ### Data Flow
1140
+
1141
+ ```
1142
+ User runs: frigg cleanup --orphaned --execute
1143
+
1144
+ cleanup-command/index.js
1145
+ ↓ calls
1146
+ CleanupOrphanedResourcesUseCase.execute()
1147
+ ↓ calls
1148
+ OrphanedResourceCategorizerService.categorize()
1149
+ ↓ returns
1150
+ { duplicates: [...] }
1151
+
1152
+ ResourceDependencyAnalyzer.analyzeDependencies()
1153
+ ↓ returns
1154
+ { blockedResources: [...], dependencies: {...} }
1155
+
1156
+ ResourceDeletionPlanner.createDeletionPlan()
1157
+ ↓ returns
1158
+ { phases: {...}, costSavings: {...}, warnings: [...] }
1159
+
1160
+ Display confirmation prompt (unless --yes)
1161
+ ↓ if confirmed
1162
+ _executeDeletionPlan()
1163
+ ↓ phase by phase
1164
+ ResourceDeleterRepository.deleteResource()
1165
+ ↓ for each resource
1166
+ AWS API calls (deleteVpc, deleteSubnet, etc.)
1167
+
1168
+ AuditLogRepository.logCleanupOperation()
1169
+
1170
+ Display result summary
1171
+ ```
1172
+
1173
+ ## Implementation Plan
1174
+
1175
+ ### Phase 1: Core Dependency Analysis (TDD)
1176
+
1177
+ 1. **Write tests** for `ResourceDependencyAnalyzer`
1178
+ - Test VPC dependency checking
1179
+ - Test security group dependency checking
1180
+ - Test deletion order determination
1181
+ - Test blocking dependency detection
1182
+
1183
+ 2. **Implement** `ResourceDependencyAnalyzer`
1184
+ - Implement dependency checking methods
1185
+ - Implement deletion order logic
1186
+ - Handle AWS API calls
1187
+
1188
+ 3. **Verify** tests pass
1189
+
1190
+ ### Phase 2: Deletion Planning (TDD)
1191
+
1192
+ 1. **Write tests** for `ResourceDeletionPlanner`
1193
+ - Test deletion plan creation
1194
+ - Test cost calculation
1195
+ - Test warning generation
1196
+ - Test phase organization
1197
+
1198
+ 2. **Implement** `ResourceDeletionPlanner`
1199
+ - Implement plan creation
1200
+ - Implement cost calculator
1201
+ - Implement warning generator
1202
+
1203
+ 3. **Verify** tests pass
1204
+
1205
+ ### Phase 3: Use Case Orchestration (TDD)
1206
+
1207
+ 1. **Write tests** for `CleanupOrphanedResourcesUseCase`
1208
+ - Test dry-run mode
1209
+ - Test execute mode
1210
+ - Test filtering (resource type, logical ID)
1211
+ - Test error handling
1212
+ - Test rollback behavior
1213
+
1214
+ 2. **Implement** use case
1215
+ - Orchestrate all services
1216
+ - Implement deletion execution
1217
+ - Handle errors gracefully
1218
+
1219
+ 3. **Verify** tests pass
1220
+
1221
+ ### Phase 4: Infrastructure Repository (TDD)
1222
+
1223
+ 1. **Write tests** for `ResourceDeleterRepository`
1224
+ - Test VPC deletion
1225
+ - Test subnet deletion
1226
+ - Test security group deletion
1227
+ - Test error handling
1228
+
1229
+ 2. **Implement** repository
1230
+ - AWS SDK integration
1231
+ - Error handling and retries
1232
+
1233
+ 3. **Verify** tests pass
1234
+
1235
+ ### Phase 5: CLI Integration
1236
+
1237
+ 1. **Create** `cleanup-command/index.js`
1238
+ - Command line parsing
1239
+ - Confirmation prompts
1240
+ - Output formatting
1241
+ - Progress indicators
1242
+
1243
+ 2. **Test** with real data
1244
+ - Test dry-run mode
1245
+ - Test execute mode
1246
+ - Test error scenarios
1247
+ - Test with `quo-integrations-dev`
1248
+
1249
+ 3. **Update documentation**
1250
+
1251
+ ## Testing Strategy
1252
+
1253
+ ### Unit Tests
1254
+
1255
+ **ResourceDependencyAnalyzer**:
1256
+ - ✅ Analyze VPC with dependencies
1257
+ - ✅ Analyze VPC without dependencies
1258
+ - ✅ Analyze security group in use
1259
+ - ✅ Analyze security group not in use
1260
+ - ✅ Determine deletion order correctly
1261
+ - ✅ Handle API errors gracefully
1262
+
1263
+ **ResourceDeletionPlanner**:
1264
+ - ✅ Create plan with multiple phases
1265
+ - ✅ Calculate cost savings accurately
1266
+ - ✅ Generate appropriate warnings
1267
+ - ✅ Handle blocked resources
1268
+ - ✅ Filter resources correctly
1269
+
1270
+ **CleanupOrphanedResourcesUseCase**:
1271
+ - ✅ Dry-run returns plan without deleting
1272
+ - ✅ Execute mode deletes resources
1273
+ - ✅ Stop on first error (optional behavior)
1274
+ - ✅ Continue on non-fatal errors
1275
+ - ✅ Filter by resource type
1276
+ - ✅ Filter by logical ID pattern
1277
+ - ✅ Log to audit trail
1278
+
1279
+ **ResourceDeleterRepository**:
1280
+ - ✅ Delete VPC successfully
1281
+ - ✅ Delete subnet successfully
1282
+ - ✅ Delete security group successfully
1283
+ - ✅ Handle dependency errors
1284
+ - ✅ Handle permission errors
1285
+ - ✅ Retry on throttling
1286
+
1287
+ ### Integration Tests
1288
+
1289
+ - ✅ End-to-end cleanup with mock AWS data
1290
+ - ✅ Verify deletion order is correct
1291
+ - ✅ Verify blocked resources are not deleted
1292
+ - ✅ Verify audit log is written
1293
+ - ✅ Test with `quo-integrations-dev` stack (11 duplicate resources)
1294
+
1295
+ ### Real-World Validation
1296
+
1297
+ Use `quo-integrations-dev` stack as reference:
1298
+ - 11 duplicate orphaned resources
1299
+ - 2 VPCs with dependencies (9 total)
1300
+ - Verify all dependencies are detected
1301
+ - Verify deletion plan is safe
1302
+ - Execute in isolated test environment
1303
+
1304
+ ## Security Considerations
1305
+
1306
+ ### 1. Permission Requirements
1307
+
1308
+ **Minimum IAM Policy**:
1309
+
1310
+ ```json
1311
+ {
1312
+ "Version": "2012-10-17",
1313
+ "Statement": [
1314
+ {
1315
+ "Effect": "Allow",
1316
+ "Action": [
1317
+ "ec2:DeleteVpc",
1318
+ "ec2:DeleteSubnet",
1319
+ "ec2:DeleteSecurityGroup",
1320
+ "ec2:DeleteInternetGateway",
1321
+ "ec2:DeleteNatGateway",
1322
+ "ec2:DeleteRouteTable",
1323
+ "ec2:DeleteVpcEndpoint",
1324
+ "ec2:DescribeVpcs",
1325
+ "ec2:DescribeSubnets",
1326
+ "ec2:DescribeSecurityGroups",
1327
+ "ec2:DescribeInstances",
1328
+ "ec2:DescribeNatGateways",
1329
+ "ec2:DescribeInternetGateways",
1330
+ "ec2:DescribeVpcEndpoints"
1331
+ ],
1332
+ "Resource": "*",
1333
+ "Condition": {
1334
+ "StringEquals": {
1335
+ "ec2:ResourceTag/ManagedBy": "Frigg"
1336
+ }
1337
+ }
1338
+ }
1339
+ ]
1340
+ }
1341
+ ```
1342
+
1343
+ ### 2. Audit Logging
1344
+
1345
+ All cleanup operations must be logged:
1346
+
1347
+ ```javascript
1348
+ {
1349
+ "operation": "cleanup_orphaned_resources",
1350
+ "timestamp": "2025-10-27T10:30:00Z",
1351
+ "user": "user@example.com",
1352
+ "stackName": "quo-integrations-dev",
1353
+ "region": "us-east-1",
1354
+ "mode": "execute",
1355
+ "resources": {
1356
+ "total": 11,
1357
+ "deleted": 11,
1358
+ "failed": 0
1359
+ },
1360
+ "deletedResources": [
1361
+ {
1362
+ "physicalId": "vpc-0e2351eac99adcb83",
1363
+ "resourceType": "AWS::EC2::VPC",
1364
+ "logicalId": "FriggVPC",
1365
+ "deletedAt": "2025-10-27T10:30:15Z"
1366
+ }
1367
+ // ... 10 more
1368
+ ],
1369
+ "costSavings": {
1370
+ "monthly": 72.00,
1371
+ "annual": 864.00
1372
+ }
1373
+ }
1374
+ ```
1375
+
1376
+ ### 3. Safety Mechanisms
1377
+
1378
+ - ✅ Dry-run by default
1379
+ - ✅ Explicit confirmation required
1380
+ - ✅ Dependency checking before deletion
1381
+ - ✅ Deletion order enforcement
1382
+ - ✅ Audit trail for all operations
1383
+ - ✅ Support for --yes flag (automation)
1384
+ - ✅ Permission checks before deletion
1385
+ - ✅ Resource tagging verification (only delete Frigg-managed resources)
1386
+
1387
+ ## Success Criteria
1388
+
1389
+ 1. ✅ Command correctly identifies duplicate orphaned resources
1390
+ 2. ✅ Dependency analysis detects blocking dependencies
1391
+ 3. ✅ Deletion plan orders resources correctly
1392
+ 4. ✅ Dry-run mode shows plan without deleting
1393
+ 5. ✅ Execute mode deletes resources safely
1394
+ 6. ✅ Error handling is robust (permissions, dependencies, API errors)
1395
+ 7. ✅ All deletions are logged to audit trail
1396
+ 8. ✅ Cost savings are calculated accurately
1397
+ 9. ✅ All tests pass (unit, integration, real-world validation)
1398
+ 10. ✅ Documentation is complete
1399
+
1400
+ ## Open Questions
1401
+
1402
+ 1. **Rollback strategy**: Should we support rollback if deletion fails partway through? (Complex: resources can't be "undeleted")
1403
+ 2. **Backup before delete**: Should we export resource configurations before deletion for recovery purposes?
1404
+ 3. **Concurrent deletion**: Should we delete independent resources in parallel for speed?
1405
+ 4. **Retry policy**: How many retries for throttling errors? What backoff strategy?
1406
+ 5. **Confirmation format**: Is `delete <stack-name>` sufficient, or should we require typing the exact resource count?
1407
+ 6. **Cost estimation**: Should we call AWS Pricing API for accurate costs, or use hardcoded estimates?
1408
+
1409
+ ## Related Specifications
1410
+
1411
+ - [SPEC-ENHANCED-HEALTH-REPORT.md](./SPEC-ENHANCED-HEALTH-REPORT.md) - Enhanced health report with resource categorization
1412
+ - [FIX-SUMMARY.md](../debug/FIX-SUMMARY.md) - Root cause analysis and fixes for logical ID mapping
1413
+
1414
+ ## References
1415
+
1416
+ - User testing with `quo-integrations-dev` stack (11 duplicate resources)
1417
+ - Real-world data documented in `ACTUAL-DATA-SHAPE.md`
1418
+ - AWS EC2 API documentation for resource deletion
1419
+ - CloudFormation resource import documentation