feat(store): console plugin store, index repo, and the fixes on-glass found
apple / swift (push) Successful in 1m22s
release / apple (push) Successful in 9m59s
windows-host / package (push) Successful in 9m52s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m20s
apple / screenshots (push) Successful in 6m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m21s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m39s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m45s
audit / cargo-audit (push) Successful in 2m58s
audit / bun-audit (push) Successful in 13s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 59s
android / android (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / rust (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
decky / build-publish (push) Successful in 23s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 35s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 49s
flatpak / build-publish (push) Successful in 7m53s
docker / deploy-docs (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
deb / build-publish (push) Successful in 11m41s
deb / build-publish-host (push) Successful in 13m32s

Console: a Plugins section (Browse / Installed / Sources) on a static nav
entry, with install friction proportional to trust — a plain confirm for a
verified entry, a warning naming the curator for an external one, and a
danger dialog that makes you retype the spec for a raw package. Tier badges
are permanent and follow the plugin onto its own UI page.

Index: unom/punktfunk-plugin-index published and served from Gitea's raw
endpoint (real HTTPS, byte-exact, no vhost to stand up) — merge to main is
publish, which resolves the design's open hosting question.

Four things only running it could find:
- runner discovery matched @punktfunk/plugin-* only, so a third-party
  scoped plugin (which D8 requires) would install and never run
- ...and that convention also matches @punktfunk/plugin-kit, a plugin's
  own framework: it listed as installed and would have been imported as a
  unit. Both now key off the plugins dir's top-level dependencies, with an
  emptied dependency list meaning 'nothing installed' rather than falling
  back to the naming convention
- the store must not pass new flags to the runner: the scripting package
  ships separately and an older one reads an unknown flag's value as a
  package name. The host writes the bunfig scope mapping itself
- ureq reports only >= 400 as Err, so a conditional request's 304 arrives
  as Ok with an empty body — handled as an error it made every refresh
  after the first verify a signature over zero bytes and sit stale

Also: the console's first Tabs use exposed an @unom/ui theme gap that
rendered inactive tabs invisible (caught in a browser pass, not by types).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 20:44:05 +02:00
parent 45c3b96907
commit 833f3348a0
30 changed files with 4028 additions and 21 deletions
File diff suppressed because one or more lines are too long
+19
View File
@@ -302,8 +302,27 @@ export const discoverUnits = (
// no scripts dir — fine
}
const modules = path.join(pluginsDir, "node_modules");
// The packages the operator actually installed — `bun add` records them as the plugins dir's
// own `dependencies`. This is what separates a plugin from a plugin's LIBRARY:
// `@punktfunk/plugin-kit` matches the `plugin-*` naming convention exactly but arrives as a
// transitive dependency of every kit-built plugin, and running it as a unit is nonsense.
// `undefined` only when there is no readable `package.json` at all — a hand-assembled tree then
// falls back to the naming convention rather than discovering nothing. A package.json with no
// `dependencies` key yields an EMPTY set, not `undefined`: `bun remove` drops the key when the
// last plugin goes, and orphaned transitive packages can outlive it, so falling back there
// would start running a plugin's library the moment you uninstall the last real plugin.
let topLevel: Set<string> | undefined;
try {
const root = JSON.parse(
fs.readFileSync(path.join(pluginsDir, "package.json"), "utf8"),
) as { dependencies?: Record<string, string> };
topLevel = new Set(Object.keys(root.dependencies ?? {}));
} catch {
// no package.json — fall back to the convention
}
// Read a plugin package's manifest (`module`/`main` entry) and add it as a unit.
const addPlugin = (dir: string, name: string): void => {
if (topLevel && !topLevel.has(name)) return; // a dependency, not an installed plugin
try {
const manifest = JSON.parse(
fs.readFileSync(path.join(dir, "package.json"), "utf8"),
+77
View File
@@ -107,6 +107,83 @@ describe("discovery", () => {
const units = discoverUnits(d);
expect(units.map((u) => u.name)).toEqual(["@punktfunk/plugin-y"]);
});
test("discovers plugins in ANY scope, not just @punktfunk", () => {
// Plugin-store catalog entries must be scoped so the scope can map to that entry's
// registry, so a third-party plugin necessarily arrives under its own scope. Discovery
// limited to @punktfunk would let it install and then never run.
const d = mkdirs("discover-foreign-scope");
write(
path.join(d.pluginsDir, "node_modules", "@retro-hub", "plugin-z", "package.json"),
JSON.stringify({ name: "@retro-hub/plugin-z", main: "index.js" }),
);
write(
path.join(d.pluginsDir, "node_modules", "@retro-hub", "plugin-z", "index.js"),
"export default { name: 'z', main: async () => {} };",
);
expect(discoverUnits(d).map((u) => u.name)).toEqual(["@retro-hub/plugin-z"]);
});
test("a plugin's LIBRARY is not a unit — top-level dependencies win", () => {
// Regression from a live install: `@punktfunk/plugin-kit` is the framework kit-built
// plugins depend on. It matches the `plugin-*` convention exactly and lands in
// node_modules transitively, so a convention-only scan would import and run the framework
// as if it were a plugin.
const d = mkdirs("discover-toplevel");
for (const name of ["plugin-real", "plugin-kit"]) {
write(
path.join(d.pluginsDir, "node_modules", "@punktfunk", name, "package.json"),
JSON.stringify({ name: `@punktfunk/${name}`, main: "index.js" }),
);
write(
path.join(d.pluginsDir, "node_modules", "@punktfunk", name, "index.js"),
"export default { name: 'p', main: async () => {} };",
);
}
write(
path.join(d.pluginsDir, "package.json"),
JSON.stringify({ dependencies: { "@punktfunk/plugin-real": "^1.0.0" } }),
);
expect(discoverUnits(d).map((u) => u.name)).toEqual(["@punktfunk/plugin-real"]);
});
test("no package.json at all falls back to the naming convention", () => {
// Hand-assembled or older trees must still run, not silently discover nothing.
const d = mkdirs("discover-nodeps");
write(
path.join(d.pluginsDir, "node_modules", "punktfunk-plugin-legacy", "package.json"),
JSON.stringify({ name: "punktfunk-plugin-legacy", main: "index.js" }),
);
write(
path.join(d.pluginsDir, "node_modules", "punktfunk-plugin-legacy", "index.js"),
"export default { name: 'l', main: async () => {} };",
);
expect(discoverUnits(d).map((u) => u.name)).toEqual(["punktfunk-plugin-legacy"]);
});
test("an emptied dependency list means nothing to run", () => {
// `bun remove` drops the `dependencies` key when the last plugin goes, while orphaned
// transitive packages linger in node_modules. Falling back to the naming convention there
// would start running a plugin's LIBRARY right after the operator removed the only real
// plugin — seen on-glass.
const d = mkdirs("discover-emptied");
write(
path.join(d.pluginsDir, "node_modules", "@punktfunk", "plugin-kit", "package.json"),
JSON.stringify({ name: "@punktfunk/plugin-kit", main: "index.js" }),
);
write(
path.join(d.pluginsDir, "node_modules", "@punktfunk", "plugin-kit", "index.js"),
"export default {};",
);
write(path.join(d.pluginsDir, "package.json"), JSON.stringify({ name: "plugins" }));
expect(discoverUnits(d)).toEqual([]);
write(
path.join(d.pluginsDir, "package.json"),
JSON.stringify({ name: "plugins", dependencies: {} }),
);
expect(discoverUnits(d)).toEqual([]);
});
});
describe("windowsSddlUnsafeReason (the sshd rule's Windows half, pure)", () => {