UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

317 lines (316 loc) 13.3 kB
import * as fs from "fs"; import * as path from "path"; import { execFileSync } from "child_process"; import { logInfo, logSuccess, logWarn } from "../utils/log.js"; import { MeshCliError } from "../utils/errors.js"; import { findPackageRoot } from "./local/stack.js"; const MANAGED_MARKER = "<!-- managed-by: mesh skills sync — edits are overwritten; copy content elsewhere to customize -->"; const FENCE_START = "<!-- intent-skills:start -->"; const HOOK_RELATIVE = path.join(".intent", "hooks", "intent-claude-gate.mjs"); export const INTENT_RANGE = "^0.3.6"; export function resolveTargetRoot(startDir = process.cwd()) { try { return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: startDir, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], }).trim(); } catch { return startDir; } } function cliAsset(...segments) { return path.join(findPackageRoot(), ...segments); } function listBaseSkills() { const dir = cliAsset("skills"); if (!fs.existsSync(dir)) return []; return fs .readdirSync(dir, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(dir, entry.name, "SKILL.md"))) .map((entry) => ({ name: entry.name, source: path.join(dir, entry.name, "SKILL.md") })); } function renderManagedSkill(source) { const raw = fs.readFileSync(source, "utf-8"); const frontmatter = raw.match(/^(---\n[\s\S]*?\n---\n)/)?.[1]; if (!frontmatter) return `${MANAGED_MARKER}\n\n${raw}`; return `${frontmatter}\n${MANAGED_MARKER}\n${raw.slice(frontmatter.length)}`; } const BASE_SKILL_PACKAGE = "mesh-cli"; function meshTechScopeDirs(root, maxDepth = 6) { const scopes = []; const walk = (dir, depth) => { if (depth > maxDepth) return; let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { if (!entry.isDirectory()) continue; if (entry.name === "node_modules") { const scope = path.join(dir, entry.name, "@mesh-tech"); if (fs.existsSync(scope)) scopes.push(scope); continue; } if (entry.name.startsWith(".")) continue; walk(path.join(dir, entry.name), depth + 1); } }; walk(root, 0); return scopes; } export function listPackageSkills(root) { const byName = new Map(); for (const scopeDir of meshTechScopeDirs(root)) { for (const pkg of fs.readdirSync(scopeDir, { withFileTypes: true })) { if (!pkg.isDirectory() && !pkg.isSymbolicLink()) continue; if (pkg.name === BASE_SKILL_PACKAGE) continue; const skillsDir = path.join(scopeDir, pkg.name, "skills"); if (!fs.existsSync(skillsDir)) continue; for (const domain of fs.readdirSync(skillsDir, { withFileTypes: true })) { const source = path.join(skillsDir, domain.name, "SKILL.md"); if (!fs.existsSync(source)) continue; const name = `mesh-${pkg.name}-${domain.name}`; if (!byName.has(name)) byName.set(name, { name, dir: path.join(skillsDir, domain.name), source }); } } } return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)); } function copySkillExtras(sourceDir, targetDir) { for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) { if (entry.name === "SKILL.md") continue; fs.cpSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name), { recursive: true }); } } export function seedAppSkill(root, appName, appRelPath) { const target = path.join(root, ".claude", "skills", appName, "SKILL.md"); if (fs.existsSync(target)) return null; const template = fs.readFileSync(cliAsset("assets", "app-skill", "SKILL.md"), "utf-8"); const body = template.replaceAll("__APP_NAME__", appName).replaceAll("__APP_PATH__", appRelPath || "."); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, body); return path.relative(root, target); } function planSync(root) { const items = []; for (const skill of listBaseSkills()) { const target = path.join(root, ".claude", "skills", `mesh-${skill.name}`, "SKILL.md"); const desired = renderManagedSkill(skill.source); const label = `.claude/skills/mesh-${skill.name}/SKILL.md (base skill)`; if (!fs.existsSync(target)) { items.push({ label, state: "write", apply: () => { fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, desired); }, }); } else { const current = fs.readFileSync(target, "utf-8"); if (current === desired) { items.push({ label, state: "ok" }); } else if (current.includes(MANAGED_MARKER)) { items.push({ label, state: "write", apply: () => fs.writeFileSync(target, desired) }); } else { items.push({ label: `${label} — exists without the managed marker, leaving as-is`, state: "skip" }); } } } const packageSkills = listPackageSkills(root); for (const skill of packageSkills) { const targetDir = path.join(root, ".claude", "skills", skill.name); const target = path.join(targetDir, "SKILL.md"); const desired = renderManagedSkill(skill.source); const label = `.claude/skills/${skill.name}/SKILL.md (platform skill)`; if (!fs.existsSync(target)) { items.push({ label, state: "write", apply: () => { fs.mkdirSync(targetDir, { recursive: true }); fs.writeFileSync(target, desired); copySkillExtras(skill.dir, targetDir); }, }); } else { const current = fs.readFileSync(target, "utf-8"); if (current === desired) { items.push({ label, state: "ok" }); } else if (current.includes(MANAGED_MARKER)) { items.push({ label, state: "write", apply: () => { fs.writeFileSync(target, desired); copySkillExtras(skill.dir, targetDir); }, }); } else { items.push({ label: `${label} — exists without the managed marker, leaving as-is`, state: "skip" }); } } } if (packageSkills.length === 0 && meshTechScopeDirs(root).length === 0) { logInfo("No @mesh-tech packages installed yet — run `pnpm install`, then `mesh skills sync` again for the platform skills."); } const hookSource = cliAsset("assets", "intent", "intent-claude-gate.mjs"); const hookTarget = path.join(root, HOOK_RELATIVE); const hookDesired = fs.readFileSync(hookSource, "utf-8"); const hookCurrent = fs.existsSync(hookTarget) ? fs.readFileSync(hookTarget, "utf-8") : null; items.push(hookCurrent === hookDesired ? { label: `${HOOK_RELATIVE} (Intent gate)`, state: "ok" } : { label: `${HOOK_RELATIVE} (Intent gate)`, state: "write", apply: () => { fs.mkdirSync(path.dirname(hookTarget), { recursive: true }); fs.writeFileSync(hookTarget, hookDesired, { mode: 0o755 }); }, }); const rootPkgPath = path.join(root, "package.json"); if (fs.existsSync(rootPkgPath)) { let rootPkg = null; try { rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, "utf-8")); } catch { rootPkg = null; } const label = "package.json (@tanstack/intent devDependency)"; if (!rootPkg) { items.push({ label: `${label} — package.json is unparseable, leaving as-is`, state: "skip" }); } else if (rootPkg.devDependencies?.["@tanstack/intent"] || rootPkg.dependencies?.["@tanstack/intent"]) { items.push({ label, state: "ok" }); } else { items.push({ label, remediation: "Added @tanstack/intent — run `pnpm install` so `pnpm exec intent` resolves.", state: "write", apply: () => { rootPkg.devDependencies = rootPkg.devDependencies ?? {}; rootPkg.devDependencies["@tanstack/intent"] = INTENT_RANGE; rootPkg.devDependencies = Object.fromEntries(Object.entries(rootPkg.devDependencies).sort(([a], [b]) => a.localeCompare(b))); fs.writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2) + "\n"); }, }); } } const settingsPath = path.join(root, ".claude", "settings.json"); let settings = {}; try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8")); } catch { settings = {}; } const sessionStart = settings?.hooks?.SessionStart ?? []; const hasHook = sessionStart.some((entry) => (entry?.hooks ?? []).some((h) => JSON.stringify(h?.args ?? h?.command ?? "").includes("intent-claude-gate.mjs"))); items.push(hasHook ? { label: ".claude/settings.json (SessionStart Intent hook)", state: "ok" } : { label: ".claude/settings.json (SessionStart Intent hook)", state: "write", apply: () => { settings.hooks = settings.hooks ?? {}; settings.hooks.SessionStart = settings.hooks.SessionStart ?? []; settings.hooks.SessionStart.push({ matcher: "startup|resume|clear|compact", hooks: [ { type: "command", command: "node", args: ["${CLAUDE_PROJECT_DIR}/.intent/hooks/intent-claude-gate.mjs"], timeout: 10, statusMessage: "Loading Intent skill catalog", }, ], }); fs.mkdirSync(path.dirname(settingsPath), { recursive: true }); fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 1) + "\n"); }, }); const agentsPath = path.join(root, "AGENTS.md"); const fence = fs.readFileSync(cliAsset("assets", "intent", "agents-fence.md"), "utf-8"); const agentsCurrent = fs.existsSync(agentsPath) ? fs.readFileSync(agentsPath, "utf-8") : null; items.push(agentsCurrent?.includes(FENCE_START) ? { label: "AGENTS.md (intent-skills fence)", state: "ok" } : { label: "AGENTS.md (intent-skills fence)", state: "write", apply: () => { const next = agentsCurrent ? `${fence}\n${agentsCurrent}` : `${fence}\n# Agent Instructions\n`; fs.writeFileSync(agentsPath, next); }, }); return items; } export function syncSkills(root, opts = {}) { const items = planSync(root); let dirty = false; for (const item of items) { if (item.state === "ok") continue; if (item.state === "skip") { logWarn(item.label); continue; } dirty = true; if (opts.check) { logWarn(`missing/stale: ${item.label}`); } else { item.apply?.(); logSuccess(`synced: ${item.label}`); if (item.remediation) logInfo(item.remediation); } } if (!dirty) { logInfo(`Agent skills are in sync (${root})`); } return !dirty; } export function registerSkillsCommands(program) { const skills = program.command("skills").description("Agent-skill distribution (base skills + Intent discovery)"); skills .command("sync") .description("Install the base building-with-Mesh skills into .claude/skills/, fetch the platform skills shipped by the repo's installed @mesh-tech/* packages, and wire TanStack-Intent discovery (intent devDependency + hook + settings + AGENTS.md fence)") .option("--check", "verify only (CI/doctor): exit 1 when anything is missing or stale", false) .option("--root <path>", "target repo root (default: enclosing git root)") .action((opts) => { const root = path.resolve(opts.root ?? resolveTargetRoot()); const ok = syncSkills(root, { check: opts.check }); if (opts.check && !ok) { throw new MeshCliError("Agent skills are missing or stale.", { remediation: { command: "mesh skills sync" }, }); } }); }