@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,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
  }
@@ -224,7 +224,7 @@ function createBaseDefinition(
224
224
  ],
225
225
  packager: 'npm',
226
226
  keepNames: true,
227
- keepOutputDirectory: false, // Clean up .esbuild directory after packaging
227
+ keepOutputDirectory: true, // Keep .esbuild directory to prevent ENOENT errors during packaging
228
228
  exclude: [
229
229
  'aws-sdk',
230
230
  '@aws-sdk/*',
@@ -11,13 +11,34 @@ const path = require('path');
11
11
  const fs = require('fs');
12
12
  const { buildPrismaLayer } = require('../../../scripts/build-prisma-layer');
13
13
 
14
+ /**
15
+ * Check if a process is still running
16
+ * @param {number} pid - Process ID to check
17
+ * @returns {boolean} True if process is running
18
+ */
19
+ function isProcessRunning(pid) {
20
+ try {
21
+ // Signal 0 checks if process exists without killing it
22
+ process.kill(pid, 0);
23
+ return true;
24
+ } catch (error) {
25
+ return false;
26
+ }
27
+ }
28
+
14
29
  /**
15
30
  * Ensure Prisma Lambda Layer exists
16
- *
31
+ *
17
32
  * Automatically builds the layer if it doesn't exist.
18
33
  * The layer contains ONLY the Prisma runtime client (minimal, ~10-15MB).
19
34
  * Prisma CLI is bundled separately in the dbMigrate function.
20
- *
35
+ *
36
+ * Domain Concept: Build Completion State & Process Locking
37
+ * - Uses .build-complete marker file to track successful builds
38
+ * - Uses .build-lock PID file to prevent concurrent builds
39
+ * - Waits for active builds to complete before starting new one
40
+ * - Cleans stale locks and incomplete builds before retry
41
+ *
21
42
  * @param {Object} databaseConfig - Database configuration from app definition
22
43
  * @returns {Promise<void>}
23
44
  * @throws {Error} If layer build fails
@@ -25,27 +46,110 @@ const { buildPrismaLayer } = require('../../../scripts/build-prisma-layer');
25
46
  async function ensurePrismaLayerExists(databaseConfig = {}) {
26
47
  const projectRoot = process.cwd();
27
48
  const layerPath = path.join(projectRoot, 'layers/prisma');
49
+ const completionMarkerPath = path.join(layerPath, '.build-complete');
50
+ const lockFilePath = path.join(layerPath, '.build-lock');
28
51
 
29
- // Check if layer already exists
30
- if (fs.existsSync(layerPath)) {
52
+ // Check if build is complete (marker exists)
53
+ if (fs.existsSync(completionMarkerPath)) {
31
54
  console.log('✓ Prisma Lambda Layer already exists at', layerPath);
32
55
  return;
33
56
  }
34
57
 
35
- // Layer doesn't exist - build it automatically
58
+ // Check for active build process
59
+ if (fs.existsSync(lockFilePath)) {
60
+ const lockPid = parseInt(fs.readFileSync(lockFilePath, 'utf-8').trim(), 10);
61
+
62
+ if (isProcessRunning(lockPid)) {
63
+ console.log(`⏳ Another build process (PID ${lockPid}) is active - waiting...`);
64
+
65
+ // Wait up to 60 seconds for the other process to complete
66
+ for (let i = 0; i < 60; i++) {
67
+ await new Promise(resolve => setTimeout(resolve, 1000));
68
+
69
+ // Check if build completed
70
+ if (fs.existsSync(completionMarkerPath)) {
71
+ console.log('✓ Concurrent build completed successfully');
72
+ return;
73
+ }
74
+
75
+ // Check if process died
76
+ if (!isProcessRunning(lockPid)) {
77
+ console.log(`⚠ Build process ${lockPid} terminated - cleaning up stale lock`);
78
+ fs.rmSync(lockFilePath, { force: true });
79
+ break;
80
+ }
81
+ }
82
+
83
+ // Timeout - check one final time
84
+ if (fs.existsSync(completionMarkerPath)) {
85
+ console.log('✓ Concurrent build completed');
86
+ return;
87
+ }
88
+
89
+ // Still locked after 60s - remove stale lock
90
+ console.log('⚠ Build timeout - removing stale lock and rebuilding');
91
+ fs.rmSync(lockFilePath, { force: true });
92
+ } else {
93
+ // Stale lock file (process not running)
94
+ console.log(`⚠ Stale lock file detected (PID ${lockPid} not running) - cleaning up`);
95
+ fs.rmSync(lockFilePath, { force: true });
96
+ }
97
+ }
98
+
99
+ // Check if incomplete build exists (directory without marker)
100
+ if (fs.existsSync(layerPath) && !fs.existsSync(completionMarkerPath)) {
101
+ console.log('⚠ Incomplete Prisma layer detected - will be cleaned by buildPrismaLayer()');
102
+ }
103
+
104
+ // Build layer
36
105
  console.log('📦 Prisma Lambda Layer not found - building automatically...');
37
106
  console.log(' Building MINIMAL layer (runtime only, NO CLI)');
38
107
  console.log(' CLI is packaged separately in dbMigrate function');
39
108
  console.log(' This may take a minute on first deployment.\n');
40
109
 
110
+ // Create lock file with current process PID
111
+ try {
112
+ if (!fs.existsSync(layerPath)) {
113
+ fs.mkdirSync(layerPath, { recursive: true });
114
+ }
115
+ fs.writeFileSync(lockFilePath, process.pid.toString());
116
+ } catch (error) {
117
+ console.warn('⚠ Could not create lock file:', error.message);
118
+ }
119
+
41
120
  try {
42
121
  // Build layer WITHOUT CLI (runtime only for minimal size)
43
122
  await buildPrismaLayer(databaseConfig);
123
+
124
+ // Create completion marker
125
+ fs.writeFileSync(
126
+ completionMarkerPath,
127
+ `Build completed: ${new Date().toISOString()}\nNode: ${process.version}\nPlatform: ${process.platform}\n`
128
+ );
129
+
44
130
  console.log('✓ Prisma Lambda Layer built successfully (~10-15MB)\n');
45
131
  } catch (error) {
132
+ // Clean up partial build on failure
133
+ if (fs.existsSync(layerPath)) {
134
+ try {
135
+ fs.rmSync(layerPath, { recursive: true, force: true });
136
+ } catch (cleanupError) {
137
+ console.warn('⚠ Could not clean failed build:', cleanupError.message);
138
+ }
139
+ }
140
+
46
141
  console.error('✗ Failed to build Prisma Lambda Layer:', error.message);
47
142
  console.error(' You may need to run: npm install @friggframework/core\n');
48
143
  throw error;
144
+ } finally {
145
+ // Always remove lock file when done (success or failure)
146
+ if (fs.existsSync(lockFilePath)) {
147
+ try {
148
+ fs.rmSync(lockFilePath, { force: true });
149
+ } catch (error) {
150
+ console.warn('⚠ Could not remove lock file:', error.message);
151
+ }
152
+ }
49
153
  }
50
154
  }
51
155