envlock-cli 1.1.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.
- envlock_cli-1.1.0/LICENSE +21 -0
- envlock_cli-1.1.0/PKG-INFO +185 -0
- envlock_cli-1.1.0/README.md +157 -0
- envlock_cli-1.1.0/envlock/__init__.py +5 -0
- envlock_cli-1.1.0/envlock/__main__.py +3 -0
- envlock_cli-1.1.0/envlock/cli.py +175 -0
- envlock_cli-1.1.0/envlock/collectors.py +221 -0
- envlock_cli-1.1.0/envlock/diff.py +167 -0
- envlock_cli-1.1.0/envlock/renderer.py +180 -0
- envlock_cli-1.1.0/envlock/snapshot.py +58 -0
- envlock_cli-1.1.0/envlock/versions.py +49 -0
- envlock_cli-1.1.0/envlock_cli.egg-info/PKG-INFO +185 -0
- envlock_cli-1.1.0/envlock_cli.egg-info/SOURCES.txt +21 -0
- envlock_cli-1.1.0/envlock_cli.egg-info/dependency_links.txt +1 -0
- envlock_cli-1.1.0/envlock_cli.egg-info/entry_points.txt +2 -0
- envlock_cli-1.1.0/envlock_cli.egg-info/requires.txt +8 -0
- envlock_cli-1.1.0/envlock_cli.egg-info/top_level.txt +1 -0
- envlock_cli-1.1.0/pyproject.toml +51 -0
- envlock_cli-1.1.0/setup.cfg +4 -0
- envlock_cli-1.1.0/tests/test_cli.py +76 -0
- envlock_cli-1.1.0/tests/test_collectors.py +81 -0
- envlock_cli-1.1.0/tests/test_diff.py +69 -0
- envlock_cli-1.1.0/tests/test_versions.py +23 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Serber1990
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: envlock-cli
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Freeze and compare Python, Node, and system environments — detect dependency drift before it breaks your builds.
|
|
5
|
+
Author-email: Serber1990 <serber1990@pm.me>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/serber1990/envlock
|
|
8
|
+
Project-URL: Repository, https://github.com/serber1990/envlock
|
|
9
|
+
Project-URL: Issues, https://github.com/serber1990/envlock/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/serber1990/envlock/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: environment,dependencies,drift,reproducibility,devops,ci,python,node
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: POSIX
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: shellcolorize>=1.1.0
|
|
23
|
+
Requires-Dist: tomli>=1.1; python_version < "3.11"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# envlock
|
|
30
|
+
|
|
31
|
+
[](https://github.com/serber1990/envlock/actions/workflows/ci.yml)
|
|
32
|
+
[](https://badge.fury.io/py/envlock-cli)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
Freeze your environment. Detect when it drifts.
|
|
36
|
+
|
|
37
|
+
**envlock** snapshots a project's Python packages, Node.js dependencies and system runtimes, then tells you
|
|
38
|
+
exactly what changed — so "works on my machine" stops being an excuse. Use it locally, in CI, or to compare
|
|
39
|
+
staging with production.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## ✨ What it tracks
|
|
44
|
+
|
|
45
|
+
| Layer | What's captured |
|
|
46
|
+
|-------|----------------|
|
|
47
|
+
| **Python** | Interpreter and version (the project's `.venv` / `venv` / `env`, or the active virtualenv), installed packages, `requirements.txt`, `pyproject.toml` dependencies |
|
|
48
|
+
| **Node.js** | Node and npm versions, `package.json` dependencies + devDependencies, `package-lock.json` resolved versions, `yarn.lock` presence |
|
|
49
|
+
| **System** | OS, architecture, Go / Ruby / Java / Rust versions, relevant env vars (`VIRTUAL_ENV`, `NODE_ENV`, `JAVA_HOME`…) |
|
|
50
|
+
|
|
51
|
+
### Severity levels
|
|
52
|
+
|
|
53
|
+
| Severity | Examples |
|
|
54
|
+
|----------|----------|
|
|
55
|
+
| ⛔ **CRITICAL** | Python/Node major or minor version changed (3.12 → 3.13), OS or architecture changed |
|
|
56
|
+
| ⚠️ **WARNING** | Package removed or downgraded, declared constraint changed, runtime patch release, lockfile or env var removed |
|
|
57
|
+
| ℹ️ **INFO** | Package added or upgraded, new lockfile or env var |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 📥 Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install envlock-cli # the command is `envlock`
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 🛠 Usage
|
|
70
|
+
|
|
71
|
+
### 1 — Take a baseline snapshot
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
envlock snapshot # saves .envlock.json in the current directory
|
|
75
|
+
envlock snapshot --path /srv/myapp # scan another project
|
|
76
|
+
envlock snapshot --output locks/prod.json # custom location
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2 — Check for drift
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
envlock check # terminal report
|
|
83
|
+
envlock check --format json | jq .summary # JSON on stdout (progress goes to stderr)
|
|
84
|
+
envlock check --format markdown -o drift-report.md # Markdown report file
|
|
85
|
+
envlock check --baseline locks/prod.json --fail-on critical
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3 — Diff any two snapshots
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
envlock diff staging.json production.json
|
|
92
|
+
envlock diff before.json after.json --format markdown -o drift.md
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 📄 Example output
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
╔════════════════════════════════════════╗
|
|
101
|
+
║ envlock · environment drift report ║
|
|
102
|
+
╚════════════════════════════════════════╝
|
|
103
|
+
|
|
104
|
+
Baseline 2026-05-10T09:00:00+00:00 (dev-laptop) /srv/app
|
|
105
|
+
Current 2026-05-12T14:22:18+00:00 (dev-laptop) /srv/app
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
── Python Runtime ────────────────────────────
|
|
109
|
+
⛔ ~ Python version changed 3.12.1 → 3.13.0
|
|
110
|
+
|
|
111
|
+
── Python Packages ───────────────────────────
|
|
112
|
+
· + pip: httpx added 0.27.0
|
|
113
|
+
· ~ pip: requests upgraded 2.31.0 → 2.32.3
|
|
114
|
+
⚠ ~ pip: urllib3 downgraded 2.0.7 → 1.26.18
|
|
115
|
+
|
|
116
|
+
── Node.js Packages ──────────────────────────
|
|
117
|
+
· + npm dep: express added ^4.19.2
|
|
118
|
+
|
|
119
|
+
────────────────────────────────────────────
|
|
120
|
+
5 changes detected (1 critical, 1 warning, 3 info)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 🔁 Use in CI
|
|
126
|
+
|
|
127
|
+
The exit code tells your pipeline what happened:
|
|
128
|
+
|
|
129
|
+
| Exit code | Meaning |
|
|
130
|
+
|-----------|---------|
|
|
131
|
+
| `0` | No drift at or above `--fail-on` |
|
|
132
|
+
| `1` | Drift detected |
|
|
133
|
+
| `2` | Error (missing or invalid baseline, unreadable path) |
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
# GitHub Actions — fail only on critical drift, keep the report as an artifact
|
|
137
|
+
- name: Check environment drift
|
|
138
|
+
run: envlock check --baseline .envlock.json --fail-on critical --format markdown -o drift-report.md
|
|
139
|
+
- uses: actions/upload-artifact@v4
|
|
140
|
+
if: always()
|
|
141
|
+
with:
|
|
142
|
+
name: drift-report
|
|
143
|
+
path: drift-report.md
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 📋 Options
|
|
149
|
+
|
|
150
|
+
### `envlock snapshot`
|
|
151
|
+
| Option | Description |
|
|
152
|
+
|--------|-------------|
|
|
153
|
+
| `-p`, `--path DIR` | Project directory to scan (default: `.`) |
|
|
154
|
+
| `-o`, `--output FILE` | Where to save the baseline (default: `.envlock.json`) |
|
|
155
|
+
|
|
156
|
+
### `envlock check` / `envlock diff BASELINE CURRENT`
|
|
157
|
+
| Option | Description |
|
|
158
|
+
|--------|-------------|
|
|
159
|
+
| `-b`, `--baseline FILE` | *(check)* Baseline to compare against (default: `.envlock.json`) |
|
|
160
|
+
| `-p`, `--path DIR` | *(check)* Project directory to scan (default: `.`) |
|
|
161
|
+
| `-f`, `--format` | `terminal` (default), `json` or `markdown` |
|
|
162
|
+
| `-o`, `--output FILE` | Write the report to a file (JSON or Markdown) |
|
|
163
|
+
| `--fail-on LEVEL` | `info` (default: any change), `warning`, `critical` or `never` |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 🧪 Development
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
pip install -e ".[dev]"
|
|
171
|
+
ruff check .
|
|
172
|
+
pytest
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 📝 License
|
|
180
|
+
|
|
181
|
+
MIT — see [LICENSE](LICENSE).
|
|
182
|
+
|
|
183
|
+
## 🌐 Connect
|
|
184
|
+
|
|
185
|
+
[](https://github.com/serber1990)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# envlock
|
|
2
|
+
|
|
3
|
+
[](https://github.com/serber1990/envlock/actions/workflows/ci.yml)
|
|
4
|
+
[](https://badge.fury.io/py/envlock-cli)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Freeze your environment. Detect when it drifts.
|
|
8
|
+
|
|
9
|
+
**envlock** snapshots a project's Python packages, Node.js dependencies and system runtimes, then tells you
|
|
10
|
+
exactly what changed — so "works on my machine" stops being an excuse. Use it locally, in CI, or to compare
|
|
11
|
+
staging with production.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ What it tracks
|
|
16
|
+
|
|
17
|
+
| Layer | What's captured |
|
|
18
|
+
|-------|----------------|
|
|
19
|
+
| **Python** | Interpreter and version (the project's `.venv` / `venv` / `env`, or the active virtualenv), installed packages, `requirements.txt`, `pyproject.toml` dependencies |
|
|
20
|
+
| **Node.js** | Node and npm versions, `package.json` dependencies + devDependencies, `package-lock.json` resolved versions, `yarn.lock` presence |
|
|
21
|
+
| **System** | OS, architecture, Go / Ruby / Java / Rust versions, relevant env vars (`VIRTUAL_ENV`, `NODE_ENV`, `JAVA_HOME`…) |
|
|
22
|
+
|
|
23
|
+
### Severity levels
|
|
24
|
+
|
|
25
|
+
| Severity | Examples |
|
|
26
|
+
|----------|----------|
|
|
27
|
+
| ⛔ **CRITICAL** | Python/Node major or minor version changed (3.12 → 3.13), OS or architecture changed |
|
|
28
|
+
| ⚠️ **WARNING** | Package removed or downgraded, declared constraint changed, runtime patch release, lockfile or env var removed |
|
|
29
|
+
| ℹ️ **INFO** | Package added or upgraded, new lockfile or env var |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 📥 Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install envlock-cli # the command is `envlock`
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🛠 Usage
|
|
42
|
+
|
|
43
|
+
### 1 — Take a baseline snapshot
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
envlock snapshot # saves .envlock.json in the current directory
|
|
47
|
+
envlock snapshot --path /srv/myapp # scan another project
|
|
48
|
+
envlock snapshot --output locks/prod.json # custom location
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2 — Check for drift
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
envlock check # terminal report
|
|
55
|
+
envlock check --format json | jq .summary # JSON on stdout (progress goes to stderr)
|
|
56
|
+
envlock check --format markdown -o drift-report.md # Markdown report file
|
|
57
|
+
envlock check --baseline locks/prod.json --fail-on critical
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3 — Diff any two snapshots
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
envlock diff staging.json production.json
|
|
64
|
+
envlock diff before.json after.json --format markdown -o drift.md
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 📄 Example output
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
╔════════════════════════════════════════╗
|
|
73
|
+
║ envlock · environment drift report ║
|
|
74
|
+
╚════════════════════════════════════════╝
|
|
75
|
+
|
|
76
|
+
Baseline 2026-05-10T09:00:00+00:00 (dev-laptop) /srv/app
|
|
77
|
+
Current 2026-05-12T14:22:18+00:00 (dev-laptop) /srv/app
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
── Python Runtime ────────────────────────────
|
|
81
|
+
⛔ ~ Python version changed 3.12.1 → 3.13.0
|
|
82
|
+
|
|
83
|
+
── Python Packages ───────────────────────────
|
|
84
|
+
· + pip: httpx added 0.27.0
|
|
85
|
+
· ~ pip: requests upgraded 2.31.0 → 2.32.3
|
|
86
|
+
⚠ ~ pip: urllib3 downgraded 2.0.7 → 1.26.18
|
|
87
|
+
|
|
88
|
+
── Node.js Packages ──────────────────────────
|
|
89
|
+
· + npm dep: express added ^4.19.2
|
|
90
|
+
|
|
91
|
+
────────────────────────────────────────────
|
|
92
|
+
5 changes detected (1 critical, 1 warning, 3 info)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 🔁 Use in CI
|
|
98
|
+
|
|
99
|
+
The exit code tells your pipeline what happened:
|
|
100
|
+
|
|
101
|
+
| Exit code | Meaning |
|
|
102
|
+
|-----------|---------|
|
|
103
|
+
| `0` | No drift at or above `--fail-on` |
|
|
104
|
+
| `1` | Drift detected |
|
|
105
|
+
| `2` | Error (missing or invalid baseline, unreadable path) |
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
# GitHub Actions — fail only on critical drift, keep the report as an artifact
|
|
109
|
+
- name: Check environment drift
|
|
110
|
+
run: envlock check --baseline .envlock.json --fail-on critical --format markdown -o drift-report.md
|
|
111
|
+
- uses: actions/upload-artifact@v4
|
|
112
|
+
if: always()
|
|
113
|
+
with:
|
|
114
|
+
name: drift-report
|
|
115
|
+
path: drift-report.md
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 📋 Options
|
|
121
|
+
|
|
122
|
+
### `envlock snapshot`
|
|
123
|
+
| Option | Description |
|
|
124
|
+
|--------|-------------|
|
|
125
|
+
| `-p`, `--path DIR` | Project directory to scan (default: `.`) |
|
|
126
|
+
| `-o`, `--output FILE` | Where to save the baseline (default: `.envlock.json`) |
|
|
127
|
+
|
|
128
|
+
### `envlock check` / `envlock diff BASELINE CURRENT`
|
|
129
|
+
| Option | Description |
|
|
130
|
+
|--------|-------------|
|
|
131
|
+
| `-b`, `--baseline FILE` | *(check)* Baseline to compare against (default: `.envlock.json`) |
|
|
132
|
+
| `-p`, `--path DIR` | *(check)* Project directory to scan (default: `.`) |
|
|
133
|
+
| `-f`, `--format` | `terminal` (default), `json` or `markdown` |
|
|
134
|
+
| `-o`, `--output FILE` | Write the report to a file (JSON or Markdown) |
|
|
135
|
+
| `--fail-on LEVEL` | `info` (default: any change), `warning`, `critical` or `never` |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 🧪 Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pip install -e ".[dev]"
|
|
143
|
+
ruff check .
|
|
144
|
+
pytest
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
See [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 📝 License
|
|
152
|
+
|
|
153
|
+
MIT — see [LICENSE](LICENSE).
|
|
154
|
+
|
|
155
|
+
## 🌐 Connect
|
|
156
|
+
|
|
157
|
+
[](https://github.com/serber1990)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
envlock — freeze a project's Python/Node/system environment and detect drift.
|
|
4
|
+
|
|
5
|
+
Exit codes: 0 no drift (at or above --fail-on) 1 drift detected 2 usage or I/O error
|
|
6
|
+
"""
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from shellcolorize import Color
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .diff import diff_snapshots
|
|
15
|
+
from .renderer import render_json, render_markdown, render_terminal
|
|
16
|
+
from .snapshot import DEFAULT_PATH, EnvSnapshot, take_snapshot
|
|
17
|
+
|
|
18
|
+
EXIT_OK, EXIT_DRIFT, EXIT_ERROR = 0, 1, 2
|
|
19
|
+
FAIL_LEVELS = {'info': 'INFO', 'warning': 'WARNING', 'critical': 'CRITICAL'}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Progress and errors go to stderr so stdout only carries the report (safe to pipe into jq).
|
|
23
|
+
def _ok(msg): print(f" {Color.GREEN}✔{Color.RESET} {msg}", file=sys.stderr)
|
|
24
|
+
def _step(msg): print(f" {Color.CYAN}▶{Color.RESET} {msg}", file=sys.stderr)
|
|
25
|
+
def _err(msg): print(f" {Color.RED}✖{Color.RESET} {msg}", file=sys.stderr)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── snapshot ──────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
def cmd_snapshot(args: argparse.Namespace) -> int:
|
|
31
|
+
output = Path(args.output) if args.output else DEFAULT_PATH
|
|
32
|
+
path = Path(args.path)
|
|
33
|
+
if not path.is_dir():
|
|
34
|
+
_err(f'Not a directory: {path}')
|
|
35
|
+
return EXIT_ERROR
|
|
36
|
+
|
|
37
|
+
print(f"\n {Color.CYAN}{Color.BOLD}envlock · snapshot{Color.RESET}\n", file=sys.stderr)
|
|
38
|
+
_step(f'Scanning environment at {path.resolve()}...')
|
|
39
|
+
snap = take_snapshot(path=str(path))
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
snap.save(output)
|
|
43
|
+
except OSError as e:
|
|
44
|
+
_err(f'Cannot write {output}: {e.strerror}')
|
|
45
|
+
return EXIT_ERROR
|
|
46
|
+
|
|
47
|
+
py, nd, system = snap.python, snap.node, snap.system
|
|
48
|
+
n_pkgs = len(py.get('packages', {}).get('pip_installed', {}))
|
|
49
|
+
print()
|
|
50
|
+
_ok(f'Baseline saved: {output}')
|
|
51
|
+
print(f" Python : {py.get('python_version') or 'not found'} "
|
|
52
|
+
f"({n_pkgs} packages, {py.get('interpreter') or 'no interpreter'})")
|
|
53
|
+
print(f" Node : {nd.get('node_version') or 'not found'} "
|
|
54
|
+
f"(files: {', '.join(nd.get('lockfiles', [])) or 'none'})")
|
|
55
|
+
print(f" OS : {system.get('os') or '?'} [{system.get('arch') or '?'}]")
|
|
56
|
+
print(f" Runtimes : {', '.join(system.get('runtimes', {})) or 'none detected'}")
|
|
57
|
+
print()
|
|
58
|
+
return EXIT_OK
|
|
59
|
+
|
|
60
|
+
# ── check / diff ──────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
def _load(path: Path):
|
|
63
|
+
try:
|
|
64
|
+
return EnvSnapshot.load(path)
|
|
65
|
+
except FileNotFoundError:
|
|
66
|
+
_err(f'Snapshot not found: {path}')
|
|
67
|
+
except (OSError, ValueError) as e:
|
|
68
|
+
_err(f'Cannot read snapshot {path}: {e}')
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _report(report, args: argparse.Namespace) -> int:
|
|
73
|
+
fmt = args.format
|
|
74
|
+
if fmt == 'terminal' and args.output:
|
|
75
|
+
fmt = 'markdown' if str(args.output).endswith('.md') else 'json'
|
|
76
|
+
|
|
77
|
+
if fmt == 'terminal':
|
|
78
|
+
render_terminal(report)
|
|
79
|
+
else:
|
|
80
|
+
text = render_json(report) if fmt == 'json' else render_markdown(report)
|
|
81
|
+
if args.output:
|
|
82
|
+
try:
|
|
83
|
+
Path(args.output).write_text(text + '\n', encoding='utf-8')
|
|
84
|
+
except OSError as e:
|
|
85
|
+
_err(f'Cannot write {args.output}: {e.strerror}')
|
|
86
|
+
return EXIT_ERROR
|
|
87
|
+
_ok(f'Report saved to {args.output}')
|
|
88
|
+
else:
|
|
89
|
+
print(text)
|
|
90
|
+
|
|
91
|
+
if args.fail_on == 'never':
|
|
92
|
+
return EXIT_OK
|
|
93
|
+
return EXIT_DRIFT if report.has_at_least(FAIL_LEVELS[args.fail_on]) else EXIT_OK
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cmd_check(args: argparse.Namespace) -> int:
|
|
97
|
+
baseline_path = Path(args.baseline) if args.baseline else DEFAULT_PATH
|
|
98
|
+
if not baseline_path.exists():
|
|
99
|
+
_err(f'Baseline not found: {baseline_path}')
|
|
100
|
+
_err("Run 'envlock snapshot' first to create one.")
|
|
101
|
+
return EXIT_ERROR
|
|
102
|
+
baseline = _load(baseline_path)
|
|
103
|
+
if baseline is None:
|
|
104
|
+
return EXIT_ERROR
|
|
105
|
+
|
|
106
|
+
_step('Collecting current environment...')
|
|
107
|
+
current = take_snapshot(path=args.path)
|
|
108
|
+
return _report(diff_snapshots(baseline, current), args)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cmd_diff(args: argparse.Namespace) -> int:
|
|
112
|
+
snap_a, snap_b = _load(Path(args.snapshot_a)), _load(Path(args.snapshot_b))
|
|
113
|
+
if snap_a is None or snap_b is None:
|
|
114
|
+
return EXIT_ERROR
|
|
115
|
+
return _report(diff_snapshots(snap_a, snap_b), args)
|
|
116
|
+
|
|
117
|
+
# ── main ──────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
def _add_report_options(p: argparse.ArgumentParser) -> None:
|
|
120
|
+
p.add_argument('--format', '-f', choices=['terminal', 'json', 'markdown'], default='terminal',
|
|
121
|
+
help='Report format (default: terminal)')
|
|
122
|
+
p.add_argument('--output', '-o', metavar='FILE',
|
|
123
|
+
help='Write the report to FILE (json or markdown; inferred from .md/.json if needed)')
|
|
124
|
+
p.add_argument('--fail-on', choices=['info', 'warning', 'critical', 'never'], default='info',
|
|
125
|
+
help='Lowest severity that makes the exit code 1 (default: info = any change)')
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
129
|
+
parser = argparse.ArgumentParser(
|
|
130
|
+
prog='envlock',
|
|
131
|
+
description='Freeze Python, Node and system environments — detect drift before it breaks your builds.',
|
|
132
|
+
epilog='exit codes: 0 no drift · 1 drift detected · 2 error',
|
|
133
|
+
)
|
|
134
|
+
parser.add_argument('-v', '--version', action='version', version=f'envlock {__version__}')
|
|
135
|
+
sub = parser.add_subparsers(dest='command', metavar='COMMAND')
|
|
136
|
+
|
|
137
|
+
p_snap = sub.add_parser('snapshot', help='Capture the current environment as baseline')
|
|
138
|
+
p_snap.add_argument('--path', '-p', metavar='DIR', default='.',
|
|
139
|
+
help='Project directory to scan (default: current dir)')
|
|
140
|
+
p_snap.add_argument('--output', '-o', metavar='FILE',
|
|
141
|
+
help=f'Output path (default: {DEFAULT_PATH})')
|
|
142
|
+
|
|
143
|
+
p_check = sub.add_parser('check', help='Compare the current environment against the baseline')
|
|
144
|
+
p_check.add_argument('--baseline', '-b', metavar='FILE',
|
|
145
|
+
help=f'Baseline to compare against (default: {DEFAULT_PATH})')
|
|
146
|
+
p_check.add_argument('--path', '-p', metavar='DIR', default='.',
|
|
147
|
+
help='Project directory to scan (default: current dir)')
|
|
148
|
+
_add_report_options(p_check)
|
|
149
|
+
|
|
150
|
+
p_diff = sub.add_parser('diff', help='Compare two snapshot files')
|
|
151
|
+
p_diff.add_argument('snapshot_a', metavar='BASELINE')
|
|
152
|
+
p_diff.add_argument('snapshot_b', metavar='CURRENT')
|
|
153
|
+
_add_report_options(p_diff)
|
|
154
|
+
return parser
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main(argv=None) -> None:
|
|
158
|
+
parser = build_parser()
|
|
159
|
+
args = parser.parse_args(argv)
|
|
160
|
+
Color.auto()
|
|
161
|
+
|
|
162
|
+
if args.command is None:
|
|
163
|
+
parser.print_help()
|
|
164
|
+
sys.exit(EXIT_OK)
|
|
165
|
+
|
|
166
|
+
dispatch = {'snapshot': cmd_snapshot, 'check': cmd_check, 'diff': cmd_diff}
|
|
167
|
+
try:
|
|
168
|
+
sys.exit(dispatch[args.command](args))
|
|
169
|
+
except KeyboardInterrupt:
|
|
170
|
+
_err('Interrupted')
|
|
171
|
+
sys.exit(130)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == '__main__':
|
|
175
|
+
main()
|