bmsdna-devtools 0.31.0__tar.gz → 0.32.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.
- bmsdna_devtools-0.31.0/README.md → bmsdna_devtools-0.32.0/PKG-INFO +21 -0
- bmsdna_devtools-0.31.0/PKG-INFO → bmsdna_devtools-0.32.0/README.md +7 -14
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/cli.py +22 -0
- bmsdna_devtools-0.32.0/bmsdna/devtools/env_files.py +123 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/pyproject.toml +1 -1
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/skills/bmsdna-devtools/SKILL.md +8 -2
- bmsdna_devtools-0.32.0/tests/test_env_files.py +92 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/uv.lock +1 -1
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/.github/workflows/auto-release.yml +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/.github/workflows/python-publish.yml +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/.github/workflows/python-test.yml +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/.gitignore +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/.python-version +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/__init__.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/_api_dump.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/ado_auth.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/ado_issue.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/api_usage.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/app_service_logs.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/bdt_config.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/cli_tools.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/commit.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/dead_code.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/env_config.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/find_injection.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/find_repo.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/gh_issue.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/gh_pr.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/gitrepo.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/injection_frontend.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/injection_python.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/issue_do.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/lint.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/lint_findings.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/lint_models.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/lint_sql.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/lint_sql_files.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/lint_tooling.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/lint_typescript.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/logs.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/pr_build.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/pr_info.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/pr_issue_link.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/pr_labels.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/pr_markdown.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/pull.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/testdb.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/translate.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/bmsdna/devtools/worktree.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/justfile +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/conftest.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_ado_auth.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_ado_issue.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_ado_issue_flow.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_api_usage.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_app_service_logs.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_bdt_config.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_cli_cleanup.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_cli_commit.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_cli_issue.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_cli_pr_create.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_cli_pr_id.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_cli_pull.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_cli_tools.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_commit.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_env_config.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_find_injection.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_find_repo.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_gh_issue.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_gh_pr.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_gitrepo.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_issue_do.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_lint.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_lint_models.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_lint_sql.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_lint_sql_files.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_lint_tooling.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_lint_typescript.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_pr_build.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_pr_build_flow.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_pr_info.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_pr_issue_link.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_pr_labels.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_pr_markdown.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_pull.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_testdb.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_translate.py +0 -0
- {bmsdna_devtools-0.31.0 → bmsdna_devtools-0.32.0}/tests/test_worktree_clean.py +0 -0
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bmsdna-devtools
|
|
3
|
+
Version: 0.32.0
|
|
4
|
+
Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Requires-Dist: pgdevkit[db]>=0.7.1
|
|
7
|
+
Requires-Dist: requests>=2.32.0
|
|
8
|
+
Requires-Dist: rich>=13.0.0
|
|
9
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
10
|
+
Requires-Dist: typer>=0.26.7
|
|
11
|
+
Provides-Extra: lint
|
|
12
|
+
Requires-Dist: sqlglot>=27.0.0; extra == 'lint'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
1
15
|
# bmsdna-devtools
|
|
2
16
|
|
|
3
17
|
Shared developer tooling for BMS projects: PR build/check status and a
|
|
@@ -329,6 +343,13 @@ branch's PR closes (`Fixes #N`, or an issue/work-item URL in the PR body); it re
|
|
|
329
343
|
that is none or several. If the issue's newest comment already is a "Taken by ..." claim (by anyone) it
|
|
330
344
|
says so and posts nothing, so it's safe to run repeatedly -- including before `bdt issue do`.
|
|
331
345
|
|
|
346
|
+
## `bdt env get-keys`
|
|
347
|
+
|
|
348
|
+
Lists the variable **names** (never the values) defined in `.env`, `*.env` and `.env.*` files under the
|
|
349
|
+
current directory (recursively, skipping `node_modules`, `.git`, `.venv`, `.worktrees` and `.claude`) and directly in `~`, so you (or an agent) can see which variables exist without `cat .env`.
|
|
350
|
+
`--search TEXT` filters by key name (case-insensitive); `--no-home` leaves out the files in `~`. Files in `~` -- and any file using
|
|
351
|
+
`export VAR=VALUE` -- are bash syntax, so the output adds a `source <file>` hint. Exits 1 if nothing matches.
|
|
352
|
+
|
|
332
353
|
## `bdt translate`
|
|
333
354
|
|
|
334
355
|
One `translations.toml` is the source of truth for all UI translations; `bdt translate` generates the
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: bmsdna-devtools
|
|
3
|
-
Version: 0.31.0
|
|
4
|
-
Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
|
|
5
|
-
Requires-Python: >=3.14
|
|
6
|
-
Requires-Dist: pgdevkit[db]>=0.7.1
|
|
7
|
-
Requires-Dist: requests>=2.32.0
|
|
8
|
-
Requires-Dist: rich>=13.0.0
|
|
9
|
-
Requires-Dist: tomli-w>=1.0.0
|
|
10
|
-
Requires-Dist: typer>=0.26.7
|
|
11
|
-
Provides-Extra: lint
|
|
12
|
-
Requires-Dist: sqlglot>=27.0.0; extra == 'lint'
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
|
|
15
1
|
# bmsdna-devtools
|
|
16
2
|
|
|
17
3
|
Shared developer tooling for BMS projects: PR build/check status and a
|
|
@@ -343,6 +329,13 @@ branch's PR closes (`Fixes #N`, or an issue/work-item URL in the PR body); it re
|
|
|
343
329
|
that is none or several. If the issue's newest comment already is a "Taken by ..." claim (by anyone) it
|
|
344
330
|
says so and posts nothing, so it's safe to run repeatedly -- including before `bdt issue do`.
|
|
345
331
|
|
|
332
|
+
## `bdt env get-keys`
|
|
333
|
+
|
|
334
|
+
Lists the variable **names** (never the values) defined in `.env`, `*.env` and `.env.*` files under the
|
|
335
|
+
current directory (recursively, skipping `node_modules`, `.git`, `.venv`, `.worktrees` and `.claude`) and directly in `~`, so you (or an agent) can see which variables exist without `cat .env`.
|
|
336
|
+
`--search TEXT` filters by key name (case-insensitive); `--no-home` leaves out the files in `~`. Files in `~` -- and any file using
|
|
337
|
+
`export VAR=VALUE` -- are bash syntax, so the output adds a `source <file>` hint. Exits 1 if nothing matches.
|
|
338
|
+
|
|
346
339
|
## `bdt translate`
|
|
347
340
|
|
|
348
341
|
One `translations.toml` is the source of truth for all UI translations; `bdt translate` generates the
|
|
@@ -17,6 +17,7 @@ from pgdevkit.testdb import constants as pgdevkit_constants
|
|
|
17
17
|
|
|
18
18
|
from . import ado_issue, api_usage as api_usage_mod, app_service_logs, commit as commit_mod, dead_code as dead_code_mod
|
|
19
19
|
from . import env_config
|
|
20
|
+
from . import env_files as env_files_mod
|
|
20
21
|
from . import find_repo as find_repo_mod
|
|
21
22
|
from . import issue_do as issue_do_mod
|
|
22
23
|
from . import gh_issue, gh_pr
|
|
@@ -74,6 +75,9 @@ app.add_typer(pr_app, name="pr")
|
|
|
74
75
|
translate_app = typer.Typer(name="translate", help="Generate <lng>.json files from translations.toml")
|
|
75
76
|
app.add_typer(translate_app, name="translate")
|
|
76
77
|
|
|
78
|
+
env_app = typer.Typer(name="env", help="Inspect .env files without exposing their values")
|
|
79
|
+
app.add_typer(env_app, name="env")
|
|
80
|
+
|
|
77
81
|
issue_app = typer.Typer(name="issue", help="Issue / work item commands (Azure DevOps or GitHub, auto-detected from the git remote)")
|
|
78
82
|
app.add_typer(issue_app, name="issue")
|
|
79
83
|
|
|
@@ -1209,6 +1213,24 @@ def translate_cmd(
|
|
|
1209
1213
|
typer.echo(f"Wrote {len(res.written)} file(s)")
|
|
1210
1214
|
|
|
1211
1215
|
|
|
1216
|
+
@env_app.command("get-keys")
|
|
1217
|
+
def env_get_keys(
|
|
1218
|
+
search: str | None = typer.Option(None, "--search", "-s", help="Only keys containing this text (case-insensitive)"),
|
|
1219
|
+
no_home: bool = typer.Option(False, "--no-home", help="Don't also list the env files directly in ~"),
|
|
1220
|
+
) -> None:
|
|
1221
|
+
"""List variable names (never values) from .env, *.env and .env.* files under the current directory (and in ~).
|
|
1222
|
+
|
|
1223
|
+
Skips node_modules, .git, .venv, .worktrees and .claude.
|
|
1224
|
+
"""
|
|
1225
|
+
home = None if no_home else Path.home()
|
|
1226
|
+
cwd = Path.cwd()
|
|
1227
|
+
files = env_files_mod.get_keys(cwd, home, search)
|
|
1228
|
+
if not files:
|
|
1229
|
+
typer.echo("No matching env files/keys found.", err=True)
|
|
1230
|
+
raise typer.Exit(1)
|
|
1231
|
+
typer.echo(env_files_mod.format_keys(files, home, cwd))
|
|
1232
|
+
|
|
1233
|
+
|
|
1212
1234
|
@translate_app.command("add")
|
|
1213
1235
|
def translate_add(
|
|
1214
1236
|
key: str = typer.Argument(..., help="Translation key, e.g. ADD_BUTTON"),
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""List the variable *names* defined in dotenv-style files, never their values.
|
|
2
|
+
|
|
3
|
+
Lets an agent (or human) find out which variables exist without `cat .env` /
|
|
4
|
+
`printenv` putting secrets into the transcript.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
_KEY_RE = re.compile(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=")
|
|
15
|
+
# Never descended into: dependency/VCS/tooling trees that are huge and never hold project env files.
|
|
16
|
+
SKIP_DIRS = frozenset({"node_modules", ".git", ".venv", ".worktrees", ".claude"})
|
|
17
|
+
|
|
18
|
+
_EXPORT_RE = re.compile(r"^\s*export\s+[A-Za-z_]")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class EnvFile:
|
|
23
|
+
path: Path
|
|
24
|
+
keys: list[str] = field(default_factory=list)
|
|
25
|
+
uses_export: bool = False # bash syntax (`export VAR=VALUE`): meant to be sourced
|
|
26
|
+
in_home: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_env_name(name: str) -> bool:
|
|
30
|
+
return name == ".env" or name.endswith(".env") or name.startswith(".env.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _walk_env_files(root: Path) -> list[Path]:
|
|
34
|
+
"""Env files under root, pruning SKIP_DIRS (and not following symlinked dirs) so big trees stay fast."""
|
|
35
|
+
found: list[Path] = []
|
|
36
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
37
|
+
dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS)
|
|
38
|
+
base = Path(dirpath)
|
|
39
|
+
found.extend(base / n for n in sorted(filenames) if _is_env_name(n) and (base / n).is_file())
|
|
40
|
+
return found
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def find_env_files(cwd: Path, home: Path | None = None) -> list[tuple[Path, bool]]:
|
|
44
|
+
"""`.env`, `*.env` and `.env.*` under cwd (recursive, skipping SKIP_DIRS) and directly in home.
|
|
45
|
+
|
|
46
|
+
Returns (path, in_home). When cwd is home itself it is not walked recursively.
|
|
47
|
+
"""
|
|
48
|
+
found: list[tuple[Path, bool]] = []
|
|
49
|
+
seen: set[Path] = set()
|
|
50
|
+
|
|
51
|
+
def add(p: Path, in_home: bool) -> None:
|
|
52
|
+
real = p.resolve()
|
|
53
|
+
if real not in seen:
|
|
54
|
+
seen.add(real)
|
|
55
|
+
found.append((p, in_home))
|
|
56
|
+
|
|
57
|
+
if home is not None and cwd.resolve() == home.resolve():
|
|
58
|
+
cwd_files = [p for p in sorted(cwd.iterdir()) if _is_env_name(p.name) and p.is_file()]
|
|
59
|
+
else:
|
|
60
|
+
cwd_files = _walk_env_files(cwd)
|
|
61
|
+
for p in cwd_files:
|
|
62
|
+
add(p, False)
|
|
63
|
+
if home is not None:
|
|
64
|
+
for p in sorted(home.iterdir()):
|
|
65
|
+
if _is_env_name(p.name) and p.is_file():
|
|
66
|
+
add(p, True)
|
|
67
|
+
return found
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_env_file(path: Path, in_home: bool = False) -> EnvFile:
|
|
71
|
+
res = EnvFile(path=path, in_home=in_home)
|
|
72
|
+
try:
|
|
73
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
74
|
+
except OSError:
|
|
75
|
+
return res
|
|
76
|
+
for line in text.splitlines():
|
|
77
|
+
if line.lstrip().startswith("#"):
|
|
78
|
+
continue
|
|
79
|
+
m = _KEY_RE.match(line)
|
|
80
|
+
if m:
|
|
81
|
+
if m.group(1) not in res.keys:
|
|
82
|
+
res.keys.append(m.group(1))
|
|
83
|
+
if _EXPORT_RE.match(line):
|
|
84
|
+
res.uses_export = True
|
|
85
|
+
return res
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_keys(cwd: Path, home: Path | None = None, search: str | None = None) -> list[EnvFile]:
|
|
89
|
+
"""Env files with their key names; with `search`, only keys containing it (case-insensitive)."""
|
|
90
|
+
out: list[EnvFile] = []
|
|
91
|
+
for path, in_home in find_env_files(cwd, home):
|
|
92
|
+
ef = parse_env_file(path, in_home)
|
|
93
|
+
if search:
|
|
94
|
+
ef.keys = [k for k in ef.keys if search.lower() in k.lower()]
|
|
95
|
+
if not ef.keys:
|
|
96
|
+
continue
|
|
97
|
+
out.append(ef)
|
|
98
|
+
return out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _display(path: Path, cwd: Path | None, home: Path | None) -> Path:
|
|
102
|
+
for base, prefix in ((cwd, None), (home, Path("~"))):
|
|
103
|
+
if base is None:
|
|
104
|
+
continue
|
|
105
|
+
try:
|
|
106
|
+
rel = path.relative_to(base)
|
|
107
|
+
except ValueError:
|
|
108
|
+
continue
|
|
109
|
+
return prefix / rel if prefix else rel
|
|
110
|
+
return path
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def format_keys(files: list[EnvFile], home: Path | None = None, cwd: Path | None = None) -> str:
|
|
114
|
+
"""Paths under cwd are shown relative to it, home files as ~/..., so the `source` hint works as printed."""
|
|
115
|
+
lines: list[str] = []
|
|
116
|
+
for ef in files:
|
|
117
|
+
shown = _display(ef.path, None if ef.in_home else cwd, home if ef.in_home else None)
|
|
118
|
+
lines.append(f"{shown}:")
|
|
119
|
+
if ef.in_home or ef.uses_export:
|
|
120
|
+
src = f"./{shown}" if not shown.is_absolute() and not str(shown).startswith("~") else shown
|
|
121
|
+
lines.append(f" # bash syntax, load with: source {src}")
|
|
122
|
+
lines.extend(f" {k}" for k in ef.keys)
|
|
123
|
+
return "\n".join(lines)
|
|
@@ -10,7 +10,7 @@ packages = ["bmsdna"]
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "bmsdna-devtools"
|
|
13
|
-
version = "0.
|
|
13
|
+
version = "0.32.0"
|
|
14
14
|
description = "Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects"
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.14" # pgdevkit>=0.7.1 requires 3.14; was >=3.11 before adding it as a dependency
|
|
@@ -6,11 +6,11 @@ description: >
|
|
|
6
6
|
a PR, creating a git worktree, committing and pushing files (with pre-flight
|
|
7
7
|
checks), claiming an issue / work item (`bdt issue take`), a one-shot PR summary
|
|
8
8
|
(`bdt pr info`: link, build state, closed issues), querying Azure logs, and static checks (`bdt lint`: SQL/psycopg rules, pydantic placement, tooling; `bdt dead-code`:
|
|
9
|
-
unreferenced .sql files and backend routes nothing calls). `bdt pr *` works against both Azure DevOps
|
|
9
|
+
unreferenced .sql files and backend routes nothing calls), listing which env vars a repo defines (`bdt env get-keys`, names only). `bdt pr *` works against both Azure DevOps
|
|
10
10
|
and GitHub — it auto-detects which one from the `origin` remote. Trigger
|
|
11
11
|
whenever the user asks to check a build/PR status, create a PR, make a
|
|
12
12
|
worktree, commit changes, fetch/tail application logs, lint a repo, find dead API routes /
|
|
13
|
-
unused .sql files, or start working on / take an issue ("work on issue #N"), in a repo that has
|
|
13
|
+
unused .sql files, find out which environment variables exist, or start working on / take an issue ("work on issue #N"), in a repo that has
|
|
14
14
|
bmsdna-devtools installed (check for `bdt` on PATH, or `bmsdna-devtools` in
|
|
15
15
|
pyproject.toml, before assuming it applies).
|
|
16
16
|
---
|
|
@@ -130,6 +130,12 @@ vendor a git submodule under that path.
|
|
|
130
130
|
adopt with `--update-baseline`). `--only sql|routes` runs one check. The routes check imports the app, so run it with
|
|
131
131
|
the repo's own interpreter (`uv run`). Generated API-client code and tests never count as callers.
|
|
132
132
|
|
|
133
|
+
## Environment variables
|
|
134
|
+
|
|
135
|
+
`bdt env get-keys [--search TEXT] [--no-home]` lists the variable **names** (never values) from `.env`, `*.env` and `.env.*`
|
|
136
|
+
files under the current directory (recursive; skips node_modules/.git/.venv/.worktrees/.claude) and in `~` (`--no-home` skips those). Use it instead of `cat .env` / `printenv` to find out which variables exist;
|
|
137
|
+
files in `~` (and ones using `export VAR=...`) are bash syntax, so the output says to `source` them.
|
|
138
|
+
|
|
133
139
|
## Application Insights logs
|
|
134
140
|
|
|
135
141
|
```bash
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from typer.testing import CliRunner
|
|
4
|
+
|
|
5
|
+
from bmsdna.devtools.cli import app
|
|
6
|
+
from bmsdna.devtools.env_files import format_keys, get_keys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _setup(tmp_path: Path) -> tuple[Path, Path]:
|
|
10
|
+
cwd = tmp_path / "proj"
|
|
11
|
+
home = tmp_path / "home"
|
|
12
|
+
cwd.mkdir()
|
|
13
|
+
home.mkdir()
|
|
14
|
+
(cwd / ".env").write_text("# comment\nDB_URL=postgres://u:secret@h/db\n\nAPI_KEY = 'topsecret'\nDB_URL=dup\n")
|
|
15
|
+
(cwd / "ci.env").write_text("export TOKEN=abc\n")
|
|
16
|
+
(cwd / "notes.txt").write_text("NOT_AN_ENV=1\n")
|
|
17
|
+
(home / "work.env").write_text("export HOME_VAR=hunter2\n")
|
|
18
|
+
return cwd, home
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_lists_keys_only_never_values(tmp_path):
|
|
22
|
+
cwd, home = _setup(tmp_path)
|
|
23
|
+
out = format_keys(get_keys(cwd, home), home, cwd)
|
|
24
|
+
assert "DB_URL" in out and "API_KEY" in out and "TOKEN" in out and "HOME_VAR" in out
|
|
25
|
+
assert "secret" not in out and "hunter2" not in out and "abc" not in out
|
|
26
|
+
assert "NOT_AN_ENV" not in out
|
|
27
|
+
assert out.count("DB_URL") == 1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_bash_syntax_hint_for_home_and_export_files(tmp_path):
|
|
31
|
+
cwd, home = _setup(tmp_path)
|
|
32
|
+
out = format_keys(get_keys(cwd, home), home, cwd)
|
|
33
|
+
assert "source ~/work.env" in out
|
|
34
|
+
assert "source ./ci.env" in out
|
|
35
|
+
assert "source ./.env" not in out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_search_filters_keys_and_drops_empty_files(tmp_path):
|
|
39
|
+
cwd, home = _setup(tmp_path)
|
|
40
|
+
files = get_keys(cwd, home, "api")
|
|
41
|
+
assert [f.keys for f in files] == [["API_KEY"]]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_cli_exit_1_when_nothing_found(tmp_path, monkeypatch):
|
|
45
|
+
monkeypatch.chdir(tmp_path)
|
|
46
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
47
|
+
res = CliRunner().invoke(app, ["env", "get-keys"])
|
|
48
|
+
assert res.exit_code == 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_cli_prints_keys(tmp_path, monkeypatch):
|
|
52
|
+
cwd, home = _setup(tmp_path)
|
|
53
|
+
monkeypatch.chdir(cwd)
|
|
54
|
+
monkeypatch.setenv("HOME", str(home))
|
|
55
|
+
res = CliRunner().invoke(app, ["env", "get-keys", "--search", "token"])
|
|
56
|
+
assert res.exit_code == 0
|
|
57
|
+
assert "TOKEN" in res.output and "abc" not in res.output
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_scans_subdirectories_but_skips_excluded_dirs(tmp_path):
|
|
61
|
+
cwd, home = _setup(tmp_path)
|
|
62
|
+
(cwd / "app").mkdir()
|
|
63
|
+
(cwd / "app" / ".env").write_text("SUB_VAR=1\n")
|
|
64
|
+
for d in ("node_modules", ".git", ".venv", ".worktrees", ".claude"):
|
|
65
|
+
(cwd / d / "pkg").mkdir(parents=True)
|
|
66
|
+
(cwd / d / "pkg" / ".env").write_text(f"SKIPPED_{d.strip('.').upper()}=1\n")
|
|
67
|
+
out = format_keys(get_keys(cwd, home), home, cwd)
|
|
68
|
+
assert "app/.env:" in out and "SUB_VAR" in out
|
|
69
|
+
assert "SKIPPED_" not in out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_no_home_omits_home_files(tmp_path):
|
|
73
|
+
cwd, home = _setup(tmp_path)
|
|
74
|
+
out = format_keys(get_keys(cwd, None), None, cwd)
|
|
75
|
+
assert "HOME_VAR" not in out and "DB_URL" in out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_cwd_equal_home_is_not_walked_recursively(tmp_path):
|
|
79
|
+
cwd, home = _setup(tmp_path)
|
|
80
|
+
(home / "deep").mkdir()
|
|
81
|
+
(home / "deep" / ".env").write_text("DEEP=1\n")
|
|
82
|
+
out = format_keys(get_keys(home, home), home, home)
|
|
83
|
+
assert "HOME_VAR" in out and "DEEP" not in out
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_cli_no_home_flag(tmp_path, monkeypatch):
|
|
87
|
+
cwd, home = _setup(tmp_path)
|
|
88
|
+
monkeypatch.chdir(cwd)
|
|
89
|
+
monkeypatch.setenv("HOME", str(home))
|
|
90
|
+
res = CliRunner().invoke(app, ["env", "get-keys", "--no-home"])
|
|
91
|
+
assert res.exit_code == 0
|
|
92
|
+
assert "DB_URL" in res.output and "HOME_VAR" not in res.output
|
|
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
|
|
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
|