@friggframework/devtools 2.0.0--canary.517.179491e.0 → 2.0.0--canary.522.893db5d.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.
Files changed (115) hide show
  1. package/frigg-cli/README.md +1 -1
  2. package/frigg-cli/__tests__/application/use-cases/AddApiModuleToIntegrationUseCase.test.js +326 -0
  3. package/frigg-cli/__tests__/application/use-cases/CreateApiModuleUseCase.test.js +337 -0
  4. package/frigg-cli/__tests__/domain/entities/ApiModule.test.js +373 -0
  5. package/frigg-cli/__tests__/domain/entities/AppDefinition.test.js +313 -0
  6. package/frigg-cli/__tests__/domain/services/IntegrationValidator.test.js +269 -0
  7. package/frigg-cli/__tests__/domain/value-objects/IntegrationName.test.js +82 -0
  8. package/frigg-cli/__tests__/infrastructure/adapters/IntegrationJsUpdater.test.js +408 -0
  9. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemApiModuleRepository.test.js +583 -0
  10. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemAppDefinitionRepository.test.js +314 -0
  11. package/frigg-cli/__tests__/infrastructure/repositories/FileSystemIntegrationRepository.test.js +383 -0
  12. package/frigg-cli/__tests__/unit/commands/doctor.test.js +0 -2
  13. package/frigg-cli/__tests__/unit/commands/init.test.js +406 -0
  14. package/frigg-cli/__tests__/unit/commands/install.test.js +23 -19
  15. package/frigg-cli/__tests__/unit/commands/repair.test.js +275 -0
  16. package/frigg-cli/__tests__/unit/dependencies.test.js +2 -2
  17. package/frigg-cli/__tests__/unit/start-command/application/RunPreflightChecksUseCase.test.js +411 -0
  18. package/frigg-cli/__tests__/unit/start-command/infrastructure/DatabaseAdapter.test.js +405 -0
  19. package/frigg-cli/__tests__/unit/start-command/infrastructure/DockerAdapter.test.js +496 -0
  20. package/frigg-cli/__tests__/unit/start-command/presentation/InteractivePromptAdapter.test.js +474 -0
  21. package/frigg-cli/__tests__/unit/utils/output.test.js +196 -0
  22. package/frigg-cli/application/use-cases/AddApiModuleToIntegrationUseCase.js +93 -0
  23. package/frigg-cli/application/use-cases/CreateApiModuleUseCase.js +93 -0
  24. package/frigg-cli/application/use-cases/CreateIntegrationUseCase.js +103 -0
  25. package/frigg-cli/container.js +172 -0
  26. package/frigg-cli/docs/OUTPUT_MIGRATION_GUIDE.md +286 -0
  27. package/frigg-cli/doctor-command/index.js +17 -16
  28. package/frigg-cli/domain/entities/ApiModule.js +272 -0
  29. package/frigg-cli/domain/entities/AppDefinition.js +227 -0
  30. package/frigg-cli/domain/entities/Integration.js +198 -0
  31. package/frigg-cli/domain/exceptions/DomainException.js +24 -0
  32. package/frigg-cli/domain/ports/IApiModuleRepository.js +53 -0
  33. package/frigg-cli/domain/ports/IAppDefinitionRepository.js +43 -0
  34. package/frigg-cli/domain/ports/IIntegrationRepository.js +61 -0
  35. package/frigg-cli/domain/services/IntegrationValidator.js +185 -0
  36. package/frigg-cli/domain/value-objects/IntegrationId.js +42 -0
  37. package/frigg-cli/domain/value-objects/IntegrationName.js +60 -0
  38. package/frigg-cli/domain/value-objects/SemanticVersion.js +70 -0
  39. package/frigg-cli/index.js +21 -6
  40. package/frigg-cli/index.test.js +7 -2
  41. package/frigg-cli/infrastructure/UnitOfWork.js +46 -0
  42. package/frigg-cli/infrastructure/adapters/BackendJsUpdater.js +197 -0
  43. package/frigg-cli/infrastructure/adapters/FileSystemAdapter.js +224 -0
  44. package/frigg-cli/infrastructure/adapters/IntegrationJsUpdater.js +249 -0
  45. package/frigg-cli/infrastructure/adapters/SchemaValidator.js +92 -0
  46. package/frigg-cli/infrastructure/repositories/FileSystemApiModuleRepository.js +373 -0
  47. package/frigg-cli/infrastructure/repositories/FileSystemAppDefinitionRepository.js +116 -0
  48. package/frigg-cli/infrastructure/repositories/FileSystemIntegrationRepository.js +277 -0
  49. package/frigg-cli/init-command/backend-first-handler.js +124 -42
  50. package/frigg-cli/init-command/index.js +2 -1
  51. package/frigg-cli/init-command/template-handler.js +13 -3
  52. package/frigg-cli/install-command/backend-js.js +3 -3
  53. package/frigg-cli/install-command/environment-variables.js +16 -19
  54. package/frigg-cli/install-command/environment-variables.test.js +12 -13
  55. package/frigg-cli/install-command/index.js +14 -9
  56. package/frigg-cli/install-command/integration-file.js +3 -3
  57. package/frigg-cli/install-command/validate-package.js +5 -9
  58. package/frigg-cli/jest.config.js +4 -1
  59. package/frigg-cli/package-lock.json +16226 -0
  60. package/frigg-cli/repair-command/index.js +101 -128
  61. package/frigg-cli/start-command/application/RunPreflightChecksUseCase.js +376 -0
  62. package/frigg-cli/start-command/index.js +246 -2
  63. package/frigg-cli/start-command/infrastructure/DatabaseAdapter.js +591 -0
  64. package/frigg-cli/start-command/infrastructure/DockerAdapter.js +306 -0
  65. package/frigg-cli/start-command/presentation/InteractivePromptAdapter.js +329 -0
  66. package/frigg-cli/templates/backend/.env.example +62 -0
  67. package/frigg-cli/templates/backend/.eslintrc.json +12 -0
  68. package/frigg-cli/templates/backend/.prettierrc +6 -0
  69. package/frigg-cli/templates/backend/docker-compose.yml +22 -0
  70. package/frigg-cli/templates/backend/index.js +96 -0
  71. package/frigg-cli/templates/backend/infrastructure.js +12 -0
  72. package/frigg-cli/templates/backend/jest.config.js +17 -0
  73. package/frigg-cli/templates/backend/package.json +50 -0
  74. package/frigg-cli/templates/backend/src/api-modules/.gitkeep +10 -0
  75. package/frigg-cli/templates/backend/src/base/.gitkeep +7 -0
  76. package/frigg-cli/templates/backend/src/integrations/.gitkeep +10 -0
  77. package/frigg-cli/templates/backend/src/integrations/ExampleIntegration.js +65 -0
  78. package/frigg-cli/templates/backend/src/utils/.gitkeep +7 -0
  79. package/frigg-cli/templates/backend/test/setup.js +30 -0
  80. package/frigg-cli/templates/backend/ui-extensions/.gitkeep +0 -0
  81. package/frigg-cli/templates/backend/ui-extensions/README.md +77 -0
  82. package/frigg-cli/ui-command/index.js +58 -36
  83. package/frigg-cli/utils/__tests__/repo-detection.test.js +436 -0
  84. package/frigg-cli/utils/output.js +382 -0
  85. package/frigg-cli/utils/repo-detection.js +85 -37
  86. package/frigg-cli/validate-command/__tests__/adapters/validate-command.test.js +205 -0
  87. package/frigg-cli/validate-command/__tests__/application/validate-app-use-case.test.js +104 -0
  88. package/frigg-cli/validate-command/__tests__/domain/fix-suggestion.test.js +153 -0
  89. package/frigg-cli/validate-command/__tests__/domain/validation-error.test.js +162 -0
  90. package/frigg-cli/validate-command/__tests__/domain/validation-result.test.js +152 -0
  91. package/frigg-cli/validate-command/__tests__/infrastructure/api-module-validator.test.js +332 -0
  92. package/frigg-cli/validate-command/__tests__/infrastructure/app-definition-validator.test.js +191 -0
  93. package/frigg-cli/validate-command/__tests__/infrastructure/integration-class-validator.test.js +146 -0
  94. package/frigg-cli/validate-command/__tests__/infrastructure/template-validation.test.js +155 -0
  95. package/frigg-cli/validate-command/adapters/cli/validate-command.js +199 -0
  96. package/frigg-cli/validate-command/application/use-cases/validate-app-use-case.js +35 -0
  97. package/frigg-cli/validate-command/domain/entities/validation-result.js +74 -0
  98. package/frigg-cli/validate-command/domain/value-objects/fix-suggestion.js +74 -0
  99. package/frigg-cli/validate-command/domain/value-objects/validation-error.js +68 -0
  100. package/frigg-cli/validate-command/infrastructure/validators/api-module-validator.js +181 -0
  101. package/frigg-cli/validate-command/infrastructure/validators/app-definition-validator.js +128 -0
  102. package/frigg-cli/validate-command/infrastructure/validators/integration-class-validator.js +113 -0
  103. package/infrastructure/docs/iam-policy-templates.md +1 -1
  104. package/infrastructure/domains/networking/vpc-builder.test.js +2 -4
  105. package/infrastructure/domains/networking/vpc-resolver.test.js +1 -1
  106. package/infrastructure/domains/shared/cloudformation-discovery.test.js +4 -7
  107. package/infrastructure/domains/shared/resource-discovery.js +5 -5
  108. package/infrastructure/domains/shared/types/discovery-result.test.js +1 -1
  109. package/infrastructure/domains/shared/utilities/base-definition-factory.js +10 -1
  110. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +2 -2
  111. package/infrastructure/infrastructure-composer.test.js +2 -2
  112. package/infrastructure/jest.config.js +16 -0
  113. package/management-ui/README.md +245 -109
  114. package/package.json +8 -7
  115. package/frigg-cli/install-command/logger.js +0 -12
@@ -0,0 +1,376 @@
1
+ /**
2
+ * RunPreflightChecksUseCase - Orchestrates pre-flight checks before starting Frigg
3
+ *
4
+ * Application Layer - Use Case that coordinates multiple infrastructure adapters
5
+ * to verify the environment is ready for Frigg to start.
6
+ *
7
+ * Checks performed:
8
+ * 1. DATABASE_URL environment variable exists
9
+ * 2. Docker is installed
10
+ * 3. Docker daemon is running
11
+ * 4. Database is reachable
12
+ */
13
+
14
+ class RunPreflightChecksUseCase {
15
+ constructor({ dockerAdapter, databaseAdapter }) {
16
+ this.dockerAdapter = dockerAdapter;
17
+ this.databaseAdapter = databaseAdapter;
18
+ }
19
+
20
+ /**
21
+ * Execute all pre-flight checks
22
+ * @param {object} options - Options
23
+ * @param {string} options.projectPath - Path to the Frigg project
24
+ * @returns {Promise<{allPassed: boolean, checks: Array}>}
25
+ */
26
+ async execute({ projectPath }) {
27
+ const checks = [];
28
+
29
+ // Check 1: DATABASE_URL exists
30
+ const dbUrlCheck = this._checkDatabaseUrl();
31
+ checks.push(dbUrlCheck);
32
+
33
+ // Short-circuit if DATABASE_URL is missing
34
+ if (dbUrlCheck.status === 'failed') {
35
+ return { allPassed: false, checks };
36
+ }
37
+
38
+ // Check 2: Docker is installed
39
+ const dockerInstalledCheck = await this._checkDockerInstalled();
40
+ checks.push(dockerInstalledCheck);
41
+
42
+ // Short-circuit if Docker is not installed
43
+ if (dockerInstalledCheck.status === 'failed') {
44
+ return { allPassed: false, checks };
45
+ }
46
+
47
+ // Check 3: Docker is running
48
+ const dockerRunningCheck = await this._checkDockerRunning();
49
+ checks.push(dockerRunningCheck);
50
+
51
+ // Short-circuit if Docker is not running
52
+ if (dockerRunningCheck.status === 'failed') {
53
+ return { allPassed: false, checks };
54
+ }
55
+
56
+ // Check 4: Database is reachable
57
+ const dbReachableCheck = await this._checkDatabaseReachable(projectPath);
58
+ checks.push(dbReachableCheck);
59
+
60
+ // Short-circuit if database is not reachable
61
+ if (dbReachableCheck.status === 'failed') {
62
+ return { allPassed: false, checks };
63
+ }
64
+
65
+ // Check 5: PostgreSQL migrations (only for PostgreSQL databases)
66
+ const dbType = this.databaseAdapter.getDatabaseType(process.env.DATABASE_URL);
67
+ if (dbType === 'postgresql') {
68
+ const migrationCheck = await this._checkPostgresMigrations(projectPath);
69
+ checks.push(migrationCheck);
70
+ }
71
+
72
+ // Check 6: LocalStack is reachable (only if AWS_ENDPOINT is configured for local)
73
+ const localstackCheck = await this._checkLocalStackReachable(projectPath);
74
+ if (localstackCheck) {
75
+ checks.push(localstackCheck);
76
+ }
77
+
78
+ const allPassed = checks.every(check => check.status === 'passed');
79
+ return { allPassed, checks };
80
+ }
81
+
82
+ /**
83
+ * Check if DATABASE_URL environment variable is set
84
+ * @returns {object} Check result
85
+ */
86
+ _checkDatabaseUrl() {
87
+ const databaseUrl = process.env.DATABASE_URL;
88
+
89
+ if (!databaseUrl || databaseUrl.trim() === '') {
90
+ return {
91
+ name: 'database_url',
92
+ status: 'failed',
93
+ message: 'DATABASE_URL environment variable is not set',
94
+ canResolve: true,
95
+ resolution: {
96
+ type: 'create_env',
97
+ prompt: 'Would you like to create a .env file from the template?',
98
+ action: 'copy_env_template'
99
+ }
100
+ };
101
+ }
102
+
103
+ const dbType = this.databaseAdapter.getDatabaseType(databaseUrl);
104
+
105
+ return {
106
+ name: 'database_url',
107
+ status: 'passed',
108
+ message: `DATABASE_URL is configured (${dbType || 'unknown'} database)`,
109
+ canResolve: false,
110
+ resolution: null
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Check if Docker is installed
116
+ * @returns {Promise<object>} Check result
117
+ */
118
+ async _checkDockerInstalled() {
119
+ const isInstalled = await this.dockerAdapter.isDockerInstalled();
120
+
121
+ if (!isInstalled) {
122
+ return {
123
+ name: 'docker_installed',
124
+ status: 'failed',
125
+ message: 'Docker is not installed',
126
+ canResolve: false,
127
+ resolution: {
128
+ type: 'manual',
129
+ instructions: 'Please install Docker Desktop from https://www.docker.com/products/docker-desktop'
130
+ }
131
+ };
132
+ }
133
+
134
+ return {
135
+ name: 'docker_installed',
136
+ status: 'passed',
137
+ message: 'Docker is installed',
138
+ canResolve: false,
139
+ resolution: null
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Check if Docker daemon is running
145
+ * @returns {Promise<object>} Check result
146
+ */
147
+ async _checkDockerRunning() {
148
+ const isRunning = await this.dockerAdapter.isDockerRunning();
149
+
150
+ if (!isRunning) {
151
+ return {
152
+ name: 'docker_running',
153
+ status: 'failed',
154
+ message: 'Docker is not running. Please start Docker Desktop.',
155
+ canResolve: true,
156
+ resolution: {
157
+ type: 'start_docker',
158
+ prompt: 'Would you like to start Docker Desktop?',
159
+ action: 'start_docker_desktop'
160
+ }
161
+ };
162
+ }
163
+
164
+ return {
165
+ name: 'docker_running',
166
+ status: 'passed',
167
+ message: 'Docker daemon is running',
168
+ canResolve: false,
169
+ resolution: null
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Check if database is reachable
175
+ * @param {string} projectPath - Path to project
176
+ * @returns {Promise<object>} Check result
177
+ */
178
+ async _checkDatabaseReachable(projectPath) {
179
+ const databaseUrl = process.env.DATABASE_URL;
180
+ const result = await this.databaseAdapter.isDatabaseReachable(databaseUrl);
181
+
182
+ if (!result.reachable) {
183
+ // Check for docker-compose file
184
+ const composePath = await this.dockerAdapter.findDockerComposeFile(projectPath);
185
+
186
+ if (composePath) {
187
+ return {
188
+ name: 'database_reachable',
189
+ status: 'failed',
190
+ message: `Database is not reachable: ${result.error || 'Connection failed'}`,
191
+ canResolve: true,
192
+ resolution: {
193
+ type: 'start_docker_compose',
194
+ prompt: 'Would you like to start the database using docker-compose?',
195
+ composePath,
196
+ action: 'docker_compose_up'
197
+ }
198
+ };
199
+ }
200
+
201
+ return {
202
+ name: 'database_reachable',
203
+ status: 'failed',
204
+ message: `Database is not reachable: ${result.error || 'Connection failed'}`,
205
+ canResolve: false,
206
+ resolution: {
207
+ type: 'manual',
208
+ instructions: 'Please ensure your database server is running and accessible at the configured DATABASE_URL.'
209
+ }
210
+ };
211
+ }
212
+
213
+ return {
214
+ name: 'database_reachable',
215
+ status: 'passed',
216
+ message: `Database is reachable at ${result.host}:${result.port}`,
217
+ canResolve: false,
218
+ resolution: null
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Check if LocalStack is reachable (for local development)
224
+ *
225
+ * The serverless plugin defaults to http://localhost:4566 for LocalStack
226
+ * even if AWS_ENDPOINT isn't set. So we check LocalStack in local dev
227
+ * mode by default.
228
+ *
229
+ * @param {string} projectPath - Path to project
230
+ * @returns {Promise<object|null>} Check result, or null if LocalStack check is not applicable
231
+ */
232
+ async _checkLocalStackReachable(projectPath) {
233
+ // Default LocalStack endpoint - same as serverless plugin default
234
+ const defaultEndpoint = 'http://localhost:4566';
235
+ const awsEndpoint = process.env.AWS_ENDPOINT;
236
+
237
+ // If AWS_ENDPOINT is set to a non-local address, skip LocalStack check
238
+ if (awsEndpoint) {
239
+ const isLocalEndpoint = awsEndpoint.includes('localhost') ||
240
+ awsEndpoint.includes('127.0.0.1') ||
241
+ awsEndpoint.includes('localstack');
242
+
243
+ if (!isLocalEndpoint) {
244
+ // Using real AWS, not LocalStack
245
+ return null;
246
+ }
247
+ }
248
+
249
+ // Use configured endpoint or default LocalStack endpoint
250
+ const endpoint = awsEndpoint || defaultEndpoint;
251
+
252
+ // Try to reach LocalStack health endpoint
253
+ const result = await this.dockerAdapter.waitForLocalStack({
254
+ maxAttempts: 1, // Just one check, not waiting
255
+ intervalMs: 0,
256
+ endpoint: endpoint
257
+ });
258
+
259
+ if (!result.ready) {
260
+ // Check for docker-compose file
261
+ const composePath = await this.dockerAdapter.findDockerComposeFile(projectPath);
262
+
263
+ if (composePath) {
264
+ return {
265
+ name: 'localstack_reachable',
266
+ status: 'failed',
267
+ message: `LocalStack is not reachable at ${endpoint}`,
268
+ canResolve: true,
269
+ resolution: {
270
+ type: 'start_docker_compose',
271
+ prompt: 'Would you like to start LocalStack using docker-compose?',
272
+ composePath,
273
+ action: 'docker_compose_up'
274
+ }
275
+ };
276
+ }
277
+
278
+ return {
279
+ name: 'localstack_reachable',
280
+ status: 'failed',
281
+ message: `LocalStack is not reachable at ${endpoint}`,
282
+ canResolve: false,
283
+ resolution: {
284
+ type: 'manual',
285
+ instructions: `Please ensure LocalStack is running at ${endpoint}. You can start it with: docker-compose up -d`
286
+ }
287
+ };
288
+ }
289
+
290
+ return {
291
+ name: 'localstack_reachable',
292
+ status: 'passed',
293
+ message: `LocalStack is reachable at ${endpoint}`,
294
+ canResolve: false,
295
+ resolution: null
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Check if PostgreSQL migrations have been applied
301
+ * @param {string} projectPath - Path to project
302
+ * @returns {Promise<object>} Check result
303
+ */
304
+ async _checkPostgresMigrations(projectPath) {
305
+ const result = await this.databaseAdapter.checkMigrationStatus(projectPath);
306
+
307
+ if (result.migrated) {
308
+ return {
309
+ name: 'postgres_migrations',
310
+ status: 'passed',
311
+ message: 'PostgreSQL database schema is up to date',
312
+ canResolve: false,
313
+ resolution: null
314
+ };
315
+ }
316
+
317
+ // Handle Prisma not installed case
318
+ if (result.needsInstall) {
319
+ return {
320
+ name: 'postgres_migrations',
321
+ status: 'failed',
322
+ message: result.error,
323
+ canResolve: false,
324
+ resolution: {
325
+ type: 'manual',
326
+ instructions: 'Run "npm install" or "pnpm install" to properly install Prisma, then try again.'
327
+ }
328
+ };
329
+ }
330
+
331
+ // Determine if this is auto-resolvable
332
+ const canResolve = result.needsSetup || result.pendingMigrations?.length > 0;
333
+
334
+ let message = result.error || 'PostgreSQL migrations have not been applied';
335
+ if (result.pendingMigrations?.length > 0) {
336
+ message = `${result.pendingMigrations.length} pending migration(s) need to be applied`;
337
+ }
338
+
339
+ return {
340
+ name: 'postgres_migrations',
341
+ status: 'failed',
342
+ message,
343
+ canResolve,
344
+ resolution: canResolve ? {
345
+ type: 'run_migrations',
346
+ prompt: 'Would you like to run database migrations now?',
347
+ action: 'prisma_migrate',
348
+ pendingMigrations: result.pendingMigrations,
349
+ needsSetup: result.needsSetup
350
+ } : {
351
+ type: 'manual',
352
+ instructions: 'Run "frigg db:setup" or "npx prisma migrate dev" to apply database migrations.'
353
+ }
354
+ };
355
+ }
356
+
357
+ /**
358
+ * Get only the failed checks from a result
359
+ * @param {object} result - Result from execute()
360
+ * @returns {Array} Failed checks
361
+ */
362
+ getFailedChecks(result) {
363
+ return result.checks.filter(check => check.status === 'failed');
364
+ }
365
+
366
+ /**
367
+ * Get only the checks that can be auto-resolved
368
+ * @param {object} result - Result from execute()
369
+ * @returns {Array} Resolvable failed checks
370
+ */
371
+ getResolvableChecks(result) {
372
+ return result.checks.filter(check => check.status === 'failed' && check.canResolve);
373
+ }
374
+ }
375
+
376
+ module.exports = { RunPreflightChecksUseCase };
@@ -1,5 +1,6 @@
1
1
  const { spawn } = require('node:child_process');
2
2
  const path = require('node:path');
3
+ const fs = require('node:fs');
3
4
  const dotenv = require('dotenv');
4
5
  const chalk = require('chalk');
5
6
  const {
@@ -13,6 +14,12 @@ const {
13
14
  getPrismaClientNotGeneratedError
14
15
  } = require('../utils/error-messages');
15
16
 
17
+ // Import new pre-flight infrastructure
18
+ const { DockerAdapter } = require('./infrastructure/DockerAdapter');
19
+ const { DatabaseAdapter } = require('./infrastructure/DatabaseAdapter');
20
+ const { RunPreflightChecksUseCase } = require('./application/RunPreflightChecksUseCase');
21
+ const { InteractivePromptAdapter } = require('./presentation/InteractivePromptAdapter');
22
+
16
23
  async function startCommand(options) {
17
24
  if (options.verbose) {
18
25
  console.log('Verbose mode enabled');
@@ -25,7 +32,26 @@ async function startCommand(options) {
25
32
  const envPath = path.join(process.cwd(), '.env');
26
33
  dotenv.config({ path: envPath });
27
34
 
28
- // Pre-flight database checks
35
+ const projectPath = process.cwd();
36
+
37
+ // Run interactive pre-flight checks if enabled (default: true)
38
+ if (options.interactive !== false) {
39
+ try {
40
+ const preflightPassed = await runInteractivePreflightChecks(projectPath, options);
41
+ if (!preflightPassed) {
42
+ console.error(chalk.red('\n❌ Pre-flight checks failed'));
43
+ console.error(chalk.gray('Fix the issues above before starting the application.\n'));
44
+ process.exit(1);
45
+ }
46
+ } catch (error) {
47
+ if (options.verbose) {
48
+ console.error(chalk.yellow(`Pre-flight check error: ${error.message}`));
49
+ }
50
+ // Fall back to legacy checks if new system fails
51
+ }
52
+ }
53
+
54
+ // Legacy pre-flight database checks (still run for Prisma validation)
29
55
  try {
30
56
  await performDatabaseChecks(options.verbose);
31
57
  } catch (error) {
@@ -34,7 +60,7 @@ async function startCommand(options) {
34
60
  process.exit(1);
35
61
  }
36
62
 
37
- console.log(chalk.green('✓ Database checks passed\n'));
63
+ console.log(chalk.green('✓ Pre-flight checks passed\n'));
38
64
  console.log('Starting backend and optional frontend...');
39
65
 
40
66
  // Suppress AWS SDK warning message about maintenance mode
@@ -83,6 +109,224 @@ async function startCommand(options) {
83
109
  });
84
110
  }
85
111
 
112
+ /**
113
+ * Run interactive pre-flight checks with resolution prompts
114
+ * @param {string} projectPath - Path to the Frigg project
115
+ * @param {object} options - Command options
116
+ * @returns {Promise<boolean>} True if all checks pass or were resolved
117
+ */
118
+ async function runInteractivePreflightChecks(projectPath, options) {
119
+ // Create adapters and use case
120
+ const dockerAdapter = new DockerAdapter();
121
+ const databaseAdapter = new DatabaseAdapter();
122
+ const preflightUseCase = new RunPreflightChecksUseCase({
123
+ dockerAdapter,
124
+ databaseAdapter
125
+ });
126
+
127
+ // Create prompt adapter based on mode (terminal or IPC)
128
+ const promptMode = options.ipc ? 'ipc' : 'terminal';
129
+ const promptAdapter = InteractivePromptAdapter.create({ mode: promptMode });
130
+
131
+ if (options.verbose) {
132
+ console.log(chalk.gray('Running pre-flight checks...'));
133
+ }
134
+
135
+ // Track which checks we've already processed to avoid infinite loops
136
+ const processedChecks = new Set();
137
+
138
+ // Keep running checks and resolving issues until all pass or no more can be resolved
139
+ let maxIterations = 10; // Safety limit to prevent infinite loops
140
+ while (maxIterations > 0) {
141
+ maxIterations--;
142
+
143
+ // Run checks
144
+ const result = await preflightUseCase.execute({ projectPath });
145
+
146
+ // If all passed, we're done
147
+ if (result.allPassed) {
148
+ return true;
149
+ }
150
+
151
+ // Get resolvable checks that we haven't already processed
152
+ const resolvableChecks = preflightUseCase.getResolvableChecks(result)
153
+ .filter(check => !processedChecks.has(check.name));
154
+
155
+ // If no new resolvable checks, show failures and exit
156
+ if (resolvableChecks.length === 0) {
157
+ const failedChecks = result.checks.filter(c => c.status === 'failed');
158
+ for (const check of failedChecks) {
159
+ console.log(chalk.red(` ✗ ${check.name}: ${check.message}`));
160
+ if (check.resolution?.instructions) {
161
+ console.log(chalk.gray(` ${check.resolution.instructions}`));
162
+ }
163
+ }
164
+ return false;
165
+ }
166
+
167
+ // Process the first resolvable check
168
+ const check = resolvableChecks[0];
169
+ processedChecks.add(check.name);
170
+
171
+ // Display the failure
172
+ console.log(chalk.yellow(`\n⚠️ ${check.message}`));
173
+
174
+ // Only prompt if check is resolvable
175
+ if (!check.canResolve) {
176
+ if (check.resolution?.instructions) {
177
+ console.log(chalk.gray(` ${check.resolution.instructions}`));
178
+ }
179
+ continue;
180
+ }
181
+
182
+ // Ask user if they want to resolve
183
+ const response = await promptAdapter.promptForResolution(check);
184
+
185
+ if (!response.shouldResolve) {
186
+ console.log(chalk.gray(' Skipping resolution'));
187
+ // User declined, show remaining failures and exit
188
+ const failedChecks = result.checks.filter(c => c.status === 'failed');
189
+ for (const failedCheck of failedChecks) {
190
+ if (failedCheck.name !== check.name) {
191
+ console.log(chalk.red(` ✗ ${failedCheck.name}: ${failedCheck.message}`));
192
+ }
193
+ }
194
+ return false;
195
+ }
196
+
197
+ // Execute the resolution
198
+ const resolved = await executeResolution(check, dockerAdapter, options);
199
+
200
+ if (!resolved) {
201
+ return false;
202
+ }
203
+
204
+ // Loop will continue and re-run checks
205
+ }
206
+
207
+ // If we exhausted iterations, something went wrong
208
+ console.log(chalk.yellow(' ⚠️ Pre-flight check loop limit reached'));
209
+ return false;
210
+ }
211
+
212
+ /**
213
+ * Execute a resolution action
214
+ * @param {object} check - The check that failed
215
+ * @param {DockerAdapter} dockerAdapter - Docker adapter
216
+ * @param {object} options - Command options
217
+ * @returns {Promise<boolean>} True if resolution succeeded
218
+ */
219
+ async function executeResolution(check, dockerAdapter, options) {
220
+ const { resolution } = check;
221
+
222
+ switch (resolution.type) {
223
+ case 'start_docker':
224
+ console.log(chalk.blue(' Starting Docker Desktop...'));
225
+ const dockerResult = await dockerAdapter.startDockerDesktop();
226
+ if (!dockerResult.success) {
227
+ console.error(chalk.red(` Failed to start Docker: ${dockerResult.error}`));
228
+ return false;
229
+ }
230
+ // Wait for Docker to be ready
231
+ console.log(chalk.gray(' Waiting for Docker to start...'));
232
+ const isReady = await dockerAdapter.waitForDockerReady({
233
+ maxAttempts: 60,
234
+ intervalMs: 1000
235
+ });
236
+ if (!isReady) {
237
+ console.error(chalk.red(' Docker did not start in time'));
238
+ return false;
239
+ }
240
+ console.log(chalk.green(' ✓ Docker is now running'));
241
+ return true;
242
+
243
+ case 'start_docker_compose':
244
+ console.log(chalk.blue(' Starting Docker Compose services...'));
245
+ const composeResult = await dockerAdapter.startDockerCompose(resolution.composePath);
246
+ if (!composeResult.success) {
247
+ console.error(chalk.red(` Failed to start services: ${composeResult.error}`));
248
+ return false;
249
+ }
250
+ console.log(chalk.green(' ✓ Docker Compose services started'));
251
+ // Poll LocalStack health endpoint until services are ready
252
+ console.log(chalk.gray(' Waiting for LocalStack to initialize...'));
253
+ const localstackResult = await dockerAdapter.waitForLocalStack({
254
+ maxAttempts: 30,
255
+ intervalMs: 2000
256
+ });
257
+ if (localstackResult.ready) {
258
+ console.log(chalk.green(' ✓ LocalStack services ready'));
259
+ if (options.verbose && localstackResult.services) {
260
+ console.log(chalk.gray(` Services: ${Object.keys(localstackResult.services).join(', ')}`));
261
+ }
262
+ } else {
263
+ console.log(chalk.yellow(' ⚠️ LocalStack may not be fully ready'));
264
+ if (localstackResult.error) {
265
+ console.log(chalk.gray(` ${localstackResult.error}`));
266
+ }
267
+ // Continue anyway - the service might still work
268
+ }
269
+ return true;
270
+
271
+ case 'create_env':
272
+ // Try to copy .env.example to .env
273
+ const projectPath = process.cwd();
274
+ const envExamplePath = path.join(projectPath, '.env.example');
275
+ const envPath = path.join(projectPath, '.env');
276
+
277
+ if (fs.existsSync(envExamplePath)) {
278
+ try {
279
+ fs.copyFileSync(envExamplePath, envPath);
280
+ console.log(chalk.green(' ✓ Created .env file from .env.example'));
281
+ console.log(chalk.yellow(' ⚠️ Please edit .env to set your DATABASE_URL'));
282
+
283
+ // Reload environment variables from the new .env file
284
+ dotenv.config({ path: envPath, override: true });
285
+
286
+ // Check if DATABASE_URL is now set
287
+ if (process.env.DATABASE_URL && process.env.DATABASE_URL.trim() !== '') {
288
+ console.log(chalk.green(' ✓ DATABASE_URL is now configured'));
289
+ return true;
290
+ } else {
291
+ console.log(chalk.yellow(' ⚠️ DATABASE_URL is still not set in .env'));
292
+ console.log(chalk.gray(' Please edit .env and set DATABASE_URL, then try again'));
293
+ return false;
294
+ }
295
+ } catch (err) {
296
+ console.error(chalk.red(` Failed to create .env: ${err.message}`));
297
+ return false;
298
+ }
299
+ } else {
300
+ console.log(chalk.yellow(' No .env.example file found'));
301
+ console.log(chalk.gray(' Please create a .env file manually with DATABASE_URL'));
302
+ return false;
303
+ }
304
+
305
+ case 'run_migrations':
306
+ console.log(chalk.blue(' Running PostgreSQL migrations...'));
307
+ const databaseAdapter = new DatabaseAdapter();
308
+ const migrationProjectPath = process.cwd();
309
+
310
+ // Use deploy mode (default) - non-interactive, applies existing migrations
311
+ const migrateResult = await databaseAdapter.runMigrations(migrationProjectPath);
312
+
313
+ if (!migrateResult.success) {
314
+ console.error(chalk.red(` Failed to run migrations: ${migrateResult.error}`));
315
+ console.log(chalk.gray(' Try running "frigg db:setup" manually'));
316
+ return false;
317
+ }
318
+
319
+ console.log(chalk.green(' ✓ Database migrations applied successfully'));
320
+ return true;
321
+
322
+ default:
323
+ if (options.verbose) {
324
+ console.log(chalk.gray(` Unknown resolution type: ${resolution.type}`));
325
+ }
326
+ return false;
327
+ }
328
+ }
329
+
86
330
  /**
87
331
  * Performs pre-flight database validation checks
88
332
  * @param {boolean} verbose - Enable verbose output