envshield 4.6.2__tar.gz → 4.7.1__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 (89) hide show
  1. {envshield-4.6.2 → envshield-4.7.1}/PKG-INFO +80 -5
  2. {envshield-4.6.2 → envshield-4.7.1}/README.md +79 -4
  3. envshield-4.7.1/envshield/__init__.py +1 -0
  4. {envshield-4.6.2 → envshield-4.7.1}/envshield/cli.py +324 -56
  5. {envshield-4.6.2 → envshield-4.7.1}/envshield/config/manager.py +100 -24
  6. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/dependency_snapshot.py +33 -6
  7. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/discovery.py +245 -31
  8. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/doctor.py +169 -18
  9. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/explain.py +106 -26
  10. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/hooks_manager.py +13 -2
  11. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/importer.py +16 -1
  12. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/scanner.py +95 -30
  13. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/schema_manager.py +371 -5
  14. envshield-4.7.1/envshield/parsers/_deployment.py +126 -0
  15. {envshield-4.6.2 → envshield-4.7.1}/envshield/parsers/_docker_compose.py +23 -14
  16. envshield-4.7.1/envshield/parsers/_kubernetes.py +222 -0
  17. envshield-4.7.1/envshield/utils/paths.py +49 -0
  18. {envshield-4.6.2 → envshield-4.7.1}/envshield.egg-info/PKG-INFO +80 -5
  19. envshield-4.7.1/envshield.egg-info/SOURCES.txt +46 -0
  20. {envshield-4.6.2 → envshield-4.7.1}/pyproject.toml +2 -1
  21. envshield-4.6.2/envshield/__init__.py +0 -1
  22. envshield-4.6.2/envshield/config/__init.py +0 -0
  23. envshield-4.6.2/envshield/parsers/_deployment.py +0 -63
  24. envshield-4.6.2/envshield/parsers/_kubernetes.py +0 -148
  25. envshield-4.6.2/envshield/tests/conftest.py +0 -33
  26. envshield-4.6.2/envshield/tests/core/test_contract_diff.py +0 -433
  27. envshield-4.6.2/envshield/tests/core/test_dependency_diff.py +0 -167
  28. envshield-4.6.2/envshield/tests/core/test_dependency_snapshot.py +0 -528
  29. envshield-4.6.2/envshield/tests/core/test_discovery.py +0 -574
  30. envshield-4.6.2/envshield/tests/core/test_doctor.py +0 -736
  31. envshield-4.6.2/envshield/tests/core/test_explain.py +0 -397
  32. envshield-4.6.2/envshield/tests/core/test_file_updater.py +0 -223
  33. envshield-4.6.2/envshield/tests/core/test_generator.py +0 -815
  34. envshield-4.6.2/envshield/tests/core/test_hook_injection.py +0 -282
  35. envshield-4.6.2/envshield/tests/core/test_hooks_manager.py +0 -37
  36. envshield-4.6.2/envshield/tests/core/test_importer.py +0 -808
  37. envshield-4.6.2/envshield/tests/core/test_post_merge_hook.py +0 -139
  38. envshield-4.6.2/envshield/tests/core/test_pre_commit_hook.py +0 -332
  39. envshield-4.6.2/envshield/tests/core/test_scanner_compliance.py +0 -1419
  40. envshield-4.6.2/envshield/tests/core/test_schema_manager.py +0 -1308
  41. envshield-4.6.2/envshield/tests/core/test_schema_snapshot.py +0 -424
  42. envshield-4.6.2/envshield/tests/core/test_schema_types.py +0 -347
  43. envshield-4.6.2/envshield/tests/core/test_service_discovery.py +0 -406
  44. envshield-4.6.2/envshield/tests/core/test_service_manager.py +0 -326
  45. envshield-4.6.2/envshield/tests/core/test_setup_manager.py +0 -1084
  46. envshield-4.6.2/envshield/tests/parsers/__init__.py +0 -0
  47. envshield-4.6.2/envshield/tests/parsers/test_docker_compose_parser.py +0 -436
  48. envshield-4.6.2/envshield/tests/parsers/test_dotenv_parser.py +0 -220
  49. envshield-4.6.2/envshield/tests/parsers/test_kubernetes_parser.py +0 -354
  50. envshield-4.6.2/envshield/tests/parsers/test_python_parser.py +0 -113
  51. envshield-4.6.2/envshield/tests/test_cli.py +0 -1569
  52. envshield-4.6.2/envshield/tests/test_config_manager.py +0 -951
  53. envshield-4.6.2/envshield/tests/test_diff_aware_scanning.py +0 -201
  54. envshield-4.6.2/envshield/tests/test_explain_cli.py +0 -306
  55. envshield-4.6.2/envshield/tests/test_git_utils.py +0 -463
  56. envshield-4.6.2/envshield/tests/test_hook_cli.py +0 -187
  57. envshield-4.6.2/envshield/tests/test_schema_diff_cli.py +0 -658
  58. envshield-4.6.2/envshield/tests/test_service_cli.py +0 -549
  59. envshield-4.6.2/envshield/tests/test_undeclared_cli.py +0 -482
  60. envshield-4.6.2/envshield/utils/__init__.py +0 -0
  61. envshield-4.6.2/envshield.egg-info/SOURCES.txt +0 -82
  62. {envshield-4.6.2 → envshield-4.7.1}/LICENSE.md +0 -0
  63. {envshield-4.6.2 → envshield-4.7.1}/envshield/__main__.py +0 -0
  64. {envshield-4.6.2/envshield/core → envshield-4.7.1/envshield/config}/__init__.py +0 -0
  65. {envshield-4.6.2/envshield/parsers → envshield-4.7.1/envshield/core}/__init__.py +0 -0
  66. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/contract_diff.py +0 -0
  67. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/dependency_diff.py +0 -0
  68. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/exceptions.py +0 -0
  69. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/file_updater.py +0 -0
  70. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/generator.py +0 -0
  71. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/inspector.py +0 -0
  72. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/schema_snapshot.py +0 -0
  73. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/schema_types.py +0 -0
  74. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/service_discovery.py +0 -0
  75. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/service_manager.py +0 -0
  76. {envshield-4.6.2 → envshield-4.7.1}/envshield/core/setup_manager.py +0 -0
  77. {envshield-4.6.2/envshield/tests → envshield-4.7.1/envshield/parsers}/__init__.py +0 -0
  78. {envshield-4.6.2 → envshield-4.7.1}/envshield/parsers/_base.py +0 -0
  79. {envshield-4.6.2 → envshield-4.7.1}/envshield/parsers/_dotenv.py +0 -0
  80. {envshield-4.6.2 → envshield-4.7.1}/envshield/parsers/_python.py +0 -0
  81. {envshield-4.6.2 → envshield-4.7.1}/envshield/parsers/factory.py +0 -0
  82. {envshield-4.6.2 → envshield-4.7.1}/envshield/state.py +0 -0
  83. {envshield-4.6.2/envshield/tests/core → envshield-4.7.1/envshield/utils}/__init__.py +0 -0
  84. {envshield-4.6.2 → envshield-4.7.1}/envshield/utils/git_utils.py +0 -0
  85. {envshield-4.6.2 → envshield-4.7.1}/envshield.egg-info/dependency_links.txt +0 -0
  86. {envshield-4.6.2 → envshield-4.7.1}/envshield.egg-info/entry_points.txt +0 -0
  87. {envshield-4.6.2 → envshield-4.7.1}/envshield.egg-info/requires.txt +0 -0
  88. {envshield-4.6.2 → envshield-4.7.1}/envshield.egg-info/top_level.txt +0 -0
  89. {envshield-4.6.2 → envshield-4.7.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: envshield
3
- Version: 4.6.2
3
+ Version: 4.7.1
4
4
  Summary: EnvShield: environment variables as a version-controlled configuration contract.
5
5
  Author-email: Rabbil Yasar Sajal <rabbilyasar@gmail.com>
6
6
  License: MIT License
@@ -100,9 +100,11 @@ env.schema.toml
100
100
  ## Quick Start
101
101
 
102
102
  ```bash
103
- pip install envshield
103
+ pipx install envshield
104
104
  ```
105
105
 
106
+ Prefer an existing Python/pip workflow instead? `pip install envshield` works the same way, as an alternative.
107
+
106
108
  In an existing project:
107
109
 
108
110
  ```bash
@@ -299,7 +301,7 @@ jobs:
299
301
  steps:
300
302
  - uses: actions/checkout@v4
301
303
  with: { fetch-depth: 0 }
302
- - run: pip install envshield
304
+ - run: pipx install envshield
303
305
  - run: |
304
306
  BASE=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
305
307
  envshield schema diff "$BASE" HEAD
@@ -353,12 +355,67 @@ envshield check --service api
353
355
 
354
356
  Every command is service-aware. `--service` is optional with exactly one service configured, inferred automatically when you're standing inside a registered service's own directory, and only prompted for interactively when there's more than one and neither applies.
355
357
 
358
+ ### Discovering reads in a shared library (`additional_source_roots`)
359
+
360
+ `undeclared` and `explain` only walk a service's own directory by default. If part of a service's real configuration is read from a shared internal library that lives outside that directory — a common-utilities package imported by several services, say — those reads are invisible to both commands, for every service that imports the library.
361
+
362
+ `additional_source_roots` widens a service's discovery scope to cover extra directories:
363
+
364
+ ```yaml
365
+ services:
366
+ api:
367
+ schema: services/api/env.schema.toml
368
+ additional_source_roots:
369
+ - libs/shared
370
+ ```
371
+
372
+ `undeclared --service api` and `explain --service api` now also look inside `libs/shared` — a variable read there counts toward `api`'s own discovery results exactly as if it lived in `services/api/` itself. The same directory can be listed by more than one service; each one independently sees the reads it contains. A path that doesn't exist yet contributes nothing (no error); a path resolving outside the project is refused, the same protection every other path in `envshield.yml` already has. `scan` is unaffected — it's already repository-wide.
373
+
374
+ ---
375
+
376
+ ## Multiple sources per service (`completeness: union`)
377
+
378
+ By default, every registered source for a service — its `local_file`, each registered deployment manifest — is validated independently against the *whole* schema: `check`/`doctor` expect any one of them to be self-sufficient on its own. Some services genuinely don't work that way: part of the configuration lives in a local config file, the rest is supplied by a deployment manifest. Under the default model, a variable that only ever lives in one of them shows up as "missing" everywhere else, even though nothing is actually wrong.
379
+
380
+ Set `completeness: union` on a service to validate against the *combined* presence of all its registered sources instead — a variable satisfied by any one of them satisfies the service as a whole:
381
+
382
+ ```yaml
383
+ services:
384
+ api:
385
+ schema: services/api/env.schema.toml
386
+ local_file: services/api/config/settings.py
387
+ completeness: union
388
+ manifests:
389
+ - file: docker-compose.yml
390
+ containers:
391
+ api: api
392
+ ```
393
+
394
+ ```toml
395
+ [DB_HOST]
396
+ description = "set in config/settings.py"
397
+
398
+ [FEATURE_MODE]
399
+ description = "set via docker-compose.yml"
400
+ ```
401
+
402
+ With `completeness: union`, `check`/`doctor` stop expecting `config/settings.py` to declare `FEATURE_MODE`, or `docker-compose.yml` to set `DB_HOST` — the schema is satisfied as long as every variable is present *somewhere* among the registered sources. `envshield schema sync` respects this too: it only ever appends a variable to a Python `local_file` that's genuinely missing from every registered source, never one that another source already covers.
403
+
404
+ A few things worth knowing:
405
+
406
+ - **Opt-in, per service.** Unset (the default) means every source must be independently self-sufficient, exactly as before this existed — nothing changes unless you set the key.
407
+ - **Union means presence, not values.** Whether an ordinary variable is satisfied depends only on whether it's present (and valid) somewhere — never on merging, comparing, or picking between its values across sources. A blank or invalid value is still reported, wherever it occurs. **The one deliberate exception** is a `requiredIf` trigger's own value (see next) — deciding whether a condition currently holds has always meant reading one specific value, even before `completeness: union` existed; union only extends *where* that one value may come from, it doesn't turn this into general value merging.
408
+ - **A `requiredIf` condition (see [Conditional requirements](#conditional-requirements-requiredif) above) is resolved against the union too** — if the variable a condition depends on lives in a different source than the one being checked, `completeness: union` still finds it. Sources that agree on the trigger's value resolve normally; if two sources genuinely disagree, EnvShield reports it as an explicit conflict rather than guessing which one wins — registration order is never used as a tiebreaker.
409
+ - **A source that fails to load is always a failure**, even if the sources that did load happen to satisfy the schema between them. `doctor` reports this as two separate signals for a union-mode service — source health (does this file exist and parse) and registered-source completeness (does the union satisfy the schema) — so one can never quietly hide behind the other. `check --json` reflects the same split: each source's own result is still reported individually, plus one `combined` entry per union-mode service carrying the aggregate verdict.
410
+
356
411
  ---
357
412
 
358
413
  ## Supported languages / discovery
359
414
 
360
415
  Source-code discovery (what powers `undeclared` and `explain`'s "used in source" section) is AST-based for **Python** (`os.environ.get`, `os.getenv`, `os.environ[...]`) and pattern-based for **JavaScript/TypeScript** (`process.env.X`, `process.env["X"]`, `import.meta.env.X`, including single-level destructuring). No other language is discovered yet — the schema and validation commands work with any stack, but `undeclared`/`explain` can only see what's read from these two.
361
416
 
417
+ `explain` additionally recognizes Flask's `current_app.config[...]`/`.get(...)` in Python — including the common `from flask import current_app as <alias>` import form — and reports it at **medium confidence**, shown inline as `(medium confidence)`: it's one level of indirection through a config object whose contents could come from anywhere, not a direct environment read. This recognition is deliberately `explain`-only — it never affects `undeclared`'s or `scan`'s pass/fail signal, so a Flask-config read can never quietly satisfy (or break) a completeness check the way a direct `os.environ`/`os.getenv` read does.
418
+
362
419
  ---
363
420
 
364
421
  ## Secret safety
@@ -370,12 +427,30 @@ A supporting check alongside the contract, not the product itself. `secret = tru
370
427
 
371
428
  ---
372
429
 
430
+ ## Removing EnvShield
431
+
432
+ ```bash
433
+ envshield uninstall
434
+ ```
435
+
436
+ Removes EnvShield's Git hook integration while preserving your project configuration — it does not delete `envshield.yml`, any `env.schema.toml`, `.env.example`, any registered local configuration file (`.env`, a Python config module, etc.), `.gitignore`, or any code `envshield generate` has produced, regardless of whether EnvShield originally created any of it. Those are your project's own configuration contract and application config, not disposable installation artifacts.
437
+
438
+ The only thing `uninstall` ever deletes is a Git hook whose content exactly matches what EnvShield would generate right now — the same ownership rule `hook remove` already uses. A hand-modified hook, a foreign hook (even one that happens to contain EnvShield's marker comment), or an EnvShield hook that's gone stale relative to your current `envshield.yml` (e.g. after registering a new service) is left in place and reported, never deleted or silently regenerated.
439
+
440
+ ```bash
441
+ envshield uninstall --yes # skip the confirmation prompt
442
+ ```
443
+
444
+ `--yes` only skips the prompt — it never overrides the ownership check; a modified, foreign, or stale hook is preserved with `--yes` exactly as it is interactively. There is no `--force` and no `--purge`: `uninstall` is intentionally not a "delete everything EnvShield ever created" command. To remove EnvShield's configuration from a project entirely, delete `envshield.yml` and the schema files yourself once you're sure you no longer need them.
445
+
446
+ ---
447
+
373
448
  ## Known limitations
374
449
 
375
450
  EnvShield is built around the patterns real projects actually use. A few narrower cases aren't handled yet — none of them let a secret leak or let a genuinely missing required variable pass as clean:
376
451
 
377
- - **Docker Compose:** `${VAR}` and `${VAR:-default}` are supported. The no-colon `${VAR-default}` form, the `${VAR:?}`/`${VAR:+}` forms, and embedded/multiple interpolation references in one value aren't evaluated yet.
378
- - **Kubernetes:** `secretKeyRef`/`configMapKeyRef` are matched by the container's environment-variable name (`env[].name`), not the referenced Secret/ConfigMap's internal key — name your schema variable after the env var, not the key. `envFrom.prefix` isn't applied yet.
452
+ - **Docker Compose:** `${VAR}`, `${VAR:-default}`, and the no-colon `${VAR-default}` form are all supported. The `${VAR:?}`/`${VAR:+}` forms and embedded/multiple interpolation references in one value aren't evaluated yet.
453
+ - **Kubernetes:** `env[].valueFrom.secretKeyRef`/`configMapKeyRef` are matched by the container's environment-variable name (`env[].name`) — name your schema variable after the env var, not the key — and, when the referenced Secret/ConfigMap is defined in the same manifest, the referenced key is validated to actually exist there. `envFrom.prefix` is applied. `initContainers` are selectable via `--container` but never auto-selected by default.
379
454
  - **Source discovery:** Python is AST-based; JavaScript/TypeScript is pattern-based. No other language is discovered yet.
380
455
  - **.env:** multiline quoted values aren't supported.
381
456
 
@@ -41,9 +41,11 @@ env.schema.toml
41
41
  ## Quick Start
42
42
 
43
43
  ```bash
44
- pip install envshield
44
+ pipx install envshield
45
45
  ```
46
46
 
47
+ Prefer an existing Python/pip workflow instead? `pip install envshield` works the same way, as an alternative.
48
+
47
49
  In an existing project:
48
50
 
49
51
  ```bash
@@ -240,7 +242,7 @@ jobs:
240
242
  steps:
241
243
  - uses: actions/checkout@v4
242
244
  with: { fetch-depth: 0 }
243
- - run: pip install envshield
245
+ - run: pipx install envshield
244
246
  - run: |
245
247
  BASE=$(git merge-base "origin/${{ github.base_ref }}" HEAD)
246
248
  envshield schema diff "$BASE" HEAD
@@ -294,12 +296,67 @@ envshield check --service api
294
296
 
295
297
  Every command is service-aware. `--service` is optional with exactly one service configured, inferred automatically when you're standing inside a registered service's own directory, and only prompted for interactively when there's more than one and neither applies.
296
298
 
299
+ ### Discovering reads in a shared library (`additional_source_roots`)
300
+
301
+ `undeclared` and `explain` only walk a service's own directory by default. If part of a service's real configuration is read from a shared internal library that lives outside that directory — a common-utilities package imported by several services, say — those reads are invisible to both commands, for every service that imports the library.
302
+
303
+ `additional_source_roots` widens a service's discovery scope to cover extra directories:
304
+
305
+ ```yaml
306
+ services:
307
+ api:
308
+ schema: services/api/env.schema.toml
309
+ additional_source_roots:
310
+ - libs/shared
311
+ ```
312
+
313
+ `undeclared --service api` and `explain --service api` now also look inside `libs/shared` — a variable read there counts toward `api`'s own discovery results exactly as if it lived in `services/api/` itself. The same directory can be listed by more than one service; each one independently sees the reads it contains. A path that doesn't exist yet contributes nothing (no error); a path resolving outside the project is refused, the same protection every other path in `envshield.yml` already has. `scan` is unaffected — it's already repository-wide.
314
+
315
+ ---
316
+
317
+ ## Multiple sources per service (`completeness: union`)
318
+
319
+ By default, every registered source for a service — its `local_file`, each registered deployment manifest — is validated independently against the *whole* schema: `check`/`doctor` expect any one of them to be self-sufficient on its own. Some services genuinely don't work that way: part of the configuration lives in a local config file, the rest is supplied by a deployment manifest. Under the default model, a variable that only ever lives in one of them shows up as "missing" everywhere else, even though nothing is actually wrong.
320
+
321
+ Set `completeness: union` on a service to validate against the *combined* presence of all its registered sources instead — a variable satisfied by any one of them satisfies the service as a whole:
322
+
323
+ ```yaml
324
+ services:
325
+ api:
326
+ schema: services/api/env.schema.toml
327
+ local_file: services/api/config/settings.py
328
+ completeness: union
329
+ manifests:
330
+ - file: docker-compose.yml
331
+ containers:
332
+ api: api
333
+ ```
334
+
335
+ ```toml
336
+ [DB_HOST]
337
+ description = "set in config/settings.py"
338
+
339
+ [FEATURE_MODE]
340
+ description = "set via docker-compose.yml"
341
+ ```
342
+
343
+ With `completeness: union`, `check`/`doctor` stop expecting `config/settings.py` to declare `FEATURE_MODE`, or `docker-compose.yml` to set `DB_HOST` — the schema is satisfied as long as every variable is present *somewhere* among the registered sources. `envshield schema sync` respects this too: it only ever appends a variable to a Python `local_file` that's genuinely missing from every registered source, never one that another source already covers.
344
+
345
+ A few things worth knowing:
346
+
347
+ - **Opt-in, per service.** Unset (the default) means every source must be independently self-sufficient, exactly as before this existed — nothing changes unless you set the key.
348
+ - **Union means presence, not values.** Whether an ordinary variable is satisfied depends only on whether it's present (and valid) somewhere — never on merging, comparing, or picking between its values across sources. A blank or invalid value is still reported, wherever it occurs. **The one deliberate exception** is a `requiredIf` trigger's own value (see next) — deciding whether a condition currently holds has always meant reading one specific value, even before `completeness: union` existed; union only extends *where* that one value may come from, it doesn't turn this into general value merging.
349
+ - **A `requiredIf` condition (see [Conditional requirements](#conditional-requirements-requiredif) above) is resolved against the union too** — if the variable a condition depends on lives in a different source than the one being checked, `completeness: union` still finds it. Sources that agree on the trigger's value resolve normally; if two sources genuinely disagree, EnvShield reports it as an explicit conflict rather than guessing which one wins — registration order is never used as a tiebreaker.
350
+ - **A source that fails to load is always a failure**, even if the sources that did load happen to satisfy the schema between them. `doctor` reports this as two separate signals for a union-mode service — source health (does this file exist and parse) and registered-source completeness (does the union satisfy the schema) — so one can never quietly hide behind the other. `check --json` reflects the same split: each source's own result is still reported individually, plus one `combined` entry per union-mode service carrying the aggregate verdict.
351
+
297
352
  ---
298
353
 
299
354
  ## Supported languages / discovery
300
355
 
301
356
  Source-code discovery (what powers `undeclared` and `explain`'s "used in source" section) is AST-based for **Python** (`os.environ.get`, `os.getenv`, `os.environ[...]`) and pattern-based for **JavaScript/TypeScript** (`process.env.X`, `process.env["X"]`, `import.meta.env.X`, including single-level destructuring). No other language is discovered yet — the schema and validation commands work with any stack, but `undeclared`/`explain` can only see what's read from these two.
302
357
 
358
+ `explain` additionally recognizes Flask's `current_app.config[...]`/`.get(...)` in Python — including the common `from flask import current_app as <alias>` import form — and reports it at **medium confidence**, shown inline as `(medium confidence)`: it's one level of indirection through a config object whose contents could come from anywhere, not a direct environment read. This recognition is deliberately `explain`-only — it never affects `undeclared`'s or `scan`'s pass/fail signal, so a Flask-config read can never quietly satisfy (or break) a completeness check the way a direct `os.environ`/`os.getenv` read does.
359
+
303
360
  ---
304
361
 
305
362
  ## Secret safety
@@ -311,12 +368,30 @@ A supporting check alongside the contract, not the product itself. `secret = tru
311
368
 
312
369
  ---
313
370
 
371
+ ## Removing EnvShield
372
+
373
+ ```bash
374
+ envshield uninstall
375
+ ```
376
+
377
+ Removes EnvShield's Git hook integration while preserving your project configuration — it does not delete `envshield.yml`, any `env.schema.toml`, `.env.example`, any registered local configuration file (`.env`, a Python config module, etc.), `.gitignore`, or any code `envshield generate` has produced, regardless of whether EnvShield originally created any of it. Those are your project's own configuration contract and application config, not disposable installation artifacts.
378
+
379
+ The only thing `uninstall` ever deletes is a Git hook whose content exactly matches what EnvShield would generate right now — the same ownership rule `hook remove` already uses. A hand-modified hook, a foreign hook (even one that happens to contain EnvShield's marker comment), or an EnvShield hook that's gone stale relative to your current `envshield.yml` (e.g. after registering a new service) is left in place and reported, never deleted or silently regenerated.
380
+
381
+ ```bash
382
+ envshield uninstall --yes # skip the confirmation prompt
383
+ ```
384
+
385
+ `--yes` only skips the prompt — it never overrides the ownership check; a modified, foreign, or stale hook is preserved with `--yes` exactly as it is interactively. There is no `--force` and no `--purge`: `uninstall` is intentionally not a "delete everything EnvShield ever created" command. To remove EnvShield's configuration from a project entirely, delete `envshield.yml` and the schema files yourself once you're sure you no longer need them.
386
+
387
+ ---
388
+
314
389
  ## Known limitations
315
390
 
316
391
  EnvShield is built around the patterns real projects actually use. A few narrower cases aren't handled yet — none of them let a secret leak or let a genuinely missing required variable pass as clean:
317
392
 
318
- - **Docker Compose:** `${VAR}` and `${VAR:-default}` are supported. The no-colon `${VAR-default}` form, the `${VAR:?}`/`${VAR:+}` forms, and embedded/multiple interpolation references in one value aren't evaluated yet.
319
- - **Kubernetes:** `secretKeyRef`/`configMapKeyRef` are matched by the container's environment-variable name (`env[].name`), not the referenced Secret/ConfigMap's internal key — name your schema variable after the env var, not the key. `envFrom.prefix` isn't applied yet.
393
+ - **Docker Compose:** `${VAR}`, `${VAR:-default}`, and the no-colon `${VAR-default}` form are all supported. The `${VAR:?}`/`${VAR:+}` forms and embedded/multiple interpolation references in one value aren't evaluated yet.
394
+ - **Kubernetes:** `env[].valueFrom.secretKeyRef`/`configMapKeyRef` are matched by the container's environment-variable name (`env[].name`) — name your schema variable after the env var, not the key — and, when the referenced Secret/ConfigMap is defined in the same manifest, the referenced key is validated to actually exist there. `envFrom.prefix` is applied. `initContainers` are selectable via `--container` but never auto-selected by default.
320
395
  - **Source discovery:** Python is AST-based; JavaScript/TypeScript is pattern-based. No other language is discovered yet.
321
396
  - **.env:** multiline quoted values aren't supported.
322
397
 
@@ -0,0 +1 @@
1
+ __version__ = "4.7.1"