@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.
- package/frigg-cli/README.md +1290 -0
- package/frigg-cli/__tests__/unit/commands/build.test.js +279 -0
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +548 -0
- package/frigg-cli/__tests__/unit/commands/deploy.test.js +320 -0
- package/frigg-cli/__tests__/unit/commands/doctor.test.js +309 -0
- package/frigg-cli/__tests__/unit/commands/install.test.js +400 -0
- package/frigg-cli/__tests__/unit/commands/ui.test.js +346 -0
- package/frigg-cli/__tests__/unit/dependencies.test.js +74 -0
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +366 -0
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +304 -0
- package/frigg-cli/__tests__/unit/version-detection.test.js +171 -0
- package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
- package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
- package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
- package/frigg-cli/__tests__/utils/test-setup.js +287 -0
- package/frigg-cli/build-command/index.js +66 -0
- package/frigg-cli/db-setup-command/index.js +193 -0
- package/frigg-cli/deploy-command/SPEC-DEPLOY-DRY-RUN.md +981 -0
- package/frigg-cli/deploy-command/index.js +302 -0
- package/frigg-cli/doctor-command/index.js +335 -0
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +301 -0
- package/frigg-cli/generate-command/azure-generator.js +43 -0
- package/frigg-cli/generate-command/gcp-generator.js +47 -0
- package/frigg-cli/generate-command/index.js +332 -0
- package/frigg-cli/generate-command/terraform-generator.js +555 -0
- package/frigg-cli/generate-iam-command.js +118 -0
- package/frigg-cli/index.js +173 -0
- package/frigg-cli/index.test.js +158 -0
- package/frigg-cli/init-command/backend-first-handler.js +756 -0
- package/frigg-cli/init-command/index.js +93 -0
- package/frigg-cli/init-command/template-handler.js +143 -0
- package/frigg-cli/install-command/backend-js.js +33 -0
- package/frigg-cli/install-command/commit-changes.js +16 -0
- package/frigg-cli/install-command/environment-variables.js +127 -0
- package/frigg-cli/install-command/environment-variables.test.js +136 -0
- package/frigg-cli/install-command/index.js +54 -0
- package/frigg-cli/install-command/install-package.js +13 -0
- package/frigg-cli/install-command/integration-file.js +30 -0
- package/frigg-cli/install-command/logger.js +12 -0
- package/frigg-cli/install-command/template.js +90 -0
- package/frigg-cli/install-command/validate-package.js +75 -0
- package/frigg-cli/jest.config.js +124 -0
- package/frigg-cli/package.json +63 -0
- package/frigg-cli/repair-command/index.js +564 -0
- package/frigg-cli/start-command/index.js +149 -0
- package/frigg-cli/start-command/start-command.test.js +297 -0
- package/frigg-cli/test/init-command.test.js +180 -0
- package/frigg-cli/test/npm-registry.test.js +319 -0
- package/frigg-cli/ui-command/index.js +154 -0
- package/frigg-cli/utils/app-resolver.js +319 -0
- package/frigg-cli/utils/backend-path.js +25 -0
- package/frigg-cli/utils/database-validator.js +154 -0
- package/frigg-cli/utils/error-messages.js +257 -0
- package/frigg-cli/utils/npm-registry.js +167 -0
- package/frigg-cli/utils/process-manager.js +199 -0
- package/frigg-cli/utils/repo-detection.js +405 -0
- package/infrastructure/create-frigg-infrastructure.js +125 -12
- package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +1317 -0
- package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +306 -0
- package/infrastructure/domains/shared/resource-discovery.js +31 -2
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +1 -1
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +109 -5
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +310 -4
- package/infrastructure/domains/shared/validation/plugin-validator.js +187 -0
- package/infrastructure/domains/shared/validation/plugin-validator.test.js +323 -0
- package/infrastructure/infrastructure-composer.js +22 -0
- package/layers/prisma/.build-complete +3 -0
- package/package.json +18 -7
- package/management-ui/package-lock.json +0 -16517
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
// Import doctor command for post-deployment health check
|
|
6
|
+
const { doctorCommand } = require('../doctor-command');
|
|
7
|
+
|
|
8
|
+
// Configuration constants
|
|
9
|
+
const PATHS = {
|
|
10
|
+
APP_DEFINITION: 'index.js',
|
|
11
|
+
INFRASTRUCTURE: 'infrastructure.js'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const COMMANDS = {
|
|
15
|
+
SERVERLESS: 'osls' // OSS-Serverless (drop-in replacement for serverless v3)
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Constructs filtered environment variables for serverless deployment
|
|
20
|
+
* @param {string[]} appDefinedVariables - Array of environment variable names from app definition
|
|
21
|
+
* @returns {Object} Filtered environment variables object
|
|
22
|
+
*/
|
|
23
|
+
function buildFilteredEnvironment(appDefinedVariables) {
|
|
24
|
+
return {
|
|
25
|
+
// Essential system variables needed to run serverless
|
|
26
|
+
PATH: process.env.PATH,
|
|
27
|
+
HOME: process.env.HOME,
|
|
28
|
+
USER: process.env.USER,
|
|
29
|
+
|
|
30
|
+
// AWS credentials and configuration (all AWS_ prefixed variables)
|
|
31
|
+
...Object.fromEntries(
|
|
32
|
+
Object.entries(process.env).filter(([key]) =>
|
|
33
|
+
key.startsWith('AWS_')
|
|
34
|
+
)
|
|
35
|
+
),
|
|
36
|
+
|
|
37
|
+
// App-defined environment variables
|
|
38
|
+
...Object.fromEntries(
|
|
39
|
+
appDefinedVariables
|
|
40
|
+
.map((key) => [key, process.env[key]])
|
|
41
|
+
.filter(([_, value]) => value !== undefined)
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Loads and parses the app definition from index.js
|
|
48
|
+
* @returns {Object|null} App definition object or null if not found
|
|
49
|
+
*/
|
|
50
|
+
function loadAppDefinition() {
|
|
51
|
+
const appDefPath = path.join(process.cwd(), PATHS.APP_DEFINITION);
|
|
52
|
+
|
|
53
|
+
if (!fs.existsSync(appDefPath)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const { Definition } = require(appDefPath);
|
|
59
|
+
return Definition;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn('Could not load appDefinition environment config:', error.message);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extracts environment variable names from app definition
|
|
68
|
+
* @param {Object} appDefinition - App definition object
|
|
69
|
+
* @returns {string[]} Array of environment variable names
|
|
70
|
+
*/
|
|
71
|
+
function extractEnvironmentVariables(appDefinition) {
|
|
72
|
+
if (!appDefinition?.environment) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('š§ Loading environment configuration from appDefinition...');
|
|
77
|
+
|
|
78
|
+
const appDefinedVariables = Object.keys(appDefinition.environment).filter(
|
|
79
|
+
(key) => appDefinition.environment[key] === true
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
console.log(` Found ${appDefinedVariables.length} environment variables: ${appDefinedVariables.join(', ')}`);
|
|
83
|
+
return appDefinedVariables;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handles environment validation warnings
|
|
88
|
+
* @param {Object} validation - Validation result object
|
|
89
|
+
* @param {Object} options - Deploy command options
|
|
90
|
+
*/
|
|
91
|
+
function handleValidationWarnings(validation, options) {
|
|
92
|
+
if (validation.missing.length === 0 || options.skipEnvValidation) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.warn(`ā ļø Warning: Missing ${validation.missing.length} environment variables: ${validation.missing.join(', ')}`);
|
|
97
|
+
console.warn(' These variables are optional and deployment will continue');
|
|
98
|
+
console.warn(' Run with --skip-env-validation to bypass this check');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validates environment variables and builds filtered environment
|
|
103
|
+
* @param {Object} appDefinition - App definition object
|
|
104
|
+
* @param {Object} options - Deploy command options
|
|
105
|
+
* @returns {Object} Filtered environment variables
|
|
106
|
+
*/
|
|
107
|
+
function validateAndBuildEnvironment(appDefinition, options) {
|
|
108
|
+
if (!appDefinition) {
|
|
109
|
+
return buildFilteredEnvironment([]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const appDefinedVariables = extractEnvironmentVariables(appDefinition);
|
|
113
|
+
|
|
114
|
+
// Try to use the env-validator if available
|
|
115
|
+
try {
|
|
116
|
+
const { validateEnvironmentVariables } = require('../../infrastructure/env-validator');
|
|
117
|
+
const validation = validateEnvironmentVariables(appDefinition);
|
|
118
|
+
|
|
119
|
+
handleValidationWarnings(validation, options);
|
|
120
|
+
return buildFilteredEnvironment(appDefinedVariables);
|
|
121
|
+
|
|
122
|
+
} catch (validatorError) {
|
|
123
|
+
// Validator not available, do basic validation
|
|
124
|
+
const missingVariables = appDefinedVariables.filter((variable) => !process.env[variable]);
|
|
125
|
+
|
|
126
|
+
if (missingVariables.length > 0) {
|
|
127
|
+
console.warn(`ā ļø Warning: Missing ${missingVariables.length} environment variables: ${missingVariables.join(', ')}`);
|
|
128
|
+
console.warn(' These variables are optional and deployment will continue');
|
|
129
|
+
console.warn(' Set them in your CI/CD environment or .env file if needed');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return buildFilteredEnvironment(appDefinedVariables);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Executes the serverless deployment command
|
|
138
|
+
* @param {Object} environment - Environment variables to pass to serverless
|
|
139
|
+
* @param {Object} options - Deploy command options
|
|
140
|
+
* @returns {Promise<number>} Exit code
|
|
141
|
+
*/
|
|
142
|
+
function executeServerlessDeployment(environment, options) {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
console.log('š Deploying serverless application...');
|
|
145
|
+
|
|
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
|
+
}
|
|
158
|
+
|
|
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
|
+
});
|
|
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
|
+
}
|
|
196
|
+
|
|
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
|
|
208
|
+
}
|
|
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
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function deployCommand(options) {
|
|
271
|
+
console.log('Deploying the serverless application...');
|
|
272
|
+
|
|
273
|
+
const appDefinition = loadAppDefinition();
|
|
274
|
+
const environment = validateAndBuildEnvironment(appDefinition, options);
|
|
275
|
+
|
|
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
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = { deployCommand };
|
|
@@ -0,0 +1,335 @@
|
|
|
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
|
+
* frigg doctor # Interactive stack selection
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const { select } = require('@inquirer/prompts');
|
|
17
|
+
const { CloudFormationClient, ListStacksCommand } = require('@aws-sdk/client-cloudformation');
|
|
18
|
+
|
|
19
|
+
// Domain and Application Layer
|
|
20
|
+
const StackIdentifier = require('../../infrastructure/domains/health/domain/value-objects/stack-identifier');
|
|
21
|
+
const RunHealthCheckUseCase = require('../../infrastructure/domains/health/application/use-cases/run-health-check-use-case');
|
|
22
|
+
|
|
23
|
+
// Infrastructure Layer - AWS Adapters
|
|
24
|
+
const AWSStackRepository = require('../../infrastructure/domains/health/infrastructure/adapters/aws-stack-repository');
|
|
25
|
+
const AWSResourceDetector = require('../../infrastructure/domains/health/infrastructure/adapters/aws-resource-detector');
|
|
26
|
+
|
|
27
|
+
// Domain Services
|
|
28
|
+
const MismatchAnalyzer = require('../../infrastructure/domains/health/domain/services/mismatch-analyzer');
|
|
29
|
+
const HealthScoreCalculator = require('../../infrastructure/domains/health/domain/services/health-score-calculator');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format health report for console output
|
|
33
|
+
* @param {StackHealthReport} report - Health check report
|
|
34
|
+
* @param {Object} options - Formatting options
|
|
35
|
+
* @returns {string} Formatted output
|
|
36
|
+
*/
|
|
37
|
+
function formatConsoleOutput(report, options = {}) {
|
|
38
|
+
const lines = [];
|
|
39
|
+
const summary = report.getSummary();
|
|
40
|
+
|
|
41
|
+
// Header
|
|
42
|
+
lines.push('');
|
|
43
|
+
lines.push('ā'.repeat(80));
|
|
44
|
+
lines.push(` FRIGG DOCTOR - Stack Health Report`);
|
|
45
|
+
lines.push('ā'.repeat(80));
|
|
46
|
+
lines.push('');
|
|
47
|
+
|
|
48
|
+
// Stack Information
|
|
49
|
+
lines.push(`Stack: ${summary.stackName}`);
|
|
50
|
+
lines.push(`Region: ${summary.region}`);
|
|
51
|
+
lines.push(`Timestamp: ${summary.timestamp}`);
|
|
52
|
+
lines.push('');
|
|
53
|
+
|
|
54
|
+
// Health Score
|
|
55
|
+
const scoreIcon = report.healthScore.isHealthy() ? 'ā' : report.healthScore.isUnhealthy() ? 'ā' : 'ā ';
|
|
56
|
+
const scoreColor = report.healthScore.isHealthy() ? '' : '';
|
|
57
|
+
lines.push(`Health Score: ${scoreIcon} ${summary.healthScore}/100 (${summary.qualitativeAssessment})`);
|
|
58
|
+
lines.push('');
|
|
59
|
+
|
|
60
|
+
// Summary Statistics
|
|
61
|
+
lines.push('ā'.repeat(80));
|
|
62
|
+
lines.push('Resources:');
|
|
63
|
+
lines.push(` Total: ${summary.resourceCount}`);
|
|
64
|
+
lines.push(` In Stack: ${report.getResourcesInStack().length}`);
|
|
65
|
+
lines.push(` Drifted: ${summary.driftedResourceCount}`);
|
|
66
|
+
lines.push(` Orphaned: ${summary.orphanedResourceCount}`);
|
|
67
|
+
lines.push(` Missing: ${summary.missingResourceCount}`);
|
|
68
|
+
lines.push('');
|
|
69
|
+
|
|
70
|
+
lines.push('Issues:');
|
|
71
|
+
lines.push(` Total: ${summary.issueCount}`);
|
|
72
|
+
lines.push(` Critical: ${summary.criticalIssueCount}`);
|
|
73
|
+
lines.push(` Warnings: ${summary.warningCount}`);
|
|
74
|
+
lines.push('');
|
|
75
|
+
|
|
76
|
+
// Issues Detail
|
|
77
|
+
if (report.issues.length > 0) {
|
|
78
|
+
lines.push('ā'.repeat(80));
|
|
79
|
+
lines.push('Issue Details:');
|
|
80
|
+
lines.push('');
|
|
81
|
+
|
|
82
|
+
// Group issues by type
|
|
83
|
+
const criticalIssues = report.getCriticalIssues();
|
|
84
|
+
const warnings = report.getWarnings();
|
|
85
|
+
|
|
86
|
+
if (criticalIssues.length > 0) {
|
|
87
|
+
lines.push(' CRITICAL ISSUES:');
|
|
88
|
+
criticalIssues.forEach((issue, idx) => {
|
|
89
|
+
lines.push(` ${idx + 1}. [${issue.type}] ${issue.description}`);
|
|
90
|
+
lines.push(` Resource: ${issue.resourceType} (${issue.resourceId})`);
|
|
91
|
+
if (issue.resolution) {
|
|
92
|
+
lines.push(` Fix: ${issue.resolution}`);
|
|
93
|
+
}
|
|
94
|
+
lines.push('');
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (warnings.length > 0) {
|
|
99
|
+
lines.push(' WARNINGS:');
|
|
100
|
+
warnings.forEach((issue, idx) => {
|
|
101
|
+
lines.push(` ${idx + 1}. [${issue.type}] ${issue.description}`);
|
|
102
|
+
lines.push(` Resource: ${issue.resourceType} (${issue.resourceId})`);
|
|
103
|
+
if (issue.resolution) {
|
|
104
|
+
lines.push(` Fix: ${issue.resolution}`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
lines.push('ā'.repeat(80));
|
|
111
|
+
lines.push('ā No issues detected - stack is healthy!');
|
|
112
|
+
lines.push('');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Recommendations
|
|
116
|
+
if (report.issues.length > 0) {
|
|
117
|
+
lines.push('ā'.repeat(80));
|
|
118
|
+
lines.push('Recommended Actions:');
|
|
119
|
+
lines.push('');
|
|
120
|
+
|
|
121
|
+
if (report.getOrphanedResourceCount() > 0) {
|
|
122
|
+
lines.push(` ⢠Import ${report.getOrphanedResourceCount()} orphaned resource(s):`);
|
|
123
|
+
lines.push(` $ frigg repair --import <stack-name>`);
|
|
124
|
+
lines.push('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (report.getDriftedResourceCount() > 0) {
|
|
128
|
+
lines.push(` ⢠Reconcile property drift for ${report.getDriftedResourceCount()} resource(s):`);
|
|
129
|
+
lines.push(` $ frigg repair --reconcile <stack-name>`);
|
|
130
|
+
lines.push('');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (report.getMissingResourceCount() > 0) {
|
|
134
|
+
lines.push(` ⢠Investigate ${report.getMissingResourceCount()} missing resource(s) and redeploy:`);
|
|
135
|
+
lines.push(` $ frigg deploy --stage <stage>`);
|
|
136
|
+
lines.push('');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
lines.push('ā'.repeat(80));
|
|
141
|
+
lines.push('');
|
|
142
|
+
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Format health report as JSON
|
|
148
|
+
* @param {StackHealthReport} report - Health check report
|
|
149
|
+
* @returns {string} JSON output
|
|
150
|
+
*/
|
|
151
|
+
function formatJsonOutput(report) {
|
|
152
|
+
return JSON.stringify(report.toJSON(), null, 2);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Write output to file
|
|
157
|
+
* @param {string} content - Content to write
|
|
158
|
+
* @param {string} filePath - Output file path
|
|
159
|
+
*/
|
|
160
|
+
function writeOutputFile(content, filePath) {
|
|
161
|
+
try {
|
|
162
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
163
|
+
console.log(`\nā Report saved to: ${filePath}`);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(`\nā Failed to write output file: ${error.message}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* List CloudFormation stacks in the specified region
|
|
172
|
+
* @param {string} region - AWS region
|
|
173
|
+
* @returns {Promise<Array>} Array of stack objects with name and status
|
|
174
|
+
*/
|
|
175
|
+
async function listStacks(region) {
|
|
176
|
+
const client = new CloudFormationClient({ region });
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const command = new ListStacksCommand({
|
|
180
|
+
StackStatusFilter: [
|
|
181
|
+
'CREATE_COMPLETE',
|
|
182
|
+
'UPDATE_COMPLETE',
|
|
183
|
+
'UPDATE_ROLLBACK_COMPLETE',
|
|
184
|
+
'ROLLBACK_COMPLETE',
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const response = await client.send(command);
|
|
189
|
+
|
|
190
|
+
return (response.StackSummaries || []).map(stack => ({
|
|
191
|
+
name: stack.StackName,
|
|
192
|
+
status: stack.StackStatus,
|
|
193
|
+
createdTime: stack.CreationTime,
|
|
194
|
+
updatedTime: stack.LastUpdatedTime,
|
|
195
|
+
}));
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw new Error(`Failed to list CloudFormation stacks: ${error.message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Prompt user to select a stack from available stacks
|
|
203
|
+
* @param {string} region - AWS region
|
|
204
|
+
* @returns {Promise<string>} Selected stack name
|
|
205
|
+
*/
|
|
206
|
+
async function promptForStackSelection(region) {
|
|
207
|
+
console.log(`\nš Fetching CloudFormation stacks in ${region}...`);
|
|
208
|
+
|
|
209
|
+
const stacks = await listStacks(region);
|
|
210
|
+
|
|
211
|
+
if (stacks.length === 0) {
|
|
212
|
+
console.error(`\nā No CloudFormation stacks found in ${region}`);
|
|
213
|
+
console.log(' Make sure you have stacks deployed and the correct AWS credentials configured.');
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(`\nā Found ${stacks.length} stack(s)\n`);
|
|
218
|
+
|
|
219
|
+
// Create choices with stack name and metadata
|
|
220
|
+
const choices = stacks.map(stack => {
|
|
221
|
+
const statusIcon = stack.status.includes('COMPLETE') ? 'ā' : 'ā ';
|
|
222
|
+
const timeInfo = stack.updatedTime
|
|
223
|
+
? `Updated: ${stack.updatedTime.toLocaleDateString()}`
|
|
224
|
+
: `Created: ${stack.createdTime.toLocaleDateString()}`;
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
name: `${statusIcon} ${stack.name} (${stack.status}) - ${timeInfo}`,
|
|
228
|
+
value: stack.name,
|
|
229
|
+
description: `Status: ${stack.status}`,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const selectedStack = await select({
|
|
234
|
+
message: 'Select a stack to run health check:',
|
|
235
|
+
choices,
|
|
236
|
+
pageSize: 15,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return selectedStack;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Execute health check
|
|
244
|
+
* @param {string} stackName - CloudFormation stack name (optional - will prompt if not provided)
|
|
245
|
+
* @param {Object} options - Command options
|
|
246
|
+
*/
|
|
247
|
+
async function doctorCommand(stackName, options = {}) {
|
|
248
|
+
try {
|
|
249
|
+
// Extract options with defaults
|
|
250
|
+
const region = options.region || process.env.AWS_REGION || 'us-east-1';
|
|
251
|
+
const format = options.format || 'console';
|
|
252
|
+
const verbose = options.verbose || false;
|
|
253
|
+
|
|
254
|
+
// If no stack name provided, prompt user to select from available stacks
|
|
255
|
+
if (!stackName) {
|
|
256
|
+
stackName = await promptForStackSelection(region);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Show progress to user (always, not just verbose mode)
|
|
260
|
+
console.log(`\nš„ Running health check on stack: ${stackName} (${region})\n`);
|
|
261
|
+
|
|
262
|
+
// 1. Create stack identifier
|
|
263
|
+
const stackIdentifier = new StackIdentifier({ stackName, region });
|
|
264
|
+
|
|
265
|
+
// 2. Wire up infrastructure layer (AWS adapters)
|
|
266
|
+
const stackRepository = new AWSStackRepository({ region });
|
|
267
|
+
const resourceDetector = new AWSResourceDetector({ region });
|
|
268
|
+
|
|
269
|
+
// 3. Wire up domain services
|
|
270
|
+
const mismatchAnalyzer = new MismatchAnalyzer();
|
|
271
|
+
const healthScoreCalculator = new HealthScoreCalculator();
|
|
272
|
+
|
|
273
|
+
// 4. Create and execute use case with progress logging
|
|
274
|
+
const runHealthCheckUseCase = new RunHealthCheckUseCase({
|
|
275
|
+
stackRepository,
|
|
276
|
+
resourceDetector,
|
|
277
|
+
mismatchAnalyzer,
|
|
278
|
+
healthScoreCalculator,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Progress callback to show execution status
|
|
282
|
+
const progressCallback = (step, message) => {
|
|
283
|
+
if (verbose) {
|
|
284
|
+
console.log(` ${message}`);
|
|
285
|
+
} else {
|
|
286
|
+
console.log(`${step} ${message}`);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const report = await runHealthCheckUseCase.execute({
|
|
291
|
+
stackIdentifier,
|
|
292
|
+
onProgress: progressCallback
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
console.log('ā Health check complete!\n');
|
|
296
|
+
|
|
297
|
+
// 5. Format and output results
|
|
298
|
+
if (format === 'json') {
|
|
299
|
+
const jsonOutput = formatJsonOutput(report);
|
|
300
|
+
|
|
301
|
+
if (options.output) {
|
|
302
|
+
writeOutputFile(jsonOutput, options.output);
|
|
303
|
+
} else {
|
|
304
|
+
console.log(jsonOutput);
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
const consoleOutput = formatConsoleOutput(report, options);
|
|
308
|
+
console.log(consoleOutput);
|
|
309
|
+
|
|
310
|
+
if (options.output) {
|
|
311
|
+
writeOutputFile(consoleOutput, options.output);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 6. Exit with appropriate code
|
|
316
|
+
// Exit code 0 = healthy, 1 = unhealthy, 2 = degraded
|
|
317
|
+
if (report.healthScore.isUnhealthy()) {
|
|
318
|
+
process.exit(1);
|
|
319
|
+
} else if (report.healthScore.isDegraded()) {
|
|
320
|
+
process.exit(2);
|
|
321
|
+
} else {
|
|
322
|
+
process.exit(0);
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error(`\nā Health check failed: ${error.message}`);
|
|
326
|
+
|
|
327
|
+
if (options.verbose && error.stack) {
|
|
328
|
+
console.error(`\nStack trace:\n${error.stack}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = { doctorCommand };
|