devit-cli 0.1.5__tar.gz → 0.1.7__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 (58) hide show
  1. {devit_cli-0.1.5/devit_cli.egg-info → devit_cli-0.1.7}/PKG-INFO +87 -2
  2. {devit_cli-0.1.5 → devit_cli-0.1.7}/README.md +86 -1
  3. {devit_cli-0.1.5 → devit_cli-0.1.7/devit_cli.egg-info}/PKG-INFO +87 -2
  4. {devit_cli-0.1.5 → devit_cli-0.1.7}/devit_cli.egg-info/SOURCES.txt +1 -0
  5. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/__init__.py +1 -1
  6. devit_cli-0.1.7/devkit_cli/commands/deps.py +732 -0
  7. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/main.py +2 -0
  8. {devit_cli-0.1.5 → devit_cli-0.1.7}/pyproject.toml +1 -1
  9. {devit_cli-0.1.5 → devit_cli-0.1.7}/LICENSE +0 -0
  10. {devit_cli-0.1.5 → devit_cli-0.1.7}/_devkit_entry.py +0 -0
  11. {devit_cli-0.1.5 → devit_cli-0.1.7}/devit_cli.egg-info/dependency_links.txt +0 -0
  12. {devit_cli-0.1.5 → devit_cli-0.1.7}/devit_cli.egg-info/entry_points.txt +0 -0
  13. {devit_cli-0.1.5 → devit_cli-0.1.7}/devit_cli.egg-info/requires.txt +0 -0
  14. {devit_cli-0.1.5 → devit_cli-0.1.7}/devit_cli.egg-info/top_level.txt +0 -0
  15. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/commands/__init__.py +0 -0
  16. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/commands/archive.py +0 -0
  17. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/commands/clean.py +0 -0
  18. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/commands/env.py +0 -0
  19. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/commands/find.py +0 -0
  20. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/commands/info.py +0 -0
  21. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/commands/init.py +0 -0
  22. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/commands/run.py +0 -0
  23. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/aws/.gitignore +0 -0
  24. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/aws/README.md +0 -0
  25. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/aws/requirements.txt +0 -0
  26. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/aws/scripts/__init__.py +0 -0
  27. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/aws/scripts/ec2.py +0 -0
  28. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/aws/scripts/main.py +0 -0
  29. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/aws/scripts/s3.py +0 -0
  30. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/aws/tests/test_scripts.py +0 -0
  31. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/.gitignore +0 -0
  32. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/README.md +0 -0
  33. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/apps/__init__.py +0 -0
  34. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/apps/core/__init__.py +0 -0
  35. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/apps/core/apps.py +0 -0
  36. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/apps/core/urls.py +0 -0
  37. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/apps/core/views.py +0 -0
  38. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/manage.py +0 -0
  39. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/requirements.txt +0 -0
  40. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/{{module_name}}/__init__.py +0 -0
  41. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/{{module_name}}/settings.py +0 -0
  42. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/{{module_name}}/urls.py +0 -0
  43. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/django/{{module_name}}/wsgi.py +0 -0
  44. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/fastapi/.gitignore +0 -0
  45. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/fastapi/README.md +0 -0
  46. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/fastapi/app/__init__.py +0 -0
  47. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/fastapi/app/routers/__init__.py +0 -0
  48. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/fastapi/app/routers/health.py +0 -0
  49. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/fastapi/main.py +0 -0
  50. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/fastapi/requirements.txt +0 -0
  51. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/fastapi/tests/test_api.py +0 -0
  52. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/package/.gitignore +0 -0
  53. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/package/README.md +0 -0
  54. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/package/pyproject.toml +0 -0
  55. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/package/tests/test_core.py +0 -0
  56. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/package/{{module_name}}/__init__.py +0 -0
  57. {devit_cli-0.1.5 → devit_cli-0.1.7}/devkit_cli/templates/package/{{module_name}}/core.py +0 -0
  58. {devit_cli-0.1.5 → devit_cli-0.1.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devit-cli
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: A full-featured CLI framework for professional Python developers
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/dipenpadhiyar/devit-cli
@@ -35,7 +35,7 @@ Dynamic: license-file
35
35
 
36
36
  <p align="center">
37
37
  <b>A full-featured CLI toolkit for professional Python developers.</b><br/>
38
- Scaffold projects &nbsp;·&nbsp; Clean builds &nbsp;·&nbsp; Inspect system &nbsp;·&nbsp; Search files &nbsp;·&nbsp; Manage archives &amp; env vars
38
+ Scaffold projects &nbsp;·&nbsp; Clean builds &nbsp;·&nbsp; Inspect system &nbsp;·&nbsp; Search files &nbsp;·&nbsp; Manage archives &amp; env vars &nbsp;·&nbsp; Track &amp; rollback dependencies
39
39
  </p>
40
40
 
41
41
  <p align="center">
@@ -67,6 +67,7 @@ devit info # system snapshot
67
67
  devit clean # remove caches & build artifacts
68
68
  devit dev # start dev server / install in dev mode
69
69
  devit test # run tests (auto-detects pytest / django)
70
+ devit deps # manage, snapshot & rollback dependencies
70
71
  ```
71
72
 
72
73
  ---
@@ -236,6 +237,90 @@ devit env diff .env .env.production # show what changed
236
237
 
237
238
  ---
238
239
 
240
+ ### `devit deps` — Dependency manager with history & rollback
241
+
242
+ Wraps `pip` with a clean interface, tracks dependency snapshots, and lets you roll back when an upgrade breaks things.
243
+
244
+ #### Install & uninstall
245
+
246
+ ```bash
247
+ devit deps add requests # install + save to requirements.txt
248
+ devit deps add "flask>=3.0" sqlalchemy # multiple packages
249
+ devit deps add numpy --no-save # install without touching requirements.txt
250
+ devit deps remove flask # uninstall + remove from requirements.txt
251
+ ```
252
+
253
+ #### Inspect
254
+
255
+ ```bash
256
+ devit deps list # list all installed packages in the project env
257
+ devit deps list --json # JSON output
258
+ devit deps outdated # show packages that have newer versions available
259
+ devit deps outdated --json
260
+ ```
261
+
262
+ #### Upgrade all at once
263
+
264
+ ```bash
265
+ devit deps add . # upgrade every outdated package & update requirements.txt
266
+ ```
267
+
268
+ #### Snapshot history
269
+
270
+ Save the current working state before making changes:
271
+
272
+ ```bash
273
+ devit deps snapshot # save current packages
274
+ devit deps snapshot -m "works with v3.0" # save with a label
275
+ devit deps history # list all saved snapshots
276
+ ```
277
+
278
+ Example output of `devit deps history`:
279
+
280
+ ```
281
+ ┏━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━
282
+ ┃ ID ┃ Message ┃ Packages ┃ Saved At
283
+ ┡━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━
284
+ │ #2 │ works with v3.0 │ 14 │ 2026-04-03T12:00:00
285
+ │ #1 │ Snapshot #1 │ 12 │ 2026-04-01T09:30:00
286
+ ```
287
+
288
+ #### Diff — see what changed and spot issues
289
+
290
+ ```bash
291
+ devit deps diff # compare current env to latest snapshot
292
+ devit deps diff 1 # compare to snapshot #1
293
+ ```
294
+
295
+ Example output when an upgrade broke something:
296
+
297
+ ```
298
+ Diff: current vs Snapshot #1
299
+ ┏━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━
300
+ ┃ Package ┃ Snapshot ┃ Current ┃ Status
301
+ ┡━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━
302
+ │ flask │ 2.3.3 │ 3.1.0 │ ~changed
303
+ │ werkzeug │ 2.3.7 │ — │ -removed
304
+
305
+ Issues detected:
306
+ • flask version changed 2.3.3 → 3.1.0
307
+ • werkzeug was removed (snapshot had 2.3.7)
308
+
309
+ Run devit deps rollback 1 to restore this snapshot.
310
+ ```
311
+
312
+ #### Rollback — restore exact pinned versions
313
+
314
+ ```bash
315
+ devit deps rollback # restore latest snapshot
316
+ devit deps rollback 1 # restore snapshot #1
317
+ devit deps rollback 1 -y # skip confirmation
318
+ ```
319
+
320
+ Snapshots are stored in `.devit/dep_snapshots.json` inside your project directory.
321
+
322
+ ---
323
+
239
324
  ## Tech Stack
240
325
 
241
326
  | Library | Purpose |
@@ -4,7 +4,7 @@
4
4
 
5
5
  <p align="center">
6
6
  <b>A full-featured CLI toolkit for professional Python developers.</b><br/>
7
- Scaffold projects &nbsp;·&nbsp; Clean builds &nbsp;·&nbsp; Inspect system &nbsp;·&nbsp; Search files &nbsp;·&nbsp; Manage archives &amp; env vars
7
+ Scaffold projects &nbsp;·&nbsp; Clean builds &nbsp;·&nbsp; Inspect system &nbsp;·&nbsp; Search files &nbsp;·&nbsp; Manage archives &amp; env vars &nbsp;·&nbsp; Track &amp; rollback dependencies
8
8
  </p>
9
9
 
10
10
  <p align="center">
@@ -36,6 +36,7 @@ devit info # system snapshot
36
36
  devit clean # remove caches & build artifacts
37
37
  devit dev # start dev server / install in dev mode
38
38
  devit test # run tests (auto-detects pytest / django)
39
+ devit deps # manage, snapshot & rollback dependencies
39
40
  ```
40
41
 
41
42
  ---
@@ -205,6 +206,90 @@ devit env diff .env .env.production # show what changed
205
206
 
206
207
  ---
207
208
 
209
+ ### `devit deps` — Dependency manager with history & rollback
210
+
211
+ Wraps `pip` with a clean interface, tracks dependency snapshots, and lets you roll back when an upgrade breaks things.
212
+
213
+ #### Install & uninstall
214
+
215
+ ```bash
216
+ devit deps add requests # install + save to requirements.txt
217
+ devit deps add "flask>=3.0" sqlalchemy # multiple packages
218
+ devit deps add numpy --no-save # install without touching requirements.txt
219
+ devit deps remove flask # uninstall + remove from requirements.txt
220
+ ```
221
+
222
+ #### Inspect
223
+
224
+ ```bash
225
+ devit deps list # list all installed packages in the project env
226
+ devit deps list --json # JSON output
227
+ devit deps outdated # show packages that have newer versions available
228
+ devit deps outdated --json
229
+ ```
230
+
231
+ #### Upgrade all at once
232
+
233
+ ```bash
234
+ devit deps add . # upgrade every outdated package & update requirements.txt
235
+ ```
236
+
237
+ #### Snapshot history
238
+
239
+ Save the current working state before making changes:
240
+
241
+ ```bash
242
+ devit deps snapshot # save current packages
243
+ devit deps snapshot -m "works with v3.0" # save with a label
244
+ devit deps history # list all saved snapshots
245
+ ```
246
+
247
+ Example output of `devit deps history`:
248
+
249
+ ```
250
+ ┏━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━
251
+ ┃ ID ┃ Message ┃ Packages ┃ Saved At
252
+ ┡━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━
253
+ │ #2 │ works with v3.0 │ 14 │ 2026-04-03T12:00:00
254
+ │ #1 │ Snapshot #1 │ 12 │ 2026-04-01T09:30:00
255
+ ```
256
+
257
+ #### Diff — see what changed and spot issues
258
+
259
+ ```bash
260
+ devit deps diff # compare current env to latest snapshot
261
+ devit deps diff 1 # compare to snapshot #1
262
+ ```
263
+
264
+ Example output when an upgrade broke something:
265
+
266
+ ```
267
+ Diff: current vs Snapshot #1
268
+ ┏━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━
269
+ ┃ Package ┃ Snapshot ┃ Current ┃ Status
270
+ ┡━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━
271
+ │ flask │ 2.3.3 │ 3.1.0 │ ~changed
272
+ │ werkzeug │ 2.3.7 │ — │ -removed
273
+
274
+ Issues detected:
275
+ • flask version changed 2.3.3 → 3.1.0
276
+ • werkzeug was removed (snapshot had 2.3.7)
277
+
278
+ Run devit deps rollback 1 to restore this snapshot.
279
+ ```
280
+
281
+ #### Rollback — restore exact pinned versions
282
+
283
+ ```bash
284
+ devit deps rollback # restore latest snapshot
285
+ devit deps rollback 1 # restore snapshot #1
286
+ devit deps rollback 1 -y # skip confirmation
287
+ ```
288
+
289
+ Snapshots are stored in `.devit/dep_snapshots.json` inside your project directory.
290
+
291
+ ---
292
+
208
293
  ## Tech Stack
209
294
 
210
295
  | Library | Purpose |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devit-cli
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: A full-featured CLI framework for professional Python developers
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/dipenpadhiyar/devit-cli
@@ -35,7 +35,7 @@ Dynamic: license-file
35
35
 
36
36
  <p align="center">
37
37
  <b>A full-featured CLI toolkit for professional Python developers.</b><br/>
38
- Scaffold projects &nbsp;·&nbsp; Clean builds &nbsp;·&nbsp; Inspect system &nbsp;·&nbsp; Search files &nbsp;·&nbsp; Manage archives &amp; env vars
38
+ Scaffold projects &nbsp;·&nbsp; Clean builds &nbsp;·&nbsp; Inspect system &nbsp;·&nbsp; Search files &nbsp;·&nbsp; Manage archives &amp; env vars &nbsp;·&nbsp; Track &amp; rollback dependencies
39
39
  </p>
40
40
 
41
41
  <p align="center">
@@ -67,6 +67,7 @@ devit info # system snapshot
67
67
  devit clean # remove caches & build artifacts
68
68
  devit dev # start dev server / install in dev mode
69
69
  devit test # run tests (auto-detects pytest / django)
70
+ devit deps # manage, snapshot & rollback dependencies
70
71
  ```
71
72
 
72
73
  ---
@@ -236,6 +237,90 @@ devit env diff .env .env.production # show what changed
236
237
 
237
238
  ---
238
239
 
240
+ ### `devit deps` — Dependency manager with history & rollback
241
+
242
+ Wraps `pip` with a clean interface, tracks dependency snapshots, and lets you roll back when an upgrade breaks things.
243
+
244
+ #### Install & uninstall
245
+
246
+ ```bash
247
+ devit deps add requests # install + save to requirements.txt
248
+ devit deps add "flask>=3.0" sqlalchemy # multiple packages
249
+ devit deps add numpy --no-save # install without touching requirements.txt
250
+ devit deps remove flask # uninstall + remove from requirements.txt
251
+ ```
252
+
253
+ #### Inspect
254
+
255
+ ```bash
256
+ devit deps list # list all installed packages in the project env
257
+ devit deps list --json # JSON output
258
+ devit deps outdated # show packages that have newer versions available
259
+ devit deps outdated --json
260
+ ```
261
+
262
+ #### Upgrade all at once
263
+
264
+ ```bash
265
+ devit deps add . # upgrade every outdated package & update requirements.txt
266
+ ```
267
+
268
+ #### Snapshot history
269
+
270
+ Save the current working state before making changes:
271
+
272
+ ```bash
273
+ devit deps snapshot # save current packages
274
+ devit deps snapshot -m "works with v3.0" # save with a label
275
+ devit deps history # list all saved snapshots
276
+ ```
277
+
278
+ Example output of `devit deps history`:
279
+
280
+ ```
281
+ ┏━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━
282
+ ┃ ID ┃ Message ┃ Packages ┃ Saved At
283
+ ┡━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━
284
+ │ #2 │ works with v3.0 │ 14 │ 2026-04-03T12:00:00
285
+ │ #1 │ Snapshot #1 │ 12 │ 2026-04-01T09:30:00
286
+ ```
287
+
288
+ #### Diff — see what changed and spot issues
289
+
290
+ ```bash
291
+ devit deps diff # compare current env to latest snapshot
292
+ devit deps diff 1 # compare to snapshot #1
293
+ ```
294
+
295
+ Example output when an upgrade broke something:
296
+
297
+ ```
298
+ Diff: current vs Snapshot #1
299
+ ┏━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━
300
+ ┃ Package ┃ Snapshot ┃ Current ┃ Status
301
+ ┡━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━
302
+ │ flask │ 2.3.3 │ 3.1.0 │ ~changed
303
+ │ werkzeug │ 2.3.7 │ — │ -removed
304
+
305
+ Issues detected:
306
+ • flask version changed 2.3.3 → 3.1.0
307
+ • werkzeug was removed (snapshot had 2.3.7)
308
+
309
+ Run devit deps rollback 1 to restore this snapshot.
310
+ ```
311
+
312
+ #### Rollback — restore exact pinned versions
313
+
314
+ ```bash
315
+ devit deps rollback # restore latest snapshot
316
+ devit deps rollback 1 # restore snapshot #1
317
+ devit deps rollback 1 -y # skip confirmation
318
+ ```
319
+
320
+ Snapshots are stored in `.devit/dep_snapshots.json` inside your project directory.
321
+
322
+ ---
323
+
239
324
  ## Tech Stack
240
325
 
241
326
  | Library | Purpose |
@@ -13,6 +13,7 @@ devkit_cli/main.py
13
13
  devkit_cli/commands/__init__.py
14
14
  devkit_cli/commands/archive.py
15
15
  devkit_cli/commands/clean.py
16
+ devkit_cli/commands/deps.py
16
17
  devkit_cli/commands/env.py
17
18
  devkit_cli/commands/find.py
18
19
  devkit_cli/commands/info.py
@@ -1,4 +1,4 @@
1
1
  """devkit-cli — A professional developer toolkit for the terminal."""
2
2
 
3
- __version__ = "0.1.5"
3
+ __version__ = "0.1.7"
4
4
  __author__ = "devkit-cli contributors"
@@ -0,0 +1,732 @@
1
+ """devit deps — Dependency manager (wraps pip with nice output)."""
2
+
3
+ import json
4
+ import re
5
+ import subprocess
6
+ import sys
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ import click
11
+ import questionary
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.progress import Progress, SpinnerColumn, TextColumn
15
+ from rich.table import Table
16
+
17
+ console = Console()
18
+
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Helpers
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def _venv_python(root: Path) -> str:
25
+ """Return the venv python for the project, or sys.executable as fallback."""
26
+ venv = root / ".venv"
27
+ if venv.exists():
28
+ win_py = venv / "Scripts" / "python.exe"
29
+ unix_py = venv / "bin" / "python"
30
+ if win_py.exists():
31
+ return str(win_py)
32
+ if unix_py.exists():
33
+ return str(unix_py)
34
+ return sys.executable
35
+
36
+
37
+ def _pip(py: str, args: list[str], capture: bool = False) -> subprocess.CompletedProcess:
38
+ """Run pip via the given interpreter. Returns CompletedProcess."""
39
+ cmd = [py, "-m", "pip"] + args
40
+ try:
41
+ return subprocess.run(cmd, capture_output=capture, text=True)
42
+ except FileNotFoundError:
43
+ console.print(f"[red]✗[/red] Python interpreter not found: [bold]{py}[/bold]")
44
+ raise SystemExit(1)
45
+
46
+
47
+ def _installed_version(py: str, name: str) -> str | None:
48
+ """Return the installed version of a package, or None if not found."""
49
+ r = _pip(py, ["show", name], capture=True)
50
+ if r.returncode != 0:
51
+ return None
52
+ for line in r.stdout.splitlines():
53
+ if line.startswith("Version:"):
54
+ return line.split(":", 1)[1].strip()
55
+ return None
56
+
57
+
58
+ def _bare_name(pkg_spec: str) -> str:
59
+ """Strip any version specifiers/extras from a package spec → bare name."""
60
+ return re.split(r"[=<>!~\[\s]", pkg_spec)[0].strip()
61
+
62
+
63
+ def _detect_req_file(root: Path) -> Path | None:
64
+ req = root / "requirements.txt"
65
+ return req if req.exists() else None
66
+
67
+
68
+ def _req_add(req_file: Path, name: str, version: str) -> None:
69
+ """Add or replace a package pin in requirements.txt."""
70
+ try:
71
+ lines = req_file.read_text(encoding="utf-8").splitlines()
72
+ except OSError as e:
73
+ raise click.ClickException(f"Cannot read {req_file.name}: {e}")
74
+
75
+ pattern = re.compile(r"^" + re.escape(name) + r"([=<>!~\[\s]|$)", re.IGNORECASE)
76
+ new_line = f"{name}=={version}"
77
+ new_lines, updated = [], False
78
+ for line in lines:
79
+ if pattern.match(line.strip()):
80
+ new_lines.append(new_line)
81
+ updated = True
82
+ else:
83
+ new_lines.append(line)
84
+ if not updated:
85
+ new_lines.append(new_line)
86
+
87
+ try:
88
+ req_file.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
89
+ except OSError as e:
90
+ raise click.ClickException(f"Cannot write {req_file.name}: {e}")
91
+
92
+
93
+ def _req_remove(req_file: Path, name: str) -> bool:
94
+ """Remove a package entry from requirements.txt. Returns True if found."""
95
+ try:
96
+ lines = req_file.read_text(encoding="utf-8").splitlines()
97
+ except OSError as e:
98
+ raise click.ClickException(f"Cannot read {req_file.name}: {e}")
99
+
100
+ pattern = re.compile(r"^" + re.escape(name) + r"([=<>!~\[\s]|$)", re.IGNORECASE)
101
+ new_lines = [l for l in lines if not pattern.match(l.strip())]
102
+ if len(new_lines) == len(lines):
103
+ return False
104
+
105
+ try:
106
+ req_file.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
107
+ except OSError as e:
108
+ raise click.ClickException(f"Cannot write {req_file.name}: {e}")
109
+ return True
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # CLI group
114
+ # ---------------------------------------------------------------------------
115
+
116
+ @click.group()
117
+ def deps():
118
+ """Manage project dependencies (wraps pip with nice output)."""
119
+ pass
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # devit deps add
124
+ # ---------------------------------------------------------------------------
125
+
126
+ @deps.command("add")
127
+ @click.argument("packages", nargs=-1, required=True)
128
+ @click.option("--no-save", is_flag=True, default=False,
129
+ help="Install without updating requirements.txt.")
130
+ @click.option("--dir", "project_dir", default=None,
131
+ help="Project directory (default: current directory).")
132
+ def deps_add(packages, no_save, project_dir):
133
+ """
134
+ Install package(s) and save to requirements.txt.
135
+ Use [bold].[/bold] as the package name to upgrade ALL outdated packages at once.
136
+
137
+ \b
138
+ Examples:
139
+ devit deps add requests
140
+ devit deps add "flask>=3.0" sqlalchemy
141
+ devit deps add numpy --no-save
142
+ devit deps add . # upgrade all outdated packages
143
+ """
144
+ root = Path(project_dir).resolve() if project_dir else Path.cwd()
145
+ py = _venv_python(root)
146
+
147
+ # ── special case: "devit deps add ." → upgrade all outdated ──────────
148
+ if list(packages) == ["."]:
149
+ console.print(Panel("[bold cyan]devit deps add .[/bold cyan] — Upgrading all outdated packages", expand=False))
150
+
151
+ with Progress(SpinnerColumn(), TextColumn("[dim]Checking for outdated packages...[/dim]"), transient=True) as p:
152
+ p.add_task("")
153
+ r = _pip(py, ["list", "--outdated", "--format=json"], capture=True)
154
+
155
+ if r.returncode != 0:
156
+ console.print("[red]✗[/red] Could not check for outdated packages.")
157
+ raise SystemExit(1)
158
+
159
+ try:
160
+ outdated = json.loads(r.stdout)
161
+ except json.JSONDecodeError:
162
+ console.print("[red]✗[/red] Failed to parse pip output.")
163
+ raise SystemExit(1)
164
+
165
+ if not outdated:
166
+ console.print("[green]✔[/green] All packages are already up to date.")
167
+ return
168
+
169
+ table = Table(title=f"Upgrading {len(outdated)} package(s)", show_lines=False)
170
+ table.add_column("Package", style="cyan")
171
+ table.add_column("Current", style="dim")
172
+ table.add_column("→ Latest", style="bold green")
173
+ for pkg in sorted(outdated, key=lambda p: p["name"].lower()):
174
+ table.add_row(pkg["name"], pkg["version"], pkg["latest_version"])
175
+ console.print(table)
176
+
177
+ names = [pkg["name"] for pkg in outdated]
178
+ result = _pip(py, ["install", "--upgrade"] + names)
179
+ if result.returncode != 0:
180
+ console.print("[red]✗[/red] Upgrade failed.")
181
+ raise SystemExit(result.returncode)
182
+
183
+ if not no_save:
184
+ req_file = _detect_req_file(root)
185
+ if req_file:
186
+ saved = []
187
+ for pkg in outdated:
188
+ new_ver = _installed_version(py, pkg["name"])
189
+ if new_ver:
190
+ try:
191
+ _req_add(req_file, pkg["name"], new_ver)
192
+ saved.append(f"{pkg['name']}=={new_ver}")
193
+ except click.ClickException as e:
194
+ console.print(f"[yellow]⚠[/yellow] {e.format_message()}")
195
+ if saved:
196
+ console.print(
197
+ f"[green]✔[/green] Updated [cyan]{req_file.name}[/cyan] with "
198
+ f"[bold]{len(saved)}[/bold] new version(s)."
199
+ )
200
+
201
+ console.print(f"[green]✔[/green] Upgraded [bold]{len(names)}[/bold] package(s) successfully.")
202
+ return
203
+ # ─────────────────────────────────────────────────────────────────────
204
+
205
+ console.print(Panel("[bold cyan]devit deps add[/bold cyan]", expand=False))
206
+
207
+ # Install via pip (stream output live so user sees progress)
208
+ result = _pip(py, ["install"] + list(packages))
209
+ if result.returncode != 0:
210
+ console.print("[red]✗[/red] Installation failed.")
211
+ raise SystemExit(result.returncode)
212
+
213
+ if no_save:
214
+ return
215
+
216
+ req_file = _detect_req_file(root)
217
+ if not req_file:
218
+ console.print(
219
+ "[dim]No requirements.txt found in this directory — "
220
+ "packages installed but not saved.[/dim]"
221
+ )
222
+ return
223
+
224
+ saved = []
225
+ for pkg in packages:
226
+ name = _bare_name(pkg)
227
+ version = _installed_version(py, name)
228
+ if not version:
229
+ console.print(f"[yellow]⚠[/yellow] Could not determine version for [bold]{name}[/bold] — skipping save.")
230
+ continue
231
+ try:
232
+ _req_add(req_file, name, version)
233
+ saved.append(f"{name}=={version}")
234
+ except click.ClickException as e:
235
+ console.print(f"[yellow]⚠[/yellow] {e.format_message()}")
236
+
237
+ if saved:
238
+ console.print(
239
+ f"[green]✔[/green] Saved to [cyan]{req_file.name}[/cyan]: "
240
+ + ", ".join(f"[bold]{s}[/bold]" for s in saved)
241
+ )
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # devit deps remove
246
+ # ---------------------------------------------------------------------------
247
+
248
+ @deps.command("remove")
249
+ @click.argument("packages", nargs=-1, required=True)
250
+ @click.option("--no-save", is_flag=True, default=False,
251
+ help="Uninstall without updating requirements.txt.")
252
+ @click.option("--dir", "project_dir", default=None,
253
+ help="Project directory (default: current directory).")
254
+ def deps_remove(packages, no_save, project_dir):
255
+ """
256
+ Uninstall package(s) and remove from requirements.txt.
257
+
258
+ \b
259
+ Examples:
260
+ devit deps remove requests
261
+ devit deps remove flask sqlalchemy
262
+ """
263
+ root = Path(project_dir).resolve() if project_dir else Path.cwd()
264
+ py = _venv_python(root)
265
+
266
+ console.print(Panel("[bold cyan]devit deps remove[/bold cyan]", expand=False))
267
+
268
+ result = _pip(py, ["uninstall", "-y"] + list(packages))
269
+ if result.returncode != 0:
270
+ console.print("[red]✗[/red] Uninstall failed.")
271
+ raise SystemExit(result.returncode)
272
+
273
+ if no_save:
274
+ return
275
+
276
+ req_file = _detect_req_file(root)
277
+ if not req_file:
278
+ return
279
+
280
+ removed = []
281
+ for pkg in packages:
282
+ name = _bare_name(pkg)
283
+ try:
284
+ found = _req_remove(req_file, name)
285
+ if found:
286
+ removed.append(name)
287
+ except click.ClickException as e:
288
+ console.print(f"[yellow]⚠[/yellow] {e.format_message()}")
289
+
290
+ if removed:
291
+ console.print(
292
+ f"[green]✔[/green] Removed from [cyan]{req_file.name}[/cyan]: "
293
+ + ", ".join(f"[bold]{n}[/bold]" for n in removed)
294
+ )
295
+
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # devit deps list
299
+ # ---------------------------------------------------------------------------
300
+
301
+ @deps.command("list")
302
+ @click.option("--dir", "project_dir", default=None,
303
+ help="Project directory (default: current directory).")
304
+ @click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON.")
305
+ def deps_list(project_dir, as_json):
306
+ """
307
+ List all packages installed in the project environment.
308
+
309
+ \b
310
+ Examples:
311
+ devit deps list
312
+ devit deps list --json
313
+ """
314
+ root = Path(project_dir).resolve() if project_dir else Path.cwd()
315
+ py = _venv_python(root)
316
+
317
+ result = _pip(py, ["list", "--format=json"], capture=True)
318
+ if result.returncode != 0:
319
+ console.print("[red]✗[/red] Could not retrieve package list.")
320
+ raise SystemExit(1)
321
+
322
+ try:
323
+ packages = json.loads(result.stdout)
324
+ except json.JSONDecodeError:
325
+ console.print("[red]✗[/red] Failed to parse pip output.")
326
+ raise SystemExit(1)
327
+
328
+ if as_json:
329
+ click.echo(json.dumps(packages, indent=2))
330
+ return
331
+
332
+ table = Table(title=f"Installed Packages ({len(packages)})", show_lines=False)
333
+ table.add_column("Package", style="cyan")
334
+ table.add_column("Version", style="bold green")
335
+
336
+ for pkg in sorted(packages, key=lambda p: p["name"].lower()):
337
+ table.add_row(pkg["name"], pkg["version"])
338
+
339
+ console.print(table)
340
+
341
+
342
+ # ---------------------------------------------------------------------------
343
+ # devit deps outdated
344
+ # ---------------------------------------------------------------------------
345
+
346
+ @deps.command("outdated")
347
+ @click.option("--dir", "project_dir", default=None,
348
+ help="Project directory (default: current directory).")
349
+ @click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON.")
350
+ def deps_outdated(project_dir, as_json):
351
+ """
352
+ Show packages that have newer versions available.
353
+
354
+ \b
355
+ Examples:
356
+ devit deps outdated
357
+ devit deps outdated --json
358
+ """
359
+ root = Path(project_dir).resolve() if project_dir else Path.cwd()
360
+ py = _venv_python(root)
361
+
362
+ with Progress(
363
+ SpinnerColumn(),
364
+ TextColumn("[dim]Checking for updates...[/dim]"),
365
+ transient=True,
366
+ ) as p:
367
+ p.add_task("")
368
+ result = _pip(py, ["list", "--outdated", "--format=json"], capture=True)
369
+
370
+ if result.returncode != 0:
371
+ console.print("[red]✗[/red] Could not check for updates.")
372
+ raise SystemExit(1)
373
+
374
+ try:
375
+ packages = json.loads(result.stdout)
376
+ except json.JSONDecodeError:
377
+ console.print("[red]✗[/red] Failed to parse pip output.")
378
+ raise SystemExit(1)
379
+
380
+ if not packages:
381
+ console.print("[green]✔[/green] All packages are up to date.")
382
+ return
383
+
384
+ if as_json:
385
+ click.echo(json.dumps(packages, indent=2))
386
+ return
387
+
388
+ table = Table(title=f"Outdated Packages ({len(packages)})", show_lines=False)
389
+ table.add_column("Package", style="cyan")
390
+ table.add_column("Installed", style="dim")
391
+ table.add_column("Latest", style="bold green")
392
+ table.add_column("Type", style="dim")
393
+
394
+ for pkg in sorted(packages, key=lambda p: p["name"].lower()):
395
+ table.add_row(
396
+ pkg["name"],
397
+ pkg["version"],
398
+ pkg["latest_version"],
399
+ pkg.get("latest_filetype", "wheel"),
400
+ )
401
+
402
+ console.print(table)
403
+ console.print(
404
+ f"\n[dim]Run [bold]devit deps add <name>[/bold] to upgrade one "
405
+ f"or [bold]devit deps add .[/bold] to upgrade all.[/dim]"
406
+ )
407
+
408
+
409
+ # ---------------------------------------------------------------------------
410
+ # Snapshot storage helpers
411
+ # ---------------------------------------------------------------------------
412
+
413
+ _SNAPSHOTS_FILE = Path(".devit") / "dep_snapshots.json"
414
+
415
+
416
+ def _snapshots_path(root: Path) -> Path:
417
+ return root / _SNAPSHOTS_FILE
418
+
419
+
420
+ def _load_snapshots(root: Path) -> list[dict]:
421
+ path = _snapshots_path(root)
422
+ if not path.exists():
423
+ return []
424
+ try:
425
+ data = json.loads(path.read_text(encoding="utf-8"))
426
+ if not isinstance(data, list):
427
+ raise ValueError("expected a list")
428
+ return data
429
+ except (json.JSONDecodeError, ValueError):
430
+ console.print(
431
+ f"[yellow]⚠[/yellow] Snapshot file is corrupted ([dim]{path}[/dim]) — "
432
+ "existing history could not be loaded."
433
+ )
434
+ return []
435
+ except OSError as e:
436
+ console.print(f"[yellow]⚠[/yellow] Could not read snapshot file: {e}")
437
+ return []
438
+
439
+
440
+ def _save_snapshots(root: Path, snapshots: list[dict]) -> None:
441
+ path = _snapshots_path(root)
442
+ try:
443
+ path.parent.mkdir(parents=True, exist_ok=True)
444
+ path.write_text(json.dumps(snapshots, indent=2), encoding="utf-8")
445
+ except OSError as e:
446
+ raise click.ClickException(f"Cannot save snapshots: {e}")
447
+
448
+
449
+ def _get_current_packages(py: str) -> list[dict]:
450
+ result = _pip(py, ["list", "--format=json"], capture=True)
451
+ if result.returncode != 0:
452
+ console.print("[red]\u2717[/red] Could not retrieve package list.")
453
+ raise SystemExit(1)
454
+ try:
455
+ return json.loads(result.stdout)
456
+ except json.JSONDecodeError:
457
+ console.print("[red]\u2717[/red] Failed to parse pip output.")
458
+ raise SystemExit(1)
459
+
460
+
461
+ # ---------------------------------------------------------------------------
462
+ # devit deps snapshot
463
+ # ---------------------------------------------------------------------------
464
+
465
+ @deps.command("snapshot")
466
+ @click.option("--message", "-m", default=None,
467
+ help="Label for this snapshot (e.g. 'working baseline').")
468
+ @click.option("--dir", "project_dir", default=None,
469
+ help="Project directory (default: current directory).")
470
+ def deps_snapshot(message, project_dir):
471
+ """
472
+ Save a snapshot of the current dependency state.
473
+
474
+ \b
475
+ Examples:
476
+ devit deps snapshot
477
+ devit deps snapshot -m "working with flask 3.0"
478
+ """
479
+ root = Path(project_dir).resolve() if project_dir else Path.cwd()
480
+ py = _venv_python(root)
481
+
482
+ packages = _get_current_packages(py)
483
+ snapshots = _load_snapshots(root)
484
+
485
+ snap_id = (max(s["id"] for s in snapshots) + 1) if snapshots else 1
486
+ timestamp = datetime.now().isoformat(timespec="seconds")
487
+ label = message or f"Snapshot #{snap_id}"
488
+
489
+ snapshots.append({
490
+ "id": snap_id,
491
+ "message": label,
492
+ "timestamp": timestamp,
493
+ "packages": [{"name": p["name"], "version": p["version"]} for p in packages],
494
+ })
495
+ try:
496
+ _save_snapshots(root, snapshots)
497
+ except click.ClickException as e:
498
+ console.print(f"[red]\u2717[/red] {e.format_message()}")
499
+ raise SystemExit(1)
500
+
501
+ console.print(
502
+ f"[green]\u2714[/green] Snapshot [bold]#{snap_id}[/bold] saved — "
503
+ f"[cyan]{label}[/cyan] [dim]({len(packages)} packages · {timestamp})[/dim]"
504
+ )
505
+
506
+
507
+ # ---------------------------------------------------------------------------
508
+ # devit deps history
509
+ # ---------------------------------------------------------------------------
510
+
511
+ @deps.command("history")
512
+ @click.option("--dir", "project_dir", default=None,
513
+ help="Project directory (default: current directory).")
514
+ def deps_history(project_dir):
515
+ """
516
+ List all saved dependency snapshots.
517
+
518
+ \b
519
+ Example:
520
+ devit deps history
521
+ """
522
+ root = Path(project_dir).resolve() if project_dir else Path.cwd()
523
+ snapshots = _load_snapshots(root)
524
+
525
+ if not snapshots:
526
+ console.print(
527
+ "[yellow]No snapshots yet.[/yellow] "
528
+ "Run [bold]devit deps snapshot[/bold] to save one."
529
+ )
530
+ return
531
+
532
+ table = Table(title=f"Dependency Snapshots ({len(snapshots)})", show_lines=False)
533
+ table.add_column("ID", style="dim", width=5)
534
+ table.add_column("Message", style="cyan")
535
+ table.add_column("Packages", justify="right", style="bold green", width=10)
536
+ table.add_column("Saved At", style="dim")
537
+
538
+ for s in reversed(snapshots):
539
+ table.add_row(
540
+ f"#{s['id']}",
541
+ s["message"],
542
+ str(len(s["packages"])),
543
+ s["timestamp"],
544
+ )
545
+
546
+ console.print(table)
547
+ console.print(
548
+ f"[dim] devit deps diff <ID> — compare to current env\n"
549
+ f" devit deps rollback <ID> — restore a snapshot[/dim]"
550
+ )
551
+
552
+
553
+ # ---------------------------------------------------------------------------
554
+ # devit deps diff
555
+ # ---------------------------------------------------------------------------
556
+
557
+ @deps.command("diff")
558
+ @click.argument("snapshot_id", type=int, required=False)
559
+ @click.option("--dir", "project_dir", default=None,
560
+ help="Project directory (default: current directory).")
561
+ def deps_diff(snapshot_id, project_dir):
562
+ """
563
+ Diff current environment against a snapshot. Shows what changed and flags issues.
564
+
565
+ \b
566
+ Examples:
567
+ devit deps diff # compare to latest snapshot
568
+ devit deps diff 2 # compare to snapshot #2
569
+ """
570
+ root = Path(project_dir).resolve() if project_dir else Path.cwd()
571
+ py = _venv_python(root)
572
+ snapshots = _load_snapshots(root)
573
+
574
+ if not snapshots:
575
+ console.print(
576
+ "[yellow]No snapshots found.[/yellow] "
577
+ "Run [bold]devit deps snapshot[/bold] first."
578
+ )
579
+ return
580
+
581
+ if snapshot_id is None:
582
+ snap = snapshots[-1]
583
+ else:
584
+ snap = next((s for s in snapshots if s["id"] == snapshot_id), None)
585
+ if not snap:
586
+ console.print(f"[red]\u2717[/red] Snapshot [bold]#{snapshot_id}[/bold] not found. "
587
+ f"Run [bold]devit deps history[/bold] to see available IDs.")
588
+ raise SystemExit(1)
589
+
590
+ current_pkgs = {p["name"].lower(): p["version"] for p in _get_current_packages(py)}
591
+ snap_pkgs = {p["name"].lower(): p["version"] for p in snap["packages"]}
592
+
593
+ all_names = sorted(set(current_pkgs) | set(snap_pkgs))
594
+
595
+ issues: list[str] = []
596
+ rows: list[tuple] = []
597
+ for name in all_names:
598
+ cur = current_pkgs.get(name)
599
+ old = snap_pkgs.get(name)
600
+ if cur == old:
601
+ continue
602
+ if old is None:
603
+ rows.append((name, "\u2014", cur, "[green]+added[/green]"))
604
+ elif cur is None:
605
+ rows.append((name, old, "\u2014", "[red]-removed[/red]"))
606
+ issues.append(f"[bold]{name}[/bold] was removed (snapshot had {old})")
607
+ else:
608
+ rows.append((name, old, cur, "[yellow]~changed[/yellow]"))
609
+ issues.append(f"[bold]{name}[/bold] version changed {old} \u2192 {cur}")
610
+
611
+ if not rows:
612
+ console.print(
613
+ f"[green]\u2714[/green] Environment matches snapshot "
614
+ f"[bold]#{snap['id']}[/bold] [dim]{snap['message']}[/dim]"
615
+ )
616
+ return
617
+
618
+ table = Table(
619
+ title=f"Diff: current vs Snapshot #{snap['id']} \u00b7 {snap['message']} [{snap['timestamp']}]",
620
+ show_lines=False,
621
+ )
622
+ table.add_column("Package", style="cyan")
623
+ table.add_column("Snapshot", style="dim")
624
+ table.add_column("Current", style="bold")
625
+ table.add_column("Status", style="bold")
626
+
627
+ for name, old, cur, status in rows:
628
+ table.add_row(name, old, cur, status)
629
+
630
+ console.print(table)
631
+
632
+ if issues:
633
+ console.print()
634
+ console.print("[red bold]Issues detected:[/red bold]")
635
+ for issue in issues:
636
+ console.print(f" [red]\u2022[/red] {issue}")
637
+ console.print()
638
+ console.print(
639
+ f" [dim]Run [bold]devit deps rollback {snap['id']}[/bold] "
640
+ f"to restore this snapshot.[/dim]"
641
+ )
642
+
643
+
644
+ # ---------------------------------------------------------------------------
645
+ # devit deps rollback
646
+ # ---------------------------------------------------------------------------
647
+
648
+ @deps.command("rollback")
649
+ @click.argument("snapshot_id", type=int, required=False)
650
+ @click.option("--yes", "-y", is_flag=True, default=False, help="Skip confirmation prompt.")
651
+ @click.option("--dir", "project_dir", default=None,
652
+ help="Project directory (default: current directory).")
653
+ def deps_rollback(snapshot_id, yes, project_dir):
654
+ """
655
+ Reinstall exact package versions from a saved snapshot.
656
+
657
+ \b
658
+ Examples:
659
+ devit deps rollback # rollback to latest snapshot
660
+ devit deps rollback 2 # rollback to snapshot #2
661
+ devit deps rollback 2 --yes
662
+ """
663
+ root = Path(project_dir).resolve() if project_dir else Path.cwd()
664
+ py = _venv_python(root)
665
+ snapshots = _load_snapshots(root)
666
+
667
+ if not snapshots:
668
+ console.print("[yellow]No snapshots found.[/yellow]")
669
+ return
670
+
671
+ if snapshot_id is None:
672
+ snap = snapshots[-1]
673
+ else:
674
+ snap = next((s for s in snapshots if s["id"] == snapshot_id), None)
675
+ if not snap:
676
+ console.print(f"[red]\u2717[/red] Snapshot [bold]#{snapshot_id}[/bold] not found. "
677
+ f"Run [bold]devit deps history[/bold] to see available IDs.")
678
+ raise SystemExit(1)
679
+
680
+ # Show what will change before asking
681
+ current_pkgs = {p["name"].lower(): p["version"] for p in _get_current_packages(py)}
682
+ snap_pkgs = {p["name"].lower(): p["version"] for p in snap["packages"]}
683
+
684
+ diffs = [
685
+ (name, current_pkgs.get(name, "\u2014"), ver)
686
+ for name, ver in snap_pkgs.items()
687
+ if current_pkgs.get(name) != ver
688
+ ]
689
+
690
+ console.print(
691
+ f"Rolling back to snapshot [bold]#{snap['id']}[/bold] — "
692
+ f"[cyan]{snap['message']}[/cyan] [dim]{snap['timestamp']}[/dim]"
693
+ )
694
+
695
+ if diffs:
696
+ table = Table(title=f"Changes that will be applied ({len(diffs)})", show_lines=False)
697
+ table.add_column("Package", style="cyan")
698
+ table.add_column("Current", style="dim")
699
+ table.add_column("\u2192 Restore", style="bold green")
700
+ for name, cur, restore in diffs:
701
+ table.add_row(name, cur, restore)
702
+ console.print(table)
703
+ else:
704
+ console.print("[green]\u2714[/green] Environment already matches this snapshot — nothing to do.")
705
+ return
706
+
707
+ if not yes:
708
+ ok = questionary.confirm(
709
+ "Reinstall these exact versions?", default=True
710
+ ).ask()
711
+ if not ok:
712
+ console.print("[yellow]Aborted.[/yellow]")
713
+ raise click.Abort()
714
+
715
+ pins = [f"{p['name']}=={p['version']}" for p in snap["packages"]]
716
+
717
+ if not pins:
718
+ console.print("[yellow]⚠[/yellow] Snapshot has no packages — nothing to install.")
719
+ return
720
+
721
+ console.print(f"[dim]Installing {len(pins)} pinned package(s)...[/dim]")
722
+ # Run pip OUTSIDE any Progress/spinner so its live output is not garbled
723
+ result = _pip(py, ["install"] + pins)
724
+
725
+ if result.returncode != 0:
726
+ console.print("[red]✗[/red] Rollback failed — see errors above.")
727
+ raise SystemExit(result.returncode)
728
+
729
+ console.print(
730
+ f"[green]\u2714[/green] Rolled back to snapshot [bold]#{snap['id']}[/bold] successfully.\n"
731
+ f" [dim]Restored [bold]{len(pins)}[/bold] packages to exact pinned versions.[/dim]"
732
+ )
@@ -16,6 +16,7 @@ from devkit_cli.commands.find import find
16
16
  from devkit_cli.commands.archive import zip_cmd, unzip_cmd
17
17
  from devkit_cli.commands.env import env
18
18
  from devkit_cli.commands.run import run, build, dev, test
19
+ from devkit_cli.commands.deps import deps
19
20
 
20
21
  console = Console()
21
22
 
@@ -83,6 +84,7 @@ cli.add_command(run)
83
84
  cli.add_command(build)
84
85
  cli.add_command(dev)
85
86
  cli.add_command(test)
87
+ cli.add_command(deps)
86
88
 
87
89
 
88
90
  if __name__ == "__main__":
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devit-cli"
7
- version = "0.1.5"
7
+ version = "0.1.7"
8
8
  description = "A full-featured CLI framework for professional Python developers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes