📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -101,7 +101,9 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const report = message.content[0]?.text ?? "";
|
||||
// The first block is not always the text one — an endpoint that returns a
|
||||
// thinking block first would otherwise yield undefined.
|
||||
const report = message.content.find((block) => block.type === "text")?.text ?? "";
|
||||
|
||||
const scoreMatch = report.match(/Health\s+Score[:\s]+(\d+)/i);
|
||||
const score = scoreMatch ? parseInt(scoreMatch[1], 10) : null;
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
// Generates the README star-history chart from first-party GitHub data.
|
||||
//
|
||||
// GitHub restricted the public stargazers API to a repository's own admins and
|
||||
// collaborators (announced 2026-06-30), which broke every third-party chart
|
||||
// service — api.star-history.com now serves an error card instead of a chart.
|
||||
// We own this repo, so we read the star data ourselves and commit the result,
|
||||
// keeping the README free of any third-party image host.
|
||||
//
|
||||
// The committed dataset, not the drawing, is the source of truth: the SVG is a
|
||||
// pure function of assets/star-history.json. That keeps the chart re-renderable
|
||||
// with no credentials if the endpoint tightens further, keeps the weekly diff
|
||||
// readable (dates, not shifted path coordinates), and lets `npm run validate`
|
||||
// prove the two files agree.
|
||||
//
|
||||
// Run: node scripts/gen-star-history.mjs → refetch, rewrite both files
|
||||
// node scripts/gen-star-history.mjs --render-only → redraw the SVG offline
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
// README-only assets, so they live in assets/ alone — unlike the banners, the
|
||||
// GitHub Pages site under docs/ does not render them.
|
||||
const OUT = join(ROOT, "assets", "star-history.svg");
|
||||
const DATA = join(ROOT, "assets", "star-history.json");
|
||||
|
||||
const REPO = process.env.GITHUB_REPOSITORY ?? "hyhmrright/brooks-lint";
|
||||
|
||||
const W = 800, H = 400;
|
||||
const PAD = { top: 44, right: 24, bottom: 48, left: 64 };
|
||||
const PLOT_W = W - PAD.left - PAD.right;
|
||||
const PLOT_H = H - PAD.top - PAD.bottom;
|
||||
const ACCENT = "#3b82f6"; // matches the logo palette used by gen-banner.mjs
|
||||
const FONT = `-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif`;
|
||||
|
||||
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
|
||||
// The stargazers endpoint is no longer public: it needs credentials that can
|
||||
// read this repo. CI supplies GITHUB_TOKEN; locally we borrow the gh CLI's.
|
||||
function resolveToken() {
|
||||
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
||||
try {
|
||||
return execFileSync("gh", ["auth", "token"], { encoding: "utf8" }).trim();
|
||||
} catch {
|
||||
throw new Error(
|
||||
"No GITHUB_TOKEN set and `gh auth token` failed. Since GitHub restricted " +
|
||||
"the stargazers API, this script needs credentials for a repo admin or collaborator.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the raw starred_at strings, oldest first — exactly what we commit.
|
||||
async function fetchStarTimestamps(token) {
|
||||
const stamps = [];
|
||||
for (let page = 1; ; page++) {
|
||||
const url = `https://api.github.com/repos/${REPO}/stargazers?per_page=100&page=${page}`;
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
// The star+json media type is what adds starred_at to each entry.
|
||||
Accept: "application/vnd.github.star+json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"User-Agent": "brooks-lint-gen-star-history",
|
||||
},
|
||||
});
|
||||
// Past 400 pages GitHub answers 422 rather than paginating, so a repo above
|
||||
// 40,000 stars can no longer be read in full — the committed dataset is what
|
||||
// preserves the history when that day comes.
|
||||
if (!res.ok) {
|
||||
throw new Error(`GitHub API ${res.status} on page ${page}: ${(await res.text()).slice(0, 200)}`);
|
||||
}
|
||||
const batch = await res.json();
|
||||
for (const entry of batch) {
|
||||
// A missing starred_at means the star+json media type stopped being
|
||||
// honoured. Fail loudly rather than plot NaN coordinates.
|
||||
if (Number.isNaN(Date.parse(entry.starred_at))) {
|
||||
throw new Error(`Stargazer without a usable starred_at on page ${page}.`);
|
||||
}
|
||||
stamps.push(entry.starred_at);
|
||||
}
|
||||
if (batch.length < 100) return stamps.sort((a, b) => Date.parse(a) - Date.parse(b));
|
||||
}
|
||||
}
|
||||
|
||||
export function readStamps() {
|
||||
return JSON.parse(readFileSync(DATA, "utf8")).starredAt;
|
||||
}
|
||||
|
||||
function writeStamps(stamps) {
|
||||
// Deliberately no generated-at field: the file has to stay byte-identical when
|
||||
// no star was added, or the workflow's "commit only when it moved" guard would
|
||||
// fire every single run. Git already records when it last changed.
|
||||
writeFileSync(DATA, `${JSON.stringify({ repo: REPO, starredAt: stamps }, null, 2)}\n`);
|
||||
}
|
||||
|
||||
// Round the axis maximum up to a 1/2/5 × 10ⁿ step so tick labels stay readable.
|
||||
function niceStep(max, targetTicks) {
|
||||
const raw = max / targetTicks;
|
||||
const mag = 10 ** Math.floor(Math.log10(raw));
|
||||
for (const m of [1, 2, 5]) if (raw <= m * mag) return m * mag;
|
||||
return 10 * mag;
|
||||
}
|
||||
|
||||
// One tick per month, thinned out so labels never collide on a long history.
|
||||
function monthTicks(from, to) {
|
||||
const all = [];
|
||||
const cursor = new Date(from);
|
||||
cursor.setUTCDate(1);
|
||||
cursor.setUTCHours(0, 0, 0, 0);
|
||||
if (cursor.getTime() < from) cursor.setUTCMonth(cursor.getUTCMonth() + 1);
|
||||
while (cursor.getTime() <= to) {
|
||||
all.push(cursor.getTime());
|
||||
cursor.setUTCMonth(cursor.getUTCMonth() + 1);
|
||||
}
|
||||
const stride = Math.ceil(all.length / 8) || 1;
|
||||
return all.filter((_, i) => i % stride === 0);
|
||||
}
|
||||
|
||||
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
|
||||
function monthLabel(ms, showYear) {
|
||||
const d = new Date(ms);
|
||||
const month = MONTHS[d.getUTCMonth()];
|
||||
return showYear ? `${month} ${d.getUTCFullYear()}` : month;
|
||||
}
|
||||
|
||||
// 1,392 points would bloat the committed SVG for no visible gain. Keep every
|
||||
// nth sample plus the exact final point, so the headline count stays truthful.
|
||||
function downsample(points, limit) {
|
||||
if (points.length <= limit) return points;
|
||||
const stride = Math.ceil(points.length / limit);
|
||||
const kept = points.filter((_, i) => i % stride === 0);
|
||||
if (kept.at(-1) !== points.at(-1)) kept.push(points.at(-1));
|
||||
return kept;
|
||||
}
|
||||
|
||||
export function render(stamps) {
|
||||
const times = stamps.map((iso) => Date.parse(iso));
|
||||
const total = times.length;
|
||||
const t0 = times[0];
|
||||
// The axis ends at the newest star rather than at "now". Anchoring it to the
|
||||
// clock would shift every x coordinate on every run, so the workflow could
|
||||
// never tell a real change from a redraw and would commit noise weekly.
|
||||
const t1 = times.at(-1);
|
||||
// Stars are whole numbers, so never let a tiny repo produce fractional ticks.
|
||||
const step = Math.max(1, niceStep(total, 5));
|
||||
const yMax = Math.ceil(total / step) * step;
|
||||
|
||||
const x = (ms) => PAD.left + ((ms - t0) / (t1 - t0)) * PLOT_W;
|
||||
const y = (n) => PAD.top + PLOT_H - (n / yMax) * PLOT_H;
|
||||
|
||||
const points = downsample(
|
||||
times.map((ms, i) => [ms, i + 1]),
|
||||
300,
|
||||
);
|
||||
const line = points.map(([ms, n], i) => `${i === 0 ? "M" : "L"}${x(ms).toFixed(1)} ${y(n).toFixed(1)}`).join("");
|
||||
const area = `${line}L${x(t1).toFixed(1)} ${y(0).toFixed(1)}L${x(t0).toFixed(1)} ${y(0).toFixed(1)}Z`;
|
||||
|
||||
const yTicks = [];
|
||||
for (let n = 0; n <= yMax; n += step) yTicks.push(n);
|
||||
|
||||
const xTicks = monthTicks(t0, t1);
|
||||
const spansYears = new Date(t0).getUTCFullYear() !== new Date(t1).getUTCFullYear();
|
||||
|
||||
const gridLines = yTicks
|
||||
.map(
|
||||
(n) =>
|
||||
`<line x1="${PAD.left}" y1="${y(n).toFixed(1)}" x2="${PAD.left + PLOT_W}" y2="${y(n).toFixed(1)}" class="grid"/>` +
|
||||
`<text x="${PAD.left - 10}" y="${(y(n) + 4).toFixed(1)}" class="tick" text-anchor="end">${n.toLocaleString("en-US")}</text>`,
|
||||
)
|
||||
.join("\n ");
|
||||
|
||||
const xLabels = xTicks
|
||||
.map(
|
||||
(ms, i) =>
|
||||
`<text x="${x(ms).toFixed(1)}" y="${PAD.top + PLOT_H + 22}" class="tick" text-anchor="middle">` +
|
||||
`${monthLabel(ms, spansYears || i === 0)}</text>`,
|
||||
)
|
||||
.join("\n ");
|
||||
|
||||
// Anchoring the axis to the newest star puts the final point exactly on the
|
||||
// right edge, so the callout always hangs back inside the plot.
|
||||
const lastX = PAD.left + PLOT_W;
|
||||
const lastY = y(total);
|
||||
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="Star history for ${esc(REPO)}: ${total} stars">
|
||||
<style>
|
||||
.bg { fill: #ffffff; }
|
||||
.title { fill: #111827; font: 600 16px ${FONT}; }
|
||||
.sub { fill: #6b7280; font: 400 12px ${FONT}; }
|
||||
.tick { fill: #6b7280; font: 400 11px ${FONT}; }
|
||||
.grid { stroke: #e5e7eb; stroke-width: 1; }
|
||||
.axis { stroke: #d1d5db; stroke-width: 1; }
|
||||
.total { fill: ${ACCENT}; font: 600 13px ${FONT}; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.bg { fill: #0d1117; }
|
||||
.title { fill: #e6edf3; }
|
||||
.sub, .tick { fill: #8b949e; }
|
||||
.grid { stroke: #21262d; }
|
||||
.axis { stroke: #30363d; }
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<linearGradient id="fade" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="${ACCENT}" stop-opacity="0.28"/>
|
||||
<stop offset="100%" stop-color="${ACCENT}" stop-opacity="0.02"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" class="bg"/>
|
||||
<text x="${PAD.left}" y="26" class="title">Star History</text>
|
||||
<text x="${W - PAD.right}" y="26" class="sub" text-anchor="end">${esc(REPO)}</text>
|
||||
<g>
|
||||
${gridLines}
|
||||
</g>
|
||||
<line x1="${PAD.left}" y1="${PAD.top + PLOT_H}" x2="${PAD.left + PLOT_W}" y2="${PAD.top + PLOT_H}" class="axis"/>
|
||||
<path d="${area}" fill="url(#fade)"/>
|
||||
<path d="${line}" fill="none" stroke="${ACCENT}" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="${lastX.toFixed(1)}" cy="${lastY.toFixed(1)}" r="4" fill="${ACCENT}"/>
|
||||
<text x="${(lastX - 12).toFixed(1)}" y="${(lastY + 4).toFixed(1)}" class="total" text-anchor="end">${total.toLocaleString("en-US")}</text>
|
||||
<g>
|
||||
${xLabels}
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const renderOnly = process.argv.includes("--render-only");
|
||||
const stamps = renderOnly ? readStamps() : await fetchStarTimestamps(resolveToken());
|
||||
// Two points are the minimum a time axis can span; one would divide by zero
|
||||
// and silently write a chart full of NaN coordinates.
|
||||
if (stamps.length < 2) throw new Error(`Only ${stamps.length} stargazer(s) for ${REPO} — refusing to write a chart.`);
|
||||
if (!renderOnly) writeStamps(stamps);
|
||||
writeFileSync(OUT, render(stamps));
|
||||
console.log(`Wrote ${OUT} — ${stamps.length} stars through ${stamps.at(-1).slice(0, 10)}`);
|
||||
}
|
||||
|
||||
// Importable so `npm run validate` can re-render from the committed data and
|
||||
// prove the SVG is in sync; only a direct run touches the network or disk.
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) await main();
|
||||
@@ -13,8 +13,9 @@
|
||||
# ./scripts/install.sh <platform> [--project]
|
||||
# curl -fsSL https://raw.githubusercontent.com/hyhmrright/brooks-lint/main/scripts/install.sh | bash -s -- <platform>
|
||||
#
|
||||
# Platforms: opencode cursor windsurf antigravity pi kiro copilot droid gemini codex claude agents
|
||||
# agents = the vendor-neutral ~/.agents/skills folder (read by Cursor, Copilot, pi, Gemini, Codex)
|
||||
# Platforms: opencode cursor windsurf antigravity pi kiro copilot droid dsh gemini codex claude agents
|
||||
# agents = the vendor-neutral ~/.agents/skills folder (read by Cursor, Copilot, pi, Gemini,
|
||||
# Codex, and DeepSeek Harness)
|
||||
#
|
||||
# Flags:
|
||||
# --project install into the current repo (./.<platform>/skills) instead of the global folder
|
||||
@@ -24,7 +25,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="https://github.com/hyhmrright/brooks-lint.git"
|
||||
PLATFORMS="opencode cursor windsurf antigravity pi kiro copilot droid gemini codex claude agents"
|
||||
PLATFORMS="opencode cursor windsurf antigravity pi kiro copilot droid dsh gemini codex claude agents"
|
||||
|
||||
err() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; }
|
||||
info() { printf '\033[36m›\033[0m %s\n' "$*"; }
|
||||
@@ -59,6 +60,8 @@ global_dir() {
|
||||
kiro) printf '%s' "$HOME/.kiro/skills" ;;
|
||||
copilot) printf '%s' "$HOME/.copilot/skills" ;;
|
||||
droid) printf '%s' "$HOME/.factory/skills" ;;
|
||||
# DeepSeek Harness resolves its config root from $DSH_HOME, falling back to ~/.dsh.
|
||||
dsh) printf '%s' "${DSH_HOME:-$HOME/.dsh}/skills" ;;
|
||||
gemini) printf '%s' "$HOME/.gemini/skills" ;;
|
||||
codex) printf '%s' "$HOME/.codex/skills" ;;
|
||||
claude) printf '%s' "$HOME/.claude/skills" ;;
|
||||
@@ -77,6 +80,7 @@ project_dir() {
|
||||
kiro) printf '%s' "$PWD/.kiro/skills" ;;
|
||||
copilot) printf '%s' "$PWD/.github/skills" ;;
|
||||
droid) printf '%s' "$PWD/.factory/skills" ;;
|
||||
dsh) printf '%s' "$PWD/.dsh/skills" ;;
|
||||
gemini) printf '%s' "$PWD/.gemini/skills" ;;
|
||||
codex) printf '%s' "$PWD/.codex/skills" ;;
|
||||
claude) printf '%s' "$PWD/.claude/skills" ;;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Platform-inventory helpers shared by validate-repo.mjs and its tests.
|
||||
*
|
||||
* Nothing here hardcodes a platform or a translation. The platforms carrying an
|
||||
* install-table row are discovered from the docs/<name>-setup.md files on disk,
|
||||
* the documents that must show that table are discovered from README*.md, and
|
||||
* the installer's own list is parsed out of scripts/install.sh — so a new
|
||||
* platform, or a seventh language, is covered on arrival. Keeping a separate
|
||||
* hand-maintained list is what let the localized README badges go stale before
|
||||
* (see version-refs.mjs).
|
||||
*/
|
||||
|
||||
import { readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* Every document that carries the per-platform install table: all README
|
||||
* translations, plus the getting-started guide, which is the one non-README
|
||||
* page with a platform table of its own.
|
||||
*/
|
||||
export function platformDocs(root) {
|
||||
const readmes = readdirSync(root)
|
||||
.filter((file) => file.startsWith("README") && file.endsWith(".md"))
|
||||
.sort();
|
||||
return [...readmes, path.join("docs", "getting-started.md")];
|
||||
}
|
||||
|
||||
/** Every per-platform setup guide, as bare filenames. */
|
||||
export function setupGuides(root) {
|
||||
return readdirSync(path.join(root, "docs"))
|
||||
.filter((file) => file.endsWith("-setup.md"))
|
||||
.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup-guide filenames linked from one document, normalized so a README's
|
||||
* `docs/kiro-setup.md` and getting-started's sibling `kiro-setup.md` compare
|
||||
* equal. Returns a sorted, de-duplicated array.
|
||||
*/
|
||||
export function linkedSetupGuides(text) {
|
||||
const links = text.matchAll(/\((?:docs\/)?([a-z0-9-]+-setup\.md)\)/g);
|
||||
return [...new Set([...links].map((match) => match[1]))].sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* The installer's platform list plus the platforms each directory-mapping
|
||||
* function actually handles. A platform in PLATFORMS with no case arm fails
|
||||
* `install.sh <platform>` with "unknown platform"; a case arm missing from
|
||||
* PLATFORMS is invisible in --list and the help text.
|
||||
*/
|
||||
export function parseInstallerPlatforms(text) {
|
||||
const declared = text.match(/^PLATFORMS="([^"]*)"/m)?.[1].trim();
|
||||
return {
|
||||
declared: declared ? declared.split(/\s+/) : [],
|
||||
global: caseArms(text, "global_dir"),
|
||||
project: caseArms(text, "project_dir"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Platform names matched by the `case` arms of one shell function. */
|
||||
function caseArms(text, fnName) {
|
||||
const body = text.match(new RegExp(`^${fnName}\\(\\) \\{$([\\s\\S]*?)^\\}$`, "m"))?.[1] ?? "";
|
||||
return [...body.matchAll(/^\s+([a-z][a-z0-9-]*)\)/gm)].map((match) => match[1]);
|
||||
}
|
||||
@@ -81,7 +81,7 @@ for (const scenario of scenarios) {
|
||||
system: systemPrompt,
|
||||
messages: [{ role: "user", content: userMessage }],
|
||||
});
|
||||
aiText = message.content[0]?.text ?? "";
|
||||
aiText = message.content.find((block) => block.type === "text")?.text ?? "";
|
||||
verdict = classify(scenario, aiText);
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
} from "./frontmatter.mjs";
|
||||
import { GUIDE_BY_MODE, VALID_MODES } from "./assemble-prompt.mjs";
|
||||
import { versionRefs } from "./version-refs.mjs";
|
||||
import { platformDocs, setupGuides, linkedSetupGuides, parseInstallerPlatforms } from "./platforms.mjs";
|
||||
import { render as renderStarHistory, readStamps as readStarStamps } from "./gen-star-history.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, "..");
|
||||
@@ -325,6 +327,49 @@ function checkAgentsDocs() {
|
||||
}
|
||||
}
|
||||
|
||||
// Every docs/<name>-setup.md must be linked from all six READMEs and the
|
||||
// getting-started table. A platform added to one language and forgotten in the
|
||||
// others was previously caught only by hand.
|
||||
function checkPlatformDocs() {
|
||||
const guides = setupGuides(root);
|
||||
check(guides.length > 0, "docs/ should contain at least one <platform>-setup.md guide");
|
||||
|
||||
for (const file of platformDocs(root)) {
|
||||
const linked = linkedSetupGuides(readText(file));
|
||||
for (const guide of guides) {
|
||||
check(linked.includes(guide), `${file} is missing an install-table link to docs/${guide}`);
|
||||
}
|
||||
for (const link of linked) {
|
||||
check(guides.includes(link), `${file} links to docs/${link}, which does not exist`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkInstallerPlatforms() {
|
||||
const { declared, global: globalArms, project } = parseInstallerPlatforms(readText("scripts/install.sh"));
|
||||
check(declared.length > 0, "scripts/install.sh should declare a PLATFORMS list");
|
||||
|
||||
for (const [arms, fn] of [[globalArms, "global_dir"], [project, "project_dir"]]) {
|
||||
for (const platform of declared) {
|
||||
check(arms.includes(platform), `scripts/install.sh ${fn}() has no path for PLATFORMS entry '${platform}'`);
|
||||
}
|
||||
for (const platform of arms) {
|
||||
check(declared.includes(platform), `scripts/install.sh ${fn}() maps '${platform}', which PLATFORMS omits`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// assets/star-history.svg is a pure function of assets/star-history.json, so a
|
||||
// mismatch means the chart was hand-edited or the data moved without a redraw.
|
||||
// Re-rendering here needs no credentials, which is the point of committing the
|
||||
// data rather than the drawing alone.
|
||||
function checkStarHistory() {
|
||||
check(
|
||||
renderStarHistory(readStarStamps()) === readText("assets/star-history.svg"),
|
||||
"assets/star-history.svg does not match assets/star-history.json — rerun `node scripts/gen-star-history.mjs --render-only`",
|
||||
);
|
||||
}
|
||||
|
||||
function checkSecurity() {
|
||||
const security = readText("SECURITY.md");
|
||||
check(!security.includes("<!--"), "SECURITY.md still contains placeholder content");
|
||||
@@ -373,7 +418,10 @@ checkStepAlignment();
|
||||
checkEvalSuite();
|
||||
checkContributing();
|
||||
checkAgentsDocs();
|
||||
checkPlatformDocs();
|
||||
checkInstallerPlatforms();
|
||||
checkSecurity();
|
||||
checkStarHistory();
|
||||
checkHookOutput();
|
||||
|
||||
// ── Report ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -31,6 +31,7 @@ import { reportToSarif } from "./sarif.mjs";
|
||||
import { severityBreached, isRegression } from "./ci-gate.mjs";
|
||||
import { summarize } from "./benchmark.mjs";
|
||||
import { versionRefs } from "./version-refs.mjs";
|
||||
import { linkedSetupGuides, parseInstallerPlatforms } from "./platforms.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -905,6 +906,70 @@ test("patterns match the real badge and JSON-LD shapes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
console.log("\nlinkedSetupGuides");
|
||||
|
||||
test("collects guides from README-style and sibling-style links alike", () => {
|
||||
const text = "| Kiro | [setup](docs/kiro-setup.md) |\n| pi | [pi-setup.md](pi-setup.md) |";
|
||||
assert.deepEqual(linkedSetupGuides(text), ["kiro-setup.md", "pi-setup.md"]);
|
||||
});
|
||||
|
||||
test("de-duplicates a guide linked more than once", () => {
|
||||
const text = "[a](docs/dsh-setup.md) … [b](dsh-setup.md)";
|
||||
assert.deepEqual(linkedSetupGuides(text), ["dsh-setup.md"]);
|
||||
});
|
||||
|
||||
test("returns an empty array when no guide is linked", () => {
|
||||
assert.deepEqual(linkedSetupGuides("no links here"), []);
|
||||
});
|
||||
|
||||
console.log("\nparseInstallerPlatforms");
|
||||
|
||||
const INSTALLER_FIXTURE = [
|
||||
'PLATFORMS="kiro dsh"',
|
||||
"",
|
||||
"global_dir() {",
|
||||
" case $1 in",
|
||||
" kiro) printf '%s' \"$HOME/.kiro/skills\" ;;",
|
||||
" # DeepSeek Harness resolves its config root from $DSH_HOME.",
|
||||
" dsh) printf '%s' \"${DSH_HOME:-$HOME/.dsh}/skills\" ;;",
|
||||
" *) return 1 ;;",
|
||||
" esac",
|
||||
"}",
|
||||
"",
|
||||
"project_dir() {",
|
||||
" case $1 in",
|
||||
" kiro) printf '%s' \"$PWD/.kiro/skills\" ;;",
|
||||
" *) return 1 ;;",
|
||||
" esac",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
test("reads the declared list and both directory mappings", () => {
|
||||
const parsed = parseInstallerPlatforms(INSTALLER_FIXTURE);
|
||||
assert.deepEqual(parsed.declared, ["kiro", "dsh"]);
|
||||
assert.deepEqual(parsed.global, ["kiro", "dsh"]);
|
||||
});
|
||||
|
||||
test("omits a platform whose case arm is missing, so the validator can catch it", () => {
|
||||
// project_dir() in the fixture handles kiro but not dsh — running
|
||||
// `install.sh dsh --project` would die with "unknown platform".
|
||||
assert.deepEqual(parseInstallerPlatforms(INSTALLER_FIXTURE).project, ["kiro"]);
|
||||
});
|
||||
|
||||
test("skips comments and the catch-all arm", () => {
|
||||
const { global: arms } = parseInstallerPlatforms(INSTALLER_FIXTURE);
|
||||
assert.ok(!arms.includes("*"));
|
||||
assert.equal(arms.length, 2);
|
||||
});
|
||||
|
||||
test("parses the real installer, proving the patterns still match", () => {
|
||||
const installer = readFileSync(path.join(__dirname, "install.sh"), "utf8");
|
||||
const { declared, global: globalArms, project } = parseInstallerPlatforms(installer);
|
||||
assert.ok(declared.length >= 12, `expected the full platform list, got ${declared.length}`);
|
||||
assert.deepEqual(new Set(globalArms), new Set(declared));
|
||||
assert.deepEqual(new Set(project), new Set(declared));
|
||||
});
|
||||
|
||||
// ── Integration: validate-repo.mjs passes against current repo ─────────────
|
||||
|
||||
console.log("\nvalidate-repo integration");
|
||||
|
||||
Reference in New Issue
Block a user