@friggframework/devtools 2.0.0--canary.474.884529c.0 → 2.0.0--canary.474.988ec0b.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.
@@ -0,0 +1,341 @@
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('@friggframework/devtools/infrastructure/domains/health/domain/value-objects/stack-identifier');
19
+ const RunHealthCheckUseCase = require('@friggframework/devtools/infrastructure/domains/health/application/use-cases/run-health-check-use-case');
20
+ const RepairViaImportUseCase = require('@friggframework/devtools/infrastructure/domains/health/application/use-cases/repair-via-import-use-case');
21
+ const ReconcilePropertiesUseCase = require('@friggframework/devtools/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case');
22
+
23
+ // Infrastructure Layer - AWS Adapters
24
+ const AWSStackRepository = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository');
25
+ const AWSResourceDetector = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector');
26
+ const AWSResourceImporter = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer');
27
+ const AWSPropertyReconciler = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler');
28
+
29
+ // Domain Services
30
+ const MismatchAnalyzer = require('@friggframework/devtools/infrastructure/domains/health/domain/services/mismatch-analyzer');
31
+ const HealthScoreCalculator = require('@friggframework/devtools/infrastructure/domains/health/domain/services/health-score-calculator');
32
+
33
+ /**
34
+ * Create readline interface for user prompts
35
+ * @returns {readline.Interface}
36
+ */
37
+ function createReadlineInterface() {
38
+ return readline.createInterface({
39
+ input: process.stdin,
40
+ output: process.stdout,
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Prompt user for confirmation
46
+ * @param {string} question - Question to ask
47
+ * @returns {Promise<boolean>} User confirmed
48
+ */
49
+ function confirm(question) {
50
+ const rl = createReadlineInterface();
51
+
52
+ return new Promise((resolve) => {
53
+ rl.question(`${question} (y/N): `, (answer) => {
54
+ rl.close();
55
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
56
+ });
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Handle import repair operation
62
+ * @param {StackIdentifier} stackIdentifier - Stack identifier
63
+ * @param {Object} report - Health check report
64
+ * @param {Object} options - Command options
65
+ */
66
+ async function handleImportRepair(stackIdentifier, report, options) {
67
+ const orphanedResources = report.getOrphanedResources();
68
+
69
+ if (orphanedResources.length === 0) {
70
+ console.log('\nāœ“ No orphaned resources to import');
71
+ return { imported: 0, failed: 0 };
72
+ }
73
+
74
+ console.log(`\nšŸ“¦ Found ${orphanedResources.length} orphaned resource(s) to import:`);
75
+ orphanedResources.forEach((resource, idx) => {
76
+ console.log(` ${idx + 1}. ${resource.resourceType} - ${resource.physicalId}`);
77
+ });
78
+
79
+ // Confirm with user (unless --yes flag)
80
+ if (!options.yes) {
81
+ const confirmed = await confirm(`\nImport ${orphanedResources.length} orphaned resource(s)?`);
82
+ if (!confirmed) {
83
+ console.log('Import cancelled by user');
84
+ return { imported: 0, failed: 0, cancelled: true };
85
+ }
86
+ }
87
+
88
+ // Wire up use case
89
+ const resourceDetector = new AWSResourceDetector({ region: stackIdentifier.region });
90
+ const resourceImporter = new AWSResourceImporter({ region: stackIdentifier.region });
91
+ const repairUseCase = new RepairViaImportUseCase({ resourceDetector, resourceImporter });
92
+
93
+ // Prepare resources for import
94
+ const resourcesToImport = orphanedResources.map((resource, idx) => ({
95
+ logicalId: `ImportedResource${idx + 1}`, // Generate logical ID
96
+ physicalId: resource.physicalId,
97
+ resourceType: resource.resourceType,
98
+ }));
99
+
100
+ // Execute import
101
+ console.log('\nšŸ”§ Importing resources...');
102
+ const importResult = await repairUseCase.importMultipleResources({
103
+ stackIdentifier,
104
+ resources: resourcesToImport,
105
+ });
106
+
107
+ // Report results
108
+ if (importResult.success) {
109
+ console.log(`\nāœ“ Successfully imported ${importResult.importedCount} resource(s)`);
110
+ } else {
111
+ console.log(`\nāœ— Import failed: ${importResult.message}`);
112
+ if (importResult.validationErrors && importResult.validationErrors.length > 0) {
113
+ console.log('\nValidation errors:');
114
+ importResult.validationErrors.forEach((error) => {
115
+ console.log(` • ${error.logicalId}: ${error.reason}`);
116
+ });
117
+ }
118
+ }
119
+
120
+ return {
121
+ imported: importResult.importedCount,
122
+ failed: importResult.failedCount,
123
+ success: importResult.success,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Handle property reconciliation repair operation
129
+ * @param {StackIdentifier} stackIdentifier - Stack identifier
130
+ * @param {Object} report - Health check report
131
+ * @param {Object} options - Command options
132
+ */
133
+ async function handleReconcileRepair(stackIdentifier, report, options) {
134
+ const driftedResources = report.getDriftedResources();
135
+
136
+ if (driftedResources.length === 0) {
137
+ console.log('\nāœ“ No property drift to reconcile');
138
+ return { reconciled: 0, failed: 0 };
139
+ }
140
+
141
+ // Count total property mismatches
142
+ let totalMismatches = 0;
143
+ driftedResources.forEach((resource) => {
144
+ const issues = report.issues.filter(
145
+ (issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
146
+ );
147
+ totalMismatches += issues.length;
148
+ });
149
+
150
+ console.log(`\nšŸ”§ Found ${driftedResources.length} drifted resource(s) with ${totalMismatches} property mismatch(es):`);
151
+ driftedResources.forEach((resource) => {
152
+ const issues = report.issues.filter(
153
+ (issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
154
+ );
155
+ console.log(` • ${resource.logicalId} (${resource.resourceType}): ${issues.length} mismatch(es)`);
156
+ });
157
+
158
+ // Determine mode (template or resource)
159
+ const mode = options.mode || 'template';
160
+ const modeDescription = mode === 'template'
161
+ ? 'Update CloudFormation template to match actual resource state'
162
+ : 'Update cloud resources to match CloudFormation template';
163
+
164
+ console.log(`\nReconciliation mode: ${mode}`);
165
+ console.log(` ${modeDescription}`);
166
+
167
+ // Confirm with user (unless --yes flag)
168
+ if (!options.yes) {
169
+ const confirmed = await confirm(`\nReconcile ${totalMismatches} property mismatch(es) in ${mode} mode?`);
170
+ if (!confirmed) {
171
+ console.log('Reconciliation cancelled by user');
172
+ return { reconciled: 0, failed: 0, cancelled: true };
173
+ }
174
+ }
175
+
176
+ // Wire up use case
177
+ const propertyReconciler = new AWSPropertyReconciler({ region: stackIdentifier.region });
178
+ const reconcileUseCase = new ReconcilePropertiesUseCase({ propertyReconciler });
179
+
180
+ // Execute reconciliation for each drifted resource
181
+ console.log('\nšŸ”§ Reconciling property drift...');
182
+ let reconciledCount = 0;
183
+ let failedCount = 0;
184
+
185
+ for (const resource of driftedResources) {
186
+ // Get property mismatches for this resource
187
+ const resourceIssues = report.issues.filter(
188
+ (issue) => issue.type === 'PROPERTY_MISMATCH' && issue.resourceId === resource.physicalId
189
+ );
190
+
191
+ if (resourceIssues.length === 0) continue;
192
+
193
+ const mismatches = resourceIssues.map((issue) => issue.propertyMismatch);
194
+
195
+ try {
196
+ const result = await reconcileUseCase.reconcileMultipleProperties({
197
+ stackIdentifier,
198
+ logicalId: resource.logicalId,
199
+ mismatches,
200
+ mode,
201
+ });
202
+
203
+ reconciledCount += result.reconciledCount;
204
+ failedCount += result.failedCount;
205
+
206
+ console.log(` āœ“ ${resource.logicalId}: Reconciled ${result.reconciledCount} property(ies)`);
207
+ if (result.skippedCount > 0) {
208
+ console.log(` (Skipped ${result.skippedCount} immutable property(ies))`);
209
+ }
210
+ } catch (error) {
211
+ failedCount++;
212
+ console.log(` āœ— ${resource.logicalId}: ${error.message}`);
213
+ }
214
+ }
215
+
216
+ // Report results
217
+ if (failedCount === 0) {
218
+ console.log(`\nāœ“ Successfully reconciled ${reconciledCount} property mismatch(es)`);
219
+ } else {
220
+ console.log(`\n⚠ Reconciled ${reconciledCount} property(ies), ${failedCount} failed`);
221
+ }
222
+
223
+ return { reconciled: reconciledCount, failed: failedCount, success: failedCount === 0 };
224
+ }
225
+
226
+ /**
227
+ * Execute repair operations
228
+ * @param {string} stackName - CloudFormation stack name
229
+ * @param {Object} options - Command options
230
+ */
231
+ async function repairCommand(stackName, options = {}) {
232
+ try {
233
+ // Validate required parameter
234
+ if (!stackName) {
235
+ console.error('Error: Stack name is required');
236
+ console.log('Usage: frigg repair [options] <stack-name>');
237
+ console.log('Options:');
238
+ console.log(' --import Import orphaned resources');
239
+ console.log(' --reconcile Reconcile property drift');
240
+ console.log(' --yes Skip confirmation prompts');
241
+ process.exit(1);
242
+ }
243
+
244
+ // Validate at least one repair operation is selected
245
+ if (!options.import && !options.reconcile) {
246
+ console.error('Error: At least one repair operation must be specified (--import or --reconcile)');
247
+ console.log('Usage: frigg repair [options] <stack-name>');
248
+ process.exit(1);
249
+ }
250
+
251
+ // Extract options with defaults
252
+ const region = options.region || process.env.AWS_REGION || 'us-east-1';
253
+ const verbose = options.verbose || false;
254
+
255
+ console.log(`\nšŸ„ Running Frigg Repair on stack: ${stackName} (${region})`);
256
+
257
+ // 1. Create stack identifier
258
+ const stackIdentifier = new StackIdentifier({ stackName, region });
259
+
260
+ // 2. Run health check first to identify issues
261
+ console.log('\nšŸ” Running health check to identify issues...');
262
+
263
+ const stackRepository = new AWSStackRepository({ region });
264
+ const resourceDetector = new AWSResourceDetector({ region });
265
+ const mismatchAnalyzer = new MismatchAnalyzer();
266
+ const healthScoreCalculator = new HealthScoreCalculator();
267
+
268
+ const runHealthCheckUseCase = new RunHealthCheckUseCase({
269
+ stackRepository,
270
+ resourceDetector,
271
+ mismatchAnalyzer,
272
+ healthScoreCalculator,
273
+ });
274
+
275
+ const report = await runHealthCheckUseCase.execute({ stackIdentifier });
276
+
277
+ console.log(`\nHealth Score: ${report.healthScore.value}/100 (${report.healthScore.qualitativeAssessment()})`);
278
+ console.log(`Issues: ${report.getIssueCount()} total (${report.getCriticalIssueCount()} critical)`);
279
+
280
+ // 3. Execute requested repair operations
281
+ const results = {
282
+ imported: 0,
283
+ reconciled: 0,
284
+ failed: 0,
285
+ };
286
+
287
+ if (options.import) {
288
+ const importResult = await handleImportRepair(stackIdentifier, report, options);
289
+ if (!importResult.cancelled) {
290
+ results.imported = importResult.imported || 0;
291
+ results.failed += importResult.failed || 0;
292
+ }
293
+ }
294
+
295
+ if (options.reconcile) {
296
+ const reconcileResult = await handleReconcileRepair(stackIdentifier, report, options);
297
+ if (!reconcileResult.cancelled) {
298
+ results.reconciled = reconcileResult.reconciled || 0;
299
+ results.failed += reconcileResult.failed || 0;
300
+ }
301
+ }
302
+
303
+ // 4. Final summary
304
+ console.log('\n' + '═'.repeat(80));
305
+ console.log('Repair Summary:');
306
+ if (options.import) {
307
+ console.log(` Imported: ${results.imported} resource(s)`);
308
+ }
309
+ if (options.reconcile) {
310
+ console.log(` Reconciled: ${results.reconciled} property(ies)`);
311
+ }
312
+ console.log(` Failed: ${results.failed}`);
313
+ console.log('═'.repeat(80));
314
+
315
+ // Run health check again to verify repairs
316
+ console.log('\nšŸ” Running health check to verify repairs...');
317
+ const verifyReport = await runHealthCheckUseCase.execute({ stackIdentifier });
318
+ console.log(`\nNew Health Score: ${verifyReport.healthScore.value}/100 (${verifyReport.healthScore.qualitativeAssessment()})`);
319
+
320
+ if (verifyReport.healthScore.value > report.healthScore.value) {
321
+ console.log(`\nāœ“ Health improved by ${verifyReport.healthScore.value - report.healthScore.value} points!`);
322
+ }
323
+
324
+ // 5. Exit with appropriate code
325
+ if (results.failed > 0) {
326
+ process.exit(1);
327
+ } else {
328
+ process.exit(0);
329
+ }
330
+ } catch (error) {
331
+ console.error(`\nāœ— Repair failed: ${error.message}`);
332
+
333
+ if (options.verbose && error.stack) {
334
+ console.error(`\nStack trace:\n${error.stack}`);
335
+ }
336
+
337
+ process.exit(1);
338
+ }
339
+ }
340
+
341
+ module.exports = { repairCommand };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * ReconcilePropertiesUseCase - Reconcile Property Drift
3
+ *
4
+ * Application Layer - Use Case
5
+ *
6
+ * Business logic for the "frigg repair --reconcile" command. Orchestrates property
7
+ * drift reconciliation to fix mutable property mismatches between CloudFormation
8
+ * template and actual cloud resources.
9
+ *
10
+ * Responsibilities:
11
+ * - Validate properties can be reconciled
12
+ * - Preview reconciliation impact
13
+ * - Execute reconciliation (template mode or resource mode)
14
+ * - Handle batch reconciliations
15
+ * - Skip immutable properties
16
+ */
17
+
18
+ class ReconcilePropertiesUseCase {
19
+ /**
20
+ * Create use case with required dependencies
21
+ *
22
+ * @param {Object} params
23
+ * @param {IPropertyReconciler} params.propertyReconciler - Property reconciliation operations
24
+ */
25
+ constructor({ propertyReconciler }) {
26
+ if (!propertyReconciler) {
27
+ throw new Error('propertyReconciler is required');
28
+ }
29
+
30
+ this.propertyReconciler = propertyReconciler;
31
+ }
32
+
33
+ /**
34
+ * Reconcile a single property mismatch
35
+ *
36
+ * @param {Object} params
37
+ * @param {StackIdentifier} params.stackIdentifier - Stack identifier
38
+ * @param {string} params.logicalId - Logical resource ID
39
+ * @param {PropertyMismatch} params.mismatch - Property mismatch to reconcile
40
+ * @param {string} [params.mode='template'] - Reconciliation mode
41
+ * @returns {Promise<Object>} Reconciliation result
42
+ */
43
+ async reconcileSingleProperty({ stackIdentifier, logicalId, mismatch, mode = 'template' }) {
44
+ // 1. Check if property can be reconciled
45
+ const canReconcile = await this.propertyReconciler.canReconcile(mismatch);
46
+
47
+ if (!canReconcile) {
48
+ throw new Error(
49
+ `Property ${mismatch.propertyPath} cannot be reconciled automatically (immutable property requires replacement)`
50
+ );
51
+ }
52
+
53
+ // 2. Execute reconciliation
54
+ const result = await this.propertyReconciler.reconcileProperty({
55
+ stackIdentifier,
56
+ logicalId,
57
+ mismatch,
58
+ mode,
59
+ });
60
+
61
+ return result;
62
+ }
63
+
64
+ /**
65
+ * Reconcile multiple property mismatches for a resource
66
+ *
67
+ * @param {Object} params
68
+ * @param {StackIdentifier} params.stackIdentifier - Stack identifier
69
+ * @param {string} params.logicalId - Logical resource ID
70
+ * @param {PropertyMismatch[]} params.mismatches - Property mismatches to reconcile
71
+ * @param {string} [params.mode='template'] - Reconciliation mode
72
+ * @returns {Promise<Object>} Batch reconciliation result
73
+ */
74
+ async reconcileMultipleProperties({
75
+ stackIdentifier,
76
+ logicalId,
77
+ mismatches,
78
+ mode = 'template',
79
+ }) {
80
+ // 1. Filter out immutable properties (cannot be reconciled)
81
+ const reconcilableProperties = [];
82
+ const skippedProperties = [];
83
+
84
+ for (const mismatch of mismatches) {
85
+ const canReconcile = await this.propertyReconciler.canReconcile(mismatch);
86
+
87
+ if (canReconcile) {
88
+ reconcilableProperties.push(mismatch);
89
+ } else {
90
+ skippedProperties.push(mismatch);
91
+ }
92
+ }
93
+
94
+ // 2. If no properties can be reconciled, return early
95
+ if (reconcilableProperties.length === 0) {
96
+ return {
97
+ reconciledCount: 0,
98
+ failedCount: 0,
99
+ skippedCount: skippedProperties.length,
100
+ reconcilableCount: 0,
101
+ message: `All ${mismatches.length} property mismatch(es) require resource replacement (immutable)`,
102
+ results: [],
103
+ };
104
+ }
105
+
106
+ // 3. Reconcile the reconcilable properties
107
+ const batchResult = await this.propertyReconciler.reconcileMultipleProperties({
108
+ stackIdentifier,
109
+ logicalId,
110
+ mismatches: reconcilableProperties,
111
+ mode,
112
+ });
113
+
114
+ // 4. Return combined result
115
+ return {
116
+ reconciledCount: batchResult.reconciledCount,
117
+ failedCount: batchResult.failedCount,
118
+ skippedCount: skippedProperties.length,
119
+ reconcilableCount: reconcilableProperties.length,
120
+ message: batchResult.message,
121
+ results: batchResult.results,
122
+ skippedProperties: skippedProperties.length > 0 ? skippedProperties : undefined,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Preview reconciliation without applying changes
128
+ *
129
+ * @param {Object} params
130
+ * @param {StackIdentifier} params.stackIdentifier - Stack identifier
131
+ * @param {string} params.logicalId - Logical resource ID
132
+ * @param {PropertyMismatch} params.mismatch - Property mismatch to preview
133
+ * @param {string} [params.mode='template'] - Reconciliation mode
134
+ * @returns {Promise<Object>} Preview result
135
+ */
136
+ async previewReconciliation({ stackIdentifier, logicalId, mismatch, mode = 'template' }) {
137
+ return await this.propertyReconciler.previewReconciliation({
138
+ stackIdentifier,
139
+ logicalId,
140
+ mismatch,
141
+ mode,
142
+ });
143
+ }
144
+ }
145
+
146
+ module.exports = ReconcilePropertiesUseCase;