UNPKG

@brianlovin/notion-skills

Version:

Sync agent skills from a Notion database to Claude Code, Codex, OpenCode, Cursor, Gemini CLI.

165 lines 6.25 kB
import chalk from "chalk"; import { existsSync, readdirSync, statSync } from "node:fs"; import { join } from "node:path"; import { getScope } from "../scope.js"; import { loadManifest } from "../manifest.js"; import { SKILLS_STORE } from "../paths.js"; import { auditSkill, loadAuditTarget, summariseIssues, } from "../audit.js"; /** * Run the audit rules against every skill on this machine. Default * scope: every local skill (installed + drafts). `--drafts` / * `--installed` / `--source` narrow the scope. JSON output for * automation; otherwise a tabular summary. * * Hard errors exit non-zero; warnings + infos do not (matches `npm * audit` behavior). */ export async function auditCommand(args, options = {}) { const scope = await getScope(); if (!scope) { throw new Error("No scope configured. Run `notion-skills init` first."); } const manifest = await loadManifest(scope.sources); const targets = await collectTargets(args, options, manifest); if (targets.length === 0) { console.log(chalk.dim("No skills match the audit scope.")); return; } const reports = targets.map((t) => ({ localSlug: t.target.localSlug, state: t.state, source_key: t.source_key, issues: auditSkill(t.target), })); if (options.json) { console.log(JSON.stringify(reports, null, 2)); if (reports.some((r) => r.issues.some((i) => i.severity === "error"))) { process.exit(1); } return; } renderReports(reports); if (reports.some((r) => r.issues.some((i) => i.severity === "error"))) { process.exit(1); } } async function collectTargets(args, options, manifest) { // Explicit slugs: audit just those (one or more). if (args.length > 0) { const out = []; for (const slug of args) { const dir = join(SKILLS_STORE, slug); if (!existsSync(join(dir, "SKILL.md"))) { throw new Error(`Skill "${slug}" not found on this machine. Run \`notion-skills list\` to see what's installed.`); } const target = await loadAuditTarget(slug, dir); if (!target) continue; const entry = manifest?.skills[slug]; out.push({ target, state: entry ? "installed" : "draft", source_key: entry?.source_key ?? null, }); } return out; } // No explicit slugs: walk the central store. if (!existsSync(SKILLS_STORE)) return []; const entries = readdirSync(SKILLS_STORE).filter((name) => { if (name.startsWith(".")) return false; try { return statSync(join(SKILLS_STORE, name)).isDirectory(); } catch { return false; } }); const out = []; for (const localSlug of entries) { const dir = join(SKILLS_STORE, localSlug); const target = await loadAuditTarget(localSlug, dir); if (!target) continue; const entry = manifest?.skills[localSlug]; const state = entry ? "installed" : "draft"; const source_key = entry?.source_key ?? null; if (options.drafts && state !== "draft") continue; if (options.installed && state !== "installed") continue; if (options.source && source_key !== options.source) continue; out.push({ target, state, source_key }); } return out; } function renderReports(reports) { console.log(chalk.bold("notion-skills audit")); console.log(chalk.dim("─".repeat(50))); console.log(""); let totalErrors = 0; let totalWarnings = 0; let totalInfos = 0; let cleanCount = 0; for (const r of reports) { const summary = summariseIssues(r.issues); totalErrors += summary.errors; totalWarnings += summary.warnings; totalInfos += summary.infos; if (r.issues.length === 0) { cleanCount++; // Only show clean rows when total skill count is small; otherwise // they crowd out the issues. Still print under -v in the future. if (reports.length <= 10) { console.log(`${chalk.green("✓")} ${r.localSlug} ${chalk.dim("clean")}`); } continue; } const tags = []; if (summary.errors > 0) tags.push(chalk.red(`${summary.errors} ${pluralise("error", summary.errors)}`)); if (summary.warnings > 0) tags.push(chalk.yellow(`${summary.warnings} ${pluralise("warning", summary.warnings)}`)); if (summary.infos > 0) tags.push(chalk.cyan(`${summary.infos} info`)); console.log(`${markFor(r.issues)} ${chalk.bold(r.localSlug)} ${chalk.dim(`[${tags.join(", ")}]`)}`); for (const issue of r.issues) { const sev = severityLabel(issue.severity); const id = chalk.dim(issue.ruleId.padEnd(28)); const lineHint = issue.line ? chalk.dim(` (line ${issue.line})`) : ""; console.log(` ${sev} ${id} ${issue.message}${lineHint}`); } } console.log(""); const parts = [`${reports.length} ${pluralise("skill", reports.length)} audited`]; if (cleanCount > 0) parts.push(`${cleanCount} clean`); if (totalErrors > 0) parts.push(chalk.red(`${totalErrors} ${pluralise("error", totalErrors)}`)); if (totalWarnings > 0) parts.push(chalk.yellow(`${totalWarnings} ${pluralise("warning", totalWarnings)}`)); if (totalInfos > 0) parts.push(chalk.cyan(`${totalInfos} info`)); console.log(chalk.dim(parts.join(" · "))); } function severityLabel(s) { if (s === "error") return chalk.red("error"); if (s === "warning") return chalk.yellow("warn "); return chalk.cyan("info "); } function markFor(issues) { if (issues.some((i) => i.severity === "error")) return chalk.red("✗"); if (issues.some((i) => i.severity === "warning")) return chalk.yellow("⚠"); return chalk.cyan("ℹ"); } function pluralise(word, n) { return n === 1 ? word : `${word}s`; } //# sourceMappingURL=audit.js.map