@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,93 @@
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 path = require('path');
11
+ const chalk = require('chalk');
12
+ const validateProjectName = require('validate-npm-package-name');
13
+ const semver = require('semver');
14
+ const BackendFirstHandler = require('./backend-first-handler');
15
+
16
+ function checkAppName(appName) {
17
+ const validationResult = validateProjectName(appName);
18
+ if (!validationResult.validForNewPackages) {
19
+ console.error(
20
+ chalk.red(
21
+ `Cannot create a project named ${chalk.green(
22
+ `"${appName}"`
23
+ )} because of npm naming restrictions:\n`
24
+ )
25
+ );
26
+ [
27
+ ...(validationResult.errors || []),
28
+ ...(validationResult.warnings || []),
29
+ ].forEach(error => {
30
+ console.error(chalk.red(` * ${error}`));
31
+ });
32
+ console.error(chalk.red('\nPlease choose a different project name.'));
33
+ process.exit(1);
34
+ }
35
+ }
36
+
37
+ function checkNodeVersion() {
38
+ const unsupportedNodeVersion = !semver.satisfies(
39
+ semver.coerce(process.version),
40
+ '>=14'
41
+ );
42
+
43
+ if (unsupportedNodeVersion) {
44
+ console.log(
45
+ chalk.yellow(
46
+ `You are using Node ${process.version} so the project will be bootstrapped with an old unsupported version of tools.\n\n` +
47
+ `Please update to Node 14 or higher for a better, fully supported experience.\n`
48
+ )
49
+ );
50
+ }
51
+ }
52
+
53
+ async function initCommand(projectName, options) {
54
+ const verbose = options.verbose || false;
55
+ const force = options.force || false;
56
+
57
+ checkNodeVersion();
58
+
59
+ const root = path.resolve(projectName);
60
+ const appName = path.basename(root);
61
+
62
+ checkAppName(appName);
63
+
64
+ // Use backend-first handler by default
65
+ if (!options.template && !options.legacyFrontend) {
66
+ try {
67
+ const handler = new BackendFirstHandler(root, {
68
+ force,
69
+ verbose,
70
+ mode: options.mode,
71
+ frontend: options.frontend
72
+ });
73
+
74
+ await handler.initialize();
75
+ return;
76
+ } catch (error) {
77
+ console.log();
78
+ console.log(chalk.red('Aborting installation.'));
79
+ console.log(chalk.red('Error:'), error.message);
80
+ console.log();
81
+ process.exit(1);
82
+ }
83
+ }
84
+
85
+ // If we get here, show an error for legacy options
86
+ console.log();
87
+ console.log(chalk.red('Legacy template system is no longer supported.'));
88
+ console.log(chalk.yellow('Please use the new backend-first approach.'));
89
+ console.log();
90
+ process.exit(1);
91
+ }
92
+
93
+ module.exports = { initCommand };
@@ -0,0 +1,143 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ /**
6
+ * Handles template initialization for the frigg init command
7
+ */
8
+ class TemplateHandler {
9
+ constructor(targetPath, options = {}) {
10
+ this.targetPath = targetPath;
11
+ this.options = options;
12
+ this.templatesDir = path.join(__dirname, '..', 'templates');
13
+ }
14
+
15
+ /**
16
+ * Initialize a new Frigg backend project from template
17
+ */
18
+ async initializeBackendTemplate() {
19
+ const templatePath = path.join(this.templatesDir, 'backend');
20
+
21
+ if (!fs.existsSync(templatePath)) {
22
+ throw new Error('Backend template not found. Please ensure the CLI is properly installed.');
23
+ }
24
+
25
+ // Create target directory if it doesn't exist
26
+ await fs.ensureDir(this.targetPath);
27
+
28
+ // Check if directory is empty
29
+ const files = await fs.readdir(this.targetPath);
30
+ if (files.length > 0 && !this.options.force) {
31
+ throw new Error('Target directory is not empty. Use --force to override.');
32
+ }
33
+
34
+ console.log('🚀 Initializing new Frigg backend project...');
35
+
36
+ // Copy template files
37
+ await this.copyTemplateFiles(templatePath, this.targetPath);
38
+
39
+ // Update package.json with project name
40
+ await this.updatePackageJson();
41
+
42
+ // Update serverless.yml with project name
43
+ await this.updateServerlessConfig();
44
+
45
+ // Initialize git if requested
46
+ if (this.options.git !== false) {
47
+ await this.initializeGit();
48
+ }
49
+
50
+ // Install dependencies if requested
51
+ if (this.options.install !== false) {
52
+ await this.installDependencies();
53
+ }
54
+
55
+ console.log('✅ Frigg backend project initialized successfully!');
56
+ console.log('\nNext steps:');
57
+ console.log(` cd ${path.basename(this.targetPath)}`);
58
+ if (this.options.install === false) {
59
+ console.log(' npm install');
60
+ }
61
+ console.log(' npm run docker:start');
62
+ console.log(' npm run backend-start');
63
+ console.log('\nFor more information, check the README.md file.');
64
+ }
65
+
66
+ /**
67
+ * Copy template files to target directory
68
+ */
69
+ async copyTemplateFiles(source, target) {
70
+ await fs.copy(source, target, {
71
+ overwrite: this.options.force || false,
72
+ filter: (src) => {
73
+ // Skip node_modules and other build artifacts
74
+ const basename = path.basename(src);
75
+ return !['node_modules', '.serverless', 'dist', 'build'].includes(basename);
76
+ }
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Update package.json with project-specific details
82
+ */
83
+ async updatePackageJson() {
84
+ const packageJsonPath = path.join(this.targetPath, 'package.json');
85
+ const packageJson = await fs.readJson(packageJsonPath);
86
+
87
+ // Update package name based on directory name
88
+ const projectName = path.basename(this.targetPath);
89
+ packageJson.name = projectName;
90
+
91
+ // Remove private flag for new projects
92
+ delete packageJson.private;
93
+
94
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
95
+ }
96
+
97
+ /**
98
+ * Update serverless.yml with project-specific details
99
+ */
100
+ async updateServerlessConfig() {
101
+ const serverlessPath = path.join(this.targetPath, 'serverless.yml');
102
+ let serverlessContent = await fs.readFile(serverlessPath, 'utf8');
103
+
104
+ // Update service name based on directory name
105
+ const projectName = path.basename(this.targetPath);
106
+ serverlessContent = serverlessContent.replace(
107
+ /^service: create-frigg-app$/m,
108
+ `service: ${projectName}`
109
+ );
110
+
111
+ await fs.writeFile(serverlessPath, serverlessContent);
112
+ }
113
+
114
+ /**
115
+ * Initialize git repository
116
+ */
117
+ async initializeGit() {
118
+ try {
119
+ console.log('📦 Initializing git repository...');
120
+ execSync('git init', { cwd: this.targetPath, stdio: 'ignore' });
121
+ execSync('git add .', { cwd: this.targetPath, stdio: 'ignore' });
122
+ execSync('git commit -m "Initial commit from Frigg CLI"', {
123
+ cwd: this.targetPath,
124
+ stdio: 'ignore'
125
+ });
126
+ } catch (error) {
127
+ console.warn('⚠️ Git initialization failed. You can initialize git manually later.');
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Install npm dependencies
133
+ */
134
+ async installDependencies() {
135
+ console.log('📦 Installing dependencies...');
136
+ execSync('npm install', {
137
+ cwd: this.targetPath,
138
+ stdio: 'inherit'
139
+ });
140
+ }
141
+ }
142
+
143
+ module.exports = TemplateHandler;
@@ -0,0 +1,33 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { logInfo } = require('./logger');
4
+ const INTEGRATIONS_DIR = 'src/integrations';
5
+ const BACKEND_JS = 'backend.js';
6
+
7
+ function updateBackendJsFile(backendPath, apiModuleName) {
8
+ const backendJsPath = path.join(path.dirname(backendPath), BACKEND_JS);
9
+ logInfo(`Updating backend.js: ${backendJsPath}`);
10
+ updateBackendJs(backendJsPath, apiModuleName);
11
+ }
12
+
13
+ function updateBackendJs(backendJsPath, apiModuleName) {
14
+ const backendJsContent = fs.readFileSync(backendJsPath, 'utf-8');
15
+ const importStatement = `const ${apiModuleName}Integration = require('./${INTEGRATIONS_DIR}/${apiModuleName}Integration');\n`;
16
+
17
+ if (!backendJsContent.includes(importStatement)) {
18
+ const updatedContent = backendJsContent.replace(
19
+ /(integrations\s*:\s*\[)([\s\S]*?)(\])/,
20
+ `$1\n ${apiModuleName}Integration,$2$3`
21
+ );
22
+ fs.writeFileSync(backendJsPath, importStatement + updatedContent);
23
+ } else {
24
+ logInfo(
25
+ `Import statement for ${apiModuleName}Integration already exists in backend.js`
26
+ );
27
+ }
28
+ }
29
+
30
+ module.exports = {
31
+ updateBackendJsFile,
32
+ updateBackendJs,
33
+ };
@@ -0,0 +1,16 @@
1
+ const { execSync } = require('child_process');
2
+ const path = require('path');
3
+
4
+ function commitChanges(backendPath, apiModuleName) {
5
+ const apiModulePath = path.join(path.dirname(backendPath), 'src', 'integrations', `${apiModuleName}Integration.js`);
6
+ try {
7
+ execSync(`git add ${apiModulePath}`);
8
+ execSync(`git commit -m "Add ${apiModuleName}Integration to ${apiModuleName}Integration.js"`);
9
+ } catch (error) {
10
+ throw new Error('Failed to commit changes:', error);
11
+ }
12
+ }
13
+
14
+ module.exports = {
15
+ commitChanges,
16
+ };
@@ -0,0 +1,127 @@
1
+ const fs = require('fs');
2
+ const dotenv = require('dotenv');
3
+ const { readFileSync, writeFileSync, existsSync } = require('fs');
4
+ const { logInfo } = require('./logger');
5
+ const { resolve } = require('node:path');
6
+ const { confirm, input } = require('@inquirer/prompts');
7
+ const { parse } = require('@babel/parser');
8
+ const traverse = require('@babel/traverse').default;
9
+
10
+ const extractRawEnvVariables = (modulePath) => {
11
+ const filePath = resolve(modulePath, 'definition.js');
12
+
13
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
14
+ const ast = parse(fileContent, {
15
+ sourceType: 'module',
16
+ plugins: ['jsx', 'typescript'], // Add more plugins if needed
17
+ });
18
+
19
+ const envVariables = {};
20
+
21
+ traverse(ast, {
22
+ ObjectProperty(path) {
23
+ if (path.node.key.name === 'env') {
24
+ path.node.value.properties.forEach((prop) => {
25
+ const key = prop.key.name;
26
+ if (prop.value.type === 'MemberExpression') {
27
+ const property = prop.value.property.name;
28
+ envVariables[key] = `${property}`;
29
+ } else if (prop.value.type === 'TemplateLiteral') {
30
+ // Handle template literals
31
+ const expressions = prop.value.expressions.map((exp) =>
32
+ exp.type === 'MemberExpression'
33
+ ? `${exp.property.name}`
34
+ : exp.name
35
+ );
36
+ envVariables[key] = expressions.join('');
37
+ }
38
+ });
39
+ }
40
+ },
41
+ });
42
+
43
+ return envVariables;
44
+ };
45
+ const handleEnvVariables = async (backendPath, modulePath) => {
46
+ logInfo('Searching for missing environment variables...');
47
+ const Definition = { env: extractRawEnvVariables(modulePath) };
48
+ if (Definition && Definition.env) {
49
+ console.log('Here is Definition.env:', Definition.env);
50
+ const envVars = Object.values(Definition.env);
51
+
52
+ console.log(
53
+ 'Found the following environment variables in the API module:',
54
+ envVars
55
+ );
56
+
57
+ const localEnvPath = resolve(backendPath, '../.env');
58
+ const localDevConfigPath = resolve(
59
+ backendPath,
60
+ '../src/configs/dev.json'
61
+ );
62
+
63
+ // Load local .env variables
64
+ let localEnvVars = {};
65
+ if (existsSync(localEnvPath)) {
66
+ localEnvVars = dotenv.parse(readFileSync(localEnvPath, 'utf8'));
67
+ }
68
+
69
+ // Load local dev.json variables
70
+ let localDevConfig = {};
71
+ if (existsSync(localDevConfigPath)) {
72
+ localDevConfig = JSON.parse(
73
+ readFileSync(localDevConfigPath, 'utf8')
74
+ );
75
+ }
76
+
77
+ const missingEnvVars = envVars.filter(
78
+ (envVar) => !localEnvVars[envVar] && !localDevConfig[envVar]
79
+ );
80
+
81
+ logInfo(`Missing environment variables: ${missingEnvVars.join(', ')}`);
82
+
83
+ if (missingEnvVars.length > 0) {
84
+ const addEnvVars = await confirm({
85
+ message: `The following environment variables are required: ${missingEnvVars.join(
86
+ ', '
87
+ )}. Do you want to add them now?`,
88
+ });
89
+
90
+ if (addEnvVars) {
91
+ const envValues = {};
92
+ for (const envVar of missingEnvVars) {
93
+ const value = await input({
94
+ type: 'input',
95
+ name: 'value',
96
+ message: `Enter value for ${envVar}:`,
97
+ });
98
+ envValues[envVar] = value;
99
+ }
100
+
101
+ // Add the envValues to the local .env file if it exists
102
+ if (existsSync(localEnvPath)) {
103
+ const envContent = Object.entries(envValues)
104
+ .map(([key, value]) => `${key}=${value}`)
105
+ .join('\n');
106
+ fs.appendFileSync(localEnvPath, `\n${envContent}`);
107
+ }
108
+
109
+ // Add the envValues to the local dev.json file if it exists
110
+ if (existsSync(localDevConfigPath)) {
111
+ const updatedDevConfig = {
112
+ ...localDevConfig,
113
+ ...envValues,
114
+ };
115
+ writeFileSync(
116
+ localDevConfigPath,
117
+ JSON.stringify(updatedDevConfig, null, 2)
118
+ );
119
+ }
120
+ } else {
121
+ logInfo("Edit whenever you're able, safe travels friend!");
122
+ }
123
+ }
124
+ }
125
+ };
126
+
127
+ module.exports = { handleEnvVariables };
@@ -0,0 +1,136 @@
1
+ const { handleEnvVariables } = require('./environment-variables');
2
+ const { logInfo } = require('./logger');
3
+ const inquirer = require('inquirer');
4
+ const fs = require('fs');
5
+ const dotenv = require('dotenv');
6
+ const { resolve } = require('node:path');
7
+ const { parse } = require('@babel/parser');
8
+ const traverse = require('@babel/traverse');
9
+
10
+ jest.mock('inquirer');
11
+ jest.mock('fs');
12
+ jest.mock('dotenv');
13
+ jest.mock('./logger');
14
+ jest.mock('@babel/parser');
15
+ jest.mock('@babel/traverse');
16
+
17
+ describe('handleEnvVariables', () => {
18
+ const backendPath = '/mock/backend/path';
19
+ const modulePath = '/mock/module/path';
20
+
21
+ beforeEach(() => {
22
+ jest.clearAllMocks();
23
+ fs.readFileSync.mockReturnValue(`
24
+ const Definition = {
25
+ env: {
26
+ client_id: process.env.GOOGLE_CALENDAR_CLIENT_ID,
27
+ client_secret: process.env.GOOGLE_CALENDAR_CLIENT_SECRET,
28
+ redirect_uri: \`\${process.env.REDIRECT_URI}/google-calendar\`,
29
+ scope: process.env.GOOGLE_CALENDAR_SCOPE,
30
+ }
31
+ };
32
+ `);
33
+ parse.mockReturnValue({});
34
+ traverse.default.mockImplementation((ast, visitor) => {
35
+ visitor.ObjectProperty({
36
+ node: {
37
+ key: { name: 'env' },
38
+ value: {
39
+ properties: [
40
+ {
41
+ key: { name: 'client_id' },
42
+ value: {
43
+ type: 'MemberExpression',
44
+ object: { name: 'process' },
45
+ property: {
46
+ name: 'GOOGLE_CALENDAR_CLIENT_ID',
47
+ },
48
+ },
49
+ },
50
+ {
51
+ key: { name: 'client_secret' },
52
+ value: {
53
+ type: 'MemberExpression',
54
+ object: { name: 'process' },
55
+ property: {
56
+ name: 'GOOGLE_CALENDAR_CLIENT_SECRET',
57
+ },
58
+ },
59
+ },
60
+ {
61
+ key: { name: 'redirect_uri' },
62
+ value: {
63
+ type: 'MemberExpression',
64
+ object: { name: 'process' },
65
+ property: { name: 'REDIRECT_URI' },
66
+ },
67
+ },
68
+ {
69
+ key: { name: 'scope' },
70
+ value: {
71
+ type: 'MemberExpression',
72
+ object: { name: 'process' },
73
+ property: { name: 'GOOGLE_CALENDAR_SCOPE' },
74
+ },
75
+ },
76
+ ],
77
+ },
78
+ },
79
+ });
80
+ });
81
+ });
82
+
83
+ xit('should identify and handle missing environment variables', async () => {
84
+ const localEnvPath = resolve(backendPath, '../.env');
85
+ const localDevConfigPath = resolve(
86
+ backendPath,
87
+ '../src/configs/dev.json'
88
+ );
89
+
90
+ fs.existsSync.mockImplementation(
91
+ (path) => path === localEnvPath || path === localDevConfigPath
92
+ );
93
+ dotenv.parse.mockReturnValue({});
94
+ fs.readFileSync.mockImplementation((path) => {
95
+ if (path === resolve(modulePath, 'index.js'))
96
+ return 'mock module content';
97
+ if (path === localEnvPath) return '';
98
+ if (path === localDevConfigPath) return '{}';
99
+ return '';
100
+ });
101
+
102
+ inquirer.prompt
103
+ .mockResolvedValueOnce({ addEnvVars: true })
104
+ .mockResolvedValueOnce({ value: 'client_id_value' })
105
+ .mockResolvedValueOnce({ value: 'client_secret_value' })
106
+ .mockResolvedValueOnce({ value: 'redirect_uri_value' })
107
+ .mockResolvedValueOnce({ value: 'scope_value' });
108
+
109
+ await handleEnvVariables(backendPath, modulePath);
110
+
111
+ expect(logInfo).toHaveBeenCalledWith(
112
+ 'Searching for missing environment variables...'
113
+ );
114
+ expect(logInfo).toHaveBeenCalledWith(
115
+ 'Missing environment variables: GOOGLE_CALENDAR_CLIENT_ID, GOOGLE_CALENDAR_CLIENT_SECRET, REDIRECT_URI, GOOGLE_CALENDAR_SCOPE'
116
+ );
117
+ expect(inquirer.prompt).toHaveBeenCalledTimes(5);
118
+ expect(fs.appendFileSync).toHaveBeenCalledWith(
119
+ localEnvPath,
120
+ '\nGOOGLE_CALENDAR_CLIENT_ID=client_id_value\nGOOGLE_CALENDAR_CLIENT_SECRET=client_secret_value\nREDIRECT_URI=redirect_uri_value\nGOOGLE_CALENDAR_SCOPE=scope_value'
121
+ );
122
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
123
+ localDevConfigPath,
124
+ JSON.stringify(
125
+ {
126
+ GOOGLE_CALENDAR_CLIENT_ID: 'client_id_value',
127
+ GOOGLE_CALENDAR_CLIENT_SECRET: 'client_secret_value',
128
+ REDIRECT_URI: 'redirect_uri_value',
129
+ GOOGLE_CALENDAR_SCOPE: 'scope_value',
130
+ },
131
+ null,
132
+ 2
133
+ )
134
+ );
135
+ });
136
+ });
@@ -0,0 +1,54 @@
1
+ const { installPackage } = require('./install-package');
2
+ const { createIntegrationFile } = require('./integration-file');
3
+ const { resolve } = require('node:path');
4
+ const { updateBackendJsFile } = require('./backend-js');
5
+ const { logInfo, logError } = require('./logger');
6
+ const { commitChanges } = require('./commit-changes');
7
+ const { handleEnvVariables } = require('./environment-variables');
8
+ const {
9
+ validatePackageExists,
10
+ searchAndSelectPackage,
11
+ } = require('./validate-package');
12
+ const { findNearestBackendPackageJson, validateBackendPath } = require('@friggframework/core/utils');
13
+
14
+ const installCommand = async (apiModuleName) => {
15
+ try {
16
+ const packageNames = await searchAndSelectPackage(apiModuleName);
17
+ if (!packageNames || packageNames.length === 0) return;
18
+
19
+ const backendPath = findNearestBackendPackageJson();
20
+ validateBackendPath(backendPath);
21
+
22
+ for (const packageName of packageNames) {
23
+ await validatePackageExists(packageName);
24
+ installPackage(backendPath, packageName);
25
+
26
+ const modulePath = resolve(
27
+ backendPath,
28
+ `../../node_modules/${packageName}`
29
+ );
30
+ const {
31
+ Config: { label },
32
+ Api: ApiClass,
33
+ } = require(modulePath);
34
+
35
+ const sanitizedLabel = label.replace(
36
+ /[<>:"/\\|?*\x00-\x1F\s]/g,
37
+ ''
38
+ ); // Remove invalid characters and spaces console.log('Installing integration for:', sanitizedLabel);
39
+ createIntegrationFile(backendPath, sanitizedLabel, ApiClass);
40
+ updateBackendJsFile(backendPath, sanitizedLabel);
41
+ commitChanges(backendPath, sanitizedLabel);
42
+ logInfo(
43
+ `Successfully installed ${packageName} and updated the project.`
44
+ );
45
+
46
+ await handleEnvVariables(backendPath, modulePath);
47
+ }
48
+ } catch (error) {
49
+ logError('An error occurred:', error);
50
+ process.exit(1);
51
+ }
52
+ };
53
+
54
+ module.exports = { installCommand };
@@ -0,0 +1,13 @@
1
+ const { execSync } = require('child_process');
2
+ const path = require('path');
3
+
4
+ function installPackage(backendPath, packageName) {
5
+ execSync(`npm install ${packageName}`, {
6
+ cwd: path.dirname(backendPath),
7
+ stdio: 'inherit',
8
+ });
9
+ }
10
+
11
+ module.exports = {
12
+ installPackage,
13
+ };
@@ -0,0 +1,30 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const { logInfo } = require('./logger');
4
+ const { getIntegrationTemplate } = require('./template');
5
+ const INTEGRATIONS_DIR = 'src/integrations';
6
+
7
+ function createIntegrationFile(backendPath, apiModuleName, ApiClass) {
8
+ const integrationDir = path.join(
9
+ path.dirname(backendPath),
10
+ INTEGRATIONS_DIR
11
+ );
12
+ logInfo(`Ensuring directory exists: ${integrationDir}`);
13
+ fs.ensureDirSync(integrationDir);
14
+
15
+ const integrationFilePath = path.join(
16
+ integrationDir,
17
+ `${apiModuleName}Integration.js`
18
+ );
19
+ logInfo(`Writing integration file: ${integrationFilePath}`);
20
+ const integrationTemplate = getIntegrationTemplate(
21
+ apiModuleName,
22
+ backendPath,
23
+ ApiClass
24
+ );
25
+ fs.writeFileSync(integrationFilePath, integrationTemplate);
26
+ }
27
+
28
+ module.exports = {
29
+ createIntegrationFile,
30
+ };
@@ -0,0 +1,12 @@
1
+ function logInfo(message) {
2
+ console.log(message);
3
+ }
4
+
5
+ function logError(message, error) {
6
+ console.error(message, error);
7
+ }
8
+
9
+ module.exports = {
10
+ logInfo,
11
+ logError,
12
+ };