@friggframework/devtools 2.0.0--canary.474.edb48ba.0 → 2.0.0--canary.482.411f36e.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.
@@ -237,10 +237,21 @@ class MigrationBuilder extends InfrastructureBuilder {
237
237
  console.log(' 🔍 DEBUG: result.functions is:', typeof result.functions, result.functions);
238
238
  // Migration WORKER package config (needs Prisma CLI WASM files)
239
239
  const migrationWorkerPackageConfig = {
240
+ individually: true,
240
241
  exclude: [
242
+ // Exclude Prisma runtime client - it's in the Lambda Layer
243
+ 'node_modules/@prisma/client/**',
244
+ 'node_modules/.prisma/**',
245
+ 'node_modules/@friggframework/core/generated/**',
246
+ // But KEEP node_modules/prisma/** (the CLI with WASM)
247
+
248
+ // Exclude ALL nested node_modules
249
+ 'node_modules/**/node_modules/**',
250
+
241
251
  // Exclude AWS SDK (provided by Lambda runtime)
242
252
  'node_modules/aws-sdk/**',
243
253
  'node_modules/@aws-sdk/**',
254
+
244
255
  // Exclude build tools
245
256
  'node_modules/esbuild/**',
246
257
  'node_modules/@esbuild/**',
@@ -253,6 +264,8 @@ class MigrationBuilder extends InfrastructureBuilder {
253
264
  'node_modules/serverless-offline-sqs/**',
254
265
  'node_modules/serverless-dotenv-plugin/**',
255
266
  'node_modules/serverless-kms-grants/**',
267
+
268
+ // Exclude dev dependencies
256
269
  'node_modules/@friggframework/test/**',
257
270
  'node_modules/@friggframework/eslint-config/**',
258
271
  'node_modules/@friggframework/prettier-config/**',
@@ -261,23 +274,39 @@ class MigrationBuilder extends InfrastructureBuilder {
261
274
  'node_modules/jest/**',
262
275
  'node_modules/prettier/**',
263
276
  'node_modules/eslint/**',
277
+
278
+ // Exclude non-essential Frigg core modules
264
279
  'node_modules/@friggframework/core/generated/prisma-mongodb/**',
265
280
  'node_modules/@friggframework/core/integrations/**',
266
281
  'node_modules/@friggframework/core/user/**',
282
+
283
+ // Exclude other handlers we don't need (keep db-migration worker)
284
+ 'node_modules/@friggframework/core/handlers/routers/auth.js',
285
+ 'node_modules/@friggframework/core/handlers/routers/health.js',
286
+ 'node_modules/@friggframework/core/handlers/routers/user.js',
287
+ 'node_modules/@friggframework/core/handlers/routers/websocket.js',
288
+ 'node_modules/@friggframework/core/handlers/routers/integration-*.js',
289
+ 'node_modules/@friggframework/core/handlers/workers/integration-*.js',
290
+
291
+ // Exclude wrong OS binaries
267
292
  '**/query-engine-darwin*',
268
293
  '**/schema-engine-darwin*',
269
294
  '**/libquery_engine-darwin*',
270
295
  '**/*-darwin-arm64*',
271
296
  '**/*-darwin*',
297
+
272
298
  // Migration worker DOES need Prisma CLI WASM files (for migrate deploy)
273
299
  // Only exclude runtime engine WASM (query engine internals)
274
300
  '**/runtime/*.wasm',
301
+
275
302
  // Additional size optimizations
276
303
  '**/*.map',
277
304
  '**/*.md',
305
+ '**/LICENSE*',
306
+ '**/*.d.ts',
307
+ '**/*.d.mts',
278
308
  '**/examples/**',
279
309
  '**/docs/**',
280
- '**/*.d.ts',
281
310
  'src/**',
282
311
  'test/**',
283
312
  'layers/**',
@@ -297,21 +326,37 @@ class MigrationBuilder extends InfrastructureBuilder {
297
326
 
298
327
  // Migration ROUTER package config (lighter, no Prisma CLI needed)
299
328
  const migrationRouterPackageConfig = {
329
+ individually: true,
300
330
  exclude: [
331
+ // Exclude Prisma runtime client - it's in the Lambda Layer
332
+ 'node_modules/@prisma/client/**',
333
+ 'node_modules/.prisma/**',
334
+ 'node_modules/@friggframework/core/generated/**',
335
+
336
+ // Router doesn't need Prisma CLI at all
337
+ 'node_modules/prisma/**',
338
+
339
+ // Exclude ALL nested node_modules
340
+ 'node_modules/**/node_modules/**',
341
+
301
342
  // Exclude AWS SDK (provided by Lambda runtime)
302
343
  'node_modules/aws-sdk/**',
303
344
  'node_modules/@aws-sdk/**',
345
+
304
346
  // Exclude build tools
305
347
  'node_modules/esbuild/**',
306
348
  'node_modules/@esbuild/**',
307
349
  'node_modules/typescript/**',
308
350
  'node_modules/webpack/**',
351
+ 'node_modules/osls/**',
309
352
  'node_modules/serverless-esbuild/**',
310
353
  'node_modules/serverless-jetpack/**',
311
354
  'node_modules/serverless-offline/**',
312
355
  'node_modules/serverless-offline-sqs/**',
313
356
  'node_modules/serverless-dotenv-plugin/**',
314
357
  'node_modules/serverless-kms-grants/**',
358
+
359
+ // Exclude dev dependencies
315
360
  'node_modules/@friggframework/test/**',
316
361
  'node_modules/@friggframework/eslint-config/**',
317
362
  'node_modules/@friggframework/prettier-config/**',
@@ -320,25 +365,42 @@ class MigrationBuilder extends InfrastructureBuilder {
320
365
  'node_modules/jest/**',
321
366
  'node_modules/prettier/**',
322
367
  'node_modules/eslint/**',
368
+
369
+ // Exclude non-essential Frigg core modules
323
370
  'node_modules/@friggframework/core/generated/prisma-mongodb/**',
371
+ 'node_modules/@friggframework/core/integrations/**',
324
372
  'node_modules/@friggframework/core/user/**',
373
+
374
+ // Exclude other handlers we don't need (keep db-migration router)
375
+ 'node_modules/@friggframework/core/handlers/routers/auth.js',
376
+ 'node_modules/@friggframework/core/handlers/routers/health.js',
377
+ 'node_modules/@friggframework/core/handlers/routers/user.js',
378
+ 'node_modules/@friggframework/core/handlers/routers/websocket.js',
379
+ 'node_modules/@friggframework/core/handlers/routers/integration-*.js',
380
+ 'node_modules/@friggframework/core/handlers/workers/**',
381
+
382
+ // Exclude wrong OS binaries
325
383
  '**/query-engine-darwin*',
326
384
  '**/schema-engine-darwin*',
327
385
  '**/libquery_engine-darwin*',
328
386
  '**/*-darwin-arm64*',
329
387
  '**/*-darwin*',
388
+
330
389
  // Router doesn't run migrations - exclude ALL WASM files
331
390
  '**/runtime/*.wasm',
332
391
  '**/*.wasm*',
392
+
333
393
  // Additional size optimizations
334
394
  '**/*.map',
335
395
  '**/*.md',
396
+ '**/LICENSE*',
397
+ '**/*.d.ts',
398
+ '**/*.d.mts',
336
399
  '**/test/**',
337
400
  '**/tests/**',
338
401
  '**/__tests__/**',
339
402
  '**/examples/**',
340
403
  '**/docs/**',
341
- '**/*.d.ts',
342
404
  'src/**',
343
405
  'test/**',
344
406
  'layers/**',
@@ -0,0 +1,306 @@
1
+ /**
2
+ * AWS Discovery Configuration Tests
3
+ *
4
+ * Tests for AppDefinition-level discovery control options
5
+ * addressing GitHub Issue #481 - Issue 5
6
+ */
7
+
8
+ const { shouldRunDiscovery, gatherDiscoveredResources } = require('./resource-discovery');
9
+
10
+ // Mock dependencies
11
+ jest.mock('./providers/provider-factory');
12
+ jest.mock('./cloudformation-discovery');
13
+ jest.mock('../networking/vpc-discovery');
14
+ jest.mock('../security/kms-discovery');
15
+ jest.mock('../database/aurora-discovery');
16
+ jest.mock('../parameters/ssm-discovery');
17
+
18
+ describe('AWS Discovery Configuration (Issue #481 - Issue 5)', () => {
19
+ beforeEach(() => {
20
+ delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
21
+ jest.clearAllMocks();
22
+ });
23
+
24
+ describe('shouldRunDiscovery - Priority Order', () => {
25
+ it('should use AppDefinition.aws.discovery.enabled when explicitly set to true', () => {
26
+ const appDefinition = {
27
+ aws: { discovery: { enabled: true } },
28
+ vpc: { enable: false }, // Would normally skip
29
+ };
30
+
31
+ const result = shouldRunDiscovery(appDefinition);
32
+
33
+ expect(result).toBe(true);
34
+ });
35
+
36
+ it('should use AppDefinition.aws.discovery.enabled when explicitly set to false', () => {
37
+ const appDefinition = {
38
+ aws: { discovery: { enabled: false } },
39
+ vpc: { enable: true }, // Would normally run
40
+ };
41
+
42
+ const result = shouldRunDiscovery(appDefinition);
43
+
44
+ expect(result).toBe(false);
45
+ });
46
+
47
+ it('should fall back to env var when AppDefinition not set', () => {
48
+ process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
49
+
50
+ const appDefinition = {
51
+ vpc: { enable: true }, // Would normally run
52
+ };
53
+
54
+ const result = shouldRunDiscovery(appDefinition);
55
+
56
+ expect(result).toBe(false);
57
+ });
58
+
59
+ it('should auto-detect when neither AppDefinition nor env var is set', () => {
60
+ const appDefinition = {
61
+ vpc: { enable: true },
62
+ };
63
+
64
+ const result = shouldRunDiscovery(appDefinition);
65
+
66
+ expect(result).toBe(true);
67
+ });
68
+
69
+ it('should prioritize AppDefinition over env var', () => {
70
+ process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
71
+
72
+ const appDefinition = {
73
+ aws: { discovery: { enabled: true } }, // Explicit override
74
+ };
75
+
76
+ const result = shouldRunDiscovery(appDefinition);
77
+
78
+ expect(result).toBe(true); // AppDefinition wins
79
+ });
80
+ });
81
+
82
+ describe('AppDefinition.aws.discovery.enabled', () => {
83
+ it('should log when using AppDefinition configuration', () => {
84
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
85
+
86
+ const appDefinition = {
87
+ aws: { discovery: { enabled: true } },
88
+ };
89
+
90
+ shouldRunDiscovery(appDefinition);
91
+
92
+ expect(consoleSpy).toHaveBeenCalledWith(
93
+ expect.stringContaining('AppDefinition.aws.discovery.enabled: true')
94
+ );
95
+
96
+ consoleSpy.mockRestore();
97
+ });
98
+
99
+ it('should handle explicit false value', () => {
100
+ const appDefinition = {
101
+ aws: { discovery: { enabled: false } },
102
+ vpc: { enable: true },
103
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
104
+ };
105
+
106
+ const result = shouldRunDiscovery(appDefinition);
107
+
108
+ expect(result).toBe(false);
109
+ });
110
+
111
+ it('should handle undefined correctly (fall through to next priority)', () => {
112
+ const appDefinition = {
113
+ aws: { discovery: { enabled: undefined } },
114
+ vpc: { enable: true },
115
+ };
116
+
117
+ const result = shouldRunDiscovery(appDefinition);
118
+
119
+ // Should skip when undefined (fall through to env var check)
120
+ expect(result).toBe(true); // Auto-detect kicks in
121
+ });
122
+ });
123
+
124
+ describe('Auto-detection based on features', () => {
125
+ it('should run discovery when VPC is enabled', () => {
126
+ const appDefinition = {
127
+ vpc: { enable: true },
128
+ };
129
+
130
+ expect(shouldRunDiscovery(appDefinition)).toBe(true);
131
+ });
132
+
133
+ it('should run discovery when KMS encryption is enabled', () => {
134
+ const appDefinition = {
135
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
136
+ };
137
+
138
+ expect(shouldRunDiscovery(appDefinition)).toBe(true);
139
+ });
140
+
141
+ it('should run discovery when SSM is enabled', () => {
142
+ const appDefinition = {
143
+ ssm: { enable: true },
144
+ };
145
+
146
+ expect(shouldRunDiscovery(appDefinition)).toBe(true);
147
+ });
148
+
149
+ it('should run discovery when PostgreSQL is enabled', () => {
150
+ const appDefinition = {
151
+ database: { postgres: { enable: true } },
152
+ };
153
+
154
+ expect(shouldRunDiscovery(appDefinition)).toBe(true);
155
+ });
156
+
157
+ it('should not run discovery when no features are enabled', () => {
158
+ const appDefinition = {
159
+ vpc: { enable: false },
160
+ encryption: { fieldLevelEncryptionMethod: 'aes' },
161
+ };
162
+
163
+ expect(shouldRunDiscovery(appDefinition)).toBe(false);
164
+ });
165
+ });
166
+
167
+ describe('gatherDiscoveredResources - failOnError behavior', () => {
168
+ const { CloudProviderFactory } = require('./providers/provider-factory');
169
+ const { CloudFormationDiscovery } = require('./cloudformation-discovery');
170
+
171
+ beforeEach(() => {
172
+ const mockProvider = {
173
+ getVpcs: jest.fn(),
174
+ getKmsKeys: jest.fn(),
175
+ };
176
+
177
+ CloudProviderFactory.create = jest.fn().mockReturnValue(mockProvider);
178
+
179
+ // Mock CloudFormation discovery to throw error
180
+ CloudFormationDiscovery.mockImplementation(() => ({
181
+ discoverFromStack: jest.fn().mockRejectedValue(
182
+ new Error('User is not authorized to perform: ec2:DescribeVpcs')
183
+ ),
184
+ }));
185
+ });
186
+
187
+ it('should throw error when failOnError is true', async () => {
188
+ const appDefinition = {
189
+ name: 'test-app',
190
+ vpc: { enable: true },
191
+ aws: {
192
+ discovery: {
193
+ enabled: true,
194
+ failOnError: true,
195
+ },
196
+ },
197
+ };
198
+
199
+ await expect(gatherDiscoveredResources(appDefinition)).rejects.toThrow(
200
+ 'User is not authorized'
201
+ );
202
+ });
203
+
204
+ it('should return empty object when failOnError is false', async () => {
205
+ const appDefinition = {
206
+ name: 'test-app',
207
+ vpc: { enable: true },
208
+ aws: {
209
+ discovery: {
210
+ enabled: true,
211
+ failOnError: false,
212
+ },
213
+ },
214
+ };
215
+
216
+ const result = await gatherDiscoveredResources(appDefinition);
217
+
218
+ expect(result).toEqual({});
219
+ });
220
+
221
+ it('should default to false when failOnError is not set', async () => {
222
+ const appDefinition = {
223
+ name: 'test-app',
224
+ vpc: { enable: true },
225
+ };
226
+
227
+ const result = await gatherDiscoveredResources(appDefinition);
228
+
229
+ expect(result).toEqual({});
230
+ });
231
+
232
+ it('should log helpful message when failing gracefully', async () => {
233
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
234
+
235
+ const appDefinition = {
236
+ name: 'test-app',
237
+ vpc: { enable: true },
238
+ aws: { discovery: { failOnError: false } },
239
+ };
240
+
241
+ await gatherDiscoveredResources(appDefinition);
242
+
243
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
244
+ expect.stringContaining('Set aws.discovery.failOnError = true')
245
+ );
246
+
247
+ consoleWarnSpy.mockRestore();
248
+ });
249
+ });
250
+
251
+ describe('Real-world scenarios', () => {
252
+ it('should handle restrictive IAM with explicit disable', () => {
253
+ const appDefinition = {
254
+ vpc: { enable: true },
255
+ aws: {
256
+ discovery: {
257
+ enabled: false, // Explicit disable for restrictive IAM
258
+ },
259
+ },
260
+ };
261
+
262
+ const result = shouldRunDiscovery(appDefinition);
263
+
264
+ expect(result).toBe(false);
265
+ });
266
+
267
+ it('should allow strict mode for production deployments', async () => {
268
+ const { CloudProviderFactory } = require('./providers/provider-factory');
269
+ const { CloudFormationDiscovery } = require('./cloudformation-discovery');
270
+
271
+ CloudFormationDiscovery.mockImplementation(() => ({
272
+ discoverFromStack: jest.fn().mockRejectedValue(new Error('IAM error')),
273
+ }));
274
+
275
+ const appDefinition = {
276
+ name: 'prod-app',
277
+ vpc: { enable: true },
278
+ aws: {
279
+ discovery: {
280
+ enabled: true,
281
+ failOnError: true, // Strict mode for production
282
+ },
283
+ },
284
+ };
285
+
286
+ await expect(gatherDiscoveredResources(appDefinition)).rejects.toThrow();
287
+ });
288
+
289
+ it('should allow graceful degradation for dev environments', async () => {
290
+ const appDefinition = {
291
+ name: 'dev-app',
292
+ vpc: { enable: true },
293
+ aws: {
294
+ discovery: {
295
+ enabled: true,
296
+ failOnError: false, // Graceful for dev
297
+ },
298
+ },
299
+ };
300
+
301
+ const result = await gatherDiscoveredResources(appDefinition);
302
+
303
+ expect(result).toEqual({});
304
+ });
305
+ });
306
+ });
@@ -18,11 +18,26 @@ const { SsmDiscovery } = require('../parameters/ssm-discovery');
18
18
 
19
19
  /**
20
20
  * Determine if AWS discovery should run
21
- *
21
+ *
22
+ * Checks in priority order:
23
+ * 1. AppDefinition.aws.discovery.enabled (explicit opt-in/out)
24
+ * 2. FRIGG_SKIP_AWS_DISCOVERY environment variable
25
+ * 3. Auto-detection based on features enabled (VPC, KMS, SSM, PostgreSQL)
26
+ *
22
27
  * @param {Object} appDefinition - Application definition
23
28
  * @returns {boolean} True if discovery is needed
24
29
  */
25
30
  function shouldRunDiscovery(appDefinition) {
31
+ // Priority 1: Check AppDefinition-level configuration
32
+ if (appDefinition.aws?.discovery?.enabled !== undefined) {
33
+ const enabled = appDefinition.aws.discovery.enabled;
34
+ console.log(
35
+ `⚙️ Using AppDefinition.aws.discovery.enabled: ${enabled}`
36
+ );
37
+ return enabled;
38
+ }
39
+
40
+ // Priority 2: Check environment variable
26
41
  console.log(
27
42
  '⚙️ Checking FRIGG_SKIP_AWS_DISCOVERY:',
28
43
  process.env.FRIGG_SKIP_AWS_DISCOVERY
@@ -35,6 +50,7 @@ function shouldRunDiscovery(appDefinition) {
35
50
  return false;
36
51
  }
37
52
 
53
+ // Priority 3: Auto-detect based on enabled features
38
54
  return (
39
55
  appDefinition.vpc?.enable === true ||
40
56
  appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
@@ -177,10 +193,23 @@ async function gatherDiscoveredResources(appDefinition) {
177
193
  console.error('❌ Cloud resource discovery failed:', error.message);
178
194
  console.error('Stack:', error.stack);
179
195
 
180
- // Don't fail the build - return empty resources and let validation handle it
196
+ // Check if discovery failures should fail the deployment
197
+ const failOnError = appDefinition.aws?.discovery?.failOnError ?? false;
198
+
199
+ if (failOnError) {
200
+ console.error(
201
+ '❌ Discovery failure blocking deployment (aws.discovery.failOnError = true)'
202
+ );
203
+ throw error;
204
+ }
205
+
206
+ // Graceful degradation - return empty resources and let validation handle it
181
207
  console.warn(
182
208
  '⚠️ Continuing with empty discovered resources. This may cause deployment issues if resources are required.'
183
209
  );
210
+ console.warn(
211
+ '💡 Set aws.discovery.failOnError = true in AppDefinition to fail on discovery errors.'
212
+ );
184
213
  return {};
185
214
  }
186
215
  }
@@ -13,11 +13,16 @@ const { buildPrismaLayer } = require('../../../scripts/build-prisma-layer');
13
13
 
14
14
  /**
15
15
  * Ensure Prisma Lambda Layer exists
16
- *
16
+ *
17
17
  * Automatically builds the layer if it doesn't exist.
18
18
  * The layer contains ONLY the Prisma runtime client (minimal, ~10-15MB).
19
19
  * Prisma CLI is bundled separately in the dbMigrate function.
20
- *
20
+ *
21
+ * Domain Concept: Build Completion State
22
+ * - Uses .build-complete marker file to track successful builds
23
+ * - Prevents race conditions during concurrent infrastructure composition
24
+ * - Cleans incomplete builds before retry
25
+ *
21
26
  * @param {Object} databaseConfig - Database configuration from app definition
22
27
  * @returns {Promise<void>}
23
28
  * @throws {Error} If layer build fails
@@ -25,14 +30,43 @@ const { buildPrismaLayer } = require('../../../scripts/build-prisma-layer');
25
30
  async function ensurePrismaLayerExists(databaseConfig = {}) {
26
31
  const projectRoot = process.cwd();
27
32
  const layerPath = path.join(projectRoot, 'layers/prisma');
33
+ const completionMarkerPath = path.join(layerPath, '.build-complete');
28
34
 
29
- // Check if layer already exists
30
- if (fs.existsSync(layerPath)) {
35
+ // Check if build is complete (marker exists)
36
+ if (fs.existsSync(completionMarkerPath)) {
31
37
  console.log('✓ Prisma Lambda Layer already exists at', layerPath);
32
38
  return;
33
39
  }
34
40
 
35
- // Layer doesn't exist - build it automatically
41
+ // Check if incomplete build exists (directory without marker)
42
+ if (fs.existsSync(layerPath)) {
43
+ console.log('⚠ Incomplete Prisma layer detected - cleaning...');
44
+ try {
45
+ fs.rmSync(layerPath, { recursive: true, force: true });
46
+ console.log('✓ Cleaned incomplete build');
47
+ } catch (cleanupError) {
48
+ // EBUSY error means another process might be building
49
+ if (cleanupError.code === 'EBUSY' || cleanupError.message.includes('EBUSY')) {
50
+ console.warn('⏳ Could not clean (EBUSY) - waiting for concurrent build...', cleanupError.message);
51
+
52
+ // Wait 1 second and check if concurrent process completed
53
+ await new Promise(resolve => setTimeout(resolve, 1000));
54
+
55
+ // Check if concurrent build completed
56
+ if (fs.existsSync(completionMarkerPath)) {
57
+ console.log('✓ Concurrent build completed');
58
+ return;
59
+ }
60
+
61
+ // Concurrent build didn't complete, proceed with our build
62
+ console.log('⚠ Concurrent build incomplete, proceeding with rebuild');
63
+ } else {
64
+ throw cleanupError;
65
+ }
66
+ }
67
+ }
68
+
69
+ // Build layer
36
70
  console.log('📦 Prisma Lambda Layer not found - building automatically...');
37
71
  console.log(' Building MINIMAL layer (runtime only, NO CLI)');
38
72
  console.log(' CLI is packaged separately in dbMigrate function');
@@ -41,8 +75,24 @@ async function ensurePrismaLayerExists(databaseConfig = {}) {
41
75
  try {
42
76
  // Build layer WITHOUT CLI (runtime only for minimal size)
43
77
  await buildPrismaLayer(databaseConfig);
78
+
79
+ // Create completion marker
80
+ fs.writeFileSync(
81
+ completionMarkerPath,
82
+ `Build completed: ${new Date().toISOString()}\nNode: ${process.version}\nPlatform: ${process.platform}\n`
83
+ );
84
+
44
85
  console.log('✓ Prisma Lambda Layer built successfully (~10-15MB)\n');
45
86
  } catch (error) {
87
+ // Clean up partial build on failure
88
+ if (fs.existsSync(layerPath)) {
89
+ try {
90
+ fs.rmSync(layerPath, { recursive: true, force: true });
91
+ } catch (cleanupError) {
92
+ console.warn('⚠ Could not clean failed build:', cleanupError.message);
93
+ }
94
+ }
95
+
46
96
  console.error('✗ Failed to build Prisma Lambda Layer:', error.message);
47
97
  console.error(' You may need to run: npm install @friggframework/core\n');
48
98
  throw error;