@friggframework/frigg-cli
Version:
Frigg Framework CLI tool
250 lines (212 loc) • 8.88 kB
JavaScript
/**
* Frigg Doctor Command
*
* Performs comprehensive health check on deployed CloudFormation stack
* and reports issues like property drift, orphaned resources, and missing resources.
*
* Usage:
* frigg doctor <stack-name>
* frigg doctor my-app-prod --region us-east-1
* frigg doctor my-app-prod --format json --output report.json
*/
const path = require('path');
const fs = require('fs');
// Domain and Application Layer
const StackIdentifier = require('@friggframework/devtools/infrastructure/domains/health/domain/value-objects/stack-identifier');
const RunHealthCheckUseCase = require('@friggframework/devtools/infrastructure/domains/health/application/use-cases/run-health-check-use-case');
// Infrastructure Layer - AWS Adapters
const AWSStackRepository = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository');
const AWSResourceDetector = require('@friggframework/devtools/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector');
// Domain Services
const MismatchAnalyzer = require('@friggframework/devtools/infrastructure/domains/health/domain/services/mismatch-analyzer');
const HealthScoreCalculator = require('@friggframework/devtools/infrastructure/domains/health/domain/services/health-score-calculator');
/**
* Format health report for console output
* @param {StackHealthReport} report - Health check report
* @param {Object} options - Formatting options
* @returns {string} Formatted output
*/
function formatConsoleOutput(report, options = {}) {
const lines = [];
const summary = report.getSummary();
// Header
lines.push('');
lines.push('═'.repeat(80));
lines.push(` FRIGG DOCTOR - Stack Health Report`);
lines.push('═'.repeat(80));
lines.push('');
// Stack Information
lines.push(`Stack: ${summary.stackName}`);
lines.push(`Region: ${summary.region}`);
lines.push(`Timestamp: ${summary.timestamp}`);
lines.push('');
// Health Score
const scoreIcon = report.healthScore.isHealthy() ? '✓' : report.healthScore.isUnhealthy() ? '✗' : '⚠';
const scoreColor = report.healthScore.isHealthy() ? '' : '';
lines.push(`Health Score: ${scoreIcon} ${summary.healthScore}/100 (${summary.qualitativeAssessment})`);
lines.push('');
// Summary Statistics
lines.push('─'.repeat(80));
lines.push('Resources:');
lines.push(` Total: ${summary.resourceCount}`);
lines.push(` In Stack: ${report.getResourcesInStack().length}`);
lines.push(` Drifted: ${summary.driftedResourceCount}`);
lines.push(` Orphaned: ${summary.orphanedResourceCount}`);
lines.push(` Missing: ${summary.missingResourceCount}`);
lines.push('');
lines.push('Issues:');
lines.push(` Total: ${summary.issueCount}`);
lines.push(` Critical: ${summary.criticalIssueCount}`);
lines.push(` Warnings: ${summary.warningCount}`);
lines.push('');
// Issues Detail
if (report.issues.length > 0) {
lines.push('─'.repeat(80));
lines.push('Issue Details:');
lines.push('');
// Group issues by type
const criticalIssues = report.getCriticalIssues();
const warnings = report.getWarnings();
if (criticalIssues.length > 0) {
lines.push(' CRITICAL ISSUES:');
criticalIssues.forEach((issue, idx) => {
lines.push(` ${idx + 1}. [${issue.type}] ${issue.description}`);
lines.push(` Resource: ${issue.resourceType} (${issue.resourceId})`);
if (issue.resolution) {
lines.push(` Fix: ${issue.resolution}`);
}
lines.push('');
});
}
if (warnings.length > 0) {
lines.push(' WARNINGS:');
warnings.forEach((issue, idx) => {
lines.push(` ${idx + 1}. [${issue.type}] ${issue.description}`);
lines.push(` Resource: ${issue.resourceType} (${issue.resourceId})`);
if (issue.resolution) {
lines.push(` Fix: ${issue.resolution}`);
}
lines.push('');
});
}
} else {
lines.push('─'.repeat(80));
lines.push('✓ No issues detected - stack is healthy!');
lines.push('');
}
// Recommendations
if (report.issues.length > 0) {
lines.push('─'.repeat(80));
lines.push('Recommended Actions:');
lines.push('');
if (report.getOrphanedResourceCount() > 0) {
lines.push(` • Import ${report.getOrphanedResourceCount()} orphaned resource(s):`);
lines.push(` $ frigg repair --import <stack-name>`);
lines.push('');
}
if (report.getDriftedResourceCount() > 0) {
lines.push(` • Reconcile property drift for ${report.getDriftedResourceCount()} resource(s):`);
lines.push(` $ frigg repair --reconcile <stack-name>`);
lines.push('');
}
if (report.getMissingResourceCount() > 0) {
lines.push(` • Investigate ${report.getMissingResourceCount()} missing resource(s) and redeploy:`);
lines.push(` $ frigg deploy --stage <stage>`);
lines.push('');
}
}
lines.push('═'.repeat(80));
lines.push('');
return lines.join('\n');
}
/**
* Format health report as JSON
* @param {StackHealthReport} report - Health check report
* @returns {string} JSON output
*/
function formatJsonOutput(report) {
return JSON.stringify(report.toJSON(), null, 2);
}
/**
* Write output to file
* @param {string} content - Content to write
* @param {string} filePath - Output file path
*/
function writeOutputFile(content, filePath) {
try {
fs.writeFileSync(filePath, content, 'utf8');
console.log(`\n✓ Report saved to: ${filePath}`);
} catch (error) {
console.error(`\n✗ Failed to write output file: ${error.message}`);
process.exit(1);
}
}
/**
* Execute health check
* @param {string} stackName - CloudFormation stack name
* @param {Object} options - Command options
*/
async function doctorCommand(stackName, options = {}) {
try {
// Validate required parameter
if (!stackName) {
console.error('Error: Stack name is required');
console.log('Usage: frigg doctor <stack-name> [options]');
process.exit(1);
}
// Extract options with defaults
const region = options.region || process.env.AWS_REGION || 'us-east-1';
const format = options.format || 'console';
const verbose = options.verbose || false;
if (verbose) {
console.log(`\n🔍 Running health check on stack: ${stackName} (${region})`);
}
// 1. Create stack identifier
const stackIdentifier = new StackIdentifier({ stackName, region });
// 2. Wire up infrastructure layer (AWS adapters)
const stackRepository = new AWSStackRepository({ region });
const resourceDetector = new AWSResourceDetector({ region });
// 3. Wire up domain services
const mismatchAnalyzer = new MismatchAnalyzer();
const healthScoreCalculator = new HealthScoreCalculator();
// 4. Create and execute use case
const runHealthCheckUseCase = new RunHealthCheckUseCase({
stackRepository,
resourceDetector,
mismatchAnalyzer,
healthScoreCalculator,
});
const report = await runHealthCheckUseCase.execute({ stackIdentifier });
// 5. Format and output results
if (format === 'json') {
const jsonOutput = formatJsonOutput(report);
if (options.output) {
writeOutputFile(jsonOutput, options.output);
} else {
console.log(jsonOutput);
}
} else {
const consoleOutput = formatConsoleOutput(report, options);
console.log(consoleOutput);
if (options.output) {
writeOutputFile(consoleOutput, options.output);
}
}
// 6. Exit with appropriate code
// Exit code 0 = healthy, 1 = unhealthy, 2 = degraded
if (report.healthScore.isUnhealthy()) {
process.exit(1);
} else if (report.healthScore.isDegraded()) {
process.exit(2);
} else {
process.exit(0);
}
} catch (error) {
console.error(`\n✗ Health check failed: ${error.message}`);
if (options.verbose && error.stack) {
console.error(`\nStack trace:\n${error.stack}`);
}
process.exit(1);
}
}
module.exports = { doctorCommand };