重构 lib 为扁平模块并支持 Windows schtasks;新增 JS/Bash/PowerShell 模板、WebUI 调度编辑,以及 Cursor Skill 与 MCP 工具供 Agent 管理定时任务。 Co-authored-by: Cursor <cursoragent@cursor.com>
115 lines
3.3 KiB
Python
115 lines
3.3 KiB
Python
"""检测本机 Python / Node.js / Bash / PowerShell 是否可用及版本。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import asdict, dataclass
|
|
|
|
|
|
@dataclass
|
|
class RuntimeProbe:
|
|
id: str
|
|
name: str
|
|
available: bool
|
|
version: str | None = None
|
|
path: str | None = None
|
|
|
|
def to_dict(self) -> dict[str, object]:
|
|
return asdict(self)
|
|
|
|
|
|
def _first_line(text: str) -> str:
|
|
line = (text or "").strip().splitlines()[0].strip() if text else ""
|
|
return line
|
|
|
|
|
|
def _run(cmd: list[str], *, timeout: float = 8.0) -> tuple[int, str]:
|
|
try:
|
|
proc = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
timeout=timeout,
|
|
)
|
|
out = _first_line((proc.stdout or "") + "\n" + (proc.stderr or ""))
|
|
return proc.returncode, out
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
return 1, ""
|
|
|
|
|
|
def _probe_python() -> RuntimeProbe:
|
|
exe = sys.executable
|
|
code, out = _run([exe, "--version"])
|
|
version: str | None = None
|
|
if code == 0 and out:
|
|
version = out.removeprefix("Python ").strip() if out.lower().startswith("python") else out
|
|
elif sys.version:
|
|
version = sys.version.split()[0]
|
|
return RuntimeProbe(
|
|
id="python",
|
|
name="Python",
|
|
available=True,
|
|
version=version,
|
|
path=exe,
|
|
)
|
|
|
|
|
|
def _probe_nodejs() -> RuntimeProbe:
|
|
path = shutil.which("node")
|
|
if not path:
|
|
return RuntimeProbe(id="nodejs", name="JavaScript", available=False)
|
|
code, out = _run([path, "--version"])
|
|
if code != 0 or not out:
|
|
return RuntimeProbe(id="nodejs", name="JavaScript", available=False, path=path)
|
|
version = out.lstrip("vV").strip() or out
|
|
return RuntimeProbe(id="nodejs", name="JavaScript", available=True, version=version, path=path)
|
|
|
|
|
|
def _parse_bash_version(text: str) -> str | None:
|
|
match = re.search(r"version\s+([^\s(]+)", text, re.I)
|
|
return match.group(1) if match else (text or None)
|
|
|
|
|
|
def _probe_bash() -> RuntimeProbe:
|
|
path = shutil.which("bash")
|
|
if not path:
|
|
return RuntimeProbe(id="bash", name="Bash", available=False)
|
|
code, out = _run([path, "--version"])
|
|
if code != 0 or not out:
|
|
return RuntimeProbe(id="bash", name="Bash", available=False, path=path)
|
|
version = _parse_bash_version(out)
|
|
return RuntimeProbe(id="bash", name="Bash", available=True, version=version, path=path)
|
|
|
|
|
|
def _probe_powershell() -> RuntimeProbe:
|
|
for cmd_name in ("pwsh", "powershell"):
|
|
path = shutil.which(cmd_name)
|
|
if not path:
|
|
continue
|
|
code, out = _run(
|
|
[path, "-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()"]
|
|
)
|
|
if code == 0 and out:
|
|
return RuntimeProbe(
|
|
id="powershell",
|
|
name="PowerShell",
|
|
available=True,
|
|
version=out.strip(),
|
|
path=path,
|
|
)
|
|
return RuntimeProbe(id="powershell", name="PowerShell", available=False)
|
|
|
|
|
|
def probe_all_runtimes() -> list[dict[str, object]]:
|
|
return [
|
|
_probe_python().to_dict(),
|
|
_probe_nodejs().to_dict(),
|
|
_probe_bash().to_dict(),
|
|
_probe_powershell().to_dict(),
|
|
]
|