Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/security-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## 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.

### 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.
Expand Down
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions index.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
]
}
219 changes: 219 additions & 0 deletions scripts/generate_catalog.py
Original file line number Diff line number Diff line change
@@ -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())
Loading