#!/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 = [ 'ai-psychosis', 'config-audit', 'graceful-handoff', 'human-friendly-style', 'ktg-plugin-marketplace', 'linkedin-studio', 'llm-security', 'ms-ai-architect', 'okr', 'repo-mailbox', 'repo-standard', 'voyage', ]; // 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))); }