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

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 (69) hide show
  1. package/frigg-cli/README.md +1290 -0
  2. package/frigg-cli/__tests__/unit/commands/build.test.js +279 -0
  3. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +548 -0
  4. package/frigg-cli/__tests__/unit/commands/deploy.test.js +320 -0
  5. package/frigg-cli/__tests__/unit/commands/doctor.test.js +309 -0
  6. package/frigg-cli/__tests__/unit/commands/install.test.js +400 -0
  7. package/frigg-cli/__tests__/unit/commands/ui.test.js +346 -0
  8. package/frigg-cli/__tests__/unit/dependencies.test.js +74 -0
  9. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +366 -0
  10. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +304 -0
  11. package/frigg-cli/__tests__/unit/version-detection.test.js +171 -0
  12. package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
  13. package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
  14. package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
  15. package/frigg-cli/__tests__/utils/test-setup.js +287 -0
  16. package/frigg-cli/build-command/index.js +66 -0
  17. package/frigg-cli/db-setup-command/index.js +193 -0
  18. package/frigg-cli/deploy-command/SPEC-DEPLOY-DRY-RUN.md +981 -0
  19. package/frigg-cli/deploy-command/index.js +302 -0
  20. package/frigg-cli/doctor-command/index.js +335 -0
  21. package/frigg-cli/generate-command/__tests__/generate-command.test.js +301 -0
  22. package/frigg-cli/generate-command/azure-generator.js +43 -0
  23. package/frigg-cli/generate-command/gcp-generator.js +47 -0
  24. package/frigg-cli/generate-command/index.js +332 -0
  25. package/frigg-cli/generate-command/terraform-generator.js +555 -0
  26. package/frigg-cli/generate-iam-command.js +118 -0
  27. package/frigg-cli/index.js +173 -0
  28. package/frigg-cli/index.test.js +158 -0
  29. package/frigg-cli/init-command/backend-first-handler.js +756 -0
  30. package/frigg-cli/init-command/index.js +93 -0
  31. package/frigg-cli/init-command/template-handler.js +143 -0
  32. package/frigg-cli/install-command/backend-js.js +33 -0
  33. package/frigg-cli/install-command/commit-changes.js +16 -0
  34. package/frigg-cli/install-command/environment-variables.js +127 -0
  35. package/frigg-cli/install-command/environment-variables.test.js +136 -0
  36. package/frigg-cli/install-command/index.js +54 -0
  37. package/frigg-cli/install-command/install-package.js +13 -0
  38. package/frigg-cli/install-command/integration-file.js +30 -0
  39. package/frigg-cli/install-command/logger.js +12 -0
  40. package/frigg-cli/install-command/template.js +90 -0
  41. package/frigg-cli/install-command/validate-package.js +75 -0
  42. package/frigg-cli/jest.config.js +124 -0
  43. package/frigg-cli/package.json +63 -0
  44. package/frigg-cli/repair-command/index.js +564 -0
  45. package/frigg-cli/start-command/index.js +149 -0
  46. package/frigg-cli/start-command/start-command.test.js +297 -0
  47. package/frigg-cli/test/init-command.test.js +180 -0
  48. package/frigg-cli/test/npm-registry.test.js +319 -0
  49. package/frigg-cli/ui-command/index.js +154 -0
  50. package/frigg-cli/utils/app-resolver.js +319 -0
  51. package/frigg-cli/utils/backend-path.js +25 -0
  52. package/frigg-cli/utils/database-validator.js +154 -0
  53. package/frigg-cli/utils/error-messages.js +257 -0
  54. package/frigg-cli/utils/npm-registry.js +167 -0
  55. package/frigg-cli/utils/process-manager.js +199 -0
  56. package/frigg-cli/utils/repo-detection.js +405 -0
  57. package/infrastructure/create-frigg-infrastructure.js +125 -12
  58. package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +1317 -0
  59. package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +306 -0
  60. package/infrastructure/domains/shared/resource-discovery.js +31 -2
  61. package/infrastructure/domains/shared/utilities/base-definition-factory.js +1 -1
  62. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +109 -5
  63. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +310 -4
  64. package/infrastructure/domains/shared/validation/plugin-validator.js +187 -0
  65. package/infrastructure/domains/shared/validation/plugin-validator.test.js +323 -0
  66. package/infrastructure/infrastructure-composer.js +22 -0
  67. package/layers/prisma/.build-complete +3 -0
  68. package/package.json +18 -7
  69. package/management-ui/package-lock.json +0 -16517
@@ -0,0 +1,564 @@
1
+ /**
2
+ * Frigg Repair Command
3
+ *
4
+ * Repairs infrastructure issues detected by frigg doctor:
5
+ * - Import orphaned resources into CloudFormation stack
6
+ * - Reconcile property drift between template and actual resources
7
+ *
8
+ * Usage:
9
+ * frigg repair --import <stack-name>
10
+ * frigg repair --reconcile <stack-name>
11
+ * frigg repair --import --reconcile <stack-name> # Fix all issues
12
+ */
13
+
14
+ const path = require('path');
15
+ const readline = require('readline');
16
+
17
+ // Domain and Application Layer
18
+ const StackIdentifier = require('../../infrastructure/domains/health/domain/value-objects/stack-identifier');
19
+ const RunHealthCheckUseCase = require('../../infrastructure/domains/health/application/use-cases/run-health-check-use-case');
20
+ const RepairViaImportUseCase = require('../../infrastructure/domains/health/application/use-cases/repair-via-import-use-case');
21
+ const ReconcilePropertiesUseCase = require('../../infrastructure/domains/health/application/use-cases/reconcile-properties-use-case');
22
+ const ExecuteResourceImportUseCase = require('../../infrastructure/domains/health/application/use-cases/execute-resource-import-use-case');
23
+
24
+ // Infrastructure Layer - AWS Adapters
25
+ const AWSStackRepository = require('../../infrastructure/domains/health/infrastructure/adapters/aws-stack-repository');
26
+ const AWSResourceDetector = require('../../infrastructure/domains/health/infrastructure/adapters/aws-resource-detector');
27
+ const AWSResourceImporter = require('../../infrastructure/domains/health/infrastructure/adapters/aws-resource-importer');
28
+ const AWSPropertyReconciler = require('../../infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler');
29
+
30
+ // Domain Services
31
+ const MismatchAnalyzer = require('../../infrastructure/domains/health/domain/services/mismatch-analyzer');
32
+ const HealthScoreCalculator = require('../../infrastructure/domains/health/domain/services/health-score-calculator');
33
+ const { TemplateParser } = require('../../infrastructure/domains/health/domain/services/template-parser');
34
+ const { ImportTemplateGenerator } = require('../../infrastructure/domains/health/domain/services/import-template-generator');
35
+ const { ImportProgressMonitor } = require('../../infrastructure/domains/health/domain/services/import-progress-monitor');
36
+
37
+ /**
38
+ * Create readline interface for user prompts
39
+ * @returns {readline.Interface}
40
+ */
41
+ function createReadlineInterface() {
42
+ return readline.createInterface({
43
+ input: process.stdin,
44
+ output: process.stdout,
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Prompt user for confirmation
50
+ * @param {string} question - Question to ask
51
+ * @returns {Promise<boolean>} User confirmed
52
+ */
53
+ function confirm(question) {
54
+ const rl = createReadlineInterface();
55
+
56
+ return new Promise((resolve) => {
57
+ rl.question(`${question} (y/N): `, (answer) => {
58
+ rl.close();
59
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
60
+ });
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Handle import repair operation using template comparison
66
+ * @param {StackIdentifier} stackIdentifier - Stack identifier
67
+ * @param {Object} report - Health check report
68
+ * @param {Object} options - Command options
69
+ */
70
+ async function handleImportRepair(stackIdentifier, report, options) {
71
+ const orphanedResources = report.getOrphanedResources();
72
+
73
+ if (orphanedResources.length === 0) {
74
+ console.log('\n✓ No orphaned resources to import');
75
+ return { imported: 0, failed: 0 };
76
+ }
77
+
78
+ console.log(`\n📦 Found ${orphanedResources.length} orphaned resource(s) to import:`);
79
+ orphanedResources.forEach((resource, idx) => {
80
+ console.log(` ${idx + 1}. ${resource.resourceType} - ${resource.physicalId}`);
81
+ });
82
+
83
+ // Check for build template
84
+ const buildTemplatePath = TemplateParser.getBuildTemplatePath();
85
+ const buildTemplateExists = TemplateParser.buildTemplateExists();
86
+
87
+ if (!buildTemplateExists) {
88
+ console.log('\n⚠️ Build template not found. Generating sequential logical IDs (not recommended).');
89
+ console.log(` Run one of the following to generate build template:`);
90
+ console.log(` • serverless package`);
91
+ console.log(` • frigg build`);
92
+ console.log(` • frigg deploy --stage dev`);
93
+ console.log(` Then run 'frigg repair --import ${stackIdentifier.stackName}' again for correct logical IDs.\n`);
94
+
95
+ // Fallback to sequential IDs (old behavior)
96
+ const resourcesToImport = orphanedResources.map((resource, idx) => ({
97
+ logicalId: `ImportedResource${idx + 1}`,
98
+ physicalId: resource.physicalId,
99
+ resourceType: resource.resourceType,
100
+ }));
101
+
102
+ if (!options.yes) {
103
+ const confirmed = await confirm(`\nImport ${orphanedResources.length} orphaned resource(s) with sequential IDs?`);
104
+ if (!confirmed) {
105
+ console.log('Import cancelled by user');
106
+ return { imported: 0, failed: 0, cancelled: true };
107
+ }
108
+ }
109
+
110
+ const resourceDetector = new AWSResourceDetector({ region: stackIdentifier.region });
111
+ const resourceImporter = new AWSResourceImporter({ region: stackIdentifier.region });
112
+ const repairUseCase = new RepairViaImportUseCase({ resourceDetector, resourceImporter });
113
+
114
+ console.log('\n🔧 Importing resources with sequential IDs...');
115
+ const importResult = await repairUseCase.importMultipleResources({
116
+ stackIdentifier,
117
+ resources: resourcesToImport,
118
+ });
119
+
120
+ if (importResult.success) {
121
+ console.log(`\n✓ Successfully imported ${importResult.importedCount} resource(s)`);
122
+ } else {
123
+ console.log(`\n✗ Import failed: ${importResult.message}`);
124
+ if (importResult.validationErrors && importResult.validationErrors.length > 0) {
125
+ console.log('\nValidation errors:');
126
+ importResult.validationErrors.forEach((error) => {
127
+ console.log(` • ${error.logicalId}: ${error.reason}`);
128
+ });
129
+ }
130
+ }
131
+
132
+ return {
133
+ imported: importResult.importedCount,
134
+ failed: importResult.failedCount,
135
+ success: importResult.success,
136
+ };
137
+ }
138
+
139
+ // Use template comparison to find correct logical IDs
140
+ console.log(`\n🔍 Analyzing templates to map orphaned resources to correct logical IDs...`);
141
+ console.log(` Build template: ${buildTemplatePath}`);
142
+ console.log(` Deployed template: CloudFormation (via AWS API)`);
143
+
144
+ // Wire up use case with template comparison
145
+ const stackRepository = new AWSStackRepository({ region: stackIdentifier.region });
146
+ const resourceDetector = new AWSResourceDetector({ region: stackIdentifier.region });
147
+ const resourceImporter = new AWSResourceImporter({ region: stackIdentifier.region });
148
+ const repairUseCase = new RepairViaImportUseCase({
149
+ resourceDetector,
150
+ resourceImporter,
151
+ stackRepository,
152
+ });
153
+
154
+ // Execute logical ID mapping
155
+ const mappingResult = await repairUseCase.importWithLogicalIdMapping({
156
+ stackIdentifier,
157
+ orphanedResources,
158
+ buildTemplatePath,
159
+ });
160
+
161
+ if (!mappingResult.success) {
162
+ console.log(`\n✗ Mapping failed: ${mappingResult.message}`);
163
+ return { imported: 0, failed: 0, success: false };
164
+ }
165
+
166
+ // Display mapping results
167
+ console.log(`\n✅ Successfully mapped ${mappingResult.mappedCount} resource(s) to logical IDs:`);
168
+ mappingResult.mappings.forEach((mapping) => {
169
+ console.log(` • ${mapping.logicalId} ← ${mapping.physicalId} (${mapping.matchMethod}, ${mapping.confidence} confidence)`);
170
+ });
171
+
172
+ if (mappingResult.unmappedCount > 0) {
173
+ console.log(`\n⚠️ Could not map ${mappingResult.unmappedCount} resource(s):`);
174
+ mappingResult.unmappedResources.forEach((resource) => {
175
+ console.log(` • ${resource.resourceType} - ${resource.physicalId}`);
176
+ });
177
+ }
178
+
179
+ // Display warnings for multiple resources of same type
180
+ if (mappingResult.warnings && mappingResult.warnings.length > 0) {
181
+ console.log(`\n⚠️ Warnings:`);
182
+ mappingResult.warnings.forEach((warning) => {
183
+ console.log(` • ${warning.message}`);
184
+ if (warning.type === 'MULTIPLE_RESOURCES') {
185
+ warning.resources.forEach((res) => {
186
+ console.log(` - ${res.logicalId} ← ${res.physicalId} (${res.matchMethod}, ${res.confidence})`);
187
+ });
188
+ }
189
+ });
190
+ }
191
+
192
+ // Confirm with user (unless --yes flag)
193
+ if (!options.yes) {
194
+ console.log(`\n📋 The following will be imported into CloudFormation:`);
195
+ mappingResult.resourcesToImport.forEach((resource) => {
196
+ console.log(` • ${resource.LogicalResourceId} (${resource.ResourceType})`);
197
+ });
198
+
199
+ const confirmed = await confirm(`\nProceed with import of ${mappingResult.mappedCount} resource(s)?`);
200
+ if (!confirmed) {
201
+ console.log('Import cancelled by user');
202
+ return { imported: 0, failed: 0, cancelled: true };
203
+ }
204
+ }
205
+
206
+ // Execute actual CloudFormation import operation
207
+ console.log(`\n🔧 Preparing CloudFormation import operation...`);
208
+
209
+ // Wire up ExecuteResourceImportUseCase
210
+ const templateParser = new TemplateParser();
211
+ const importTemplateGenerator = new ImportTemplateGenerator({
212
+ stackRepository,
213
+ templateParser,
214
+ resourceDetector,
215
+ });
216
+ const importProgressMonitor = new ImportProgressMonitor({
217
+ cloudFormationRepository: stackRepository,
218
+ });
219
+ const executeImportUseCase = new ExecuteResourceImportUseCase({
220
+ importTemplateGenerator,
221
+ importProgressMonitor,
222
+ cloudFormationRepository: stackRepository,
223
+ stackRepository,
224
+ });
225
+
226
+ // Convert mappings to resourcesToImport format
227
+ const resourcesToImport = mappingResult.mappings.map((mapping) => ({
228
+ logicalId: mapping.logicalId,
229
+ physicalId: mapping.physicalId,
230
+ resourceType: mapping.resourceType,
231
+ }));
232
+
233
+ // Execute import with progress reporting
234
+ const importResult = await executeImportUseCase.execute({
235
+ stackIdentifier,
236
+ resourcesToImport,
237
+ buildTemplatePath,
238
+ onProgress: (progress) => {
239
+ if (progress.step === 'generate_template' && progress.status === 'in_progress') {
240
+ console.log(' • Generating import template...');
241
+ } else if (progress.step === 'generate_template' && progress.status === 'complete') {
242
+ console.log(' ✓ Template generated');
243
+ } else if (progress.step === 'create_change_set' && progress.status === 'in_progress') {
244
+ console.log(' • Creating CloudFormation change set...');
245
+ } else if (progress.step === 'create_change_set' && progress.status === 'complete') {
246
+ console.log(` ✓ Change set created: ${progress.changeSetName}`);
247
+ } else if (progress.step === 'wait_change_set' && progress.status === 'in_progress') {
248
+ console.log(' • Waiting for change set...');
249
+ } else if (progress.step === 'wait_change_set' && progress.status === 'complete') {
250
+ console.log(' ✓ Change set ready');
251
+ } else if (progress.step === 'execute_import' && progress.status === 'in_progress') {
252
+ if (progress.resourceProgress) {
253
+ const { logicalId, status, progress: resourceProgress, total } = progress.resourceProgress;
254
+ console.log(` • Importing resource ${resourceProgress}/${total}: ${logicalId} (${status})`);
255
+ } else {
256
+ console.log(' • Executing import operation...');
257
+ }
258
+ } else if (progress.step === 'execute_import' && progress.status === 'complete') {
259
+ console.log(' ✓ Import operation complete');
260
+ } else if (progress.step === 'verify' && progress.status === 'in_progress') {
261
+ console.log(' • Verifying imported resources...');
262
+ } else if (progress.step === 'verify' && progress.status === 'complete') {
263
+ console.log(' ✓ Verification complete');
264
+ }
265
+ },
266
+ });
267
+
268
+ if (importResult.success) {
269
+ console.log(`\n✅ Successfully imported ${importResult.importedCount} resource(s) into CloudFormation!`);
270
+ console.log(` Stack status: ${importResult.stackStatus}`);
271
+ console.log(` Change set: ${importResult.changeSetName}`);
272
+
273
+ return {
274
+ imported: importResult.importedCount,
275
+ failed: 0,
276
+ success: true,
277
+ };
278
+ } else {
279
+ console.error(`\n❌ Import operation failed: ${importResult.error}`);
280
+ console.error(` Failed at step: ${importResult.step}`);
281
+
282
+ return {
283
+ imported: 0,
284
+ failed: mappingResult.mappedCount,
285
+ success: false,
286
+ error: importResult.error,
287
+ };
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Handle property reconciliation repair operation
293
+ * @param {StackIdentifier} stackIdentifier - Stack identifier
294
+ * @param {Object} report - Health check report
295
+ * @param {Object} options - Command options
296
+ */
297
+ async function handleReconcileRepair(stackIdentifier, report, options) {
298
+ const driftedResources = report.getDriftedResources();
299
+
300
+ if (driftedResources.length === 0) {
301
+ console.log('\n✓ No property drift to reconcile');
302
+ return { reconciled: 0, failed: 0 };
303
+ }
304
+
305
+ // Count total property mismatches
306
+ let totalMismatches = 0;
307
+ driftedResources.forEach((resource) => {
308
+ const issues = report.issues.filter(
309
+ (issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
310
+ );
311
+ totalMismatches += issues.length;
312
+ });
313
+
314
+ console.log(`\n🔧 Found ${driftedResources.length} drifted resource(s) with ${totalMismatches} property mismatch(es):`);
315
+ driftedResources.forEach((resource) => {
316
+ const issues = report.issues.filter(
317
+ (issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
318
+ );
319
+ console.log(` • ${resource.logicalId} (${resource.resourceType}): ${issues.length} mismatch(es)`);
320
+ });
321
+
322
+ // Determine mode (template or resource)
323
+ const mode = options.mode || 'template';
324
+ const modeDescription = mode === 'template'
325
+ ? 'Update CloudFormation template to match actual resource state'
326
+ : 'Update cloud resources to match CloudFormation template';
327
+
328
+ console.log(`\nReconciliation mode: ${mode}`);
329
+ console.log(` ${modeDescription}`);
330
+
331
+ // Confirm with user (unless --yes flag)
332
+ if (!options.yes) {
333
+ const confirmed = await confirm(`\nReconcile ${totalMismatches} property mismatch(es) in ${mode} mode?`);
334
+ if (!confirmed) {
335
+ console.log('Reconciliation cancelled by user');
336
+ return { reconciled: 0, failed: 0, cancelled: true };
337
+ }
338
+ }
339
+
340
+ // Wire up use case with CloudFormation repository for monitoring
341
+ const stackRepository = new AWSStackRepository({ region: stackIdentifier.region });
342
+ const propertyReconciler = new AWSPropertyReconciler({
343
+ region: stackIdentifier.region,
344
+ cloudFormationRepository: stackRepository
345
+ });
346
+ const reconcileUseCase = new ReconcilePropertiesUseCase({ propertyReconciler });
347
+
348
+ // Execute reconciliation for each drifted resource
349
+ console.log('\n🔧 Reconciling property drift...');
350
+ let reconciledCount = 0;
351
+ let failedCount = 0;
352
+ let skippedImmutableCount = 0;
353
+ const immutableProperties = [];
354
+
355
+ for (const resource of driftedResources) {
356
+ // Get property mismatches for this resource
357
+ const resourceIssues = report.issues.filter(
358
+ (issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
359
+ );
360
+
361
+ if (resourceIssues.length === 0) continue;
362
+
363
+ const mismatches = resourceIssues.map((issue) => issue.propertyMismatch);
364
+
365
+ try {
366
+ const result = await reconcileUseCase.reconcileMultipleProperties({
367
+ stackIdentifier,
368
+ logicalId: resource.logicalId,
369
+ physicalId: resource.physicalId,
370
+ resourceType: resource.resourceType,
371
+ mismatches,
372
+ mode,
373
+ });
374
+
375
+ reconciledCount += result.reconciledCount;
376
+ failedCount += result.failedCount;
377
+ skippedImmutableCount += result.skippedCount || 0;
378
+
379
+ // Track immutable properties for reporting
380
+ if (result.skippedCount > 0) {
381
+ const skippedMismatches = mismatches.filter(m => m.requiresReplacement());
382
+ skippedMismatches.forEach(m => {
383
+ immutableProperties.push({
384
+ logicalId: resource.logicalId,
385
+ resourceType: resource.resourceType,
386
+ physicalId: resource.physicalId,
387
+ propertyPath: m.propertyPath,
388
+ expectedValue: m.expectedValue,
389
+ actualValue: m.actualValue,
390
+ });
391
+ });
392
+ }
393
+
394
+ console.log(` ✓ ${resource.logicalId}: Reconciled ${result.reconciledCount} property(ies)`);
395
+ if (result.skippedCount > 0) {
396
+ console.log(` ⚠ Skipped ${result.skippedCount} immutable property(ies) - requires manual intervention`);
397
+ }
398
+
399
+ // Debug: Log full result if reconciledCount is 0 but we expected properties
400
+ if (process.env.DEBUG_RECONCILE && result.reconciledCount === 0 && mismatches.length > 0) {
401
+ console.log(` [DEBUG] Expected ${mismatches.length} mismatches, got result:`, JSON.stringify(result, null, 2));
402
+ }
403
+ } catch (error) {
404
+ // Count failed properties, not just the resource
405
+ failedCount += mismatches.length;
406
+ console.log(` ✗ ${resource.logicalId}: ${error.message}`);
407
+
408
+ // Debug: Log full error
409
+ if (process.env.DEBUG_RECONCILE) {
410
+ console.log(` [DEBUG] Error stack:`, error.stack);
411
+ }
412
+ }
413
+ }
414
+
415
+ // Report results
416
+ console.log(''); // Blank line before summary
417
+
418
+ if (reconciledCount > 0) {
419
+ console.log(`✅ Reconciled ${reconciledCount} property(ies)`);
420
+ }
421
+
422
+ if (skippedImmutableCount > 0) {
423
+ console.log(`\n⚠ ${skippedImmutableCount} immutable property(ies) require manual intervention:`);
424
+ immutableProperties.forEach(prop => {
425
+ console.log(` • ${prop.logicalId}.${prop.propertyPath}`);
426
+ console.log(` Template: ${JSON.stringify(prop.expectedValue)}`);
427
+ console.log(` Actual: ${JSON.stringify(prop.actualValue)}`);
428
+ });
429
+
430
+ console.log(`\n💡 To resolve immutable property drift:`);
431
+ console.log(` 1. These properties require resource replacement (cannot be updated in place)`);
432
+ console.log(` 2. Options:`);
433
+ console.log(` a) Accept the drift - update your local template to match actual values`);
434
+ console.log(` b) Replace the resource - delete and recreate via CloudFormation`);
435
+ console.log(` c) Use import workflow - remove from stack, then re-import with correct values`);
436
+ console.log(`\n For automated import workflow (coming soon):`);
437
+ console.log(` frigg repair --import-drift ${stackIdentifier.stackName}`);
438
+ }
439
+
440
+ if (failedCount === 0 && skippedImmutableCount === 0) {
441
+ console.log(`✓ Successfully reconciled all ${reconciledCount} property mismatch(es)`);
442
+ } else {
443
+ console.log(`\n⚠ Reconciled ${reconciledCount} property(ies), ${failedCount} failed`);
444
+ }
445
+
446
+ return { reconciled: reconciledCount, failed: failedCount, success: failedCount === 0 };
447
+ }
448
+
449
+ /**
450
+ * Execute repair operations
451
+ * @param {string} stackName - CloudFormation stack name
452
+ * @param {Object} options - Command options
453
+ */
454
+ async function repairCommand(stackName, options = {}) {
455
+ try {
456
+ // Validate required parameter
457
+ if (!stackName) {
458
+ console.error('Error: Stack name is required');
459
+ console.log('Usage: frigg repair [options] <stack-name>');
460
+ console.log('Options:');
461
+ console.log(' --import Import orphaned resources');
462
+ console.log(' --reconcile Reconcile property drift');
463
+ console.log(' --yes Skip confirmation prompts');
464
+ process.exit(1);
465
+ }
466
+
467
+ // Validate at least one repair operation is selected
468
+ if (!options.import && !options.reconcile) {
469
+ console.error('Error: At least one repair operation must be specified (--import or --reconcile)');
470
+ console.log('Usage: frigg repair [options] <stack-name>');
471
+ process.exit(1);
472
+ }
473
+
474
+ // Extract options with defaults
475
+ const region = options.region || process.env.AWS_REGION || 'us-east-1';
476
+ const verbose = options.verbose || false;
477
+
478
+ console.log(`\n🏥 Running Frigg Repair on stack: ${stackName} (${region})`);
479
+
480
+ // 1. Create stack identifier
481
+ const stackIdentifier = new StackIdentifier({ stackName, region });
482
+
483
+ // 2. Run health check first to identify issues
484
+ console.log('\n🔍 Running health check to identify issues...');
485
+
486
+ const stackRepository = new AWSStackRepository({ region });
487
+ const resourceDetector = new AWSResourceDetector({ region });
488
+ const mismatchAnalyzer = new MismatchAnalyzer();
489
+ const healthScoreCalculator = new HealthScoreCalculator();
490
+
491
+ const runHealthCheckUseCase = new RunHealthCheckUseCase({
492
+ stackRepository,
493
+ resourceDetector,
494
+ mismatchAnalyzer,
495
+ healthScoreCalculator,
496
+ });
497
+
498
+ const report = await runHealthCheckUseCase.execute({ stackIdentifier });
499
+
500
+ console.log(`\nHealth Score: ${report.healthScore.value}/100 (${report.healthScore.qualitativeAssessment()})`);
501
+ console.log(`Issues: ${report.getIssueCount()} total (${report.getCriticalIssueCount()} critical)`);
502
+
503
+ // 3. Execute requested repair operations
504
+ const results = {
505
+ imported: 0,
506
+ reconciled: 0,
507
+ failed: 0,
508
+ };
509
+
510
+ if (options.import) {
511
+ const importResult = await handleImportRepair(stackIdentifier, report, options);
512
+ if (!importResult.cancelled) {
513
+ results.imported = importResult.imported || 0;
514
+ results.failed += importResult.failed || 0;
515
+ }
516
+ }
517
+
518
+ if (options.reconcile) {
519
+ const reconcileResult = await handleReconcileRepair(stackIdentifier, report, options);
520
+ if (!reconcileResult.cancelled) {
521
+ results.reconciled = reconcileResult.reconciled || 0;
522
+ results.failed += reconcileResult.failed || 0;
523
+ }
524
+ }
525
+
526
+ // 4. Final summary
527
+ console.log('\n' + '═'.repeat(80));
528
+ console.log('Repair Summary:');
529
+ if (options.import) {
530
+ console.log(` Imported: ${results.imported} resource(s)`);
531
+ }
532
+ if (options.reconcile) {
533
+ console.log(` Reconciled: ${results.reconciled} property(ies)`);
534
+ }
535
+ console.log(` Failed: ${results.failed}`);
536
+ console.log('═'.repeat(80));
537
+
538
+ // Run health check again to verify repairs
539
+ console.log('\n🔍 Running health check to verify repairs...');
540
+ const verifyReport = await runHealthCheckUseCase.execute({ stackIdentifier });
541
+ console.log(`\nNew Health Score: ${verifyReport.healthScore.value}/100 (${verifyReport.healthScore.qualitativeAssessment()})`);
542
+
543
+ if (verifyReport.healthScore.value > report.healthScore.value) {
544
+ console.log(`\n✓ Health improved by ${verifyReport.healthScore.value - report.healthScore.value} points!`);
545
+ }
546
+
547
+ // 5. Exit with appropriate code
548
+ if (results.failed > 0) {
549
+ process.exit(1);
550
+ } else {
551
+ process.exit(0);
552
+ }
553
+ } catch (error) {
554
+ console.error(`\n✗ Repair failed: ${error.message}`);
555
+
556
+ if (options.verbose && error.stack) {
557
+ console.error(`\nStack trace:\n${error.stack}`);
558
+ }
559
+
560
+ process.exit(1);
561
+ }
562
+ }
563
+
564
+ module.exports = { repairCommand };