diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b70e3e8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Keep every checked-out text file LF, on every platform. +# +# Without this, Git on Windows (core.autocrlf=true by default) rewrites files to +# CRLF on checkout, and the plugin/skill validators -- which parse YAML +# frontmatter with `\n`-anchored patterns -- report valid files as invalid. +* text=auto eol=lf + +*.png binary +*.jpg binary +*.svg text eol=lf diff --git a/packages/claude-plugin/scripts/check-plugin.mjs b/packages/claude-plugin/scripts/check-plugin.mjs index f74fd6c..f44a8fd 100644 --- a/packages/claude-plugin/scripts/check-plugin.mjs +++ b/packages/claude-plugin/scripts/check-plugin.mjs @@ -2,6 +2,13 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +// Paths go into failure messages with forward slashes on every platform, so +// the output is stable for tests, CI log greps and docs comparisons. Filesystem +// access still uses the native separator. +function displayPath(value) { + return String(value).split(path.sep).join("/"); +} + const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const DEFAULT_PACKAGE_ROOT = path.resolve(SCRIPT_DIR, ".."); const DEFAULT_REPO_ROOT = path.resolve(DEFAULT_PACKAGE_ROOT, "../.."); @@ -19,14 +26,14 @@ const EXPECTED_REFERENCE_FILE = "references/commands.md"; function readJson(filePath, failures) { if (!fs.existsSync(filePath)) { - failures.push(`Missing ${filePath}`); + failures.push(`Missing ${displayPath(filePath)}`); return null; } try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { - failures.push(`Invalid JSON at ${filePath}: ${error.message}`); + failures.push(`Invalid JSON at ${displayPath(filePath)}: ${error.message}`); return null; } } @@ -38,12 +45,12 @@ function assert(condition, failures, message) { } function extractFrontmatter(markdown) { - const match = /^---\n([\s\S]*?)\n---\n?/u.exec(markdown); + const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u.exec(markdown); return match ? match[1] : null; } function frontmatterValue(frontmatter, key) { - const match = new RegExp(`^${key}:\\s*([^\\n]+)\\s*$`, "mu").exec(frontmatter); + const match = new RegExp(`^${key}:\\s*([^\\r\\n]+)\\s*$`, "mu").exec(frontmatter); return match?.[1]?.trim().replace(/^['"]|['"]$/g, "") ?? null; } @@ -90,38 +97,38 @@ function checkNoMcp({ packageRoot, failures }) { } function assertCliGuidance({ source, filePath, failures }) { - assert(source.includes(`CALLE_SOURCE=${EXPECTED_CLI_SOURCE}`), failures, `${filePath} must include Claude CLI source attribution.`); + assert(source.includes(`CALLE_SOURCE=${EXPECTED_CLI_SOURCE}`), failures, `${displayPath(filePath)} must include Claude CLI source attribution.`); assert( source.includes(`CALLE_INTEGRATION=${EXPECTED_CLI_INTEGRATION}`), failures, - `${filePath} must include Claude CLI integration attribution.`, + `${displayPath(filePath)} must include Claude CLI integration attribution.`, ); - assert(source.includes("node packages/cli/bin/calle.js"), failures, `${filePath} must document the repository-local CLI command.`); - assert(source.includes("npx -y @call-e/cli"), failures, `${filePath} must document the npx CLI fallback.`); - assert(source.includes("auth status"), failures, `${filePath} must document auth status checks.`); - assert(source.includes("Run blocking `auth login`"), failures, `${filePath} must document blocking authorization login.`); + assert(source.includes("node packages/cli/bin/calle.js"), failures, `${displayPath(filePath)} must document the repository-local CLI command.`); + assert(source.includes("npx -y @call-e/cli"), failures, `${displayPath(filePath)} must document the npx CLI fallback.`); + assert(source.includes("auth status"), failures, `${displayPath(filePath)} must document auth status checks.`); + assert(source.includes("Run blocking `auth login`"), failures, `${displayPath(filePath)} must document blocking authorization login.`); assert( source.includes("do not ask the user to reply"), failures, - `${filePath} must document that browser authorization should continue without a manual chat reply.`, + `${displayPath(filePath)} must document that browser authorization should continue without a manual chat reply.`, ); assert( source.includes("assistant_hint.message"), failures, - `${filePath} must document how to display assistant_hint.message after auth login.`, + `${displayPath(filePath)} must document how to display assistant_hint.message after auth login.`, ); assert( source.includes("Before we start, please complete authorization here"), failures, - `${filePath} must include the first authorization help message.`, + `${displayPath(filePath)} must include the first authorization help message.`, ); - assert(source.includes("Great, authorization is complete"), failures, `${filePath} must include the post-authorization success message.`); - assert(source.includes("mcp tools"), failures, `${filePath} must document CLI tool discovery.`); - assert(source.includes("call plan"), failures, `${filePath} must document call planning through the CLI.`); - assert(source.includes("call run"), failures, `${filePath} must document planned call execution through the CLI.`); - assert(source.includes("call status"), failures, `${filePath} must document call status polling through the CLI.`); - assert(!source.includes("/mcp"), failures, `${filePath} must not direct users to /mcp for CALL-E authorization.`); - assert(!source.includes("authorize the `calle` server"), failures, `${filePath} must not document native Claude MCP OAuth authorization.`); + assert(source.includes("Great, authorization is complete"), failures, `${displayPath(filePath)} must include the post-authorization success message.`); + assert(source.includes("mcp tools"), failures, `${displayPath(filePath)} must document CLI tool discovery.`); + assert(source.includes("call plan"), failures, `${displayPath(filePath)} must document call planning through the CLI.`); + assert(source.includes("call run"), failures, `${displayPath(filePath)} must document planned call execution through the CLI.`); + assert(source.includes("call status"), failures, `${displayPath(filePath)} must document call status polling through the CLI.`); + assert(!source.includes("/mcp"), failures, `${displayPath(filePath)} must not direct users to /mcp for CALL-E authorization.`); + assert(!source.includes("authorize the `calle` server"), failures, `${displayPath(filePath)} must not document native Claude MCP OAuth authorization.`); } function checkSkill({ packageRoot, failures }) { @@ -129,9 +136,9 @@ function checkSkill({ packageRoot, failures }) { const skillFile = path.join(skillDir, "SKILL.md"); const referenceFile = path.join(skillDir, EXPECTED_REFERENCE_FILE); - assert(fs.existsSync(skillDir), failures, `Missing skill directory: ${skillDir}`); - assert(fs.existsSync(skillFile), failures, `Missing skill file: ${skillFile}`); - assert(fs.existsSync(referenceFile), failures, `Missing command reference: ${referenceFile}`); + assert(fs.existsSync(skillDir), failures, `Missing skill directory: ${displayPath(skillDir)}`); + assert(fs.existsSync(skillFile), failures, `Missing skill file: ${displayPath(skillFile)}`); + assert(fs.existsSync(referenceFile), failures, `Missing command reference: ${displayPath(referenceFile)}`); if (!fs.existsSync(skillFile)) { return; @@ -139,23 +146,23 @@ function checkSkill({ packageRoot, failures }) { const source = fs.readFileSync(skillFile, "utf8"); const frontmatter = extractFrontmatter(source); - assert(frontmatter, failures, `${skillFile} must start with YAML frontmatter.`); + assert(frontmatter, failures, `${displayPath(skillFile)} must start with YAML frontmatter.`); assertCliGuidance({ source, filePath: skillFile, failures }); - assert(source.includes("plan_call"), failures, `${skillFile} must document plan_call usage.`); - assert(source.includes("run_call"), failures, `${skillFile} must document run_call usage.`); - assert(source.includes("get_call_run"), failures, `${skillFile} must document get_call_run polling.`); - assert(source.includes("Always plan first"), failures, `${skillFile} must require plan-first behavior.`); - assert(source.includes("Do not guess phone numbers"), failures, `${skillFile} must forbid guessing call inputs.`); + assert(source.includes("plan_call"), failures, `${displayPath(skillFile)} must document plan_call usage.`); + assert(source.includes("run_call"), failures, `${displayPath(skillFile)} must document run_call usage.`); + assert(source.includes("get_call_run"), failures, `${displayPath(skillFile)} must document get_call_run polling.`); + assert(source.includes("Always plan first"), failures, `${displayPath(skillFile)} must require plan-first behavior.`); + assert(source.includes("Do not guess phone numbers"), failures, `${displayPath(skillFile)} must forbid guessing call inputs.`); assert( source.includes("Phone call is in progress! Progress:"), failures, - `${skillFile} must document the non-terminal call activity progress template.`, + `${displayPath(skillFile)} must document the non-terminal call activity progress template.`, ); - assert(source.includes("Poll every 10 seconds"), failures, `${skillFile} must document periodic polling.`); - assert(source.includes("Do not stay silent until a"), failures, `${skillFile} must require user-visible progress updates before terminal status.`); - assert(source.includes("[Status]"), failures, `${skillFile} must document the final status section.`); - assert(source.includes("[Transcript]"), failures, `${skillFile} must document the final transcript section.`); + assert(source.includes("Poll every 10 seconds"), failures, `${displayPath(skillFile)} must document periodic polling.`); + assert(source.includes("Do not stay silent until a"), failures, `${displayPath(skillFile)} must require user-visible progress updates before terminal status.`); + assert(source.includes("[Status]"), failures, `${displayPath(skillFile)} must document the final status section.`); + assert(source.includes("[Transcript]"), failures, `${displayPath(skillFile)} must document the final transcript section.`); if (fs.existsSync(referenceFile)) { const referenceSource = fs.readFileSync(referenceFile, "utf8"); @@ -163,17 +170,17 @@ function checkSkill({ packageRoot, failures }) { assert( referenceSource.includes("Phone call is in progress! Progress:"), failures, - `${referenceFile} must document the non-terminal call activity progress template.`, + `${displayPath(referenceFile)} must document the non-terminal call activity progress template.`, ); - assert(referenceSource.includes("Wait 10 seconds"), failures, `${referenceFile} must document the non-terminal call polling interval.`); + assert(referenceSource.includes("Wait 10 seconds"), failures, `${displayPath(referenceFile)} must document the non-terminal call polling interval.`); } if (!frontmatter) { return; } - assert(frontmatterValue(frontmatter, "name") === EXPECTED_SKILL_NAME, failures, `${skillFile} frontmatter name must be ${EXPECTED_SKILL_NAME}.`); - assert(Boolean(frontmatterValue(frontmatter, "description")), failures, `${skillFile} frontmatter must include description.`); + assert(frontmatterValue(frontmatter, "name") === EXPECTED_SKILL_NAME, failures, `${displayPath(skillFile)} frontmatter name must be ${EXPECTED_SKILL_NAME}.`); + assert(Boolean(frontmatterValue(frontmatter, "description")), failures, `${displayPath(skillFile)} frontmatter must include description.`); } function checkMarketplace({ repoRoot, packageJson, failures }) { @@ -216,15 +223,15 @@ function checkDocs({ packageRoot, repoRoot, failures }) { ]; for (const docFile of docFiles) { - assert(fs.existsSync(docFile), failures, `Missing documentation file: ${docFile}`); + assert(fs.existsSync(docFile), failures, `Missing documentation file: ${displayPath(docFile)}`); if (!fs.existsSync(docFile)) { continue; } const source = fs.readFileSync(docFile, "utf8"); - assert(source.includes(EXPECTED_SKILL_INVOCATION), failures, `${docFile} must document ${EXPECTED_SKILL_INVOCATION}.`); - assert(!source.includes(LEGACY_SKILL_INVOCATION), failures, `${docFile} must not document legacy ${LEGACY_SKILL_INVOCATION}.`); - assert(!source.includes("authorize the `calle` server"), failures, `${docFile} must not document native Claude MCP OAuth authorization.`); + assert(source.includes(EXPECTED_SKILL_INVOCATION), failures, `${displayPath(docFile)} must document ${EXPECTED_SKILL_INVOCATION}.`); + assert(!source.includes(LEGACY_SKILL_INVOCATION), failures, `${displayPath(docFile)} must not document legacy ${LEGACY_SKILL_INVOCATION}.`); + assert(!source.includes("authorize the `calle` server"), failures, `${displayPath(docFile)} must not document native Claude MCP OAuth authorization.`); } const installDoc = path.join(repoRoot, "docs", "install", "claude-plugin.md"); diff --git a/packages/codex-plugin/scripts/check-plugin.mjs b/packages/codex-plugin/scripts/check-plugin.mjs index adf4666..5f3a0c9 100644 --- a/packages/codex-plugin/scripts/check-plugin.mjs +++ b/packages/codex-plugin/scripts/check-plugin.mjs @@ -2,6 +2,13 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +// Paths go into failure messages with forward slashes on every platform, so +// the output is stable for tests, CI log greps and docs comparisons. Filesystem +// access still uses the native separator. +function displayPath(value) { + return String(value).split(path.sep).join("/"); +} + const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const DEFAULT_PACKAGE_ROOT = path.resolve(SCRIPT_DIR, ".."); const DEFAULT_REPO_ROOT = path.resolve(DEFAULT_PACKAGE_ROOT, "../.."); @@ -11,14 +18,14 @@ const EXPECTED_SKILLS = ["calle"]; function readJson(filePath, failures) { if (!fs.existsSync(filePath)) { - failures.push(`Missing ${filePath}`); + failures.push(`Missing ${displayPath(filePath)}`); return null; } try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { - failures.push(`Invalid JSON at ${filePath}: ${error.message}`); + failures.push(`Invalid JSON at ${displayPath(filePath)}: ${error.message}`); return null; } } @@ -29,8 +36,8 @@ function assert(condition, failures, message) { } } -function extractFrontmatter(markdown) { - const match = /^---\n([\s\S]*?)\n---\n?/u.exec(markdown); +export function extractFrontmatter(markdown) { + const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u.exec(markdown); return match ? match[1] : null; } @@ -39,10 +46,10 @@ function checkSkill({ skillName, skillDir, failures }) { const skillInterfaceFile = path.join(skillDir, "agents", "openai.yaml"); const referenceFile = path.join(skillDir, "references", "commands.md"); - assert(fs.existsSync(skillDir), failures, `Missing skill directory: ${skillDir}`); - assert(fs.existsSync(skillFile), failures, `Missing skill file: ${skillFile}`); - assert(fs.existsSync(skillInterfaceFile), failures, `Missing skill UI metadata: ${skillInterfaceFile}`); - assert(fs.existsSync(referenceFile), failures, `Missing command reference: ${referenceFile}`); + assert(fs.existsSync(skillDir), failures, `Missing skill directory: ${displayPath(skillDir)}`); + assert(fs.existsSync(skillFile), failures, `Missing skill file: ${displayPath(skillFile)}`); + assert(fs.existsSync(skillInterfaceFile), failures, `Missing skill UI metadata: ${displayPath(skillInterfaceFile)}`); + assert(fs.existsSync(referenceFile), failures, `Missing command reference: ${displayPath(referenceFile)}`); if (!fs.existsSync(skillFile)) { return; @@ -50,53 +57,53 @@ function checkSkill({ skillName, skillDir, failures }) { const source = fs.readFileSync(skillFile, "utf8"); const frontmatter = extractFrontmatter(source); - assert(frontmatter, failures, `${skillFile} must start with YAML frontmatter.`); + assert(frontmatter, failures, `${displayPath(skillFile)} must start with YAML frontmatter.`); assert( source.includes("assistant_hint.message"), failures, - `${skillFile} must document how to display assistant_hint.message after auth login.`, + `${displayPath(skillFile)} must document how to display assistant_hint.message after auth login.`, ); assert( source.includes("Run blocking `auth login`"), failures, - `${skillFile} must document blocking authorization login for the default Codex plugin flow.`, + `${displayPath(skillFile)} must document blocking authorization login for the default Codex plugin flow.`, ); assert( source.includes("Do not call ChatGPT App or connector tools") && source.includes("mcp__codex_apps__") && source.toLowerCase().includes("use only the `calle` cli flow"), failures, - `${skillFile} must document that the Codex plugin does not route through ChatGPT App tools.`, + `${displayPath(skillFile)} must document that the Codex plugin does not route through ChatGPT App tools.`, ); assert( source.includes("do not ask the user to reply"), failures, - `${skillFile} must document that browser authorization should continue without a manual chat reply.`, + `${displayPath(skillFile)} must document that browser authorization should continue without a manual chat reply.`, ); assert( source.includes("Before we start, please complete authorization here"), failures, - `${skillFile} must include the first authorization help message.`, + `${displayPath(skillFile)} must include the first authorization help message.`, ); assert( source.includes("Great, authorization is complete"), failures, - `${skillFile} must include the post-authorization success message.`, + `${displayPath(skillFile)} must include the post-authorization success message.`, ); assert( source.includes("Phone call is in progress! Progress:"), failures, - `${skillFile} must document the non-terminal call activity progress template.`, + `${displayPath(skillFile)} must document the non-terminal call activity progress template.`, ); assert( source.includes("Do not stay silent until a"), failures, - `${skillFile} must require user-visible progress updates before terminal status.`, + `${displayPath(skillFile)} must require user-visible progress updates before terminal status.`, ); assert( source.includes("Poll every 10 seconds"), failures, - `${skillFile} must document periodic polling while a call is non-terminal.`, + `${displayPath(skillFile)} must document periodic polling while a call is non-terminal.`, ); if (fs.existsSync(referenceFile)) { @@ -104,39 +111,39 @@ function checkSkill({ skillName, skillDir, failures }) { assert( referenceSource.includes("Phone call is in progress! Progress:"), failures, - `${referenceFile} must document the non-terminal call activity progress template.`, + `${displayPath(referenceFile)} must document the non-terminal call activity progress template.`, ); assert( referenceSource.includes("Run blocking `auth login`"), failures, - `${referenceFile} must document blocking authorization login for the default Codex plugin flow.`, + `${displayPath(referenceFile)} must document blocking authorization login for the default Codex plugin flow.`, ); assert( referenceSource.includes("Do not call ChatGPT App or connector tools") && referenceSource.includes("mcp__codex_apps__") && referenceSource.includes("Use the `calle` CLI flow"), failures, - `${referenceFile} must document that the Codex plugin does not route through ChatGPT App tools.`, + `${displayPath(referenceFile)} must document that the Codex plugin does not route through ChatGPT App tools.`, ); assert( referenceSource.includes("do not ask the user to reply"), failures, - `${referenceFile} must document that browser authorization should continue without a manual chat reply.`, + `${displayPath(referenceFile)} must document that browser authorization should continue without a manual chat reply.`, ); assert( referenceSource.includes("Before we start, please complete authorization here"), failures, - `${referenceFile} must include the first authorization help message.`, + `${displayPath(referenceFile)} must include the first authorization help message.`, ); assert( referenceSource.includes("Great, authorization is complete"), failures, - `${referenceFile} must include the post-authorization success message.`, + `${displayPath(referenceFile)} must include the post-authorization success message.`, ); assert( referenceSource.includes("Wait 10 seconds"), failures, - `${referenceFile} must document the non-terminal call polling interval.`, + `${displayPath(referenceFile)} must document the non-terminal call polling interval.`, ); } @@ -144,19 +151,19 @@ function checkSkill({ skillName, skillDir, failures }) { return; } - const nameMatch = /^name:\s*([^\n]+)\s*$/mu.exec(frontmatter); + const nameMatch = /^name:\s*([^\r\n]+)\s*$/mu.exec(frontmatter); const descriptionMatch = /^description:\s*/mu.exec(frontmatter); const declaredName = nameMatch?.[1]?.trim().replace(/^['"]|['"]$/g, ""); - assert(declaredName === skillName, failures, `${skillFile} frontmatter name must be "${skillName}".`); - assert(descriptionMatch, failures, `${skillFile} frontmatter must include description.`); + assert(declaredName === skillName, failures, `${displayPath(skillFile)} frontmatter name must be "${skillName}".`); + assert(descriptionMatch, failures, `${displayPath(skillFile)} frontmatter must include description.`); if (fs.existsSync(skillInterfaceFile)) { const skillInterfaceSource = fs.readFileSync(skillInterfaceFile, "utf8"); assert( /display_name:\s*"CALL-E"/u.test(skillInterfaceSource), failures, - `${skillInterfaceFile} must set interface.display_name to "CALL-E".`, + `${displayPath(skillInterfaceFile)} must set interface.display_name to "CALL-E".`, ); } } diff --git a/packages/codex-plugin/test/crlf.test.js b/packages/codex-plugin/test/crlf.test.js new file mode 100644 index 0000000..2ad049e --- /dev/null +++ b/packages/codex-plugin/test/crlf.test.js @@ -0,0 +1,41 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { extractFrontmatter } from "../scripts/check-plugin.mjs"; + +// Regression: the validators parsed YAML frontmatter with `\n`-anchored patterns. +// +// Git on Windows checks out with CRLF by default (core.autocrlf=true, and the +// repo shipped no .gitattributes), so every plugin and skill file arrived as +// `---\r\n...`. The delimiter never matched, and `pnpm -r test` reported six +// failures on a clean checkout -- valid files declared invalid, for a reason +// that had nothing to do with their contents. +// +// These cases run identically on POSIX, so a revert is caught on any machine +// rather than only on Windows. + +const LF = "---\ndescription: \"CALL-E safety rules.\"\nalwaysApply: true\n---\nbody\n"; +const CRLF = LF.replaceAll("\n", "\r\n"); + +test("frontmatter is recognised with LF line endings", () => { + const frontmatter = extractFrontmatter(LF); + assert.ok(frontmatter, "LF frontmatter should be recognised"); + assert.match(frontmatter, /alwaysApply: true/u); +}); + +test("frontmatter is recognised with CRLF line endings", () => { + const frontmatter = extractFrontmatter(CRLF); + assert.ok(frontmatter, "CRLF frontmatter should be recognised"); +}); + +test("a CRLF field value carries no trailing carriage return", () => { + const frontmatter = extractFrontmatter(CRLF); + const match = /^alwaysApply:\s*([^\r\n]+)\s*$/mu.exec(frontmatter); + assert.ok(match, "alwaysApply should be found"); + assert.equal(match[1], "true", "the captured value must not include \\r"); +}); + +test("content without frontmatter is still rejected", () => { + assert.equal(extractFrontmatter("no frontmatter here\n"), null); + assert.equal(extractFrontmatter("---\nunterminated\n"), null); +}); diff --git a/packages/cursor-plugin/scripts/check-plugin.mjs b/packages/cursor-plugin/scripts/check-plugin.mjs index b862657..6b332d9 100644 --- a/packages/cursor-plugin/scripts/check-plugin.mjs +++ b/packages/cursor-plugin/scripts/check-plugin.mjs @@ -2,6 +2,13 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +// Paths go into failure messages with forward slashes on every platform, so +// the output is stable for tests, CI log greps and docs comparisons. Filesystem +// access still uses the native separator. +function displayPath(value) { + return String(value).split(path.sep).join("/"); +} + const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const DEFAULT_PACKAGE_ROOT = path.resolve(SCRIPT_DIR, ".."); const DEFAULT_REPO_ROOT = path.resolve(DEFAULT_PACKAGE_ROOT, "../.."); @@ -46,14 +53,14 @@ const CURSOR_MARKETPLACE_ENTRY_KEYS = new Set(["name", "source", "description"]) function readJson(filePath, failures) { if (!fs.existsSync(filePath)) { - failures.push(`Missing ${filePath}`); + failures.push(`Missing ${displayPath(filePath)}`); return null; } try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { - failures.push(`Invalid JSON at ${filePath}: ${error.message}`); + failures.push(`Invalid JSON at ${displayPath(filePath)}: ${error.message}`); return null; } } @@ -75,12 +82,12 @@ function assertAllowedKeys(value, allowedKeys, failures, label) { } function extractFrontmatter(markdown) { - const match = /^---\n([\s\S]*?)\n---\n?/u.exec(markdown); + const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u.exec(markdown); return match ? match[1] : null; } function frontmatterValue(frontmatter, key) { - const match = new RegExp(`^${key}:\\s*([^\\n]+)\\s*$`, "mu").exec(frontmatter); + const match = new RegExp(`^${key}:\\s*([^\\r\\n]+)\\s*$`, "mu").exec(frontmatter); return match?.[1]?.trim().replace(/^['"]|['"]$/g, "") ?? null; } @@ -145,40 +152,40 @@ function checkMcpConfig({ packageRoot, failures }) { } function assertCliGuidance({ source, filePath, packageJson, failures }) { - assert(source.includes(`CALLE_SOURCE=${EXPECTED_CLI_SOURCE}`), failures, `${filePath} must include Cursor CLI source attribution.`); + assert(source.includes(`CALLE_SOURCE=${EXPECTED_CLI_SOURCE}`), failures, `${displayPath(filePath)} must include Cursor CLI source attribution.`); assert( source.includes(`CALLE_INTEGRATION=${EXPECTED_CLI_INTEGRATION}`), failures, - `${filePath} must include Cursor CLI integration attribution.`, + `${displayPath(filePath)} must include Cursor CLI integration attribution.`, ); assert( source.includes(`CALLE_INTEGRATION_VERSION=${packageJson.version}`), failures, - `${filePath} must include Cursor CLI integration version ${packageJson.version}.`, + `${displayPath(filePath)} must include Cursor CLI integration version ${packageJson.version}.`, ); - assert(source.includes("node packages/cli/bin/calle.js"), failures, `${filePath} must document the repository-local CLI command.`); - assert(source.includes("npx -y @call-e/cli"), failures, `${filePath} must document the npx CLI fallback.`); - assert(source.includes("auth status"), failures, `${filePath} must document auth status checks.`); - assert(source.includes("mcp tools"), failures, `${filePath} must document CLI tool discovery.`); - assert(source.includes("call plan"), failures, `${filePath} must document call planning through the CLI.`); - assert(source.includes("call run"), failures, `${filePath} must document planned call execution through the CLI.`); - assert(source.includes("call status"), failures, `${filePath} must document call status polling through the CLI.`); + assert(source.includes("node packages/cli/bin/calle.js"), failures, `${displayPath(filePath)} must document the repository-local CLI command.`); + assert(source.includes("npx -y @call-e/cli"), failures, `${displayPath(filePath)} must document the npx CLI fallback.`); + assert(source.includes("auth status"), failures, `${displayPath(filePath)} must document auth status checks.`); + assert(source.includes("mcp tools"), failures, `${displayPath(filePath)} must document CLI tool discovery.`); + assert(source.includes("call plan"), failures, `${displayPath(filePath)} must document call planning through the CLI.`); + assert(source.includes("call run"), failures, `${displayPath(filePath)} must document planned call execution through the CLI.`); + assert(source.includes("call status"), failures, `${displayPath(filePath)} must document call status polling through the CLI.`); } function assertCallGuidance({ source, filePath, failures }) { - assert(source.includes("plan_call"), failures, `${filePath} must document plan_call usage.`); - assert(source.includes("run_call"), failures, `${filePath} must document run_call usage.`); - assert(source.includes("get_call_run"), failures, `${filePath} must document get_call_run polling.`); - assert(source.includes("Always use plan_call before run_call."), failures, `${filePath} must require plan_call before run_call.`); + assert(source.includes("plan_call"), failures, `${displayPath(filePath)} must document plan_call usage.`); + assert(source.includes("run_call"), failures, `${displayPath(filePath)} must document run_call usage.`); + assert(source.includes("get_call_run"), failures, `${displayPath(filePath)} must document get_call_run polling.`); + assert(source.includes("Always use plan_call before run_call."), failures, `${displayPath(filePath)} must require plan_call before run_call.`); assert( source.includes("Only call run_call when the user clearly intends to place the call."), failures, - `${filePath} must require explicit user intent before run_call.`, + `${displayPath(filePath)} must require explicit user intent before run_call.`, ); - assert(source.includes("Preserve plan_id and confirm_token exactly."), failures, `${filePath} must require exact plan credential preservation.`); - assert(source.includes("Do not guess phone numbers"), failures, `${filePath} must forbid guessing call inputs.`); - assert(source.includes("Do not expose OAuth tokens"), failures, `${filePath} must forbid exposing auth secrets.`); - assert(source.includes("Do not configure CALL-E run_call for auto-run."), failures, `${filePath} must forbid run_call auto-run configuration.`); + assert(source.includes("Preserve plan_id and confirm_token exactly."), failures, `${displayPath(filePath)} must require exact plan credential preservation.`); + assert(source.includes("Do not guess phone numbers"), failures, `${displayPath(filePath)} must forbid guessing call inputs.`); + assert(source.includes("Do not expose OAuth tokens"), failures, `${displayPath(filePath)} must forbid exposing auth secrets.`); + assert(source.includes("Do not configure CALL-E run_call for auto-run."), failures, `${displayPath(filePath)} must forbid run_call auto-run configuration.`); } function checkSkill({ packageRoot, packageJson, failures }) { @@ -186,9 +193,9 @@ function checkSkill({ packageRoot, packageJson, failures }) { const skillFile = path.join(skillDir, "SKILL.md"); const referenceFile = path.join(skillDir, EXPECTED_REFERENCE_FILE); - assert(fs.existsSync(skillDir), failures, `Missing skill directory: ${skillDir}`); - assert(fs.existsSync(skillFile), failures, `Missing skill file: ${skillFile}`); - assert(fs.existsSync(referenceFile), failures, `Missing command reference: ${referenceFile}`); + assert(fs.existsSync(skillDir), failures, `Missing skill directory: ${displayPath(skillDir)}`); + assert(fs.existsSync(skillFile), failures, `Missing skill file: ${displayPath(skillFile)}`); + assert(fs.existsSync(referenceFile), failures, `Missing command reference: ${displayPath(referenceFile)}`); if (!fs.existsSync(skillFile) || !packageJson) { return; @@ -196,14 +203,14 @@ function checkSkill({ packageRoot, packageJson, failures }) { const source = fs.readFileSync(skillFile, "utf8"); const frontmatter = extractFrontmatter(source); - assert(frontmatter, failures, `${skillFile} must start with YAML frontmatter.`); + assert(frontmatter, failures, `${displayPath(skillFile)} must start with YAML frontmatter.`); assertCallGuidance({ source, filePath: skillFile, failures }); assertCliGuidance({ source, filePath: skillFile, packageJson, failures }); - assert(source.includes("Prefer the Cursor MCP tools"), failures, `${skillFile} must prefer Cursor MCP tools when available.`); + assert(source.includes("Prefer the Cursor MCP tools"), failures, `${displayPath(skillFile)} must prefer Cursor MCP tools when available.`); assert( source.includes("wait 60 seconds before the first `get_call_run`"), failures, - `${skillFile} must document the first direct MCP status wait.`, + `${displayPath(skillFile)} must document the first direct MCP status wait.`, ); if (fs.existsSync(referenceFile)) { @@ -216,34 +223,34 @@ function checkSkill({ packageRoot, packageJson, failures }) { return; } - assert(frontmatterValue(frontmatter, "name") === EXPECTED_SKILL_NAME, failures, `${skillFile} frontmatter name must be ${EXPECTED_SKILL_NAME}.`); + assert(frontmatterValue(frontmatter, "name") === EXPECTED_SKILL_NAME, failures, `${displayPath(skillFile)} frontmatter name must be ${EXPECTED_SKILL_NAME}.`); assert( frontmatterValue(frontmatter, "description") === "Use CALL-E from Cursor for setup checks, authentication recovery, phone call planning, planned call execution, and call status checks.", failures, - `${skillFile} frontmatter description must match the Cursor skill description.`, + `${displayPath(skillFile)} frontmatter description must match the Cursor skill description.`, ); } function checkRule({ packageRoot, failures }) { const rulePath = path.join(packageRoot, "plugin", "rules", "call-e-safety.mdc"); - assert(fs.existsSync(rulePath), failures, `Missing safety rule: ${rulePath}`); + assert(fs.existsSync(rulePath), failures, `Missing safety rule: ${displayPath(rulePath)}`); if (!fs.existsSync(rulePath)) { return; } const source = fs.readFileSync(rulePath, "utf8"); const frontmatter = extractFrontmatter(source); - assert(frontmatter, failures, `${rulePath} must start with YAML frontmatter.`); - assert(frontmatter?.includes('description: "CALL-E real phone call safety rules."'), failures, `${rulePath} must include the safety rule description.`); - assert(frontmatter?.includes("alwaysApply: true"), failures, `${rulePath} must contain alwaysApply: true.`); - assert(source.includes("CALL-E can place real outbound phone calls."), failures, `${rulePath} must warn that CALL-E can place real outbound calls.`); - assert(source.includes("Always use plan_call before run_call."), failures, `${rulePath} must require plan_call before run_call.`); - assert(source.includes("Only use run_call when the user clearly intends to place the call."), failures, `${rulePath} must require explicit user intent before run_call.`); - assert(source.includes("Preserve returned plan_id and confirm_token exactly."), failures, `${rulePath} must require exact returned credential preservation.`); - assert(source.includes("Never guess phone numbers"), failures, `${rulePath} must forbid guessing call inputs.`); - assert(source.includes("Never print, request, or expose OAuth tokens"), failures, `${rulePath} must forbid exposing auth secrets.`); - assert(source.includes("Do not configure CALL-E run_call for auto-run."), failures, `${rulePath} must forbid run_call auto-run configuration.`); + assert(frontmatter, failures, `${displayPath(rulePath)} must start with YAML frontmatter.`); + assert(frontmatter?.includes('description: "CALL-E real phone call safety rules."'), failures, `${displayPath(rulePath)} must include the safety rule description.`); + assert(frontmatter?.includes("alwaysApply: true"), failures, `${displayPath(rulePath)} must contain alwaysApply: true.`); + assert(source.includes("CALL-E can place real outbound phone calls."), failures, `${displayPath(rulePath)} must warn that CALL-E can place real outbound calls.`); + assert(source.includes("Always use plan_call before run_call."), failures, `${displayPath(rulePath)} must require plan_call before run_call.`); + assert(source.includes("Only use run_call when the user clearly intends to place the call."), failures, `${displayPath(rulePath)} must require explicit user intent before run_call.`); + assert(source.includes("Preserve returned plan_id and confirm_token exactly."), failures, `${displayPath(rulePath)} must require exact returned credential preservation.`); + assert(source.includes("Never guess phone numbers"), failures, `${displayPath(rulePath)} must forbid guessing call inputs.`); + assert(source.includes("Never print, request, or expose OAuth tokens"), failures, `${displayPath(rulePath)} must forbid exposing auth secrets.`); + assert(source.includes("Do not configure CALL-E run_call for auto-run."), failures, `${displayPath(rulePath)} must forbid run_call auto-run configuration.`); } function checkMarketplace({ repoRoot, packageJson, failures }) { @@ -288,18 +295,18 @@ function checkDocs({ packageRoot, repoRoot, failures }) { ]; for (const docFile of docFiles) { - assert(fs.existsSync(docFile), failures, `Missing documentation file: ${docFile}`); + assert(fs.existsSync(docFile), failures, `Missing documentation file: ${displayPath(docFile)}`); if (!fs.existsSync(docFile)) { continue; } const source = fs.readFileSync(docFile, "utf8"); - assert(source.includes(EXPECTED_REMOTE_MCP_URL), failures, `${docFile} must document the CALL-E remote MCP URL.`); - assert(source.includes("plan_call"), failures, `${docFile} must mention plan_call.`); - assert(source.includes("run_call"), failures, `${docFile} must mention run_call.`); - assert(source.includes("get_call_run"), failures, `${docFile} must mention get_call_run.`); - assert(source.includes("real outbound"), failures, `${docFile} must warn that run_call places real outbound calls.`); - assert(source.includes("auto-run"), failures, `${docFile} must warn against configuring run_call for auto-run.`); + assert(source.includes(EXPECTED_REMOTE_MCP_URL), failures, `${displayPath(docFile)} must document the CALL-E remote MCP URL.`); + assert(source.includes("plan_call"), failures, `${displayPath(docFile)} must mention plan_call.`); + assert(source.includes("run_call"), failures, `${displayPath(docFile)} must mention run_call.`); + assert(source.includes("get_call_run"), failures, `${displayPath(docFile)} must mention get_call_run.`); + assert(source.includes("real outbound"), failures, `${displayPath(docFile)} must warn that run_call places real outbound calls.`); + assert(source.includes("auto-run"), failures, `${displayPath(docFile)} must warn against configuring run_call for auto-run.`); } } diff --git a/packages/openclaw-cli-skill/scripts/check-skill.mjs b/packages/openclaw-cli-skill/scripts/check-skill.mjs index 30d3e6a..981e36f 100644 --- a/packages/openclaw-cli-skill/scripts/check-skill.mjs +++ b/packages/openclaw-cli-skill/scripts/check-skill.mjs @@ -2,6 +2,13 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +// Paths go into failure messages with forward slashes on every platform, so +// the output is stable for tests, CI log greps and docs comparisons. Filesystem +// access still uses the native separator. +function displayPath(value) { + return String(value).split(path.sep).join("/"); +} + const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const DEFAULT_PACKAGE_ROOT = path.resolve(SCRIPT_DIR, ".."); const DEFAULT_REPO_ROOT = path.resolve(DEFAULT_PACKAGE_ROOT, "../.."); @@ -21,14 +28,14 @@ const BANNED_AUTH_LINK_STRINGS = [ function readJson(filePath, failures) { if (!fs.existsSync(filePath)) { - failures.push(`Missing ${filePath}`); + failures.push(`Missing ${displayPath(filePath)}`); return null; } try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { - failures.push(`Invalid JSON at ${filePath}: ${error.message}`); + failures.push(`Invalid JSON at ${displayPath(filePath)}: ${error.message}`); return null; } } @@ -40,12 +47,12 @@ function assert(condition, failures, message) { } function extractFrontmatter(markdown) { - const match = /^---\n([\s\S]*?)\n---\n?/u.exec(markdown); + const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u.exec(markdown); return match ? match[1] : null; } function frontmatterValue(frontmatter, key) { - const match = new RegExp(`^${key}:\\s*([^\\n]+)\\s*$`, "mu").exec(frontmatter); + const match = new RegExp(`^${key}:\\s*([^\\r\\n]+)\\s*$`, "mu").exec(frontmatter); return match?.[1]?.trim().replace(/^['"]|['"]$/g, "") ?? null; } @@ -70,9 +77,9 @@ function checkSkill({ packageRoot, failures }) { const skillFile = path.join(skillDir, "SKILL.md"); const referenceFile = path.join(skillDir, "references", "commands.md"); - assert(fs.existsSync(skillDir), failures, `Missing skill directory: ${skillDir}`); - assert(fs.existsSync(skillFile), failures, `Missing skill file: ${skillFile}`); - assert(fs.existsSync(referenceFile), failures, `Missing command reference: ${referenceFile}`); + assert(fs.existsSync(skillDir), failures, `Missing skill directory: ${displayPath(skillDir)}`); + assert(fs.existsSync(skillFile), failures, `Missing skill file: ${displayPath(skillFile)}`); + assert(fs.existsSync(referenceFile), failures, `Missing command reference: ${displayPath(referenceFile)}`); if (!fs.existsSync(skillFile)) { return; @@ -80,100 +87,100 @@ function checkSkill({ packageRoot, failures }) { const source = fs.readFileSync(skillFile, "utf8"); const frontmatter = extractFrontmatter(source); - assert(frontmatter, failures, `${skillFile} must start with YAML frontmatter.`); - assert(source.includes("assistant_hint.message"), failures, `${skillFile} must document assistant_hint.message handling.`); - assert(source.includes("auth_required"), failures, `${skillFile} must document auth_required handling.`); - assert(source.includes("CALLE_INTEGRATION=openclaw_cli_skill"), failures, `${skillFile} must include OpenClaw CLI skill integration attribution.`); + assert(frontmatter, failures, `${displayPath(skillFile)} must start with YAML frontmatter.`); + assert(source.includes("assistant_hint.message"), failures, `${displayPath(skillFile)} must document assistant_hint.message handling.`); + assert(source.includes("auth_required"), failures, `${displayPath(skillFile)} must document auth_required handling.`); + assert(source.includes("CALLE_INTEGRATION=openclaw_cli_skill"), failures, `${displayPath(skillFile)} must include OpenClaw CLI skill integration attribution.`); assert( source.includes("auth login --start-only --no-browser-open"), failures, - `${skillFile} must document start-only authorization login for the default OpenClaw CLI skill flow.`, + `${displayPath(skillFile)} must document start-only authorization login for the default OpenClaw CLI skill flow.`, ); assert( source.includes("authorization instructions returned by the CLI"), failures, - `${skillFile} must use neutral CLI-provided authorization instructions.`, + `${displayPath(skillFile)} must use neutral CLI-provided authorization instructions.`, ); assert( source.includes("auth login --no-browser-open"), failures, - `${skillFile} must document how to continue and exchange a pending authorization.`, + `${displayPath(skillFile)} must document how to continue and exchange a pending authorization.`, ); assert( source.includes("Great, authorization is complete"), failures, - `${skillFile} must include the post-authorization success message.`, + `${displayPath(skillFile)} must include the post-authorization success message.`, ); assert( source.includes("Phone call is in progress! Progress:"), failures, - `${skillFile} must document the non-terminal call activity progress template.`, + `${displayPath(skillFile)} must document the non-terminal call activity progress template.`, ); assert( source.includes("do not use `run_result`"), failures, - `${skillFile} must require call run replies to ignore run_result for user-visible output.`, + `${displayPath(skillFile)} must require call run replies to ignore run_result for user-visible output.`, ); assert( source.includes("Treat `status_result.structuredContent`"), failures, - `${skillFile} must require call run replies to use status_result.structuredContent.`, + `${displayPath(skillFile)} must require call run replies to use status_result.structuredContent.`, ); assert( source.includes("Never paraphrase call results"), failures, - `${skillFile} must forbid free-form call result paraphrases.`, + `${displayPath(skillFile)} must forbid free-form call result paraphrases.`, ); assert( source.includes("the entire reply must be exactly this shape"), failures, - `${skillFile} must require the exact non-terminal progress reply shape.`, + `${displayPath(skillFile)} must require the exact non-terminal progress reply shape.`, ); assert( source.includes("Do not stay silent until a"), failures, - `${skillFile} must require user-visible progress updates before terminal status.`, + `${displayPath(skillFile)} must require user-visible progress updates before terminal status.`, ); assert( source.includes("Poll every 10 seconds"), failures, - `${skillFile} must document periodic polling while a call is non-terminal.`, + `${displayPath(skillFile)} must document periodic polling while a call is non-terminal.`, ); for (const banned of BANNED_PLUGIN_STRINGS) { - assert(!source.includes(banned), failures, `${skillFile} must not reference plugin install path: ${banned}`); + assert(!source.includes(banned), failures, `${displayPath(skillFile)} must not reference plugin install path: ${banned}`); } for (const banned of BANNED_AUTH_LINK_STRINGS) { - assert(!source.includes(banned), failures, `${skillFile} must not include handwritten authorization link template text: ${banned}`); + assert(!source.includes(banned), failures, `${displayPath(skillFile)} must not include handwritten authorization link template text: ${banned}`); } if (!frontmatter) { return; } - assert(frontmatterValue(frontmatter, "name") === EXPECTED_SKILL_NAME, failures, `${skillFile} frontmatter name must be "${EXPECTED_SKILL_NAME}".`); - assert(Boolean(frontmatterValue(frontmatter, "description")), failures, `${skillFile} frontmatter must include description.`); + assert(frontmatterValue(frontmatter, "name") === EXPECTED_SKILL_NAME, failures, `${displayPath(skillFile)} frontmatter name must be "${EXPECTED_SKILL_NAME}".`); + assert(Boolean(frontmatterValue(frontmatter, "description")), failures, `${displayPath(skillFile)} frontmatter must include description.`); const metadataMatches = [...frontmatter.matchAll(/^metadata:\s*(.*)$/gmu)]; - assert(metadataMatches.length === 1, failures, `${skillFile} frontmatter must include exactly one metadata line.`); + assert(metadataMatches.length === 1, failures, `${displayPath(skillFile)} frontmatter must include exactly one metadata line.`); const metadataSource = metadataMatches[0]?.[1]?.trim(); - assert(Boolean(metadataSource), failures, `${skillFile} metadata must be declared on one line.`); + assert(Boolean(metadataSource), failures, `${displayPath(skillFile)} metadata must be declared on one line.`); if (metadataSource) { try { const metadata = JSON.parse(metadataSource); const openclaw = metadata.openclaw; - assert(openclaw && typeof openclaw === "object", failures, `${skillFile} metadata.openclaw must be an object.`); - assert(openclaw.requires?.bins?.includes("node"), failures, `${skillFile} metadata.openclaw.requires.bins must include node.`); - assert(openclaw.requires?.anyBins?.includes("calle"), failures, `${skillFile} metadata.openclaw.requires.anyBins must include calle.`); - assert(openclaw.requires?.anyBins?.includes("npx"), failures, `${skillFile} metadata.openclaw.requires.anyBins must include npx.`); + assert(openclaw && typeof openclaw === "object", failures, `${displayPath(skillFile)} metadata.openclaw must be an object.`); + assert(openclaw.requires?.bins?.includes("node"), failures, `${displayPath(skillFile)} metadata.openclaw.requires.bins must include node.`); + assert(openclaw.requires?.anyBins?.includes("calle"), failures, `${displayPath(skillFile)} metadata.openclaw.requires.anyBins must include calle.`); + assert(openclaw.requires?.anyBins?.includes("npx"), failures, `${displayPath(skillFile)} metadata.openclaw.requires.anyBins must include npx.`); assert( Array.isArray(openclaw.install) && openclaw.install.some((entry) => entry?.kind === "node" && entry?.package === "@call-e/cli"), failures, - `${skillFile} metadata.openclaw.install must include a node installer for @call-e/cli.`, + `${displayPath(skillFile)} metadata.openclaw.install must include a node installer for @call-e/cli.`, ); } catch (error) { - failures.push(`${skillFile} metadata must be single-line JSON: ${error.message}`); + failures.push(`${displayPath(skillFile)} metadata must be single-line JSON: ${error.message}`); } } } @@ -209,14 +216,14 @@ function checkReference({ packageRoot, failures }) { ]; for (const snippet of requiredSnippets) { - assert(source.includes(snippet), failures, `${referenceFile} must include ${snippet}.`); + assert(source.includes(snippet), failures, `${displayPath(referenceFile)} must include ${snippet}.`); } for (const banned of BANNED_PLUGIN_STRINGS) { - assert(!source.includes(banned), failures, `${referenceFile} must not reference plugin install path: ${banned}`); + assert(!source.includes(banned), failures, `${displayPath(referenceFile)} must not reference plugin install path: ${banned}`); } for (const banned of BANNED_AUTH_LINK_STRINGS) { - assert(!source.includes(banned), failures, `${referenceFile} must not include handwritten authorization link template text: ${banned}`); + assert(!source.includes(banned), failures, `${displayPath(referenceFile)} must not include handwritten authorization link template text: ${banned}`); } } diff --git a/packages/skills-sh-skill/scripts/check-skill.mjs b/packages/skills-sh-skill/scripts/check-skill.mjs index a5dcbb9..9f8562e 100644 --- a/packages/skills-sh-skill/scripts/check-skill.mjs +++ b/packages/skills-sh-skill/scripts/check-skill.mjs @@ -2,6 +2,13 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +// Paths go into failure messages with forward slashes on every platform, so +// the output is stable for tests, CI log greps and docs comparisons. Filesystem +// access still uses the native separator. +function displayPath(value) { + return String(value).split(path.sep).join("/"); +} + const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const DEFAULT_PACKAGE_ROOT = path.resolve(SCRIPT_DIR, ".."); const DEFAULT_REPO_ROOT = path.resolve(DEFAULT_PACKAGE_ROOT, "../.."); @@ -14,14 +21,14 @@ const EXPECTED_INTEGRATION = "CALLE_INTEGRATION=skills_sh_skill"; function readJson(filePath, failures) { if (!fs.existsSync(filePath)) { - failures.push(`Missing ${filePath}`); + failures.push(`Missing ${displayPath(filePath)}`); return null; } try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (error) { - failures.push(`Invalid JSON at ${filePath}: ${error.message}`); + failures.push(`Invalid JSON at ${displayPath(filePath)}: ${error.message}`); return null; } } @@ -33,12 +40,12 @@ function assert(condition, failures, message) { } function extractFrontmatter(markdown) { - const match = /^---\n([\s\S]*?)\n---\n?/u.exec(markdown); + const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u.exec(markdown); return match ? match[1] : null; } function frontmatterValue(frontmatter, key) { - const match = new RegExp(`^${key}:\\s*([^\\n]+)\\s*$`, "mu").exec(frontmatter); + const match = new RegExp(`^${key}:\\s*([^\\r\\n]+)\\s*$`, "mu").exec(frontmatter); return match?.[1]?.trim().replace(/^['"]|['"]$/g, "") ?? null; } @@ -52,7 +59,7 @@ function assertRequiredSnippets({ source, filePath, snippets, failures }) { continue; } - assert(source.includes(snippet), failures, `${filePath} must include ${snippet}.`); + assert(source.includes(snippet), failures, `${displayPath(filePath)} must include ${snippet}.`); } } @@ -88,10 +95,10 @@ function checkSkill({ repoRoot, packageJson, failures }) { const referenceFile = path.join(skillDir, "references", "commands.md"); const expectedVersion = integrationVersionSnippet(packageJson); - assert(fs.existsSync(skillDir), failures, `Missing skill directory: ${skillDir}`); - assert(fs.existsSync(skillFile), failures, `Missing skill file: ${skillFile}`); - assert(fs.existsSync(skillInterfaceFile), failures, `Missing skill UI metadata: ${skillInterfaceFile}`); - assert(fs.existsSync(referenceFile), failures, `Missing command reference: ${referenceFile}`); + assert(fs.existsSync(skillDir), failures, `Missing skill directory: ${displayPath(skillDir)}`); + assert(fs.existsSync(skillFile), failures, `Missing skill file: ${displayPath(skillFile)}`); + assert(fs.existsSync(skillInterfaceFile), failures, `Missing skill UI metadata: ${displayPath(skillInterfaceFile)}`); + assert(fs.existsSync(referenceFile), failures, `Missing command reference: ${displayPath(referenceFile)}`); if (!fs.existsSync(skillFile)) { return; @@ -99,10 +106,10 @@ function checkSkill({ repoRoot, packageJson, failures }) { const source = fs.readFileSync(skillFile, "utf8"); const frontmatter = extractFrontmatter(source); - assert(frontmatter, failures, `${skillFile} must start with YAML frontmatter.`); - assert(!source.includes("[TODO:"), failures, `${skillFile} must not contain template TODO markers.`); - assert(!source.includes("npx -y @call-e/cli@"), failures, `${skillFile} must not run remote npm packages from the skill.`); - assert(!source.includes("confirm_token"), failures, `${skillFile} must not expose or instruct handling of execution confirmation tokens.`); + assert(frontmatter, failures, `${displayPath(skillFile)} must start with YAML frontmatter.`); + assert(!source.includes("[TODO:"), failures, `${displayPath(skillFile)} must not contain template TODO markers.`); + assert(!source.includes("npx -y @call-e/cli@"), failures, `${displayPath(skillFile)} must not run remote npm packages from the skill.`); + assert(!source.includes("confirm_token"), failures, `${displayPath(skillFile)} must not expose or instruct handling of execution confirmation tokens.`); assertRequiredSnippets({ source, @@ -134,15 +141,15 @@ function checkSkill({ repoRoot, packageJson, failures }) { if (frontmatter) { const keys = frontmatterKeys(frontmatter); const unexpectedKeys = keys.filter((key) => !["name", "description"].includes(key)); - assert(unexpectedKeys.length === 0, failures, `${skillFile} frontmatter must only include name and description.`); - assert(frontmatterValue(frontmatter, "name") === EXPECTED_SKILL_NAME, failures, `${skillFile} frontmatter name must be "${EXPECTED_SKILL_NAME}".`); - assert(Boolean(frontmatterValue(frontmatter, "description")), failures, `${skillFile} frontmatter must include description.`); + assert(unexpectedKeys.length === 0, failures, `${displayPath(skillFile)} frontmatter must only include name and description.`); + assert(frontmatterValue(frontmatter, "name") === EXPECTED_SKILL_NAME, failures, `${displayPath(skillFile)} frontmatter name must be "${EXPECTED_SKILL_NAME}".`); + assert(Boolean(frontmatterValue(frontmatter, "description")), failures, `${displayPath(skillFile)} frontmatter must include description.`); } if (fs.existsSync(skillInterfaceFile)) { const skillInterfaceSource = fs.readFileSync(skillInterfaceFile, "utf8"); - assert(/display_name:\s*"calle"/u.test(skillInterfaceSource), failures, `${skillInterfaceFile} must set interface.display_name to "calle".`); - assert(skillInterfaceSource.includes("Use $calle"), failures, `${skillInterfaceFile} default_prompt must mention $calle.`); + assert(/display_name:\s*"calle"/u.test(skillInterfaceSource), failures, `${displayPath(skillInterfaceFile)} must set interface.display_name to "calle".`); + assert(skillInterfaceSource.includes("Use $calle"), failures, `${displayPath(skillInterfaceFile)} default_prompt must mention $calle.`); } if (!fs.existsSync(referenceFile)) { @@ -150,8 +157,8 @@ function checkSkill({ repoRoot, packageJson, failures }) { } const referenceSource = fs.readFileSync(referenceFile, "utf8"); - assert(!referenceSource.includes("npx -y @call-e/cli@"), failures, `${referenceFile} must not run remote npm packages from the skill.`); - assert(!referenceSource.includes("confirm_token"), failures, `${referenceFile} must not expose or instruct handling of execution confirmation tokens.`); + assert(!referenceSource.includes("npx -y @call-e/cli@"), failures, `${displayPath(referenceFile)} must not run remote npm packages from the skill.`); + assert(!referenceSource.includes("confirm_token"), failures, `${displayPath(referenceFile)} must not expose or instruct handling of execution confirmation tokens.`); assertRequiredSnippets({ source: referenceSource, filePath: referenceFile,