terraguardian-cli 0.2.1 → 0.2.3

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.
package/README.md CHANGED
@@ -46,3 +46,69 @@ npm unlink
46
46
  This will remove the global symbolic link created for your package.
47
47
 
48
48
  That's it! You've successfully installed, tested, and used your package locally using npm link.
49
+
50
+ ## Build Configuration
51
+
52
+ ### Default Build Settings
53
+
54
+ The CLI uses esbuild to bundle Node.js Lambda functions with the following default settings:
55
+ - **Target**: Node.js 22 (`node22`)
56
+ - **Platform**: Node
57
+ - **Bundle**: Enabled
58
+ - **Minify**: Enabled
59
+
60
+ ### Custom esbuild Configuration
61
+
62
+ You can customize the build process by creating an `esbuild.config.js` file in your project root. The CLI will automatically detect and use your custom configuration.
63
+
64
+ **Example `esbuild.config.js`:**
65
+
66
+ ```javascript
67
+ module.exports = {
68
+ // Bundle all dependencies (default: true)
69
+ bundle: true,
70
+
71
+ // Target platform (default: 'node')
72
+ platform: 'node',
73
+
74
+ // Target Node.js version (default: 'node22')
75
+ target: 'node22',
76
+
77
+ // Minify the output (default: true)
78
+ minify: true,
79
+
80
+ // External dependencies - these will not be bundled
81
+ external: [
82
+ '@aws-sdk/*', // Exclude all AWS SDK v3 packages
83
+ ],
84
+
85
+ // Custom esbuild flags
86
+ customFlags: [
87
+ '--sourcemap', // Enable source maps
88
+ '--keep-names', // Preserve function names
89
+ ],
90
+ };
91
+ ```
92
+
93
+ ### Configuration Options
94
+
95
+ - **bundle** (boolean): Bundle all dependencies into a single file
96
+ - **platform** (string): Target platform (`'node'`, `'browser'`, etc.)
97
+ - **target** (string): Node.js version target (`'node18'`, `'node20'`, `'node22'`, etc.)
98
+ - **minify** (boolean): Minify the output for smaller bundle size
99
+ - **external** (array): List of packages to exclude from bundling (useful for AWS SDK v3)
100
+ - **customFlags** (array): Additional esbuild CLI flags
101
+
102
+ ### Environment Variables
103
+
104
+ You can also override configuration using environment variables:
105
+
106
+ - `NODE_TARGET`: Node.js version (default: `node22`)
107
+ - `ESBUILD_CONFIG_FILE_PATH`: Path to custom config file (default: `esbuild.config.js`)
108
+ - `NODE_BUILD_FILE_NAME`: Output filename (default: `index.js`)
109
+ - `NODE_ENTRY_POINT`: Entry point filename (default: `index.js`)
110
+
111
+ **Example:**
112
+ ```bash
113
+ NODE_TARGET=node20 tg-cli --node-build
114
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terraguardian-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "CLI that helps with projects that are built with terraform and helps with deployments of lambda src code",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -29,6 +29,7 @@
29
29
  "dependencies": {
30
30
  "archiver": "^7.0.0",
31
31
  "chalk": "^4.0.0",
32
+ "glob": "^10.3.10",
32
33
  "inquirer": "^8.2.6",
33
34
  "yargs": "^17.7.2"
34
35
  },
@@ -42,4 +43,4 @@
42
43
  "jest": "^29.7.0",
43
44
  "prettier": "^3.2.5"
44
45
  }
45
- }
46
+ }
@@ -3,6 +3,8 @@ const WEBPACK_FILE_PATH = process.env.WEBPACK_FILE_PATH || 'webpack.config.js';
3
3
  const NODE_LIBARY_TYPE = process.env.NODE_LIBARY_TYPE || 'commonjs2';
4
4
  const DEFAULT_WEBPACK_MODE = process.env.DEFAULT_WEBPACK_MODE || 'production';
5
5
  const NODE_BUILD_FILE_NAME = process.env.NODE_BUILD_FILE_NAME || 'index.js';
6
+ const NODE_TARGET = process.env.NODE_TARGET || 'node22';
7
+ const ESBUILD_CONFIG_FILE_PATH = process.env.ESBUILD_CONFIG_FILE_PATH || 'esbuild.config.js';
6
8
 
7
9
  const NODE_RUNTIME_TF_VAR = 'node_runtime';
8
10
  const GO_RUNTIME_TF_VAR = 'go_runtime';
@@ -11,6 +13,7 @@ const GO_ENTRY_POINT = process.env.GO_ENTRY_POINT || 'main.go';
11
13
 
12
14
  module.exports = {
13
15
  DEFAULT_WEBPACK_MODE,
16
+ ESBUILD_CONFIG_FILE_PATH,
14
17
  GO_ENTRY_POINT,
15
18
  GO_RUNTIME_TF_VAR,
16
19
  LAMBDA_BUILD_DIR,
@@ -18,5 +21,6 @@ module.exports = {
18
21
  NODE_ENTRY_POINT,
19
22
  NODE_LIBARY_TYPE,
20
23
  NODE_RUNTIME_TF_VAR,
24
+ NODE_TARGET,
21
25
  WEBPACK_FILE_PATH,
22
26
  };
@@ -1,25 +1,69 @@
1
1
  const chalk = require('chalk');
2
+ const path = require('path');
2
3
  const { executeCommand } = require('../../common/helpers');
3
4
  const FileHandler = require('../../fs-utils/FileHandler');
4
5
  const { findEntryPoints, getNormalizedLambdaNames } = require('./helpers');
5
6
 
6
7
  const {
8
+ ESBUILD_CONFIG_FILE_PATH,
7
9
  LAMBDA_BUILD_DIR,
8
10
  NODE_BUILD_FILE_NAME,
9
11
  NODE_RUNTIME_TF_VAR,
12
+ NODE_TARGET,
10
13
  } = require('../../common/constants');
11
14
 
15
+ // Load custom esbuild configuration if it exists
16
+ function loadCustomEsbuildConfig() {
17
+ const configPath = path.resolve(process.cwd(), ESBUILD_CONFIG_FILE_PATH);
18
+
19
+ if (FileHandler.doesFileExist(configPath)) {
20
+ try {
21
+ console.log(chalk.blue(`Using custom esbuild config from ${ESBUILD_CONFIG_FILE_PATH}`));
22
+ return require(configPath); // eslint-disable-line import/no-dynamic-require, global-require
23
+ } catch (error) {
24
+ console.log(chalk.yellow(`Warning: Failed to load custom esbuild config: ${error.message}`));
25
+ console.log(chalk.yellow('Falling back to default configuration'));
26
+ return null;
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+
32
+ // Get default esbuild configuration
33
+ function getDefaultEsbuildConfig() {
34
+ return {
35
+ bundle: true,
36
+ platform: 'node',
37
+ target: NODE_TARGET,
38
+ minify: true,
39
+ };
40
+ }
41
+
12
42
  // Create esbuild build command
13
43
  function getEsbuildBuildCommand(lambdaName, lambdaPath, buildOutputDir) {
14
- const buildCommand = [
15
- 'npx esbuild',
16
- lambdaPath, // Input file (entry point)
17
- '--bundle', // Bundle the files together
18
- '--platform=node', // Set platform to node for AWS Lambda
19
- '--target=node20', // Change as needed for your Lambda runtime
20
- `--outfile=${buildOutputDir}/${NODE_BUILD_FILE_NAME}`, // Output file name
21
- '--minify', // Optional: minify the output (you can disable if not needed)
22
- ];
44
+ const customConfig = loadCustomEsbuildConfig();
45
+ const config = customConfig || getDefaultEsbuildConfig();
46
+
47
+ const buildCommand = ['npx esbuild', lambdaPath];
48
+
49
+ // Add configuration flags
50
+ if (config.bundle !== false) buildCommand.push('--bundle');
51
+ if (config.platform) buildCommand.push(`--platform=${config.platform}`);
52
+ if (config.target) buildCommand.push(`--target=${config.target}`);
53
+ if (config.minify) buildCommand.push('--minify');
54
+
55
+ // Add output file
56
+ buildCommand.push(`--outfile=${buildOutputDir}/${NODE_BUILD_FILE_NAME}`);
57
+
58
+ // Add external dependencies if specified
59
+ if (config.external && Array.isArray(config.external)) {
60
+ config.external.forEach((ext) => buildCommand.push(`--external:${ext}`));
61
+ }
62
+
63
+ // Add any custom flags
64
+ if (config.customFlags && Array.isArray(config.customFlags)) {
65
+ buildCommand.push(...config.customFlags);
66
+ }
23
67
 
24
68
  return buildCommand.join(' ');
25
69
  }
@@ -48,9 +92,10 @@ async function buildNodeLambdas() {
48
92
  }
49
93
  }
50
94
 
51
- lambdaPaths.forEach((lambdaPath, index) => {
52
- buildNodeLambda(normalizedLambdaNames[index], lambdaPath);
53
- });
95
+ // Build all lambdas and await each one
96
+ for (let index = 0; index < lambdaPaths.length; index++) {
97
+ await buildNodeLambda(normalizedLambdaNames[index], lambdaPaths[index]);
98
+ }
54
99
  }
55
100
 
56
101
  module.exports = {
@@ -97,4 +97,5 @@ async function zipAndMoveFolders(sourceFolder, destinationFolder) {
97
97
 
98
98
  module.exports = {
99
99
  zipAndMoveFolders,
100
+ zipFolder,
100
101
  };
@@ -1,8 +1,71 @@
1
+ const { execSync } = require('child_process');
2
+ const inquirer = require('inquirer');
1
3
  const { executeCommand } = require('../../common/helpers');
2
4
 
3
- // uses the aws cli to deploy a lambda to AWS
4
- function deployLambda(lambdaName, zipFileName) {
5
- const deployCommand = `aws lambda update-function-code --function-name ${lambdaName} --zip-file fileb://dist/${zipFileName}.zip 1> /dev/null`;
5
+ function getGitRepoName() {
6
+ const repoPath = execSync('git rev-parse --show-toplevel').toString().trim();
7
+ return repoPath.split('/').pop();
8
+ }
9
+
10
+ function getGitBranch() {
11
+ return execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
12
+ }
13
+
14
+ function isTagBranch() {
15
+ const result = execSync('git describe --exact-match --tags HEAD || true').toString().trim();
16
+ return result.length > 0;
17
+ }
18
+
19
+ function normalizeProjectName(name) {
20
+ return name.toLowerCase();
21
+ }
22
+
23
+ function shortenProjectName(name) {
24
+ return name.replace(/[-_]/g, '').substring(0, 10);
25
+ }
26
+
27
+ function cleanBranch(branch) {
28
+ return branch.toLowerCase().replace(/[^a-zA-Z0-9]/g, '');
29
+ }
30
+
31
+ function getEnvironment(branch) {
32
+ if (branch === 'main' || branch === 'master') {
33
+ return 'test';
34
+ }
35
+ if (isTagBranch()) {
36
+ return 'prod';
37
+ }
38
+ return cleanBranch(branch);
39
+ }
40
+
41
+ // Deploys the zip file using the lambda key (e.g., 'lambda_get_products')
42
+ async function deployLambda(lambdaKey, zipFileName) {
43
+ const projectName = getGitRepoName();
44
+ const branchName = getGitBranch();
45
+ const shortName = shortenProjectName(normalizeProjectName(projectName));
46
+ const environment = getEnvironment(branchName);
47
+ const formattedKey = lambdaKey.replace(/_/g, '-');
48
+
49
+ const fullLambdaName = `${shortName}-${environment}-${formattedKey}`;
50
+
51
+ console.log(`\nPreparing to deploy: ${fullLambdaName}`);
52
+ const { confirmDeploy } = await inquirer.prompt([
53
+ {
54
+ type: 'confirm',
55
+ name: 'confirmDeploy',
56
+ message: 'Are you sure you want to deploy this Lambda?',
57
+ default: false,
58
+ },
59
+ ]);
60
+
61
+ if (!confirmDeploy) {
62
+ console.log('Deployment cancelled.');
63
+ return undefined;
64
+ }
65
+
66
+ const deployCommand = `aws lambda update-function-code --function-name ${fullLambdaName} --zip-file fileb://dist/${zipFileName}.zip 1> /dev/null`;
67
+
68
+ console.log(`Deploying: ${fullLambdaName}...`);
6
69
  return executeCommand(deployCommand);
7
70
  }
8
71
 
@@ -0,0 +1,19 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function extractBranchPrefix(tfDir) {
5
+ const tfFiles = fs.readdirSync(tfDir).filter((file) => file.endsWith('.tf'));
6
+
7
+ // eslint-disable-next-line no-restricted-syntax
8
+ for (const file of tfFiles) {
9
+ const content = fs.readFileSync(path.join(tfDir, file), 'utf-8');
10
+ const match = content.match(/\s*branch_prefix\s*=\s*"(.*?)"/);
11
+ if (match && match[1]) {
12
+ return match[1];
13
+ }
14
+ }
15
+
16
+ throw new Error('Could not find branch_prefix in any .tf file.');
17
+ }
18
+
19
+ module.exports = extractBranchPrefix;
@@ -0,0 +1,11 @@
1
+ // helpers/file.js
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function writeFile(filePath, content) {
7
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
8
+ fs.writeFileSync(filePath, content);
9
+ }
10
+
11
+ module.exports = { writeFile };
@@ -0,0 +1,17 @@
1
+ // helpers/format.js
2
+
3
+ function toSnakeCase(name) {
4
+ return name.trim().toLowerCase().replace(/[-\s]/g, '_');
5
+ }
6
+
7
+ function toPascalCase(name) {
8
+ return name
9
+ .replace(/[-_]+/g, ' ')
10
+ .replace(/\s(.)/g, (_, group1) => group1.toUpperCase())
11
+ .replace(/^(.)/, (_, group1) => group1.toUpperCase());
12
+ }
13
+
14
+ module.exports = {
15
+ toSnakeCase,
16
+ toPascalCase,
17
+ };
@@ -0,0 +1,29 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const glob = require('glob');
4
+
5
+ async function scanServiceResources(serviceKey, serviceConfig, modulesDir = 'modules') {
6
+ const basePath = path.join(process.cwd(), modulesDir, serviceKey);
7
+
8
+ if (!fs.existsSync(basePath)) {
9
+ return []; // No such module
10
+ }
11
+
12
+ const files = glob.sync(serviceConfig.scanPattern, {
13
+ cwd: basePath,
14
+ absolute: true,
15
+ });
16
+
17
+ const resourceNames = new Set();
18
+
19
+ // eslint-disable-next-line no-restricted-syntax
20
+ for (const file of files) {
21
+ const contents = fs.readFileSync(file, 'utf8');
22
+ const names = serviceConfig.extractResourceNames(contents);
23
+ names.forEach((n) => resourceNames.add(n));
24
+ }
25
+
26
+ return [...resourceNames];
27
+ }
28
+
29
+ module.exports = { scanServiceResources };
@@ -0,0 +1,10 @@
1
+ // scaffold.js
2
+
3
+ const Scaffolder = require('./Scaffolder');
4
+
5
+ async function scaffold() {
6
+ const scaffolder = new Scaffolder();
7
+ await scaffolder.promptAndRun();
8
+ }
9
+
10
+ module.exports = { scaffold };
@@ -0,0 +1,49 @@
1
+ // Scaffolder.js
2
+
3
+ const path = require('path');
4
+ const inquirer = require('inquirer');
5
+ const chalk = require('chalk');
6
+
7
+ const LambdaScaffolder = require('./services/Lambda');
8
+ const extractBranchPrefix = require('./helpers/extractBranchPrefix');
9
+
10
+ const SERVICES = {
11
+ lambda: LambdaScaffolder,
12
+ };
13
+
14
+ class Scaffolder {
15
+ // eslint-disable-next-line class-methods-use-this
16
+ async promptAndRun() {
17
+ const { service } = await inquirer.prompt([
18
+ {
19
+ type: 'list',
20
+ name: 'service',
21
+ message: 'What service do you want to scaffold?',
22
+ choices: Object.keys(SERVICES),
23
+ },
24
+ ]);
25
+
26
+ const { name } = await inquirer.prompt([
27
+ {
28
+ type: 'input',
29
+ name: 'name',
30
+ message: `Enter a name for the ${service} (e.g., get-user):`,
31
+ validate: (input) => !!input.trim() || 'Name cannot be empty.',
32
+ },
33
+ ]);
34
+
35
+ let branchPrefix = 'YOUR_PREFIX_HERE';
36
+ const terraformProjectPath = path.join(process.cwd(), 'modules');
37
+ try {
38
+ branchPrefix = extractBranchPrefix(terraformProjectPath);
39
+ } catch (e) {
40
+ console.warn(chalk.yellow('Warning:'), e.message);
41
+ }
42
+
43
+ const ServiceScaffolder = SERVICES[service];
44
+ const scaffolder = new ServiceScaffolder(name, branchPrefix);
45
+ await scaffolder.run();
46
+ }
47
+ }
48
+
49
+ module.exports = Scaffolder;
@@ -0,0 +1,83 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const inquirer = require('inquirer');
5
+ const { toSnakeCase } = require('../helpers/format');
6
+ const { writeFile } = require('../helpers/file');
7
+ const mainTfTemplate = require('../templates/lambda/main.tf');
8
+ const iamTfTemplate = require('../templates/lambda/iam.tf');
9
+ const handlerTemplate = require('../templates/lambda/src.js');
10
+
11
+ const ServiceRegistry = require('./ServiceRegistry');
12
+ const { scanServiceResources } = require('../helpers/scanServiceResources');
13
+
14
+ class LambdaScaffolder {
15
+ constructor(name, branchPrefix) {
16
+ this.originalName = name; // e.g., get-user
17
+ this.snakeName = toSnakeCase(name); // e.g., get_user
18
+ this.folderName = `lambda_${this.snakeName}`; // e.g., lambda_get_user
19
+ this.basePath = path.join(process.cwd(), 'modules/lambda', this.folderName);
20
+ this.branchPrefix = branchPrefix;
21
+ this.shortName = `l${name.split('-').join('')}`;
22
+ }
23
+
24
+ // eslint-disable-next-line class-methods-use-this
25
+ async promptResourceConnections() {
26
+ const choices = [];
27
+
28
+ // eslint-disable-next-line no-restricted-syntax
29
+ for (const [serviceKey, config] of Object.entries(ServiceRegistry)) {
30
+ const resources = await scanServiceResources(serviceKey, config);
31
+ if (resources.length > 0) {
32
+ choices.push(new inquirer.Separator(`--- ${config.displayName} ---`));
33
+ // Sort resources alphabetically within each service group
34
+ const sortedResources = resources.sort((a, b) => a.localeCompare(b));
35
+ sortedResources.forEach((r) => choices.push({
36
+ name: `${config.displayName}: ${r}`,
37
+ value: { serviceKey, resourceName: r },
38
+ }));
39
+ }
40
+ }
41
+
42
+ if (choices.length === 0) {
43
+ console.log(chalk.yellow('⚠️ No connectable resources found.'));
44
+ return [];
45
+ }
46
+
47
+ const { selectedResources } = await inquirer.prompt([
48
+ {
49
+ type: 'checkbox',
50
+ name: 'selectedResources',
51
+ message: 'Which resources should this Lambda connect to?',
52
+ choices,
53
+ },
54
+ ]);
55
+
56
+ return selectedResources;
57
+ }
58
+
59
+ async run() {
60
+ if (fs.existsSync(this.basePath)) {
61
+ console.log(chalk.red(`❌ Folder already exists: ${this.basePath}`));
62
+ return;
63
+ }
64
+
65
+ console.log(chalk.blue(`📦 Creating Lambda: ${this.folderName}`));
66
+
67
+ // ✅ Create base folders (recursive)
68
+ fs.mkdirSync(path.join(this.basePath, 'src'), { recursive: true });
69
+
70
+ const connectedResources = await this.promptResourceConnections();
71
+
72
+ writeFile(path.join(this.basePath, 'main.tf'), mainTfTemplate(this));
73
+ writeFile(
74
+ path.join(this.basePath, 'iam.tf'),
75
+ iamTfTemplate(this, connectedResources),
76
+ );
77
+ writeFile(path.join(this.basePath, 'src', 'index.js'), handlerTemplate(this));
78
+
79
+ console.log(chalk.green(`✅ Lambda scaffolded at ${this.basePath}`));
80
+ }
81
+ }
82
+
83
+ module.exports = LambdaScaffolder;
@@ -0,0 +1,50 @@
1
+ module.exports = {
2
+ dynamodb: {
3
+ displayName: 'DynamoDB',
4
+ scanPattern: '**/*.tf', // Scan for any .tf file
5
+ extractResourceNames: (fileContents) => {
6
+ const regex = /resource\s+"aws_dynamodb_table"\s+"([^"]+)"/g;
7
+ const matches = [...fileContents.matchAll(regex)];
8
+ return matches.map((m) => m[1]); // e.g., ["users_table", "orders_table"]
9
+ },
10
+ getPolicy: (resourceName) => `
11
+ {
12
+ "Action": [
13
+ "dynamodb:GetItem",
14
+ "dynamodb:PutItem",
15
+ "dynamodb:UpdateItem",
16
+ "dynamodb:DeleteItem",
17
+ "dynamodb:Query",
18
+ "dynamodb:Scan"
19
+ ],
20
+ "Effect": "Allow",
21
+ "Resource": "\${aws_dynamodb_table.${resourceName}.arn}"
22
+ }
23
+ `,
24
+ },
25
+ s3: {
26
+ displayName: 'S3 Bucket',
27
+ scanPattern: '**/*.tf',
28
+ extractResourceNames: (fileContents) => {
29
+ const regex = /resource\s+"aws_s3_bucket"\s+"([^"]+)"/g;
30
+ const matches = [...fileContents.matchAll(regex)];
31
+ return matches.map((m) => m[1]);
32
+ },
33
+ getPolicy: (resourceName) => `
34
+ {
35
+ "Action": [
36
+ "s3:GetObject",
37
+ "s3:PutObject",
38
+ "s3:DeleteObject",
39
+ "s3:ListBucket"
40
+ ],
41
+ "Effect": "Allow",
42
+ "Resource": [
43
+ "\${aws_s3_bucket.${resourceName}.arn}",
44
+ "\${aws_s3_bucket.${resourceName}.arn}/*"
45
+ ]
46
+ }
47
+ `,
48
+ },
49
+ // Add more services (SQS, API Gateway, etc.) later
50
+ };
@@ -0,0 +1,55 @@
1
+ const ServiceRegistry = require('../../services/ServiceRegistry');
2
+
3
+ module.exports = ({ originalName, snakeName }, connectedResources = []) => `
4
+ resource "aws_iam_role" "lambda_${snakeName}" {
5
+ name = "\${local.branch_prefix}-lambda-${originalName}-role"
6
+
7
+ assume_role_policy = jsonencode({
8
+ Version = "2012-10-17"
9
+ Statement = [
10
+ {
11
+ Action = "sts:AssumeRole"
12
+ Principal = {
13
+ Service = "lambda.amazonaws.com"
14
+ }
15
+ Effect = "Allow"
16
+ Sid = ""
17
+ },
18
+ ]
19
+ })
20
+
21
+ tags = local.tags
22
+ }
23
+
24
+ resource "aws_iam_role_policy_attachment" "lambda_${snakeName}_common_permissions" {
25
+ role = aws_iam_role.lambda_${snakeName}.name
26
+ policy_arn = aws_iam_policy.policy_lambda_common_permissions.arn
27
+ }
28
+
29
+ resource "aws_iam_role_policy_attachment" "lambda_${snakeName}_basic_execution" {
30
+ role = aws_iam_role.lambda_${snakeName}.name
31
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
32
+ }
33
+
34
+ ${connectedResources.length > 0
35
+ ? `
36
+ resource "aws_iam_policy" "lambda_${snakeName}_connected_resources" {
37
+ name = "\${local.branch_prefix}-lambda-${originalName}-resources"
38
+ policy = jsonencode({
39
+ Version = "2012-10-17",
40
+ Statement = [
41
+ ${connectedResources
42
+ .map((r) => ServiceRegistry[r.serviceKey].getPolicy(r.resourceName))
43
+ .join(',\n')}
44
+ ]
45
+ })
46
+ }
47
+
48
+ resource "aws_iam_role_policy_attachment" "lambda_${snakeName}_connected_resources_attach" {
49
+ role = aws_iam_role.lambda_${snakeName}.name
50
+ policy_arn = aws_iam_policy.lambda_${snakeName}_connected_resources.arn
51
+ }
52
+ `.trim()
53
+ : ''
54
+ }
55
+ `.trim();
@@ -0,0 +1,26 @@
1
+ module.exports = ({ originalName, snakeName, shortName }) => `
2
+ locals {
3
+ ${shortName} = "lambda-${originalName}"
4
+ ${shortName}_function_name = "\${local.branch_prefix}-\${local.${shortName}}"
5
+ path_to_${shortName}_zip = local.zip_info.lambda_${snakeName}.path
6
+ }
7
+
8
+ resource "aws_lambda_function" "lambda_${snakeName}" {
9
+ function_name = local.${shortName}_function_name
10
+ role = aws_iam_role.lambda_${snakeName}.arn
11
+ handler = var.node_handler
12
+ runtime = var.node_runtime
13
+ filename = local.path_to_${shortName}_zip
14
+ source_code_hash = filebase64sha256(local.path_to_${shortName}_zip)
15
+
16
+ environment {
17
+ variables = {
18
+ ENVIRONMENT = var.branch == "prod" ? "prod" : "dev"
19
+ }
20
+ }
21
+ }
22
+
23
+ resource "aws_cloudwatch_log_group" "lambda_${snakeName}_log_group" {
24
+ name = "/aws/lambda/\${local.${shortName}_function_name}"
25
+ retention_in_days = 1
26
+ }`;
@@ -0,0 +1,10 @@
1
+ module.exports = ({ originalName }) => `
2
+ exports.handler = async (event) => {
3
+ console.log("Received event:", JSON.stringify(event, null, 2));
4
+
5
+ return {
6
+ statusCode: 200,
7
+ body: JSON.stringify({ message: "Hello from ${originalName}!" }),
8
+ };
9
+ };
10
+ `.trim();
package/src/index.js CHANGED
@@ -6,19 +6,16 @@ const path = require('path');
6
6
  const fs = require('fs');
7
7
  const inquirer = require('inquirer');
8
8
 
9
- const { zipAndMoveFolders } = require('./features/build/zip');
10
- const { buildNodeLambdas } = require('./features/build/node-build');
9
+ const { zipFolder, zipAndMoveFolders } = require('./features/build/zip');
10
+ const { buildNodeLambdas, buildNodeLambda } = require('./features/build/node-build');
11
11
  const { deployLambda } = require('./features/deploy/lambda');
12
12
  const { validateTerraformCode } = require('./features/validate-terraform/validate');
13
-
14
- // A function that gets the names of the zip files in the dist folder and filters out the extension
15
- function getZipNames(dirPath) {
16
- const files = fs.readdirSync(dirPath);
17
- const zipFiles = files.filter((file) => path.extname(file) === '.zip').map((file) => path.basename(file, '.zip'));
18
- return zipFiles;
19
- }
13
+ const { findEntryPoints, getNormalizedLambdaNames } = require('./features/build/helpers');
14
+ const { NODE_RUNTIME_TF_VAR } = require('./common/constants');
15
+ const { scaffold } = require('./features/scaffold');
20
16
 
21
17
  const usage = chalk.keyword('violet')('\nUsage: tg-cli [<command> [, options]]');
18
+
22
19
  const argv = yargs
23
20
  .version('1.0.0')
24
21
  .usage(usage)
@@ -34,61 +31,90 @@ const argv = yargs
34
31
  })
35
32
  .option('d', {
36
33
  alias: 'deploy',
37
- describe: 'deploy a lambda',
34
+ describe: 'Deploy a Lambda',
38
35
  type: 'boolean',
39
36
  })
40
37
  .option('v', {
41
38
  alias: 'validate-tf',
42
- describe: 'validate terraform code even if it is nested',
39
+ describe: 'Validate Terraform code even if nested',
40
+ type: 'boolean',
41
+ })
42
+ .option('s', {
43
+ alias: 'scaffold',
44
+ describe: 'Scaffold boilerplate code for a service',
43
45
  type: 'boolean',
44
46
  })
45
47
  .help(true)
46
48
  .alias('h', 'help').argv;
47
49
 
48
50
  async function executeUserCommand(userCommands) {
49
- if (userCommands.z || userCommands.zip) {
50
- // Write the result to a JSON file
51
- console.log('Zipping folders...');
52
- const zipDetails = await zipAndMoveFolders('build', 'dist');
53
-
54
- // Write the result to a JSON file
55
- const resultFilePath = path.join(process.cwd(), 'zip-details.json');
56
- await fs.promises.writeFile(
57
- resultFilePath,
58
- JSON.stringify(zipDetails, null, 2),
59
- 'utf-8',
60
- );
61
-
62
- console.log(`Zipping complete. Details saved to ${resultFilePath}`);
63
- } else if (userCommands.d || userCommands.deploy) {
64
- console.log('deploying');
65
- // get all the zip folder names from the dist folder
66
- const lambdasOptions = getZipNames('dist');
67
- const response = await inquirer.prompt([
68
- {
69
- type: 'list',
70
- name: 'deployOptions',
71
- message: 'Which Lambda would you like to deploy?',
72
- choices: lambdasOptions,
73
- },
74
- ]);
75
- // get name of the lambda to deploy
76
- const res = await inquirer.prompt([
77
- {
78
- type: 'input',
79
- name: 'nameOfLambda',
80
- message: 'What is the name of the lambda?',
81
- },
82
- ]);
83
- await deployLambda(res.nameOfLambda, response.deployOptions);
84
- } else if (userCommands.N || userCommands['node-build']) {
85
- console.log('building node lambda');
86
- await buildNodeLambdas();
87
- } else if (userCommands.v || userCommands['validate-tf']) {
88
- console.log('Validating terraform code');
89
- await validateTerraformCode();
90
- } else {
91
- yargs.showHelp();
51
+ try {
52
+ if (userCommands.z || userCommands.zip) {
53
+ console.log(chalk.blue('🔄 Zipping folders...'));
54
+ const zipDetails = await zipAndMoveFolders('build', 'dist');
55
+
56
+ const resultFilePath = path.join(process.cwd(), 'zip-details.json');
57
+ await fs.promises.writeFile(resultFilePath, JSON.stringify(zipDetails, null, 2), 'utf-8');
58
+ console.log(chalk.green(`✅ Zipping complete. Details saved to ${resultFilePath}`));
59
+ } else if (userCommands.d || userCommands.deploy) {
60
+ console.log(chalk.blue('🚀 Deploying Lambda...'));
61
+ const lambdaPaths = findEntryPoints(NODE_RUNTIME_TF_VAR);
62
+ const normalizedLambdaNames = getNormalizedLambdaNames(lambdaPaths);
63
+
64
+ // Create a map of lambda names to their paths and sort alphabetically
65
+ const lambdaMap = normalizedLambdaNames.map((name, index) => ({
66
+ name,
67
+ path: lambdaPaths[index],
68
+ })).sort((a, b) => a.name.localeCompare(b.name));
69
+
70
+ const sortedNames = lambdaMap.map((item) => item.name);
71
+
72
+ const { deployOptions } = await inquirer.prompt([
73
+ {
74
+ type: 'list',
75
+ name: 'deployOptions',
76
+ message: 'Which Lambda would you like to deploy?',
77
+ choices: sortedNames,
78
+ },
79
+ ]);
80
+
81
+ const selectedLambda = lambdaMap.find((item) => item.name === deployOptions);
82
+ const lambdaPath = selectedLambda.path;
83
+
84
+ // Build the selected Lambda
85
+ const builtFile = await buildNodeLambda(deployOptions, lambdaPath);
86
+
87
+ // Ensure dist folder exists
88
+ const distDir = path.join(process.cwd(), 'dist');
89
+ if (!fs.existsSync(distDir)) {
90
+ fs.mkdirSync(distDir);
91
+ }
92
+
93
+ const zipFilePath = `dist/${deployOptions}.zip`;
94
+ if (fs.existsSync(zipFilePath)) {
95
+ fs.unlinkSync(zipFilePath);
96
+ }
97
+
98
+ await zipFolder(path.dirname(builtFile), zipFilePath);
99
+ await deployLambda(deployOptions, deployOptions);
100
+
101
+ console.log(chalk.green(`✅ Lambda ${deployOptions} deployed successfully`));
102
+ } else if (userCommands.N || userCommands['node-build']) {
103
+ console.log(chalk.blue('🔨 Building Node Lambdas...'));
104
+ await buildNodeLambdas();
105
+ } else if (userCommands.s || userCommands.scaffold) {
106
+ console.log(chalk.blue('📦 Scaffolding new service...'));
107
+ await scaffold();
108
+ } else if (userCommands.v || userCommands['validate-tf']) {
109
+ console.log(chalk.blue('📝 Validating Terraform code...'));
110
+ await validateTerraformCode();
111
+ } else {
112
+ yargs.showHelp();
113
+ }
114
+ } catch (err) {
115
+ console.error(chalk.red(`❌ Error: ${err.message}`));
116
+ console.error(err.stack);
117
+ process.exit(1);
92
118
  }
93
119
  }
94
120