envilder 0.4.0 → 0.5.0

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,58 @@
1
+ name: 🍄 Test CLI
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/cli-validation.yml"
16
+ - "src/**"
17
+ - "scripts/**"
18
+ - "tests/**"
19
+
20
+ jobs:
21
+ validate:
22
+ runs-on: ubuntu-24.04
23
+ if: ${{ !github.event.pull_request.draft }}
24
+ timeout-minutes: 30
25
+
26
+ steps:
27
+ - name: 🚀 ♂️ Checkout
28
+ uses: actions/checkout@v4
29
+
30
+ - name: 🛠️ Setup Node.js with Cache
31
+ uses: actions/setup-node@v4
32
+ with:
33
+ node-version: '20.x'
34
+ cache: 'yarn'
35
+
36
+ - name: 💉 Configure AWS credentials
37
+ uses: aws-actions/configure-aws-credentials@v4
38
+ with:
39
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
40
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
41
+ aws-region: "eu-west-1"
42
+
43
+ - name: 🪙 Collect dependencies
44
+ run: yarn install --frozen-lockfile
45
+
46
+ - name: 🏗️ Build the castle
47
+ run: yarn build
48
+
49
+ - name: 🧩 Ensure executable permissions
50
+ run: |
51
+ chmod +x ./lib/cli/cli.js
52
+
53
+ - name: 🌈 Install envilder globally (with npm)
54
+ run: |
55
+ sudo npm install -g .
56
+
57
+ - name: 🎮 Test cli envilder
58
+ run: yarn validate-cli
@@ -14,6 +14,7 @@ on:
14
14
  paths:
15
15
  - ".github/workflows/unit-tests.yml"
16
16
  - "src/**"
17
+ - "tests/**"
17
18
 
18
19
  concurrency:
19
20
  group: ${{ github.workflow }}-${{ github.head_ref || github.sha }}
package/biome.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
3
3
  "files": {
4
- "include": ["./src/**", "./tests/**"],
4
+ "include": ["./src/**", "./tests/**", "./scripts/**"],
5
5
  "ignore": ["**/node_modules/**", "**/lib/**", "**/dist/**", "**/coverage/**", "**/.lock", "**/.md"]
6
6
  },
7
7
  "organizeImports": {
package/package.json CHANGED
@@ -1,19 +1,22 @@
1
1
  {
2
2
  "name": "envilder",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A CLI tool to generate .env files from AWS SSM parameters",
5
5
  "exports": {
6
6
  ".": {
7
+ "import": "./lib/index.js",
7
8
  "types": "./lib/index.d.ts"
8
9
  }
9
10
  },
11
+ "main": "./lib/index.js",
10
12
  "bin": {
11
- "envilder": "lib/cli/cliRunner.js"
13
+ "envilder": "lib/cli/cli.js"
12
14
  },
13
15
  "scripts": {
14
16
  "clean": "npx jest --clearCache && yarn cache clean --force && npx rimraf lib && npx rimraf node_modules && npx rimraf coverage && npx rimraf yarn.lock",
15
- "test-run": "yarn build && node lib/cli/cliRunner.js --map=tests/sample/param-map.json --envfile=tests/sample/autogenerated.env",
16
- "build": "tsc -p tsconfig.build.json --sourceMap --declaration",
17
+ "test-run": "yarn build && node lib/cli/cli.js --map=tests/sample/param-map.json --envfile=tests/sample/autogenerated.env",
18
+ "build": "tsc -p tsconfig.build.json --sourceMap --declaration && node --no-warnings scripts/chmod-cli.js",
19
+ "validate-cli": "node --no-warnings --loader ts-node/esm scripts/validate-cli.ts",
17
20
  "format": "npx biome format",
18
21
  "format:write": "npx biome format --write",
19
22
  "lint": "npx secretlint \"**/*\" && biome lint --write && biome format --write && biome check --write && tsc --noEmit",
@@ -40,10 +43,8 @@
40
43
  },
41
44
  "type": "module",
42
45
  "dependencies": {
43
- "@aws-sdk/client-ssm": "^3.654.0",
46
+ "@aws-sdk/client-ssm": "^3.806.0",
44
47
  "@aws-sdk/credential-providers": "^3.806.0",
45
- "@secretlint/core": "^9.2.1",
46
- "@secretlint/secretlint-rule-preset-recommend": "^9.0.0",
47
48
  "@types/node": "^22.5.5",
48
49
  "commander": "^13.1.0",
49
50
  "dotenv": "^16.4.5",
@@ -51,17 +52,14 @@
51
52
  },
52
53
  "devDependencies": {
53
54
  "@biomejs/biome": "^1.9.1",
55
+ "@secretlint/secretlint-rule-preset-recommend": "^9.3.2",
54
56
  "@vitest/coverage-v8": "^3.1.1",
55
57
  "rimraf": "^6.0.1",
56
- "secretlint": "^9.0.0",
58
+ "secretlint": "^9.3.2",
57
59
  "ts-node": "^10.9.2",
58
60
  "typescript": "^5.6.2",
59
61
  "vitest": "^3.1.1"
60
62
  },
61
- "resolutions": {
62
- "string-width": "4.2.3",
63
- "strip-ansi": "6.0.1"
64
- },
65
63
  "engines": {
66
64
  "node": ">=20.0.0",
67
65
  "yarn": ">=1.22"
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Simple script to set executable permissions on the CLI entry point.
4
+ * Only runs the chmod command on Unix-based systems.
5
+ */
6
+ import { chmod } from 'node:fs/promises';
7
+ import { join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ // Get the project root directory
11
+ const rootDir = join(fileURLToPath(new URL('.', import.meta.url)), '..');
12
+ const cliFilePath = join(rootDir, 'lib', 'cli', 'cli.js');
13
+
14
+ // Only run on non-Windows platforms
15
+ if (process.platform !== 'win32') {
16
+ chmod(cliFilePath, 0o755) // rwxr-xr-x permissions
17
+ .then(() => console.log(`✅ Set executable permissions for: ${cliFilePath}`))
18
+ .catch((error) => {
19
+ console.error(`❌ Error setting permissions: ${error.message}`);
20
+ process.exit(1);
21
+ });
22
+ } else {
23
+ console.log('📋 Skipping executable permissions on Windows platform');
24
+ }
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const rootDir = join(__dirname, '..');
10
+ const isGithubAction = process.env.GITHUB_ACTIONS === 'true';
11
+ const isWindows = process.platform === 'win32';
12
+ const findCommand = isWindows ? 'where' : 'which';
13
+
14
+ /**
15
+ * Run a command with the given arguments
16
+ */
17
+ function runCommand(command: string, args: string[]): Promise<{ code: number; output: string }> {
18
+ return new Promise((resolve) => {
19
+ const proc = spawn(command, args, { shell: true });
20
+ let output = '';
21
+
22
+ proc.stdout.on('data', (data) => {
23
+ output += data.toString();
24
+ });
25
+
26
+ proc.stderr.on('data', (data) => {
27
+ output += data.toString();
28
+ });
29
+
30
+ proc.on('close', (code) => {
31
+ resolve({ code: code ?? 0, output });
32
+ });
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Try to run a command with both global and local CLI approaches
38
+ */
39
+ async function tryCommandWithFallback(
40
+ args: string[],
41
+ ): Promise<{ code: number; output: string; usedFallback: boolean }> {
42
+ // Try global command first
43
+ const globalResult = await runCommand('envilder', args);
44
+
45
+ if (globalResult.code === 0) {
46
+ return { ...globalResult, usedFallback: false };
47
+ }
48
+
49
+ // Fall back to direct node execution
50
+ console.log('Global envilder command failed, trying direct Node.js execution...');
51
+ const directResult = await runCommand('node', ['./lib/cli/cli.js', ...args]);
52
+
53
+ return {
54
+ ...directResult,
55
+ usedFallback: true,
56
+ };
57
+ }
58
+
59
+ async function validateCLI() {
60
+ console.log(`🔍 Validating envilder CLI installation${isGithubAction ? ' in GitHub Actions' : ''}...`);
61
+
62
+ try {
63
+ // Test 1: Check CLI version and accessibility
64
+ console.log('Testing envilder version command...');
65
+ const versionResult = await tryCommandWithFallback(['--version']);
66
+
67
+ if (versionResult.code !== 0) {
68
+ throw new Error(`❌ envilder version command failed: ${versionResult.output}`);
69
+ }
70
+ console.log('✅ envilder version command works');
71
+
72
+ // Test 2: Check help command
73
+ console.log('Testing envilder help command...');
74
+ const helpResult = await tryCommandWithFallback(['--help']);
75
+
76
+ if (helpResult.code !== 0) {
77
+ throw new Error(`❌ envilder help command failed: ${helpResult.output}`);
78
+ }
79
+
80
+ if (!helpResult.output.includes('--map') || !helpResult.output.includes('--envfile')) {
81
+ throw new Error('❌ envilder help command output is missing expected options');
82
+ }
83
+ console.log('✅ envilder help command works');
84
+
85
+ // Test 3: Check sample file generation
86
+ const testEnvFile = join(rootDir, 'tests', 'sample', 'cli-validation.env');
87
+ console.log('Testing envilder file generation...');
88
+
89
+ const sampleResult = await tryCommandWithFallback([
90
+ '--map',
91
+ join(rootDir, 'tests', 'sample', 'param-map.json'),
92
+ '--envfile',
93
+ testEnvFile,
94
+ ]);
95
+
96
+ if (sampleResult.code !== 0) {
97
+ throw new Error(`❌ envilder failed to generate environment file: ${sampleResult.output}`);
98
+ }
99
+
100
+ if (!sampleResult.output.includes('Environment File generated')) {
101
+ throw new Error('❌ envilder did not output expected success message');
102
+ }
103
+
104
+ if (!existsSync(testEnvFile)) {
105
+ throw new Error(`❌ envilder did not create the environment file at ${testEnvFile}`);
106
+ }
107
+ console.log('✅ envilder successfully generated environment file');
108
+
109
+ // Test 4: Check error handling for invalid arguments
110
+ console.log('Testing envilder with invalid arguments...');
111
+ const errorResult = await tryCommandWithFallback(['--invalid']);
112
+
113
+ if (errorResult.code === 0) {
114
+ throw new Error('❌ envilder should fail with invalid arguments');
115
+ }
116
+ console.log('✅ envilder properly handles invalid arguments');
117
+
118
+ // Test 5: Check error handling for missing required options
119
+ console.log('Testing envilder with missing required options...');
120
+ const missingResult = await tryCommandWithFallback([]);
121
+
122
+ if (missingResult.code === 0) {
123
+ throw new Error('❌ envilder should fail when required options are missing');
124
+ }
125
+ console.log('✅ envilder properly handles missing required options');
126
+
127
+ console.log('🎉 All CLI validation tests passed!');
128
+ } catch (error) {
129
+ if (error instanceof Error) {
130
+ console.error(error.message);
131
+ } else {
132
+ console.error('Unknown error occurred during CLI validation');
133
+ }
134
+ process.exit(1);
135
+ }
136
+ }
137
+
138
+ validateCLI().catch((error) => {
139
+ console.error(error.message);
140
+ process.exit(1);
141
+ });
package/src/cli/cli.ts ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Command } from 'commander';
6
+ import { run } from '../index.js';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ /**
12
+ * Find the package.json file by traversing up directories
13
+ * @param startDir The directory to start searching from
14
+ * @param maxDepth Maximum number of parent directories to check
15
+ * @returns Path to package.json if found, or null if not found
16
+ */
17
+ function findPackageJson(startDir: string, maxDepth = 5): string | null {
18
+ let currentDir = startDir;
19
+ let depth = 0;
20
+
21
+ while (depth < maxDepth) {
22
+ const packagePath = join(currentDir, 'package.json');
23
+ if (existsSync(packagePath)) {
24
+ return packagePath;
25
+ }
26
+
27
+ // Go up one directory
28
+ const parentDir = dirname(currentDir);
29
+ if (parentDir === currentDir) {
30
+ // We've reached the root
31
+ break;
32
+ }
33
+
34
+ currentDir = parentDir;
35
+ depth++;
36
+ }
37
+
38
+ return null;
39
+ }
40
+
41
+ // Get package.json path by searching up from current file
42
+ const packageJsonPath = findPackageJson(__dirname) || join(__dirname, '..', '..', 'package.json');
43
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
44
+
45
+ /**
46
+ * Parses CLI arguments and runs the environment file generator.
47
+ *
48
+ * Expects `--map` and `--envfile` options to be provided, with an optional `--profile` for AWS CLI profile selection. Invokes the main process to generate a `.env` file from AWS SSM parameters based on the provided mapping.
49
+ *
50
+ * @throws {Error} If either `--map` or `--envfile` arguments are missing.
51
+ */
52
+ export async function main() {
53
+ const program = new Command();
54
+ program
55
+ .name('envilder')
56
+ .description('A CLI tool to generate .env files from AWS SSM parameters')
57
+ .version(packageJson.version)
58
+ .requiredOption('--map <path>', 'Path to the JSON file with environment variable mapping')
59
+ .requiredOption('--envfile <path>', 'Path to the .env file to be generated')
60
+ .option('--profile <name>', 'AWS CLI profile to use');
61
+
62
+ await program.parseAsync(process.argv);
63
+ const options = program.opts();
64
+
65
+ if (!options.map || !options.envfile) {
66
+ throw new Error('Missing required arguments: --map and --envfile');
67
+ }
68
+
69
+ await run(options.map, options.envfile, options.profile);
70
+ }
71
+
72
+ // Execute the CLI
73
+ main().catch((error) => {
74
+ console.error('🚨 Uh-oh! Looks like Mario fell into the wrong pipe! 🍄💥');
75
+ console.error(error);
76
+ });
@@ -1,12 +1,12 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { cliRunner } from '../../src/cli/cliRunner';
2
+ import { main } from '../../src/cli/cli';
3
3
  import { run } from '../../src/index';
4
4
 
5
5
  vi.mock('../../src/index', () => ({
6
6
  run: vi.fn(),
7
7
  }));
8
8
 
9
- describe('cliRunner', () => {
9
+ describe('CLI', () => {
10
10
  const originalArgv = process.argv;
11
11
 
12
12
  beforeEach(() => {
@@ -24,7 +24,7 @@ describe('cliRunner', () => {
24
24
  process.argv.push('--map', mockMapPath, '--envfile', mockEnvFilePath);
25
25
 
26
26
  // Act
27
- await cliRunner();
27
+ await main();
28
28
 
29
29
  // Assert
30
30
  expect(run).toHaveBeenCalledWith(mockMapPath, mockEnvFilePath, undefined);
@@ -37,7 +37,7 @@ describe('cliRunner', () => {
37
37
  });
38
38
 
39
39
  // Act
40
- const action = cliRunner();
40
+ const action = main();
41
41
 
42
42
  // Assert
43
43
  await expect(action).rejects.toThrow('process.exit called');
@@ -51,7 +51,7 @@ describe('cliRunner', () => {
51
51
  process.argv.push('--map', mockMapPath, '--envfile', mockEnvFilePath, '--profile', mockProfile);
52
52
 
53
53
  // Act
54
- await cliRunner();
54
+ await main();
55
55
 
56
56
  // Assert
57
57
  expect(run).toHaveBeenCalledWith(mockMapPath, mockEnvFilePath, mockProfile);
@@ -0,0 +1 @@
1
+ TOKEN_SECRET=this_is_for_test
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "extends": "./tsconfig.json",
3
3
  "compilerOptions": {
4
- "rootDir": "./src"
4
+ "rootDir": "./src",
5
+ "outDir": "./lib",
6
+ "module": "ESNext",
7
+ "moduleResolution": "Node",
8
+ "preserveConstEnums": true,
9
+ "sourceMap": true
5
10
  },
6
- "include": ["./src/**/*"]
7
- }
11
+ "include": [
12
+ "./src/**/*"
13
+ ]
14
+ }
package/tsconfig.json CHANGED
@@ -10,13 +10,21 @@
10
10
  "esModuleInterop": true,
11
11
  "noImplicitAny": true,
12
12
  "skipLibCheck": true,
13
- "lib": ["es2022"],
13
+ "lib": [
14
+ "es2022"
15
+ ],
14
16
  "declaration": true,
15
17
  "declarationMap": true,
16
18
  "sourceMap": true,
17
19
  "forceConsistentCasingInFileNames": true,
18
20
  "resolveJsonModule": true,
19
- "types": ["node", "picocolors"]
21
+ "types": [
22
+ "node",
23
+ "picocolors"
24
+ ]
20
25
  },
21
- "include": ["src/**/*"]
22
- }
26
+ "include": [
27
+ "src/**/*",
28
+ "scripts/**/*"
29
+ ]
30
+ }
package/vite.config.ts CHANGED
@@ -11,7 +11,7 @@ export default defineConfig({
11
11
  reportsDirectory: './coverage',
12
12
  all: true,
13
13
  include: ['src/**/*.ts'],
14
- exclude: ['node_modules', 'test', 'coverage', 'dist'],
14
+ exclude: ['node_modules', 'test', 'coverage', 'dist', 'scripts'],
15
15
  },
16
16
  },
17
17
  });
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { run } from '../index.js';
4
-
5
- /**
6
- * Parses CLI arguments and runs the environment file generator.
7
- *
8
- * Expects `--map` and `--envfile` options to be provided, with an optional `--profile` for AWS CLI profile selection. Invokes the main process to generate a `.env` file from AWS SSM parameters based on the provided mapping.
9
- *
10
- * @throws {Error} If either `--map` or `--envfile` arguments are missing.
11
- */
12
- export async function cliRunner() {
13
- const program = new Command();
14
-
15
- program
16
- .name('envilder')
17
- .description('A CLI tool to generate .env files from AWS SSM parameters')
18
- .version('0.1.0')
19
- .requiredOption('--map <path>', 'Path to the JSON file with environment variable mapping')
20
- .requiredOption('--envfile <path>', 'Path to the .env file to be generated')
21
- .option('--profile <name>', 'AWS CLI profile to use');
22
-
23
- await program.parseAsync(process.argv);
24
- const options = program.opts();
25
-
26
- if (!options.map || !options.envfile) {
27
- throw new Error('Missing required arguments: --map and --envfile');
28
- }
29
-
30
- await run(options.map, options.envfile, options.profile);
31
- }
32
-
33
- cliRunner().catch((error) => {
34
- console.error('🚨 Uh-oh! Looks like Mario fell into the wrong pipe! 🍄💥');
35
- });