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:
@@ -14,6 +14,10 @@ __pycache__/
|
|||||||
|
|
||||||
# Local AI tool config (contains internal hostnames)
|
# Local AI tool config (contains internal hostnames)
|
||||||
opencode.json
|
opencode.json
|
||||||
|
opencode.json.pwenv
|
||||||
|
|
||||||
|
# Playwright MCP artifacts
|
||||||
|
.playwright-mcp/
|
||||||
|
|
||||||
# Runtime artifacts
|
# Runtime artifacts
|
||||||
build/
|
build/
|
||||||
|
|||||||
+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",
|
||||||
|
]
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Download and vendor frontend JS libraries into webui/static/
|
||||||
|
# and backend binaries into vendor/.
|
||||||
|
# Run from the project root after updating the VERSION variables below.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---- Library versions ----
|
||||||
|
HTMX_VERSION="2.0.4"
|
||||||
|
HTMX_JSON_ENC_VERSION="2.0.0"
|
||||||
|
ACME_VERSION="3.1.3"
|
||||||
|
|
||||||
|
STATIC_DIR="webui/static"
|
||||||
|
|
||||||
|
download() {
|
||||||
|
local name="$1" url="$2" dest="$3"
|
||||||
|
if [[ -n "${SKIP_DOWNLOAD:-}" ]]; then
|
||||||
|
echo "[skip] $name (SKIP_DOWNLOAD is set)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "[download] $name → $dest"
|
||||||
|
curl -sfL -o "$dest" "$url"
|
||||||
|
}
|
||||||
|
|
||||||
|
download "htmx@${HTMX_VERSION}" \
|
||||||
|
"https://unpkg.com/htmx.org@${HTMX_VERSION}/dist/htmx.min.js" \
|
||||||
|
"${STATIC_DIR}/htmx.min.js"
|
||||||
|
|
||||||
|
download "htmx-ext-json-enc@${HTMX_JSON_ENC_VERSION}" \
|
||||||
|
"https://unpkg.com/htmx-ext-json-enc@${HTMX_JSON_ENC_VERSION}/json-enc.js" \
|
||||||
|
"${STATIC_DIR}/json-enc.js"
|
||||||
|
|
||||||
|
download "acme.sh@${ACME_VERSION}" \
|
||||||
|
"https://raw.githubusercontent.com/acmesh-official/acme.sh/${ACME_VERSION}/acme.sh" \
|
||||||
|
"vendor/acme.sh"
|
||||||
|
|
||||||
|
chmod +x "vendor/acme.sh"
|
||||||
|
|
||||||
|
echo "[done] All libraries vendored."
|
||||||
+8326
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
|||||||
|
"""Shared API response helpers.
|
||||||
|
|
||||||
|
Used by all API blueprints to produce consistent JSON responses
|
||||||
|
per the API response contract: ``{"ok": true, "data": <value>}`` /
|
||||||
|
``{"ok": false, "error": "msg"}``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import jsonify
|
||||||
|
|
||||||
|
|
||||||
|
def _ok(data=None):
|
||||||
|
"""Return a success JSON response."""
|
||||||
|
return jsonify({"ok": True, "data": data})
|
||||||
|
|
||||||
|
|
||||||
|
def _error(msg: str, code: int = 400):
|
||||||
|
"""Return an error JSON response with the given HTTP status code."""
|
||||||
|
return jsonify({"ok": False, "error": msg}), code
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
|
|||||||
|
htmx.defineExtension('json-enc', {
|
||||||
|
onEvent: function(name, evt) {
|
||||||
|
if (name === 'htmx:configRequest') {
|
||||||
|
evt.detail.headers['Content-Type'] = 'application/json'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
encodeParameters: function(xhr, parameters, elt) {
|
||||||
|
xhr.overrideMimeType('text/json')
|
||||||
|
return (JSON.stringify(parameters))
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user