envshield 4.2.0__tar.gz → 4.4.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.
- {envshield-4.2.0 → envshield-4.4.0}/PKG-INFO +111 -23
- {envshield-4.2.0 → envshield-4.4.0}/README.md +109 -21
- envshield-4.4.0/envshield/__init__.py +1 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/cli.py +222 -61
- {envshield-4.2.0 → envshield-4.4.0}/envshield/config/manager.py +30 -14
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/doctor.py +49 -11
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/exceptions.py +9 -5
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/generator.py +25 -12
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/hooks_manager.py +4 -4
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/importer.py +7 -8
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/inspector.py +3 -2
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/scanner.py +87 -25
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/schema_manager.py +72 -9
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/schema_types.py +1 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/service_discovery.py +80 -10
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/setup_manager.py +18 -13
- {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_base.py +1 -1
- {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_docker_compose.py +5 -5
- {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_dotenv.py +2 -1
- {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_kubernetes.py +4 -3
- {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_python.py +1 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_doctor.py +6 -11
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_generator.py +10 -4
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_importer.py +6 -15
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_scanner_compliance.py +8 -20
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_schema_manager.py +28 -44
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_schema_types.py +8 -3
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_service_discovery.py +65 -6
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_service_manager.py +10 -8
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_setup_manager.py +13 -21
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/parsers/test_docker_compose_parser.py +9 -22
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/parsers/test_kubernetes_parser.py +4 -10
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/parsers/test_python_parser.py +1 -9
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/test_cli.py +247 -11
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/test_config_manager.py +57 -38
- envshield-4.2.0/envshield/tests/test_c6_diff_aware_scanning.py → envshield-4.4.0/envshield/tests/test_diff_aware_scanning.py +24 -12
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/test_git_utils.py +15 -5
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/test_service_cli.py +27 -2
- {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/PKG-INFO +111 -23
- {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/SOURCES.txt +1 -1
- {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/requires.txt +1 -1
- {envshield-4.2.0 → envshield-4.4.0}/pyproject.toml +19 -2
- envshield-4.2.0/envshield/__init__.py +0 -1
- {envshield-4.2.0 → envshield-4.4.0}/LICENSE.md +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/__main__.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/config/__init.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/file_updater.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/core/service_manager.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_deployment.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/factory.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/state.py +1 -1
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/conftest.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_file_updater.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/parsers/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/parsers/test_dotenv_parser.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/utils/__init__.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield/utils/git_utils.py +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/dependency_links.txt +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/entry_points.txt +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/top_level.txt +0 -0
- {envshield-4.2.0 → envshield-4.4.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: envshield
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
4
4
|
Summary: EnvShield: A CLI for secure local environment management and secret prevention.
|
|
5
5
|
Author-email: Rabbil Yasar Sajal <rabbilyasar@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -47,7 +47,7 @@ Requires-Dist: questionary<3.0,>=2.0
|
|
|
47
47
|
Requires-Dist: typer[all]<1.0,>=0.9
|
|
48
48
|
Requires-Dist: toml<1.0,>=0.10
|
|
49
49
|
Provides-Extra: dev
|
|
50
|
-
Requires-Dist: ruff; extra == "dev"
|
|
50
|
+
Requires-Dist: ruff<0.17,>=0.16; extra == "dev"
|
|
51
51
|
Requires-Dist: pytest; extra == "dev"
|
|
52
52
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
53
53
|
Requires-Dist: bump2version; extra == "dev"
|
|
@@ -84,12 +84,15 @@ It works the same way whether you have one repo with one `.env` file, or a monor
|
|
|
84
84
|
- [Installation](#installation)
|
|
85
85
|
- [Quick start](#quick-start)
|
|
86
86
|
- [A single service](#a-single-service)
|
|
87
|
-
- [A monorepo with multiple services](#a-monorepo-with-multiple-services)
|
|
87
|
+
- [A monorepo with multiple services (monorepo)](#a-monorepo-with-multiple-services)
|
|
88
88
|
- [Core concept: the schema is the contract](#core-concept-the-schema-is-the-contract)
|
|
89
89
|
- [Every field a variable can have](#every-field-a-variable-can-have)
|
|
90
90
|
- [Conditional requirements (`requiredIf`)](#conditional-requirements-requiredif)
|
|
91
|
-
- [Sharing variables across services (`extends`)](#sharing-variables-across-services-extends)
|
|
91
|
+
- [Sharing variables across services (monorepo, `extends`)](#sharing-variables-across-services-extends)
|
|
92
92
|
- [Command reference](#command-reference)
|
|
93
|
+
- [Core commands](#core-commands)
|
|
94
|
+
- [Monorepo: managing multiple services (monorepo)](#monorepo-managing-multiple-services)
|
|
95
|
+
- [Machine-readable output (`--json`)](#machine-readable-output---json)
|
|
93
96
|
- [Typed config code generation](#typed-config-code-generation)
|
|
94
97
|
- [Validating deployment manifests](#validating-deployment-manifests)
|
|
95
98
|
- [Secret scanning and git hooks](#secret-scanning-and-git-hooks)
|
|
@@ -138,22 +141,28 @@ envshield --version
|
|
|
138
141
|
|
|
139
142
|
### A single service
|
|
140
143
|
|
|
141
|
-
The common case: one repo, one `.env` file.
|
|
144
|
+
The common case: one repo, one `.env` file. This is the whole workflow — nothing else in this README is required to get full value out of EnvShield.
|
|
142
145
|
|
|
143
146
|
```bash
|
|
144
147
|
cd my-project
|
|
145
|
-
envshield init # Detects your framework
|
|
146
|
-
envshield import .env # Or: seed the schema from an existing .env file
|
|
148
|
+
envshield init # Detects your framework and builds env.schema.toml from your real config
|
|
147
149
|
envshield setup # Interactive wizard: fills in .env from the schema
|
|
148
150
|
envshield generate --lang python # Generates a typed config.py (or config.ts for TypeScript)
|
|
149
151
|
```
|
|
150
152
|
|
|
153
|
+
`init` looks for a real config source first — an existing `.env`, `.env.example`, or a recognizable Python config module (`config/settings.py` and similar) — and builds the schema from its actual variables, classifying each as secret or not, with a suggested default and, where the value's shape is unambiguous, an inferred type. Only a genuinely fresh project with nothing to read yet falls back to a generic framework template:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
Found config/settings.py -- building your schema from its real variables.
|
|
157
|
+
✓ Created/updated schema: env.schema.toml
|
|
158
|
+
```
|
|
159
|
+
|
|
151
160
|
`init` also offers to install a git pre-commit hook (secret scanning) and a post-merge hook (drift check after every `git pull`) — say yes unless you already have your own hook-management tooling (Husky, `pre-commit`, etc.), in which case EnvShield will detect it and won't clobber it (see [Secret scanning and git hooks](#secret-scanning-and-git-hooks)).
|
|
152
161
|
|
|
153
|
-
|
|
162
|
+
`envshield import <file>` does the same real-variable analysis `init` runs automatically, as its own command — reach for it later, when you want to re-import after adding new variables to your code, point at a file `init` wouldn't have found on its own, or add `--interactive` to confirm each classification by hand instead of accepting the automatic guess:
|
|
154
163
|
|
|
155
164
|
```bash
|
|
156
|
-
envshield import .env
|
|
165
|
+
envshield import .env --interactive
|
|
157
166
|
```
|
|
158
167
|
|
|
159
168
|
```
|
|
@@ -166,8 +175,14 @@ Analyzing variables...
|
|
|
166
175
|
- Inferred a type (int/port/bool/url/email) for 3 variable(s).
|
|
167
176
|
```
|
|
168
177
|
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
**Everything above is the complete single-service story.** Everything below this point — multiple services, schema composition, deployment manifests with more than one container — is opt-in, and only relevant once your repo actually has more than one service. Skip straight to [Core concept: the schema is the contract](#core-concept-the-schema-is-the-contract) if that's not you yet.
|
|
181
|
+
|
|
169
182
|
### A monorepo with multiple services
|
|
170
183
|
|
|
184
|
+
*(Optional — skip this if you only have one service.)*
|
|
185
|
+
|
|
171
186
|
If your repo has more than one service (an API, a web frontend, a worker), run `service discover` at the repo root instead of `init`:
|
|
172
187
|
|
|
173
188
|
```bash
|
|
@@ -300,6 +315,8 @@ With `FEATURE_X_ENABLED=false`, `FEATURE_X_API_KEY` is optional — `setup` won'
|
|
|
300
315
|
|
|
301
316
|
### Sharing variables across services (`extends`)
|
|
302
317
|
|
|
318
|
+
*(Monorepo-only — skip this if you have one service.)*
|
|
319
|
+
|
|
303
320
|
A monorepo with ten services usually has five or six variables every single one of them needs — `LOG_LEVEL`, `SENTRY_DSN`, `DATADOG_API_KEY` — and copy-pasting the same `[LOG_LEVEL]` block into ten schema files is exactly the kind of drift EnvShield exists to prevent.
|
|
304
321
|
|
|
305
322
|
Factor them into a shared base schema, and have each service extend it:
|
|
@@ -336,22 +353,21 @@ Loading `services/api/env.schema.toml` now transparently gives you `LOG_LEVEL`,
|
|
|
336
353
|
|
|
337
354
|
## Command reference
|
|
338
355
|
|
|
339
|
-
|
|
356
|
+
### Core commands
|
|
357
|
+
|
|
358
|
+
Everything a single-service project ever needs. `--service` shows up on most of these for the multi-service case (see below), but it's entirely optional until you actually have more than one service.
|
|
340
359
|
|
|
341
360
|
| Command | What it does |
|
|
342
361
|
|---|---|
|
|
343
|
-
| `envshield init [--force/-f]` | Detects your framework
|
|
344
|
-
| `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` |
|
|
345
|
-
| `envshield check [file] [--service NAME] [--container NAME]` | Validates a local file (or, if omitted, the project's/service's default local file *and* its registered deployment manifest, if any) against the schema. `file` can be a plain `.env`, a Python config module, a docker-compose file, or a Kubernetes manifest. `--container` picks which service/container to check in a manifest that declares more than one (tried against `--service`'s name automatically first). Exits non-zero on any drift — safe to use as a CI gate. |
|
|
346
|
-
| `envshield doctor [--fix] [--service NAME]` | Runs every health check at once (see below) and reports a summary. `--fix` interactively offers to fix whatever it can — re-running `init`, regenerating the template, installing the git hook, or running `setup` to fill in missing/invalid local values. Exits non-zero if anything's still broken afterward. |
|
|
362
|
+
| `envshield init [--force/-f]` | Detects your framework and builds `env.schema.toml` from a real config source if it finds one, otherwise a framework-aware template. Also scaffolds `envshield.yml`, updates `.gitignore`, and offers to install git hooks. Auto-registers a root-level `docker-compose.yml` as the project's deployment manifest if it finds one. `--force` re-runs on a project that already has a config (with a confirmation before overwriting). |
|
|
363
|
+
| `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` | Runs the same real-variable analysis `init` does automatically, as its own command — for re-importing after your code gains new variables, pointing at a file `init` wouldn't have found, or adding `--interactive` to confirm each secret/type classification by hand instead of accepting the automatic guess. `--output` changes where the schema is written (defaults to `env.schema.toml`, or the target service's schema path with `--service`). |
|
|
364
|
+
| `envshield check [file] [--service NAME] [--container NAME] [--json]` | Validates a local file (or, if omitted, the project's/service's default local file *and* its registered deployment manifest, if any) against the schema. `file` can be a plain `.env`, a Python config module, a docker-compose file, or a Kubernetes manifest. `--container` picks which service/container to check in a manifest that declares more than one (tried against `--service`'s name automatically first). Exits non-zero on any drift — safe to use as a CI gate. `--json` prints a machine-readable result instead (see below) and never falls into an interactive service picker. |
|
|
365
|
+
| `envshield doctor [--fix] [--service NAME] [--json]` | Runs every health check at once (see below) and reports a summary. `--fix` interactively offers to fix whatever it can — re-running `init`, regenerating the template, installing the git hook, or running `setup` to fill in missing/invalid local values. Exits non-zero if anything's still broken afterward. `--json` is incompatible with `--fix` (an interactive confirm prompt makes no sense in a machine-readable mode). |
|
|
347
366
|
| `envshield setup [output_file] [--service NAME]` | Interactive onboarding wizard: walks through every variable that's missing, blank, or has an existing value the schema no longer allows, prompting with the variable's description, masking secret input, and offering a picker for `enum` fields. Leaves everything already correct untouched. |
|
|
348
|
-
| `envshield schema sync [--service NAME]` | Regenerates `.env.example` from the schema (a dotenv project), or patches a Python-module local file in place to declare any schema variable it's missing (never rewrites it wholesale — only appends/patches the specific lines it owns). |
|
|
349
|
-
| `envshield generate [output_file] [--lang/-l python\|typescript] [--force/-f] [--service NAME]` | Compiles the schema into a typed, validated config module. `--lang` is auto-detected from your project (Next.js/Vite/Node.js → TypeScript,
|
|
350
|
-
| `envshield scan [paths...] [--staged] [--config/-c PATH] [--exclude/-e PATTERN]
|
|
367
|
+
| `envshield schema sync [--service NAME]` | Regenerates `.env.example` from the schema (a dotenv project), or patches a Python-module local file in place to declare any schema variable it's missing (never rewrites it wholesale — only appends/patches the specific lines it owns). `import` already calls this automatically for you when it changes a project's/service's real schema, so you'll rarely need to run it by hand except after a manual schema edit. |
|
|
368
|
+
| `envshield generate [output_file] [--lang/-l python\|typescript] [--force/-f] [--service NAME]` | Compiles the schema into a typed, validated config module. `--lang` is auto-detected from your project (Next.js/Vite/Node.js → TypeScript; Python/Django/Flask, or nothing detected → Python) if omitted. A detected ecosystem with no codegen target at all (currently: Go) errors and asks for `--lang` explicitly, rather than silently guessing Python. Defaults to writing `config.py`/`config.ts`; `--force` overwrites an existing output file. See [Typed config code generation](#typed-config-code-generation). |
|
|
369
|
+
| `envshield scan [paths...] [--staged] [--config/-c PATH] [--exclude/-e PATTERN] [--service NAME] [--json]` | Scans code for hardcoded secrets and for env vars used in code (`os.getenv`, `os.environ.get`, `process.env.X`) but never declared in the schema. `--staged` scans only what's staged for the next commit (what the pre-commit hook runs); `--exclude` (repeatable) adds glob patterns to skip, on top of whatever `secret_scanning.exclude_files` is set in `envshield.yml`. See [Secret scanning and git hooks](#secret-scanning-and-git-hooks). |
|
|
351
370
|
| `envshield install-hook` | Installs both git hooks by hand, without going through `init`/`setup`/`service discover`'s interactive prompt. |
|
|
352
|
-
| `envshield service list` | Lists every service currently configured in `envshield.yml`, with its schema and local file paths. |
|
|
353
|
-
| `envshield service add <name> <directory> [--local-file PATH] [--example-file PATH] [--description/-d TEXT] [--schema PATH] [--import FILE] [--deployment-manifest PATH] [--container NAME]` | Registers one service by hand. `--local-file` is required when the service's real config isn't a dotenv file (e.g. a Python module) — see below. `--import` seeds the new service's schema from an existing config file in one step. `--deployment-manifest` is auto-detected (a compose file in the given directory, or the project root) if not given explicitly. |
|
|
354
|
-
| `envshield service discover [root] [--yes/-y]` | Scans for service-like directories not already registered, and offers to add them — see [Quick start](#a-monorepo-with-multiple-services). `--yes` skips the interactive confirmation, for CI/scripting. |
|
|
355
371
|
| `envshield --version` / `-v` | Prints the installed version and exits. |
|
|
356
372
|
|
|
357
373
|
**Not using a `.env` file at all?** Some projects (a Flask app whose local config is a checked-in Python module, for example) don't use dotenv at all. Point `local_file` at it instead, and EnvShield reads and writes it as source code, not as a dotenv file — appending or patching only the specific assignments it owns, never touching anything else in the file:
|
|
@@ -363,6 +379,73 @@ services:
|
|
|
363
379
|
local_file: athena/config/env_config.local.py
|
|
364
380
|
```
|
|
365
381
|
|
|
382
|
+
### Monorepo: managing multiple services
|
|
383
|
+
|
|
384
|
+
*(Only relevant once your repo has more than one service — see [A monorepo with multiple services](#a-monorepo-with-multiple-services).)* Every core command above already accepts `--service`: automatically, if there's only one service configured; interactively (or against every service at once, via "All services"), if there's more than one.
|
|
385
|
+
|
|
386
|
+
| Command | What it does |
|
|
387
|
+
|---|---|
|
|
388
|
+
| `envshield service list` | Lists every service currently configured in `envshield.yml`, with its schema and local file paths. |
|
|
389
|
+
| `envshield service add <name> <directory> [--local-file PATH] [--example-file PATH] [--description/-d TEXT] [--schema PATH] [--import FILE] [--deployment-manifest PATH] [--container NAME]` | Registers one service by hand. `--local-file` is required when the service's real config isn't a dotenv file (e.g. a Python module) — see above. `--import` seeds the new service's schema from an existing config file in one step. `--deployment-manifest` is auto-detected (a compose file in the given directory or the project root that actually declares this service) if not given explicitly. |
|
|
390
|
+
| `envshield service discover [root] [--yes/-y]` | Scans for service-like directories not already registered, and offers to add them — see [Quick start](#a-monorepo-with-multiple-services). `--yes` skips the interactive confirmation, for CI/scripting. |
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Machine-readable output (`--json`)
|
|
395
|
+
|
|
396
|
+
`check`, `doctor`, and `scan` all accept `--json`: every Rich table/panel/progress-bar is suppressed, and exactly one JSON object is printed to stdout instead. Exit codes are unchanged (non-zero on any issue), so `--json` is a drop-in for CI gates, dashboards, or an agent loop that needs to branch on the result instead of parsing colored text. If multiple services are configured and `--service` isn't given, `--json` runs every service automatically rather than falling into the interactive "Which service?" picker.
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
envshield check --json
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
```json
|
|
403
|
+
{
|
|
404
|
+
"success": false,
|
|
405
|
+
"results": [
|
|
406
|
+
{
|
|
407
|
+
"file": ".env",
|
|
408
|
+
"service": null,
|
|
409
|
+
"clean": false,
|
|
410
|
+
"missing": [],
|
|
411
|
+
"blank": ["SECRETS_ENCRYPTION_KEY"],
|
|
412
|
+
"invalid": {},
|
|
413
|
+
"extra": []
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
`doctor --json` returns the same per-service shape, with each service's individual health checks instead of a variable diff (`--fix` is rejected together with `--json` — an interactive confirm prompt has no place in a machine-readable mode):
|
|
420
|
+
|
|
421
|
+
```json
|
|
422
|
+
{
|
|
423
|
+
"success": true,
|
|
424
|
+
"results": [
|
|
425
|
+
{
|
|
426
|
+
"service": "hermes",
|
|
427
|
+
"passed": true,
|
|
428
|
+
"checks": [
|
|
429
|
+
{"name": "Configuration Files", "passed": true, "message": "Found and accessible."}
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
]
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
`scan --json` returns one flat object (scan doesn't fan out per-service the way check/doctor do — it scans a set of files against whichever schema each one belongs to in a single pass):
|
|
437
|
+
|
|
438
|
+
```json
|
|
439
|
+
{
|
|
440
|
+
"clean": false,
|
|
441
|
+
"secrets": [
|
|
442
|
+
{"file_path": "./config.py", "line_num": 12, "secret_type": "Generic API Key", "line_content": "..."}
|
|
443
|
+
],
|
|
444
|
+
"undeclared_variables": [],
|
|
445
|
+
"skipped_files": []
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
366
449
|
---
|
|
367
450
|
|
|
368
451
|
## Typed config code generation
|
|
@@ -380,6 +463,7 @@ Source of truth: env.schema.toml. Regenerate with: envshield generate
|
|
|
380
463
|
|
|
381
464
|
Requires: pip install pydantic pydantic-settings
|
|
382
465
|
"""
|
|
466
|
+
|
|
383
467
|
from pydantic import Field, SecretStr
|
|
384
468
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
385
469
|
|
|
@@ -392,11 +476,13 @@ class Settings(BaseSettings):
|
|
|
392
476
|
)
|
|
393
477
|
|
|
394
478
|
database_url: SecretStr = Field(
|
|
395
|
-
...,
|
|
479
|
+
...,
|
|
480
|
+
description="PostgreSQL connection string for the API",
|
|
481
|
+
alias="DATABASE_URL",
|
|
396
482
|
)
|
|
397
483
|
|
|
398
484
|
api_port: int = Field(
|
|
399
|
-
|
|
485
|
+
"5000", description="Port the API listens on", alias="API_PORT", ge=1, le=65535
|
|
400
486
|
)
|
|
401
487
|
|
|
402
488
|
|
|
@@ -406,7 +492,9 @@ settings = Settings()
|
|
|
406
492
|
```python
|
|
407
493
|
from config import settings
|
|
408
494
|
|
|
409
|
-
db = psycopg2.connect(
|
|
495
|
+
db = psycopg2.connect(
|
|
496
|
+
settings.database_url.get_secret_value()
|
|
497
|
+
) # SecretStr — masked in logs/reprs
|
|
410
498
|
port = settings.api_port # a real int, validated 1-65535 on startup — not a string
|
|
411
499
|
```
|
|
412
500
|
|
|
@@ -29,12 +29,15 @@ It works the same way whether you have one repo with one `.env` file, or a monor
|
|
|
29
29
|
- [Installation](#installation)
|
|
30
30
|
- [Quick start](#quick-start)
|
|
31
31
|
- [A single service](#a-single-service)
|
|
32
|
-
- [A monorepo with multiple services](#a-monorepo-with-multiple-services)
|
|
32
|
+
- [A monorepo with multiple services (monorepo)](#a-monorepo-with-multiple-services)
|
|
33
33
|
- [Core concept: the schema is the contract](#core-concept-the-schema-is-the-contract)
|
|
34
34
|
- [Every field a variable can have](#every-field-a-variable-can-have)
|
|
35
35
|
- [Conditional requirements (`requiredIf`)](#conditional-requirements-requiredif)
|
|
36
|
-
- [Sharing variables across services (`extends`)](#sharing-variables-across-services-extends)
|
|
36
|
+
- [Sharing variables across services (monorepo, `extends`)](#sharing-variables-across-services-extends)
|
|
37
37
|
- [Command reference](#command-reference)
|
|
38
|
+
- [Core commands](#core-commands)
|
|
39
|
+
- [Monorepo: managing multiple services (monorepo)](#monorepo-managing-multiple-services)
|
|
40
|
+
- [Machine-readable output (`--json`)](#machine-readable-output---json)
|
|
38
41
|
- [Typed config code generation](#typed-config-code-generation)
|
|
39
42
|
- [Validating deployment manifests](#validating-deployment-manifests)
|
|
40
43
|
- [Secret scanning and git hooks](#secret-scanning-and-git-hooks)
|
|
@@ -83,22 +86,28 @@ envshield --version
|
|
|
83
86
|
|
|
84
87
|
### A single service
|
|
85
88
|
|
|
86
|
-
The common case: one repo, one `.env` file.
|
|
89
|
+
The common case: one repo, one `.env` file. This is the whole workflow — nothing else in this README is required to get full value out of EnvShield.
|
|
87
90
|
|
|
88
91
|
```bash
|
|
89
92
|
cd my-project
|
|
90
|
-
envshield init # Detects your framework
|
|
91
|
-
envshield import .env # Or: seed the schema from an existing .env file
|
|
93
|
+
envshield init # Detects your framework and builds env.schema.toml from your real config
|
|
92
94
|
envshield setup # Interactive wizard: fills in .env from the schema
|
|
93
95
|
envshield generate --lang python # Generates a typed config.py (or config.ts for TypeScript)
|
|
94
96
|
```
|
|
95
97
|
|
|
98
|
+
`init` looks for a real config source first — an existing `.env`, `.env.example`, or a recognizable Python config module (`config/settings.py` and similar) — and builds the schema from its actual variables, classifying each as secret or not, with a suggested default and, where the value's shape is unambiguous, an inferred type. Only a genuinely fresh project with nothing to read yet falls back to a generic framework template:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
Found config/settings.py -- building your schema from its real variables.
|
|
102
|
+
✓ Created/updated schema: env.schema.toml
|
|
103
|
+
```
|
|
104
|
+
|
|
96
105
|
`init` also offers to install a git pre-commit hook (secret scanning) and a post-merge hook (drift check after every `git pull`) — say yes unless you already have your own hook-management tooling (Husky, `pre-commit`, etc.), in which case EnvShield will detect it and won't clobber it (see [Secret scanning and git hooks](#secret-scanning-and-git-hooks)).
|
|
97
106
|
|
|
98
|
-
|
|
107
|
+
`envshield import <file>` does the same real-variable analysis `init` runs automatically, as its own command — reach for it later, when you want to re-import after adding new variables to your code, point at a file `init` wouldn't have found on its own, or add `--interactive` to confirm each classification by hand instead of accepting the automatic guess:
|
|
99
108
|
|
|
100
109
|
```bash
|
|
101
|
-
envshield import .env
|
|
110
|
+
envshield import .env --interactive
|
|
102
111
|
```
|
|
103
112
|
|
|
104
113
|
```
|
|
@@ -111,8 +120,14 @@ Analyzing variables...
|
|
|
111
120
|
- Inferred a type (int/port/bool/url/email) for 3 variable(s).
|
|
112
121
|
```
|
|
113
122
|
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
**Everything above is the complete single-service story.** Everything below this point — multiple services, schema composition, deployment manifests with more than one container — is opt-in, and only relevant once your repo actually has more than one service. Skip straight to [Core concept: the schema is the contract](#core-concept-the-schema-is-the-contract) if that's not you yet.
|
|
126
|
+
|
|
114
127
|
### A monorepo with multiple services
|
|
115
128
|
|
|
129
|
+
*(Optional — skip this if you only have one service.)*
|
|
130
|
+
|
|
116
131
|
If your repo has more than one service (an API, a web frontend, a worker), run `service discover` at the repo root instead of `init`:
|
|
117
132
|
|
|
118
133
|
```bash
|
|
@@ -245,6 +260,8 @@ With `FEATURE_X_ENABLED=false`, `FEATURE_X_API_KEY` is optional — `setup` won'
|
|
|
245
260
|
|
|
246
261
|
### Sharing variables across services (`extends`)
|
|
247
262
|
|
|
263
|
+
*(Monorepo-only — skip this if you have one service.)*
|
|
264
|
+
|
|
248
265
|
A monorepo with ten services usually has five or six variables every single one of them needs — `LOG_LEVEL`, `SENTRY_DSN`, `DATADOG_API_KEY` — and copy-pasting the same `[LOG_LEVEL]` block into ten schema files is exactly the kind of drift EnvShield exists to prevent.
|
|
249
266
|
|
|
250
267
|
Factor them into a shared base schema, and have each service extend it:
|
|
@@ -281,22 +298,21 @@ Loading `services/api/env.schema.toml` now transparently gives you `LOG_LEVEL`,
|
|
|
281
298
|
|
|
282
299
|
## Command reference
|
|
283
300
|
|
|
284
|
-
|
|
301
|
+
### Core commands
|
|
302
|
+
|
|
303
|
+
Everything a single-service project ever needs. `--service` shows up on most of these for the multi-service case (see below), but it's entirely optional until you actually have more than one service.
|
|
285
304
|
|
|
286
305
|
| Command | What it does |
|
|
287
306
|
|---|---|
|
|
288
|
-
| `envshield init [--force/-f]` | Detects your framework
|
|
289
|
-
| `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` |
|
|
290
|
-
| `envshield check [file] [--service NAME] [--container NAME]` | Validates a local file (or, if omitted, the project's/service's default local file *and* its registered deployment manifest, if any) against the schema. `file` can be a plain `.env`, a Python config module, a docker-compose file, or a Kubernetes manifest. `--container` picks which service/container to check in a manifest that declares more than one (tried against `--service`'s name automatically first). Exits non-zero on any drift — safe to use as a CI gate. |
|
|
291
|
-
| `envshield doctor [--fix] [--service NAME]` | Runs every health check at once (see below) and reports a summary. `--fix` interactively offers to fix whatever it can — re-running `init`, regenerating the template, installing the git hook, or running `setup` to fill in missing/invalid local values. Exits non-zero if anything's still broken afterward. |
|
|
307
|
+
| `envshield init [--force/-f]` | Detects your framework and builds `env.schema.toml` from a real config source if it finds one, otherwise a framework-aware template. Also scaffolds `envshield.yml`, updates `.gitignore`, and offers to install git hooks. Auto-registers a root-level `docker-compose.yml` as the project's deployment manifest if it finds one. `--force` re-runs on a project that already has a config (with a confirmation before overwriting). |
|
|
308
|
+
| `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` | Runs the same real-variable analysis `init` does automatically, as its own command — for re-importing after your code gains new variables, pointing at a file `init` wouldn't have found, or adding `--interactive` to confirm each secret/type classification by hand instead of accepting the automatic guess. `--output` changes where the schema is written (defaults to `env.schema.toml`, or the target service's schema path with `--service`). |
|
|
309
|
+
| `envshield check [file] [--service NAME] [--container NAME] [--json]` | Validates a local file (or, if omitted, the project's/service's default local file *and* its registered deployment manifest, if any) against the schema. `file` can be a plain `.env`, a Python config module, a docker-compose file, or a Kubernetes manifest. `--container` picks which service/container to check in a manifest that declares more than one (tried against `--service`'s name automatically first). Exits non-zero on any drift — safe to use as a CI gate. `--json` prints a machine-readable result instead (see below) and never falls into an interactive service picker. |
|
|
310
|
+
| `envshield doctor [--fix] [--service NAME] [--json]` | Runs every health check at once (see below) and reports a summary. `--fix` interactively offers to fix whatever it can — re-running `init`, regenerating the template, installing the git hook, or running `setup` to fill in missing/invalid local values. Exits non-zero if anything's still broken afterward. `--json` is incompatible with `--fix` (an interactive confirm prompt makes no sense in a machine-readable mode). |
|
|
292
311
|
| `envshield setup [output_file] [--service NAME]` | Interactive onboarding wizard: walks through every variable that's missing, blank, or has an existing value the schema no longer allows, prompting with the variable's description, masking secret input, and offering a picker for `enum` fields. Leaves everything already correct untouched. |
|
|
293
|
-
| `envshield schema sync [--service NAME]` | Regenerates `.env.example` from the schema (a dotenv project), or patches a Python-module local file in place to declare any schema variable it's missing (never rewrites it wholesale — only appends/patches the specific lines it owns). |
|
|
294
|
-
| `envshield generate [output_file] [--lang/-l python\|typescript] [--force/-f] [--service NAME]` | Compiles the schema into a typed, validated config module. `--lang` is auto-detected from your project (Next.js/Vite/Node.js → TypeScript,
|
|
295
|
-
| `envshield scan [paths...] [--staged] [--config/-c PATH] [--exclude/-e PATTERN]
|
|
312
|
+
| `envshield schema sync [--service NAME]` | Regenerates `.env.example` from the schema (a dotenv project), or patches a Python-module local file in place to declare any schema variable it's missing (never rewrites it wholesale — only appends/patches the specific lines it owns). `import` already calls this automatically for you when it changes a project's/service's real schema, so you'll rarely need to run it by hand except after a manual schema edit. |
|
|
313
|
+
| `envshield generate [output_file] [--lang/-l python\|typescript] [--force/-f] [--service NAME]` | Compiles the schema into a typed, validated config module. `--lang` is auto-detected from your project (Next.js/Vite/Node.js → TypeScript; Python/Django/Flask, or nothing detected → Python) if omitted. A detected ecosystem with no codegen target at all (currently: Go) errors and asks for `--lang` explicitly, rather than silently guessing Python. Defaults to writing `config.py`/`config.ts`; `--force` overwrites an existing output file. See [Typed config code generation](#typed-config-code-generation). |
|
|
314
|
+
| `envshield scan [paths...] [--staged] [--config/-c PATH] [--exclude/-e PATTERN] [--service NAME] [--json]` | Scans code for hardcoded secrets and for env vars used in code (`os.getenv`, `os.environ.get`, `process.env.X`) but never declared in the schema. `--staged` scans only what's staged for the next commit (what the pre-commit hook runs); `--exclude` (repeatable) adds glob patterns to skip, on top of whatever `secret_scanning.exclude_files` is set in `envshield.yml`. See [Secret scanning and git hooks](#secret-scanning-and-git-hooks). |
|
|
296
315
|
| `envshield install-hook` | Installs both git hooks by hand, without going through `init`/`setup`/`service discover`'s interactive prompt. |
|
|
297
|
-
| `envshield service list` | Lists every service currently configured in `envshield.yml`, with its schema and local file paths. |
|
|
298
|
-
| `envshield service add <name> <directory> [--local-file PATH] [--example-file PATH] [--description/-d TEXT] [--schema PATH] [--import FILE] [--deployment-manifest PATH] [--container NAME]` | Registers one service by hand. `--local-file` is required when the service's real config isn't a dotenv file (e.g. a Python module) — see below. `--import` seeds the new service's schema from an existing config file in one step. `--deployment-manifest` is auto-detected (a compose file in the given directory, or the project root) if not given explicitly. |
|
|
299
|
-
| `envshield service discover [root] [--yes/-y]` | Scans for service-like directories not already registered, and offers to add them — see [Quick start](#a-monorepo-with-multiple-services). `--yes` skips the interactive confirmation, for CI/scripting. |
|
|
300
316
|
| `envshield --version` / `-v` | Prints the installed version and exits. |
|
|
301
317
|
|
|
302
318
|
**Not using a `.env` file at all?** Some projects (a Flask app whose local config is a checked-in Python module, for example) don't use dotenv at all. Point `local_file` at it instead, and EnvShield reads and writes it as source code, not as a dotenv file — appending or patching only the specific assignments it owns, never touching anything else in the file:
|
|
@@ -308,6 +324,73 @@ services:
|
|
|
308
324
|
local_file: athena/config/env_config.local.py
|
|
309
325
|
```
|
|
310
326
|
|
|
327
|
+
### Monorepo: managing multiple services
|
|
328
|
+
|
|
329
|
+
*(Only relevant once your repo has more than one service — see [A monorepo with multiple services](#a-monorepo-with-multiple-services).)* Every core command above already accepts `--service`: automatically, if there's only one service configured; interactively (or against every service at once, via "All services"), if there's more than one.
|
|
330
|
+
|
|
331
|
+
| Command | What it does |
|
|
332
|
+
|---|---|
|
|
333
|
+
| `envshield service list` | Lists every service currently configured in `envshield.yml`, with its schema and local file paths. |
|
|
334
|
+
| `envshield service add <name> <directory> [--local-file PATH] [--example-file PATH] [--description/-d TEXT] [--schema PATH] [--import FILE] [--deployment-manifest PATH] [--container NAME]` | Registers one service by hand. `--local-file` is required when the service's real config isn't a dotenv file (e.g. a Python module) — see above. `--import` seeds the new service's schema from an existing config file in one step. `--deployment-manifest` is auto-detected (a compose file in the given directory or the project root that actually declares this service) if not given explicitly. |
|
|
335
|
+
| `envshield service discover [root] [--yes/-y]` | Scans for service-like directories not already registered, and offers to add them — see [Quick start](#a-monorepo-with-multiple-services). `--yes` skips the interactive confirmation, for CI/scripting. |
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Machine-readable output (`--json`)
|
|
340
|
+
|
|
341
|
+
`check`, `doctor`, and `scan` all accept `--json`: every Rich table/panel/progress-bar is suppressed, and exactly one JSON object is printed to stdout instead. Exit codes are unchanged (non-zero on any issue), so `--json` is a drop-in for CI gates, dashboards, or an agent loop that needs to branch on the result instead of parsing colored text. If multiple services are configured and `--service` isn't given, `--json` runs every service automatically rather than falling into the interactive "Which service?" picker.
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
envshield check --json
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
```json
|
|
348
|
+
{
|
|
349
|
+
"success": false,
|
|
350
|
+
"results": [
|
|
351
|
+
{
|
|
352
|
+
"file": ".env",
|
|
353
|
+
"service": null,
|
|
354
|
+
"clean": false,
|
|
355
|
+
"missing": [],
|
|
356
|
+
"blank": ["SECRETS_ENCRYPTION_KEY"],
|
|
357
|
+
"invalid": {},
|
|
358
|
+
"extra": []
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
`doctor --json` returns the same per-service shape, with each service's individual health checks instead of a variable diff (`--fix` is rejected together with `--json` — an interactive confirm prompt has no place in a machine-readable mode):
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{
|
|
368
|
+
"success": true,
|
|
369
|
+
"results": [
|
|
370
|
+
{
|
|
371
|
+
"service": "hermes",
|
|
372
|
+
"passed": true,
|
|
373
|
+
"checks": [
|
|
374
|
+
{"name": "Configuration Files", "passed": true, "message": "Found and accessible."}
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
]
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
`scan --json` returns one flat object (scan doesn't fan out per-service the way check/doctor do — it scans a set of files against whichever schema each one belongs to in a single pass):
|
|
382
|
+
|
|
383
|
+
```json
|
|
384
|
+
{
|
|
385
|
+
"clean": false,
|
|
386
|
+
"secrets": [
|
|
387
|
+
{"file_path": "./config.py", "line_num": 12, "secret_type": "Generic API Key", "line_content": "..."}
|
|
388
|
+
],
|
|
389
|
+
"undeclared_variables": [],
|
|
390
|
+
"skipped_files": []
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
311
394
|
---
|
|
312
395
|
|
|
313
396
|
## Typed config code generation
|
|
@@ -325,6 +408,7 @@ Source of truth: env.schema.toml. Regenerate with: envshield generate
|
|
|
325
408
|
|
|
326
409
|
Requires: pip install pydantic pydantic-settings
|
|
327
410
|
"""
|
|
411
|
+
|
|
328
412
|
from pydantic import Field, SecretStr
|
|
329
413
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
330
414
|
|
|
@@ -337,11 +421,13 @@ class Settings(BaseSettings):
|
|
|
337
421
|
)
|
|
338
422
|
|
|
339
423
|
database_url: SecretStr = Field(
|
|
340
|
-
...,
|
|
424
|
+
...,
|
|
425
|
+
description="PostgreSQL connection string for the API",
|
|
426
|
+
alias="DATABASE_URL",
|
|
341
427
|
)
|
|
342
428
|
|
|
343
429
|
api_port: int = Field(
|
|
344
|
-
|
|
430
|
+
"5000", description="Port the API listens on", alias="API_PORT", ge=1, le=65535
|
|
345
431
|
)
|
|
346
432
|
|
|
347
433
|
|
|
@@ -351,7 +437,9 @@ settings = Settings()
|
|
|
351
437
|
```python
|
|
352
438
|
from config import settings
|
|
353
439
|
|
|
354
|
-
db = psycopg2.connect(
|
|
440
|
+
db = psycopg2.connect(
|
|
441
|
+
settings.database_url.get_secret_value()
|
|
442
|
+
) # SecretStr — masked in logs/reprs
|
|
355
443
|
port = settings.api_port # a real int, validated 1-65535 on startup — not a string
|
|
356
444
|
```
|
|
357
445
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.4.0"
|