@friggframework/devtools 2.0.0--canary.474.82fd52e.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.
@@ -2,6 +2,9 @@ const { spawn } = require('child_process');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
4
 
5
+ // Import doctor command for post-deployment health check
6
+ const { doctorCommand } = require('../doctor-command');
7
+
5
8
  // Configuration constants
6
9
  const PATHS = {
7
10
  APP_DEFINITION: 'index.js',
@@ -134,41 +137,134 @@ function validateAndBuildEnvironment(appDefinition, options) {
134
137
  * Executes the serverless deployment command
135
138
  * @param {Object} environment - Environment variables to pass to serverless
136
139
  * @param {Object} options - Deploy command options
140
+ * @returns {Promise<number>} Exit code
137
141
  */
138
142
  function executeServerlessDeployment(environment, options) {
139
- console.log('šŸš€ Deploying serverless application...');
140
-
141
- const serverlessArgs = [
142
- 'deploy',
143
- '--config',
144
- PATHS.INFRASTRUCTURE,
145
- '--stage',
146
- options.stage,
147
- ];
148
-
149
- // Add --force flag if force option is true
150
- if (options.force === true) {
151
- serverlessArgs.push('--force');
152
- }
143
+ return new Promise((resolve, reject) => {
144
+ console.log('šŸš€ Deploying serverless application...');
153
145
 
154
- const childProcess = spawn(COMMANDS.SERVERLESS, serverlessArgs, {
155
- cwd: path.resolve(process.cwd()),
156
- stdio: 'inherit',
157
- env: {
158
- ...environment,
159
- SLS_STAGE: options.stage, // Set stage for resource discovery
160
- },
161
- });
146
+ const serverlessArgs = [
147
+ 'deploy',
148
+ '--config',
149
+ PATHS.INFRASTRUCTURE,
150
+ '--stage',
151
+ options.stage,
152
+ ];
153
+
154
+ // Add --force flag if force option is true
155
+ if (options.force === true) {
156
+ serverlessArgs.push('--force');
157
+ }
162
158
 
163
- childProcess.on('error', (error) => {
164
- console.error(`Error executing command: ${error.message}`);
159
+ const childProcess = spawn(COMMANDS.SERVERLESS, serverlessArgs, {
160
+ cwd: path.resolve(process.cwd()),
161
+ stdio: 'inherit',
162
+ env: {
163
+ ...environment,
164
+ SLS_STAGE: options.stage, // Set stage for resource discovery
165
+ },
166
+ });
167
+
168
+ childProcess.on('error', (error) => {
169
+ console.error(`Error executing command: ${error.message}`);
170
+ reject(error);
171
+ });
172
+
173
+ childProcess.on('close', (code) => {
174
+ if (code !== 0) {
175
+ console.log(`Child process exited with code ${code}`);
176
+ resolve(code);
177
+ } else {
178
+ resolve(0);
179
+ }
180
+ });
165
181
  });
182
+ }
183
+
184
+ /**
185
+ * Get stack name from app definition
186
+ * @param {Object} appDefinition - App definition
187
+ * @param {Object} options - Deploy options
188
+ * @returns {string|null} Stack name
189
+ */
190
+ function getStackName(appDefinition, options) {
191
+ // Try to get from app definition
192
+ if (appDefinition?.name) {
193
+ const stage = options.stage || 'dev';
194
+ return `${appDefinition.name}-${stage}`;
195
+ }
166
196
 
167
- childProcess.on('close', (code) => {
168
- if (code !== 0) {
169
- console.log(`Child process exited with code ${code}`);
197
+ // Try to get from infrastructure.js
198
+ const infraPath = path.join(process.cwd(), PATHS.INFRASTRUCTURE);
199
+ if (fs.existsSync(infraPath)) {
200
+ try {
201
+ const infraModule = require(infraPath);
202
+ if (infraModule.service) {
203
+ const stage = options.stage || 'dev';
204
+ return `${infraModule.service}-${stage}`;
205
+ }
206
+ } catch (error) {
207
+ // Ignore errors reading infrastructure file
170
208
  }
171
- });
209
+ }
210
+
211
+ return null;
212
+ }
213
+
214
+ /**
215
+ * Run post-deployment health check
216
+ * @param {string} stackName - CloudFormation stack name
217
+ * @param {Object} options - Deploy options
218
+ */
219
+ async function runPostDeploymentHealthCheck(stackName, options) {
220
+ console.log('\n' + '═'.repeat(80));
221
+ console.log('Running post-deployment health check...');
222
+ console.log('═'.repeat(80));
223
+
224
+ try {
225
+ // Run doctor command (will exit process on its own)
226
+ // Note: We need to catch the exit to prevent deploy from exiting
227
+ const originalExit = process.exit;
228
+ let doctorExitCode = 0;
229
+
230
+ // Temporarily override process.exit to capture exit code
231
+ process.exit = (code) => {
232
+ doctorExitCode = code || 0;
233
+ };
234
+
235
+ try {
236
+ await doctorCommand(stackName, {
237
+ region: options.region,
238
+ format: 'console',
239
+ verbose: options.verbose,
240
+ });
241
+ } catch (error) {
242
+ console.log(`\nāš ļø Health check encountered an error: ${error.message}`);
243
+ if (options.verbose) {
244
+ console.error(error.stack);
245
+ }
246
+ } finally {
247
+ // Restore original process.exit
248
+ process.exit = originalExit;
249
+ }
250
+
251
+ // Inform user about health check results
252
+ if (doctorExitCode === 0) {
253
+ console.log('\nāœ“ Post-deployment health check: PASSED');
254
+ } else if (doctorExitCode === 2) {
255
+ console.log('\nāš ļø Post-deployment health check: DEGRADED');
256
+ console.log(' Run "frigg repair" to fix detected issues');
257
+ } else {
258
+ console.log('\nāœ— Post-deployment health check: FAILED');
259
+ console.log(' Run "frigg doctor" for detailed report');
260
+ console.log(' Run "frigg repair" to fix detected issues');
261
+ }
262
+ } catch (error) {
263
+ console.log(`\nāš ļø Post-deployment health check failed: ${error.message}`);
264
+ if (options.verbose) {
265
+ console.error(error.stack);
266
+ }
267
+ }
172
268
  }
173
269
 
174
270
  async function deployCommand(options) {
@@ -177,7 +273,30 @@ async function deployCommand(options) {
177
273
  const appDefinition = loadAppDefinition();
178
274
  const environment = validateAndBuildEnvironment(appDefinition, options);
179
275
 
180
- executeServerlessDeployment(environment, options);
276
+ // Execute deployment
277
+ const exitCode = await executeServerlessDeployment(environment, options);
278
+
279
+ // Check if deployment was successful
280
+ if (exitCode !== 0) {
281
+ console.error(`\nāœ— Deployment failed with exit code ${exitCode}`);
282
+ process.exit(exitCode);
283
+ }
284
+
285
+ console.log('\nāœ“ Deployment completed successfully!');
286
+
287
+ // Run post-deployment health check (unless --skip-doctor)
288
+ if (!options.skipDoctor) {
289
+ const stackName = getStackName(appDefinition, options);
290
+
291
+ if (stackName) {
292
+ await runPostDeploymentHealthCheck(stackName, options);
293
+ } else {
294
+ console.log('\nāš ļø Could not determine stack name - skipping health check');
295
+ console.log(' Run "frigg doctor <stack-name>" manually to check stack health');
296
+ }
297
+ } else {
298
+ console.log('\nā­ļø Skipping post-deployment health check (--skip-doctor)');
299
+ }
181
300
  }
182
301
 
183
302
  module.exports = { deployCommand };
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Frigg Doctor Command
3
+ *
4
+ * Performs comprehensive health check on deployed CloudFormation stack
5
+ * and reports issues like property drift, orphaned resources, and missing resources.
6
+ *
7
+ * Usage:
8
+ * frigg doctor <stack-name>
9
+ * frigg doctor my-app-prod --region us-east-1
10
+ * frigg doctor my-app-prod --format json --output report.json
11
+ */
12
+
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+
16
+ // Domain and Application Layer
17
+ const StackIdentifier = require('@friggframework/devtools/infrastructure/domains/health/domain/value-objects/stack-identifier');
18
+ const RunHealthCheckUseCase = require('@friggframework/devtools/infrastructure/domains/health/application/use-cases/run-health-check-use-case');
19
+
20
+ // Infrastructure Layer - AWS Adapters
21
+ const AWSStackRepository = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository');
22
+ const AWSResourceDetector = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector');
23
+
24
+ // Domain Services
25
+ const MismatchAnalyzer = require('@friggframework/devtools/infrastructure/domains/health/domain/services/mismatch-analyzer');
26
+ const HealthScoreCalculator = require('@friggframework/devtools/infrastructure/domains/health/domain/services/health-score-calculator');
27
+
28
+ /**
29
+ * Format health report for console output
30
+ * @param {StackHealthReport} report - Health check report
31
+ * @param {Object} options - Formatting options
32
+ * @returns {string} Formatted output
33
+ */
34
+ function formatConsoleOutput(report, options = {}) {
35
+ const lines = [];
36
+ const summary = report.getSummary();
37
+
38
+ // Header
39
+ lines.push('');
40
+ lines.push('═'.repeat(80));
41
+ lines.push(` FRIGG DOCTOR - Stack Health Report`);
42
+ lines.push('═'.repeat(80));
43
+ lines.push('');
44
+
45
+ // Stack Information
46
+ lines.push(`Stack: ${summary.stackName}`);
47
+ lines.push(`Region: ${summary.region}`);
48
+ lines.push(`Timestamp: ${summary.timestamp}`);
49
+ lines.push('');
50
+
51
+ // Health Score
52
+ const scoreIcon = report.healthScore.isHealthy() ? 'āœ“' : report.healthScore.isUnhealthy() ? 'āœ—' : '⚠';
53
+ const scoreColor = report.healthScore.isHealthy() ? '' : '';
54
+ lines.push(`Health Score: ${scoreIcon} ${summary.healthScore}/100 (${summary.qualitativeAssessment})`);
55
+ lines.push('');
56
+
57
+ // Summary Statistics
58
+ lines.push('─'.repeat(80));
59
+ lines.push('Resources:');
60
+ lines.push(` Total: ${summary.resourceCount}`);
61
+ lines.push(` In Stack: ${report.getResourcesInStack().length}`);
62
+ lines.push(` Drifted: ${summary.driftedResourceCount}`);
63
+ lines.push(` Orphaned: ${summary.orphanedResourceCount}`);
64
+ lines.push(` Missing: ${summary.missingResourceCount}`);
65
+ lines.push('');
66
+
67
+ lines.push('Issues:');
68
+ lines.push(` Total: ${summary.issueCount}`);
69
+ lines.push(` Critical: ${summary.criticalIssueCount}`);
70
+ lines.push(` Warnings: ${summary.warningCount}`);
71
+ lines.push('');
72
+
73
+ // Issues Detail
74
+ if (report.issues.length > 0) {
75
+ lines.push('─'.repeat(80));
76
+ lines.push('Issue Details:');
77
+ lines.push('');
78
+
79
+ // Group issues by type
80
+ const criticalIssues = report.getCriticalIssues();
81
+ const warnings = report.getWarnings();
82
+
83
+ if (criticalIssues.length > 0) {
84
+ lines.push(' CRITICAL ISSUES:');
85
+ criticalIssues.forEach((issue, idx) => {
86
+ lines.push(` ${idx + 1}. [${issue.type}] ${issue.description}`);
87
+ lines.push(` Resource: ${issue.resourceType} (${issue.resourceId})`);
88
+ if (issue.resolution) {
89
+ lines.push(` Fix: ${issue.resolution}`);
90
+ }
91
+ lines.push('');
92
+ });
93
+ }
94
+
95
+ if (warnings.length > 0) {
96
+ lines.push(' WARNINGS:');
97
+ warnings.forEach((issue, idx) => {
98
+ lines.push(` ${idx + 1}. [${issue.type}] ${issue.description}`);
99
+ lines.push(` Resource: ${issue.resourceType} (${issue.resourceId})`);
100
+ if (issue.resolution) {
101
+ lines.push(` Fix: ${issue.resolution}`);
102
+ }
103
+ lines.push('');
104
+ });
105
+ }
106
+ } else {
107
+ lines.push('─'.repeat(80));
108
+ lines.push('āœ“ No issues detected - stack is healthy!');
109
+ lines.push('');
110
+ }
111
+
112
+ // Recommendations
113
+ if (report.issues.length > 0) {
114
+ lines.push('─'.repeat(80));
115
+ lines.push('Recommended Actions:');
116
+ lines.push('');
117
+
118
+ if (report.getOrphanedResourceCount() > 0) {
119
+ lines.push(` • Import ${report.getOrphanedResourceCount()} orphaned resource(s):`);
120
+ lines.push(` $ frigg repair --import <stack-name>`);
121
+ lines.push('');
122
+ }
123
+
124
+ if (report.getDriftedResourceCount() > 0) {
125
+ lines.push(` • Reconcile property drift for ${report.getDriftedResourceCount()} resource(s):`);
126
+ lines.push(` $ frigg repair --reconcile <stack-name>`);
127
+ lines.push('');
128
+ }
129
+
130
+ if (report.getMissingResourceCount() > 0) {
131
+ lines.push(` • Investigate ${report.getMissingResourceCount()} missing resource(s) and redeploy:`);
132
+ lines.push(` $ frigg deploy --stage <stage>`);
133
+ lines.push('');
134
+ }
135
+ }
136
+
137
+ lines.push('═'.repeat(80));
138
+ lines.push('');
139
+
140
+ return lines.join('\n');
141
+ }
142
+
143
+ /**
144
+ * Format health report as JSON
145
+ * @param {StackHealthReport} report - Health check report
146
+ * @returns {string} JSON output
147
+ */
148
+ function formatJsonOutput(report) {
149
+ return JSON.stringify(report.toJSON(), null, 2);
150
+ }
151
+
152
+ /**
153
+ * Write output to file
154
+ * @param {string} content - Content to write
155
+ * @param {string} filePath - Output file path
156
+ */
157
+ function writeOutputFile(content, filePath) {
158
+ try {
159
+ fs.writeFileSync(filePath, content, 'utf8');
160
+ console.log(`\nāœ“ Report saved to: ${filePath}`);
161
+ } catch (error) {
162
+ console.error(`\nāœ— Failed to write output file: ${error.message}`);
163
+ process.exit(1);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Execute health check
169
+ * @param {string} stackName - CloudFormation stack name
170
+ * @param {Object} options - Command options
171
+ */
172
+ async function doctorCommand(stackName, options = {}) {
173
+ try {
174
+ // Validate required parameter
175
+ if (!stackName) {
176
+ console.error('Error: Stack name is required');
177
+ console.log('Usage: frigg doctor <stack-name> [options]');
178
+ process.exit(1);
179
+ }
180
+
181
+ // Extract options with defaults
182
+ const region = options.region || process.env.AWS_REGION || 'us-east-1';
183
+ const format = options.format || 'console';
184
+ const verbose = options.verbose || false;
185
+
186
+ if (verbose) {
187
+ console.log(`\nšŸ” Running health check on stack: ${stackName} (${region})`);
188
+ }
189
+
190
+ // 1. Create stack identifier
191
+ const stackIdentifier = new StackIdentifier({ stackName, region });
192
+
193
+ // 2. Wire up infrastructure layer (AWS adapters)
194
+ const stackRepository = new AWSStackRepository({ region });
195
+ const resourceDetector = new AWSResourceDetector({ region });
196
+
197
+ // 3. Wire up domain services
198
+ const mismatchAnalyzer = new MismatchAnalyzer();
199
+ const healthScoreCalculator = new HealthScoreCalculator();
200
+
201
+ // 4. Create and execute use case
202
+ const runHealthCheckUseCase = new RunHealthCheckUseCase({
203
+ stackRepository,
204
+ resourceDetector,
205
+ mismatchAnalyzer,
206
+ healthScoreCalculator,
207
+ });
208
+
209
+ const report = await runHealthCheckUseCase.execute({ stackIdentifier });
210
+
211
+ // 5. Format and output results
212
+ if (format === 'json') {
213
+ const jsonOutput = formatJsonOutput(report);
214
+
215
+ if (options.output) {
216
+ writeOutputFile(jsonOutput, options.output);
217
+ } else {
218
+ console.log(jsonOutput);
219
+ }
220
+ } else {
221
+ const consoleOutput = formatConsoleOutput(report, options);
222
+ console.log(consoleOutput);
223
+
224
+ if (options.output) {
225
+ writeOutputFile(consoleOutput, options.output);
226
+ }
227
+ }
228
+
229
+ // 6. Exit with appropriate code
230
+ // Exit code 0 = healthy, 1 = unhealthy, 2 = degraded
231
+ if (report.healthScore.isUnhealthy()) {
232
+ process.exit(1);
233
+ } else if (report.healthScore.isDegraded()) {
234
+ process.exit(2);
235
+ } else {
236
+ process.exit(0);
237
+ }
238
+ } catch (error) {
239
+ console.error(`\nāœ— Health check failed: ${error.message}`);
240
+
241
+ if (options.verbose && error.stack) {
242
+ console.error(`\nStack trace:\n${error.stack}`);
243
+ }
244
+
245
+ process.exit(1);
246
+ }
247
+ }
248
+
249
+ module.exports = { doctorCommand };
@@ -9,6 +9,8 @@ const { deployCommand } = require('./deploy-command');
9
9
  const { generateIamCommand } = require('./generate-iam-command');
10
10
  const { uiCommand } = require('./ui-command');
11
11
  const { dbSetupCommand } = require('./db-setup-command');
12
+ const { doctorCommand } = require('./doctor-command');
13
+ const { repairCommand } = require('./repair-command');
12
14
 
13
15
  const program = new Command();
14
16
 
@@ -46,6 +48,7 @@ program
46
48
  .option('-s, --stage <stage>', 'deployment stage', 'dev')
47
49
  .option('-v, --verbose', 'enable verbose output')
48
50
  .option('-f, --force', 'force deployment (bypasses caching for layers and functions)')
51
+ .option('--skip-doctor', 'skip post-deployment health check')
49
52
  .action(deployCommand);
50
53
 
51
54
  program
@@ -71,6 +74,26 @@ program
71
74
  .option('-v, --verbose', 'enable verbose output')
72
75
  .action(dbSetupCommand);
73
76
 
77
+ program
78
+ .command('doctor <stackName>')
79
+ .description('Run health check on deployed CloudFormation stack')
80
+ .option('-r, --region <region>', 'AWS region (defaults to AWS_REGION env var or us-east-1)')
81
+ .option('-f, --format <format>', 'output format (console or json)', 'console')
82
+ .option('-o, --output <path>', 'save report to file')
83
+ .option('-v, --verbose', 'enable verbose output')
84
+ .action(doctorCommand);
85
+
86
+ program
87
+ .command('repair <stackName>')
88
+ .description('Repair infrastructure issues (import orphaned resources, reconcile property drift)')
89
+ .option('-r, --region <region>', 'AWS region (defaults to AWS_REGION env var or us-east-1)')
90
+ .option('--import', 'import orphaned resources into stack')
91
+ .option('--reconcile', 'reconcile property drift')
92
+ .option('--mode <mode>', 'reconciliation mode (template or resource)', 'template')
93
+ .option('-y, --yes', 'skip confirmation prompts')
94
+ .option('-v, --verbose', 'enable verbose output')
95
+ .action(repairCommand);
96
+
74
97
  program.parse(process.argv);
75
98
 
76
- module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand, dbSetupCommand };
99
+ module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand, dbSetupCommand, doctorCommand, repairCommand };
@@ -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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.474.82fd52e.0",
4
+ "version": "2.0.0--canary.474.988ec0b.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-ec2": "^3.835.0",
7
7
  "@aws-sdk/client-kms": "^3.835.0",
@@ -11,8 +11,8 @@
11
11
  "@babel/eslint-parser": "^7.18.9",
12
12
  "@babel/parser": "^7.25.3",
13
13
  "@babel/traverse": "^7.25.3",
14
- "@friggframework/schemas": "2.0.0--canary.474.82fd52e.0",
15
- "@friggframework/test": "2.0.0--canary.474.82fd52e.0",
14
+ "@friggframework/schemas": "2.0.0--canary.474.988ec0b.0",
15
+ "@friggframework/test": "2.0.0--canary.474.988ec0b.0",
16
16
  "@hapi/boom": "^10.0.1",
17
17
  "@inquirer/prompts": "^5.3.8",
18
18
  "axios": "^1.7.2",
@@ -34,8 +34,8 @@
34
34
  "serverless-http": "^2.7.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@friggframework/eslint-config": "2.0.0--canary.474.82fd52e.0",
38
- "@friggframework/prettier-config": "2.0.0--canary.474.82fd52e.0",
37
+ "@friggframework/eslint-config": "2.0.0--canary.474.988ec0b.0",
38
+ "@friggframework/prettier-config": "2.0.0--canary.474.988ec0b.0",
39
39
  "aws-sdk-client-mock": "^4.1.0",
40
40
  "aws-sdk-client-mock-jest": "^4.1.0",
41
41
  "jest": "^30.1.3",
@@ -70,5 +70,5 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  },
73
- "gitHead": "82fd52ebb10378b6abe016b5abe6c8a4bb13b28f"
73
+ "gitHead": "988ec0bd25a84c55638db9a71fcbccb2b678e603"
74
74
  }