#!/usr/bin/env node /** * Build System for Cross-Provider Design Skills * * Transforms source skills into provider-specific formats: * - Cursor: .cursor/skills/ * - Claude Code: .claude/skills/ * - Gemini: .gemini/skills/ * - Codex: dist/codex/ only (OpenAI-metadata bundle; not synced to repo root) * - Agents: .agents/skills/ (Codex repo/user installs) * - GitHub: .github/skills/ (GitHub Copilot) * - Veto: .veto/skills/ (Veto model-routing harness) * * Also assembles a universal ZIP containing all providers, * and builds Tailwind CSS for production deployment. */ import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; import { readSourceFiles, readPatterns, stashPerProjectArtifacts, restorePerProjectArtifacts } from './lib/utils.js'; import { syncRootCommands } from './lib/root-commands-sync.mjs'; import { createTransformer, PROVIDERS } from './lib/transformers/index.js'; import { hooksJsonFor, buildClaudePluginHooksManifest } from './lib/transformers/hooks.js'; import { createAllZips, createProviderZip } from './lib/zip.js'; import { collectPluginVersions } from './lib/validate-plugin-versions.js'; import { collectPluginManifestFindings } from './lib/validate-plugin-manifest.js'; import { rewritePluginMarkdownTree, rewritePluginAgentMarkdown, verifyPluginSkillRewrite, verifyPluginAgentRewrite, } from './lib/plugin-paths.js'; import { stageOpenAIPlugin } from './lib/openai-plugin.js'; import { ENGINE_TARGETS, binaryName, main as fetchEngineMain, readEngineVersion } from './fetch-engine.mjs'; // Sub-page generation is now handled by Astro content collections. /** * Generate authoritative counts from source data and write to site/public/js/generated/counts.js. * Also validates that key HTML files reference the correct numbers. */ function generateCounts(rootDir, skills, buildDir) { // Count active commands. After the v3.0 consolidation, commands are sub-commands // of /impeccable. Count them from the command router table in SKILL.md. const impeccableSkill = skills.find(s => s.name === 'impeccable'); let commandCount; if (impeccableSkill) { // Count lines in the command table that start with | `...` | — tolerant // of argument hints inside the backticks (e.g. `craft [feature]`) and of // multi-word commands (e.g. `pin `). const routerMatches = impeccableSkill.body.match(/^\| `[^`]+` \|/gm); commandCount = routerMatches ? routerMatches.length : 0; } else { // Fallback: count user-invocable skills const activeCommands = skills.filter(s => { if (!s.userInvocable) return false; const content = fs.readFileSync(s.filePath, 'utf-8'); return !content.includes('DEPRECATED'); }); commandCount = activeCommands.length; } // Count detection rules from the rule registry as `cargo xtask bundle` // emits it. crates/live/assets/antipatterns.json is tracked, so a fresh // checkout has it; extension/detector/antipatterns.json is the gitignored // extension copy and only stands in for an older tree. With neither, the // detection-count check is skipped rather than guessed. const { count: detectionCount, reason: detectionReason } = readDetectionRuleCount(rootDir); // Validate counts in key files const filesToCheck = [ 'site/pages/index.astro', 'README.md', 'README.npm.md', 'AGENTS.md', '.claude-plugin/plugin.json', '.claude-plugin/marketplace.json', ]; let errors = 0; for (const relPath of filesToCheck) { const absPath = path.join(rootDir, relPath); if (!fs.existsSync(absPath)) continue; const content = fs.readFileSync(absPath, 'utf-8'); // Check for stale command counts (look for "N commands" or "N skills" patterns) // Strip changelog list content to avoid flagging historical counts const strippedContent = content.replace(/