devready-cli 0.0.4__py3-none-any.whl → 0.1.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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.0.4"
1
+ __version__ = "0.1.0"
@@ -0,0 +1,116 @@
1
+ """Project health: .env ignored by git, node_modules vs lockfile, ports the project declares.
2
+
3
+ Read-only. Only numbers and fixed phrases reach the report, never file content.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import re
10
+ import shutil
11
+ import socket
12
+ from collections.abc import Callable
13
+ from pathlib import Path
14
+
15
+ from devready.checks.extras import DOCKER_FILES, _run
16
+ from devready.checks.project import _read
17
+ from devready.models import Result, Status
18
+
19
+ LOCKFILES = ("package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "bun.lock")
20
+ _COMPOSE_PORT = re.compile(r"^\s*-\s*[\"']?(?:[\d.]+:)?(\d{2,5}):\d+", re.M)
21
+ _ENV_PORT = re.compile(r"^\s*(?:export\s+)?[A-Z_]*PORT\s*=\s*[\"']?(\d{2,5})\b", re.M)
22
+ MAX_PORTS = 20
23
+
24
+
25
+ def _listening(port: int) -> bool:
26
+ with socket.socket() as s:
27
+ s.settimeout(0.3)
28
+ return s.connect_ex(("127.0.0.1", port)) == 0
29
+
30
+
31
+ def check_env_ignored(root: Path, run: Callable = _run) -> list[Result]:
32
+ """A .env that git would commit is a leak risk. Silent outside a git repo."""
33
+ git = shutil.which("git")
34
+ if not (root / ".env").is_file() or not git:
35
+ return []
36
+ p = run([git, "-C", str(root), "check-ignore", "-q", ".env"])
37
+ if p is None or p.returncode not in (0, 1):
38
+ return []
39
+ if p.returncode == 0:
40
+ return [Result("project.env.ignored", "Env ignored", Status.PASS, ".env is git-ignored")]
41
+ return [
42
+ Result(
43
+ "project.env.ignored",
44
+ "Env ignored",
45
+ Status.WARN,
46
+ ".env is not ignored by git (or is already tracked)",
47
+ "Add .env to .gitignore; if it is tracked, untrack it and rotate its secrets.",
48
+ )
49
+ ]
50
+
51
+
52
+ def check_node_modules(root: Path) -> list[Result]:
53
+ text = _read(root / "package.json")
54
+ try:
55
+ data = json.loads(text) if text else None
56
+ except ValueError:
57
+ return [] # project.py already reports the parse error
58
+ if not isinstance(data, dict) or not any(
59
+ isinstance(data.get(k), dict) and data[k] for k in ("dependencies", "devDependencies")
60
+ ):
61
+ return []
62
+ cid, title = "project.node.modules", "node_modules"
63
+ nm = root / "node_modules"
64
+ if not nm.is_dir():
65
+ return [
66
+ Result(
67
+ cid,
68
+ title,
69
+ Status.WARN,
70
+ "node_modules not found",
71
+ "Install dependencies with your package manager (npm, pnpm, yarn or bun).",
72
+ )
73
+ ]
74
+ # ponytail: directory mtime, not a content comparison; upgrade if it proves noisy.
75
+ locks = [root / f for f in LOCKFILES if (root / f).is_file()]
76
+ if any(lock.stat().st_mtime > nm.stat().st_mtime for lock in locks):
77
+ return [
78
+ Result(
79
+ cid,
80
+ title,
81
+ Status.WARN,
82
+ "a lockfile is newer than node_modules",
83
+ "Re-run your package manager's install to sync dependencies.",
84
+ )
85
+ ]
86
+ return [Result(cid, title, Status.PASS, "node_modules is present and not older than lockfiles")]
87
+
88
+
89
+ def check_ports(root: Path, in_use: Callable[[int], bool] = _listening) -> list[Result]:
90
+ ports: set[int] = set()
91
+ for name in DOCKER_FILES[1:]: # compose files only
92
+ if (text := _read(root / name)) is not None:
93
+ ports.update(int(p) for p in _COMPOSE_PORT.findall(text))
94
+ if (text := _read(root / ".env.example")) is not None:
95
+ ports.update(int(p) for p in _ENV_PORT.findall(text))
96
+ ports = {p for p in ports if 0 < p < 65536}
97
+ if not ports:
98
+ return []
99
+ cid, title = "project.ports.free", "Ports"
100
+ shown = sorted(ports)[:MAX_PORTS]
101
+ busy = [p for p in shown if in_use(p)]
102
+ if busy:
103
+ return [
104
+ Result(
105
+ cid,
106
+ title,
107
+ Status.WARN,
108
+ f"already in use on localhost: {', '.join(map(str, busy))}",
109
+ "Stop the other process or change the port; it may be this project running.",
110
+ )
111
+ ]
112
+ return [Result(cid, title, Status.PASS, f"{len(shown)} declared port(s) are free")]
113
+
114
+
115
+ def check_health(root: Path) -> list[Result]:
116
+ return check_env_ignored(root) + check_node_modules(root) + check_ports(root)
@@ -0,0 +1,103 @@
1
+ """Go, Ruby, Java, Rust, .NET, pnpm, Yarn, Bun: checked only when the project pins them.
2
+
3
+ Same read-only rules as project.py. Only digits/validated specs ever reach the report.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import re
10
+ from collections.abc import Callable
11
+ from pathlib import Path
12
+
13
+ from devready.checks.project import _compare, _read
14
+ from devready.checks.tools import check_tool
15
+ from devready.models import Result, Status
16
+ from devready.versions import parse_version
17
+
18
+
19
+ def _first_version(text: str) -> str | None:
20
+ m = re.search(r"\d+(?:\.\d+){0,2}", text.strip().split("\n", 1)[0])
21
+ return m.group() if m else None
22
+
23
+
24
+ def _go_mod(text: str) -> str | None:
25
+ m = re.search(r"^go\s+(\d+(?:\.\d+){1,2})", text, re.M)
26
+ return f">={m.group(1)}" if m else None
27
+
28
+
29
+ def _rust(text: str) -> str | None:
30
+ m = re.search(r"^\s*channel\s*=\s*[\"']([^\"']*)[\"']", text, re.M)
31
+ return m.group(1) if m else text.strip().split("\n", 1)[0] or None
32
+
33
+
34
+ def _global_json(text: str) -> str | None:
35
+ try:
36
+ spec = json.loads(text).get("sdk", {}).get("version")
37
+ except (ValueError, AttributeError):
38
+ return None
39
+ return spec if isinstance(spec, str) else None
40
+
41
+
42
+ def _pkg_manager(name: str) -> Callable[[str], str | None]:
43
+ def extract(text: str) -> str | None:
44
+ try:
45
+ value = json.loads(text).get("packageManager")
46
+ except (ValueError, AttributeError):
47
+ return None
48
+ m = (
49
+ re.fullmatch(rf"{name}@(\d+(?:\.\d+){{0,2}})(?:\+.*)?", value)
50
+ if isinstance(value, str)
51
+ else None
52
+ )
53
+ return m.group(1) if m else None
54
+
55
+ return extract
56
+
57
+
58
+ # key -> (title, exe, version args, docs, .tool-versions name, [(id, file, extractor, miss)])
59
+ RUNTIMES: dict[str, tuple] = {
60
+ "go": ("Go", "go", ("version",), "https://go.dev/doc/install", "golang",
61
+ [("project.go.mod", "go.mod", _go_mod, Status.FAIL)]),
62
+ "ruby": ("Ruby", "ruby", ("--version",), "https://www.ruby-lang.org/en/downloads/", "ruby",
63
+ [("project.ruby.pin", ".ruby-version", _first_version, Status.WARN)]),
64
+ "java": ("Java", "java", ("-version",), "https://adoptium.net/", "java",
65
+ [("project.java.pin", ".java-version", _first_version, Status.WARN)]),
66
+ "rust": ("Rust", "rustc", ("--version",), "https://rustup.rs/", "rust",
67
+ [("project.rust.toolchain", "rust-toolchain.toml", _rust, Status.WARN),
68
+ ("project.rust.toolchain", "rust-toolchain", _rust, Status.WARN)]),
69
+ "dotnet": ("dotnet", "dotnet", ("--version",), "https://dotnet.microsoft.com/download",
70
+ "dotnet", [("project.dotnet.global", "global.json", _global_json, Status.WARN)]),
71
+ # packageManager in package.json pins one manager, e.g. "pnpm@9.1.0"
72
+ "pnpm": ("pnpm", "pnpm", ("--version",), "https://pnpm.io/installation", "pnpm",
73
+ [("project.pnpm.pin", "package.json", _pkg_manager("pnpm"), Status.WARN)]),
74
+ "yarn": ("Yarn", "yarn", ("--version",), "https://yarnpkg.com/getting-started/install", "yarn",
75
+ [("project.yarn.pin", "package.json", _pkg_manager("yarn"), Status.WARN)]),
76
+ "bun": ("Bun", "bun", ("--version",), "https://bun.sh/docs/installation", "bun",
77
+ [("project.bun.pin", "package.json", _pkg_manager("bun"), Status.WARN)]),
78
+ } # fmt: skip
79
+
80
+
81
+ def check_runtimes(
82
+ root: Path, tool: Callable = check_tool, runtimes: dict[str, tuple] = RUNTIMES
83
+ ) -> list[Result]:
84
+ tv = _read(root / ".tool-versions") or ""
85
+ tv_pins = {p[0]: p[1] for line in tv.splitlines() if len(p := line.split("#")[0].split()) >= 2}
86
+ out: list[Result] = []
87
+ for key, (title, exe, args, docs, tv_name, pins) in runtimes.items():
88
+ found: list[tuple[str, str, str, Status]] = []
89
+ for cid, fname, extract, miss in pins:
90
+ text = _read(root / fname)
91
+ if text is not None and (spec := extract(text)):
92
+ found.append((cid, fname, spec, miss))
93
+ break
94
+ if tv_name in tv_pins:
95
+ found.append((f"project.{key}.tool_versions", ".tool-versions", tv_pins[tv_name],
96
+ Status.WARN)) # fmt: skip
97
+ if not found:
98
+ continue
99
+ res, ver = tool(f"{key}.version", title, exe, docs, args)
100
+ out.append(res)
101
+ v = parse_version(ver) if ver else None
102
+ out += [_compare(c, f"{title} pin", src, spec, v, miss) for c, src, spec, miss in found]
103
+ return out
devready/checks/tools.py CHANGED
@@ -19,7 +19,9 @@ TOOLS = [
19
19
  ]
20
20
 
21
21
 
22
- def check_tool(check_id: str, title: str, exe: str, docs: str) -> tuple[Result, str | None]:
22
+ def check_tool(
23
+ check_id: str, title: str, exe: str, docs: str, args: tuple[str, ...] = ("--version",)
24
+ ) -> tuple[Result, str | None]:
23
25
  """Returns (result, parsed version or None). Raw command output is never reported."""
24
26
  path = shutil.which(exe)
25
27
  if not path:
@@ -33,11 +35,12 @@ def check_tool(check_id: str, title: str, exe: str, docs: str) -> tuple[Result,
33
35
  ), None
34
36
  try:
35
37
  proc = subprocess.run(
36
- [path, "--version"], capture_output=True, text=True, timeout=5, errors="replace"
38
+ [path, *args], capture_output=True, text=True, timeout=5, errors="replace"
37
39
  )
38
40
  except (subprocess.TimeoutExpired, OSError):
39
41
  return Result(check_id, title, Status.ERROR, "Could not run version command"), None
40
- m = re.search(r"\d+(?:\.\d+)+", proc.stdout[:200]) if proc.returncode == 0 else None
42
+ out = (proc.stdout or proc.stderr)[:200] # java -version prints to stderr
43
+ m = re.search(r"\d+(?:\.\d+)+", out) if proc.returncode == 0 else None
41
44
  if not m:
42
45
  return Result(check_id, title, Status.ERROR, "Version output not recognized"), None
43
46
  return Result(check_id, title, Status.PASS, f"{m.group()} found"), m.group()
devready/cli.py CHANGED
@@ -6,6 +6,7 @@ import sys
6
6
  from pathlib import Path
7
7
 
8
8
  from devready import __version__
9
+ from devready.formats import fix_plan, junit, sarif
9
10
  from devready.models import Result, Status
10
11
  from devready.runner import report, run_checks
11
12
 
@@ -19,16 +20,29 @@ def _text(results: list[Result]) -> str:
19
20
  return "\n".join(lines)
20
21
 
21
22
 
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
+ def _selected(check_id: str, only: list[str], skip: list[str]) -> bool:
28
+ return (not only or _matches(check_id, only)) and not _matches(check_id, skip)
29
+
30
+
22
31
  def main(argv: list[str] | None = None) -> int:
23
32
  parser = argparse.ArgumentParser(prog="devready", description=__doc__)
24
33
  parser.add_argument("--version", action="version", version=f"devready {__version__}")
25
34
  sub = parser.add_subparsers(dest="command", required=True)
26
35
  check = sub.add_parser("check", help="run read-only environment checks")
27
- check.add_argument("--format", choices=["text", "json"], default="text")
36
+ check.add_argument("--format", choices=["text", "json", "junit", "sarif"], default="text")
37
+ fix = sub.add_parser("fix", help="print suggested fixes (never runs them)")
38
+ fix.add_argument("--plan", action="store_true", help="required: list steps without running")
28
39
  explain = sub.add_parser("explain", help="explain what a check id means")
29
40
  explain.add_argument("check_id")
30
41
  sub.add_parser("mcp", help="run as an MCP server over stdio")
31
- check.add_argument("--project", default=".", help="directory whose version pins to compare")
42
+ for p in (check, fix):
43
+ p.add_argument("--project", default=".", help="directory whose version pins to compare")
44
+ p.add_argument("--only", action="append", default=[], metavar="ID", help="check id/prefix")
45
+ p.add_argument("--skip", action="append", default=[], metavar="ID", help="check id/prefix")
32
46
 
33
47
  try:
34
48
  args = parser.parse_args(argv)
@@ -50,6 +64,13 @@ def main(argv: list[str] | None = None) -> int:
50
64
  print(text)
51
65
  return EXIT_OK
52
66
 
67
+ if args.command == "fix" and not args.plan:
68
+ print(
69
+ "devready: fix only supports --plan (DevReady never changes your system)",
70
+ file=sys.stderr,
71
+ )
72
+ return EXIT_USAGE
73
+
53
74
  root = Path(args.project)
54
75
  if not root.is_dir():
55
76
  print(f"devready: not a directory: {args.project}", file=sys.stderr)
@@ -60,8 +81,15 @@ def main(argv: list[str] | None = None) -> int:
60
81
  print(f"devready: internal error ({type(e).__name__})", file=sys.stderr)
61
82
  return EXIT_INTERNAL
62
83
 
84
+ results = [r for r in results if _selected(r.check_id, args.only, args.skip)]
85
+
86
+ if args.command == "fix":
87
+ print(fix_plan(results))
88
+ return EXIT_OK
63
89
  if args.format == "json":
64
90
  print(json.dumps(report(results), indent=2))
91
+ elif args.format in ("junit", "sarif"):
92
+ print(junit(results) if args.format == "junit" else sarif(results))
65
93
  else:
66
94
  print(_text(results))
67
95
  return EXIT_FAILED if any(r.status is Status.FAIL for r in results) else EXIT_OK
devready/explain.py CHANGED
@@ -16,4 +16,31 @@ EXPLANATIONS = {
16
16
  "docker.daemon": "The Docker CLI can exist while the daemon is stopped. Start Docker Desktop or the service.",
17
17
  "git.identity": "Commits need user.name and user.email. DevReady checks they exist, never what they are.",
18
18
  "disk.space": "Warns below 1 GB free where the project lives; installs and builds can fail when the disk is full.",
19
+ "go.version": "Go toolchain; checked only when the project has a go.mod or a golang line in .tool-versions.",
20
+ "ruby.version": "Ruby interpreter; checked only when the project pins it (.ruby-version or .tool-versions).",
21
+ "java.version": "Java runtime; checked only when the project pins it (.java-version or .tool-versions).",
22
+ "rust.version": "Rust compiler (rustc); checked only when the project has a rust-toolchain file or pin.",
23
+ "dotnet.version": "The .NET SDK; checked only when the project has a global.json or a dotnet pin.",
24
+ "project.go.mod": "The go directive in go.mod is the minimum Go version. A lower toolchain fails the check.",
25
+ "project.go.tool_versions": ".tool-versions (asdf/mise) pins Go for this project.",
26
+ "project.ruby.pin": ".ruby-version pins Ruby for this project (rbenv, rvm, mise).",
27
+ "project.ruby.tool_versions": ".tool-versions (asdf/mise) pins Ruby for this project.",
28
+ "project.java.pin": ".java-version pins Java for this project (jenv, mise).",
29
+ "project.java.tool_versions": ".tool-versions (asdf/mise) pins Java for this project.",
30
+ "project.rust.toolchain": "rust-toolchain(.toml) pins the Rust channel. Named channels such as stable are reported as not evaluated.",
31
+ "project.rust.tool_versions": ".tool-versions (asdf/mise) pins Rust for this project.",
32
+ "project.dotnet.global": "global.json sdk.version pins the .NET SDK for this project.",
33
+ "project.dotnet.tool_versions": ".tool-versions (asdf/mise) pins .NET for this project.",
34
+ "pnpm.version": "pnpm package manager; checked only when package.json packageManager names pnpm.",
35
+ "yarn.version": "Yarn package manager; checked only when package.json packageManager names yarn.",
36
+ "bun.version": "Bun runtime and package manager; checked only when package.json packageManager names bun.",
37
+ "project.pnpm.pin": "package.json packageManager pins the pnpm version (corepack enable can install it).",
38
+ "project.yarn.pin": "package.json packageManager pins the Yarn version (corepack enable can install it).",
39
+ "project.bun.pin": "package.json packageManager pins the Bun version.",
40
+ "project.pnpm.tool_versions": ".tool-versions (asdf/mise) pins pnpm for this project.",
41
+ "project.yarn.tool_versions": ".tool-versions (asdf/mise) pins Yarn for this project.",
42
+ "project.bun.tool_versions": ".tool-versions (asdf/mise) pins Bun for this project.",
43
+ "project.env.ignored": "Warns when .env exists but git would commit it (not ignored, or already tracked). Values are never shown.",
44
+ "project.node.modules": "Checked when package.json lists dependencies: node_modules must exist and not be older than a lockfile.",
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.",
19
46
  }
devready/formats.py ADDED
@@ -0,0 +1,82 @@
1
+ """Extra output formats for CI: JUnit XML, SARIF 2.1.0, and the read-only fix plan."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import xml.etree.ElementTree as ET
7
+
8
+ from devready import __version__
9
+ from devready.models import Result, Status
10
+
11
+ _LEVEL = {Status.FAIL: "error", Status.ERROR: "error", Status.WARN: "warning"}
12
+
13
+
14
+ def junit(results: list[Result]) -> str:
15
+ count = lambda *s: str(sum(r.status in s for r in results)) # noqa: E731
16
+ suite = ET.Element(
17
+ "testsuite",
18
+ name="devready",
19
+ tests=str(len(results)),
20
+ failures=count(Status.FAIL),
21
+ errors=count(Status.ERROR),
22
+ skipped=count(Status.SKIP),
23
+ )
24
+ for r in results:
25
+ case = ET.SubElement(suite, "testcase", classname="devready", name=r.check_id)
26
+ if r.status is Status.FAIL:
27
+ ET.SubElement(case, "failure", message=r.summary)
28
+ elif r.status is Status.ERROR:
29
+ ET.SubElement(case, "error", message=r.summary)
30
+ elif r.status is Status.SKIP:
31
+ ET.SubElement(case, "skipped", message=r.summary)
32
+ elif r.status is Status.WARN: # warnings do not fail the suite; keep them visible
33
+ ET.SubElement(case, "system-out").text = f"WARN: {r.summary}"
34
+ return ET.tostring(suite, encoding="unicode", xml_declaration=False)
35
+
36
+
37
+ def sarif(results: list[Result]) -> str:
38
+ # ponytail: findings carry no file location, so GitHub code scanning may not display them;
39
+ # add locations once checks know which file they read.
40
+ bad = [r for r in results if r.status in _LEVEL]
41
+ doc = {
42
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
43
+ "version": "2.1.0",
44
+ "runs": [
45
+ {
46
+ "tool": {
47
+ "driver": {
48
+ "name": "devready",
49
+ "version": __version__,
50
+ "rules": [
51
+ {"id": i, "shortDescription": {"text": i}}
52
+ for i in sorted({r.check_id for r in bad})
53
+ ],
54
+ }
55
+ },
56
+ "results": [
57
+ {
58
+ "ruleId": r.check_id,
59
+ "level": _LEVEL[r.status],
60
+ "message": {"text": f"{r.summary}. {r.suggestion}".strip()},
61
+ }
62
+ for r in bad
63
+ ],
64
+ }
65
+ ],
66
+ }
67
+ return json.dumps(doc, indent=2)
68
+
69
+
70
+ def fix_plan(results: list[Result]) -> str:
71
+ """Suggested next steps only. DevReady never runs any of them."""
72
+ todo = [r for r in results if r.status in (Status.FAIL, Status.WARN) and r.suggestion]
73
+ lines = ["DevReady - Fix Plan (suggestions only)", ""]
74
+ if not todo:
75
+ lines.append("Nothing to fix.")
76
+ for i, r in enumerate(todo, 1):
77
+ lines.append(f"{i}. [{r.status.value.upper()}] {r.title}: {r.summary}")
78
+ lines.append(f" -> {r.suggestion}")
79
+ if r.docs_url:
80
+ lines.append(f" {r.docs_url}")
81
+ lines += ["", "DevReady did not run any of these. Nothing was changed."]
82
+ return "\n".join(lines)
devready/runner.py CHANGED
@@ -4,8 +4,10 @@ import platform
4
4
  from pathlib import Path
5
5
 
6
6
  from devready.checks.extras import check_disk, check_docker, check_git_identity
7
+ from devready.checks.health import check_health
7
8
  from devready.checks.project import check_project
8
9
  from devready.checks.python_check import check_python
10
+ from devready.checks.runtimes import check_runtimes
9
11
  from devready.checks.tools import TOOLS, check_tool
10
12
  from devready.models import Result
11
13
 
@@ -18,6 +20,7 @@ def run_checks(root: Path) -> list[Result]:
18
20
  results.append(result)
19
21
  results += check_git_identity()
20
22
  results += check_project(root, versions["node"], platform.python_version())
23
+ results += check_runtimes(root) + check_health(root)
21
24
  return results + check_docker(root) + check_disk(root)
22
25
 
23
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: devready-cli
3
- Version: 0.0.4
3
+ Version: 0.1.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
@@ -111,6 +111,9 @@ devready check # human-readable report for the current direct
111
111
  devready check --project ../app # inspect another project's pins
112
112
  devready check --format json # machine-readable (see docs/schema.json)
113
113
  devready explain docker.daemon # what a check means and how to fix it
114
+ devready check --format junit # or sarif, for CI
115
+ devready check --only project --skip project.ports # filter by id or prefix
116
+ devready fix --plan # suggested steps only; never runs them
114
117
  devready mcp # run as an MCP server over stdio
115
118
  devready --version
116
119
  ```
@@ -148,6 +151,16 @@ devready --version
148
151
  }
149
152
  ```
150
153
 
154
+ ## Use it in CI (GitHub Actions)
155
+
156
+ ```yaml
157
+ - uses: Evalogical-Inc/devReady@main # pin a release tag once published
158
+ with:
159
+ skip: project.ports # optional; also: only, format, project, version
160
+ ```
161
+
162
+ The job fails when a declared requirement is unmet (`fail`). It installs `devready-cli` from PyPI, so flags added after your installed release (`--only`, `--skip`, junit/sarif) need a release that includes them; set `version:` to pin one.
163
+
151
164
  ## Use it with AI coding agents
152
165
 
153
166
  DevReady ships the same engine three ways, so your agent can verify the environment *before* it installs, builds or tests.
@@ -181,15 +194,15 @@ See [SECURITY.md](SECURITY.md) to report a vulnerability.
181
194
 
182
195
  ## Limitations (alpha)
183
196
 
184
- - Checks today: Python, Git (+ identity set), Node.js, npm, project pins (`.nvmrc`, `.node-version`, `.python-version`, `.tool-versions`, `package.json` engines, `pyproject.toml` requires-python), `.env.example` vs `.env` key names, Docker CLI + daemon (only when the project has a Dockerfile/compose file), and free disk space.
197
+ - Checks today: Python, Git (+ identity set), Node.js, npm, project pins (`.nvmrc`, `.node-version`, `.python-version`, `.tool-versions`, `package.json` engines, `pyproject.toml` requires-python), `.env.example` vs `.env` key names, Docker CLI + daemon (only when the project has a Dockerfile/compose file), free disk space, and (only when the project declares them) Go, Ruby, Java, Rust, .NET, pnpm, Yarn and Bun pins, `.env` not git-ignored, `node_modules` vs lockfile, and declared ports already in use.
185
198
  - The version-range matcher covers common npm/PEP 440 syntax; pre-releases and exotic ranges report `skip` rather than guess.
186
199
  - The Python version compared is the interpreter running DevReady, not necessarily your project's virtualenv.
187
200
 
188
201
  ## Roadmap
189
202
 
190
203
  - [x] Docker CLI + daemon reachability, `.tool-versions`, `.env` key-name diff, disk space, git identity, `devready explain`
191
- - [ ] Port-in-use checks (ports read from compose files)
192
- - [ ] More ecosystems (Go, Rust, Java, Ruby pins)
204
+ - [x] Port-in-use checks, more ecosystems (Go, Rust, Java, Ruby, .NET, pnpm/yarn/bun), JUnit/SARIF output, `fix --plan`, GitHub Action
205
+ - [ ] Config file, shared baseline (see [ROADMAP.md](ROADMAP.md))
193
206
  - [ ] Standalone binaries
194
207
 
195
208
  Have a setup problem DevReady should catch? [Open an issue](https://github.com/Evalogical-Inc/devReady/issues/new/choose). Early feedback shapes the roadmap.
@@ -202,6 +215,10 @@ PRs welcome, especially new read-only checks and platform fixes. Start with [CON
202
215
  ruff check . && pytest
203
216
  ```
204
217
 
218
+ ## About Evalogical
219
+
220
+ DevReady is built and maintained by [Evalogical](https://evalogical.com/), a software company offering AI-enabled product engineering, custom software and IT services. Need help with a product or an engineering team? [Talk to us](https://evalogical.com/contactus).
221
+
205
222
  ## License
206
223
 
207
224
  [MIT](LICENSE) © Evalogical Inc
@@ -0,0 +1,21 @@
1
+ devready/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ devready/__main__.py,sha256=c-4qpm2SKZs1nENGzSnPnChNTo8iMwjY0XaPyAApn4I,60
3
+ devready/cli.py,sha256=OucYELHjvcFY2qw96Kc6yi4lp1RX9GnSif3MlelJ3ks,3787
4
+ devready/explain.py,sha256=UbYKUdfdVsITUtuFIvG4vJ21SP8GI-HNPKh9xp6LCRE,4641
5
+ devready/formats.py,sha256=akN62ouKLeZQB6POcXNzINYKHwpUd4im0n2-9rtB8eE,3129
6
+ devready/mcp_server.py,sha256=zVm89kZL_-pZwTSWkt6rxKonytNklUSa0RCQkX0lzFk,3177
7
+ devready/models.py,sha256=KyGp3ie2LVQmuSfzLV8tbEH_hJyU5-Vi11d0JcQPAuE,646
8
+ devready/runner.py,sha256=3OLWash5jGZKQM30iA1ObAzF2FTmM7STsiDD3LMO9Qs,1005
9
+ devready/versions.py,sha256=dXKIl1-qvcfadRNaCURMoftwx7YtadDZ-YfUp-85XZc,2112
10
+ devready/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ devready/checks/extras.py,sha256=pDJbFaAwjkLc92tcFagOmj-5c_vMx32_MtYa_DBfCz8,3428
12
+ devready/checks/health.py,sha256=woaaUL2TJ_biRuiStXqJtnvPua_y69bW2quNPlwd7tQ,4235
13
+ devready/checks/project.py,sha256=UnnjuZmG2aXXBAY45wTEP-bj4CWODMv3DPfEi1V_QDE,5983
14
+ devready/checks/python_check.py,sha256=uQCd-N0LEwt9R5gwZm5mlYiPMST1JXVDNQOdZ5xpako,328
15
+ devready/checks/runtimes.py,sha256=OiC5LEhmfkiMseP2PbQS6Dc6TC2JhYNICU6LmdFFvqk,4446
16
+ devready/checks/tools.py,sha256=AiL9TdIREmY7tEkkpt62DELKuJ2lhu3XcA2yGak9QkA,1616
17
+ devready_cli-0.1.0.dist-info/METADATA,sha256=jlbjbIv1omn0U0G-ZlgrrHdDMP-O33GC4tMmAJC8pdE,10176
18
+ devready_cli-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
19
+ devready_cli-0.1.0.dist-info/entry_points.txt,sha256=7oAecSWtsIt7qLOP8PipOeDqhXCkEJat0oxhD4T1RpY,47
20
+ devready_cli-0.1.0.dist-info/licenses/LICENSE,sha256=S_AQsEw289FIsOgT-OPWL5JZeV8nn5kicwEq0r9qu7s,1071
21
+ devready_cli-0.1.0.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- devready/__init__.py,sha256=1mptEzQihbdyqqzMgdns_j5ZGK9gz7hR2bsgA_TnjO4,22
2
- devready/__main__.py,sha256=c-4qpm2SKZs1nENGzSnPnChNTo8iMwjY0XaPyAApn4I,60
3
- devready/cli.py,sha256=yStxOgFcYqVOVmcbMZGwU83zaZZemecJKGsg0KbVUEE,2521
4
- devready/explain.py,sha256=PaAt0K0-tdQB5pS9Y2vxZEKChwra_SdY7-qAT0juKto,1765
5
- devready/mcp_server.py,sha256=zVm89kZL_-pZwTSWkt6rxKonytNklUSa0RCQkX0lzFk,3177
6
- devready/models.py,sha256=KyGp3ie2LVQmuSfzLV8tbEH_hJyU5-Vi11d0JcQPAuE,646
7
- devready/runner.py,sha256=5WML6vsoVer0FUpgCderZuqip6ldhWtWGZ-ufVxQV7U,848
8
- devready/versions.py,sha256=dXKIl1-qvcfadRNaCURMoftwx7YtadDZ-YfUp-85XZc,2112
9
- devready/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- devready/checks/extras.py,sha256=pDJbFaAwjkLc92tcFagOmj-5c_vMx32_MtYa_DBfCz8,3428
11
- devready/checks/project.py,sha256=UnnjuZmG2aXXBAY45wTEP-bj4CWODMv3DPfEi1V_QDE,5983
12
- devready/checks/python_check.py,sha256=uQCd-N0LEwt9R5gwZm5mlYiPMST1JXVDNQOdZ5xpako,328
13
- devready/checks/tools.py,sha256=bjO61jFefp0b6iBHg-a79uaz8nZGCldakfHyB1G8Gf4,1511
14
- devready_cli-0.0.4.dist-info/METADATA,sha256=UPBgdGDMBm_Uvwzb2vd-ZAhs3EL-rzn7qCCwHgYvHj0,8953
15
- devready_cli-0.0.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
16
- devready_cli-0.0.4.dist-info/entry_points.txt,sha256=7oAecSWtsIt7qLOP8PipOeDqhXCkEJat0oxhD4T1RpY,47
17
- devready_cli-0.0.4.dist-info/licenses/LICENSE,sha256=S_AQsEw289FIsOgT-OPWL5JZeV8nn5kicwEq0r9qu7s,1071
18
- devready_cli-0.0.4.dist-info/RECORD,,