envforge-agent 1.0.0__py3-none-any.whl
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.
- envforge_agent/__init__.py +8 -0
- envforge_agent/__main__.py +5 -0
- envforge_agent/cli.py +496 -0
- envforge_agent/detectors/__init__.py +17 -0
- envforge_agent/detectors/cuda_detector.py +277 -0
- envforge_agent/detectors/gpu_detector.py +141 -0
- envforge_agent/detectors/os_detector.py +118 -0
- envforge_agent/detectors/python_detector.py +142 -0
- envforge_agent/detectors/rocm_detector.py +79 -0
- envforge_agent/detectors/system_detector.py +72 -0
- envforge_agent/report.py +55 -0
- envforge_agent/schemas.py +142 -0
- envforge_agent-1.0.0.dist-info/METADATA +111 -0
- envforge_agent-1.0.0.dist-info/RECORD +16 -0
- envforge_agent-1.0.0.dist-info/WHEEL +4 -0
- envforge_agent-1.0.0.dist-info/entry_points.txt +2 -0
envforge_agent/cli.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""
|
|
2
|
+
envforge CLI — main command group and subcommands.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
envforge diagnose Collect and display a DiagnosticReport
|
|
6
|
+
envforge verify Check if a profile is compatible with this system
|
|
7
|
+
envforge fix Generate a repair script from a saved report
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import platform
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import httpx
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.panel import Panel
|
|
20
|
+
from rich.syntax import Syntax
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich import box
|
|
23
|
+
|
|
24
|
+
from envforge_agent import __version__
|
|
25
|
+
from envforge_agent.report import ReportBuilder
|
|
26
|
+
from envforge_agent.schemas import DiagnosticReport
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
err_console = Console(stderr=True, style="bold red")
|
|
30
|
+
|
|
31
|
+
def check_macos_support():
|
|
32
|
+
if platform.system() == "Darwin":
|
|
33
|
+
err_console.print("[ERROR] EnvForge is not currently supported on macOS.")
|
|
34
|
+
err_console.print(" Hint: This tool is designed for Linux and Windows (WSL) environments.")
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
# ── Root command group ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
@click.group()
|
|
39
|
+
@click.version_option(__version__, prog_name="envforge-agent")
|
|
40
|
+
def cli() -> None:
|
|
41
|
+
"""EnvForge CLI Diagnostic Agent — inspect your ML environment."""
|
|
42
|
+
check_macos_support()
|
|
43
|
+
|
|
44
|
+
# ── envforge diagnose ──────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
@cli.command("diagnose")
|
|
47
|
+
@click.option(
|
|
48
|
+
"--output", "-o",
|
|
49
|
+
type=click.Path(dir_okay=False, writable=True),
|
|
50
|
+
default=None,
|
|
51
|
+
help="Save report to a JSON file instead of printing to stdout.",
|
|
52
|
+
)
|
|
53
|
+
@click.option(
|
|
54
|
+
"--send", is_flag=True, default=False,
|
|
55
|
+
help="Send the report to the EnvForge API for compatibility analysis.",
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--api-url",
|
|
59
|
+
default="http://localhost:8000",
|
|
60
|
+
show_default=True,
|
|
61
|
+
envvar="ENVFORGE_API_URL",
|
|
62
|
+
help="Base URL of the EnvForge API.",
|
|
63
|
+
)
|
|
64
|
+
@click.option(
|
|
65
|
+
"--quiet", "-q", is_flag=True, default=False,
|
|
66
|
+
help="Suppress all output except the JSON report.",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@click.option(
|
|
70
|
+
"--sarif", is_flag=True, default=False,
|
|
71
|
+
help="Output diagnostics in SARIF 2.1.0 format for CI/CD pipeline integrations.",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def diagnose(output: str | None, send: bool, api_url: str, quiet: bool, sarif: bool) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Collect a full diagnostic report of this machine's ML environment.
|
|
77
|
+
|
|
78
|
+
Detects: OS, CPU, RAM, GPU, CUDA, cuDNN, Python installations.
|
|
79
|
+
Outputs: DiagnosticReport JSON compatible with POST /api/v1/diagnose.
|
|
80
|
+
"""
|
|
81
|
+
if not quiet:
|
|
82
|
+
console.print(Panel(
|
|
83
|
+
f"[bold cyan]EnvForge Diagnostic Agent[/] v{__version__}\n"
|
|
84
|
+
"[dim]Scanning your environment...[/]",
|
|
85
|
+
expand=False,
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
report = ReportBuilder().build()
|
|
89
|
+
|
|
90
|
+
if not quiet:
|
|
91
|
+
_print_report_summary(report)
|
|
92
|
+
|
|
93
|
+
# ── SARIF output ────────────────────────────────────────────────────────
|
|
94
|
+
if sarif:
|
|
95
|
+
import json as _json
|
|
96
|
+
click.echo(_json.dumps(report.to_sarif(), indent=2))
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
report_json = report.to_json(indent=2)
|
|
100
|
+
|
|
101
|
+
# ── Output to file ──────────────────────────────────────────────────────
|
|
102
|
+
if output:
|
|
103
|
+
Path(output).write_text(report_json, encoding="utf-8")
|
|
104
|
+
if not quiet:
|
|
105
|
+
console.print(f"\n[green][+][/] Report saved to [bold]{output}[/]")
|
|
106
|
+
elif not send:
|
|
107
|
+
# Print JSON to stdout (pipe-friendly)
|
|
108
|
+
click.echo(report_json)
|
|
109
|
+
|
|
110
|
+
# ── Send to API ─────────────────────────────────────────────────────────
|
|
111
|
+
if send:
|
|
112
|
+
_send_report(report, api_url, quiet)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _print_report_summary(report: DiagnosticReport) -> None:
|
|
116
|
+
"""Print a human-readable summary table to the terminal."""
|
|
117
|
+
table = Table(box=box.ROUNDED, show_header=False, padding=(0, 1))
|
|
118
|
+
table.add_column("Category", style="bold cyan", width=14)
|
|
119
|
+
table.add_column("Value")
|
|
120
|
+
|
|
121
|
+
table.add_row("OS", f"{report.os.name} {report.os.version} ({report.os.architecture})")
|
|
122
|
+
if report.os.wsl_version:
|
|
123
|
+
table.add_row("WSL", report.os.wsl_version)
|
|
124
|
+
|
|
125
|
+
table.add_row("CPU", f"{report.cpu.brand} — {report.cpu.cores}C / {report.cpu.threads}T")
|
|
126
|
+
ram_str = f"{report.ram.total_gb} GB total, {report.ram.available_gb} GB free"
|
|
127
|
+
if report.ram.total_gb < 8:
|
|
128
|
+
ram_str += " [bold red][!] CRITICAL: Under 8 GB — heavy ML profiles will fail[/]"
|
|
129
|
+
elif report.ram.total_gb < 16:
|
|
130
|
+
ram_str += " [yellow][!] WARNING: Under 16 GB — some ML profiles may be slow[/]"
|
|
131
|
+
table.add_row("RAM", ram_str)
|
|
132
|
+
|
|
133
|
+
if report.gpus:
|
|
134
|
+
for gpu in report.gpus:
|
|
135
|
+
vram = f"{gpu.vram_gb} GB" if gpu.vram_gb else "?"
|
|
136
|
+
driver = gpu.driver_version or "?"
|
|
137
|
+
table.add_row("GPU", f"{gpu.name} ({vram} VRAM, driver {driver})")
|
|
138
|
+
else:
|
|
139
|
+
table.add_row("GPU", "[dim]No NVIDIA GPU detected[/]")
|
|
140
|
+
|
|
141
|
+
if report.cuda.version:
|
|
142
|
+
cudnn = f", cuDNN {report.cuda.cudnn_version}" if report.cuda.cudnn_version else ""
|
|
143
|
+
table.add_row("CUDA", f"{report.cuda.version}{cudnn}")
|
|
144
|
+
if report.cuda.toolkit_path:
|
|
145
|
+
table.add_row("CUDA Path", report.cuda.toolkit_path)
|
|
146
|
+
else:
|
|
147
|
+
table.add_row("CUDA", "[dim]Not detected[/]")
|
|
148
|
+
|
|
149
|
+
if report.rocm.version:
|
|
150
|
+
gcn = f" (GCN {report.rocm.gcn_arch})" if report.rocm.gcn_arch else ""
|
|
151
|
+
table.add_row("ROCm", f"{report.rocm.version}{gcn}")
|
|
152
|
+
else:
|
|
153
|
+
table.add_row("ROCm", "[dim]Not detected[/]")
|
|
154
|
+
|
|
155
|
+
if report.active_python:
|
|
156
|
+
py = report.active_python
|
|
157
|
+
venv = " [dim](venv)[/]" if py.is_venv else ""
|
|
158
|
+
table.add_row("Python", f"{py.version} at {py.path}{venv}")
|
|
159
|
+
|
|
160
|
+
if len(report.python_installations) > 1:
|
|
161
|
+
others = [p for p in report.python_installations
|
|
162
|
+
if p.path != (report.active_python.path if report.active_python else "")]
|
|
163
|
+
if others:
|
|
164
|
+
table.add_row(
|
|
165
|
+
"Other Pythons",
|
|
166
|
+
", ".join(f"{p.version} ({p.path})" for p in others[:3]),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
console.print(table)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _send_report(report: DiagnosticReport, api_url: str, quiet: bool) -> None:
|
|
173
|
+
"""POST the DiagnosticReport to the EnvForge API."""
|
|
174
|
+
url = f"{api_url.rstrip('/')}/api/v1/diagnose"
|
|
175
|
+
if not quiet:
|
|
176
|
+
console.print(f"\n[bold]Sending report to[/] {url} ...")
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
response = httpx.post(
|
|
180
|
+
url,
|
|
181
|
+
content=report.to_json(),
|
|
182
|
+
headers={"Content-Type": "application/json"},
|
|
183
|
+
timeout=30,
|
|
184
|
+
)
|
|
185
|
+
response.raise_for_status()
|
|
186
|
+
result = response.json()
|
|
187
|
+
|
|
188
|
+
if not quiet:
|
|
189
|
+
_print_diagnose_response(result)
|
|
190
|
+
else:
|
|
191
|
+
click.echo(json.dumps(result, indent=2))
|
|
192
|
+
|
|
193
|
+
except httpx.ConnectError:
|
|
194
|
+
err_console.print(f"[ERROR] Cannot connect to {url}")
|
|
195
|
+
err_console.print(" Hint: Is the EnvForge API running? Check ENVFORGE_API_URL.")
|
|
196
|
+
sys.exit(1)
|
|
197
|
+
except httpx.HTTPStatusError as e:
|
|
198
|
+
err_console.print(f"[ERROR] API returned {e.response.status_code}")
|
|
199
|
+
err_console.print(e.response.text)
|
|
200
|
+
sys.exit(1)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _print_diagnose_response(result: dict) -> None:
|
|
204
|
+
"""Pretty-print the API diagnose response."""
|
|
205
|
+
console.print("\n[bold green][+] Compatibility Analysis[/]")
|
|
206
|
+
|
|
207
|
+
if result.get("compatible_profiles"):
|
|
208
|
+
console.print(f" Compatible profiles: {', '.join(result['compatible_profiles'])}")
|
|
209
|
+
|
|
210
|
+
if result.get("recommendations"):
|
|
211
|
+
console.print("\n[bold]Recommendations:[/]")
|
|
212
|
+
for rec in result["recommendations"]:
|
|
213
|
+
console.print(f" - {rec}")
|
|
214
|
+
|
|
215
|
+
if result.get("issues"):
|
|
216
|
+
console.print("\n[bold yellow]Issues:[/]")
|
|
217
|
+
for issue in result["issues"]:
|
|
218
|
+
sev = issue.get("severity", "INFO")
|
|
219
|
+
color = {"ERROR": "red", "WARNING": "yellow", "INFO": "cyan"}.get(sev, "white")
|
|
220
|
+
console.print(f" [{color}][{sev}][/] {issue['message']}")
|
|
221
|
+
if issue.get("suggested_fix"):
|
|
222
|
+
console.print(f" Fix: {issue['suggested_fix']}")
|
|
223
|
+
|
|
224
|
+
console.print(f"\n Report ID: {result.get('report_id', '?')}")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ── envforge verify ────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
@cli.command("verify")
|
|
230
|
+
@click.option(
|
|
231
|
+
"--profile", "-p", required=False,
|
|
232
|
+
help="Profile slug to verify against (e.g. pytorch-cuda).",
|
|
233
|
+
)
|
|
234
|
+
@click.option(
|
|
235
|
+
"--output", "-o",
|
|
236
|
+
type=click.Path(dir_okay=False, writable=True),
|
|
237
|
+
default=None,
|
|
238
|
+
help="Save verification report to a JSON file instead of printing to stdout.",
|
|
239
|
+
)
|
|
240
|
+
@click.option(
|
|
241
|
+
"--quiet", "-q", is_flag=True, default=False,
|
|
242
|
+
help="Suppress all output except the JSON verification report.",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def verify(profile: str | None, output: str | None, quiet: bool) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Verify whether the generated ML environment works after setup.
|
|
248
|
+
|
|
249
|
+
Checks PyTorch import and, if a GPU profile is detected, CUDA availability.
|
|
250
|
+
Returns a structured PASS/FAIL JSON result.
|
|
251
|
+
"""
|
|
252
|
+
if not quiet:
|
|
253
|
+
console.print(Panel(
|
|
254
|
+
f"[bold cyan]EnvForge Verification Agent[/] v{__version__}\n"
|
|
255
|
+
"[dim]Running framework sanity checks...[/]",
|
|
256
|
+
expand=False,
|
|
257
|
+
))
|
|
258
|
+
|
|
259
|
+
import subprocess
|
|
260
|
+
|
|
261
|
+
# 1. Determine active Python
|
|
262
|
+
report = ReportBuilder().build()
|
|
263
|
+
active_py = report.active_python
|
|
264
|
+
py_executable = active_py.path if active_py else sys.executable
|
|
265
|
+
|
|
266
|
+
# 2. Run inline Python script to test torch import and CUDA
|
|
267
|
+
inspector_script = (
|
|
268
|
+
"import sys\n"
|
|
269
|
+
"import json\n"
|
|
270
|
+
"result = {'import_ok': False, 'cuda_ok': False, 'error': None}\n"
|
|
271
|
+
"try:\n"
|
|
272
|
+
" import torch\n"
|
|
273
|
+
" result['import_ok'] = True\n"
|
|
274
|
+
" result['torch_version'] = torch.__version__\n"
|
|
275
|
+
" try:\n"
|
|
276
|
+
" result['cuda_ok'] = torch.cuda.is_available()\n"
|
|
277
|
+
" result['cuda_version'] = torch.version.cuda\n"
|
|
278
|
+
" except Exception as e:\n"
|
|
279
|
+
" result['cuda_ok'] = False\n"
|
|
280
|
+
"except Exception as e:\n"
|
|
281
|
+
" result['import_ok'] = False\n"
|
|
282
|
+
" result['error'] = f'{type(e).__name__}: {str(e)}'\n"
|
|
283
|
+
"print(json.dumps(result))\n"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
proc = subprocess.run(
|
|
288
|
+
[py_executable, "-c", inspector_script],
|
|
289
|
+
capture_output=True,
|
|
290
|
+
text=True,
|
|
291
|
+
timeout=15
|
|
292
|
+
)
|
|
293
|
+
if proc.returncode != 0:
|
|
294
|
+
res = {
|
|
295
|
+
"status": "FAIL",
|
|
296
|
+
"message": "Python verification script failed to execute",
|
|
297
|
+
"error": proc.stderr.strip() or f"Exit code {proc.returncode}"
|
|
298
|
+
}
|
|
299
|
+
click.echo(json.dumps(res, indent=2))
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
|
|
302
|
+
data = json.loads(proc.stdout.strip())
|
|
303
|
+
|
|
304
|
+
# 3. Analyze checks
|
|
305
|
+
if not data["import_ok"]:
|
|
306
|
+
if not quiet:
|
|
307
|
+
_print_verification_summary(data, is_gpu_profile=False)
|
|
308
|
+
res = {
|
|
309
|
+
"status": "FAIL",
|
|
310
|
+
"message": "PyTorch import failed — is it installed?",
|
|
311
|
+
"error": data["error"]
|
|
312
|
+
}
|
|
313
|
+
click.echo(json.dumps(res, indent=2))
|
|
314
|
+
sys.exit(1)
|
|
315
|
+
|
|
316
|
+
# Check if CUDA profile is detected
|
|
317
|
+
is_gpu_profile = False
|
|
318
|
+
if profile:
|
|
319
|
+
is_gpu_profile = any(term in profile.lower() for term in ["cuda", "gpu", "diffusion", "finetune"])
|
|
320
|
+
|
|
321
|
+
if is_gpu_profile and not data["cuda_ok"]:
|
|
322
|
+
if not quiet:
|
|
323
|
+
_print_verification_summary(data, is_gpu_profile=is_gpu_profile)
|
|
324
|
+
res = {
|
|
325
|
+
"status": "FAIL",
|
|
326
|
+
"message": "PyTorch installed but CUDA not available",
|
|
327
|
+
"error": "torch.cuda.is_available() returned False"
|
|
328
|
+
}
|
|
329
|
+
click.echo(json.dumps(res, indent=2))
|
|
330
|
+
sys.exit(1)
|
|
331
|
+
|
|
332
|
+
# All required checks passed!
|
|
333
|
+
msg = "Environment works: PyTorch imported successfully"
|
|
334
|
+
if data["cuda_ok"]:
|
|
335
|
+
msg += " with CUDA support"
|
|
336
|
+
else:
|
|
337
|
+
msg += " (CPU only)"
|
|
338
|
+
|
|
339
|
+
if not quiet:
|
|
340
|
+
_print_verification_summary(data, is_gpu_profile=is_gpu_profile)
|
|
341
|
+
|
|
342
|
+
res = {
|
|
343
|
+
"status": "PASS",
|
|
344
|
+
"message": msg
|
|
345
|
+
}
|
|
346
|
+
click.echo(json.dumps(res, indent=2))
|
|
347
|
+
sys.exit(0)
|
|
348
|
+
|
|
349
|
+
except subprocess.TimeoutExpired:
|
|
350
|
+
res = {
|
|
351
|
+
"status": "FAIL",
|
|
352
|
+
"message": "Verification timed out",
|
|
353
|
+
"error": "Subprocess took longer than 15 seconds"
|
|
354
|
+
}
|
|
355
|
+
click.echo(json.dumps(res, indent=2))
|
|
356
|
+
sys.exit(1)
|
|
357
|
+
except Exception as e:
|
|
358
|
+
res = {
|
|
359
|
+
"status": "FAIL",
|
|
360
|
+
"message": "Verification failed due to an unexpected error",
|
|
361
|
+
"error": str(e)
|
|
362
|
+
}
|
|
363
|
+
click.echo(json.dumps(res, indent=2))
|
|
364
|
+
sys.exit(1)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _print_verification_summary(data: dict, is_gpu_profile: bool) -> None:
|
|
368
|
+
"""Print a beautiful human-readable verification matrix to the terminal."""
|
|
369
|
+
table = Table(box=box.ROUNDED, show_header=True, padding=(0, 1))
|
|
370
|
+
table.add_column("Check Matrix", style="bold cyan", width=22)
|
|
371
|
+
table.add_column("Status", width=12, justify="center")
|
|
372
|
+
table.add_column("Details")
|
|
373
|
+
|
|
374
|
+
# PyTorch import check
|
|
375
|
+
if data.get("import_ok"):
|
|
376
|
+
torch_v = data.get("torch_version", "Unknown")
|
|
377
|
+
table.add_row("PyTorch Core Import", "[bold green]PASS[/]", f"Framework loaded cleanly (v{torch_v}).")
|
|
378
|
+
else:
|
|
379
|
+
table.add_row("PyTorch Core Import", "[bold red]FAIL[/]", f"[red]{data.get('error')}[/]")
|
|
380
|
+
|
|
381
|
+
# CUDA compute engine check
|
|
382
|
+
if data.get("cuda_ok"):
|
|
383
|
+
table.add_row("CUDA Compute Engine", "[bold green]PASS[/]", "Graphics hardware handshake succeeded.")
|
|
384
|
+
else:
|
|
385
|
+
# If the profile requires GPU but CUDA check failed, mark as FAIL. Otherwise, SKIP.
|
|
386
|
+
if is_gpu_profile:
|
|
387
|
+
table.add_row("CUDA Compute Engine", "[bold red]FAIL[/]", "[red]Required by profile, but unavailable.[/]")
|
|
388
|
+
else:
|
|
389
|
+
table.add_row("CUDA Compute Engine", "[dim yellow]SKIP[/]", "Running on native CPU space.")
|
|
390
|
+
cuda_v = data.get("cuda_version") or "Not Detected"
|
|
391
|
+
table.add_row("Installed CUDA Version", "[dim]INFO[/]", f"{cuda_v}")
|
|
392
|
+
|
|
393
|
+
table.add_row("Required CUDA Profile", "[dim]INFO[/]", ">= 11.8 (Recommended for CUDA paths)")
|
|
394
|
+
|
|
395
|
+
console.print("\n[bold]=== Verification Report ===[/]")
|
|
396
|
+
console.print(table)
|
|
397
|
+
|
|
398
|
+
# ── envforge fix ───────────────────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
@cli.command("fix")
|
|
401
|
+
@click.option(
|
|
402
|
+
"--report", "-r",
|
|
403
|
+
type=click.Path(exists=True, dir_okay=False, readable=True),
|
|
404
|
+
required=True,
|
|
405
|
+
help="Path to a saved DiagnosticReport JSON file.",
|
|
406
|
+
)
|
|
407
|
+
@click.option(
|
|
408
|
+
"--profile", "-p",
|
|
409
|
+
required=True,
|
|
410
|
+
help="Profile slug to generate a repair script for.",
|
|
411
|
+
)
|
|
412
|
+
@click.option(
|
|
413
|
+
"--api-url",
|
|
414
|
+
default="http://localhost:8000",
|
|
415
|
+
show_default=True,
|
|
416
|
+
envvar="ENVFORGE_API_URL",
|
|
417
|
+
)
|
|
418
|
+
@click.option(
|
|
419
|
+
"--dry-run", is_flag=True, default=False,
|
|
420
|
+
help="Preview the names of the scripts and resolved packages without printing their full contents.",
|
|
421
|
+
)
|
|
422
|
+
def fix(report: str, profile: str, api_url: str, dry_run: bool) -> None:
|
|
423
|
+
"""
|
|
424
|
+
Generate a repair script based on a saved diagnostic report.
|
|
425
|
+
|
|
426
|
+
Sends the report to the API and requests a setup script for the target profile.
|
|
427
|
+
"""
|
|
428
|
+
console.print(f"[bold cyan]Generating repair script[/] for profile: {profile}")
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
raw = Path(report).read_text(encoding="utf-8")
|
|
432
|
+
parsed = DiagnosticReport.model_validate_json(raw)
|
|
433
|
+
except Exception as e:
|
|
434
|
+
err_console.print(f"Failed to parse report file: {e}")
|
|
435
|
+
sys.exit(1)
|
|
436
|
+
|
|
437
|
+
# Detect OS and Python from the loaded report to build a generate request
|
|
438
|
+
target_os = _map_os_to_target(parsed)
|
|
439
|
+
python_version = _extract_python_version(parsed)
|
|
440
|
+
|
|
441
|
+
url = f"{api_url.rstrip('/')}/api/v1/scripts/generate"
|
|
442
|
+
payload = {
|
|
443
|
+
"profile_id": profile,
|
|
444
|
+
"target_os": target_os,
|
|
445
|
+
"python_version": python_version,
|
|
446
|
+
"cuda_version": parsed.cuda.version,
|
|
447
|
+
"output_formats": ["setup.sh"] if target_os != "WIN" else ["setup.ps1", "requirements.txt"],
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
response = httpx.post(url, json=payload, timeout=30)
|
|
452
|
+
response.raise_for_status()
|
|
453
|
+
result = response.json()
|
|
454
|
+
|
|
455
|
+
console.print(f"[green][+][/] Scripts generated (job: {result.get('job_id', '?')})")
|
|
456
|
+
|
|
457
|
+
if result.get("resolved_packages"):
|
|
458
|
+
console.print(f" [cyan]Resolved Packages:[/] {', '.join(result['resolved_packages'])}")
|
|
459
|
+
|
|
460
|
+
if dry_run:
|
|
461
|
+
console.print("\n[bold]Files to be generated:[/]")
|
|
462
|
+
for script in result.get("scripts", []):
|
|
463
|
+
console.print(f" - {script['filename']}")
|
|
464
|
+
else:
|
|
465
|
+
for script in result.get("scripts", []):
|
|
466
|
+
console.print(
|
|
467
|
+
Panel(
|
|
468
|
+
Syntax(script["content"], "bash", theme="monokai", line_numbers=True),
|
|
469
|
+
title=f"[bold]{script['filename']}[/]",
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
download_url = f"{api_url.rstrip('/')}{result.get('download_url', '')}"
|
|
474
|
+
console.print(f"\n Download all: [link={download_url}]{download_url}[/link]")
|
|
475
|
+
|
|
476
|
+
except httpx.ConnectError:
|
|
477
|
+
err_console.print(f"Cannot connect to {url}. Is the API running?")
|
|
478
|
+
sys.exit(1)
|
|
479
|
+
except httpx.HTTPStatusError as e:
|
|
480
|
+
err_console.print(f"API error {e.response.status_code}: {e.response.text}")
|
|
481
|
+
sys.exit(1)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _map_os_to_target(report: DiagnosticReport) -> str:
|
|
485
|
+
if report.os.wsl_version:
|
|
486
|
+
return "WSL"
|
|
487
|
+
if "windows" in report.os.name.lower():
|
|
488
|
+
return "WIN"
|
|
489
|
+
return "LINUX"
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _extract_python_version(report: DiagnosticReport) -> str:
|
|
493
|
+
if report.active_python:
|
|
494
|
+
parts = report.active_python.version.split(".")
|
|
495
|
+
return f"{parts[0]}.{parts[1]}"
|
|
496
|
+
return "3.11" # safe default
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Detectors package."""
|
|
2
|
+
from envforge_agent.detectors.cuda_detector import detect_cuda
|
|
3
|
+
from envforge_agent.detectors.gpu_detector import detect_gpus
|
|
4
|
+
from envforge_agent.detectors.os_detector import detect_os
|
|
5
|
+
from envforge_agent.detectors.python_detector import detect_python
|
|
6
|
+
from envforge_agent.detectors.rocm_detector import detect_rocm
|
|
7
|
+
from envforge_agent.detectors.system_detector import detect_cpu, detect_ram
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"detect_os",
|
|
11
|
+
"detect_cpu",
|
|
12
|
+
"detect_ram",
|
|
13
|
+
"detect_gpus",
|
|
14
|
+
"detect_cuda",
|
|
15
|
+
"detect_rocm",
|
|
16
|
+
"detect_python",
|
|
17
|
+
]
|