The CLI loads .env itself — init's promise finally holds
First finding of Eric's clean-room run, and the exact kind the rehearsal exists for: init writes credentials to .env, but nothing ever loaded it — the dev repo's committed .envrc + direnv did it invisibly, and a fresh directory has neither. The web banner then advised "run bggpipe init or load .env", circular counsel for someone who just ran init. A typer callback now loads ./.env before every command, using the same parsing rules as the wizard that writes it (export prefixes, quoted values, quoted-empty = unset). Real environment variables always outrank the file, so direnv setups and explicit overrides keep working unchanged. Verified in a scrubbed-environment clean room: the credentials banner is gone with nothing but .env present. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
co-authored by
Claude Fable 5
parent
2542560d57
commit
1df784e253
@@ -14,6 +14,17 @@ app = typer.Typer(
|
||||
no_args_is_help=True,
|
||||
)
|
||||
|
||||
|
||||
@app.callback()
|
||||
def _load_dotenv() -> None:
|
||||
# credentials live in ./.env (written by init); load them so a fresh
|
||||
# directory works without direnv or manual sourcing. Real environment
|
||||
# variables always take precedence over the file.
|
||||
from bggpipe.init_wizard import load_env_file
|
||||
|
||||
load_env_file(Path(".env"))
|
||||
|
||||
|
||||
ConfigOpt = Annotated[
|
||||
Path | None,
|
||||
typer.Option("--config", help="Path to config.toml (default: ./config.toml)"),
|
||||
|
||||
@@ -91,6 +91,31 @@ class InitReport:
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def load_env_file(env_path: Path) -> list[str]:
|
||||
"""Export .env values into this process's environment — only keys the
|
||||
environment doesn't already set (a real env var always wins). Returns
|
||||
the keys loaded. Same parsing rules as _env_file_keys; values go
|
||||
straight into os.environ and are never printed or logged."""
|
||||
if not env_path.exists():
|
||||
return []
|
||||
loaded = []
|
||||
for line in env_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if "=" not in line or line.startswith("#"):
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
key = key.strip()
|
||||
if key.startswith("export "):
|
||||
key = key.removeprefix("export ").strip()
|
||||
value = value.strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
|
||||
value = value[1:-1]
|
||||
if key and value and not os.environ.get(key):
|
||||
os.environ[key] = value
|
||||
loaded.append(key)
|
||||
return loaded
|
||||
|
||||
|
||||
def _env_file_keys(env_path: Path) -> set[str]:
|
||||
"""Key names with non-empty values in .env. Handles `export KEY=v` and
|
||||
quoted values; a quoted-empty value ("" / '') counts as NOT set. Values
|
||||
|
||||
Reference in New Issue
Block a user