Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI for Frigg - Install command for now #322

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
463 changes: 385 additions & 78 deletions package-lock.json

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions packages/devtools/frigg-cli/backendJs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const fs = require('fs-extra');
const path = require('path');
const { logInfo } = require('./logger');
const INTEGRATIONS_DIR = 'src/integrations';
const BACKEND_JS = 'backend.js';

function updateBackendJsFile(backendPath, apiModuleName) {
const backendJsPath = path.join(path.dirname(backendPath), BACKEND_JS);
logInfo(`Updating backend.js: ${backendJsPath}`);
updateBackendJs(backendJsPath, apiModuleName);
}

function updateBackendJs(backendJsPath, apiModuleName) {
const backendJsContent = fs.readFileSync(backendJsPath, 'utf-8');
const importStatement = `const ${apiModuleName}Integration = require('./${INTEGRATIONS_DIR}/${apiModuleName}Integration');\n`;

if (!backendJsContent.includes(importStatement)) {
const updatedContent = backendJsContent.replace(
/(integrations\s*:\s*\[)([\s\S]*?)(\])/,
`$1\n ${apiModuleName}Integration,$2$3`
);
fs.writeFileSync(backendJsPath, importStatement + updatedContent);
} else {
logInfo(
`Import statement for ${apiModuleName}Integration already exists in backend.js`
);
}
}

module.exports = {
updateBackendJsFile,
updateBackendJs,
};
26 changes: 26 additions & 0 deletions packages/devtools/frigg-cli/backendPath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const fs = require('fs-extra');
const path = require('path');
const PACKAGE_JSON = 'package.json';

function findNearestBackendPackageJson() {
let currentDir = process.cwd();
while (currentDir !== path.parse(currentDir).root) {
const packageJsonPath = path.join(currentDir, 'backend', PACKAGE_JSON);
if (fs.existsSync(packageJsonPath)) {
return packageJsonPath;
}
currentDir = path.dirname(currentDir);
}
return null;
}

function validateBackendPath(backendPath) {
if (!backendPath) {
throw new Error('Could not find a backend package.json file.');
}
}

module.exports = {
findNearestBackendPackageJson,
validateBackendPath,
};
16 changes: 16 additions & 0 deletions packages/devtools/frigg-cli/commitChanges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const { execSync } = require('child_process');
const path = require('path');

function commitChanges(backendPath, apiModuleName) {
const apiModulePath = path.join(path.dirname(backendPath), 'src', 'integrations', `${apiModuleName}Integration.js`);
try {
execSync(`git add ${apiModulePath}`);

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium

This shell command depends on an uncontrolled
absolute path
.
execSync(`git commit -m "Add ${apiModuleName}Integration to ${apiModuleName}Integration.js"`);
} catch (error) {
throw new Error('Failed to commit changes:', error);
}
}

module.exports = {
commitChanges,
};
134 changes: 134 additions & 0 deletions packages/devtools/frigg-cli/environmentVariables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const fs = require('fs');
const dotenv = require('dotenv');
const { readFileSync, writeFileSync, existsSync } = require('fs');
const { logInfo } = require('./logger');
const { resolve } = require('node:path');
const inquirer = require('inquirer');

const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;

const extractRawEnvVariables = (modulePath) => {
const filePath = resolve(modulePath, 'definition.js');

const fileContent = fs.readFileSync(filePath, 'utf-8');
const ast = parse(fileContent, {
sourceType: 'module',
plugins: ['jsx', 'typescript'], // Add more plugins if needed
});

const envVariables = {};

traverse(ast, {
ObjectProperty(path) {
if (path.node.key.name === 'env') {
path.node.value.properties.forEach((prop) => {
const key = prop.key.name;
if (prop.value.type === 'MemberExpression') {
const property = prop.value.property.name;
envVariables[key] = `${property}`;
} else if (prop.value.type === 'TemplateLiteral') {
// Handle template literals
const expressions = prop.value.expressions.map((exp) =>
exp.type === 'MemberExpression'
? `${exp.property.name}`
: exp.name
);
envVariables[key] = expressions.join('');
}
});
}
},
});

return envVariables;
};
const handleEnvVariables = async (backendPath, modulePath) => {
logInfo('Searching for missing environment variables...');
const Definition = { env: extractRawEnvVariables(modulePath) };
if (Definition && Definition.env) {
console.log('Here is Definition.env:', Definition.env);
const envVars = Object.values(Definition.env);

console.log(
'Found the following environment variables in the API module:',
envVars
);

const localEnvPath = resolve(backendPath, '../.env');
const localDevConfigPath = resolve(
backendPath,
'../src/configs/dev.json'
);

// Load local .env variables
let localEnvVars = {};
if (existsSync(localEnvPath)) {
localEnvVars = dotenv.parse(readFileSync(localEnvPath, 'utf8'));
}

// Load local dev.json variables
let localDevConfig = {};
if (existsSync(localDevConfigPath)) {
localDevConfig = JSON.parse(
readFileSync(localDevConfigPath, 'utf8')
);
}

const missingEnvVars = envVars.filter(
(envVar) => !localEnvVars[envVar] && !localDevConfig[envVar]
);

logInfo(`Missing environment variables: ${missingEnvVars.join(', ')}`);

if (missingEnvVars.length > 0) {
const { addEnvVars } = await inquirer.prompt([
{
type: 'confirm',
name: 'addEnvVars',
message: `The following environment variables are required: ${missingEnvVars.join(
', '
)}. Do you want to add them now?`,
},
]);

if (addEnvVars) {
const envValues = {};
for (const envVar of missingEnvVars) {
const { value } = await inquirer.prompt([
{
type: 'input',
name: 'value',
message: `Enter value for ${envVar}:`,
},
]);
envValues[envVar] = value;
}

// Add the envValues to the local .env file if it exists
if (existsSync(localEnvPath)) {
const envContent = Object.entries(envValues)
.map(([key, value]) => `${key}=${value}`)
.join('\n');
fs.appendFileSync(localEnvPath, `\n${envContent}`);
}

// Add the envValues to the local dev.json file if it exists
if (existsSync(localDevConfigPath)) {
const updatedDevConfig = {
...localDevConfig,
...envValues,
};
writeFileSync(
localDevConfigPath,
JSON.stringify(updatedDevConfig, null, 2)
);
}
} else {
logInfo("Edit whenever you're able, safe travels friend!");
}
}
}
};

module.exports = { handleEnvVariables };
86 changes: 86 additions & 0 deletions packages/devtools/frigg-cli/environmentVariables.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const { handleEnvVariables } = require('./environmentVariables');
const { logInfo } = require('./logger');
const inquirer = require('inquirer');
const fs = require('fs');
const dotenv = require('dotenv');
const { resolve } = require('node:path');
const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse');

jest.mock('inquirer');
jest.mock('fs');
jest.mock('dotenv');
jest.mock('./logger');
jest.mock('@babel/parser');
jest.mock('@babel/traverse');

describe('handleEnvVariables', () => {
const backendPath = '/mock/backend/path';
const modulePath = '/mock/module/path';

beforeEach(() => {
jest.clearAllMocks();
fs.readFileSync.mockReturnValue(`
const Definition = {
env: {
client_id: process.env.GOOGLE_CALENDAR_CLIENT_ID,
client_secret: process.env.GOOGLE_CALENDAR_CLIENT_SECRET,
redirect_uri: \`\${process.env.REDIRECT_URI}/google-calendar\`,
scope: process.env.GOOGLE_CALENDAR_SCOPE,
}
};
`);
parse.mockReturnValue({});
traverse.default.mockImplementation((ast, visitor) => {
visitor.ObjectProperty({
node: {
key: { name: 'env' },
value: {
properties: [
{ key: { name: 'client_id' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'GOOGLE_CALENDAR_CLIENT_ID' } } },
{ key: { name: 'client_secret' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'GOOGLE_CALENDAR_CLIENT_SECRET' } } },
{ key: { name: 'redirect_uri' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'REDIRECT_URI' } } },
{ key: { name: 'scope' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'GOOGLE_CALENDAR_SCOPE' } } },
]
}
}
});
});
});

it('should identify and handle missing environment variables', async () => {
const localEnvPath = resolve(backendPath, '../.env');
const localDevConfigPath = resolve(backendPath, '../src/configs/dev.json');

fs.existsSync.mockImplementation((path) => path === localEnvPath || path === localDevConfigPath);
dotenv.parse.mockReturnValue({});
fs.readFileSync.mockImplementation((path) => {
if (path === resolve(modulePath, 'index.js')) return 'mock module content';
if (path === localEnvPath) return '';
if (path === localDevConfigPath) return '{}';
return '';
});

inquirer.prompt.mockResolvedValueOnce({ addEnvVars: true })
.mockResolvedValueOnce({ value: 'client_id_value' })
.mockResolvedValueOnce({ value: 'client_secret_value' })
.mockResolvedValueOnce({ value: 'redirect_uri_value' })
.mockResolvedValueOnce({ value: 'scope_value' });

await handleEnvVariables(backendPath, modulePath);

expect(logInfo).toHaveBeenCalledWith('Searching for missing environment variables...');
expect(logInfo).toHaveBeenCalledWith('Missing environment variables: GOOGLE_CALENDAR_CLIENT_ID, GOOGLE_CALENDAR_CLIENT_SECRET, REDIRECT_URI, GOOGLE_CALENDAR_SCOPE');
expect(inquirer.prompt).toHaveBeenCalledTimes(5);
expect(fs.appendFileSync).toHaveBeenCalledWith(localEnvPath, '\nGOOGLE_CALENDAR_CLIENT_ID=client_id_value\nGOOGLE_CALENDAR_CLIENT_SECRET=client_secret_value\nREDIRECT_URI=redirect_uri_value\nGOOGLE_CALENDAR_SCOPE=scope_value');
expect(fs.writeFileSync).toHaveBeenCalledWith(
localDevConfigPath,
JSON.stringify({
GOOGLE_CALENDAR_CLIENT_ID: 'client_id_value',
GOOGLE_CALENDAR_CLIENT_SECRET: 'client_secret_value',
REDIRECT_URI: 'redirect_uri_value',
GOOGLE_CALENDAR_SCOPE: 'scope_value'
}, null, 2)
);
});
});
14 changes: 14 additions & 0 deletions packages/devtools/frigg-cli/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env node

const { Command } = require('commander');
const { installCommand } = require('./installCommand');

const program = new Command();
program
.command('install [apiModuleName]')
.description('Install an API module')
.action(installCommand);

program.parse(process.argv);

module.exports = { installCommand };
Loading
Loading