envshield 4.7.0__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.
- {envshield-4.7.0 → envshield-4.7.1}/PKG-INFO +19 -1
- {envshield-4.7.0 → envshield-4.7.1}/README.md +18 -0
- envshield-4.7.1/envshield/__init__.py +1 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/cli.py +121 -1
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/hooks_manager.py +13 -2
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/scanner.py +68 -26
- {envshield-4.7.0 → envshield-4.7.1}/envshield.egg-info/PKG-INFO +19 -1
- {envshield-4.7.0 → envshield-4.7.1}/pyproject.toml +1 -1
- envshield-4.7.0/envshield/__init__.py +0 -1
- {envshield-4.7.0 → envshield-4.7.1}/LICENSE.md +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/__main__.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/config/__init__.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/config/manager.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/__init__.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/contract_diff.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/dependency_diff.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/dependency_snapshot.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/discovery.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/doctor.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/exceptions.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/explain.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/file_updater.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/generator.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/importer.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/inspector.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/schema_manager.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/schema_snapshot.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/schema_types.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/service_discovery.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/service_manager.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/core/setup_manager.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/parsers/__init__.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/parsers/_base.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/parsers/_deployment.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/parsers/_docker_compose.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/parsers/_dotenv.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/parsers/_kubernetes.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/parsers/_python.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/parsers/factory.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/state.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/utils/__init__.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/utils/git_utils.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield/utils/paths.py +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield.egg-info/SOURCES.txt +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield.egg-info/dependency_links.txt +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield.egg-info/entry_points.txt +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield.egg-info/requires.txt +0 -0
- {envshield-4.7.0 → envshield-4.7.1}/envshield.egg-info/top_level.txt +0 -0
- {envshield-4.7.0 → 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.7.
|
|
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
|
|
@@ -427,6 +427,24 @@ A supporting check alongside the contract, not the product itself. `secret = tru
|
|
|
427
427
|
|
|
428
428
|
---
|
|
429
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
|
+
|
|
430
448
|
## Known limitations
|
|
431
449
|
|
|
432
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:
|
|
@@ -368,6 +368,24 @@ A supporting check alongside the contract, not the product itself. `secret = tru
|
|
|
368
368
|
|
|
369
369
|
---
|
|
370
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
|
+
|
|
371
389
|
## Known limitations
|
|
372
390
|
|
|
373
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:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.7.1"
|
|
@@ -1855,11 +1855,12 @@ def hook_remove(
|
|
|
1855
1855
|
console.print("[yellow]Cancelled.[/yellow]")
|
|
1856
1856
|
raise typer.Exit()
|
|
1857
1857
|
try:
|
|
1858
|
-
|
|
1858
|
+
results = scanner.remove_hooks()
|
|
1859
1859
|
except EnvShieldException as e:
|
|
1860
1860
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
|
1861
1861
|
raise typer.Exit(code=1)
|
|
1862
1862
|
|
|
1863
|
+
removed = [r["name"] for r in results if r["status"] == "removed"]
|
|
1863
1864
|
if not removed:
|
|
1864
1865
|
console.print("[yellow]No EnvShield-installed hooks found to remove.[/yellow]")
|
|
1865
1866
|
return
|
|
@@ -1867,6 +1868,125 @@ def hook_remove(
|
|
|
1867
1868
|
console.print(f"[bold green]✓[/bold green] Removed {hook_name} hook.")
|
|
1868
1869
|
|
|
1869
1870
|
|
|
1871
|
+
@app.command()
|
|
1872
|
+
def uninstall(
|
|
1873
|
+
yes: bool = typer.Option(
|
|
1874
|
+
False, "--yes", "-y", help="Skip the confirmation prompt."
|
|
1875
|
+
),
|
|
1876
|
+
):
|
|
1877
|
+
"""
|
|
1878
|
+
Removes EnvShield's Git hook integration from this project.
|
|
1879
|
+
|
|
1880
|
+
Only ever deletes a hook file whose content exactly matches what
|
|
1881
|
+
EnvShield would generate right now -- the same ownership standard
|
|
1882
|
+
'hook remove' already uses. A hand-modified hook, a foreign hook, or
|
|
1883
|
+
an EnvShield hook that's gone stale relative to your current
|
|
1884
|
+
envshield.yml (e.g. after registering a new service) is left alone
|
|
1885
|
+
and reported, never deleted.
|
|
1886
|
+
|
|
1887
|
+
Never touches project configuration -- envshield.yml, any
|
|
1888
|
+
env.schema.toml, .env.example, or any registered local configuration
|
|
1889
|
+
file are always preserved and reported, never removed. This is not a
|
|
1890
|
+
"delete everything EnvShield ever created" command.
|
|
1891
|
+
"""
|
|
1892
|
+
if not _confirm_hook_action(
|
|
1893
|
+
"EnvShield will remove Git hooks that are verified as EnvShield-owned.\n"
|
|
1894
|
+
"Project configuration and schema files will be preserved.\n\n"
|
|
1895
|
+
"Continue?",
|
|
1896
|
+
yes,
|
|
1897
|
+
):
|
|
1898
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
1899
|
+
raise typer.Exit()
|
|
1900
|
+
|
|
1901
|
+
console.print("\n[bold]EnvShield uninstall[/bold]\n")
|
|
1902
|
+
|
|
1903
|
+
try:
|
|
1904
|
+
hook_results = scanner.remove_hooks()
|
|
1905
|
+
except EnvShieldException:
|
|
1906
|
+
# No Git repository -- there are no hooks to remove, which isn't
|
|
1907
|
+
# an error condition for a project-level uninstall the way it is
|
|
1908
|
+
# for 'hook remove' (a command that only ever makes sense inside
|
|
1909
|
+
# one). Continue on to reporting project configuration.
|
|
1910
|
+
hook_results = []
|
|
1911
|
+
|
|
1912
|
+
# Reused, not re-derived: 'remove_hooks()' already resolves this same
|
|
1913
|
+
# directory (respecting a configured 'core.hooksPath', e.g. Husky) to
|
|
1914
|
+
# decide where to look/delete -- the report must name that same real
|
|
1915
|
+
# location, never a hardcoded '.git/hooks/' that could be wrong.
|
|
1916
|
+
# 'get_hooks_dir()' always returns an absolute path; displaying it
|
|
1917
|
+
# relative to the current directory (when possible) is purely
|
|
1918
|
+
# cosmetic -- it keeps the common case reading exactly as it always
|
|
1919
|
+
# has, and avoids Rich hard-wrapping a long absolute path mid-string.
|
|
1920
|
+
hooks_dir = git_utils.get_hooks_dir()
|
|
1921
|
+
if hooks_dir:
|
|
1922
|
+
try:
|
|
1923
|
+
hooks_dir = os.path.relpath(hooks_dir, os.getcwd())
|
|
1924
|
+
except ValueError:
|
|
1925
|
+
pass
|
|
1926
|
+
else:
|
|
1927
|
+
hooks_dir = os.path.join(".git", "hooks")
|
|
1928
|
+
|
|
1929
|
+
removed_names = [r["name"] for r in hook_results if r["status"] == "removed"]
|
|
1930
|
+
preserved_hooks = [r for r in hook_results if r["status"] == "preserved"]
|
|
1931
|
+
|
|
1932
|
+
if removed_names:
|
|
1933
|
+
console.print("[bold]Removed:[/bold]")
|
|
1934
|
+
for hook_name in removed_names:
|
|
1935
|
+
console.print(
|
|
1936
|
+
f" [bold green]✓[/bold green] {os.path.join(hooks_dir, hook_name)}"
|
|
1937
|
+
)
|
|
1938
|
+
console.print()
|
|
1939
|
+
else:
|
|
1940
|
+
console.print(
|
|
1941
|
+
"[yellow]Nothing to remove -- no safely-owned EnvShield hooks were found.[/yellow]\n"
|
|
1942
|
+
)
|
|
1943
|
+
|
|
1944
|
+
# Everything below is report-only -- uninstall never deletes or
|
|
1945
|
+
# modifies any of it, regardless of whether EnvShield originally
|
|
1946
|
+
# created it (envshield.yml, a schema, .env.example) or only ever
|
|
1947
|
+
# populated it (a registered local_file, which may hold real secret
|
|
1948
|
+
# values and is never "EnvShield's own" to reclaim).
|
|
1949
|
+
preserved_lines = []
|
|
1950
|
+
for r in preserved_hooks:
|
|
1951
|
+
preserved_lines.append(
|
|
1952
|
+
(
|
|
1953
|
+
os.path.join(hooks_dir, r["name"]),
|
|
1954
|
+
"hook contents do not exactly match what EnvShield would "
|
|
1955
|
+
"generate now -- hand-modified, foreign, or stale relative "
|
|
1956
|
+
"to your current config",
|
|
1957
|
+
)
|
|
1958
|
+
)
|
|
1959
|
+
|
|
1960
|
+
if os.path.exists(config_manager.CONFIG_FILE_NAME):
|
|
1961
|
+
preserved_lines.append(
|
|
1962
|
+
(config_manager.CONFIG_FILE_NAME, "project configuration")
|
|
1963
|
+
)
|
|
1964
|
+
|
|
1965
|
+
for service_name in config_manager.get_services():
|
|
1966
|
+
schema_path = config_manager.get_service_schema_path(service_name)
|
|
1967
|
+
if schema_path and os.path.exists(schema_path):
|
|
1968
|
+
preserved_lines.append((schema_path, "project configuration"))
|
|
1969
|
+
|
|
1970
|
+
paths = config_manager.get_env_paths(service_name)
|
|
1971
|
+
if os.path.exists(paths["example_file"]):
|
|
1972
|
+
preserved_lines.append((paths["example_file"], "preserved"))
|
|
1973
|
+
if os.path.exists(paths["local_file"]):
|
|
1974
|
+
preserved_lines.append((paths["local_file"], "application configuration"))
|
|
1975
|
+
|
|
1976
|
+
if preserved_lines:
|
|
1977
|
+
console.print("[bold]Preserved:[/bold]")
|
|
1978
|
+
for path, reason in preserved_lines:
|
|
1979
|
+
console.print(f" • {path} -- {reason}")
|
|
1980
|
+
console.print()
|
|
1981
|
+
|
|
1982
|
+
if removed_names:
|
|
1983
|
+
console.print(
|
|
1984
|
+
"[bold green]EnvShield Git hook integration removed safely.[/bold green]"
|
|
1985
|
+
)
|
|
1986
|
+
if preserved_lines:
|
|
1987
|
+
console.print("Project configuration was preserved.")
|
|
1988
|
+
|
|
1989
|
+
|
|
1870
1990
|
@app.command(name="import")
|
|
1871
1991
|
def import_command(
|
|
1872
1992
|
file: str = typer.Argument(
|
|
@@ -122,6 +122,14 @@ class HooksManager:
|
|
|
122
122
|
def _do_install_hooks(self, force: bool = False) -> None:
|
|
123
123
|
"""Actually install the hooks (called after user confirms).
|
|
124
124
|
|
|
125
|
+
Only installs whichever hook(s) are actually missing -- an
|
|
126
|
+
already-present hook must not be touched (and re-prompted for its
|
|
127
|
+
own separate overwrite confirmation) merely because the OTHER hook
|
|
128
|
+
needed installing. `prompt_install_hooks`'s own "Install git
|
|
129
|
+
hooks? (...)" prompt already tells the user which hook(s) are
|
|
130
|
+
about to be installed; installing an unrelated, already-present
|
|
131
|
+
one here would silently do more than that prompt described.
|
|
132
|
+
|
|
125
133
|
Args:
|
|
126
134
|
force: If True, overwrite existing hooks.
|
|
127
135
|
"""
|
|
@@ -129,8 +137,11 @@ class HooksManager:
|
|
|
129
137
|
# Import here to avoid circular imports
|
|
130
138
|
from . import scanner
|
|
131
139
|
|
|
132
|
-
|
|
133
|
-
|
|
140
|
+
pre_commit, post_merge = self.are_hooks_installed()
|
|
141
|
+
if not pre_commit:
|
|
142
|
+
scanner.install_pre_commit_hook(force=force, non_interactive=False)
|
|
143
|
+
if not post_merge:
|
|
144
|
+
scanner.install_post_merge_hook(force=force, non_interactive=False)
|
|
134
145
|
except EnvShieldException as e:
|
|
135
146
|
console.print(f"[bold yellow]⚠️ Warning:[/] {e}")
|
|
136
147
|
|
|
@@ -890,39 +890,70 @@ def scan_result(
|
|
|
890
890
|
ENVSHIELD_HOOK_MARKER = "# Hook installed by EnvShield"
|
|
891
891
|
|
|
892
892
|
|
|
893
|
-
def remove_hooks() -> List[str]:
|
|
893
|
+
def remove_hooks() -> List[Dict[str, str]]:
|
|
894
894
|
"""
|
|
895
|
-
Removes
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
895
|
+
Removes only a hook file EnvShield can prove is its own, unmodified
|
|
896
|
+
output -- an exact content match against what EnvShield would generate
|
|
897
|
+
right now, the same ownership standard install_pre_commit_hook/
|
|
898
|
+
install_post_merge_hook already use (see their own comment: the marker
|
|
899
|
+
comment alone is not proof of ownership -- a user can modify a
|
|
900
|
+
generated hook, or hand-write one containing the same comment, while
|
|
901
|
+
keeping the marker text intact). A hook that isn't provably
|
|
902
|
+
EnvShield's own unmodified output is left alone -- whether it's
|
|
903
|
+
genuinely foreign (Husky, a hand-written script) or an EnvShield hook
|
|
904
|
+
a user has since extended.
|
|
905
|
+
|
|
906
|
+
Returns one {"name": ..., "status": ...} entry per hook type EnvShield
|
|
907
|
+
manages (pre-commit, post-merge), regardless of outcome -- "removed"
|
|
908
|
+
(deleted, exact match), "preserved" (a file exists but its content
|
|
909
|
+
doesn't exactly match current generated output -- hand-modified,
|
|
910
|
+
foreign, or stale relative to the current config), or "missing" (no
|
|
911
|
+
file at that path). This is the single source of truth for hook
|
|
912
|
+
ownership; 'hook remove' filters for "removed", 'uninstall' also
|
|
913
|
+
reports "preserved" -- neither re-derives the ownership check itself.
|
|
902
914
|
"""
|
|
903
915
|
git_root = git_utils.get_git_root()
|
|
904
916
|
if not git_root:
|
|
905
917
|
raise EnvShieldException("Not inside a Git repository.")
|
|
906
918
|
|
|
907
919
|
hooks_dir = git_utils.get_hooks_dir()
|
|
908
|
-
|
|
909
|
-
|
|
920
|
+
generators = {
|
|
921
|
+
"pre-commit": _generate_pre_commit_hook_content,
|
|
922
|
+
"post-merge": _generate_post_merge_hook_content,
|
|
923
|
+
}
|
|
924
|
+
results = []
|
|
925
|
+
for hook_name, generate_content in generators.items():
|
|
910
926
|
hook_path = os.path.join(hooks_dir, hook_name)
|
|
911
927
|
if not os.path.exists(hook_path):
|
|
928
|
+
results.append({"name": hook_name, "status": "missing"})
|
|
912
929
|
continue
|
|
913
930
|
with open(hook_path, "r") as f:
|
|
914
931
|
content = f.read()
|
|
915
|
-
if
|
|
932
|
+
if content != generate_content():
|
|
933
|
+
results.append({"name": hook_name, "status": "preserved"})
|
|
916
934
|
continue
|
|
917
935
|
os.remove(hook_path)
|
|
918
|
-
|
|
919
|
-
return
|
|
936
|
+
results.append({"name": hook_name, "status": "removed"})
|
|
937
|
+
return results
|
|
920
938
|
|
|
921
939
|
|
|
922
|
-
def _describe_existing_hook(content: str) -> str:
|
|
923
|
-
"""
|
|
924
|
-
|
|
940
|
+
def _describe_existing_hook(content: str, is_safely_regeneratable: bool) -> str:
|
|
941
|
+
"""
|
|
942
|
+
Best-effort description of an existing hook file, for the overwrite
|
|
943
|
+
warning. `is_safely_regeneratable` is an exact-content match against
|
|
944
|
+
what EnvShield would generate right now -- the marker comment alone is
|
|
945
|
+
not proof of that (see the call site's own comment): a user can modify
|
|
946
|
+
a generated hook, or hand-write one, while keeping the marker text
|
|
947
|
+
intact.
|
|
948
|
+
"""
|
|
949
|
+
if is_safely_regeneratable:
|
|
925
950
|
return "previously installed by EnvShield -- safe to regenerate"
|
|
951
|
+
if ENVSHIELD_HOOK_MARKER in content:
|
|
952
|
+
return (
|
|
953
|
+
"previously installed by EnvShield, but its contents no longer match "
|
|
954
|
+
"what EnvShield would generate now (hand-edited, or stale relative to "
|
|
955
|
+
"your current config) -- overwriting could discard those changes"
|
|
956
|
+
)
|
|
926
957
|
if "husky.sh" in content or ".husky" in content:
|
|
927
958
|
return "managed by Husky"
|
|
928
959
|
non_comment_lines = [
|
|
@@ -1056,10 +1087,18 @@ def install_pre_commit_hook(force: bool = False, non_interactive: bool = False):
|
|
|
1056
1087
|
with open(pre_commit_path, "r") as f:
|
|
1057
1088
|
existing_content = f.read()
|
|
1058
1089
|
|
|
1059
|
-
#
|
|
1060
|
-
#
|
|
1061
|
-
#
|
|
1062
|
-
|
|
1090
|
+
# A marker comment alone is not proof this file is EnvShield's own,
|
|
1091
|
+
# untouched output -- a user can modify a generated hook (or
|
|
1092
|
+
# hand-write one containing the same comment) while keeping the
|
|
1093
|
+
# marker text intact. Only an exact match against what EnvShield
|
|
1094
|
+
# would generate right now is provably safe to replace without
|
|
1095
|
+
# asking; non-interactive mode's fast path is scoped to exactly
|
|
1096
|
+
# that case -- anything else (including a marker-bearing file
|
|
1097
|
+
# that no longer matches) falls through to the same protected
|
|
1098
|
+
# confirmation/warning path as a genuinely foreign hook.
|
|
1099
|
+
is_safely_regeneratable = existing_content == hook_script_content
|
|
1100
|
+
|
|
1101
|
+
if non_interactive and not is_safely_regeneratable:
|
|
1063
1102
|
console.print(
|
|
1064
1103
|
"[bold yellow]⚠️ Warning:[/] A pre-commit hook already exists. EnvShield was not installed automatically."
|
|
1065
1104
|
)
|
|
@@ -1070,7 +1109,7 @@ def install_pre_commit_hook(force: bool = False, non_interactive: bool = False):
|
|
|
1070
1109
|
|
|
1071
1110
|
if not force and not non_interactive:
|
|
1072
1111
|
overwrite = questionary.confirm(
|
|
1073
|
-
f"A pre-commit hook already exists ({_describe_existing_hook(existing_content)}). Do you want to overwrite it?",
|
|
1112
|
+
f"A pre-commit hook already exists ({_describe_existing_hook(existing_content, is_safely_regeneratable)}). Do you want to overwrite it?",
|
|
1074
1113
|
default=False,
|
|
1075
1114
|
).ask()
|
|
1076
1115
|
if not overwrite:
|
|
@@ -1179,10 +1218,13 @@ def install_post_merge_hook(force: bool = False, non_interactive: bool = False):
|
|
|
1179
1218
|
with open(post_merge_path, "r") as f:
|
|
1180
1219
|
existing_content = f.read()
|
|
1181
1220
|
|
|
1182
|
-
#
|
|
1183
|
-
#
|
|
1184
|
-
#
|
|
1185
|
-
|
|
1221
|
+
# See install_pre_commit_hook's identical comment: a marker
|
|
1222
|
+
# comment alone is not proof this file is EnvShield's own,
|
|
1223
|
+
# untouched output. Only an exact match against what EnvShield
|
|
1224
|
+
# would generate right now is treated as provably safe.
|
|
1225
|
+
is_safely_regeneratable = existing_content == hook_script_content
|
|
1226
|
+
|
|
1227
|
+
if non_interactive and not is_safely_regeneratable:
|
|
1186
1228
|
console.print(
|
|
1187
1229
|
"[bold yellow]⚠️ Warning:[/] A post-merge hook already exists. EnvShield was not installed automatically."
|
|
1188
1230
|
)
|
|
@@ -1193,7 +1235,7 @@ def install_post_merge_hook(force: bool = False, non_interactive: bool = False):
|
|
|
1193
1235
|
|
|
1194
1236
|
if not force and not non_interactive:
|
|
1195
1237
|
overwrite = questionary.confirm(
|
|
1196
|
-
f"A post-merge hook already exists ({_describe_existing_hook(existing_content)}). Do you want to overwrite it?",
|
|
1238
|
+
f"A post-merge hook already exists ({_describe_existing_hook(existing_content, is_safely_regeneratable)}). Do you want to overwrite it?",
|
|
1197
1239
|
default=False,
|
|
1198
1240
|
).ask()
|
|
1199
1241
|
if not overwrite:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: envshield
|
|
3
|
-
Version: 4.7.
|
|
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
|
|
@@ -427,6 +427,24 @@ A supporting check alongside the contract, not the product itself. `secret = tru
|
|
|
427
427
|
|
|
428
428
|
---
|
|
429
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
|
+
|
|
430
448
|
## Known limitations
|
|
431
449
|
|
|
432
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:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "envshield"
|
|
7
|
-
version = "4.7.
|
|
7
|
+
version = "4.7.1" # must be a string for PEP 621 compliance
|
|
8
8
|
description = "EnvShield: environment variables as a version-controlled configuration contract."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "4.7.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|