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.
- {devready_cli-0.0.4 → devready_cli-0.2.0}/.claude-plugin/plugin.json +1 -1
- {devready_cli-0.0.4 → devready_cli-0.2.0}/CHANGELOG.md +11 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/PKG-INFO +25 -4
- {devready_cli-0.0.4 → devready_cli-0.2.0}/README.md +24 -3
- devready_cli-0.2.0/ROADMAP.md +68 -0
- devready_cli-0.2.0/action.yml +50 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/pyproject.toml +1 -1
- devready_cli-0.2.0/src/devready/__init__.py +1 -0
- devready_cli-0.2.0/src/devready/baseline.py +43 -0
- devready_cli-0.2.0/src/devready/checks/config.py +102 -0
- devready_cli-0.2.0/src/devready/checks/health.py +116 -0
- devready_cli-0.2.0/src/devready/checks/runtimes.py +103 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/checks/tools.py +6 -3
- {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/cli.py +50 -3
- devready_cli-0.2.0/src/devready/explain.py +50 -0
- devready_cli-0.2.0/src/devready/formats.py +82 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/runner.py +15 -1
- devready_cli-0.2.0/tests/unit/test_baseline.py +49 -0
- devready_cli-0.2.0/tests/unit/test_config.py +67 -0
- devready_cli-0.2.0/tests/unit/test_formats.py +62 -0
- devready_cli-0.2.0/tests/unit/test_health.py +56 -0
- devready_cli-0.2.0/tests/unit/test_runtimes.py +72 -0
- devready_cli-0.0.4/src/devready/__init__.py +0 -1
- devready_cli-0.0.4/src/devready/explain.py +0 -19
- {devready_cli-0.0.4 → devready_cli-0.2.0}/.claude-plugin/marketplace.json +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/.gitattributes +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/.github/workflows/ci.yml +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/.github/workflows/publish.yml +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/.gitignore +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/.mcp.json +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/AGENTS.md +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/CODE_OF_CONDUCT.md +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/CONTRIBUTING.md +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/LICENSE +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/SECURITY.md +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/commands/doctor.md +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/docs/agents.md +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/docs/research.md +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/docs/schema.json +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/docs/technical-plan.md +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/skills/devready/SKILL.md +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/__main__.py +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/checks/__init__.py +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/checks/extras.py +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/checks/project.py +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/checks/python_check.py +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/mcp_server.py +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/models.py +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/src/devready/versions.py +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/tests/unit/test_cli.py +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/tests/unit/test_extras.py +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/tests/unit/test_mcp.py +0 -0
- {devready_cli-0.0.4 → devready_cli-0.2.0}/tests/unit/test_schema.py +0 -0
- {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
|
|
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
|
|
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),
|
|
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
|
-
- [
|
|
192
|
-
- [
|
|
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),
|
|
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
|
-
- [
|
|
168
|
-
- [
|
|
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
|
|
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
|