menv-npm 0.1.1 → 0.1.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 +3 -5
- package/dist/core/envWatcher.d.ts +1 -1
- package/dist/core/envWatcher.js +9 -4
- package/dist/core/secretScanner.js +32 -4
- package/dist/index.js +29 -23
- package/dist/utils/template.d.ts +6 -0
- package/dist/utils/template.js +20 -0
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -21,24 +21,22 @@ Menv is a lightweight CLI designed to bring consistency, safety, and automation
|
|
|
21
21
|
npm install menv-npm --save-dev
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
## Quick Start
|
|
25
|
-
|
|
26
24
|
Generate your initial .env.example:
|
|
27
25
|
|
|
28
26
|
```bash
|
|
29
|
-
npx menv generate
|
|
27
|
+
npx menv-npm generate
|
|
30
28
|
```
|
|
31
29
|
|
|
32
30
|
Check if your environment is in sync:
|
|
33
31
|
|
|
34
32
|
```bash
|
|
35
|
-
npx menv sync
|
|
33
|
+
npx menv-npm sync
|
|
36
34
|
```
|
|
37
35
|
|
|
38
36
|
Validate requirements for CI:
|
|
39
37
|
|
|
40
38
|
```bash
|
|
41
|
-
npx menv check
|
|
39
|
+
npx menv-npm check
|
|
42
40
|
```
|
|
43
41
|
|
|
44
42
|
## Commands
|
package/dist/core/envWatcher.js
CHANGED
|
@@ -4,20 +4,25 @@ import path from "node:path";
|
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import { parseEnv } from "./envParser.js";
|
|
6
6
|
import { generateExample } from "./envGenerator.js";
|
|
7
|
+
import { findEnvFile } from "../utils/template.js";
|
|
7
8
|
/**
|
|
8
|
-
* Watches for changes in
|
|
9
|
+
* Watches for changes in the primary environment file and regenerates .env.example.
|
|
9
10
|
*/
|
|
10
11
|
export async function startWatcher() {
|
|
11
|
-
const envPath =
|
|
12
|
+
const envPath = await findEnvFile();
|
|
13
|
+
if (!envPath) {
|
|
14
|
+
throw new Error("No environment file found to watch.");
|
|
15
|
+
}
|
|
16
|
+
const envName = path.basename(envPath);
|
|
12
17
|
const examplePath = path.resolve(process.cwd(), ".env.example");
|
|
13
|
-
console.log(pc.dim(`Watching for changes in ${pc.cyan(
|
|
18
|
+
console.log(pc.dim(`Watching for changes in ${pc.cyan(envName)}...`));
|
|
14
19
|
const watcher = chokidar.watch(envPath, {
|
|
15
20
|
persistent: true,
|
|
16
21
|
ignoreInitial: true,
|
|
17
22
|
});
|
|
18
23
|
watcher.on("change", async () => {
|
|
19
24
|
try {
|
|
20
|
-
console.log(pc.blue(
|
|
25
|
+
console.log(pc.blue(`⚡ ${envName} changed, regenerating .env.example...`));
|
|
21
26
|
const content = await fs.readFile(envPath, "utf8");
|
|
22
27
|
const parsed = parseEnv(content);
|
|
23
28
|
const generated = generateExample(parsed);
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
const PATTERNS = [
|
|
2
|
-
|
|
3
|
-
{ name: "
|
|
4
|
-
{ name: "
|
|
5
|
-
|
|
2
|
+
// AWS
|
|
3
|
+
{ name: "AWS Access Key ID", regex: /AKIA[0-9A-Z]{16}/ },
|
|
4
|
+
{ name: "AWS Secret Access Key", regex: /(?<![A-Za-z0-9])[A-Za-z0-9\/+]{40}(?![A-Za-z0-9\/+])/ },
|
|
5
|
+
// Stripe
|
|
6
|
+
{ name: "Stripe Live Secret Key", regex: /sk_live_[0-9a-zA-Z]{24,}/ },
|
|
7
|
+
{ name: "Stripe Test Secret Key", regex: /sk_test_[0-9a-zA-Z]{24,}/ },
|
|
8
|
+
// GitHub
|
|
9
|
+
{ name: "GitHub Personal Access Token (classic)", regex: /ghp_[0-9a-zA-Z]{36}/ },
|
|
10
|
+
{ name: "GitHub Fine-grained Token", regex: /github_pat_[0-9a-zA-Z_]{82}/ },
|
|
11
|
+
{ name: "GitHub OAuth Token", regex: /gho_[0-9a-zA-Z]{36}/ },
|
|
12
|
+
{ name: "GitHub Actions Token", regex: /ghs_[0-9a-zA-Z]{36}/ },
|
|
13
|
+
// Slack
|
|
14
|
+
{ name: "Slack Bot Token", regex: /xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{23,25}/ },
|
|
15
|
+
{ name: "Slack User Token", regex: /xoxp-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{23,25}/ },
|
|
16
|
+
{ name: "Slack Webhook URL", regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[a-zA-Z0-9]+/ },
|
|
17
|
+
// Twilio
|
|
18
|
+
{ name: "Twilio Account SID", regex: /AC[a-zA-Z0-9]{32}/ },
|
|
19
|
+
{ name: "Twilio Auth Token", regex: /SK[a-zA-Z0-9]{32}/ },
|
|
20
|
+
// Paystack
|
|
21
|
+
{ name: "Paystack Live Secret Key", regex: /sk_live_[a-zA-Z0-9]{40,}/ },
|
|
22
|
+
{ name: "Paystack Test Secret Key", regex: /sk_test_[a-zA-Z0-9]{40,}/ },
|
|
23
|
+
// SendGrid
|
|
24
|
+
{ name: "SendGrid API Key", regex: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/ },
|
|
25
|
+
// Mailgun
|
|
26
|
+
{ name: "Mailgun API Key", regex: /key-[0-9a-zA-Z]{32}/ },
|
|
27
|
+
// Private keys
|
|
28
|
+
{ name: "Private Key (PEM)", regex: /-----BEGIN [A-Z ]+ PRIVATE KEY-----/ },
|
|
29
|
+
{ name: "Certificate", regex: /-----BEGIN CERTIFICATE-----/ },
|
|
30
|
+
// Generic high-entropy patterns (base64 and hex)
|
|
31
|
+
{ name: "High-entropy base64 secret (40+ chars)", regex: /(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{40,}={0,2}(?![A-Za-z0-9+/=])/ },
|
|
32
|
+
{ name: "High-entropy hex secret (32+ chars)", regex: /(?<![0-9a-fA-F])[0-9a-fA-F]{32,}(?![0-9a-fA-F])/ },
|
|
6
33
|
];
|
|
7
34
|
/**
|
|
8
35
|
* Scans environment lines for potential secret leaks.
|
|
@@ -19,6 +46,7 @@ export function scanSecrets(lines) {
|
|
|
19
46
|
type: pattern.name,
|
|
20
47
|
line: index + 1,
|
|
21
48
|
});
|
|
49
|
+
break; // one match per line is enough
|
|
22
50
|
}
|
|
23
51
|
}
|
|
24
52
|
});
|
package/dist/index.js
CHANGED
|
@@ -3,28 +3,34 @@ import { cac } from "cac";
|
|
|
3
3
|
import pc from "picocolors";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { version } = require("../package.json");
|
|
6
9
|
import { parseEnv } from "./core/envParser.js";
|
|
7
10
|
import { generateExample } from "./core/envGenerator.js";
|
|
8
11
|
import { compareEnvKeys } from "./core/envValidator.js";
|
|
9
12
|
import { startWatcher } from "./core/envWatcher.js";
|
|
10
13
|
import { scanSecrets } from "./core/secretScanner.js";
|
|
11
14
|
import { ensureGitignore } from "./utils/git.js";
|
|
12
|
-
import { findTemplateFile } from "./utils/template.js";
|
|
15
|
+
import { findTemplateFile, findEnvFile } from "./utils/template.js";
|
|
13
16
|
const cli = cac("menv");
|
|
14
|
-
cli
|
|
17
|
+
cli
|
|
18
|
+
.command("generate", "Generate .env.example from environment file")
|
|
19
|
+
.action(async () => {
|
|
15
20
|
try {
|
|
16
21
|
await ensureGitignore();
|
|
17
|
-
const envPath =
|
|
22
|
+
const envPath = await findEnvFile();
|
|
18
23
|
const examplePath = path.resolve(process.cwd(), ".env.example");
|
|
19
|
-
if (!
|
|
20
|
-
console.error(pc.red("✖ .env
|
|
24
|
+
if (!envPath) {
|
|
25
|
+
console.error(pc.red("✖ No environment file (.env, .env.local, etc.) found in current directory."));
|
|
21
26
|
process.exit(1);
|
|
22
27
|
}
|
|
28
|
+
const envName = path.basename(envPath);
|
|
23
29
|
const content = await fs.readFile(envPath, "utf8");
|
|
24
30
|
const parsed = parseEnv(content);
|
|
25
31
|
const generated = generateExample(parsed);
|
|
26
32
|
await fs.writeFile(examplePath, generated);
|
|
27
|
-
console.log(pc.green(
|
|
33
|
+
console.log(pc.green(`✔ .env.example generated successfully from ${envName}.`));
|
|
28
34
|
}
|
|
29
35
|
catch (error) {
|
|
30
36
|
console.error(pc.red("✖ Failed to generate .env.example:"), error);
|
|
@@ -32,23 +38,24 @@ cli.command("generate", "Generate .env.example from .env").action(async () => {
|
|
|
32
38
|
}
|
|
33
39
|
});
|
|
34
40
|
cli
|
|
35
|
-
.command("sync", "Check for inconsistencies between
|
|
36
|
-
.alias("
|
|
41
|
+
.command("sync", "Check for inconsistencies between environment and template")
|
|
42
|
+
.alias("sync")
|
|
37
43
|
.action(async () => {
|
|
38
44
|
try {
|
|
39
45
|
await ensureGitignore();
|
|
40
|
-
const envPath =
|
|
46
|
+
const envPath = await findEnvFile();
|
|
41
47
|
const templatePath = await findTemplateFile();
|
|
42
|
-
if (!
|
|
43
|
-
console.error(pc.red("✖
|
|
48
|
+
if (!envPath) {
|
|
49
|
+
console.error(pc.red("✖ No environment file found."));
|
|
44
50
|
process.exit(1);
|
|
45
51
|
}
|
|
46
52
|
if (!templatePath) {
|
|
47
53
|
console.error(pc.red("✖ No environment template (.env.example or .env.sample) found."));
|
|
48
54
|
process.exit(1);
|
|
49
55
|
}
|
|
56
|
+
const envName = path.basename(envPath);
|
|
50
57
|
const templateName = path.basename(templatePath);
|
|
51
|
-
console.log(pc.dim(`\nComparing
|
|
58
|
+
console.log(pc.dim(`\nComparing ${envName} with ${templateName}...`));
|
|
52
59
|
const envContent = await fs.readFile(envPath, "utf8");
|
|
53
60
|
const templateContent = await fs.readFile(templatePath, "utf8");
|
|
54
61
|
const envParsed = parseEnv(envContent);
|
|
@@ -73,21 +80,20 @@ cli
|
|
|
73
80
|
}
|
|
74
81
|
});
|
|
75
82
|
cli
|
|
76
|
-
.command("check", "Validate that all required variables in
|
|
83
|
+
.command("check", "Validate that all required variables in template exist in environment")
|
|
77
84
|
.action(async () => {
|
|
78
85
|
try {
|
|
79
86
|
await ensureGitignore();
|
|
80
|
-
const envPath =
|
|
87
|
+
const envPath = await findEnvFile();
|
|
81
88
|
const templatePath = await findTemplateFile();
|
|
82
89
|
if (!templatePath) {
|
|
83
90
|
console.error(pc.red("✖ No environment template (.env.example or .env.sample) found."));
|
|
84
91
|
process.exit(1);
|
|
85
92
|
}
|
|
86
93
|
const templateName = path.basename(templatePath);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
: "";
|
|
94
|
+
const envName = envPath ? path.basename(envPath) : ".env";
|
|
95
|
+
console.log(pc.dim(`\nValidating ${envName} against ${templateName}...`));
|
|
96
|
+
const envContent = envPath ? await fs.readFile(envPath, "utf8") : "";
|
|
91
97
|
const templateContent = await fs.readFile(templatePath, "utf8");
|
|
92
98
|
const envParsed = parseEnv(envContent);
|
|
93
99
|
const exampleParsed = parseEnv(templateContent);
|
|
@@ -118,13 +124,13 @@ cli
|
|
|
118
124
|
}
|
|
119
125
|
});
|
|
120
126
|
cli
|
|
121
|
-
.command("watch", "Watch for
|
|
127
|
+
.command("watch", "Watch for environment changes and automatically update template")
|
|
122
128
|
.action(async () => {
|
|
123
129
|
try {
|
|
124
130
|
await ensureGitignore();
|
|
125
|
-
const envPath =
|
|
126
|
-
if (!
|
|
127
|
-
console.error(pc.red("✖
|
|
131
|
+
const envPath = await findEnvFile();
|
|
132
|
+
if (!envPath) {
|
|
133
|
+
console.error(pc.red("✖ No environment file found to watch."));
|
|
128
134
|
process.exit(1);
|
|
129
135
|
}
|
|
130
136
|
await startWatcher();
|
|
@@ -175,5 +181,5 @@ cli
|
|
|
175
181
|
}
|
|
176
182
|
});
|
|
177
183
|
cli.help();
|
|
178
|
-
cli.version(
|
|
184
|
+
cli.version(version);
|
|
179
185
|
cli.parse();
|
package/dist/utils/template.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds the primary environment file in the current directory.
|
|
3
|
+
* Priority: .env > .env.local > .env.development > .env.production
|
|
4
|
+
* Excludes templates and test files.
|
|
5
|
+
*/
|
|
6
|
+
export declare function findEnvFile(): Promise<string | null>;
|
|
1
7
|
/**
|
|
2
8
|
* Finds the environment template file in the current directory.
|
|
3
9
|
* Returns the path to .env.example or .env.sample if found.
|
package/dist/utils/template.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
const ENV_VARIANTS = [
|
|
4
|
+
".env",
|
|
5
|
+
".env.local",
|
|
6
|
+
".env.development",
|
|
7
|
+
".env.production",
|
|
8
|
+
];
|
|
3
9
|
const TEMPLATE_VARIANTS = [".env.example", ".env.sample"];
|
|
10
|
+
/**
|
|
11
|
+
* Finds the primary environment file in the current directory.
|
|
12
|
+
* Priority: .env > .env.local > .env.development > .env.production
|
|
13
|
+
* Excludes templates and test files.
|
|
14
|
+
*/
|
|
15
|
+
export async function findEnvFile() {
|
|
16
|
+
for (const variant of ENV_VARIANTS) {
|
|
17
|
+
const filePath = path.resolve(process.cwd(), variant);
|
|
18
|
+
if (await fs.stat(filePath).catch(() => false)) {
|
|
19
|
+
return filePath;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
4
24
|
/**
|
|
5
25
|
* Finds the environment template file in the current directory.
|
|
6
26
|
* Returns the path to .env.example or .env.sample if found.
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "menv-npm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Managed environment variables for Node.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"menv": "./dist/index.js"
|
|
7
|
+
"menv": "./dist/index.js",
|
|
8
|
+
"menv-npm": "./dist/index.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist"
|
|
@@ -22,8 +23,19 @@
|
|
|
22
23
|
"validator",
|
|
23
24
|
"security"
|
|
24
25
|
],
|
|
25
|
-
"author": "",
|
|
26
|
+
"author": "Daniel Dallas Okoye <dev@thedanieldallas.com> (https://thedanieldallas.com)",
|
|
26
27
|
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/DanielDallas/menv.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://menv-npm.vercel.app",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/DanielDallas/menv/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
27
39
|
"dependencies": {
|
|
28
40
|
"cac": "^6.7.14",
|
|
29
41
|
"picocolors": "^1.0.0",
|