From ff4afbbfb76ed98d405a557b37a2d8d685eaf3c0 Mon Sep 17 00:00:00 2001 From: Al Amin Date: Thu, 15 Oct 2026 21:56:50 +0200 Subject: [PATCH] fix: move the tooling out of the plugin root and close checker gaps Claude Code ran npm ci on the root package.json in every plugin install. Front matter is now parsed as YAML, as agents do, and marketplace sources and the Gemini context file must exist. --- .github/workflows/validate.yml | 5 +++++ README.md | 2 +- package.json | 14 -------------- scripts/check-manifests.mjs | 19 +++++++++++++------ scripts/check-manifests.test.mjs | 18 ++++++++++++++++++ .../package-lock.json | 19 ++++++++++++++++++- scripts/package.json | 17 +++++++++++++++++ 7 files changed, 72 insertions(+), 22 deletions(-) delete mode 100644 package.json rename package-lock.json => scripts/package-lock.json (84%) create mode 100644 scripts/package.json diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 5f3c182..1054c94 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,8 +17,13 @@ jobs: with: node-version: 22 cache: npm + cache-dependency-path: scripts/package-lock.json + # The tooling lives in scripts/ so plugin installs don't run npm ci on it. - run: npm ci + working-directory: scripts - run: npm test + working-directory: scripts - run: npm run check + working-directory: scripts - name: Validate Claude Code plugin run: npx -y @anthropic-ai/claude-code@2.1.293 plugin validate . diff --git a/README.md b/README.md index c0d6153..e378c25 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ The key gives full access to its workspace. `-g` writes it to your user config i ## Developing -Run `npm install`, then `npm test` and `npm run check`. CI runs the same checks. +In `scripts/`, run `npm install`, then `npm test` and `npm run check`. CI runs the same checks. The tooling lives there so plugin installs don't install its dependencies. The Claude Code plugin declares its server in `.claude-plugin/plugin.json`, so it can use the URL you enter. `.mcp.json` holds the baserow.io URL for other tools that read Claude-format plugins, such as GitHub Copilot CLI and Cursor. diff --git a/package.json b/package.json deleted file mode 100644 index cdcf1c5..0000000 --- a/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "baserow-mcp", - "private": true, - "type": "module", - "scripts": { - "test": "node --test scripts/*.test.mjs", - "check": "node scripts/check-manifests.mjs" - }, - "devDependencies": { - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1" - }, - "engines": { "node": ">=22" } -} diff --git a/scripts/check-manifests.mjs b/scripts/check-manifests.mjs index 4afe75a..be8e254 100644 --- a/scripts/check-manifests.mjs +++ b/scripts/check-manifests.mjs @@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url"; import Ajv from "ajv"; import Ajv2020 from "ajv/dist/2020.js"; import addFormats from "ajv-formats"; +import { parse as parseYaml } from "yaml"; export const SERVER_NAME = "baserow"; const CLAUDE_URL = "${user_config.mcp_url}"; @@ -230,11 +231,14 @@ export async function checkSkills(root) { errors.push(`${file}: front matter missing`); continue; } - const field = (k) => - front[1] - .match(new RegExp(`^${k}:[ \\t]*(.*)$`, "m"))?.[1] - .trim() - .replace(/^(["'])(.*)\1$/, "$2"); + let meta; + try { + meta = parseYaml(front[1]); + } catch (e) { + errors.push(`${file}: front matter isn't valid YAML (${e.message.split("\n")[0]})`); + continue; + } + const field = (k) => (typeof meta?.[k] === "string" ? meta[k].trim() : undefined); const name = field("name"); if (!name) errors.push(`${file}: name missing`); else if (name !== dir) errors.push(`${file}: name "${name}" must match folder "${dir}"`); @@ -243,12 +247,15 @@ export async function checkSkills(root) { return errors; } -// Every logo and file a listing points at must exist in the repo. +// Every logo, file and plugin folder a manifest points at must exist in the repo. export async function checkAssets(m, root) { const ui = m.agentPlugin?.extensions?.["com.openai"]?.interface ?? {}; const refs = [ ...["logo", "logoDark", "composerIcon", "composerIconDark"].map((k) => [FILES.agentPlugin, `interface.${k}`, ui[k]]), ...["logo", "mcpServers", "skills"].map((k) => [FILES.cursorPlugin, k, m.cursorPlugin?.[k]]), + [FILES.gemini, "contextFileName", m.gemini?.contextFileName], + [FILES.claudeMarketplace, "plugins[0].source", m.claudeMarketplace?.plugins?.[0]?.source], + [FILES.codexMarketplace, "plugins[0].source.path", m.codexMarketplace?.plugins?.[0]?.source?.path], ]; const errors = []; for (const [file, key, rel] of refs) { diff --git a/scripts/check-manifests.test.mjs b/scripts/check-manifests.test.mjs index 89f7766..8de9749 100644 --- a/scripts/check-manifests.test.mjs +++ b/scripts/check-manifests.test.mjs @@ -11,6 +11,7 @@ import { checkSchemas, checkSkills, loadManifests, + runAll, } from "./check-manifests.mjs"; const SCHEMA_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "schemas"); @@ -290,3 +291,20 @@ test("registry URL variables must match the URL", () => { assert.match(errors, /uses \{baserow_host\}, which isn't in its variables/); assert.match(errors, /declares host, which its URL doesn't use/); }); + +test("front matter that isn't valid YAML fails", async (t) => { + const root = await repoWithSkill(t, "baserow-basics", "---\nname: baserow-basics\ndescription: Use when: the user asks\n---\n"); + assert.match((await checkSkills(root)).join("\n"), /front matter isn't valid YAML/); +}); + +test("a marketplace source that doesn't exist is reported", async (t) => { + const root = await tempDir(t, "source-"); + const m = validManifests(); + m.claudeMarketplace.plugins[0].source = "./plugins/baserow"; + assert.ok((await checkAssets(m, root)).includes(".claude-plugin/marketplace.json: plugins[0].source ./plugins/baserow not found")); +}); + +test("the repo itself passes every check", async () => { + const root = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); + assert.deepEqual(await runAll(root), []); +}); diff --git a/package-lock.json b/scripts/package-lock.json similarity index 84% rename from package-lock.json rename to scripts/package-lock.json index e1cf7f4..321fcc7 100644 --- a/package-lock.json +++ b/scripts/package-lock.json @@ -7,7 +7,8 @@ "name": "baserow-mcp", "devDependencies": { "ajv": "^8.17.1", - "ajv-formats": "^3.0.1" + "ajv-formats": "^3.0.1", + "yaml": "^2.9.1" }, "engines": { "node": ">=22" @@ -88,6 +89,22 @@ "engines": { "node": ">=0.10.0" } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } } } } diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..51444d5 --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,17 @@ +{ + "name": "baserow-mcp", + "private": true, + "type": "module", + "scripts": { + "test": "node --test *.test.mjs", + "check": "node check-manifests.mjs .." + }, + "devDependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "yaml": "^2.9.1" + }, + "engines": { + "node": ">=22" + } +}