@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,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 };