reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
271
vendor-new/win-dev-skills/scripts/check-catalogs.mjs
Normal file
271
vendor-new/win-dev-skills/scripts/check-catalogs.mjs
Normal file
@@ -0,0 +1,271 @@
|
||||
// Validates the three host catalogs:
|
||||
// .github/plugin/marketplace.json (GitHub Copilot)
|
||||
// .claude-plugin/marketplace.json (Claude Code)
|
||||
// .agents/plugins/marketplace.json (OpenAI Codex)
|
||||
//
|
||||
// - All three list the same plugins, from the same repo at the same pinned commit.
|
||||
// - Every entry resolves: the repo, commit, path, and the host's plugin manifest
|
||||
// exist, and the manifest's name and version match the catalog.
|
||||
// Local entries (./path in this repo) are checked against the working tree.
|
||||
// Set GITHUB_TOKEN to avoid API rate limits. CATALOG_ROOT points the check at
|
||||
// another tree (used by scripts/tests/test-catalog-tools.mjs).
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = process.env.CATALOG_ROOT
|
||||
? path.resolve(process.env.CATALOG_ROOT)
|
||||
: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
||||
const errors = [];
|
||||
const fail = (msg) => errors.push(msg);
|
||||
const SEMVER = /^\d+\.\d+\.\d+$/;
|
||||
const SHA = /^[0-9a-f]{40}$/;
|
||||
|
||||
const AGENT_PLUGINS_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
||||
|
||||
// Manifest lookup order per host. `portable` entries only count when the file
|
||||
// declares the Agent Plugins schema.
|
||||
const hosts = {
|
||||
copilot: {
|
||||
file: ".github/plugin/marketplace.json",
|
||||
// Copilot accepts a manifest at the plugin root or in a host folder.
|
||||
manifests: ["plugin.json", ".github/plugin/plugin.json", ".claude-plugin/plugin.json"],
|
||||
catalogVersion: (c) => c.metadata?.version,
|
||||
},
|
||||
claude: {
|
||||
file: ".claude-plugin/marketplace.json",
|
||||
manifests: [".claude-plugin/plugin.json"],
|
||||
catalogVersion: (c) => c.version,
|
||||
},
|
||||
codex: {
|
||||
file: ".agents/plugins/marketplace.json",
|
||||
// openai/codex utils/plugins/src/plugin_namespace.rs: a portable root
|
||||
// plugin.json first, then the legacy host folders.
|
||||
manifests: [{ file: "plugin.json", portable: true }, ".codex-plugin/plugin.json", ".claude-plugin/plugin.json"],
|
||||
catalogVersion: () => undefined,
|
||||
},
|
||||
};
|
||||
|
||||
function readJson(file) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path.join(repoRoot, file), "utf8"));
|
||||
} catch (e) {
|
||||
fail(`${file}: cannot read JSON (${e.message})`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const trimSlashes = (p) => (p ?? "").replace(/^\.?\/+/, "").replace(/\/+$/, "");
|
||||
|
||||
function repoFromUrl(url) {
|
||||
const m = /^https:\/\/github\.com\/([^/]+\/[^/]+?)(?:\.git)?\/?$/i.exec(url ?? "");
|
||||
return m ? m[1] : undefined;
|
||||
}
|
||||
|
||||
// Normalize a host-specific source into { kind: "local", path } or
|
||||
// { kind: "remote", repo, path, sha, ref }.
|
||||
function normalizeSource(src) {
|
||||
if (typeof src === "string") {
|
||||
return src.startsWith("./") ? { kind: "local", path: trimSlashes(src) } : { kind: "unknown", raw: src };
|
||||
}
|
||||
if (!src || typeof src !== "object") return { kind: "unknown", raw: src };
|
||||
switch (src.source) {
|
||||
case "local":
|
||||
return { kind: "local", path: trimSlashes(src.path) };
|
||||
case "github":
|
||||
return { kind: "remote", repo: src.repo, path: trimSlashes(src.path), sha: src.sha, ref: src.ref };
|
||||
case "url":
|
||||
case "git-subdir":
|
||||
return { kind: "remote", repo: repoFromUrl(src.url), path: trimSlashes(src.path), sha: src.sha ?? src.rev, ref: src.ref };
|
||||
default:
|
||||
return { kind: "unknown", raw: src };
|
||||
}
|
||||
}
|
||||
|
||||
async function gh(url) {
|
||||
const headers = { "User-Agent": "win-dev-skills-catalog-check", Accept: "application/vnd.github+json" };
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
for (let attempt = 1; ; attempt++) {
|
||||
const res = await fetch(url, { headers });
|
||||
if (res.status < 500 || attempt === 3) return res;
|
||||
await new Promise((r) => setTimeout(r, 1000 * attempt));
|
||||
}
|
||||
}
|
||||
|
||||
const commitCache = new Map();
|
||||
async function commitExists(repo, sha) {
|
||||
const key = `${repo}@${sha}`;
|
||||
if (!commitCache.has(key)) {
|
||||
commitCache.set(key, gh(`https://api.github.com/repos/${repo}/commits/${sha}`).then((r) => r.status));
|
||||
}
|
||||
return commitCache.get(key);
|
||||
}
|
||||
|
||||
async function readRemoteFile(repo, sha, file) {
|
||||
const res = await gh(`https://api.github.com/repos/${repo}/contents/${file}?ref=${sha}`);
|
||||
if (res.status === 404) return undefined;
|
||||
if (!res.ok) throw new Error(`GET ${repo}/${file}@${sha}: HTTP ${res.status}`);
|
||||
const body = await res.json();
|
||||
if (Array.isArray(body)) return { dir: true };
|
||||
return { text: Buffer.from(body.content, "base64").toString("utf8") };
|
||||
}
|
||||
|
||||
function readLocalFile(file) {
|
||||
const full = path.join(repoRoot, file);
|
||||
if (!fs.existsSync(full)) return undefined;
|
||||
if (fs.statSync(full).isDirectory()) return { dir: true };
|
||||
return { text: fs.readFileSync(full, "utf8") };
|
||||
}
|
||||
|
||||
async function checkEntry(host, entry, src, expectedVersion) {
|
||||
const where = `${hosts[host].file} → ${entry.name}`;
|
||||
const read = src.kind === "local"
|
||||
? async (f) => readLocalFile(f)
|
||||
: async (f) => readRemoteFile(src.repo, src.sha, f);
|
||||
const label = src.kind === "local" ? `./${src.path}` : `${src.repo}/${src.path}@${src.sha.slice(0, 12)}`;
|
||||
|
||||
const root = src.path ? await read(src.path) : { dir: true };
|
||||
if (!root?.dir) return fail(`${where}: path '${src.path}' not found in ${label}`);
|
||||
|
||||
let manifest;
|
||||
let manifestFile;
|
||||
for (const candidate of hosts[host].manifests) {
|
||||
const rel = typeof candidate === "string" ? candidate : candidate.file;
|
||||
manifestFile = src.path ? `${src.path}/${rel}` : rel;
|
||||
const file = await read(manifestFile);
|
||||
if (file?.text) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(file.text);
|
||||
} catch (e) {
|
||||
return fail(`${where}: ${manifestFile} is not valid JSON (${e.message})`);
|
||||
}
|
||||
if (candidate.portable && parsed.$schema !== AGENT_PLUGINS_SCHEMA) continue;
|
||||
manifest = parsed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!manifest) {
|
||||
const names = hosts[host].manifests.map((c) => (typeof c === "string" ? c : c.file));
|
||||
return fail(`${where}: no plugin manifest (${names.join(", ")}) under ${label}`);
|
||||
}
|
||||
let ok = true;
|
||||
if (manifest.name !== entry.name) {
|
||||
ok = false;
|
||||
fail(`${where}: manifest ${manifestFile} has name '${manifest.name}', catalog says '${entry.name}'`);
|
||||
}
|
||||
if (manifest.version !== expectedVersion) {
|
||||
ok = false;
|
||||
fail(`${where}: manifest ${manifestFile} has version '${manifest.version}', catalog says '${expectedVersion}'`);
|
||||
}
|
||||
if (ok) console.log(`ok ${host.padEnd(7)} ${entry.name}@${manifest.version} ← ${label} (${manifestFile})`);
|
||||
}
|
||||
|
||||
const catalogs = Object.fromEntries(Object.entries(hosts).map(([h, cfg]) => [h, readJson(cfg.file)]));
|
||||
|
||||
// One catalog name everywhere, so `<plugin>@<catalog>` works on every host.
|
||||
const catalogNames = new Set(Object.values(catalogs).map((c) => c.name));
|
||||
if (catalogNames.size !== 1) {
|
||||
fail(`Catalog names differ across hosts: ${Object.entries(catalogs).map(([h, c]) => `${h}=${c.name}`).join(", ")}`);
|
||||
}
|
||||
|
||||
// Catalog-level version: Copilot and Claude carry one; keep them in sync.
|
||||
const catalogVersions = ["copilot", "claude"].map((h) => hosts[h].catalogVersion(catalogs[h]));
|
||||
if (!catalogVersions.every((v) => typeof v === "string" && SEMVER.test(v))) {
|
||||
fail(`Catalog version must be X.Y.Z in ${hosts.copilot.file} (metadata.version) and ${hosts.claude.file} (version); got ${catalogVersions.join(", ")}`);
|
||||
} else if (catalogVersions[0] !== catalogVersions[1]) {
|
||||
fail(`Catalog versions differ: Copilot ${catalogVersions[0]}, Claude ${catalogVersions[1]}`);
|
||||
}
|
||||
|
||||
// Index entries by name per host.
|
||||
const byHost = {};
|
||||
for (const host of Object.keys(hosts)) {
|
||||
const plugins = catalogs[host].plugins;
|
||||
if (!Array.isArray(plugins)) {
|
||||
fail(`${hosts[host].file}: plugins must be an array`);
|
||||
byHost[host] = new Map();
|
||||
continue;
|
||||
}
|
||||
byHost[host] = new Map();
|
||||
for (const entry of plugins) {
|
||||
if (byHost[host].has(entry.name)) fail(`${hosts[host].file}: duplicate plugin '${entry.name}'`);
|
||||
byHost[host].set(entry.name, entry);
|
||||
}
|
||||
}
|
||||
|
||||
const allNames = new Set(Object.values(byHost).flatMap((m) => [...m.keys()]));
|
||||
const checks = [];
|
||||
for (const name of [...allNames].sort()) {
|
||||
const missing = Object.keys(hosts).filter((h) => !byHost[h].has(name));
|
||||
if (missing.length) {
|
||||
fail(`Plugin '${name}' is missing from: ${missing.map((h) => hosts[h].file).join(", ")}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const errorsBefore = errors.length;
|
||||
const version = byHost.copilot.get(name).version;
|
||||
if (typeof version !== "string" || !SEMVER.test(version)) {
|
||||
fail(`${hosts.copilot.file} → ${name}: version must be X.Y.Z, got '${version}'`);
|
||||
}
|
||||
if (byHost.claude.get(name).version !== version) {
|
||||
fail(`Plugin '${name}': Claude catalog version '${byHost.claude.get(name).version}' != Copilot catalog version '${version}'`);
|
||||
}
|
||||
|
||||
const sources = Object.fromEntries(Object.keys(hosts).map((h) => [h, normalizeSource(byHost[h].get(name).source)]));
|
||||
for (const [h, s] of Object.entries(sources)) {
|
||||
if (s.kind === "unknown") fail(`${hosts[h].file} → ${name}: unsupported source ${JSON.stringify(s.raw)}`);
|
||||
if (s.kind === "remote") {
|
||||
if (!s.repo) fail(`${hosts[h].file} → ${name}: remote source must be a github.com repository`);
|
||||
if (!SHA.test(s.sha ?? "")) fail(`${hosts[h].file} → ${name}: remote source must pin a full 40-character commit sha`);
|
||||
}
|
||||
}
|
||||
const kinds = new Set(Object.values(sources).map((s) => s.kind));
|
||||
if (kinds.size > 1) {
|
||||
fail(`Plugin '${name}': hosts disagree on local vs remote source`);
|
||||
continue;
|
||||
}
|
||||
if (kinds.has("remote")) {
|
||||
const pins = new Set(Object.values(sources).map((s) => `${(s.repo ?? "").toLowerCase()}@${s.sha}`));
|
||||
if (pins.size > 1) {
|
||||
fail(`Plugin '${name}': hosts pin different repos/commits: ${[...pins].join(", ")}`);
|
||||
continue;
|
||||
}
|
||||
const refs = new Set(Object.values(sources).map((s) => s.ref ?? ""));
|
||||
if (refs.size > 1) {
|
||||
fail(`Plugin '${name}': hosts use different refs: ${[...refs].map((r) => r || "(none)").join(", ")}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (errors.length > errorsBefore) continue;
|
||||
|
||||
if (kinds.has("remote")) {
|
||||
const { repo, sha, ref } = sources.copilot;
|
||||
checks.push((async () => {
|
||||
const status = await commitExists(repo, sha);
|
||||
if (status !== 200) return fail(`Plugin '${name}': commit ${repo}@${sha} not found (HTTP ${status})`);
|
||||
if (ref) {
|
||||
// A ref (normally a release tag) must still point at the pinned commit.
|
||||
const res = await gh(`https://api.github.com/repos/${repo}/commits/${encodeURIComponent(ref)}`);
|
||||
const resolved = res.ok ? (await res.json()).sha : undefined;
|
||||
if (resolved !== sha) {
|
||||
return fail(`Plugin '${name}': ref '${ref}' in ${repo} resolves to ${resolved ?? `nothing (HTTP ${res.status})`}, but the catalogs pin ${sha}`);
|
||||
}
|
||||
}
|
||||
await Promise.all(Object.entries(sources).map(([h, s]) =>
|
||||
checkEntry(h, byHost[h].get(name), s, version).catch((e) => fail(`${hosts[h].file} → ${name}: ${e.message}`))));
|
||||
})().catch((e) => fail(`Plugin '${name}': ${e.message}`)));
|
||||
} else {
|
||||
for (const [h, s] of Object.entries(sources)) {
|
||||
checks.push(checkEntry(h, byHost[h].get(name), s, version).catch((e) => fail(`${hosts[h].file} → ${name}: ${e.message}`)));
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(checks);
|
||||
|
||||
if (errors.length) {
|
||||
for (const e of errors) console.log(`::error::${e}`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`All ${allNames.size} plugin(s) resolve and match across the three catalogs.`);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "fixture",
|
||||
"plugins": [
|
||||
{ "name": "sample", "source": { "source": "local", "path": "./plugins/sample" } }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "fixture",
|
||||
"owner": { "name": "Fixture" },
|
||||
"description": "Local plugin fixture.",
|
||||
"version": "1.0.0",
|
||||
"plugins": [
|
||||
{ "name": "sample", "description": "Sample local plugin.", "version": "1.2.3", "source": "./plugins/sample" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "fixture",
|
||||
"owner": { "name": "Fixture" },
|
||||
"metadata": { "description": "Local plugin fixture.", "version": "1.0.0" },
|
||||
"plugins": [
|
||||
{ "name": "sample", "description": "Sample local plugin.", "version": "1.2.3", "source": "./plugins/sample" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "sample",
|
||||
"version": "1.2.3",
|
||||
"description": "Sample local plugin."
|
||||
}
|
||||
6
vendor-new/win-dev-skills/scripts/tests/fixtures/local-plugin/plugins/sample/plugin.json
vendored
Normal file
6
vendor-new/win-dev-skills/scripts/tests/fixtures/local-plugin/plugins/sample/plugin.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"name": "sample",
|
||||
"version": "1.2.3",
|
||||
"description": "Sample local plugin."
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: sample-skill
|
||||
description: Sample skill used to test the catalog tooling. Use when testing CI. Not for real work.
|
||||
---
|
||||
|
||||
# Sample skill
|
||||
|
||||
This skill exists only to exercise the catalog check and lint.
|
||||
@@ -0,0 +1,47 @@
|
||||
// Proves the catalog tooling handles a plugin stored in this repo, using the
|
||||
// fixture in scripts/tests/fixtures/local-plugin. Needs `npm ci --prefix scripts/vally`.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const fixture = path.join(here, "fixtures", "local-plugin");
|
||||
const checkScript = path.join(here, "..", "check-catalogs.mjs");
|
||||
const lintScript = path.join(here, "..", "vally", "lint-skills.mjs");
|
||||
|
||||
let failed = 0;
|
||||
function expect(label, script, root, wantExit, wantText) {
|
||||
const r = spawnSync(process.execPath, [script], { env: { ...process.env, CATALOG_ROOT: root }, encoding: "utf8" });
|
||||
const out = `${r.stdout}${r.stderr}`;
|
||||
const ok = r.status === wantExit && (!wantText || out.includes(wantText));
|
||||
console.log(`${ok ? "pass" : "FAIL"} ${label} (exit ${r.status})`);
|
||||
if (!ok) {
|
||||
failed++;
|
||||
console.log(out);
|
||||
}
|
||||
}
|
||||
|
||||
function mutatedCopy(file, from, to) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "catalog-fixture-"));
|
||||
fs.cpSync(fixture, dir, { recursive: true });
|
||||
const target = path.join(dir, file);
|
||||
fs.writeFileSync(target, fs.readFileSync(target, "utf8").replace(from, to));
|
||||
return dir;
|
||||
}
|
||||
|
||||
expect("catalog check accepts a local plugin", checkScript, fixture, 0, "ok codex sample@1.2.3 ← ./plugins/sample (plugins/sample/plugin.json)");
|
||||
expect("vally lints the local plugin", lintScript, fixture, 0, "sample-skill");
|
||||
|
||||
const badVersion = mutatedCopy("plugins/sample/.claude-plugin/plugin.json", '"1.2.3"', '"1.2.4"');
|
||||
expect("catalog check rejects a manifest version mismatch", checkScript, badVersion, 1, "has version '1.2.4'");
|
||||
|
||||
const missingCodex = mutatedCopy(".agents/plugins/marketplace.json", '"name": "sample"', '"name": "other"');
|
||||
expect("catalog check rejects catalogs that disagree", checkScript, missingCodex, 1, "is missing from");
|
||||
|
||||
const badSkill = mutatedCopy("plugins/sample/skills/sample-skill/SKILL.md", "name: sample-skill", "name: Not A Valid Name");
|
||||
expect("vally rejects an invalid skill", lintScript, badSkill, 1);
|
||||
|
||||
for (const dir of [badVersion, missingCodex, badSkill]) fs.rmSync(dir, { recursive: true, force: true });
|
||||
process.exitCode = failed ? 1 : 0;
|
||||
@@ -0,0 +1,134 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repositoryRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"..",
|
||||
"..",
|
||||
);
|
||||
const validator = path.join(
|
||||
repositoryRoot,
|
||||
"plugins",
|
||||
"windbg",
|
||||
"skills",
|
||||
"windbg-diagnostic-method",
|
||||
"scripts",
|
||||
"validate-diagnosis-output.ps1",
|
||||
);
|
||||
const temporaryDirectory = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "diagnosis-validator-"),
|
||||
);
|
||||
|
||||
const validReport = `## Analysis
|
||||
Observed evidence.
|
||||
|
||||
## Root Cause
|
||||
Candidate cause.
|
||||
|
||||
## Fix
|
||||
Candidate fix.
|
||||
|
||||
## Reasoning Chain
|
||||
OBSERVE: evidence.
|
||||
|
||||
## Alternatives Considered
|
||||
- Alternative one
|
||||
- Alternative two
|
||||
|
||||
## Trigger Verification
|
||||
| Evidence | Status |
|
||||
|---|---|
|
||||
| Trigger | VERIFIED |
|
||||
|
||||
## Mermaid
|
||||
\`\`\`mermaid
|
||||
sequenceDiagram
|
||||
A->>B: request
|
||||
B-->>A: response
|
||||
\`\`\`
|
||||
|
||||
## Contrarian Verdict
|
||||
ACCEPTED
|
||||
|
||||
contrarian_loopback: false
|
||||
|
||||
## JSON Output Contract Summary
|
||||
\`\`\`json
|
||||
{"diagnosis_status":"final","routing_path":"validate","root_cause":"cause","confidence":0.8,"fix_confidence":0.7,"fix_code_path_coverage":"symbol-or-disassembly","contrarian_review":"ACCEPTED","contrarian_loopback":false}
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const invalidReport = `## Analysis
|
||||
The words OBSERVE and VERIFIED appear outside their required sections.
|
||||
|
||||
## Root Cause
|
||||
Candidate cause.
|
||||
|
||||
## Fix
|
||||
Candidate fix.
|
||||
|
||||
## Reasoning Chain
|
||||
No phase marker appears here.
|
||||
|
||||
## Alternatives Considered
|
||||
- Only one alternative
|
||||
|
||||
## Trigger Verification
|
||||
No table appears here.
|
||||
|
||||
## Mermaid
|
||||
\`\`\`mermaid
|
||||
sequenceDiagram
|
||||
participant A
|
||||
\`\`\`
|
||||
|
||||
## Contrarian Verdict
|
||||
ACCEPTED
|
||||
|
||||
contrarian_loopback: 1
|
||||
|
||||
## JSON Output Contract Summary
|
||||
\`\`\`json
|
||||
{"diagnosis_status":"final","routing_path":"validate","root_cause":"cause","confidence":0.8,"fix_confidence":0.7,"fix_code_path_coverage":"symbol-or-disassembly","contrarian_review":"ACCEPTED","contrarian_loopback":1}
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
function runValidator(file) {
|
||||
return spawnSync(
|
||||
"pwsh",
|
||||
["-NoProfile", "-File", validator, "-Path", file],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const validPath = path.join(temporaryDirectory, "valid.md");
|
||||
const invalidPath = path.join(temporaryDirectory, "invalid.md");
|
||||
fs.writeFileSync(validPath, validReport);
|
||||
fs.writeFileSync(invalidPath, invalidReport);
|
||||
|
||||
const validResult = runValidator(validPath);
|
||||
assert.equal(validResult.status, 0, validResult.stderr || validResult.stdout);
|
||||
const validOutput = JSON.parse(validResult.stdout);
|
||||
assert.equal(validOutput.Pass, true);
|
||||
assert.equal(validOutput.Checks.MermaidDiagram, true);
|
||||
|
||||
const invalidResult = runValidator(invalidPath);
|
||||
assert.equal(invalidResult.status, 1, invalidResult.stderr);
|
||||
const invalidOutput = JSON.parse(invalidResult.stdout);
|
||||
assert.equal(invalidOutput.Pass, false);
|
||||
assert.equal(invalidOutput.Checks.ReasoningChain, false);
|
||||
assert.equal(invalidOutput.Checks.Alternatives, false);
|
||||
assert.equal(invalidOutput.Checks.TriggerVerification, false);
|
||||
assert.equal(invalidOutput.Checks.MermaidDiagram, false);
|
||||
assert.equal(invalidOutput.Checks.ContrarianVerdict, false);
|
||||
assert.equal(invalidOutput.Checks.JsonSummary, false);
|
||||
|
||||
console.log("Diagnosis validator accepts valid reports and rejects malformed sections.");
|
||||
} finally {
|
||||
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
||||
}
|
||||
128
vendor-new/win-dev-skills/scripts/validate-windbg.mjs
Normal file
128
vendor-new/win-dev-skills/scripts/validate-windbg.mjs
Normal file
@@ -0,0 +1,128 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repositoryRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"..",
|
||||
);
|
||||
const pluginRoot = path.join(
|
||||
repositoryRoot,
|
||||
"plugins",
|
||||
"windbg",
|
||||
);
|
||||
const require = createRequire(
|
||||
path.join(repositoryRoot, "scripts", "vally", "package.json"),
|
||||
);
|
||||
const { parseDocument } = require("yaml");
|
||||
|
||||
const inventory = JSON.parse(
|
||||
fs.readFileSync(path.join(pluginRoot, "skills.json"), "utf8"),
|
||||
);
|
||||
assert.deepEqual(inventory.methodSkills, ["windbg-diagnostic-method"]);
|
||||
|
||||
const allowedSkills = [
|
||||
...inventory.diagnosticSkills,
|
||||
...inventory.methodSkills,
|
||||
].sort();
|
||||
const skillDirectories = fs
|
||||
.readdirSync(path.join(pluginRoot, "skills"), { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
assert.deepEqual(skillDirectories, allowedSkills);
|
||||
|
||||
const allowedUrlHosts = new Set([
|
||||
"agent-plugins.org",
|
||||
"devblogs.microsoft.com",
|
||||
"github.com",
|
||||
"learn.microsoft.com",
|
||||
]);
|
||||
|
||||
function assertPublicText(text, label) {
|
||||
for (const match of text.matchAll(/https?:\/\/[^\s<>"'`]+/g)) {
|
||||
const value = match[0].replace(/[),.;:]+$/, "");
|
||||
const host = new URL(value).hostname.toLowerCase();
|
||||
assert.ok(
|
||||
allowedUrlHosts.has(host),
|
||||
`${label} references unapproved URL host ${host}`,
|
||||
);
|
||||
}
|
||||
|
||||
assert.doesNotMatch(
|
||||
text,
|
||||
/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i,
|
||||
`${label} contains an email address`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const skill of skillDirectories) {
|
||||
const relativePath = path.join("skills", skill, "SKILL.md");
|
||||
const text = fs.readFileSync(path.join(pluginRoot, relativePath), "utf8");
|
||||
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
||||
assert.ok(frontmatter, relativePath);
|
||||
|
||||
const document = parseDocument(frontmatter[1], { uniqueKeys: true });
|
||||
assert.equal(document.errors.length, 0, relativePath);
|
||||
|
||||
const header = document.toJS();
|
||||
assert.equal(header.name, skill);
|
||||
assert.ok(!("owner" in header));
|
||||
assert.ok(!("metadata" in header));
|
||||
assert.ok(!("version" in header));
|
||||
assertPublicText(text, relativePath);
|
||||
}
|
||||
|
||||
assert.ok(!fs.existsSync(path.join(pluginRoot, ".mcp.json")));
|
||||
assert.ok(!fs.existsSync(path.join(pluginRoot, "mcp")));
|
||||
|
||||
const workflowFiles = [
|
||||
"README.md",
|
||||
"FEEDBACK.md",
|
||||
"com.github.copilot/agents/diagnostician.agent.md",
|
||||
"com.github.copilot/agents/contrarian.agent.md",
|
||||
"skills/windbg-diagnostic-method/SKILL.md",
|
||||
];
|
||||
|
||||
for (const relativePath of workflowFiles) {
|
||||
const text = fs.readFileSync(path.join(pluginRoot, relativePath), "utf8");
|
||||
assertPublicText(text, relativePath);
|
||||
}
|
||||
|
||||
const confidenceContractFiles = [
|
||||
"README.md",
|
||||
"com.github.copilot/agents/diagnostician.agent.md",
|
||||
"com.github.copilot/agents/contrarian.agent.md",
|
||||
"skills/windbg-diagnostic-method/SKILL.md",
|
||||
"skills/windbg-diagnostic-method/scripts/validate-diagnosis-output.ps1",
|
||||
];
|
||||
|
||||
for (const relativePath of confidenceContractFiles) {
|
||||
const text = fs.readFileSync(path.join(pluginRoot, relativePath), "utf8");
|
||||
assert.match(text, /fix_confidence/, `${relativePath} omits fix_confidence`);
|
||||
assert.match(
|
||||
text,
|
||||
/fix_code_path_coverage/,
|
||||
`${relativePath} omits fix_code_path_coverage`,
|
||||
);
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
fs.existsSync(
|
||||
path.join(
|
||||
pluginRoot,
|
||||
"skills",
|
||||
"windbg-diagnostic-method",
|
||||
"scripts",
|
||||
"validate-diagnosis-output.ps1",
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Validated ${inventory.diagnosticSkills.length} diagnostic skills, ` +
|
||||
`${inventory.methodSkills.length} method skill, agent workflow, ` +
|
||||
"validator script, and public-only links.",
|
||||
);
|
||||
40
vendor-new/win-dev-skills/scripts/vally/lint-skills.mjs
Normal file
40
vendor-new/win-dev-skills/scripts/vally/lint-skills.mjs
Normal file
@@ -0,0 +1,40 @@
|
||||
// Runs the same vally lint that marketplaces such as github/awesome-copilot
|
||||
// apply to external plugins, on every LOCAL plugin in the Copilot catalog
|
||||
// (entries whose source is a ./path in this repo). Plugins hosted in other
|
||||
// repositories are linted there.
|
||||
// Mirrors awesome-copilot's eng/external-plugin-quality-gates.mjs: lint each
|
||||
// skills path declared in plugin.json, or the plugin root when none is declared.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { runLint, LintConsoleReporter } from "@microsoft/vally";
|
||||
|
||||
const repoRoot = process.env.CATALOG_ROOT
|
||||
? path.resolve(process.env.CATALOG_ROOT)
|
||||
: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const catalog = JSON.parse(fs.readFileSync(path.join(repoRoot, ".github", "plugin", "marketplace.json"), "utf8"));
|
||||
|
||||
const localRoots = (catalog.plugins ?? [])
|
||||
.map((p) => (typeof p.source === "string" ? p.source : p.source?.source === "local" ? p.source.path : undefined))
|
||||
.filter((s) => typeof s === "string" && s.startsWith("./"))
|
||||
.map((s) => path.resolve(repoRoot, s));
|
||||
|
||||
if (localRoots.length === 0) {
|
||||
console.log("No local plugins in the catalog; nothing to lint.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let passed = true;
|
||||
for (const pluginRoot of localRoots) {
|
||||
const manifestPath = path.join(pluginRoot, "plugin.json");
|
||||
const manifest = fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, "utf8")) : {};
|
||||
const skillPaths = [].concat(manifest.skills ?? [])
|
||||
.map((p) => path.resolve(pluginRoot, p))
|
||||
.filter((p) => fs.existsSync(p) && fs.statSync(p).isDirectory());
|
||||
for (const target of skillPaths.length > 0 ? skillPaths : [pluginRoot]) {
|
||||
const result = await runLint({ rootPath: target });
|
||||
await new LintConsoleReporter({ verbose: true, stream: process.stdout }).report(result);
|
||||
passed &&= result.passed;
|
||||
}
|
||||
}
|
||||
process.exit(passed ? 0 : 1);
|
||||
1149
vendor-new/win-dev-skills/scripts/vally/package-lock.json
generated
Normal file
1149
vendor-new/win-dev-skills/scripts/vally/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
vendor-new/win-dev-skills/scripts/vally/package.json
Normal file
15
vendor-new/win-dev-skills/scripts/vally/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "win-dev-skills-vally-lint",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Marketplace skill lint (vally), pinned to the version github/awesome-copilot uses.",
|
||||
"scripts": {
|
||||
"lint": "node lint-skills.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/vally": "0.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user