80d73d1b74
- redis/client.py: use BlockingConnectionPool.from_url() (fixes 'url' kwarg error) - database/engine_compat.py: patch aiomysql do_ping to satisfy pool_pre_ping - database/session.py: apply engine_compat patch before create_async_engine - requirements.txt: SQLAlchemy 2.0.49 (fixes aiomysql ping() reconnect signature) - .env.example: document NEXUS_REDIS_URL format - scripts/wsl_ensure_venv.sh: create project .venv (PEP 668 safe) - scripts/wsl_start_dev.sh: use .venv python, validate Redis before start - scripts/wsl_integration_smoke.sh: code-only verification mode - scripts/wsl_check_mysql.py / bootstrap_database.py: MySQL setup helpers - scripts/sync_frontend_vendor.py: vendor asset sync utility Co-authored-by: Cursor <cursoragent@cursor.com>
109 lines
3.8 KiB
Python
109 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Download pinned frontend vendor assets into web/app/vendor/ (F2e-06).
|
|
|
|
Run after upgrading versions in VENDOR_MANIFEST, then commit vendor/ + HTML path updates.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
VENDOR_DIR = ROOT / "web" / "app" / "vendor"
|
|
|
|
VENDOR_MANIFEST: dict[str, dict[str, str]] = {
|
|
"tailwindcss-browser.js": {
|
|
"url": "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.0.6/dist/index.global.js",
|
|
"version": "@tailwindcss/browser@4.0.6",
|
|
},
|
|
"alpinejs.min.js": {
|
|
"url": "https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js",
|
|
"version": "alpinejs@3.14.8",
|
|
},
|
|
"xterm.css": {
|
|
"url": "https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css",
|
|
"version": "xterm@5.3.0",
|
|
},
|
|
"xterm.js": {
|
|
"url": "https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js",
|
|
"version": "xterm@5.3.0",
|
|
},
|
|
"xterm-addon-fit.js": {
|
|
"url": "https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js",
|
|
"version": "xterm-addon-fit@0.8.0",
|
|
},
|
|
"xterm-addon-web-links.js": {
|
|
"url": "https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js",
|
|
"version": "xterm-addon-web-links@0.9.0",
|
|
},
|
|
"qrious.min.js": {
|
|
"url": "https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js",
|
|
"version": "qrious@4.0.2",
|
|
},
|
|
}
|
|
|
|
CDN_REPLACEMENTS = {
|
|
"https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4": "/app/vendor/tailwindcss-browser.js",
|
|
"https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js": "/app/vendor/alpinejs.min.js",
|
|
"https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css": "/app/vendor/xterm.css",
|
|
"https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js": "/app/vendor/xterm.js",
|
|
"https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js": "/app/vendor/xterm-addon-fit.js",
|
|
"https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js": "/app/vendor/xterm-addon-web-links.js",
|
|
"https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js": "/app/vendor/qrious.min.js",
|
|
}
|
|
|
|
|
|
def _sha384(path: Path) -> str:
|
|
digest = hashlib.sha384(path.read_bytes()).digest()
|
|
return "sha384-" + __import__("base64").b64encode(digest).decode()
|
|
|
|
|
|
def download_vendor() -> dict:
|
|
VENDOR_DIR.mkdir(parents=True, exist_ok=True)
|
|
manifest: dict[str, dict] = {"files": {}, "replacements": CDN_REPLACEMENTS}
|
|
for filename, meta in VENDOR_MANIFEST.items():
|
|
dest = VENDOR_DIR / filename
|
|
print(f"Fetching {meta['version']} -> {dest.name}")
|
|
with urllib.request.urlopen(meta["url"], timeout=60) as resp:
|
|
data = resp.read()
|
|
dest.write_bytes(data)
|
|
manifest["files"][filename] = {
|
|
"version": meta["version"],
|
|
"url": meta["url"],
|
|
"bytes": len(data),
|
|
"integrity": _sha384(dest),
|
|
}
|
|
(VENDOR_DIR / "manifest.json").write_text(
|
|
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
return manifest
|
|
|
|
|
|
def patch_html_files() -> int:
|
|
app_dir = ROOT / "web" / "app"
|
|
changed = 0
|
|
for html in sorted(app_dir.glob("*.html")):
|
|
text = html.read_text(encoding="utf-8")
|
|
original = text
|
|
for old, new in CDN_REPLACEMENTS.items():
|
|
text = text.replace(old, new)
|
|
if text != original:
|
|
html.write_text(text, encoding="utf-8")
|
|
changed += 1
|
|
print(f"Updated {html.name}")
|
|
return changed
|
|
|
|
|
|
def main() -> None:
|
|
download_vendor()
|
|
n = patch_html_files()
|
|
print(f"Done. {n} HTML file(s) patched.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|