devready-cli 0.0.4__tar.gz → 0.2.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.
Files changed (57) hide show
  1. {devready_cli-0.0.4 → devready_cli-0.2.0}/.claude-plugin/plugin.json +1 -1
  2. {devready_cli-0.0.4 → devready_cli-0.2.0}/CHANGELOG.md +11 -0
  3. {devready_cli-0.0.4 → devready_cli-0.2.0}/PKG-INFO +25 -4
  4. {devready_cli-0.0.4 → devready_cli-0.2.0}/README.md +24 -3
  5. devready_cli-0.2.0/ROADMAP.md +68 -0
  6. devready_cli-0.2.0/action.yml +50 -0
  7. {devready_cli-0.0.4 → devready_cli-0.2.0}/pyproject.toml +1 -1
  8. devready_cli-0.2.0/src/devready/__init__.py +1 -0
  9. devready_cli-0.2.0/src/devready/baseline.py +43 -0
  10. devready_cli-0.2.0/src/devready/checks/config.py +102 -0
  11. devready_cli-0.2.0/src/devready/checks/health.py +116 -0
  12. devready_cli-0.2.0/src/devready/checks/runtimes.py +103 -0
  13. {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/checks/tools.py +6 -3
  14. {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/cli.py +50 -3
  15. devready_cli-0.2.0/src/devready/explain.py +50 -0
  16. devready_cli-0.2.0/src/devready/formats.py +82 -0
  17. {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/runner.py +15 -1
  18. devready_cli-0.2.0/tests/unit/test_baseline.py +49 -0
  19. devready_cli-0.2.0/tests/unit/test_config.py +67 -0
  20. devready_cli-0.2.0/tests/unit/test_formats.py +62 -0
  21. devready_cli-0.2.0/tests/unit/test_health.py +56 -0
  22. devready_cli-0.2.0/tests/unit/test_runtimes.py +72 -0
  23. devready_cli-0.0.4/src/devready/__init__.py +0 -1
  24. devready_cli-0.0.4/src/devready/explain.py +0 -19
  25. {devready_cli-0.0.4 → devready_cli-0.2.0}/.claude-plugin/marketplace.json +0 -0
  26. {devready_cli-0.0.4 → devready_cli-0.2.0}/.gitattributes +0 -0
  27. {devready_cli-0.0.4 → devready_cli-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  28. {devready_cli-0.0.4 → devready_cli-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  29. {devready_cli-0.0.4 → devready_cli-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  30. {devready_cli-0.0.4 → devready_cli-0.2.0}/.github/workflows/ci.yml +0 -0
  31. {devready_cli-0.0.4 → devready_cli-0.2.0}/.github/workflows/publish.yml +0 -0
  32. {devready_cli-0.0.4 → devready_cli-0.2.0}/.gitignore +0 -0
  33. {devready_cli-0.0.4 → devready_cli-0.2.0}/.mcp.json +0 -0
  34. {devready_cli-0.0.4 → devready_cli-0.2.0}/AGENTS.md +0 -0
  35. {devready_cli-0.0.4 → devready_cli-0.2.0}/CODE_OF_CONDUCT.md +0 -0
  36. {devready_cli-0.0.4 → devready_cli-0.2.0}/CONTRIBUTING.md +0 -0
  37. {devready_cli-0.0.4 → devready_cli-0.2.0}/LICENSE +0 -0
  38. {devready_cli-0.0.4 → devready_cli-0.2.0}/SECURITY.md +0 -0
  39. {devready_cli-0.0.4 → devready_cli-0.2.0}/commands/doctor.md +0 -0
  40. {devready_cli-0.0.4 → devready_cli-0.2.0}/docs/agents.md +0 -0
  41. {devready_cli-0.0.4 → devready_cli-0.2.0}/docs/research.md +0 -0
  42. {devready_cli-0.0.4 → devready_cli-0.2.0}/docs/schema.json +0 -0
  43. {devready_cli-0.0.4 → devready_cli-0.2.0}/docs/technical-plan.md +0 -0
  44. {devready_cli-0.0.4 → devready_cli-0.2.0}/skills/devready/SKILL.md +0 -0
  45. {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/__main__.py +0 -0
  46. {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/checks/__init__.py +0 -0
  47. {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/checks/extras.py +0 -0
  48. {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/checks/project.py +0 -0
  49. {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/checks/python_check.py +0 -0
  50. {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/mcp_server.py +0 -0
  51. {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/models.py +0 -0
  52. {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/versions.py +0 -0
  53. {devready_cli-0.0.4 → devready_cli-0.2.0}/tests/unit/test_cli.py +0 -0
  54. {devready_cli-0.0.4 → devready_cli-0.2.0}/tests/unit/test_extras.py +0 -0
  55. {devready_cli-0.0.4 → devready_cli-0.2.0}/tests/unit/test_mcp.py +0 -0
  56. {devready_cli-0.0.4 → devready_cli-0.2.0}/tests/unit/test_schema.py +0 -0
  57. {devready_cli-0.0.4 → devready_cli-0.2.0}/tests/unit/test_versions_project.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devready",
3
- "version": "0.0.4",
3
+ "version": "0.2.0",
4
4
  "homepage": "https://devready-ten.vercel.app/",
5
5
  "description": "Read-only environment preflight: compares a project's declared Node/Python versions with what is installed.",
6
6
  "author": { "name": "DevReady contributors" },
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 - 2026-10-16
4
+ - `check --baseline FILE` (and `baseline:` Action input): show only results that changed since a saved JSON report.
5
+ - Optional `.devready.json` (`skip`, `require`, `env`) and `devready init` (prints a starter).
6
+
7
+ ## 0.1.0 - 2026-10-16
8
+ - GitHub Action (`action.yml`) wrapping `devready check`.
9
+ - `--format junit|sarif`, `--only`/`--skip` filters, and `devready fix --plan` (suggestions only).
10
+ - Project health: `.env` not git-ignored, `node_modules` missing/older than lockfile, declared ports already in use.
11
+ - pnpm, Yarn and Bun checks when `package.json` `packageManager` names them.
12
+ - Go, Ruby, Java, Rust and .NET checks, run only when the project pins them (`go.mod`, `.ruby-version`, `.java-version`, `rust-toolchain[.toml]`, `global.json`, `.tool-versions`).
13
+
3
14
  ## 0.0.4 - 2026-10-16
4
15
  - `python -m devready` entry point.
5
16
  - Skill, `/devready:doctor` and `AGENTS.md` now fall back to `python -m devready`, `uvx`, then `pipx run`, and tell the user how to install instead of failing.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: devready-cli
3
- Version: 0.0.4
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
@@ -111,6 +111,12 @@ 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
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
114
120
  devready mcp # run as an MCP server over stdio
115
121
  devready --version
116
122
  ```
@@ -148,6 +154,16 @@ devready --version
148
154
  }
149
155
  ```
150
156
 
157
+ ## Use it in CI (GitHub Actions)
158
+
159
+ ```yaml
160
+ - uses: Evalogical-Inc/devReady@main # pin a release tag once published
161
+ with:
162
+ skip: project.ports # optional; also: only, format, project, version
163
+ ```
164
+
165
+ 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.
166
+
151
167
  ## Use it with AI coding agents
152
168
 
153
169
  DevReady ships the same engine three ways, so your agent can verify the environment *before* it installs, builds or tests.
@@ -181,15 +197,16 @@ See [SECURITY.md](SECURITY.md) to report a vulnerability.
181
197
 
182
198
  ## Limitations (alpha)
183
199
 
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.
200
+ - 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
201
  - The version-range matcher covers common npm/PEP 440 syntax; pre-releases and exotic ranges report `skip` rather than guess.
186
202
  - The Python version compared is the interpreter running DevReady, not necessarily your project's virtualenv.
187
203
 
188
204
  ## Roadmap
189
205
 
190
206
  - [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)
207
+ - [x] Port-in-use checks, more ecosystems (Go, Rust, Java, Ruby, .NET, pnpm/yarn/bun), JUnit/SARIF output, `fix --plan`, GitHub Action
208
+ - [x] `.devready.json` config, `--baseline` snapshots
209
+ - [ ] Platform-specific checks (see [ROADMAP.md](ROADMAP.md))
193
210
  - [ ] Standalone binaries
194
211
 
195
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.
@@ -202,6 +219,10 @@ PRs welcome, especially new read-only checks and platform fixes. Start with [CON
202
219
  ruff check . && pytest
203
220
  ```
204
221
 
222
+ ## About Evalogical
223
+
224
+ 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).
225
+
205
226
  ## License
206
227
 
207
228
  [MIT](LICENSE) © Evalogical Inc
@@ -87,6 +87,12 @@ devready check # human-readable report for the current direct
87
87
  devready check --project ../app # inspect another project's pins
88
88
  devready check --format json # machine-readable (see docs/schema.json)
89
89
  devready explain docker.daemon # what a check means and how to fix it
90
+ devready check --format junit # or sarif, for CI
91
+ devready check --only project --skip project.ports # filter by id or prefix
92
+ devready fix --plan # suggested steps only; never runs them
93
+ devready check --format json > baseline.json # save a snapshot (commit it for a team baseline)
94
+ devready check --baseline baseline.json # show only what changed; fail only on new/worse failures
95
+ devready init > .devready.json # optional config: skip ids, require node/python, required env names
90
96
  devready mcp # run as an MCP server over stdio
91
97
  devready --version
92
98
  ```
@@ -124,6 +130,16 @@ devready --version
124
130
  }
125
131
  ```
126
132
 
133
+ ## Use it in CI (GitHub Actions)
134
+
135
+ ```yaml
136
+ - uses: Evalogical-Inc/devReady@main # pin a release tag once published
137
+ with:
138
+ skip: project.ports # optional; also: only, format, project, version
139
+ ```
140
+
141
+ 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.
142
+
127
143
  ## Use it with AI coding agents
128
144
 
129
145
  DevReady ships the same engine three ways, so your agent can verify the environment *before* it installs, builds or tests.
@@ -157,15 +173,16 @@ See [SECURITY.md](SECURITY.md) to report a vulnerability.
157
173
 
158
174
  ## Limitations (alpha)
159
175
 
160
- - 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.
176
+ - 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.
161
177
  - The version-range matcher covers common npm/PEP 440 syntax; pre-releases and exotic ranges report `skip` rather than guess.
162
178
  - The Python version compared is the interpreter running DevReady, not necessarily your project's virtualenv.
163
179
 
164
180
  ## Roadmap
165
181
 
166
182
  - [x] Docker CLI + daemon reachability, `.tool-versions`, `.env` key-name diff, disk space, git identity, `devready explain`
167
- - [ ] Port-in-use checks (ports read from compose files)
168
- - [ ] More ecosystems (Go, Rust, Java, Ruby pins)
183
+ - [x] Port-in-use checks, more ecosystems (Go, Rust, Java, Ruby, .NET, pnpm/yarn/bun), JUnit/SARIF output, `fix --plan`, GitHub Action
184
+ - [x] `.devready.json` config, `--baseline` snapshots
185
+ - [ ] Platform-specific checks (see [ROADMAP.md](ROADMAP.md))
169
186
  - [ ] Standalone binaries
170
187
 
171
188
  Have a setup problem DevReady should catch? [Open an issue](https://github.com/Evalogical-Inc/devReady/issues/new/choose). Early feedback shapes the roadmap.
@@ -178,6 +195,10 @@ PRs welcome, especially new read-only checks and platform fixes. Start with [CON
178
195
  ruff check . && pytest
179
196
  ```
180
197
 
198
+ ## About Evalogical
199
+
200
+ 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).
201
+
181
202
  ## License
182
203
 
183
204
  [MIT](LICENSE) © Evalogical Inc
@@ -0,0 +1,68 @@
1
+ # Roadmap
2
+
3
+ Principles (unchanged): read-only, local-first, zero runtime dependencies, Python >= 3.10, never print env values or raw command output.
4
+
5
+ ## Where we are (0.2.0)
6
+
7
+ - Tools: Git, Node, npm, Python. Docker, Go, Ruby, Java, Rust, .NET, pnpm, Yarn, Bun only when the project declares them (details in the sections below).
8
+ - Pins compared: `.nvmrc`, `.node-version`, `.python-version`, `package.json` engines.node, `pyproject.toml` requires-python, `.tool-versions` (node/python only).
9
+ - Also: `.env.example` vs `.env` key names, git identity presence, free disk space.
10
+ - Interfaces: `check` (text/JSON/JUnit/SARIF, `--only`/`--skip`/`--baseline`, exit 1 on `fail`), `fix --plan`, `init`, `explain`, `mcp`, GitHub Action, Claude Code plugin, `AGENTS.md`.
11
+ - CI and PyPI publish workflows exist.
12
+
13
+ ## Decisions (from the docs review)
14
+
15
+ - Keep `check` + `explain`; no separate `doctor` command.
16
+ - JSON schema stays at v1. No `details` field; new fields need a version bump (revisit at 1.0).
17
+ - Check ids must match `^[a-z_]+(\.[a-z_]+)+$`: no digits or hyphens.
18
+ - Anything echoing project file content only uses validated strings (`SAFE_SPEC`, extracted digits).
19
+ - Docs gaps: `docs/architecture.md`, `docs/security.md`, `tests/integration/`, `tests/fixtures/`.
20
+
21
+ ## 0.1 - More runtimes (in progress)
22
+
23
+ - [x] Go (`go.mod`), Ruby (`.ruby-version`), Java (`.java-version`), Rust (`rust-toolchain[.toml]`), .NET (`global.json`), plus their `.tool-versions` lines. Checked only when the project declares them, so no noise on other projects. (`checks/runtimes.py`)
24
+ - [x] `explain` entries and tests for every new id.
25
+ - [x] pnpm/yarn/bun via the `packageManager` field in `package.json`.
26
+ - [ ] `uv`/`pip` (no pin file to compare against yet).
27
+ - [ ] Table-drive the existing Node/Python pins in `project.py` the same way.
28
+ - [ ] Add `pom.xml` / `build.gradle` Java targets, `Gemfile` ruby version.
29
+ - [ ] Fixture-based integration tests with real tools on CI.
30
+
31
+ ## 0.2 - Project health (in progress)
32
+
33
+ - [x] Node dependency drift: `node_modules` missing or older than a lockfile (directory mtime).
34
+ - [ ] Python drift (venv present, `pip check`); skipped for now since conda/global envs make "no venv" noisy.
35
+ - [x] Port conflicts for ports declared in compose files and `*PORT=` in `.env.example` (`package.json` scripts not parsed).
36
+ - [x] `.env` hygiene: warn if `.env` is not gitignored or is tracked.
37
+ - [ ] Docker: compose file validity, image platform mismatch (arm64 vs amd64).
38
+
39
+ ## 0.3 - Actionable output (in progress)
40
+
41
+ - [x] `devready fix --plan`: prints each `fail`/`warn` with its suggestion. Never runs anything. (Suggestions are prose today, not exact commands.)
42
+ - [x] `--format sarif` and `junit` (SARIF has no file locations yet, so GitHub code scanning may not show alerts).
43
+ - [x] `--only` / `--skip` filters by check id or dotted prefix (repeatable).
44
+ - [x] Optional `.devready.json` (JSON, so no parser dependency): `skip` ids, `require` node/python, required `env` names.
45
+ - [x] `devready init` prints a starter file (does not write it; DevReady stays read-only).
46
+
47
+ ## 0.4 - Team and CI
48
+
49
+ - [x] GitHub Action (`action.yml`, composite). Untested on a real runner; needs a release with the new flags.
50
+ - [x] `--baseline FILE`: compare with a saved `--format json` report (commit it for a team baseline). Shows new/changed results; exit 1 only for a new or worse `fail`. Results that disappeared are not reported.
51
+ - [x] Diff mode = `--baseline` against your own saved snapshot. DevReady never writes the snapshot (read-only), and there is no `--watch` loop.
52
+
53
+ ## 0.5 - Platform specifics
54
+
55
+ - [ ] Windows: long paths enabled, execution policy, `core.autocrlf`.
56
+ - [ ] WSL version and Docker Desktop backend.
57
+ - [ ] macOS: Xcode CLT; Linux: common missing build deps.
58
+
59
+ ## 1.0 - Stabilize
60
+
61
+ - [ ] Freeze JSON schema (`docs/schema.json`) with a versioning policy.
62
+ - [ ] Test matrix on Windows/macOS/Linux x Python 3.10-3.13.
63
+ - [ ] Docs pass: check catalog, plugin/MCP setup, contributing guide for adding a check.
64
+
65
+ ## Opt-in only (needs explicit design review)
66
+
67
+ - Network reachability checks (registry, DB host): off by default, strict timeouts.
68
+ - Anything that executes project code or installs software: out of scope.
@@ -0,0 +1,50 @@
1
+ name: DevReady check
2
+ description: Read-only environment preflight. Fails the job when a declared requirement is unmet.
3
+ branding:
4
+ icon: check-circle
5
+ color: green
6
+ inputs:
7
+ project:
8
+ description: Directory whose version pins to compare.
9
+ default: "."
10
+ format:
11
+ description: text, json, junit or sarif.
12
+ default: text
13
+ only:
14
+ description: Space-separated check ids or prefixes to run (empty = all).
15
+ default: ""
16
+ skip:
17
+ description: Space-separated check ids or prefixes to skip.
18
+ default: ""
19
+ baseline:
20
+ description: Path to a committed `--format json` report; only changes are shown and only a new or worse failure fails the job.
21
+ default: ""
22
+ version:
23
+ description: devready-cli version to install (empty = latest).
24
+ default: ""
25
+ python-version:
26
+ description: Python used to run DevReady (not your project's Python).
27
+ default: "3.12"
28
+ runs:
29
+ using: composite
30
+ steps:
31
+ - uses: actions/setup-python@v5
32
+ with:
33
+ python-version: ${{ inputs.python-version }}
34
+ # Inputs go through env vars, never interpolated into the script, to avoid shell injection.
35
+ - shell: bash
36
+ env:
37
+ DR_PROJECT: ${{ inputs.project }}
38
+ DR_FORMAT: ${{ inputs.format }}
39
+ DR_ONLY: ${{ inputs.only }}
40
+ DR_SKIP: ${{ inputs.skip }}
41
+ DR_VERSION: ${{ inputs.version }}
42
+ DR_BASELINE: ${{ inputs.baseline }}
43
+ run: |
44
+ set -eu
45
+ python -m pip install --quiet "devready-cli${DR_VERSION:+==$DR_VERSION}"
46
+ args=(check --project "$DR_PROJECT" --format "$DR_FORMAT")
47
+ for id in $DR_ONLY; do args+=(--only "$id"); done
48
+ for id in $DR_SKIP; do args+=(--skip "$id"); done
49
+ if [ -n "$DR_BASELINE" ]; then args+=(--baseline "$DR_BASELINE"); fi
50
+ python -m devready "${args[@]}"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "devready-cli"
7
- version = "0.0.4"
7
+ version = "0.2.0"
8
8
  description = "A local-first, read-only CLI that checks a project's environment requirements against what is installed."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -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)
@@ -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