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.
Files changed (65) hide show
  1. {envshield-4.2.0 → envshield-4.4.0}/PKG-INFO +111 -23
  2. {envshield-4.2.0 → envshield-4.4.0}/README.md +109 -21
  3. envshield-4.4.0/envshield/__init__.py +1 -0
  4. {envshield-4.2.0 → envshield-4.4.0}/envshield/cli.py +222 -61
  5. {envshield-4.2.0 → envshield-4.4.0}/envshield/config/manager.py +30 -14
  6. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/doctor.py +49 -11
  7. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/exceptions.py +9 -5
  8. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/generator.py +25 -12
  9. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/hooks_manager.py +4 -4
  10. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/importer.py +7 -8
  11. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/inspector.py +3 -2
  12. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/scanner.py +87 -25
  13. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/schema_manager.py +72 -9
  14. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/schema_types.py +1 -0
  15. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/service_discovery.py +80 -10
  16. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/setup_manager.py +18 -13
  17. {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_base.py +1 -1
  18. {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_docker_compose.py +5 -5
  19. {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_dotenv.py +2 -1
  20. {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_kubernetes.py +4 -3
  21. {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_python.py +1 -0
  22. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_doctor.py +6 -11
  23. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_generator.py +10 -4
  24. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_importer.py +6 -15
  25. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_scanner_compliance.py +8 -20
  26. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_schema_manager.py +28 -44
  27. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_schema_types.py +8 -3
  28. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_service_discovery.py +65 -6
  29. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_service_manager.py +10 -8
  30. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_setup_manager.py +13 -21
  31. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/parsers/test_docker_compose_parser.py +9 -22
  32. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/parsers/test_kubernetes_parser.py +4 -10
  33. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/parsers/test_python_parser.py +1 -9
  34. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/test_cli.py +247 -11
  35. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/test_config_manager.py +57 -38
  36. 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
  37. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/test_git_utils.py +15 -5
  38. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/test_service_cli.py +27 -2
  39. {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/PKG-INFO +111 -23
  40. {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/SOURCES.txt +1 -1
  41. {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/requires.txt +1 -1
  42. {envshield-4.2.0 → envshield-4.4.0}/pyproject.toml +19 -2
  43. envshield-4.2.0/envshield/__init__.py +0 -1
  44. {envshield-4.2.0 → envshield-4.4.0}/LICENSE.md +0 -0
  45. {envshield-4.2.0 → envshield-4.4.0}/envshield/__main__.py +0 -0
  46. {envshield-4.2.0 → envshield-4.4.0}/envshield/config/__init.py +0 -0
  47. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/__init__.py +0 -0
  48. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/file_updater.py +0 -0
  49. {envshield-4.2.0 → envshield-4.4.0}/envshield/core/service_manager.py +0 -0
  50. {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/__init__.py +0 -0
  51. {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/_deployment.py +0 -0
  52. {envshield-4.2.0 → envshield-4.4.0}/envshield/parsers/factory.py +0 -0
  53. {envshield-4.2.0 → envshield-4.4.0}/envshield/state.py +1 -1
  54. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/__init__.py +0 -0
  55. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/conftest.py +0 -0
  56. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/__init__.py +0 -0
  57. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/core/test_file_updater.py +0 -0
  58. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/parsers/__init__.py +0 -0
  59. {envshield-4.2.0 → envshield-4.4.0}/envshield/tests/parsers/test_dotenv_parser.py +0 -0
  60. {envshield-4.2.0 → envshield-4.4.0}/envshield/utils/__init__.py +0 -0
  61. {envshield-4.2.0 → envshield-4.4.0}/envshield/utils/git_utils.py +0 -0
  62. {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/dependency_links.txt +0 -0
  63. {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/entry_points.txt +0 -0
  64. {envshield-4.2.0 → envshield-4.4.0}/envshield.egg-info/top_level.txt +0 -0
  65. {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.2.0
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, creates env.schema.toml + envshield.yml
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
- If you already have a real `.env` with real values, `import` is usually the better starting point than `init`'s blank framework defaults — it reads your actual file and classifies each variable (secret or not, with a suggested default and, where the value's shape is unambiguous, a type) in seconds:
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
- Every command that takes `--service` also works without it: automatically, if there's only one service configured; interactively (or against every service at once, via "All services"), if there's more than one.
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, scaffolds `env.schema.toml` + `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). |
344
- | `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` | Converts an existing `.env` file or Python config module into `env.schema.toml`, classifying each variable as secret or not, suggesting a default, and inferring a type where the value's shape is unambiguous. `--interactive` confirms each classification with you instead of applying it silently. `--output` changes where the schema is written (defaults to `env.schema.toml`, or the target service's schema path with `--service`). |
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, everything else → Python) if omitted. Defaults to writing `config.py`/`config.ts`; `--force` overwrites an existing output file. See [Typed config code generation](#typed-config-code-generation). |
350
- | `envshield scan [paths...] [--staged] [--config/-c PATH] [--exclude/-e PATTERN]` [--service NAME] | 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). |
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
- ..., description="PostgreSQL connection string for the API", alias="DATABASE_URL"
479
+ ...,
480
+ description="PostgreSQL connection string for the API",
481
+ alias="DATABASE_URL",
396
482
  )
397
483
 
398
484
  api_port: int = Field(
399
- '5000', description="Port the API listens on", alias="API_PORT", ge=1, le=65535
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(settings.database_url.get_secret_value()) # SecretStr — masked in logs/reprs
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, creates env.schema.toml + envshield.yml
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
- If you already have a real `.env` with real values, `import` is usually the better starting point than `init`'s blank framework defaults — it reads your actual file and classifies each variable (secret or not, with a suggested default and, where the value's shape is unambiguous, a type) in seconds:
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
- Every command that takes `--service` also works without it: automatically, if there's only one service configured; interactively (or against every service at once, via "All services"), if there's more than one.
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, scaffolds `env.schema.toml` + `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). |
289
- | `envshield import <file> [--output/-o PATH] [--force/-f] [--interactive] [--service NAME]` | Converts an existing `.env` file or Python config module into `env.schema.toml`, classifying each variable as secret or not, suggesting a default, and inferring a type where the value's shape is unambiguous. `--interactive` confirms each classification with you instead of applying it silently. `--output` changes where the schema is written (defaults to `env.schema.toml`, or the target service's schema path with `--service`). |
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, everything else → Python) if omitted. Defaults to writing `config.py`/`config.ts`; `--force` overwrites an existing output file. See [Typed config code generation](#typed-config-code-generation). |
295
- | `envshield scan [paths...] [--staged] [--config/-c PATH] [--exclude/-e PATTERN]` [--service NAME] | 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). |
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
- ..., description="PostgreSQL connection string for the API", alias="DATABASE_URL"
424
+ ...,
425
+ description="PostgreSQL connection string for the API",
426
+ alias="DATABASE_URL",
341
427
  )
342
428
 
343
429
  api_port: int = Field(
344
- '5000', description="Port the API listens on", alias="API_PORT", ge=1, le=65535
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(settings.database_url.get_secret_value()) # SecretStr — masked in logs/reprs
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"