envilder 0.1.5 → 0.2.1

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.
@@ -0,0 +1,40 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5
+
6
+ version: 2
7
+ updates:
8
+ - package-ecosystem: npm
9
+ directory: "/"
10
+ pull-request-branch-name:
11
+ separator: "/"
12
+ schedule:
13
+ interval: monthly
14
+ day: monday
15
+ time: "10:00"
16
+ timezone: Europe/Madrid
17
+ labels:
18
+ - "npm"
19
+ - "dependencies"
20
+ reviewers:
21
+ - macalbert
22
+ assignees:
23
+ - macalbert
24
+
25
+ - package-ecosystem: github-actions
26
+ directory: "/"
27
+ pull-request-branch-name:
28
+ separator: "/"
29
+ schedule:
30
+ interval: monthly
31
+ day: monday
32
+ time: "10:00"
33
+ timezone: Europe/Madrid
34
+ labels:
35
+ - "github-actions"
36
+ - "dependencies"
37
+ reviewers:
38
+ - macalbert
39
+ assignees:
40
+ - macalbert
@@ -0,0 +1,20 @@
1
+ # Description
2
+
3
+ _Describe the problem or feature in addition to a link to the issues._
4
+
5
+ ## Approach
6
+
7
+ _How does this change address the problem?_
8
+
9
+ ## Open Questions and Pre-Merge TODOs
10
+
11
+ - [ ] Use github checklists. When solved, check the box and explain the answer.
12
+
13
+ ## Learning
14
+
15
+ _Describe the research stage_
16
+ _Links to blog posts, patterns, libraries or addons used to solve this problem_
17
+
18
+ ## Blog Posts
19
+
20
+ - [How to Pull Request](https://github.com/flexyford/pull-request) Github Repo with Learning focused Pull Request Template.
@@ -0,0 +1,49 @@
1
+ name: CodeQL
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ pull_request:
7
+ branches: [main]
8
+ paths:
9
+ - ".github/workflows/codeql-analysis.yml"
10
+
11
+ push:
12
+ branches: [main]
13
+ paths:
14
+ - ".github/workflows/codeql-analysis.yml"
15
+ - "src/**"
16
+ - "test/**"
17
+
18
+ concurrency:
19
+ group: ${{ github.workflow }}-${{ github.head_ref || github.sha }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ analyze:
24
+ strategy:
25
+ fail-fast: false
26
+ matrix:
27
+ language: ["javascript"]
28
+
29
+ permissions:
30
+ security-events: write
31
+
32
+ runs-on: ubuntu-22.04
33
+
34
+ steps:
35
+ - name: Checkout Repository
36
+ uses: actions/checkout@v4
37
+ with:
38
+ lfs: true
39
+
40
+ - name: Initialize CodeQL
41
+ uses: github/codeql-action/init@v3
42
+ with:
43
+ languages: ${{ matrix.language }}
44
+
45
+ - name: Autobuild
46
+ uses: github/codeql-action/autobuild@v3
47
+
48
+ - name: Perform CodeQL Analysis
49
+ uses: github/codeql-action/analyze@v3
@@ -0,0 +1,73 @@
1
+ name: 🔦 code-coverage
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ pull_request:
7
+ paths:
8
+ - ".github/workflows/coverage-report.yml"
9
+
10
+ push:
11
+ branches:
12
+ - "main"
13
+ paths:
14
+ - ".github/workflows/coverage-report.yml"
15
+ - "src/**"
16
+ - "test/**"
17
+
18
+ concurrency:
19
+ group: "pages"
20
+ cancel-in-progress: false
21
+
22
+ permissions:
23
+ contents: read
24
+ pages: write
25
+ id-token: write
26
+
27
+ jobs:
28
+ build-coverage:
29
+ environment:
30
+ name: github-pages
31
+ url: ${{ steps.deployment.outputs.page_url }}
32
+ runs-on: ubuntu-latest
33
+ if: ${{ !github.event.pull_request.draft }}
34
+ steps:
35
+ - name: Check if PR is created by Dependabot
36
+ id: dependabot-check
37
+ run: |
38
+ if [[ "${{ github.actor }}" == *dependabot* ]]; then
39
+ echo "is_dependabot=true" >> "$GITHUB_OUTPUT"
40
+ else
41
+ echo "is_dependabot=false" >> "$GITHUB_OUTPUT"
42
+ fi
43
+
44
+ - name: Check if the workflow should run
45
+ if: ${{ steps.dependabot-check.outputs.is_dependabot == 'false' }}
46
+ run: echo "The workflow is allowed to run for this PR"
47
+
48
+ - name: 🧲 Checkout
49
+ uses: actions/checkout@v4
50
+
51
+ - name: 🛠️ Setup Node.js with Cache
52
+ uses: actions/setup-node@v4
53
+ with:
54
+ node-version: "20.x"
55
+ cache: "yarn"
56
+
57
+ - name: 📦 Install dependencies
58
+ run: yarn install
59
+
60
+ - name: 🔥 Run tests and collect coverage
61
+ run: yarn test
62
+
63
+ - name: Setup Pages
64
+ uses: actions/configure-pages@v5
65
+
66
+ - name: Upload artifact
67
+ uses: actions/upload-pages-artifact@v3
68
+ with:
69
+ path: "./coverage"
70
+
71
+ - name: Deploy to GitHub Pages
72
+ id: deployment
73
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,48 @@
1
+ name: 🌱 unit-tests
2
+
3
+ on:
4
+ workflow_dispatch: {}
5
+
6
+ pull_request:
7
+ branches:
8
+ - "*"
9
+ types:
10
+ - opened
11
+ - reopened
12
+ - synchronize
13
+ - ready_for_review
14
+ paths:
15
+ - ".github/workflows/unit-tests.yml"
16
+ - "src/**"
17
+
18
+ concurrency:
19
+ group: ${{ github.workflow }}-${{ github.head_ref || github.sha }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ envilder-test:
24
+ runs-on: ubuntu-24.04
25
+ if: ${{ !github.event.pull_request.draft }}
26
+ timeout-minutes: 30
27
+
28
+ steps:
29
+ - name: 🚀 ♂️ Checkout
30
+ uses: actions/checkout@v4
31
+
32
+ - name: 🛠️ Setup Node.js with Cache
33
+ uses: actions/setup-node@v4
34
+ with:
35
+ node-version: '20.x'
36
+ cache: 'yarn'
37
+
38
+ - name: 📦 Install packages
39
+ run: yarn install
40
+
41
+ - name: 🔍 Run code quality checker
42
+ run: yarn lint
43
+
44
+ - name: 🚧 Build
45
+ run: yarn build
46
+
47
+ - name: 🚴‍♀️ Run unit tests
48
+ run: yarn test
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🌱 Envilder
2
2
 
3
- **Envilder** is a CLI tool designed to generate `.env` files from AWS SSM parameters. This is useful for managing environment variables securely in your projects without exposing sensitive information in your codebase.
3
+ `Envilder` is a CLI tool to manage AWS SSM Parameter Store parameters and automatically generate the required `.env` file. This tool simplifies environment variable management for projects, avoiding manual updates and ensuring consistency across environments.
4
4
 
5
5
  ## ✨ Features
6
6
 
@@ -9,6 +9,32 @@
9
9
  - 🛡️ Handles both encrypted and unencrypted SSM parameters.
10
10
  - 🪶 Lightweight and simple to use.
11
11
 
12
+ ## Prerequisites
13
+ Before using `Envilder`, ensure that you have the AWS CLI installed and properly configured on your local machine. This configuration is required for `Envilder` to access and manage parameters in AWS SSM.
14
+
15
+ ### AWS CLI Installation & Configuration
16
+ 1. Install the AWS CLI by following the instructions [here](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).
17
+ 2. After installation, configure the AWS CLI using the following command:
18
+
19
+ ```bash
20
+ aws configure
21
+ ```
22
+
23
+ You'll be prompted to provide:
24
+ - AWS Access Key ID
25
+ - AWS Secret Access Key
26
+ - Default region name (e.g., `us-east-1`)
27
+ - Default output format (e.g., `json`)
28
+
29
+ Make sure that the AWS credentials you're using have the appropriate permissions to access the SSM Parameter Store in your AWS account.
30
+
31
+ ## Installation
32
+ You can install `Envilder` globally using yarn. This will allow you to use the `envilder` command from any directory on your system.
33
+
34
+ ```bash
35
+ yarn global add envilder
36
+ ```
37
+
12
38
  ## 📦 Installation
13
39
 
14
40
  You can install **envilder** globally or locally using npm:
@@ -28,6 +28,6 @@ export function cliRunner() {
28
28
  });
29
29
  }
30
30
  cliRunner().catch((error) => {
31
- console.error('Error in CLI Runner:', error);
31
+ console.error('🚨 Uh-oh! Looks like Mario fell into the wrong pipe! 🍄💥');
32
32
  });
33
33
  //# sourceMappingURL=cliRunner.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cliRunner.js","sourceRoot":"","sources":["../../src/cli/cliRunner.ts"],"names":[],"mappings":";;;;;;;;;;AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,UAAgB,SAAS;;QAC7B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;QAE9B,OAAO;aACJ,IAAI,CAAC,UAAU,CAAC;aAChB,WAAW,CAAC,2DAA2D,CAAC;aACxE,OAAO,CAAC,OAAO,CAAC;aAChB,cAAc,CAAC,cAAc,EAAE,yDAAyD,CAAC;aACzF,cAAc,CAAC,kBAAkB,EAAE,uCAAuC,CAAC,CAAC;QAE/E,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAE/B,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACrE,CAAC;QAED,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;CAAA;AAED,SAAS,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IAC1B,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"cliRunner.js","sourceRoot":"","sources":["../../src/cli/cliRunner.ts"],"names":[],"mappings":";;;;;;;;;;AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,UAAgB,SAAS;;QAC7B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;QAE9B,OAAO;aACJ,IAAI,CAAC,UAAU,CAAC;aAChB,WAAW,CAAC,2DAA2D,CAAC;aACxE,OAAO,CAAC,OAAO,CAAC;aAChB,cAAc,CAAC,cAAc,EAAE,yDAAyD,CAAC;aACzF,cAAc,CAAC,kBAAkB,EAAE,uCAAuC,CAAC,CAAC;QAE/E,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAE/B,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACrE,CAAC;QAED,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;CAAA;AAED,SAAS,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IAC1B,OAAO,CAAC,KAAK,CAAC,2DAA2D,CAAC,CAAC;AAC7E,CAAC,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,wBAAsB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,iBA8B7D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,wBAAsB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,iBAQ7D"}
package/lib/index.js CHANGED
@@ -9,36 +9,78 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import * as fs from 'node:fs';
11
11
  import { GetParameterCommand, SSM } from '@aws-sdk/client-ssm';
12
+ import * as dotenv from 'dotenv';
12
13
  const ssm = new SSM({});
13
14
  export function run(mapPath, envFilePath) {
14
15
  return __awaiter(this, void 0, void 0, function* () {
15
- const content = fs.readFileSync(mapPath, 'utf-8');
16
- const paramMap = JSON.parse(content);
17
- const envContent = [];
18
- console.log('Fetching parameters...');
16
+ const paramMap = loadParamMap(mapPath);
17
+ const existingEnvVariables = loadExistingEnvVariables(envFilePath);
18
+ const updatedEnvVariables = yield fetchAndUpdateEnvVariables(paramMap, existingEnvVariables);
19
+ writeEnvFile(envFilePath, updatedEnvVariables);
20
+ console.log(`Environment File generated at '${envFilePath}'`);
21
+ });
22
+ }
23
+ function loadParamMap(mapPath) {
24
+ const content = fs.readFileSync(mapPath, 'utf-8');
25
+ try {
26
+ return JSON.parse(content);
27
+ }
28
+ catch (error) {
29
+ console.error(`Error parsing JSON from ${mapPath}`);
30
+ throw new Error(`Invalid JSON in parameter map file: ${mapPath}`);
31
+ }
32
+ }
33
+ function loadExistingEnvVariables(envFilePath) {
34
+ const envVariables = {};
35
+ if (!fs.existsSync(envFilePath))
36
+ return envVariables;
37
+ const existingEnvContent = fs.readFileSync(envFilePath, 'utf-8');
38
+ const parsedEnv = dotenv.parse(existingEnvContent);
39
+ Object.assign(envVariables, parsedEnv);
40
+ return envVariables;
41
+ }
42
+ function fetchAndUpdateEnvVariables(paramMap, existingEnvVariables) {
43
+ return __awaiter(this, void 0, void 0, function* () {
44
+ const errors = [];
19
45
  for (const [envVar, ssmName] of Object.entries(paramMap)) {
20
46
  try {
21
- const command = new GetParameterCommand({
22
- Name: ssmName,
23
- WithDecryption: true,
24
- });
25
- const { Parameter } = yield ssm.send(command);
26
- const value = Parameter === null || Parameter === void 0 ? void 0 : Parameter.Value;
47
+ const value = yield fetchSSMParameter(ssmName);
27
48
  if (value) {
28
- envContent.push(`${envVar}=${value}`);
29
- console.log(`${envVar}=${value}`);
49
+ existingEnvVariables[envVar] = value;
50
+ console.log(`${envVar}=${value.length > 3 ? '*'.repeat(value.length - 3) + value.slice(-3) : '*'.repeat(value.length)}`);
30
51
  }
31
52
  else {
32
- console.error(`Warning: No value found for ${ssmName}`);
53
+ console.error(`Warning: No value found for: '${ssmName}'`);
33
54
  }
34
55
  }
35
56
  catch (error) {
36
- console.error(`Error fetching parameter ${ssmName}: ${error}`);
37
- throw new Error(`ParameterNotFound: ${ssmName}`);
57
+ console.error(`Error fetching parameter: '${ssmName}'`);
58
+ errors.push(`ParameterNotFound: ${ssmName}`);
38
59
  }
39
60
  }
40
- fs.writeFileSync(envFilePath, envContent.join('\n'));
41
- console.log(`.env file generated at ${envFilePath}`);
61
+ if (errors.length > 0) {
62
+ throw new Error(`Some parameters could not be fetched:\n${errors.join('\n')}`);
63
+ }
64
+ return existingEnvVariables;
42
65
  });
43
66
  }
67
+ function fetchSSMParameter(ssmName) {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ const command = new GetParameterCommand({
70
+ Name: ssmName,
71
+ WithDecryption: true,
72
+ });
73
+ const { Parameter } = yield ssm.send(command);
74
+ return Parameter === null || Parameter === void 0 ? void 0 : Parameter.Value;
75
+ });
76
+ }
77
+ function writeEnvFile(envFilePath, envVariables) {
78
+ const envContent = Object.entries(envVariables)
79
+ .map(([key, value]) => {
80
+ const escapedValue = value.replace(/(\n|\r|\n\r)/g, '\\n').replace(/"/g, '\\"');
81
+ return `${key}=${escapedValue}`;
82
+ })
83
+ .join('\n');
84
+ fs.writeFileSync(envFilePath, envContent);
85
+ }
44
86
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,mBAAmB,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAE/D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC;AAExB,MAAM,UAAgB,GAAG,CAAC,OAAe,EAAE,WAAmB;;QAC5D,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAClD,MAAM,QAAQ,GAA2B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAE7D,MAAM,UAAU,GAAa,EAAE,CAAC;QAEhC,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QACtC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,IAAI,mBAAmB,CAAC;oBACtC,IAAI,EAAE,OAAO;oBACb,cAAc,EAAE,IAAI;iBACrB,CAAC,CAAC;gBAEH,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC9C,MAAM,KAAK,GAAG,SAAS,aAAT,SAAS,uBAAT,SAAS,CAAE,KAAK,CAAC;gBAC/B,IAAI,KAAK,EAAE,CAAC;oBACV,UAAU,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,CAAC,CAAC;oBACtC,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,CAAC,CAAC;gBACpC,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,KAAK,CAAC,+BAA+B,OAAO,EAAE,CAAC,CAAC;gBAC1D,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,4BAA4B,OAAO,KAAK,KAAK,EAAE,CAAC,CAAC;gBAC/D,MAAM,IAAI,KAAK,CAAC,sBAAsB,OAAO,EAAE,CAAC,CAAC;YACnD,CAAC;QACH,CAAC;QAED,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACrD,OAAO,CAAC,GAAG,CAAC,0BAA0B,WAAW,EAAE,CAAC,CAAC;IACvD,CAAC;CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envilder",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "A CLI tool to generate .env files from AWS SSM parameters",
5
5
  "exports": {
6
6
  ".": {
@@ -15,7 +15,7 @@
15
15
  "test-run": "npm run build && node lib/cli/cliRunner.js --map=tests/sample/param_map.json --envfile=.env",
16
16
  "build": "npm run clean && tsc -p tsconfig.build.json --sourceMap --declaration",
17
17
  "lint": "biome lint --write && biome format --write && biome check --write && tsc --noEmit && secretlint **/*",
18
- "test": "npx vitest run --reporter verbose --coverage",
18
+ "test": "vitest run --reporter verbose --coverage",
19
19
  "cli-run": "npm run build && node --trace-warnings lib",
20
20
  "npm-publish": "npm run lint && npm run build && npm publish",
21
21
  "npm-release-patch": "npm version patch && npm run npm-publish",
@@ -42,6 +42,7 @@
42
42
  "@secretlint/secretlint-rule-preset-recommend": "^8.2.4",
43
43
  "@types/node": "^22.5.5",
44
44
  "commander": "^12.1.0",
45
+ "dotenv": "^16.4.5",
45
46
  "picocolors": "^1.1.0"
46
47
  },
47
48
  "devDependencies": {
@@ -51,10 +52,14 @@
51
52
  "secretlint": "^8.2.4",
52
53
  "ts-node": "^10.9.2",
53
54
  "typescript": "^5.6.2",
54
- "vitest": "^2.1.1"
55
+ "vitest": "^2.1.2"
56
+ },
57
+ "resolutions": {
58
+ "string-width": "4.2.3",
59
+ "strip-ansi": "6.0.1"
55
60
  },
56
61
  "engines": {
57
- "node": ">=18.0.0",
62
+ "node": ">=20.0.0",
58
63
  "yarn": ">=1.22"
59
64
  }
60
65
  }
@@ -24,5 +24,5 @@ export async function cliRunner() {
24
24
  }
25
25
 
26
26
  cliRunner().catch((error) => {
27
- console.error('Error in CLI Runner:', error);
27
+ console.error('🚨 Uh-oh! Looks like Mario fell into the wrong pipe! 🍄💥');
28
28
  });
package/src/index.ts CHANGED
@@ -1,36 +1,88 @@
1
1
  import * as fs from 'node:fs';
2
2
  import { GetParameterCommand, SSM } from '@aws-sdk/client-ssm';
3
+ import * as dotenv from 'dotenv';
3
4
 
4
5
  const ssm = new SSM({});
5
6
 
6
7
  export async function run(mapPath: string, envFilePath: string) {
8
+ const paramMap = loadParamMap(mapPath);
9
+ const existingEnvVariables = loadExistingEnvVariables(envFilePath);
10
+
11
+ const updatedEnvVariables = await fetchAndUpdateEnvVariables(paramMap, existingEnvVariables);
12
+
13
+ writeEnvFile(envFilePath, updatedEnvVariables);
14
+ console.log(`Environment File generated at '${envFilePath}'`);
15
+ }
16
+
17
+ function loadParamMap(mapPath: string): Record<string, string> {
7
18
  const content = fs.readFileSync(mapPath, 'utf-8');
8
- const paramMap: Record<string, string> = JSON.parse(content);
19
+ try {
20
+ return JSON.parse(content);
21
+ } catch (error) {
22
+ console.error(`Error parsing JSON from ${mapPath}`);
23
+ throw new Error(`Invalid JSON in parameter map file: ${mapPath}`);
24
+ }
25
+ }
26
+
27
+ function loadExistingEnvVariables(envFilePath: string): Record<string, string> {
28
+ const envVariables: Record<string, string> = {};
29
+
30
+ if (!fs.existsSync(envFilePath)) return envVariables;
31
+
32
+ const existingEnvContent = fs.readFileSync(envFilePath, 'utf-8');
33
+ const parsedEnv = dotenv.parse(existingEnvContent);
34
+ Object.assign(envVariables, parsedEnv);
35
+
36
+ return envVariables;
37
+ }
9
38
 
10
- const envContent: string[] = [];
39
+ async function fetchAndUpdateEnvVariables(
40
+ paramMap: Record<string, string>,
41
+ existingEnvVariables: Record<string, string>,
42
+ ): Promise<Record<string, string>> {
43
+ const errors: string[] = [];
11
44
 
12
- console.log('Fetching parameters...');
13
45
  for (const [envVar, ssmName] of Object.entries(paramMap)) {
14
46
  try {
15
- const command = new GetParameterCommand({
16
- Name: ssmName,
17
- WithDecryption: true,
18
- });
19
-
20
- const { Parameter } = await ssm.send(command);
21
- const value = Parameter?.Value;
47
+ const value = await fetchSSMParameter(ssmName);
22
48
  if (value) {
23
- envContent.push(`${envVar}=${value}`);
24
- console.log(`${envVar}=${value}`);
49
+ existingEnvVariables[envVar] = value;
50
+ console.log(
51
+ `${envVar}=${value.length > 3 ? '*'.repeat(value.length - 3) + value.slice(-3) : '*'.repeat(value.length)}`,
52
+ );
25
53
  } else {
26
- console.error(`Warning: No value found for ${ssmName}`);
54
+ console.error(`Warning: No value found for: '${ssmName}'`);
27
55
  }
28
56
  } catch (error) {
29
- console.error(`Error fetching parameter ${ssmName}: ${error}`);
30
- throw new Error(`ParameterNotFound: ${ssmName}`);
57
+ console.error(`Error fetching parameter: '${ssmName}'`);
58
+ errors.push(`ParameterNotFound: ${ssmName}`);
31
59
  }
32
60
  }
33
61
 
34
- fs.writeFileSync(envFilePath, envContent.join('\n'));
35
- console.log(`.env file generated at ${envFilePath}`);
62
+ if (errors.length > 0) {
63
+ throw new Error(`Some parameters could not be fetched:\n${errors.join('\n')}`);
64
+ }
65
+
66
+ return existingEnvVariables;
67
+ }
68
+
69
+ async function fetchSSMParameter(ssmName: string): Promise<string | undefined> {
70
+ const command = new GetParameterCommand({
71
+ Name: ssmName,
72
+ WithDecryption: true,
73
+ });
74
+
75
+ const { Parameter } = await ssm.send(command);
76
+ return Parameter?.Value;
77
+ }
78
+
79
+ function writeEnvFile(envFilePath: string, envVariables: Record<string, string>): void {
80
+ const envContent = Object.entries(envVariables)
81
+ .map(([key, value]) => {
82
+ const escapedValue = value.replace(/(\n|\r|\n\r)/g, '\\n').replace(/"/g, '\\"');
83
+ return `${key}=${escapedValue}`;
84
+ })
85
+ .join('\n');
86
+
87
+ fs.writeFileSync(envFilePath, envContent);
36
88
  }
@@ -42,17 +42,4 @@ describe('cliRunner', () => {
42
42
  // Assert
43
43
  await expect(action).rejects.toThrow('process.exit called');
44
44
  });
45
-
46
- it('Should_DisplayHelp_When_NoArgumentsAreProvided', async () => {
47
- // Arrange
48
- vi.spyOn(process, 'exit').mockImplementation(() => {
49
- throw new Error('process.exit called with code 0');
50
- });
51
-
52
- // Act
53
- const action = cliRunner();
54
-
55
- // Assert
56
- await expect(action).rejects.toThrow('process.exit called with code 0');
57
- });
58
45
  });
@@ -11,12 +11,17 @@ vi.mock('@aws-sdk/client-ssm', () => {
11
11
  Parameter: { Value: 'mockedEmail@example.com' },
12
12
  });
13
13
  }
14
+
14
15
  if (command.input.Name === '/path/to/ssm/password') {
15
16
  return Promise.resolve({
16
17
  Parameter: { Value: 'mockedPassword' },
17
18
  });
18
19
  }
19
20
 
21
+ if (command.input.Name === '/path/to/ssm/password_no_value') {
22
+ return Promise.resolve({ Parameter: { Value: '' } });
23
+ }
24
+
20
25
  return Promise.reject(new Error(`ParameterNotFound: ${command.input.Name}`));
21
26
  }),
22
27
  })),
@@ -57,7 +62,7 @@ describe('Envilder CLI', () => {
57
62
  const mockMapPath = './tests/param_map.json';
58
63
  const mockEnvFilePath = './tests/.env.test';
59
64
  const paramMapContent = {
60
- NEXT_PUBLIC_CREDENTIAL_EMAIL: '/path/to/ssm/unknown-email', // Non-existent parameter
65
+ NEXT_PUBLIC_CREDENTIAL_EMAIL: 'non-existent parameter',
61
66
  };
62
67
  fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
63
68
 
@@ -65,7 +70,74 @@ describe('Envilder CLI', () => {
65
70
  const action = run(mockMapPath, mockEnvFilePath);
66
71
 
67
72
  // Assert
68
- await expect(action).rejects.toThrow('ParameterNotFound: /path/to/ssm/unknown-email');
73
+ await expect(action).rejects.toThrow('ParameterNotFound: non-existent parameter');
74
+ fs.unlinkSync(mockMapPath);
75
+ });
76
+
77
+ it('Should_AppendNewSSMParameters_When_EnvFileContainsExistingVariables', async () => {
78
+ // Arrange
79
+ const mockMapPath = './tests/param_map.json';
80
+ const mockEnvFilePath = './tests/.env.test';
81
+
82
+ const existingEnvContent = 'EXISTING_VAR=existingValue';
83
+ fs.writeFileSync(mockEnvFilePath, existingEnvContent);
84
+ const paramMapContent = {
85
+ NEXT_PUBLIC_CREDENTIAL_EMAIL: '/path/to/ssm/email',
86
+ NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password',
87
+ };
88
+ fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
89
+
90
+ // Act
91
+ await run(mockMapPath, mockEnvFilePath);
92
+
93
+ // Assert
94
+ const updatedEnvFileContent = fs.readFileSync(mockEnvFilePath, 'utf-8');
95
+ expect(updatedEnvFileContent).toContain('EXISTING_VAR=existingValue');
96
+ expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_EMAIL=mockedEmail@example.com');
97
+ expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_PASSWORD=mockedPassword');
98
+ fs.unlinkSync(mockEnvFilePath);
99
+ fs.unlinkSync(mockMapPath);
100
+ });
101
+
102
+ it('Should_OverwriteSSMParameters_When_EnvFileContainsSameVariables', async () => {
103
+ // Arrange
104
+ const mockMapPath = './tests/param_map.json';
105
+ const mockEnvFilePath = './tests/.env.test';
106
+ const existingEnvContent = 'NEXT_PUBLIC_CREDENTIAL_EMAIL=oldEmail@example.com';
107
+ fs.writeFileSync(mockEnvFilePath, existingEnvContent);
108
+ const paramMapContent = {
109
+ NEXT_PUBLIC_CREDENTIAL_EMAIL: '/path/to/ssm/email',
110
+ NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password',
111
+ };
112
+ fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
113
+
114
+ // Act
115
+ await run(mockMapPath, mockEnvFilePath);
116
+
117
+ // Assert
118
+ const updatedEnvFileContent = fs.readFileSync(mockEnvFilePath, 'utf-8');
119
+ expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_EMAIL=mockedEmail@example.com');
120
+ expect(updatedEnvFileContent).toContain('NEXT_PUBLIC_CREDENTIAL_PASSWORD=mockedPassword');
121
+ fs.unlinkSync(mockEnvFilePath);
122
+ fs.unlinkSync(mockMapPath);
123
+ });
124
+
125
+ it('Should_LogWarning_When_SSMParameterHasNoValue', async () => {
126
+ // Arrange
127
+ const mockMapPath = './tests/param_map.json';
128
+ const mockEnvFilePath = './tests/.env.test';
129
+ const paramMapContent = {
130
+ NEXT_PUBLIC_CREDENTIAL_PASSWORD: '/path/to/ssm/password_no_value',
131
+ };
132
+ fs.writeFileSync(mockMapPath, JSON.stringify(paramMapContent));
133
+ const consoleSpy = vi.spyOn(console, 'error');
134
+
135
+ // Act
136
+ await run(mockMapPath, mockEnvFilePath);
137
+
138
+ // Assert
139
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Warning: No value found for'));
140
+ fs.unlinkSync(mockEnvFilePath);
69
141
  fs.unlinkSync(mockMapPath);
70
142
  });
71
143
  });
@@ -1,5 +1,3 @@
1
1
  {
2
- "NEXT_PUBLIC_CREDENTIAL_EMAIL": "/M47.Claims.Apps.Minimal.Api/Production/Auth/CredentialEmail",
3
- "NEXT_PUBLIC_CREDENTIAL_PASSWORD": "/M47.Claims.Apps.Minimal.Api/Production/Auth/CredentialPassword",
4
- "NEXT_PUBLIC_JWT_SECRET": "/M47.Claims.Apps.Minimal.Api/Production/Auth/JwtSecret"
2
+ "TOKEN_SECRET": "/Test/Token"
5
3
  }
package/vite.config.ts CHANGED
@@ -6,8 +6,12 @@ export default defineConfig({
6
6
  environment: 'node',
7
7
  include: ['tests/**/*.test.ts'],
8
8
  coverage: {
9
- include: ['src/**/*'],
10
- reporter: ['text', 'json', 'html'],
9
+ provider: 'v8',
10
+ reporter: ['text', 'html', 'json'],
11
+ reportsDirectory: './coverage',
12
+ all: true,
13
+ include: ['src/**/*.ts'],
14
+ exclude: ['node_modules', 'test', 'coverage', 'dist'],
11
15
  },
12
16
  },
13
17
  });
@@ -0,0 +1,12 @@
1
+ // vitest.config.js
2
+ import { defineConfig } from 'vitest/config';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ coverage: {
7
+ provider: 'v8',
8
+ reporter: ['text', 'html'],
9
+ reportsDirectory: './coverage',
10
+ },
11
+ },
12
+ });