devit-cli 0.1.5__tar.gz → 0.1.6__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.
- {devit_cli-0.1.5/devit_cli.egg-info → devit_cli-0.1.6}/PKG-INFO +1 -1
- {devit_cli-0.1.5 → devit_cli-0.1.6/devit_cli.egg-info}/PKG-INFO +1 -1
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devit_cli.egg-info/SOURCES.txt +1 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/__init__.py +1 -1
- devit_cli-0.1.6/devkit_cli/commands/deps.py +732 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/main.py +2 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/pyproject.toml +1 -1
- {devit_cli-0.1.5 → devit_cli-0.1.6}/LICENSE +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/README.md +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/_devkit_entry.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devit_cli.egg-info/dependency_links.txt +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devit_cli.egg-info/entry_points.txt +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devit_cli.egg-info/requires.txt +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devit_cli.egg-info/top_level.txt +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/commands/__init__.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/commands/archive.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/commands/clean.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/commands/env.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/commands/find.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/commands/info.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/commands/init.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/commands/run.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/aws/.gitignore +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/aws/README.md +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/aws/requirements.txt +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/aws/scripts/__init__.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/aws/scripts/ec2.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/aws/scripts/main.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/aws/scripts/s3.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/aws/tests/test_scripts.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/.gitignore +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/README.md +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/apps/__init__.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/apps/core/__init__.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/apps/core/apps.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/apps/core/urls.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/apps/core/views.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/manage.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/requirements.txt +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/{{module_name}}/__init__.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/{{module_name}}/settings.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/{{module_name}}/urls.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/django/{{module_name}}/wsgi.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/fastapi/.gitignore +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/fastapi/README.md +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/fastapi/app/__init__.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/fastapi/app/routers/__init__.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/fastapi/app/routers/health.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/fastapi/main.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/fastapi/requirements.txt +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/fastapi/tests/test_api.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/package/.gitignore +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/package/README.md +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/package/pyproject.toml +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/package/tests/test_core.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/package/{{module_name}}/__init__.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/package/{{module_name}}/core.py +0 -0
- {devit_cli-0.1.5 → devit_cli-0.1.6}/setup.cfg +0 -0
|
@@ -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__":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devit_cli-0.1.5 → devit_cli-0.1.6}/devkit_cli/templates/package/{{module_name}}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|