chore: import upstream snapshot with attribution
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:36 +08:00
commit f99010fae1
1834 changed files with 662985 additions and 0 deletions
+461
View File
@@ -0,0 +1,461 @@
#!/usr/bin/env python3
from __future__ import annotations
import html
import html.parser
import pathlib
import re
import sys
import urllib.parse
import xml.etree.ElementTree as ET
ROOT = pathlib.Path(__file__).resolve().parents[1]
SITE = ROOT / "site"
ROUTES = [
"/",
"/quickstart/",
"/usage/",
"/activity/",
"/recent-edits/",
"/session-intelligence/",
"/mcp/",
"/token-usage/",
"/chat-import/",
"/insights/",
"/commands/",
"/stats/",
"/session-api/",
"/configuration/",
"/remote-access/",
"/pg-sync/",
"/duckdb/",
"/changelog/",
]
REQUIRED_FRAGMENTS = [
"/configuration/#session-discovery",
"/token-usage/#how-it-compares-to-ccusage",
"/session-api/#agentsview-session-usage",
]
REQUIRED_META = {
("property", "og:image"): "https://agentsview.io/assets/static/og-image.png",
("property", "og:image:width"): "1200",
("property", "og:image:height"): "630",
("property", "og:type"): "website",
("property", "og:site_name"): "AgentsView",
("name", "twitter:card"): "summary_large_image",
("name", "twitter:image"): "https://agentsview.io/assets/static/og-image.png",
}
REQUIRED_SITEMAP_URLS = [
"https://agentsview.io/",
]
COMPACT_SVG_MAX_HEIGHTS: dict[str, float] = {}
FORBIDDEN_PATTERNS = [
"virtual:starlight",
"@astrojs/starlight",
"discord-top-link",
"<aside class=\"md-banner\"",
"<Tabs",
"<TabItem",
"<Card",
"<CardGrid",
"<Screenshot",
"<Aside",
"set:html",
]
MAX_HTML_UNESCAPE_PASSES = 50
FETCHED_LINK_RELS = {
"apple-touch-icon",
"apple-touch-startup-image",
"icon",
"manifest",
"mask-icon",
"modulepreload",
"prefetch",
"preload",
"prerender",
"stylesheet",
}
CSS_URL_RE = re.compile(
r"url\(\s*(?:\"([^\"]*)\"|'([^']*)'|([^)]*?))\s*\)", re.IGNORECASE
)
CSS_IMPORT_RE = re.compile(
r"@import\s+(?:url\(\s*(?:\"([^\"]*)\"|'([^']*)'|([^)]*?))\s*\)|\"([^\"]*)\"|'([^']*)')",
re.IGNORECASE,
)
def srcset_urls(value: str) -> list[str]:
urls: list[str] = []
index = 0
while index < len(value):
while index < len(value) and value[index] in " \t\r\n,":
index += 1
if index >= len(value):
break
start = index
if value.startswith("data:", index):
while index < len(value) and not value[index].isspace():
index += 1
else:
while index < len(value) and not value[index].isspace() and value[index] != ",":
index += 1
url = value[start:index]
if url:
urls.append(url)
while index < len(value) and value[index] != ",":
index += 1
if index < len(value):
index += 1
return urls
def css_url_refs(text: str) -> list[str]:
refs: list[str] = []
for match in CSS_URL_RE.finditer(text):
ref = next((group for group in match.groups() if group is not None), "")
ref = ref.strip()
if ref:
refs.append(ref)
return refs
def css_import_refs(text: str) -> list[str]:
refs: list[str] = []
for match in CSS_IMPORT_RE.finditer(text):
ref = next((group for group in match.groups() if group is not None), "")
ref = ref.strip()
if ref:
refs.append(ref)
return refs
def rel_tokens(value: str) -> set[str]:
return {token.lower() for token in value.split()}
def is_fetched_link_resource(attrs: dict[str, str]) -> bool:
return bool(rel_tokens(attrs.get("rel", "")) & FETCHED_LINK_RELS)
class LinkParser(html.parser.HTMLParser):
def __init__(self) -> None:
super().__init__()
self.ids: set[str] = set()
self.links: list[str] = []
self.link_attrs: list[dict[str, str]] = []
self.assets: list[str] = []
self.style_attrs: list[str] = []
self.style_blocks: list[str] = []
self.meta: list[dict[str, str]] = []
self.svg_use_hrefs: list[str] = []
self._in_style = False
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
attr = {key: value or "" for key, value in attrs}
if "id" in attr:
self.ids.add(attr["id"])
if tag == "a" and "href" in attr:
self.links.append(attr["href"])
self.link_attrs.append(attr)
if tag in {"img", "script", "source"} and "src" in attr:
self.assets.append(attr["src"])
if tag in {"img", "source"} and "srcset" in attr:
self.assets.extend(srcset_urls(attr["srcset"]))
if tag == "video" and "poster" in attr:
self.assets.append(attr["poster"])
if tag == "link" and "href" in attr and is_fetched_link_resource(attr):
self.assets.append(attr["href"])
if tag == "meta":
self.meta.append(attr)
if tag == "use" and "href" in attr:
self.svg_use_hrefs.append(attr["href"])
if "style" in attr:
self.style_attrs.append(attr["style"])
if tag == "style":
self._in_style = True
def handle_data(self, data: str) -> None:
if self._in_style:
self.style_blocks.append(data)
def handle_endtag(self, tag: str) -> None:
if tag == "style":
self._in_style = False
def route_to_file(route: str) -> pathlib.Path:
if route == "/":
return SITE / "index.html"
return SITE / route.strip("/") / "index.html"
def route_to_markdown_file(route: str) -> pathlib.Path:
if route == "/":
return SITE / "index.md"
return SITE / f"{route.strip('/')}.md"
def is_local_file_path(path: str) -> bool:
return pathlib.PurePosixPath(path).suffix != ""
def fail(message: str) -> None:
print(f"FAIL: {message}", file=sys.stderr)
raise SystemExit(1)
def svg_length(value: str, path: pathlib.Path, attr: str) -> float:
match = re.fullmatch(r"\s*([0-9]+(?:\.[0-9]+)?)(?:px)?\s*", value)
if match is None:
fail(f"invalid SVG {attr} value {value!r} in {path}")
return float(match.group(1))
def check_compact_svg_assets() -> None:
for name, max_height in COMPACT_SVG_MAX_HEIGHTS.items():
path = SITE / name
if not path.is_file():
fail(f"missing compact SVG asset {name}")
try:
root = ET.parse(path).getroot()
except ET.ParseError as exc:
fail(f"invalid compact SVG asset {name}: {exc}")
height_attr = root.get("height")
if height_attr is None:
fail(f"compact SVG asset {name} is missing height")
height = svg_length(height_attr, path, "height")
if height > max_height:
fail(f"compact SVG asset {name} is {height:g}px tall, expected at most {max_height:g}px")
def require_site_child(target: pathlib.Path, reference: str, current: pathlib.Path) -> pathlib.Path:
resolved = target.resolve()
site = SITE.resolve()
if not resolved.is_relative_to(site):
fail(f"local reference escapes site output: {reference} in {current}")
return resolved
def target_file(current: pathlib.Path, href: str) -> pathlib.Path | None:
parsed = urllib.parse.urlparse(href)
if parsed.scheme or parsed.netloc:
return None
decoded_path = urllib.parse.unquote(parsed.path)
if decoded_path.startswith("/"):
if is_local_file_path(decoded_path):
return require_site_child(SITE / decoded_path.lstrip("/"), href, current)
route = decoded_path if decoded_path.endswith("/") else decoded_path + "/"
return require_site_child(route_to_file(route), href, current)
base = current.parent
path = decoded_path or current.name
resolved = (base / path).resolve()
require_site_child(resolved, href, current)
if resolved.is_dir():
return require_site_child(resolved / "index.html", href, current)
if resolved.suffix:
return resolved
return require_site_child(resolved / "index.html", href, current)
def parse_html(path: pathlib.Path) -> LinkParser:
parser = LinkParser()
parser.feed(path.read_text(encoding="utf-8"))
return parser
def html_unescape_variants(text: str) -> list[str]:
variants = [text]
current = text
for _ in range(MAX_HTML_UNESCAPE_PASSES):
unescaped = html.unescape(current)
if unescaped == current:
break
variants.append(unescaped)
current = unescaped
return variants
def check_global_metadata(current: pathlib.Path, parser: LinkParser) -> None:
for (kind, name), value in REQUIRED_META.items():
found = any(
meta.get(kind) == name and meta.get("content") == value for meta in parser.meta
)
if not found:
fail(f"missing metadata {kind}={name} with value {value} in {current}")
def check_no_svg_use_href(current: pathlib.Path, parser: LinkParser) -> None:
for href in parser.svg_use_hrefs:
fail(
f"svg <use href={href!r}> found in {current}; SVGUseElement.href is "
"read-only, so Zensical navigation.instant throws while loading the "
"page and site navigation freezes. Use xlink:href instead."
)
def check_discord_header_link(current: pathlib.Path, parser: LinkParser) -> None:
found = any(
link.get("href") == "https://discord.gg/fDnmxB8Wkq"
and "agentsview-discord-link" in link.get("class", "").split()
and link.get("aria-label") == "Join Discord"
for link in parser.link_attrs
)
if not found:
fail(f"missing Discord header link in {current}")
def check_local_asset(current: pathlib.Path, asset: str) -> pathlib.Path | None:
parsed = urllib.parse.urlparse(asset)
if parsed.scheme or parsed.netloc or asset.startswith("data:"):
return None
decoded_path = urllib.parse.unquote(parsed.path)
if not decoded_path:
return None
if decoded_path.startswith("/"):
target = SITE / decoded_path.lstrip("/")
else:
target = current.parent / decoded_path
target = require_site_child(target, asset, current)
if not target.is_file():
fail(f"missing asset {asset} referenced by {current}")
return target
def check_css_text(current: pathlib.Path, text: str, visited: set[pathlib.Path]) -> None:
for asset in css_import_refs(text):
target = check_local_asset(current, asset)
if target is not None and target.suffix.lower() == ".css":
check_css_assets(target, visited)
for asset in css_url_refs(text):
check_local_asset(current, asset)
def check_css_assets(css_file: pathlib.Path, visited: set[pathlib.Path] | None = None) -> None:
if visited is None:
visited = set()
css_file = css_file.resolve()
if css_file in visited:
return
visited.add(css_file)
text = css_file.read_text(encoding="utf-8", errors="ignore")
check_css_text(css_file, text, visited)
def fragment_id(fragment: str) -> str:
return urllib.parse.unquote(fragment)
def main() -> None:
if not SITE.exists():
fail("site directory does not exist. Run the Zensical build first.")
for route in ROUTES:
path = route_to_file(route)
if not path.exists():
fail(f"missing route {route}: {path}")
markdown_path = route_to_markdown_file(route)
if not markdown_path.exists():
fail(f"missing route markdown {route}: {markdown_path}")
if not (SITE / "404.html").exists():
fail("missing 404.html")
if not (SITE / "sitemap.xml").exists():
fail("missing sitemap.xml")
sitemap_text = (SITE / "sitemap.xml").read_text(encoding="utf-8", errors="ignore")
for url in REQUIRED_SITEMAP_URLS:
if f"<loc>{url}</loc>" not in sitemap_text:
fail(f"missing sitemap URL {url}")
html_files = list(SITE.rglob("*.html"))
all_text = "\n".join(
path.read_text(encoding="utf-8", errors="ignore") for path in html_files
)
text_variants = html_unescape_variants(all_text)
for pattern in FORBIDDEN_PATTERNS:
if any(pattern in text for text in text_variants):
fail(f"forbidden generated marker found: {pattern}")
parsed_by_file: dict[pathlib.Path, LinkParser] = {}
for path in html_files:
parsed_by_file[path.resolve()] = parse_html(path)
for current, parser in parsed_by_file.items():
check_global_metadata(current, parser)
check_discord_header_link(current, parser)
check_no_svg_use_href(current, parser)
for spec in REQUIRED_FRAGMENTS:
parsed = urllib.parse.urlparse(spec)
path = route_to_file(parsed.path)
parser = parsed_by_file.get(path.resolve())
if parser is None:
fail(f"required fragment page missing: {spec}")
if fragment_id(parsed.fragment) not in parser.ids:
fail(f"required fragment missing: {spec}")
css_files: set[pathlib.Path] = set()
visited_css: set[pathlib.Path] = set()
for current, parser in parsed_by_file.items():
for href in parser.links:
parsed = urllib.parse.urlparse(href)
if href.startswith("#"):
fragment = fragment_id(parsed.fragment)
if fragment and fragment not in parser.ids:
fail(f"missing local fragment {href} in {current}")
continue
target = target_file(current, href)
if target is None:
continue
if target.suffix == ".html":
if parsed.fragment:
target_parser = parsed_by_file.get(target.resolve())
if target_parser is None:
fail(f"missing linked page for fragment {href} in {current}")
if fragment_id(parsed.fragment) not in target_parser.ids:
fail(f"missing fragment {href} in {target}")
elif not target.exists():
fail(f"missing internal page {href} in {current}")
elif not target.exists():
fail(f"missing linked file {href} in {current}")
for asset in parser.assets:
target = check_local_asset(current, asset)
if target is not None and target.suffix.lower() == ".css":
css_files.add(target)
for css_text in parser.style_attrs:
for asset in css_url_refs(css_text):
check_local_asset(current, asset)
for css_text in parser.style_blocks:
check_css_text(current, css_text, visited_css)
for css_file in css_files:
check_css_assets(css_file, visited_css)
check_compact_svg_assets()
print("built site checks passed")
if __name__ == "__main__":
main()
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
from __future__ import annotations
import pathlib
import re
import sys
ROOT = pathlib.Path(__file__).resolve().parents[1]
FRONTMATTER_LIMIT = 80
ADMONITION_DIRECTIVE_RE = re.compile(
r'!!!\s+[A-Za-z][\w-]*(?:\s+"[^"]+")?\s*'
)
def public_markdown_files() -> list[pathlib.Path]:
return [
path
for path in sorted(ROOT.glob("*.md"))
if path.name != "README.md"
]
def check_frontmatter(path: pathlib.Path, lines: list[str], errors: list[str]) -> None:
if not lines or lines[0] != "---":
errors.append(f"{path.relative_to(ROOT)}: missing YAML frontmatter")
return
closing = next(
(
index
for index, line in enumerate(lines[1:FRONTMATTER_LIMIT], start=1)
if line == "---"
),
None,
)
if closing is None:
errors.append(
f"{path.relative_to(ROOT)}: missing closing YAML frontmatter delimiter"
)
return
frontmatter = "\n".join(lines[1:closing])
if not re.search(r"^title:\s+\S", frontmatter, flags=re.MULTILINE):
errors.append(f"{path.relative_to(ROOT)}: missing title in YAML frontmatter")
if not re.search(r"^description:\s+\S", frontmatter, flags=re.MULTILINE):
errors.append(
f"{path.relative_to(ROOT)}: missing description in YAML frontmatter"
)
def check_admonitions(path: pathlib.Path, lines: list[str], errors: list[str]) -> None:
for index, line in enumerate(lines):
stripped = line.strip()
if not stripped.startswith("!!! "):
continue
if ADMONITION_DIRECTIVE_RE.fullmatch(stripped) is None:
errors.append(
f"{path.relative_to(ROOT)}:{index + 1}: "
"malformed or collapsed admonition"
)
continue
following = next(
(
candidate
for candidate in lines[index + 1 :]
if candidate.strip()
),
None,
)
if following is None or not following.startswith(" "):
errors.append(
f"{path.relative_to(ROOT)}:{index + 1}: "
"admonition body must be indented"
)
def main() -> None:
errors: list[str] = []
for path in public_markdown_files():
lines = path.read_text(encoding="utf-8").splitlines()
check_frontmatter(path, lines, errors)
check_admonitions(path, lines, errors)
if errors:
print("docs markdown source check failed:", file=sys.stderr)
for error in errors:
print(f" {error}", file=sys.stderr)
raise SystemExit(1)
print("docs markdown source checks passed")
if __name__ == "__main__":
main()
+293
View File
@@ -0,0 +1,293 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import math
import pathlib
import sys
from typing import Any
ROOT = pathlib.Path(__file__).resolve().parents[1]
VERCEL = ROOT / "vercel.json"
PERMANENT: dict[str, str] = {}
TEMPORARY = {
"/install.sh": "https://raw.githubusercontent.com/kenn-io/agentsview/main/scripts/install.sh",
"/install.ps1": "https://raw.githubusercontent.com/kenn-io/agentsview/main/scripts/install.ps1",
}
MAX_REDIRECTS = 2048
MAX_CONDITIONS = 16
MAX_SCHEMA_STRING_LENGTH = 4096
MAX_ENV_ITEMS = 64
MAX_ENV_VALUE_LENGTH = 256
ALLOWED_REDIRECT_KEYS = {
"source",
"destination",
"permanent",
"statusCode",
"has",
"missing",
"env",
}
ALLOWED_CONDITION_KEYS = {"type", "key", "value"}
CONDITION_TYPES_REQUIRING_KEY = {"header", "cookie", "query"}
CONDITION_TYPES = {"host", *CONDITION_TYPES_REQUIRING_KEY}
CONDITION_OPERATION_KEYS = {
"eq",
"neq",
"inc",
"ninc",
"pre",
"suf",
"re",
"gt",
"gte",
"lt",
"lte",
}
STRING_CONDITION_OPERATIONS = {"neq", "pre", "suf", "re"}
STRING_LIST_CONDITION_OPERATIONS = {"inc", "ninc"}
NUMBER_CONDITION_OPERATIONS = {"gt", "gte", "lt", "lte"}
def fail(message: str) -> None:
print(f"FAIL: {message}", file=sys.stderr)
raise SystemExit(1)
def reject_json_constant(constant: str) -> None:
raise ValueError(f"non-finite numeric constant {constant}")
def validate_schema_string(path: str, value: str) -> None:
if len(value) > MAX_SCHEMA_STRING_LENGTH:
fail(f"{path} must be at most {MAX_SCHEMA_STRING_LENGTH} characters")
def validate_finite_json_numbers(path: str, value: object) -> None:
if isinstance(value, bool):
return
if isinstance(value, float):
if not math.isfinite(value):
fail(f"{path} contains non-finite number")
return
if isinstance(value, dict):
for key, item in value.items():
validate_finite_json_numbers(f"{path}.{key}", item)
elif isinstance(value, list):
for index, item in enumerate(value):
validate_finite_json_numbers(f"{path}[{index}]", item)
def is_number(value: object) -> bool:
if isinstance(value, bool):
return False
if isinstance(value, int):
return True
return isinstance(value, float) and math.isfinite(value)
def validate_condition_operation_value(path: str, operation: str, value: object) -> None:
if operation == "eq":
if not isinstance(value, str) and not is_number(value):
fail(f"{path} eq must be a string or finite number")
if isinstance(value, str):
validate_schema_string(f"{path} eq", value)
elif operation in STRING_CONDITION_OPERATIONS:
if not isinstance(value, str):
fail(f"{path} {operation} must be a string")
validate_schema_string(f"{path} {operation}", value)
elif operation in STRING_LIST_CONDITION_OPERATIONS:
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
fail(f"{path} {operation} must be a list of strings")
for item_index, item in enumerate(value):
validate_schema_string(f"{path} {operation}[{item_index}]", item)
elif operation in NUMBER_CONDITION_OPERATIONS:
if not is_number(value):
fail(f"{path} {operation} must be a finite number")
def validate_condition_value(path: str, value: object) -> None:
if isinstance(value, str):
validate_schema_string(path, value)
return
if not isinstance(value, dict):
fail(f"{path} must be a string or condition operation object")
if not value:
fail(f"{path} condition operation object must not be empty")
unknown_keys = set(value) - CONDITION_OPERATION_KEYS
if unknown_keys:
keys = ", ".join(sorted(unknown_keys))
fail(f"{path} has unknown operation keys: {keys}")
for operation, operation_value in value.items():
validate_condition_operation_value(path, operation, operation_value)
def validate_condition_list(index: int, item: dict[str, object], key: str) -> None:
if key not in item:
return
conditions = item[key]
if not isinstance(conditions, list):
fail(f"redirect entry {index} {key} must be a list")
if len(conditions) > MAX_CONDITIONS:
fail(f"redirect entry {index} {key} must contain at most {MAX_CONDITIONS} items")
for condition_index, condition in enumerate(conditions):
if not isinstance(condition, dict):
fail(f"redirect entry {index} {key}[{condition_index}] must be an object")
unknown_keys = set(condition) - ALLOWED_CONDITION_KEYS
if unknown_keys:
keys = ", ".join(sorted(unknown_keys))
fail(f"redirect entry {index} {key}[{condition_index}] has unknown keys: {keys}")
condition_type = condition.get("type")
if not isinstance(condition_type, str):
fail(f"redirect entry {index} {key}[{condition_index}] type must be a string")
if condition_type not in CONDITION_TYPES:
fail(f"redirect entry {index} {key}[{condition_index}] has invalid type: {condition_type}")
condition_key = condition.get("key")
if condition_type in CONDITION_TYPES_REQUIRING_KEY:
if not isinstance(condition_key, str) or not condition_key:
fail(f"redirect entry {index} {key}[{condition_index}] missing key")
validate_schema_string(
f"redirect entry {index} {key}[{condition_index}] key",
condition_key,
)
elif "key" in condition:
fail(f"redirect entry {index} {key}[{condition_index}] host condition must not set key")
elif "value" not in condition:
fail(f"redirect entry {index} {key}[{condition_index}] host condition missing value")
if "value" in condition:
validate_condition_value(
f"redirect entry {index} {key}[{condition_index}] value",
condition["value"],
)
def validate_redirect_mode(index: int, item: dict[str, object]) -> None:
has_permanent = "permanent" in item
has_status_code = "statusCode" in item
if not has_permanent and not has_status_code:
fail(f"redirect entry {index} must set permanent or statusCode")
if has_permanent and has_status_code:
fail(f"redirect entry {index} must not set both permanent and statusCode")
if has_permanent and not isinstance(item["permanent"], bool):
fail(f"redirect entry {index} permanent must be boolean")
if has_status_code:
status_code = item["statusCode"]
if not isinstance(status_code, int) or isinstance(status_code, bool):
fail(f"redirect entry {index} statusCode must be an integer")
if status_code < 100 or status_code > 999:
fail(f"redirect entry {index} statusCode must be between 100 and 999")
def validate_redirect_env(index: int, item: dict[str, object]) -> None:
if "env" not in item:
return
env = item["env"]
if not isinstance(env, list):
fail(f"redirect entry {index} env must be a list")
if not env:
fail(f"redirect entry {index} env must not be empty")
if len(env) > MAX_ENV_ITEMS:
fail(f"redirect entry {index} env must contain at most {MAX_ENV_ITEMS} items")
for env_index, value in enumerate(env):
if not isinstance(value, str):
fail(f"redirect entry {index} env[{env_index}] must be a string")
if len(value) > MAX_ENV_VALUE_LENGTH:
fail(
f"redirect entry {index} env[{env_index}] "
f"must be at most {MAX_ENV_VALUE_LENGTH} characters"
)
def collect_redirects(data: dict[str, object]) -> dict[str, dict[str, object]]:
raw_redirects = data.get("redirects", [])
if not isinstance(raw_redirects, list):
fail("vercel redirects must be a list")
if len(raw_redirects) > MAX_REDIRECTS:
fail(f"vercel redirects must contain at most {MAX_REDIRECTS} items")
redirects: dict[str, dict[str, object]] = {}
for index, item in enumerate(raw_redirects):
if not isinstance(item, dict):
fail(f"redirect entry {index} must be an object")
unknown_keys = set(item) - ALLOWED_REDIRECT_KEYS
if unknown_keys:
keys = ", ".join(sorted(unknown_keys))
fail(f"redirect entry {index} has unknown keys: {keys}")
source = item.get("source")
if not isinstance(source, str) or not source:
fail(f"redirect entry {index} missing source")
validate_schema_string(f"redirect entry {index} source", source)
destination = item.get("destination")
if not isinstance(destination, str) or not destination:
fail(f"redirect entry {index} missing destination")
validate_schema_string(f"redirect entry {index} destination", destination)
validate_redirect_mode(index, item)
validate_redirect_env(index, item)
validate_condition_list(index, item, "has")
validate_condition_list(index, item, "missing")
if source in redirects:
fail(f"duplicate redirect source {source}")
redirects[source] = item
return redirects
def load_vercel() -> dict[str, Any]:
try:
data = json.loads(
VERCEL.read_text(encoding="utf-8"),
parse_constant=reject_json_constant,
)
except FileNotFoundError:
fail("missing vercel.json")
except json.JSONDecodeError as error:
fail(f"invalid vercel.json: {error}")
except ValueError as error:
fail(f"invalid vercel.json: {error}")
if not isinstance(data, dict):
fail("vercel.json must contain an object")
validate_finite_json_numbers("vercel.json", data)
return data
def main() -> None:
data = load_vercel()
if "framework" not in data or data["framework"] is not None:
fail("vercel framework must be null")
if data.get("installCommand") != "uv sync --frozen --no-dev":
fail("unexpected Vercel installCommand")
if data.get("buildCommand") != "uv run --frozen bash ./vercel-build.sh":
fail("unexpected Vercel buildCommand")
if data.get("outputDirectory") != "site":
fail("unexpected Vercel outputDirectory")
if data.get("trailingSlash") is not True:
fail("vercel.json must set trailingSlash true")
redirects = collect_redirects(data)
for source, destination in PERMANENT.items():
item = redirects.get(source)
if not item:
fail(f"missing permanent redirect {source}")
if item.get("destination") != destination or item.get("permanent") is not True:
fail(f"incorrect permanent redirect {source}")
for source, destination in TEMPORARY.items():
item = redirects.get(source)
if not item:
fail(f"missing temporary redirect {source}")
if item.get("destination") != destination or item.get("permanent") is not False:
fail(f"incorrect temporary redirect {source}")
print("vercel redirect checks passed")
if __name__ == "__main__":
main()