From a08399085f650a6a561edc7fee6304df3224fc31 Mon Sep 17 00:00:00 2001 From: Padreug Date: Mon, 14 Sep 2026 15:34:56 +0200 Subject: [PATCH] docs: add CLAUDE.md (catalog rules, validation snippet, commit convention) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015RQhTgskDx8LLpD47rzLRS --- CLAUDE.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..796fd17 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,105 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repo is + +A single-file catalog: `extensions.json` is the manifest LNbits hosts read +via `LNBITS_EXTENSIONS_MANIFESTS`. No server code, no build, no tests, no +tags. Every commit on `main` is live for every host that pulls the raw URL +(`…/aiolabs/lnbits-extensions/raw/branch/main/extensions.json`), including +bohm's native lnbits and the regtest compose stack. Hosts pick up changes +on lnbits restart, then upgrade via the admin UI. Audit trail is `git log`; +rollback is `git revert`. + +This is an **infra repo: commit straight to `main`**, no PR. The release +procedure that leads up to a catalog commit (merge PRs in the extension +repo, `chore(release)` bump, test on dev lnbits, tag, hash) lives in +`~/dev/CLAUDE.md` under "Extension release procedure". This file only +covers the catalog side. + +## Manifest shape + +``` +{ "featured": [, …], "extensions": [, …] } +``` + +Required entry fields: `id`, `repo`, `name`, `version`, `short_description`, +`archive`, `hash` (sha256 of the archive zip). Optional: `icon`, +`min_lnbits_version`, `max_lnbits_version`, `details_link`. Many entries +omit `min_lnbits_version` entirely; copy the previous entry for the same +`id` and change only `version`, `archive`, `hash`. + +Entries are grouped by `id` in the file. Insert a new version directly after +the last existing entry for that `id`, not at the end of the array. + +## Rules that bite + +- **Add, never overwrite.** A new version is a new object alongside the old + ones; installs on the previous version must still resolve it. The one + historical exception (`lnurlp: replace v1.3.0-aio.1 with v1.3.0-aio.2`) + was a same-day fix of a broken tag. +- **Keep 4-space indent and `ensure_ascii=False`.** A 2-space re-serialise + once turned a one-entry bump into a whole-file diff and needed a follow-up + `chore: restore 4-space JSON formatting` commit. Hand-edit the block, or + round-trip with the snippet below (verified byte-identical). +- **Aio forks point at `git.atitlan.io/aiolabs/`, never GitHub.** The + three `github.com/lnbits/lnurlp` entries (1.1.3, 1.2.0, 1.3.0) predate the + fork and stay only for installs that still run them. Don't add new + upstream entries for any extension that has an aio fork; production only + sees this manifest, so an upstream URL here bypasses our vetting. +- **Aio semver must outrank the upstream tag** (`v1.3.0-aio.2` > `v1.3.0`). + Dev instances also see LNbits's built-in catalog; if upstream wins the + semver comparison, clicking Upgrade overwrites the git checkout under + `~/dev/shared/extensions/`. +- **`hash` is the integrity gate.** LNbits refuses to install when the + downloaded archive's sha256 differs. Always recompute from the tag's + archive URL; never copy a hash from a previous entry. + +## Commands + +Compute the hash for a tagged release (Forgejo archive URL form): + +```sh +curl -sL https://git.atitlan.io/aiolabs//archive/v.zip | sha256sum +``` + +Validate the manifest before committing (parse, required fields, duplicate +`id`+`version`, and that the `archive` URL's tag matches `version`): + +```sh +python3 - <<'EOF' +import json, sys +d = json.load(open("extensions.json")) +req = {"id","repo","name","version","short_description","archive","hash"} +seen = set(); bad = 0 +for e in d["extensions"]: + key = (e.get("id"), e.get("version")) + for m in (req - e.keys(), ["dup"] if key in seen else [], ["archive!=version"] if f"v{e.get('version')}.zip" not in e.get("archive","") else []): + if m: print(key, m); bad += 1 + seen.add(key) +sys.exit(bad) +EOF +``` + +Re-serialise without formatting drift (only if editing programmatically): + +```sh +python3 -c " +import json +d = json.load(open('extensions.json')) +open('extensions.json','w').write(json.dumps(d, indent=4, ensure_ascii=False) + '\n')" +``` + +Verify a committed entry matches the live archive: + +```sh +git diff -U0 HEAD~1 | grep -oE '"(archive|hash)": "[^"]+"' +``` + +## Commit message convention + +One entry per commit, subject `: add v ()`, +e.g. `events: add v1.6.1-aio.15 (promo codes enforced)`. The parenthetical +is the only human-readable changelog the catalog has, so name the user-visible +change, not "bump". Formatting-only fixes use `chore:`.