@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,405 @@
1
+ /**
2
+ * Copyright (c) 2024 Frigg Integration Framework
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const fs = require('fs-extra');
11
+ const path = require('path');
12
+ const os = require('os');
13
+ const chalk = require('chalk');
14
+
15
+ /**
16
+ * Checks if a directory is a Frigg repository
17
+ * @param {string} directory - Path to check
18
+ * @returns {Promise<{isFriggRepo: boolean, repoInfo: object|null}>}
19
+ */
20
+ async function isFriggRepository(directory) {
21
+ try {
22
+ const packageJsonPath = path.join(directory, 'package.json');
23
+
24
+ // Check if package.json exists
25
+ if (!fs.existsSync(packageJsonPath)) {
26
+ return { isFriggRepo: false, repoInfo: null };
27
+ }
28
+
29
+ const packageJson = await fs.readJson(packageJsonPath);
30
+
31
+ // Primary indicators of a Frigg repository
32
+ const indicators = {
33
+ hasFriggDependencies: false,
34
+ hasBackendWorkspace: false,
35
+ hasFrontendWorkspace: false,
36
+ hasServerlessConfig: false,
37
+ friggDependencies: []
38
+ };
39
+
40
+ // Check for @friggframework dependencies
41
+ const allDeps = {
42
+ ...packageJson.dependencies,
43
+ ...packageJson.devDependencies,
44
+ ...packageJson.peerDependencies
45
+ };
46
+
47
+ for (const dep in allDeps) {
48
+ if (dep.startsWith('@friggframework/')) {
49
+ indicators.hasFriggDependencies = true;
50
+ indicators.friggDependencies.push(dep);
51
+ }
52
+ }
53
+
54
+ // Check for Frigg-specific files
55
+ const friggConfigFiles = [
56
+ 'frigg.config.js',
57
+ 'frigg.config.json',
58
+ '.friggrc',
59
+ '.friggrc.json',
60
+ '.friggrc.js'
61
+ ];
62
+
63
+ indicators.hasFriggConfig = friggConfigFiles.some(file =>
64
+ fs.existsSync(path.join(directory, file))
65
+ );
66
+
67
+ // Check for Frigg-specific directories
68
+ const friggDirs = [
69
+ '.frigg',
70
+ 'frigg-modules',
71
+ 'api-modules'
72
+ ];
73
+
74
+ indicators.hasFriggDirectories = friggDirs.some(dir =>
75
+ fs.existsSync(path.join(directory, dir))
76
+ );
77
+
78
+ // Check for Frigg-specific scripts in package.json
79
+ indicators.hasFriggScripts = false;
80
+ if (packageJson.scripts) {
81
+ const friggScriptPatterns = ['frigg', 'frigg-dev', 'frigg-build', 'frigg-deploy'];
82
+ indicators.hasFriggScripts = Object.keys(packageJson.scripts).some(script =>
83
+ friggScriptPatterns.some(pattern => script.includes(pattern)) ||
84
+ Object.values(packageJson.scripts).some(cmd =>
85
+ typeof cmd === 'string' && cmd.includes('frigg ')
86
+ )
87
+ );
88
+ }
89
+
90
+ // Check for workspace structure
91
+ if (packageJson.workspaces) {
92
+ const workspaces = Array.isArray(packageJson.workspaces)
93
+ ? packageJson.workspaces
94
+ : packageJson.workspaces.packages || [];
95
+
96
+ indicators.hasBackendWorkspace = workspaces.some(ws =>
97
+ ws.includes('backend') || ws === 'backend'
98
+ );
99
+ indicators.hasFrontendWorkspace = workspaces.some(ws =>
100
+ ws.includes('frontend') || ws === 'frontend'
101
+ );
102
+ }
103
+
104
+ // Check for backend/serverless.yml
105
+ const serverlessPath = path.join(directory, 'backend', 'serverless.yml');
106
+ indicators.hasServerlessConfig = fs.existsSync(serverlessPath);
107
+
108
+ // Check for individual frontend directories (React, Vue, etc.)
109
+ const frontendDirs = ['frontend', 'react', 'vue', 'svelte', 'angular'];
110
+ const existingFrontendDirs = frontendDirs.filter(dir =>
111
+ fs.existsSync(path.join(directory, dir))
112
+ );
113
+
114
+ // Skip @friggframework packages (they're framework packages, not Frigg apps)
115
+ if (packageJson.name && packageJson.name.startsWith('@friggframework/')) {
116
+ return { isFriggRepo: false, repoInfo: null };
117
+ }
118
+
119
+ // Additional check for Zapier apps that shouldn't be detected as Frigg repos
120
+ const isZapierApp = packageJson.name && (
121
+ packageJson.name.includes('zapier-public') ||
122
+ packageJson.name.includes('zapier-app') ||
123
+ (packageJson.scripts && packageJson.scripts.zapier)
124
+ );
125
+
126
+ // Check for specific Frigg indicators in serverless.yml
127
+ let hasFriggServerlessIndicators = false;
128
+ if (indicators.hasServerlessConfig) {
129
+ try {
130
+ const serverlessContent = fs.readFileSync(serverlessPath, 'utf8');
131
+ hasFriggServerlessIndicators = serverlessContent.includes('frigg') ||
132
+ serverlessContent.includes('FriggHandler') ||
133
+ serverlessContent.includes('@friggframework');
134
+ } catch (error) {
135
+ // Ignore read errors
136
+ }
137
+ }
138
+
139
+ // A directory is considered a Frigg repo if it has:
140
+ // 1. Frigg dependencies (MANDATORY - most reliable indicator) OR
141
+ // 2. Frigg-specific configuration files OR
142
+ // 3. Frigg-specific directories OR
143
+ // 4. Frigg-specific scripts in package.json OR
144
+ // 5. Serverless config with explicit Frigg references AND proper structure
145
+ //
146
+ // For Zapier apps, we require explicit Frigg indicators
147
+ const hasFriggIndicators = indicators.hasFriggDependencies ||
148
+ indicators.hasFriggConfig ||
149
+ indicators.hasFriggDirectories ||
150
+ indicators.hasFriggScripts ||
151
+ hasFriggServerlessIndicators;
152
+
153
+ // Determine if it's a Frigg repository
154
+ let isFriggRepo = false;
155
+
156
+ if (isZapierApp) {
157
+ // For Zapier apps, require explicit Frigg dependencies or config
158
+ isFriggRepo = indicators.hasFriggDependencies || indicators.hasFriggConfig;
159
+ } else {
160
+ // For non-Zapier apps, any Frigg indicator is sufficient
161
+ isFriggRepo = hasFriggIndicators;
162
+ }
163
+
164
+ // Additional validation for edge cases
165
+ if (isZapierApp && !indicators.hasFriggDependencies && !indicators.hasFriggConfig) {
166
+ return { isFriggRepo: false, repoInfo: null };
167
+ }
168
+
169
+ if (isFriggRepo) {
170
+ return {
171
+ isFriggRepo: true,
172
+ repoInfo: {
173
+ name: packageJson.name || path.basename(directory),
174
+ path: directory,
175
+ version: packageJson.version,
176
+ framework: detectFramework(directory, existingFrontendDirs),
177
+ hasBackend: fs.existsSync(path.join(directory, 'backend')),
178
+ friggDependencies: indicators.friggDependencies,
179
+ workspaces: packageJson.workspaces,
180
+ hasFriggConfig: indicators.hasFriggConfig,
181
+ hasFriggDirectories: indicators.hasFriggDirectories,
182
+ isZapierApp: isZapierApp,
183
+ ...indicators
184
+ }
185
+ };
186
+ }
187
+
188
+ return { isFriggRepo: false, repoInfo: null };
189
+
190
+ } catch (error) {
191
+ return { isFriggRepo: false, repoInfo: null };
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Detects the frontend framework used in the Frigg repository
197
+ * @param {string} directory - Repository directory
198
+ * @param {string[]} existingFrontendDirs - List of existing frontend directories
199
+ * @returns {string} Framework name
200
+ */
201
+ function detectFramework(directory, existingFrontendDirs) {
202
+ // Check for framework-specific directories
203
+ const frameworkDirs = {
204
+ 'react': 'React',
205
+ 'vue': 'Vue',
206
+ 'svelte': 'Svelte',
207
+ 'angular': 'Angular'
208
+ };
209
+
210
+ for (const dir of existingFrontendDirs) {
211
+ if (frameworkDirs[dir]) {
212
+ return frameworkDirs[dir];
213
+ }
214
+ }
215
+
216
+ // Check frontend directory for framework indicators
217
+ const frontendPath = path.join(directory, 'frontend');
218
+ if (fs.existsSync(frontendPath)) {
219
+ try {
220
+ const frontendPackageJson = path.join(frontendPath, 'package.json');
221
+ if (fs.existsSync(frontendPackageJson)) {
222
+ const frontendPkg = fs.readJsonSync(frontendPackageJson);
223
+ const deps = { ...frontendPkg.dependencies, ...frontendPkg.devDependencies };
224
+
225
+ if (deps.react) return 'React';
226
+ if (deps.vue) return 'Vue';
227
+ if (deps.svelte) return 'Svelte';
228
+ if (deps['@angular/core']) return 'Angular';
229
+ }
230
+ } catch (error) {
231
+ // Ignore errors
232
+ }
233
+ }
234
+
235
+ return 'Unknown';
236
+ }
237
+
238
+ /**
239
+ * Searches for Frigg repositories in common locations
240
+ * @param {Object} options - Search options
241
+ * @returns {Promise<Array>} Array of discovered repositories
242
+ */
243
+ async function discoverFriggRepositories(options = {}) {
244
+ const {
245
+ searchPaths = [
246
+ process.cwd(),
247
+ path.join(os.homedir(), 'Documents'),
248
+ path.join(os.homedir(), 'Projects'),
249
+ path.join(os.homedir(), 'Development'),
250
+ path.join(os.homedir(), 'dev'),
251
+ path.join(os.homedir(), 'Code')
252
+ ],
253
+ maxDepth = 3,
254
+ excludePatterns = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage']
255
+ } = options;
256
+
257
+ const discoveredRepos = [];
258
+ const visited = new Set();
259
+
260
+ for (const searchPath of searchPaths) {
261
+ if (fs.existsSync(searchPath)) {
262
+ await searchDirectory(searchPath, 0, maxDepth, excludePatterns, discoveredRepos, visited);
263
+ }
264
+ }
265
+
266
+ // Remove duplicates and sort by name
267
+ const uniqueRepos = Array.from(
268
+ new Map(discoveredRepos.map(repo => [repo.path, repo])).values()
269
+ );
270
+
271
+ return uniqueRepos.sort((a, b) => a.name.localeCompare(b.name));
272
+ }
273
+
274
+ /**
275
+ * Recursively searches a directory for Frigg repositories
276
+ */
277
+ async function searchDirectory(dirPath, currentDepth, maxDepth, excludePatterns, results, visited) {
278
+ // Avoid infinite loops from symlinks
279
+ const realPath = fs.realpathSync(dirPath);
280
+ if (visited.has(realPath)) return;
281
+ visited.add(realPath);
282
+
283
+ if (currentDepth > maxDepth) return;
284
+
285
+ try {
286
+ // Check if current directory is a Frigg repo
287
+ const { isFriggRepo, repoInfo } = await isFriggRepository(dirPath);
288
+ if (isFriggRepo) {
289
+ results.push(repoInfo);
290
+ return; // Don't search inside Frigg repos
291
+ }
292
+
293
+ // Continue searching subdirectories
294
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
295
+
296
+ for (const entry of entries) {
297
+ if (entry.isDirectory()) {
298
+ const entryName = entry.name;
299
+
300
+ // Skip excluded patterns
301
+ if (excludePatterns.some(pattern => entryName.includes(pattern))) {
302
+ continue;
303
+ }
304
+
305
+ // Skip hidden directories except .git for workspace detection
306
+ if (entryName.startsWith('.') && entryName !== '.git') {
307
+ continue;
308
+ }
309
+
310
+ const entryPath = path.join(dirPath, entryName);
311
+ await searchDirectory(entryPath, currentDepth + 1, maxDepth, excludePatterns, results, visited);
312
+ }
313
+ }
314
+ } catch (error) {
315
+ // Silently skip directories we can't access
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Gets the current directory's Frigg repository status
321
+ * @returns {Promise<Object>} Current repository info
322
+ */
323
+ async function getCurrentRepositoryInfo() {
324
+ const currentDir = process.cwd();
325
+
326
+ // Check current directory
327
+ let { isFriggRepo, repoInfo } = await isFriggRepository(currentDir);
328
+
329
+ if (isFriggRepo) {
330
+ return { ...repoInfo, isCurrent: true };
331
+ }
332
+
333
+ // Check parent directories up to 3 levels
334
+ let checkDir = currentDir;
335
+ for (let i = 0; i < 3; i++) {
336
+ const parentDir = path.dirname(checkDir);
337
+ if (parentDir === checkDir) break; // Reached root
338
+
339
+ const result = await isFriggRepository(parentDir);
340
+ if (result.isFriggRepo) {
341
+ return { ...result.repoInfo, isCurrent: false, currentSubPath: path.relative(parentDir, currentDir) };
342
+ }
343
+ checkDir = parentDir;
344
+ }
345
+
346
+ return null;
347
+ }
348
+
349
+ /**
350
+ * Prompts user to select a repository from discovered repos
351
+ * @param {Array} repositories - List of discovered repositories
352
+ * @returns {Promise<Object|null>} Selected repository or null
353
+ */
354
+ async function promptRepositorySelection(repositories) {
355
+ if (repositories.length === 0) {
356
+ console.log(chalk.yellow('No Frigg repositories found.'));
357
+ console.log(chalk.gray('To create a new Frigg project, run: frigg init <project-name>'));
358
+ return null;
359
+ }
360
+
361
+ if (repositories.length === 1) {
362
+ console.log(chalk.green(`Found 1 Frigg repository: ${repositories[0].name}`));
363
+ return repositories[0];
364
+ }
365
+
366
+ console.log(chalk.blue(`Found ${repositories.length} Frigg repositories:`));
367
+ console.log();
368
+
369
+ repositories.forEach((repo, index) => {
370
+ const framework = repo.framework !== 'Unknown' ? chalk.gray(`(${repo.framework})`) : '';
371
+ console.log(` ${chalk.cyan((index + 1).toString().padStart(2))}. ${chalk.white(repo.name)} ${framework}`);
372
+ console.log(` ${chalk.gray(repo.path)}`);
373
+ });
374
+
375
+ console.log();
376
+
377
+ // For now, return the first one. In a full implementation, you'd use a prompt library
378
+ console.log(chalk.yellow('Auto-selecting first repository. Use interactive selection in future versions.'));
379
+ return repositories[0];
380
+ }
381
+
382
+ /**
383
+ * Formats repository information for display
384
+ * @param {Object} repoInfo - Repository information
385
+ * @returns {string} Formatted display string
386
+ */
387
+ function formatRepositoryInfo(repoInfo) {
388
+ const parts = [
389
+ chalk.white(repoInfo.name),
390
+ repoInfo.version ? chalk.gray(`v${repoInfo.version}`) : '',
391
+ repoInfo.framework !== 'Unknown' ? chalk.blue(`[${repoInfo.framework}]`) : '',
392
+ repoInfo.hasBackend ? chalk.green('[Backend]') : ''
393
+ ].filter(Boolean);
394
+
395
+ return parts.join(' ');
396
+ }
397
+
398
+ module.exports = {
399
+ isFriggRepository,
400
+ discoverFriggRepositories,
401
+ getCurrentRepositoryInfo,
402
+ promptRepositorySelection,
403
+ formatRepositoryInfo,
404
+ detectFramework
405
+ };
@@ -1,8 +1,28 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs-extra');
3
+ const crypto = require('crypto');
3
4
  const { composeServerlessDefinition } = require('./infrastructure-composer');
4
5
  const { findNearestBackendPackageJson } = require('@friggframework/core');
5
6
 
7
+ // Filesystem-based cache to persist across osls require cache clears
8
+ const getCachePath = (backendDir) => {
9
+ return path.join(backendDir, '.frigg-infrastructure-cache.json');
10
+ };
11
+
12
+ const getLockPath = (backendDir) => {
13
+ return path.join(backendDir, '.frigg-infrastructure-lock');
14
+ };
15
+
16
+ // Check if process is still running
17
+ function isProcessRunning(pid) {
18
+ try {
19
+ process.kill(pid, 0);
20
+ return true;
21
+ } catch (error) {
22
+ return false;
23
+ }
24
+ }
25
+
6
26
  async function createFriggInfrastructure() {
7
27
  const backendPath = findNearestBackendPackageJson();
8
28
  if (!backendPath) {
@@ -15,20 +35,113 @@ async function createFriggInfrastructure() {
15
35
  throw new Error('Could not find index.js');
16
36
  }
17
37
 
18
- const backend = require(backendFilePath);
19
- const appDefinition = backend.Definition;
38
+ const cachePath = getCachePath(backendDir);
39
+ const lockPath = getLockPath(backendDir);
40
+
41
+ // Check for cached infrastructure (filesystem-based for osls require cache clearing)
42
+ if (fs.existsSync(cachePath)) {
43
+ try {
44
+ const cached = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
45
+ // Verify cache is still valid (less than 60 seconds old)
46
+ if (Date.now() - cached.timestamp < 60000) {
47
+ console.log('✓ Using filesystem-cached infrastructure definition');
48
+ return cached.definition;
49
+ } else {
50
+ console.log('⚠️ Cache expired (> 60s), recomposing...');
51
+ fs.removeSync(cachePath);
52
+ }
53
+ } catch (error) {
54
+ console.log('⚠️ Invalid cache file, recomposing...', error.message);
55
+ fs.removeSync(cachePath);
56
+ }
57
+ }
58
+
59
+ // Check for active composition process
60
+ if (fs.existsSync(lockPath)) {
61
+ const lockPid = parseInt(fs.readFileSync(lockPath, 'utf-8').trim(), 10);
62
+
63
+ if (isProcessRunning(lockPid)) {
64
+ console.log(`⏳ Another composition (PID ${lockPid}) in progress - waiting...`);
65
+
66
+ // Wait up to 30 seconds for the other process to complete
67
+ for (let i = 0; i < 30; i++) {
68
+ await new Promise(resolve => setTimeout(resolve, 1000));
20
69
 
21
- // const serverlessTemplate = require(path.resolve(
22
- // __dirname,
23
- // './serverless-template.js'
24
- // ));
25
- const definition = await composeServerlessDefinition(
26
- appDefinition,
27
- );
70
+ // Check if cache was created by other process
71
+ if (fs.existsSync(cachePath)) {
72
+ try {
73
+ const cached = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
74
+ console.log('✓ Using infrastructure composed by concurrent process');
75
+ return cached.definition;
76
+ } catch (error) {
77
+ // Cache file corrupted, continue to compose ourselves
78
+ break;
79
+ }
80
+ }
28
81
 
29
- return {
30
- ...definition,
31
- };
82
+ // Check if process died
83
+ if (!isProcessRunning(lockPid)) {
84
+ console.log(`⚠ Composition process ${lockPid} terminated - cleaning up stale lock`);
85
+ fs.removeSync(lockPath);
86
+ break;
87
+ }
88
+ }
89
+ } else {
90
+ // Stale lock file
91
+ console.log(`⚠️ Stale lock file detected (PID ${lockPid} not running) - cleaning up`);
92
+ fs.removeSync(lockPath);
93
+ }
94
+ }
95
+
96
+ // Create lock file with current process PID
97
+ try {
98
+ fs.writeFileSync(lockPath, process.pid.toString());
99
+ } catch (error) {
100
+ console.warn('⚠ Could not create lock file:', error.message);
101
+ }
102
+
103
+ try {
104
+ const backend = require(backendFilePath);
105
+ const appDefinition = backend.Definition;
106
+
107
+ const definition = await composeServerlessDefinition(
108
+ appDefinition,
109
+ );
110
+
111
+ // Write cache to filesystem (persists across osls require cache clears)
112
+ try {
113
+ fs.writeFileSync(cachePath, JSON.stringify({
114
+ timestamp: Date.now(),
115
+ definition
116
+ }));
117
+ console.log('✓ Infrastructure definition cached to filesystem');
118
+ } catch (error) {
119
+ console.warn('⚠ Could not write cache file:', error.message);
120
+ }
121
+
122
+ return definition;
123
+ } catch (error) {
124
+ // Clean up partial cache on error
125
+ if (fs.existsSync(cachePath)) {
126
+ try {
127
+ fs.removeSync(cachePath);
128
+ } catch (cleanupError) {
129
+ console.warn('⚠ Could not clean failed cache:', cleanupError.message);
130
+ }
131
+ }
132
+
133
+ console.error('✗ Failed to compose infrastructure:', error.message);
134
+ throw error;
135
+ } finally {
136
+ // Always remove lock file when done (success or failure)
137
+ if (fs.existsSync(lockPath)) {
138
+ try {
139
+ fs.removeSync(lockPath);
140
+ } catch (error) {
141
+ console.warn('⚠ Could not remove lock file:', error.message);
142
+ }
143
+ }
144
+ }
32
145
  }
33
146
 
34
147
  module.exports = { createFriggInfrastructure };