ktg-plugin-marketplace/scripts/check-public-links.mjs
Kjell Tore Guttormsen 71ccd88bf3
feat(catalog): list dynamisk-rettsstat v0.1.0
Operator instruction 2026-11-11. The plugin runs the model of the
concept note Den dynamiske rettsstaten on the reader's own numbers;
its skills and documentation are Norwegian. Manifest entry pinned to
the annotated, signed tag v0.1.0, README block, opening line and
PUBLIC_REPOS change in the same commit, as the public-link gate
requires.

NOT TO BE PUSHED before open/dynamisk-rettsstat is public with the tag:
at commit time the repository exists only locally (ktg/ and open/ both
answer 404 with a token), so this entry would be a broken install. The
forge description of this catalog must be set to the new opening line
in the same round; it still reads "Currently Voyage".

Stat line: 4 skills (skills/*/SKILL.md at the tag) and 260 tests
(pytest at the tag); neither is badged, so check-versions names both
as not gated. check-public-links, check-versions (1/1 OK),
check-retired-refs, check-nav-golden, check-okf-parity exit 0; 244 of
244 tests pass.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 19:41:11 +01:00

75 lines
3 KiB
JavaScript

#!/usr/bin/env node
// Public-link gate: fails when the catalog's public surface (README.md, the marketplace
// manifest) links to an org repository that is not on the tracked list of public repos
// below. A repository that is made private or removed turns every link to it into a dead
// link — and a manifest entry into a broken install — so the catalog may only point at
// repositories that are deliberately listed as public here.
//
// Usage: node scripts/check-public-links.mjs [repo-root]
// exit 0 = checked, 0 hits · exit 1 = hit(s), or 0 links found (verified nothing)
//
// Adding a plugin to the catalog means adding its repository to PUBLIC_REPOS in the same
// commit; removing one means taking it out of both.
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
// Org repositories the catalog may link to — the published plugins plus this catalog.
export const PUBLIC_REPOS = [
'dynamisk-rettsstat',
'ktg-plugin-marketplace',
];
// The files a reader or an installer reaches from the catalog.
export const SURFACE_FILES = ['README.md', '.claude-plugin/marketplace.json'];
const LINK = /git\.fromaitochitta\.com\/open\/([A-Za-z0-9._-]+)/g;
// Returns one { line, repo } per org-repo link in `content`; a trailing `.git` is dropped.
export function extractRepoLinks(content) {
const links = [];
content.split('\n').forEach((text, i) => {
for (const m of text.matchAll(LINK)) links.push({ line: i + 1, repo: m[1].replace(/\.git$/, '') });
});
return links;
}
// files: [{ path, content }]. Returns every link plus the ones not on the public list.
export function scanFiles(files, publicRepos = PUBLIC_REPOS) {
const links = [];
for (const { path, content } of files) {
for (const l of extractRepoLinks(content)) links.push({ path, ...l });
}
return { links, findings: links.filter((l) => !publicRepos.includes(l.repo)) };
}
export function runCheck(root) {
const files = SURFACE_FILES.filter((p) => existsSync(join(root, p))).map((path) => ({
path,
content: readFileSync(join(root, path), 'utf8'),
}));
return { files: files.length, ...scanFiles(files) };
}
function main(argv) {
const root = argv[0] ?? join(HERE, '..');
const { files, links, findings } = runCheck(root);
for (const f of findings) {
console.log(`[ERROR] link to a repository not listed as public — ${f.path}:${f.line}: open/${f.repo}`);
}
console.log(
`check-public-links: ${links.length} org-repo links in ${files}/${SURFACE_FILES.length} files, ` +
`${PUBLIC_REPOS.length} public repos listed — ${findings.length} hit(s)`,
);
if (links.length === 0) {
console.log('check-public-links: 0 links found — verified nothing');
return 1;
}
return findings.length > 0 ? 1 : 0;
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
process.exit(main(process.argv.slice(2)));
}