From 377debc6a0a978b1b35a3682962725a6a6ccc367 Mon Sep 17 00:00:00 2001 From: xscriptor Date: Tue, 3 Nov 2026 14:28:50 +0100 Subject: [PATCH 1/4] feat: add machine-readable addon catalog --- CHANGELOG.md | 7 ++ index.json | 48 ++++++++ scripts/generate_catalog.py | 219 ++++++++++++++++++++++++++++++++++++ 3 files changed, 274 insertions(+) create mode 100644 index.json create mode 100755 scripts/generate_catalog.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f2c8ad..a16c152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2026-11-03 — Addon catalog index + +- Added root `index.json`, a machine-readable catalog of the four installable effects (`decrypt`, `glitch`, `wasm-matrix`, `wasm-python-pulse`), consumed by `xfetch effects search`. +- Native Rust effects publish their Cargo version and description; wasm guests publish the metadata from their adjacent `xfetch-effect.json`, with `guest_runtime` set to `core` or `component`. +- Added `scripts/generate_catalog.py`, a standard-library-only deterministic generator with `--check` support for CI and local verification. +- No effect code or manifests changed. Crate versions remain 0.2.0. + ## 2026-10-24 — Dependency security automation - Added `.github/workflows/security-audit.yml`: runs `cargo audit` 0.22.2 with `--deny warnings` on push, pull request and a weekly schedule, with actions pinned by commit SHA and no persisted checkout credentials. diff --git a/index.json b/index.json new file mode 100644 index 0000000..e371b0d --- /dev/null +++ b/index.json @@ -0,0 +1,48 @@ +{ + "schema_version": 1, + "catalog_version": "1.0.2", + "entries": [ + { + "name": "decrypt", + "description": "Decrypt intro effect for xfetch: reveals the info lines from scrambled glyphs", + "version": "0.2.0", + "kind": "effect", + "runtime": "native", + "path": "effects/decrypt", + "install": "decrypt", + "capabilities": {} + }, + { + "name": "glitch", + "description": "Glitch intro effect for xfetch: stuttery scrambled flicker with corruption bursts before the content settles", + "version": "0.2.0", + "kind": "effect", + "runtime": "native", + "path": "effects/glitch", + "install": "glitch", + "capabilities": {} + }, + { + "name": "wasm-matrix", + "description": "Matrix-style reveal effect compiled to WebAssembly", + "version": "0.2.0", + "kind": "effect", + "runtime": "wasm", + "guest_runtime": "core", + "path": "effects/wasm-matrix", + "install": "wasm-matrix", + "capabilities": {} + }, + { + "name": "wasm-python-pulse", + "description": "Python WebAssembly effect example built with componentize-py", + "version": "0.2.0", + "kind": "effect", + "runtime": "wasm", + "guest_runtime": "component", + "path": "effects/wasm-python-pulse", + "install": "wasm-python-pulse", + "capabilities": {} + } + ] +} diff --git a/scripts/generate_catalog.py b/scripts/generate_catalog.py new file mode 100755 index 0000000..8687f87 --- /dev/null +++ b/scripts/generate_catalog.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +"""Generate the machine-readable addon catalog for the effects repository. + +Scans ``effects/*/`` and writes ``index.json`` at the repository root. Native +Rust effects take their version and description from ``Cargo.toml``; wasm +guests take theirs from the adjacent ``xfetch-effect.json`` manifest. The +shared ``effects-lib`` crate is not an installable effect and is skipped. + +Usage: + python3 scripts/generate_catalog.py + python3 scripts/generate_catalog.py --check + +Only the Python standard library is used. +""" + +from __future__ import annotations + +import argparse +import difflib +import json +import sys +import tomllib +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parent.parent +EFFECTS_DIR = ROOT / "effects" +OUTPUT = ROOT / "index.json" + +SCHEMA_VERSION = 1 +CATALOG_VERSION = "1.0.2" +EXCLUDED_DIRS = {"effects-lib"} +WASM_RUNTIMES = {"core", "component"} + + +def fail(message: str) -> None: + raise SystemExit(f"generate_catalog: {message}") + + +def read_toml(path: Path) -> dict[str, Any]: + try: + with path.open("rb") as handle: + return tomllib.load(handle) + except OSError as exc: + fail(f"cannot read {path.relative_to(ROOT)}: {exc}") + except tomllib.TOMLDecodeError as exc: + fail(f"invalid TOML in {path.relative_to(ROOT)}: {exc}") + + +def read_json(path: Path) -> dict[str, Any]: + try: + with path.open(encoding="utf-8") as handle: + data = json.load(handle) + except OSError as exc: + fail(f"cannot read {path.relative_to(ROOT)}: {exc}") + except json.JSONDecodeError as exc: + fail(f"invalid JSON in {path.relative_to(ROOT)}: {exc}") + if not isinstance(data, dict): + fail(f"{path.relative_to(ROOT)} must contain a JSON object") + return data + + +def require_string(data: dict[str, Any], key: str, source: Path) -> str: + value = data.get(key) + if not isinstance(value, str) or not value.strip(): + fail(f"{source.relative_to(ROOT)} is missing a non-empty {key!r}") + return value + + +def native_entry(directory: Path) -> dict[str, Any]: + cargo_path = directory / "Cargo.toml" + package = read_toml(cargo_path).get("package") + if not isinstance(package, dict): + fail(f"{cargo_path.relative_to(ROOT)} has no [package] table") + + name = directory.name + return { + "name": name, + "description": require_string(package, "description", cargo_path), + "version": require_string(package, "version", cargo_path), + "kind": "effect", + "runtime": "native", + "path": f"effects/{name}", + "install": name, + "capabilities": {}, + } + + +def wasm_entry(directory: Path, manifest_path: Path) -> dict[str, Any]: + manifest = read_json(manifest_path) + name = directory.name + manifest_name = require_string(manifest, "name", manifest_path) + if manifest_name != name: + fail( + f"{manifest_path.relative_to(ROOT)} declares name {manifest_name!r}, " + f"but its directory is {name!r}" + ) + + guest_runtime = require_string(manifest, "runtime", manifest_path) + if guest_runtime not in WASM_RUNTIMES: + fail( + f"{manifest_path.relative_to(ROOT)} has runtime {guest_runtime!r}; " + f"expected one of {sorted(WASM_RUNTIMES)}" + ) + + version = require_string(manifest, "version", manifest_path) + cargo_path = directory / "Cargo.toml" + if cargo_path.is_file(): + package = read_toml(cargo_path).get("package") + if not isinstance(package, dict): + fail(f"{cargo_path.relative_to(ROOT)} has no [package] table") + cargo_version = require_string(package, "version", cargo_path) + if cargo_version != version: + fail( + f"{cargo_path.relative_to(ROOT)} version {cargo_version!r} does not " + f"match {manifest_path.relative_to(ROOT)} version {version!r}" + ) + + capabilities = manifest.get("capabilities", {}) + if not isinstance(capabilities, dict): + fail(f"{manifest_path.relative_to(ROOT)} capabilities must be an object") + + kind = manifest.get("kind", "effect") + if not isinstance(kind, str) or not kind: + fail(f"{manifest_path.relative_to(ROOT)} has an invalid kind") + + return { + "name": name, + "description": require_string(manifest, "description", manifest_path), + "version": version, + "kind": kind, + "runtime": "wasm", + "guest_runtime": guest_runtime, + "path": f"effects/{name}", + "install": name, + "capabilities": capabilities, + } + + +def build_catalog() -> dict[str, Any]: + if not EFFECTS_DIR.is_dir(): + fail(f"effects directory not found at {EFFECTS_DIR}") + + entries: list[dict[str, Any]] = [] + for directory in sorted(EFFECTS_DIR.iterdir(), key=lambda path: path.name): + if not directory.is_dir() or directory.name.startswith("."): + continue + if directory.name in EXCLUDED_DIRS: + continue + + manifest_path = directory / "xfetch-effect.json" + cargo_path = directory / "Cargo.toml" + if manifest_path.is_file(): + entries.append(wasm_entry(directory, manifest_path)) + elif cargo_path.is_file(): + entries.append(native_entry(directory)) + else: + fail( + f"effects/{directory.name} has neither xfetch-effect.json nor Cargo.toml" + ) + + entries.sort(key=lambda entry: entry["name"]) + names = [entry["name"] for entry in entries] + if len(names) != len(set(names)): + fail(f"duplicate effect names in catalog: {names}") + + return { + "schema_version": SCHEMA_VERSION, + "catalog_version": CATALOG_VERSION, + "entries": entries, + } + + +def render(catalog: dict[str, Any]) -> str: + return json.dumps(catalog, indent=2, ensure_ascii=False) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Generate or verify the effects repository catalog." + ) + parser.add_argument( + "--check", + action="store_true", + help="fail if index.json is missing or differs from the generated catalog", + ) + args = parser.parse_args() + + expected = render(build_catalog()) + + if args.check: + if not OUTPUT.is_file(): + print(f"generate_catalog: {OUTPUT.name} is missing", file=sys.stderr) + return 1 + actual = OUTPUT.read_text(encoding="utf-8") + if actual != expected: + diff = difflib.unified_diff( + actual.splitlines(keepends=True), + expected.splitlines(keepends=True), + fromfile="index.json (current)", + tofile="index.json (generated)", + ) + sys.stderr.writelines(diff) + print( + "generate_catalog: index.json is stale; " + "run python3 scripts/generate_catalog.py", + file=sys.stderr, + ) + return 1 + print("index.json is up to date") + return 0 + + OUTPUT.write_text(expected, encoding="utf-8") + print(f"wrote {OUTPUT.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 3729b1f7f3bbc07e15dd11508a8775f8cfae9cfd Mon Sep 17 00:00:00 2001 From: xscriptor Date: Tue, 3 Nov 2026 14:50:43 +0100 Subject: [PATCH 2/4] chore(deps): update compatible lockfile dependencies --- CHANGELOG.md | 5 +++++ Cargo.lock | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a16c152..328486c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2026-11-03 — Dependency updates + +- Updated the workspace lockfile with semver-compatible patches: `serde_json` 1.0.154, `syn` 3.0.9 and `unicode-ident` 1.0.27. +- No effect code or manifests changed; crate versions remain 0.2.0. The catalog index is unchanged. + ## 2026-11-03 — Addon catalog index - Added root `index.json`, a machine-readable catalog of the four installable effects (`decrypt`, `glitch`, `wasm-matrix`, `wasm-python-pulse`), consumed by `xfetch effects search`. diff --git a/Cargo.lock b/Cargo.lock index 335af46..c2dcfd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,9 +64,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", @@ -77,9 +77,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "xfetch-effect-api" From 963b3930a34e8608bc5be14c0d3a07b6769f35d9 Mon Sep 17 00:00:00 2001 From: xscriptor Date: Tue, 3 Nov 2026 15:05:48 +0100 Subject: [PATCH 3/4] ci: update security-audit workflow actions --- .github/workflows/security-audit.yml | 4 ++-- CHANGELOG.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index 89b1c76..bf63ec0 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -20,12 +20,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install stable Rust toolchain - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable + uses: dtolnay/rust-toolchain@18f0487477426dce244608110aa2660c2893deb7 # stable with: toolchain: stable diff --git a/CHANGELOG.md b/CHANGELOG.md index 328486c..fa0d710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2026-11-03 — Dependency updates - Updated the workspace lockfile with semver-compatible patches: `serde_json` 1.0.154, `syn` 3.0.9 and `unicode-ident` 1.0.27. +- Updated the security-audit workflow to `actions/checkout` 7.0.1 and the pinned `dtolnay/rust-toolchain` commit. - No effect code or manifests changed; crate versions remain 0.2.0. The catalog index is unchanged. ## 2026-11-03 — Addon catalog index From 403803463065fc6aa8bd19deff28227c87f62a5e Mon Sep 17 00:00:00 2001 From: xscriptor Date: Tue, 3 Nov 2026 21:05:34 +0100 Subject: [PATCH 4/4] docs: mark the v1.0.2 release train in the changelog --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa0d710..1996d09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,16 @@ # Changelog -## 2026-11-03 — Dependency updates +## 2026-11-03 — v1.0.2 + +Release train v1.0.2 (catalog schema 1, catalog_version 1.0.2). + +### Dependency updates - Updated the workspace lockfile with semver-compatible patches: `serde_json` 1.0.154, `syn` 3.0.9 and `unicode-ident` 1.0.27. - Updated the security-audit workflow to `actions/checkout` 7.0.1 and the pinned `dtolnay/rust-toolchain` commit. - No effect code or manifests changed; crate versions remain 0.2.0. The catalog index is unchanged. -## 2026-11-03 — Addon catalog index +### Addon catalog index - Added root `index.json`, a machine-readable catalog of the four installable effects (`decrypt`, `glitch`, `wasm-matrix`, `wasm-python-pulse`), consumed by `xfetch effects search`. - Native Rust effects publish their Cargo version and description; wasm guests publish the metadata from their adjacent `xfetch-effect.json`, with `guest_runtime` set to `core` or `component`.