envertor 0.1.0__tar.gz

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.
envertor-0.1.0/LICENSE ADDED
File without changes
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: envertor
3
+ Version: 0.1.0
4
+ Summary: Generate example .env files from existing .env files with placeholder detection.
5
+ Author-email: Samin Yeasar <syeasar.kuet@gmail.com>
6
+ Project-URL: Homepage, https://github.com/Y3454R/envertor
7
+ Project-URL: Repository, https://github.com/Y3454R/envertor
8
+ Keywords: dotenv,env,environment,cli,devtools
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Environment :: Console
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # Envertor 🔍
19
+
20
+ Envertor is a CLI tool that generates `.env.example` files by extracting environment variables from existing `.env` files or by scanning Python and JavaScript/TypeScript projects. It also helps keep your secrets safe with automatic `.gitignore` protection and CI/CD-ready parity checks.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install envertor
26
+ ```
27
+
28
+ **Development install:**
29
+ ```bash
30
+ pip install -e .
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Usage
36
+
37
+ ### Generate `.env.example` from an existing `.env`
38
+
39
+ Strips real values and replaces them with type-appropriate placeholders:
40
+
41
+ ```bash
42
+ envertor -i .env -o .env.example
43
+ ```
44
+
45
+ | Original `.env` | Generated `.env.example` |
46
+ |---|---|
47
+ | `SECRET=mysecret` | `SECRET=''` |
48
+ | `PORT=8080` | `PORT=0` |
49
+ | `DEBUG=true` | `DEBUG=false` |
50
+ | `RATE=3.14` | `RATE=0.0` |
51
+
52
+ ---
53
+
54
+ ### Scan a project for environment variables
55
+
56
+ Detects env vars used in source code and generates `.env.example` from actual usage — no existing `.env` required:
57
+
58
+ ```bash
59
+ envertor -p ./my-project -o .env.example
60
+ ```
61
+
62
+ Limit scanning to a specific language:
63
+ ```bash
64
+ envertor -p ./my-project --lang python
65
+ envertor -p ./my-project --lang js
66
+ ```
67
+
68
+ Supports:
69
+ - **Python**: `os.getenv("VAR")`, `os.environ["VAR"]`, `os.environ.get("VAR")`
70
+ - **JS/TS**: `process.env.VAR` (`.js`, `.ts`, `.jsx`, `.tsx`)
71
+
72
+ ---
73
+
74
+ ### Create `.env` from `.env.example`
75
+
76
+ Bootstrap a local `.env` from the example file so teammates can fill in their own values:
77
+
78
+ ```bash
79
+ envertor --create-env # reads .env.example by default
80
+ envertor --create-env staging.env.example # reads a custom file
81
+ ```
82
+
83
+ If `.env` already exists, writes `.env.envertor` instead to avoid overwriting.
84
+
85
+ ---
86
+
87
+ ### Check parity between `.env` and `.env.example`
88
+
89
+ Explicitly verify that both files have the same keys. Designed for CI/CD pipelines — exits `1` on mismatch:
90
+
91
+ ```bash
92
+ envertor --check
93
+ envertor --check --env-file /deploy/.env --example-file /repo/.env.example
94
+ ```
95
+
96
+ Example output on failure:
97
+ ```
98
+ [envertor] FAIL: Keys mismatch between .env and .env.example
99
+ Missing from .env: NEW_KEY
100
+ Missing from .env.example: OLD_KEY
101
+ ```
102
+
103
+ Use in a pipeline:
104
+ ```yaml
105
+ # GitHub Actions example
106
+ - name: Check env parity
107
+ run: envertor --check --env-file .env --example-file .env.example
108
+ ```
109
+
110
+ ---
111
+
112
+ ### Show version
113
+
114
+ ```bash
115
+ envertor -v
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Automatic safety checks
121
+
122
+ Every time envertor runs, it performs these checks automatically:
123
+
124
+ **`.gitignore` protection** — ensures `.env` is listed in `.gitignore`. Creates the file if it doesn't exist:
125
+ ```
126
+ [envertor] Created .gitignore with .env
127
+ [envertor] Added .env to .gitignore
128
+ ```
129
+
130
+ **Key parity warning** — warns if `.env` and `.env.example` have different keys:
131
+ ```
132
+ [envertor] WARNING: Keys in .env not documented in .env.example: DB_URL
133
+ [envertor] WARNING: Keys in .env.example not found in .env: NEW_KEY
134
+ ```
135
+
136
+ **Leftover values warning** — warns if `.env.example` contains non-placeholder values (real secrets accidentally left behind):
137
+ ```
138
+ [envertor] WARNING: .env.example has a real value set for API_KEY
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Options
144
+
145
+ | Flag | Description |
146
+ |---|---|
147
+ | `-i, --input FILE` | Path to input `.env` file |
148
+ | `-o, --output FILE` | Path to output `.env.example` (default: `.env.example`) |
149
+ | `-p, --project DIR` | Project folder to scan for env variable usage |
150
+ | `--lang python\|js\|both` | Language filter for project scanning (default: `both`) |
151
+ | `--create-env [FILE]` | Create `.env` from `FILE` (default: `.env.example`) |
152
+ | `--check` | Check key parity and exit `1` on mismatch (CI/CD mode) |
153
+ | `--env-file FILE` | `.env` path for `--check` (default: `.env`) |
154
+ | `--example-file FILE` | `.env.example` path for `--check` (default: `.env.example`) |
155
+ | `-v, --version` | Show version |
156
+
157
+ ---
158
+
159
+ ## Notes
160
+
161
+ - Automatically skips `node_modules/`, `venv/`, `__pycache__/`, `.next/`, `.git/`, `.idea/`, `.vscode/`
162
+ - Regex-based scanning catches the most common patterns (`os.getenv`, `os.environ`, `process.env`)
163
+ - `.env.envertor` is a safe backup — rename it to `.env` or diff it against your existing one
@@ -0,0 +1,146 @@
1
+ # Envertor 🔍
2
+
3
+ Envertor is a CLI tool that generates `.env.example` files by extracting environment variables from existing `.env` files or by scanning Python and JavaScript/TypeScript projects. It also helps keep your secrets safe with automatic `.gitignore` protection and CI/CD-ready parity checks.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install envertor
9
+ ```
10
+
11
+ **Development install:**
12
+ ```bash
13
+ pip install -e .
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Usage
19
+
20
+ ### Generate `.env.example` from an existing `.env`
21
+
22
+ Strips real values and replaces them with type-appropriate placeholders:
23
+
24
+ ```bash
25
+ envertor -i .env -o .env.example
26
+ ```
27
+
28
+ | Original `.env` | Generated `.env.example` |
29
+ |---|---|
30
+ | `SECRET=mysecret` | `SECRET=''` |
31
+ | `PORT=8080` | `PORT=0` |
32
+ | `DEBUG=true` | `DEBUG=false` |
33
+ | `RATE=3.14` | `RATE=0.0` |
34
+
35
+ ---
36
+
37
+ ### Scan a project for environment variables
38
+
39
+ Detects env vars used in source code and generates `.env.example` from actual usage — no existing `.env` required:
40
+
41
+ ```bash
42
+ envertor -p ./my-project -o .env.example
43
+ ```
44
+
45
+ Limit scanning to a specific language:
46
+ ```bash
47
+ envertor -p ./my-project --lang python
48
+ envertor -p ./my-project --lang js
49
+ ```
50
+
51
+ Supports:
52
+ - **Python**: `os.getenv("VAR")`, `os.environ["VAR"]`, `os.environ.get("VAR")`
53
+ - **JS/TS**: `process.env.VAR` (`.js`, `.ts`, `.jsx`, `.tsx`)
54
+
55
+ ---
56
+
57
+ ### Create `.env` from `.env.example`
58
+
59
+ Bootstrap a local `.env` from the example file so teammates can fill in their own values:
60
+
61
+ ```bash
62
+ envertor --create-env # reads .env.example by default
63
+ envertor --create-env staging.env.example # reads a custom file
64
+ ```
65
+
66
+ If `.env` already exists, writes `.env.envertor` instead to avoid overwriting.
67
+
68
+ ---
69
+
70
+ ### Check parity between `.env` and `.env.example`
71
+
72
+ Explicitly verify that both files have the same keys. Designed for CI/CD pipelines — exits `1` on mismatch:
73
+
74
+ ```bash
75
+ envertor --check
76
+ envertor --check --env-file /deploy/.env --example-file /repo/.env.example
77
+ ```
78
+
79
+ Example output on failure:
80
+ ```
81
+ [envertor] FAIL: Keys mismatch between .env and .env.example
82
+ Missing from .env: NEW_KEY
83
+ Missing from .env.example: OLD_KEY
84
+ ```
85
+
86
+ Use in a pipeline:
87
+ ```yaml
88
+ # GitHub Actions example
89
+ - name: Check env parity
90
+ run: envertor --check --env-file .env --example-file .env.example
91
+ ```
92
+
93
+ ---
94
+
95
+ ### Show version
96
+
97
+ ```bash
98
+ envertor -v
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Automatic safety checks
104
+
105
+ Every time envertor runs, it performs these checks automatically:
106
+
107
+ **`.gitignore` protection** — ensures `.env` is listed in `.gitignore`. Creates the file if it doesn't exist:
108
+ ```
109
+ [envertor] Created .gitignore with .env
110
+ [envertor] Added .env to .gitignore
111
+ ```
112
+
113
+ **Key parity warning** — warns if `.env` and `.env.example` have different keys:
114
+ ```
115
+ [envertor] WARNING: Keys in .env not documented in .env.example: DB_URL
116
+ [envertor] WARNING: Keys in .env.example not found in .env: NEW_KEY
117
+ ```
118
+
119
+ **Leftover values warning** — warns if `.env.example` contains non-placeholder values (real secrets accidentally left behind):
120
+ ```
121
+ [envertor] WARNING: .env.example has a real value set for API_KEY
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Options
127
+
128
+ | Flag | Description |
129
+ |---|---|
130
+ | `-i, --input FILE` | Path to input `.env` file |
131
+ | `-o, --output FILE` | Path to output `.env.example` (default: `.env.example`) |
132
+ | `-p, --project DIR` | Project folder to scan for env variable usage |
133
+ | `--lang python\|js\|both` | Language filter for project scanning (default: `both`) |
134
+ | `--create-env [FILE]` | Create `.env` from `FILE` (default: `.env.example`) |
135
+ | `--check` | Check key parity and exit `1` on mismatch (CI/CD mode) |
136
+ | `--env-file FILE` | `.env` path for `--check` (default: `.env`) |
137
+ | `--example-file FILE` | `.env.example` path for `--check` (default: `.env.example`) |
138
+ | `-v, --version` | Show version |
139
+
140
+ ---
141
+
142
+ ## Notes
143
+
144
+ - Automatically skips `node_modules/`, `venv/`, `__pycache__/`, `.next/`, `.git/`, `.idea/`, `.vscode/`
145
+ - Regex-based scanning catches the most common patterns (`os.getenv`, `os.environ`, `process.env`)
146
+ - `.env.envertor` is a safe backup — rename it to `.env` or diff it against your existing one
@@ -0,0 +1,3 @@
1
+ from .core import generate_example_env
2
+ from .version import __version__
3
+
@@ -0,0 +1,64 @@
1
+ import os
2
+
3
+ SAFE_PLACEHOLDERS = {"", "''", '""', "0", "0.0", "false"}
4
+
5
+
6
+ def _parse_keys(filepath: str) -> set:
7
+ keys = set()
8
+ with open(filepath, "r") as f:
9
+ for line in f:
10
+ stripped = line.strip()
11
+ if not stripped or stripped.startswith("#"):
12
+ continue
13
+ if "=" in stripped:
14
+ key = stripped.split("=", 1)[0].strip()
15
+ keys.add(key)
16
+ return keys
17
+
18
+
19
+ def _is_safe_placeholder(value: str) -> bool:
20
+ value = value.strip()
21
+ if "#" in value:
22
+ value = value.split("#", 1)[0].strip()
23
+ return value.lower() in SAFE_PLACEHOLDERS
24
+
25
+
26
+ def check_key_parity(env_path: str, example_path: str, strict: bool = False):
27
+ env_keys = _parse_keys(env_path)
28
+ example_keys = _parse_keys(example_path)
29
+
30
+ missing_from_env = example_keys - env_keys
31
+ missing_from_example = env_keys - example_keys
32
+
33
+ if not missing_from_env and not missing_from_example:
34
+ if strict:
35
+ print(f"[envertor] OK: {env_path} and {example_path} keys match.")
36
+ return True
37
+ return None
38
+
39
+ if strict:
40
+ print(f"[envertor] FAIL: Keys mismatch between {env_path} and {example_path}")
41
+ if missing_from_env:
42
+ print(f" Missing from {env_path}: {', '.join(sorted(missing_from_env))}")
43
+ if missing_from_example:
44
+ print(f" Missing from {example_path}: {', '.join(sorted(missing_from_example))}")
45
+ return False
46
+
47
+ if missing_from_env:
48
+ print(f"[envertor] WARNING: Keys in {example_path} not found in {env_path}: {', '.join(sorted(missing_from_env))}")
49
+ if missing_from_example:
50
+ print(f"[envertor] WARNING: Keys in {env_path} not documented in {example_path}: {', '.join(sorted(missing_from_example))}")
51
+ return None
52
+
53
+
54
+ def check_example_values(example_path: str) -> None:
55
+ with open(example_path, "r") as f:
56
+ for line in f:
57
+ stripped = line.strip()
58
+ if not stripped or stripped.startswith("#"):
59
+ continue
60
+ if "=" in stripped:
61
+ key, value = stripped.split("=", 1)
62
+ key = key.strip()
63
+ if not _is_safe_placeholder(value):
64
+ print(f"[envertor] WARNING: {example_path} has a real value set for {key}")
@@ -0,0 +1,113 @@
1
+ import os
2
+ import shutil
3
+ import sys
4
+ import argparse
5
+
6
+ from .core import generate_example_env
7
+ from .scanner import scan_project
8
+ from .gitignore import ensure_env_in_gitignore
9
+ from .checker import check_key_parity, check_example_values
10
+ from .version import __version__
11
+
12
+
13
+ def main():
14
+ parser = argparse.ArgumentParser(
15
+ description="Envertor: Generate example .env files from existing .env or project"
16
+ )
17
+
18
+ parser.add_argument(
19
+ "-i", "--input",
20
+ default=None,
21
+ help="Path to input .env file"
22
+ )
23
+ parser.add_argument(
24
+ "-o", "--output",
25
+ default=".env.example",
26
+ help="Path to output example .env file"
27
+ )
28
+ parser.add_argument(
29
+ "-p", "--project",
30
+ default=None,
31
+ help="Project folder to auto-scan for env variables"
32
+ )
33
+ parser.add_argument(
34
+ "--lang",
35
+ choices=["python", "js", "both"],
36
+ default="both",
37
+ help="Languages to scan in project (default: both)"
38
+ )
39
+ parser.add_argument(
40
+ "-v", "--version",
41
+ action="store_true",
42
+ help="Show envertor version"
43
+ )
44
+ parser.add_argument(
45
+ "--create-env",
46
+ nargs="?",
47
+ const=".env.example",
48
+ default=None,
49
+ metavar="FILE",
50
+ help="Create .env from FILE (default: .env.example). Writes .env.envertor if .env already exists."
51
+ )
52
+ parser.add_argument(
53
+ "--check",
54
+ action="store_true",
55
+ help="Check that .env and .env.example have matching keys. Exits 1 on mismatch."
56
+ )
57
+ parser.add_argument(
58
+ "--env-file",
59
+ default=".env",
60
+ help="Path to .env file for --check (default: .env)"
61
+ )
62
+ parser.add_argument(
63
+ "--example-file",
64
+ default=".env.example",
65
+ help="Path to .env.example file for --check (default: .env.example)"
66
+ )
67
+
68
+ args = parser.parse_args()
69
+
70
+ if args.version:
71
+ print(f"envertor v{__version__}")
72
+ return
73
+
74
+ if args.check:
75
+ ok = check_key_parity(args.env_file, args.example_file, strict=True)
76
+ sys.exit(0 if ok else 1)
77
+
78
+ if args.create_env is not None:
79
+ source = args.create_env
80
+ if not os.path.exists(source):
81
+ print(f"[envertor] ERROR: Source file '{source}' not found.")
82
+ sys.exit(1)
83
+ dest = ".env" if not os.path.exists(".env") else ".env.envertor"
84
+ shutil.copy(source, dest)
85
+ print(f"[envertor] Created {dest} from {source}")
86
+ return
87
+
88
+ if args.project:
89
+ languages = ("python", "js") if args.lang == "both" else (args.lang,)
90
+ env_vars = scan_project(args.project, languages)
91
+ with open(args.output, "w") as f:
92
+ for key in sorted(env_vars):
93
+ f.write(f"{key}=\n")
94
+ print(f"Generated {args.output} from project scan.")
95
+ ensure_env_in_gitignore(args.project)
96
+ env_path = os.path.join(args.project, ".env")
97
+ if os.path.exists(env_path) and os.path.exists(args.output):
98
+ check_key_parity(env_path, args.output)
99
+ if os.path.exists(args.output):
100
+ check_example_values(args.output)
101
+
102
+ elif args.input:
103
+ generate_example_env(args.input, args.output)
104
+ print(f"Created {args.output} from {args.input}")
105
+ project_dir = os.path.dirname(os.path.abspath(args.input))
106
+ ensure_env_in_gitignore(project_dir)
107
+ if os.path.exists(args.input) and os.path.exists(args.output):
108
+ check_key_parity(args.input, args.output)
109
+ if os.path.exists(args.output):
110
+ check_example_values(args.output)
111
+
112
+ else:
113
+ print("Provide either --input or --project")
@@ -0,0 +1,62 @@
1
+ # envertor/core.py
2
+
3
+ def detect_placeholder(value):
4
+ value = value.strip()
5
+
6
+ if value.lower() in ["true", "false"]:
7
+ return "false"
8
+
9
+ try:
10
+ int(value)
11
+ return "0"
12
+ except ValueError:
13
+ pass
14
+
15
+ try:
16
+ float(value)
17
+ return "0.0"
18
+ except ValueError:
19
+ pass
20
+
21
+ return "''"
22
+
23
+
24
+ def generate_example_env(input_file, output_file):
25
+ with open(input_file, "r") as f:
26
+ lines = f.readlines()
27
+
28
+ new_lines = []
29
+
30
+ for line in lines:
31
+ stripped = line.strip()
32
+
33
+ if stripped.startswith("#") or not stripped:
34
+ new_lines.append(line)
35
+ continue
36
+
37
+ if "=" in line:
38
+ key, value_part = line.split("=", 1)
39
+ key = key.strip()
40
+
41
+ if "#" in value_part:
42
+ value, inline_comment = value_part.split("#", 1)
43
+ value = value.strip()
44
+ inline_comment = "# " + inline_comment.strip()
45
+ else:
46
+ value = value_part.strip()
47
+ inline_comment = ""
48
+
49
+ placeholder = detect_placeholder(value)
50
+
51
+ if inline_comment:
52
+ new_line = f"{key}={placeholder} {inline_comment}\n"
53
+ else:
54
+ new_line = f"{key}={placeholder}\n"
55
+
56
+ new_lines.append(new_line)
57
+ else:
58
+ new_lines.append(line)
59
+
60
+ with open(output_file, "w") as f:
61
+ f.writelines(new_lines)
62
+
@@ -0,0 +1,25 @@
1
+ import os
2
+
3
+
4
+ def ensure_env_in_gitignore(project_path: str) -> None:
5
+ gitignore_path = os.path.join(project_path, ".gitignore")
6
+
7
+ if not os.path.exists(gitignore_path):
8
+ with open(gitignore_path, "w") as f:
9
+ f.write(".env\n")
10
+ print("[envertor] Created .gitignore with .env")
11
+ return
12
+
13
+ with open(gitignore_path, "r") as f:
14
+ lines = f.readlines()
15
+
16
+ for line in lines:
17
+ stripped = line.strip()
18
+ if stripped in (".env", "*.env"):
19
+ return
20
+
21
+ with open(gitignore_path, "a") as f:
22
+ if lines and not lines[-1].endswith("\n"):
23
+ f.write("\n")
24
+ f.write(".env\n")
25
+ print("[envertor] Added .env to .gitignore")
@@ -0,0 +1,74 @@
1
+ # envertor/scanner.py
2
+ import os
3
+ import re
4
+
5
+ # Directories that should NEVER be scanned
6
+ SKIP_DIRS = {
7
+ "node_modules",
8
+ "venv",
9
+ "__pycache__",
10
+ ".next",
11
+ ".git",
12
+ ".idea",
13
+ ".vscode"
14
+ }
15
+
16
+ # Regex patterns
17
+ PY_GETENV_PATTERN = re.compile(r'os\.getenv\(\s*[\'"]([A-Z0-9_]+)[\'"]\s*\)')
18
+ PY_ENVIRON_PATTERN = re.compile(
19
+ r'os\.environ(?:\.get|\[)\s*[\'"]([A-Z0-9_]+)[\'"]\s*[\]\)]'
20
+ )
21
+ JS_ENV_PATTERN = re.compile(r'process\.env\.([A-Z0-9_]+)')
22
+
23
+ def scan_python_file(path: str) -> set[str]:
24
+ """Extract env vars from a Python file."""
25
+ env_vars = set()
26
+ try:
27
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
28
+ code = f.read()
29
+
30
+ env_vars.update(PY_GETENV_PATTERN.findall(code))
31
+ env_vars.update(PY_ENVIRON_PATTERN.findall(code))
32
+ except Exception:
33
+ pass
34
+
35
+ return env_vars
36
+
37
+ def scan_js_file(path: str) -> set[str]:
38
+ """Extract env vars from a JS/TS file."""
39
+ env_vars = set()
40
+ try:
41
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
42
+ code = f.read()
43
+
44
+ env_vars.update(JS_ENV_PATTERN.findall(code))
45
+ except Exception:
46
+ pass
47
+
48
+ return env_vars
49
+
50
+ def scan_project(project_path: str, languages=("python", "js")) -> set[str]:
51
+ """
52
+ Scan a project directory for environment variable usage.
53
+
54
+ :param project_path: Root directory of project
55
+ :param languages: ("python", "js") or ("python",) or ("js",)
56
+ :return: Set of environment variable names
57
+ """
58
+ env_vars = set()
59
+
60
+ for root, dirs, files in os.walk(project_path):
61
+ # 🚫 Skip unwanted directories
62
+ dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
63
+
64
+ for file in files:
65
+ full_path = os.path.join(root, file)
66
+
67
+ if "python" in languages and file.endswith(".py"):
68
+ env_vars.update(scan_python_file(full_path))
69
+
70
+ if "js" in languages and file.endswith((".js", ".ts", ".jsx", ".tsx")):
71
+ env_vars.update(scan_js_file(full_path))
72
+
73
+ return env_vars
74
+
@@ -0,0 +1,2 @@
1
+ __version__ = "0.1.0"
2
+
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: envertor
3
+ Version: 0.1.0
4
+ Summary: Generate example .env files from existing .env files with placeholder detection.
5
+ Author-email: Samin Yeasar <syeasar.kuet@gmail.com>
6
+ Project-URL: Homepage, https://github.com/Y3454R/envertor
7
+ Project-URL: Repository, https://github.com/Y3454R/envertor
8
+ Keywords: dotenv,env,environment,cli,devtools
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Environment :: Console
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # Envertor 🔍
19
+
20
+ Envertor is a CLI tool that generates `.env.example` files by extracting environment variables from existing `.env` files or by scanning Python and JavaScript/TypeScript projects. It also helps keep your secrets safe with automatic `.gitignore` protection and CI/CD-ready parity checks.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install envertor
26
+ ```
27
+
28
+ **Development install:**
29
+ ```bash
30
+ pip install -e .
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Usage
36
+
37
+ ### Generate `.env.example` from an existing `.env`
38
+
39
+ Strips real values and replaces them with type-appropriate placeholders:
40
+
41
+ ```bash
42
+ envertor -i .env -o .env.example
43
+ ```
44
+
45
+ | Original `.env` | Generated `.env.example` |
46
+ |---|---|
47
+ | `SECRET=mysecret` | `SECRET=''` |
48
+ | `PORT=8080` | `PORT=0` |
49
+ | `DEBUG=true` | `DEBUG=false` |
50
+ | `RATE=3.14` | `RATE=0.0` |
51
+
52
+ ---
53
+
54
+ ### Scan a project for environment variables
55
+
56
+ Detects env vars used in source code and generates `.env.example` from actual usage — no existing `.env` required:
57
+
58
+ ```bash
59
+ envertor -p ./my-project -o .env.example
60
+ ```
61
+
62
+ Limit scanning to a specific language:
63
+ ```bash
64
+ envertor -p ./my-project --lang python
65
+ envertor -p ./my-project --lang js
66
+ ```
67
+
68
+ Supports:
69
+ - **Python**: `os.getenv("VAR")`, `os.environ["VAR"]`, `os.environ.get("VAR")`
70
+ - **JS/TS**: `process.env.VAR` (`.js`, `.ts`, `.jsx`, `.tsx`)
71
+
72
+ ---
73
+
74
+ ### Create `.env` from `.env.example`
75
+
76
+ Bootstrap a local `.env` from the example file so teammates can fill in their own values:
77
+
78
+ ```bash
79
+ envertor --create-env # reads .env.example by default
80
+ envertor --create-env staging.env.example # reads a custom file
81
+ ```
82
+
83
+ If `.env` already exists, writes `.env.envertor` instead to avoid overwriting.
84
+
85
+ ---
86
+
87
+ ### Check parity between `.env` and `.env.example`
88
+
89
+ Explicitly verify that both files have the same keys. Designed for CI/CD pipelines — exits `1` on mismatch:
90
+
91
+ ```bash
92
+ envertor --check
93
+ envertor --check --env-file /deploy/.env --example-file /repo/.env.example
94
+ ```
95
+
96
+ Example output on failure:
97
+ ```
98
+ [envertor] FAIL: Keys mismatch between .env and .env.example
99
+ Missing from .env: NEW_KEY
100
+ Missing from .env.example: OLD_KEY
101
+ ```
102
+
103
+ Use in a pipeline:
104
+ ```yaml
105
+ # GitHub Actions example
106
+ - name: Check env parity
107
+ run: envertor --check --env-file .env --example-file .env.example
108
+ ```
109
+
110
+ ---
111
+
112
+ ### Show version
113
+
114
+ ```bash
115
+ envertor -v
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Automatic safety checks
121
+
122
+ Every time envertor runs, it performs these checks automatically:
123
+
124
+ **`.gitignore` protection** — ensures `.env` is listed in `.gitignore`. Creates the file if it doesn't exist:
125
+ ```
126
+ [envertor] Created .gitignore with .env
127
+ [envertor] Added .env to .gitignore
128
+ ```
129
+
130
+ **Key parity warning** — warns if `.env` and `.env.example` have different keys:
131
+ ```
132
+ [envertor] WARNING: Keys in .env not documented in .env.example: DB_URL
133
+ [envertor] WARNING: Keys in .env.example not found in .env: NEW_KEY
134
+ ```
135
+
136
+ **Leftover values warning** — warns if `.env.example` contains non-placeholder values (real secrets accidentally left behind):
137
+ ```
138
+ [envertor] WARNING: .env.example has a real value set for API_KEY
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Options
144
+
145
+ | Flag | Description |
146
+ |---|---|
147
+ | `-i, --input FILE` | Path to input `.env` file |
148
+ | `-o, --output FILE` | Path to output `.env.example` (default: `.env.example`) |
149
+ | `-p, --project DIR` | Project folder to scan for env variable usage |
150
+ | `--lang python\|js\|both` | Language filter for project scanning (default: `both`) |
151
+ | `--create-env [FILE]` | Create `.env` from `FILE` (default: `.env.example`) |
152
+ | `--check` | Check key parity and exit `1` on mismatch (CI/CD mode) |
153
+ | `--env-file FILE` | `.env` path for `--check` (default: `.env`) |
154
+ | `--example-file FILE` | `.env.example` path for `--check` (default: `.env.example`) |
155
+ | `-v, --version` | Show version |
156
+
157
+ ---
158
+
159
+ ## Notes
160
+
161
+ - Automatically skips `node_modules/`, `venv/`, `__pycache__/`, `.next/`, `.git/`, `.idea/`, `.vscode/`
162
+ - Regex-based scanning catches the most common patterns (`os.getenv`, `os.environ`, `process.env`)
163
+ - `.env.envertor` is a safe backup — rename it to `.env` or diff it against your existing one
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ envertor/__init__.py
5
+ envertor/checker.py
6
+ envertor/cli.py
7
+ envertor/core.py
8
+ envertor/gitignore.py
9
+ envertor/scanner.py
10
+ envertor/version.py
11
+ envertor.egg-info/PKG-INFO
12
+ envertor.egg-info/SOURCES.txt
13
+ envertor.egg-info/dependency_links.txt
14
+ envertor.egg-info/entry_points.txt
15
+ envertor.egg-info/top_level.txt
16
+ tests/test_basic.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ envertor = envertor.cli:main
@@ -0,0 +1 @@
1
+ envertor
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "envertor"
3
+ version = "0.1.0"
4
+ description = "Generate example .env files from existing .env files with placeholder detection."
5
+ authors = [
6
+ { name = "Samin Yeasar", email = "syeasar.kuet@gmail.com" }
7
+ ]
8
+ readme = "README.md"
9
+ license = { file = "LICENSE" }
10
+ requires-python = ">=3.8"
11
+ dependencies = []
12
+ keywords = ["dotenv", "env", "environment", "cli", "devtools"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Environment :: Console",
17
+ "Topic :: Software Development :: Build Tools",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/Y3454R/envertor"
22
+ Repository = "https://github.com/Y3454R/envertor"
23
+
24
+ [project.scripts]
25
+ envertor = "envertor.cli:main"
26
+
27
+ [build-system]
28
+ requires = ["setuptools>=61.0"]
29
+ build-backend = "setuptools.build_meta"
30
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,109 @@
1
+ import os
2
+ import tempfile
3
+ import pytest
4
+
5
+ from envertor.core import detect_placeholder
6
+ from envertor.gitignore import ensure_env_in_gitignore
7
+ from envertor.checker import check_key_parity, check_example_values
8
+
9
+
10
+ def test_placeholder_detection():
11
+ assert detect_placeholder("123") == "0"
12
+ assert detect_placeholder("3.14") == "0.0"
13
+ assert detect_placeholder("true") == "false"
14
+ assert detect_placeholder("hello") == "''"
15
+
16
+
17
+ # --- ensure_env_in_gitignore ---
18
+
19
+ def test_gitignore_created_when_missing():
20
+ with tempfile.TemporaryDirectory() as d:
21
+ ensure_env_in_gitignore(d)
22
+ gitignore = os.path.join(d, ".gitignore")
23
+ assert os.path.exists(gitignore)
24
+ assert ".env" in open(gitignore).read()
25
+
26
+
27
+ def test_gitignore_appended_when_env_missing():
28
+ with tempfile.TemporaryDirectory() as d:
29
+ gitignore = os.path.join(d, ".gitignore")
30
+ with open(gitignore, "w") as f:
31
+ f.write("node_modules/\n")
32
+ ensure_env_in_gitignore(d)
33
+ content = open(gitignore).read()
34
+ assert ".env" in content
35
+ assert "node_modules/" in content
36
+
37
+
38
+ def test_gitignore_no_duplicate_when_env_present():
39
+ with tempfile.TemporaryDirectory() as d:
40
+ gitignore = os.path.join(d, ".gitignore")
41
+ with open(gitignore, "w") as f:
42
+ f.write(".env\nnode_modules/\n")
43
+ ensure_env_in_gitignore(d)
44
+ content = open(gitignore).read()
45
+ assert content.count(".env") == 1
46
+
47
+
48
+ # --- check_key_parity ---
49
+
50
+ def _write(path, content):
51
+ with open(path, "w") as f:
52
+ f.write(content)
53
+
54
+
55
+ def test_parity_match(capsys):
56
+ with tempfile.TemporaryDirectory() as d:
57
+ env = os.path.join(d, ".env")
58
+ example = os.path.join(d, ".env.example")
59
+ _write(env, "KEY_A=secret\nKEY_B=123\n")
60
+ _write(example, "KEY_A=''\nKEY_B=0\n")
61
+ result = check_key_parity(env, example, strict=True)
62
+ assert result is True
63
+
64
+
65
+ def test_parity_mismatch_strict(capsys):
66
+ with tempfile.TemporaryDirectory() as d:
67
+ env = os.path.join(d, ".env")
68
+ example = os.path.join(d, ".env.example")
69
+ _write(env, "KEY_A=secret\nKEY_C=extra\n")
70
+ _write(example, "KEY_A=''\nKEY_B=''\n")
71
+ result = check_key_parity(env, example, strict=True)
72
+ assert result is False
73
+ out = capsys.readouterr().out
74
+ assert "KEY_B" in out
75
+ assert "KEY_C" in out
76
+
77
+
78
+ def test_parity_mismatch_warns(capsys):
79
+ with tempfile.TemporaryDirectory() as d:
80
+ env = os.path.join(d, ".env")
81
+ example = os.path.join(d, ".env.example")
82
+ _write(env, "KEY_A=secret\n")
83
+ _write(example, "KEY_A=''\nUNDOCUMENTED=''\n")
84
+ check_key_parity(env, example, strict=False)
85
+ out = capsys.readouterr().out
86
+ assert "WARNING" in out
87
+ assert "UNDOCUMENTED" in out
88
+
89
+
90
+ # --- check_example_values ---
91
+
92
+ def test_example_values_safe(capsys):
93
+ with tempfile.TemporaryDirectory() as d:
94
+ example = os.path.join(d, ".env.example")
95
+ _write(example, "KEY_A=''\nKEY_B=0\nKEY_C=false\nKEY_D=\n")
96
+ check_example_values(example)
97
+ out = capsys.readouterr().out
98
+ assert "WARNING" not in out
99
+
100
+
101
+ def test_example_values_warns_on_real_value(capsys):
102
+ with tempfile.TemporaryDirectory() as d:
103
+ example = os.path.join(d, ".env.example")
104
+ _write(example, "KEY_A=real_secret\nKEY_B=''\n")
105
+ check_example_values(example)
106
+ out = capsys.readouterr().out
107
+ assert "WARNING" in out
108
+ assert "KEY_A" in out
109
+ assert "KEY_B" not in out