devready-cli 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- devready/__init__.py +1 -1
- devready/baseline.py +43 -0
- devready/checks/config.py +102 -0
- devready/cli.py +25 -6
- devready/explain.py +4 -0
- devready/runner.py +12 -1
- {devready_cli-0.1.0.dist-info → devready_cli-0.2.0.dist-info}/METADATA +6 -2
- {devready_cli-0.1.0.dist-info → devready_cli-0.2.0.dist-info}/RECORD +11 -9
- {devready_cli-0.1.0.dist-info → devready_cli-0.2.0.dist-info}/WHEEL +0 -0
- {devready_cli-0.1.0.dist-info → devready_cli-0.2.0.dist-info}/entry_points.txt +0 -0
- {devready_cli-0.1.0.dist-info → devready_cli-0.2.0.dist-info}/licenses/LICENSE +0 -0
devready/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.2.0"
|
devready/baseline.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Compare a run with a saved report (`devready check --format json > baseline.json`).
|
|
2
|
+
|
|
3
|
+
DevReady only reads the baseline; saving or committing it is up to you. That covers both a
|
|
4
|
+
shared team baseline (committed file) and diffing against an earlier local run.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from devready.checks.project import _read
|
|
14
|
+
from devready.models import Result, Status
|
|
15
|
+
|
|
16
|
+
_ID = re.compile(r"^[a-z_]+(\.[a-z_]+)+$")
|
|
17
|
+
_RANK = {Status.PASS: 0, Status.SKIP: 0, Status.WARN: 1, Status.ERROR: 2, Status.FAIL: 3}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_baseline(path: Path) -> dict[str, Status] | None:
|
|
21
|
+
"""check_id -> status, or None if the file is missing, too big, or not a v1 report."""
|
|
22
|
+
text = _read(path)
|
|
23
|
+
try:
|
|
24
|
+
data = json.loads(text) if text else None
|
|
25
|
+
if data["schema_version"] != 1:
|
|
26
|
+
return None
|
|
27
|
+
return {
|
|
28
|
+
r["check_id"]: Status(r["status"])
|
|
29
|
+
for r in data["results"]
|
|
30
|
+
if isinstance(r["check_id"], str) and _ID.match(r["check_id"])
|
|
31
|
+
}
|
|
32
|
+
except (ValueError, KeyError, TypeError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def diff(results: list[Result], base: dict[str, Status]) -> tuple[list[Result], bool]:
|
|
37
|
+
"""(results that are new or changed status, whether any FAIL got worse)."""
|
|
38
|
+
changed = [r for r in results if base.get(r.check_id) is not r.status]
|
|
39
|
+
worse = any(
|
|
40
|
+
r.status is Status.FAIL and _RANK[r.status] > _RANK.get(base.get(r.check_id), 0)
|
|
41
|
+
for r in changed
|
|
42
|
+
)
|
|
43
|
+
return changed, worse
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Optional `.devready.json`: extra requirements, required env var names, checks to skip.
|
|
2
|
+
|
|
3
|
+
{"skip": ["project.ports"], "require": {"node": ">=20", "python": ">=3.11"}, "env": ["API_KEY"]}
|
|
4
|
+
|
|
5
|
+
Read-only, same file rules as project.py. Env vars are tested for presence only; values are
|
|
6
|
+
never read. Invalid files give one ERROR result and never echo file content.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from devready.checks.project import _compare, _read
|
|
18
|
+
from devready.models import Result, Status
|
|
19
|
+
from devready.versions import SAFE_SPEC, parse_version
|
|
20
|
+
|
|
21
|
+
CONFIG_FILE = ".devready.json"
|
|
22
|
+
_ID = re.compile(r"^[a-z_]+(\.[a-z_]+)*$")
|
|
23
|
+
_ENV_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,63}$")
|
|
24
|
+
_RUNTIMES = {"node": "Node", "python": "Python"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _valid(cfg: object) -> bool:
|
|
28
|
+
if not isinstance(cfg, dict) or not set(cfg) <= {"skip", "require", "env"}:
|
|
29
|
+
return False
|
|
30
|
+
skip, req, env = cfg.get("skip", []), cfg.get("require", {}), cfg.get("env", [])
|
|
31
|
+
return (
|
|
32
|
+
isinstance(skip, list)
|
|
33
|
+
and all(isinstance(i, str) and _ID.match(i) for i in skip)
|
|
34
|
+
and isinstance(req, dict)
|
|
35
|
+
and all(
|
|
36
|
+
k in _RUNTIMES and isinstance(v, str) and SAFE_SPEC.match(v) for k, v in req.items()
|
|
37
|
+
)
|
|
38
|
+
and isinstance(env, list)
|
|
39
|
+
and all(isinstance(n, str) and _ENV_NAME.match(n) for n in env)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_config(root: Path) -> tuple[dict | None, Result | None]:
|
|
44
|
+
"""(config, None) if absent/valid ({} when absent), (None, error result) if invalid."""
|
|
45
|
+
text = _read(root / CONFIG_FILE)
|
|
46
|
+
if text is None:
|
|
47
|
+
return {}, None
|
|
48
|
+
try:
|
|
49
|
+
cfg = json.loads(text)
|
|
50
|
+
except ValueError:
|
|
51
|
+
cfg = None
|
|
52
|
+
if _valid(cfg):
|
|
53
|
+
return cfg, None
|
|
54
|
+
return None, Result(
|
|
55
|
+
"project.config.file",
|
|
56
|
+
"Config",
|
|
57
|
+
Status.ERROR,
|
|
58
|
+
f"{CONFIG_FILE} is invalid; it was ignored",
|
|
59
|
+
'Allowed keys: "skip" (check ids), "require" (node/python specs), "env" (variable names).',
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_config(
|
|
64
|
+
cfg: dict, node: str | None, python: str | None, environ: Mapping[str, str] = os.environ
|
|
65
|
+
) -> list[Result]:
|
|
66
|
+
out: list[Result] = []
|
|
67
|
+
versions = {
|
|
68
|
+
"node": parse_version(node) if node else None,
|
|
69
|
+
"python": parse_version(python or ""),
|
|
70
|
+
}
|
|
71
|
+
for key, spec in cfg.get("require", {}).items():
|
|
72
|
+
out.append(
|
|
73
|
+
_compare(
|
|
74
|
+
f"project.{key}.config",
|
|
75
|
+
f"{_RUNTIMES[key]} required",
|
|
76
|
+
CONFIG_FILE,
|
|
77
|
+
spec,
|
|
78
|
+
versions[key],
|
|
79
|
+
Status.FAIL,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
wanted = cfg.get("env", [])
|
|
83
|
+
if wanted:
|
|
84
|
+
missing = [n for n in wanted if n not in environ]
|
|
85
|
+
cid, title = "project.env.required", "Required env"
|
|
86
|
+
if missing:
|
|
87
|
+
out.append(
|
|
88
|
+
Result(
|
|
89
|
+
cid,
|
|
90
|
+
title,
|
|
91
|
+
Status.FAIL,
|
|
92
|
+
f"{len(missing)} of {len(wanted)} not set: {', '.join(missing[:10])}",
|
|
93
|
+
"Set these variables in your shell or CI (values are never read).",
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
out.append(Result(cid, title, Status.PASS, f"all {len(wanted)} variables are set"))
|
|
98
|
+
return out
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Empty on purpose: pre-filling "env" from .env.example would make optional variables fail.
|
|
102
|
+
TEMPLATE = json.dumps({"skip": [], "require": {}, "env": []}, indent=2)
|
devready/cli.py
CHANGED
|
@@ -6,9 +6,10 @@ import sys
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from devready import __version__
|
|
9
|
+
from devready.baseline import diff, load_baseline
|
|
9
10
|
from devready.formats import fix_plan, junit, sarif
|
|
10
11
|
from devready.models import Result, Status
|
|
11
|
-
from devready.runner import report, run_checks
|
|
12
|
+
from devready.runner import matches, report, run_checks
|
|
12
13
|
|
|
13
14
|
EXIT_OK, EXIT_FAILED, EXIT_USAGE, EXIT_INTERNAL = 0, 1, 2, 3
|
|
14
15
|
|
|
@@ -20,12 +21,8 @@ def _text(results: list[Result]) -> str:
|
|
|
20
21
|
return "\n".join(lines)
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
def _matches(check_id: str, ids: list[str]) -> bool:
|
|
24
|
-
return any(check_id == i or check_id.startswith(i + ".") for i in ids)
|
|
25
|
-
|
|
26
|
-
|
|
27
24
|
def _selected(check_id: str, only: list[str], skip: list[str]) -> bool:
|
|
28
|
-
return (not only or
|
|
25
|
+
return (not only or matches(check_id, only)) and not matches(check_id, skip)
|
|
29
26
|
|
|
30
27
|
|
|
31
28
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -34,10 +31,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
34
31
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
35
32
|
check = sub.add_parser("check", help="run read-only environment checks")
|
|
36
33
|
check.add_argument("--format", choices=["text", "json", "junit", "sarif"], default="text")
|
|
34
|
+
check.add_argument(
|
|
35
|
+
"--baseline", metavar="FILE", help="saved --format json report; show only what changed"
|
|
36
|
+
)
|
|
37
37
|
fix = sub.add_parser("fix", help="print suggested fixes (never runs them)")
|
|
38
38
|
fix.add_argument("--plan", action="store_true", help="required: list steps without running")
|
|
39
39
|
explain = sub.add_parser("explain", help="explain what a check id means")
|
|
40
40
|
explain.add_argument("check_id")
|
|
41
|
+
sub.add_parser("init", help="print a starter .devready.json (does not write it)")
|
|
41
42
|
sub.add_parser("mcp", help="run as an MCP server over stdio")
|
|
42
43
|
for p in (check, fix):
|
|
43
44
|
p.add_argument("--project", default=".", help="directory whose version pins to compare")
|
|
@@ -54,6 +55,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
54
55
|
|
|
55
56
|
return serve()
|
|
56
57
|
|
|
58
|
+
if args.command == "init":
|
|
59
|
+
from devready.checks.config import TEMPLATE
|
|
60
|
+
|
|
61
|
+
print(TEMPLATE)
|
|
62
|
+
return EXIT_OK
|
|
63
|
+
|
|
57
64
|
if args.command == "explain":
|
|
58
65
|
from devready.explain import EXPLANATIONS
|
|
59
66
|
|
|
@@ -86,12 +93,24 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
86
93
|
if args.command == "fix":
|
|
87
94
|
print(fix_plan(results))
|
|
88
95
|
return EXIT_OK
|
|
96
|
+
|
|
97
|
+
regressed = None
|
|
98
|
+
if args.baseline:
|
|
99
|
+
base = load_baseline(Path(args.baseline))
|
|
100
|
+
if base is None:
|
|
101
|
+
print("devready: baseline is not a readable devready JSON report", file=sys.stderr)
|
|
102
|
+
return EXIT_USAGE
|
|
103
|
+
total = len(results)
|
|
104
|
+
results, regressed = diff(results, base)
|
|
105
|
+
print(f"Compared with baseline: {len(results)} of {total} changed.", file=sys.stderr)
|
|
89
106
|
if args.format == "json":
|
|
90
107
|
print(json.dumps(report(results), indent=2))
|
|
91
108
|
elif args.format in ("junit", "sarif"):
|
|
92
109
|
print(junit(results) if args.format == "junit" else sarif(results))
|
|
93
110
|
else:
|
|
94
111
|
print(_text(results))
|
|
112
|
+
if regressed is not None: # with a baseline only a new or worse FAIL breaks the build
|
|
113
|
+
return EXIT_FAILED if regressed else EXIT_OK
|
|
95
114
|
return EXIT_FAILED if any(r.status is Status.FAIL for r in results) else EXIT_OK
|
|
96
115
|
|
|
97
116
|
|
devready/explain.py
CHANGED
|
@@ -43,4 +43,8 @@ EXPLANATIONS = {
|
|
|
43
43
|
"project.env.ignored": "Warns when .env exists but git would commit it (not ignored, or already tracked). Values are never shown.",
|
|
44
44
|
"project.node.modules": "Checked when package.json lists dependencies: node_modules must exist and not be older than a lockfile.",
|
|
45
45
|
"project.ports.free": "Ports declared in compose files (host side) or *PORT= lines in .env.example, tested by connecting to localhost. A busy port may be this project already running.",
|
|
46
|
+
"project.config.file": ".devready.json is optional. With unknown keys or invalid values it is ignored entirely. Run `devready init` for a starter.",
|
|
47
|
+
"project.node.config": "require.node in .devready.json is a hard requirement (fails when unmet).",
|
|
48
|
+
"project.python.config": "require.python in .devready.json is a hard requirement (fails when unmet).",
|
|
49
|
+
"project.env.required": "Variable NAMES listed under env in .devready.json must be set in the environment. Values are never read.",
|
|
46
50
|
}
|
devready/runner.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import platform
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
from devready.checks.config import check_config, load_config
|
|
6
7
|
from devready.checks.extras import check_disk, check_docker, check_git_identity
|
|
7
8
|
from devready.checks.health import check_health
|
|
8
9
|
from devready.checks.project import check_project
|
|
@@ -12,7 +13,12 @@ from devready.checks.tools import TOOLS, check_tool
|
|
|
12
13
|
from devready.models import Result
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
def matches(check_id: str, ids: list[str]) -> bool:
|
|
17
|
+
return any(check_id == i or check_id.startswith(i + ".") for i in ids)
|
|
18
|
+
|
|
19
|
+
|
|
15
20
|
def run_checks(root: Path) -> list[Result]:
|
|
21
|
+
cfg, cfg_error = load_config(root)
|
|
16
22
|
results = [check_python()]
|
|
17
23
|
versions = {}
|
|
18
24
|
for tool in TOOLS:
|
|
@@ -21,7 +27,12 @@ def run_checks(root: Path) -> list[Result]:
|
|
|
21
27
|
results += check_git_identity()
|
|
22
28
|
results += check_project(root, versions["node"], platform.python_version())
|
|
23
29
|
results += check_runtimes(root) + check_health(root)
|
|
24
|
-
|
|
30
|
+
results += check_docker(root) + check_disk(root)
|
|
31
|
+
results += (
|
|
32
|
+
[cfg_error] if cfg_error else check_config(cfg, versions["node"], platform.python_version())
|
|
33
|
+
)
|
|
34
|
+
skip = (cfg or {}).get("skip", [])
|
|
35
|
+
return [r for r in results if not matches(r.check_id, skip)]
|
|
25
36
|
|
|
26
37
|
|
|
27
38
|
def report(results: list[Result]) -> dict:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: devready-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A local-first, read-only CLI that checks a project's environment requirements against what is installed.
|
|
5
5
|
Project-URL: Homepage, https://devready-ten.vercel.app/
|
|
6
6
|
Project-URL: Repository, https://github.com/Evalogical-Inc/devReady
|
|
@@ -114,6 +114,9 @@ devready explain docker.daemon # what a check means and how to fix it
|
|
|
114
114
|
devready check --format junit # or sarif, for CI
|
|
115
115
|
devready check --only project --skip project.ports # filter by id or prefix
|
|
116
116
|
devready fix --plan # suggested steps only; never runs them
|
|
117
|
+
devready check --format json > baseline.json # save a snapshot (commit it for a team baseline)
|
|
118
|
+
devready check --baseline baseline.json # show only what changed; fail only on new/worse failures
|
|
119
|
+
devready init > .devready.json # optional config: skip ids, require node/python, required env names
|
|
117
120
|
devready mcp # run as an MCP server over stdio
|
|
118
121
|
devready --version
|
|
119
122
|
```
|
|
@@ -202,7 +205,8 @@ See [SECURITY.md](SECURITY.md) to report a vulnerability.
|
|
|
202
205
|
|
|
203
206
|
- [x] Docker CLI + daemon reachability, `.tool-versions`, `.env` key-name diff, disk space, git identity, `devready explain`
|
|
204
207
|
- [x] Port-in-use checks, more ecosystems (Go, Rust, Java, Ruby, .NET, pnpm/yarn/bun), JUnit/SARIF output, `fix --plan`, GitHub Action
|
|
205
|
-
- [
|
|
208
|
+
- [x] `.devready.json` config, `--baseline` snapshots
|
|
209
|
+
- [ ] Platform-specific checks (see [ROADMAP.md](ROADMAP.md))
|
|
206
210
|
- [ ] Standalone binaries
|
|
207
211
|
|
|
208
212
|
Have a setup problem DevReady should catch? [Open an issue](https://github.com/Evalogical-Inc/devReady/issues/new/choose). Early feedback shapes the roadmap.
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
devready/__init__.py,sha256=
|
|
1
|
+
devready/__init__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
|
|
2
2
|
devready/__main__.py,sha256=c-4qpm2SKZs1nENGzSnPnChNTo8iMwjY0XaPyAApn4I,60
|
|
3
|
-
devready/
|
|
4
|
-
devready/
|
|
3
|
+
devready/baseline.py,sha256=pfVzZ695bLEiSWKUBK0PNDp1S2UWTWj9lfUCDAsVS4g,1524
|
|
4
|
+
devready/cli.py,sha256=RPijit2MT_ouZgD2TOUu1x-6YJevRIkh_a1wTWbMeuY,4619
|
|
5
|
+
devready/explain.py,sha256=nYzzwtZGpn-FQQR8_KxC61_FLf7jCuQEv4iX-Fvqqr0,5140
|
|
5
6
|
devready/formats.py,sha256=akN62ouKLeZQB6POcXNzINYKHwpUd4im0n2-9rtB8eE,3129
|
|
6
7
|
devready/mcp_server.py,sha256=zVm89kZL_-pZwTSWkt6rxKonytNklUSa0RCQkX0lzFk,3177
|
|
7
8
|
devready/models.py,sha256=KyGp3ie2LVQmuSfzLV8tbEH_hJyU5-Vi11d0JcQPAuE,646
|
|
8
|
-
devready/runner.py,sha256=
|
|
9
|
+
devready/runner.py,sha256=MVQh-dlsXnihWSbvbF4XKuFxgIyoywtL-9I-y0XrPpc,1456
|
|
9
10
|
devready/versions.py,sha256=dXKIl1-qvcfadRNaCURMoftwx7YtadDZ-YfUp-85XZc,2112
|
|
10
11
|
devready/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
devready/checks/config.py,sha256=SGyIot_QXmZtPLIybtxnyJEFL2aV1NemSqX_fXbFI6A,3471
|
|
11
13
|
devready/checks/extras.py,sha256=pDJbFaAwjkLc92tcFagOmj-5c_vMx32_MtYa_DBfCz8,3428
|
|
12
14
|
devready/checks/health.py,sha256=woaaUL2TJ_biRuiStXqJtnvPua_y69bW2quNPlwd7tQ,4235
|
|
13
15
|
devready/checks/project.py,sha256=UnnjuZmG2aXXBAY45wTEP-bj4CWODMv3DPfEi1V_QDE,5983
|
|
14
16
|
devready/checks/python_check.py,sha256=uQCd-N0LEwt9R5gwZm5mlYiPMST1JXVDNQOdZ5xpako,328
|
|
15
17
|
devready/checks/runtimes.py,sha256=OiC5LEhmfkiMseP2PbQS6Dc6TC2JhYNICU6LmdFFvqk,4446
|
|
16
18
|
devready/checks/tools.py,sha256=AiL9TdIREmY7tEkkpt62DELKuJ2lhu3XcA2yGak9QkA,1616
|
|
17
|
-
devready_cli-0.
|
|
18
|
-
devready_cli-0.
|
|
19
|
-
devready_cli-0.
|
|
20
|
-
devready_cli-0.
|
|
21
|
-
devready_cli-0.
|
|
19
|
+
devready_cli-0.2.0.dist-info/METADATA,sha256=b76wbK0qZnoG8aNy66wxg8QmFDPENvL3GhzAwa2-RMQ,10533
|
|
20
|
+
devready_cli-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
21
|
+
devready_cli-0.2.0.dist-info/entry_points.txt,sha256=7oAecSWtsIt7qLOP8PipOeDqhXCkEJat0oxhD4T1RpY,47
|
|
22
|
+
devready_cli-0.2.0.dist-info/licenses/LICENSE,sha256=S_AQsEw289FIsOgT-OPWL5JZeV8nn5kicwEq0r9qu7s,1071
|
|
23
|
+
devready_cli-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|