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 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
@@ -1,5 +1,5 @@
1
1
  import chokidar from "chokidar";
2
2
  /**
3
- * Watches for changes in .env and regenerates .env.example.
3
+ * Watches for changes in the primary environment file and regenerates .env.example.
4
4
  */
5
5
  export declare function startWatcher(): Promise<chokidar.FSWatcher>;
@@ -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 .env and regenerates .env.example.
9
+ * Watches for changes in the primary environment file and regenerates .env.example.
9
10
  */
10
11
  export async function startWatcher() {
11
- const envPath = path.resolve(process.cwd(), ".env");
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(".env")}...`));
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("⚡ .env changed, regenerating .env.example..."));
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
- { name: "AWS Access Key", regex: /AKIA[0-9A-Z]{16}/ },
3
- { name: "Stripe Secret Key", regex: /sk_live_[0-9a-zA-Z]{24}/ },
4
- { name: "GitHub Token", regex: /ghp_[0-9a-zA-Z]{36}/ },
5
- { name: "Private Key", regex: /-----BEGIN [A-Z ]+ PRIVATE KEY-----/ },
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.command("generate", "Generate .env.example from .env").action(async () => {
17
+ cli
18
+ .command("generate", "Generate .env.example from environment file")
19
+ .action(async () => {
15
20
  try {
16
21
  await ensureGitignore();
17
- const envPath = path.resolve(process.cwd(), ".env");
22
+ const envPath = await findEnvFile();
18
23
  const examplePath = path.resolve(process.cwd(), ".env.example");
19
- if (!(await fs.stat(envPath).catch(() => false))) {
20
- console.error(pc.red("✖ .env file not found in current directory."));
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("✔ .env.example generated successfully."));
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 .env and .env.example")
36
- .alias("synch")
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 = path.resolve(process.cwd(), ".env");
46
+ const envPath = await findEnvFile();
41
47
  const templatePath = await findTemplateFile();
42
- if (!(await fs.stat(envPath).catch(() => false))) {
43
- console.error(pc.red("✖ .env file not found."));
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 .env with ${templateName}...`));
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 .env.example exist in .env")
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 = path.resolve(process.cwd(), ".env");
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
- console.log(pc.dim(`\nValidating .env against ${templateName}...`));
88
- const envContent = (await fs.stat(envPath).catch(() => false))
89
- ? await fs.readFile(envPath, "utf8")
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 .env changes and automatically update .env.example")
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 = path.resolve(process.cwd(), ".env");
126
- if (!(await fs.stat(envPath).catch(() => false))) {
127
- console.error(pc.red("✖ .env file not found."));
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("0.1.0");
184
+ cli.version(version);
179
185
  cli.parse();
@@ -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.
@@ -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.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",