add: track shared modules, vendor scripts, and update gitignore
- lib/common.py: shared run/load_json/save_json utilities - webui/api/common.py: _ok/_error response helpers - webui/static/htmx.min.js, json-enc.js: vendored frontend deps - scripts/update-vendor.sh: frontend vendor updater - vendor/acme.sh: bundled ACME client - .gitignore: add .playwright-mcp/ and opencode.json.pwenv
This commit is contained in:
+137
@@ -0,0 +1,137 @@
|
||||
"""Shared utilities for Vacuum Wall lib/ modules.
|
||||
|
||||
Provides common helpers for JSON persistence, subprocess execution,
|
||||
deep merging, and directory creation used across all subsystem modules.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def run(
|
||||
cmd: list[str],
|
||||
check: bool = True,
|
||||
sudo: bool = False,
|
||||
timeout: int | None = None,
|
||||
) -> str:
|
||||
"""Run a command and return stripped stdout.
|
||||
|
||||
Args:
|
||||
cmd: Command arguments.
|
||||
check: Raise RuntimeError on non-zero exit.
|
||||
sudo: Prefix command with ``sudo``.
|
||||
timeout: Timeout in seconds (``None`` → no timeout).
|
||||
|
||||
Returns:
|
||||
``stdout`` with trailing whitespace removed.
|
||||
|
||||
Raises:
|
||||
RuntimeError: When ``check=True`` and the process exits non-zero.
|
||||
"""
|
||||
full_cmd = ["sudo", *cmd] if sudo else list(cmd)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
full_cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=check,
|
||||
timeout=timeout,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise RuntimeError(
|
||||
f"Command failed: {' '.join(full_cmd)} (rc={exc.returncode}): "
|
||||
f"{exc.stderr.strip()}"
|
||||
) from exc
|
||||
|
||||
|
||||
def run_proc(
|
||||
cmd: list[str],
|
||||
check: bool = True,
|
||||
sudo: bool = False,
|
||||
timeout: int | None = None,
|
||||
input: str | None = None,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
"""Run a command and return the full ``CompletedProcess``.
|
||||
|
||||
Args:
|
||||
cmd: Command arguments.
|
||||
check: Raise ``subprocess.CalledProcessError`` on non-zero exit.
|
||||
sudo: Prefix command with ``sudo``.
|
||||
timeout: Timeout in seconds.
|
||||
input: String to pass as stdin to the subprocess.
|
||||
|
||||
Returns:
|
||||
The completed process object.
|
||||
"""
|
||||
full_cmd = ["sudo", *cmd] if sudo else list(cmd)
|
||||
return subprocess.run(
|
||||
full_cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=check,
|
||||
timeout=timeout,
|
||||
input=input,
|
||||
)
|
||||
|
||||
|
||||
def load_json(path: Path, default: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Load JSON from *path*.
|
||||
|
||||
Returns *default* (default ``{}``) if the file does not exist.
|
||||
"""
|
||||
if default is None:
|
||||
default = {}
|
||||
if not path.exists():
|
||||
return deepcopy(default)
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_json(path: Path, data: dict[str, Any], indent: int = 4) -> None:
|
||||
"""Atomically write *data* as JSON to *path*.
|
||||
|
||||
Writes to ``path.tmp`` first, then replaces *path* via ``os.replace()``
|
||||
to avoid partial writes.
|
||||
"""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(data, f, indent=indent)
|
||||
f.write("\n")
|
||||
os.replace(tmp, path)
|
||||
|
||||
|
||||
def deep_merge(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Recursively merge *overrides* into a deep copy of *base*.
|
||||
|
||||
For nested dicts the merge recurses; for all other values
|
||||
*overrides* wins.
|
||||
"""
|
||||
result = deepcopy(base)
|
||||
for k, v in overrides.items():
|
||||
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
|
||||
result[k] = deep_merge(result[k], v)
|
||||
else:
|
||||
result[k] = deepcopy(v)
|
||||
return result
|
||||
|
||||
|
||||
def ensure_dirs(*dirs: Path) -> None:
|
||||
"""Create each directory (and parents) if it does not exist."""
|
||||
for d in dirs:
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"deep_merge",
|
||||
"ensure_dirs",
|
||||
"load_json",
|
||||
"run",
|
||||
"run_proc",
|
||||
"save_json",
|
||||
]
|
||||
Reference in New Issue
Block a user