devready-cli 0.0.1__py3-none-any.whl → 0.0.3__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.1"
1
+ __version__ = "0.0.3"
@@ -0,0 +1,104 @@
1
+ """Docker (only when the project uses it), git identity presence, and free disk space."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ from devready.models import Result, Status
11
+
12
+ DOCKER_FILES = (
13
+ "Dockerfile",
14
+ "docker-compose.yml",
15
+ "docker-compose.yaml",
16
+ "compose.yml",
17
+ "compose.yaml",
18
+ )
19
+ DOCKER_DOCS = "https://docs.docker.com/get-started/get-docker/"
20
+ MIN_FREE_GB = 1
21
+
22
+
23
+ def _run(argv: list[str]) -> subprocess.CompletedProcess | None:
24
+ try:
25
+ return subprocess.run(argv, capture_output=True, text=True, timeout=10, errors="replace")
26
+ except (subprocess.TimeoutExpired, OSError):
27
+ return None
28
+
29
+
30
+ def check_docker(root: Path) -> list[Result]:
31
+ """CLI vs daemon are separate results. Skipped silently unless the project uses Docker."""
32
+ if not any((root / f).is_file() and not (root / f).is_symlink() for f in DOCKER_FILES):
33
+ return []
34
+ exe = shutil.which("docker")
35
+ if not exe:
36
+ return [
37
+ Result(
38
+ "docker.cli",
39
+ "Docker",
40
+ Status.WARN,
41
+ "Project uses Docker; CLI not found on PATH",
42
+ "Install Docker if you need to build or run this project's containers.",
43
+ DOCKER_DOCS,
44
+ )
45
+ ]
46
+ p = _run([exe, "version", "--format", "{{.Server.Version}}"])
47
+ if p is None:
48
+ return [Result("docker.daemon", "Docker daemon", Status.ERROR, "Could not query Docker")]
49
+ m = re.fullmatch(r"\d+(?:\.\d+)+", p.stdout.strip()) if p.returncode == 0 else None
50
+ if not m:
51
+ return [
52
+ Result(
53
+ "docker.daemon",
54
+ "Docker daemon",
55
+ Status.WARN,
56
+ "Docker CLI found but the daemon is not reachable",
57
+ "Start Docker Desktop (or the docker service) and re-run.",
58
+ DOCKER_DOCS,
59
+ )
60
+ ]
61
+ return [
62
+ Result("docker.daemon", "Docker daemon", Status.PASS, f"reachable (server {m.group()})")
63
+ ]
64
+
65
+
66
+ def check_git_identity() -> list[Result]:
67
+ """Presence only: the configured name/email are never captured into output."""
68
+ exe = shutil.which("git")
69
+ if not exe:
70
+ return []
71
+ missing = []
72
+ for key in ("user.name", "user.email"):
73
+ p = _run([exe, "config", "--get", key])
74
+ if p is None or p.returncode != 0 or not p.stdout.strip():
75
+ missing.append(key)
76
+ if not missing:
77
+ return [Result("git.identity", "Git identity", Status.PASS, "user.name and user.email set")]
78
+ return [
79
+ Result(
80
+ "git.identity",
81
+ "Git identity",
82
+ Status.WARN,
83
+ f"not set: {', '.join(missing)}",
84
+ 'Run: git config --global user.name "Your Name" (and user.email).',
85
+ )
86
+ ]
87
+
88
+
89
+ def check_disk(root: Path) -> list[Result]:
90
+ try:
91
+ free_gb = shutil.disk_usage(root).free / 1024**3
92
+ except OSError:
93
+ return [Result("disk.space", "Disk space", Status.ERROR, "Could not read disk usage")]
94
+ if free_gb < MIN_FREE_GB:
95
+ return [
96
+ Result(
97
+ "disk.space",
98
+ "Disk space",
99
+ Status.WARN,
100
+ f"only {free_gb:.1f} GB free",
101
+ "Free some space before installing dependencies.",
102
+ )
103
+ ]
104
+ return [Result("disk.space", "Disk space", Status.PASS, f"{free_gb:.0f} GB free")]
@@ -113,4 +113,60 @@ def check_project(root: Path, node: str | None, python: str | None) -> list[Resu
113
113
  Status.FAIL,
114
114
  )
115
115
  )
116
- return out
116
+
117
+ if (text := _read(root / ".tool-versions")) is not None:
118
+ want = {"nodejs": "node", "node": "node", "python": "python"}
119
+ for line in text.splitlines():
120
+ parts = line.split("#")[0].split()
121
+ if len(parts) >= 2 and parts[0] in want:
122
+ is_node = want[parts[0]] == "node"
123
+ out.append(
124
+ _compare(
125
+ f"project.{want[parts[0]]}.tool_versions",
126
+ "Node tool-versions" if is_node else "Python tool-versions",
127
+ ".tool-versions",
128
+ parts[1],
129
+ node_v if is_node else py_v,
130
+ Status.WARN,
131
+ )
132
+ )
133
+
134
+ return out + _check_env(root)
135
+
136
+
137
+ _ENV_KEY = re.compile(r"^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=", re.M)
138
+ _SAFE_NAME = re.compile(r"^[A-Z0-9_]{1,40}$")
139
+
140
+
141
+ def _check_env(root: Path) -> list[Result]:
142
+ """Compare variable NAMES in .env.example with .env. Values are never kept or shown."""
143
+ example = _read(root / ".env.example")
144
+ if example is None:
145
+ return []
146
+ cid, title = "project.env.keys", "Env keys"
147
+ wanted = set(_ENV_KEY.findall(example))
148
+ env_text = _read(root / ".env")
149
+ if env_text is None:
150
+ return [
151
+ Result(
152
+ cid,
153
+ title,
154
+ Status.WARN,
155
+ f".env not found ({len(wanted)} keys in .env.example)",
156
+ "Copy .env.example to .env and fill in the values.",
157
+ )
158
+ ]
159
+ missing = sorted(wanted - set(_ENV_KEY.findall(env_text)))
160
+ if not missing:
161
+ return [Result(cid, title, Status.PASS, f"all {len(wanted)} .env.example keys present")]
162
+ shown = [m for m in missing if _SAFE_NAME.match(m)][:10]
163
+ extra = f": {', '.join(shown)}" if shown else ""
164
+ return [
165
+ Result(
166
+ cid,
167
+ title,
168
+ Status.WARN,
169
+ f"{len(missing)} key(s) in .env.example missing from .env{extra}",
170
+ "Add the missing keys to .env (values are never read or shown by DevReady).",
171
+ )
172
+ ]
devready/cli.py CHANGED
@@ -25,6 +25,8 @@ def main(argv: list[str] | None = None) -> int:
25
25
  sub = parser.add_subparsers(dest="command", required=True)
26
26
  check = sub.add_parser("check", help="run read-only environment checks")
27
27
  check.add_argument("--format", choices=["text", "json"], default="text")
28
+ explain = sub.add_parser("explain", help="explain what a check id means")
29
+ explain.add_argument("check_id")
28
30
  sub.add_parser("mcp", help="run as an MCP server over stdio")
29
31
  check.add_argument("--project", default=".", help="directory whose version pins to compare")
30
32
 
@@ -38,6 +40,16 @@ def main(argv: list[str] | None = None) -> int:
38
40
 
39
41
  return serve()
40
42
 
43
+ if args.command == "explain":
44
+ from devready.explain import EXPLANATIONS
45
+
46
+ text = EXPLANATIONS.get(args.check_id)
47
+ if text is None:
48
+ print(f"devready: unknown check id; known: {', '.join(EXPLANATIONS)}", file=sys.stderr)
49
+ return EXIT_USAGE
50
+ print(text)
51
+ return EXIT_OK
52
+
41
53
  root = Path(args.project)
42
54
  if not root.is_dir():
43
55
  print(f"devready: not a directory: {args.project}", file=sys.stderr)
devready/explain.py ADDED
@@ -0,0 +1,19 @@
1
+ """Static, beginner-friendly explanations per check id (used by `devready explain`)."""
2
+
3
+ EXPLANATIONS = {
4
+ "python.version": "The Python interpreter running DevReady; not necessarily your project's virtualenv.",
5
+ "git.version": "Git is needed to clone, branch and commit. Install: https://git-scm.com/downloads",
6
+ "node.version": "Node.js runs JavaScript tooling. Many projects pin a version; see project.node.* checks.",
7
+ "npm.version": "npm installs JavaScript dependencies and ships with Node.js.",
8
+ "project.node.pin": "A .nvmrc/.node-version file says which Node the team uses. A mismatch is a warning; switch with nvm, fnm or mise.",
9
+ "project.node.engines": "package.json engines.node is a hard requirement. A mismatch fails the check and npm may refuse to install.",
10
+ "project.node.tool_versions": ".tool-versions (asdf/mise) pins Node for this project.",
11
+ "project.python.pin": ".python-version (pyenv/uv) pins Python for this project.",
12
+ "project.python.requires": "pyproject.toml requires-python is a hard requirement; pip refuses incompatible versions.",
13
+ "project.python.tool_versions": ".tool-versions (asdf/mise) pins Python for this project.",
14
+ "project.env.keys": "Compares variable NAMES in .env.example with .env. Values are never read or shown.",
15
+ "docker.cli": "Shown only when the project has a Dockerfile or compose file and the docker command is missing.",
16
+ "docker.daemon": "The Docker CLI can exist while the daemon is stopped. Start Docker Desktop or the service.",
17
+ "git.identity": "Commits need user.name and user.email. DevReady checks they exist, never what they are.",
18
+ "disk.space": "Warns below 1 GB free where the project lives; installs and builds can fail when the disk is full.",
19
+ }
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.extras import check_disk, check_docker, check_git_identity
6
7
  from devready.checks.project import check_project
7
8
  from devready.checks.python_check import check_python
8
9
  from devready.checks.tools import TOOLS, check_tool
@@ -15,7 +16,9 @@ def run_checks(root: Path) -> list[Result]:
15
16
  for tool in TOOLS:
16
17
  result, versions[tool[2]] = check_tool(*tool)
17
18
  results.append(result)
18
- return results + check_project(root, versions["node"], platform.python_version())
19
+ results += check_git_identity()
20
+ results += check_project(root, versions["node"], platform.python_version())
21
+ return results + check_docker(root) + check_disk(root)
19
22
 
20
23
 
21
24
  def report(results: list[Result]) -> dict:
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: devready-cli
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: A local-first, read-only CLI that checks a project's environment requirements against what is installed.
5
- Project-URL: Homepage, https://github.com/Evalogical-Inc/devReady
5
+ Project-URL: Homepage, https://devready-ten.vercel.app/
6
+ Project-URL: Repository, https://github.com/Evalogical-Inc/devReady
6
7
  Project-URL: Issues, https://github.com/Evalogical-Inc/devReady/issues
7
8
  Author: Evalogical Inc
8
9
  License-Expression: MIT
@@ -31,6 +32,8 @@ Description-Content-Type: text/markdown
31
32
  Built for humans, CI, and AI coding agents (Claude Code, Codex, any MCP client).
32
33
 
33
34
  [![CI](https://github.com/Evalogical-Inc/devReady/actions/workflows/ci.yml/badge.svg)](https://github.com/Evalogical-Inc/devReady/actions/workflows/ci.yml)
35
+ [![PyPI](https://img.shields.io/pypi/v/devready-cli)](https://pypi.org/project/devready-cli/)
36
+ [![Website](https://img.shields.io/badge/website-devready--ten.vercel.app-ff6a35)](https://devready-ten.vercel.app/)
34
37
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
35
38
  ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)
36
39
  ![Dependencies: 0](https://img.shields.io/badge/dependencies-0-brightgreen)
@@ -84,12 +87,12 @@ No changes were made to your system.
84
87
 
85
88
  ## Install
86
89
 
87
- PyPI release is planned. Until then, install straight from GitHub:
90
+ Install from [PyPI](https://pypi.org/project/devready-cli/):
88
91
 
89
92
  ```bash
90
- pipx install git+https://github.com/Evalogical-Inc/devReady
93
+ pipx install devready-cli
91
94
  # or run without installing:
92
- uvx --from git+https://github.com/Evalogical-Inc/devReady devready check
95
+ uvx --from devready-cli devready check
93
96
  ```
94
97
 
95
98
  From a clone (for development):
@@ -107,6 +110,7 @@ python -m pip install -e ".[dev]"
107
110
  devready check # human-readable report for the current directory
108
111
  devready check --project ../app # inspect another project's pins
109
112
  devready check --format json # machine-readable (see docs/schema.json)
113
+ devready explain docker.daemon # what a check means and how to fix it
110
114
  devready mcp # run as an MCP server over stdio
111
115
  devready --version
112
116
  ```
@@ -161,7 +165,7 @@ DevReady ships the same engine three ways, so your agent can verify the environm
161
165
  # ~/.codex/config.toml (verify keys against your Codex version)
162
166
  [mcp_servers.devready]
163
167
  command = "uvx"
164
- args = ["--from", "git+https://github.com/Evalogical-Inc/devReady", "devready", "mcp"]
168
+ args = ["--from", "devready-cli", "devready", "mcp"]
165
169
  ```
166
170
 
167
171
  **Anything else** - drop [`AGENTS.md`](AGENTS.md) in your repo, or copy [`skills/devready`](skills/devready/SKILL.md). Details in [docs/agents.md](docs/agents.md).
@@ -177,16 +181,15 @@ See [SECURITY.md](SECURITY.md) to report a vulnerability.
177
181
 
178
182
  ## Limitations (alpha)
179
183
 
180
- - Checks today: Python, Git, Node.js, npm, and the project pins listed above.
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.
181
185
  - The version-range matcher covers common npm/PEP 440 syntax; pre-releases and exotic ranges report `skip` rather than guess.
182
186
  - The Python version compared is the interpreter running DevReady, not necessarily your project's virtualenv.
183
187
 
184
188
  ## Roadmap
185
189
 
186
- - [ ] Docker CLI + daemon reachability
187
- - [ ] `.tool-versions` (asdf/mise) and `.env.example` vs `.env` key-name diff (names only)
188
- - [ ] Port-in-use, disk space, git identity checks
189
- - [ ] PyPI release, `devready explain <check_id>`
190
+ - [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)
190
193
  - [ ] Standalone binaries
191
194
 
192
195
  Have a setup problem DevReady should catch? [Open an issue](https://github.com/Evalogical-Inc/devReady/issues/new/choose). Early feedback shapes the roadmap.
@@ -0,0 +1,17 @@
1
+ devready/__init__.py,sha256=4GZKi13lDTD25YBkGakhZyEQZWTER_OWQMNPoH_UM2c,22
2
+ devready/cli.py,sha256=yStxOgFcYqVOVmcbMZGwU83zaZZemecJKGsg0KbVUEE,2521
3
+ devready/explain.py,sha256=PaAt0K0-tdQB5pS9Y2vxZEKChwra_SdY7-qAT0juKto,1765
4
+ devready/mcp_server.py,sha256=zVm89kZL_-pZwTSWkt6rxKonytNklUSa0RCQkX0lzFk,3177
5
+ devready/models.py,sha256=KyGp3ie2LVQmuSfzLV8tbEH_hJyU5-Vi11d0JcQPAuE,646
6
+ devready/runner.py,sha256=5WML6vsoVer0FUpgCderZuqip6ldhWtWGZ-ufVxQV7U,848
7
+ devready/versions.py,sha256=dXKIl1-qvcfadRNaCURMoftwx7YtadDZ-YfUp-85XZc,2112
8
+ devready/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ devready/checks/extras.py,sha256=pDJbFaAwjkLc92tcFagOmj-5c_vMx32_MtYa_DBfCz8,3428
10
+ devready/checks/project.py,sha256=UnnjuZmG2aXXBAY45wTEP-bj4CWODMv3DPfEi1V_QDE,5983
11
+ devready/checks/python_check.py,sha256=uQCd-N0LEwt9R5gwZm5mlYiPMST1JXVDNQOdZ5xpako,328
12
+ devready/checks/tools.py,sha256=bjO61jFefp0b6iBHg-a79uaz8nZGCldakfHyB1G8Gf4,1511
13
+ devready_cli-0.0.3.dist-info/METADATA,sha256=H2V7HI5_AC5Gi5cGM2_UPGk5b6OKiGr-nbJPP2s1c2w,8835
14
+ devready_cli-0.0.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
15
+ devready_cli-0.0.3.dist-info/entry_points.txt,sha256=7oAecSWtsIt7qLOP8PipOeDqhXCkEJat0oxhD4T1RpY,47
16
+ devready_cli-0.0.3.dist-info/licenses/LICENSE,sha256=S_AQsEw289FIsOgT-OPWL5JZeV8nn5kicwEq0r9qu7s,1071
17
+ devready_cli-0.0.3.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- devready/__init__.py,sha256=sXLh7g3KC4QCFxcZGBTpG2scR7hmmBsMjq6LqRptkRg,22
2
- devready/cli.py,sha256=vpZdDBOJ02VfwiK-qkDUjqaqukZ34lXMDUdk_viQgyI,2075
3
- devready/mcp_server.py,sha256=zVm89kZL_-pZwTSWkt6rxKonytNklUSa0RCQkX0lzFk,3177
4
- devready/models.py,sha256=KyGp3ie2LVQmuSfzLV8tbEH_hJyU5-Vi11d0JcQPAuE,646
5
- devready/runner.py,sha256=6OrItxl-jxHlBZnYiY-_qEBee_FEZ9PTz36I6m3OXcU,679
6
- devready/versions.py,sha256=dXKIl1-qvcfadRNaCURMoftwx7YtadDZ-YfUp-85XZc,2112
7
- devready/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- devready/checks/project.py,sha256=MggS-7KGVeyVv9p7jVtF5zlBoOO3XjeQQUC5WT5fhAE,3878
9
- devready/checks/python_check.py,sha256=uQCd-N0LEwt9R5gwZm5mlYiPMST1JXVDNQOdZ5xpako,328
10
- devready/checks/tools.py,sha256=bjO61jFefp0b6iBHg-a79uaz8nZGCldakfHyB1G8Gf4,1511
11
- devready_cli-0.0.1.dist-info/METADATA,sha256=irpieA9wAkrJPQ52_3buSMTcfZe58jMrEX2_DJQNb7M,8349
12
- devready_cli-0.0.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
13
- devready_cli-0.0.1.dist-info/entry_points.txt,sha256=7oAecSWtsIt7qLOP8PipOeDqhXCkEJat0oxhD4T1RpY,47
14
- devready_cli-0.0.1.dist-info/licenses/LICENSE,sha256=S_AQsEw289FIsOgT-OPWL5JZeV8nn5kicwEq0r9qu7s,1071
15
- devready_cli-0.0.1.dist-info/RECORD,,