UNPKG

@brianlovin/notion-skills

Version:

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

123 lines 4.36 kB
const KEY_RE = /^[a-z0-9][a-z0-9-]*$/; const KEY_MAX = 32; export function validateKey(key) { if (!key || key.length === 0) return "key cannot be empty"; if (key.length > KEY_MAX) return `key must be ${KEY_MAX} characters or fewer`; if (!KEY_RE.test(key)) { return "key must be lowercase letters, digits, and hyphens (starting with a letter or digit)"; } return null; } export function findByKey(sources, key) { return sources.find((s) => s.key === key); } export function findByDatabaseId(sources, databaseId) { return sources.find((s) => s.database_id === databaseId); } export function defaultSource(sources) { return sources.find((s) => s.default); } /** * Pure slug-from-title transform. No collision handling; caller decides * what to do when this collides with an existing key. Used by `source * add` so it can detect "would collide?" and only prompt in that case. */ export function slugifyDbTitle(databaseTitle) { return (databaseTitle .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, KEY_MAX) || "source"); } /** * Slug-from-title with collision avoidance. Suffix `-2`, `-3`, … until * unique. Used at v1→v2 migration time and as the prompt default in * `source add` when a collision forces re-prompting. */ export function deriveKey(databaseTitle, existingKeys) { const base = slugifyDbTitle(databaseTitle); if (!existingKeys.has(base)) return base; for (let i = 2; i < 1000; i++) { const candidate = `${base}-${i}`.slice(0, KEY_MAX); if (!existingKeys.has(candidate)) return candidate; } // Pathological: 1000+ collisions. Fall through to a hashlike suffix. return `${base}-${Date.now().toString(36)}`.slice(0, KEY_MAX); } /** * Parse a CLI ref like "team/deploy" into its parts. Returns `slug` only * for bare refs ("deploy"); `sourceKey` is undefined in that case and * the caller resolves it against scope. */ export function parseSkillRef(input) { const idx = input.indexOf("/"); if (idx === -1) return { slug: input }; return { sourceKey: input.slice(0, idx), slug: input.slice(idx + 1) }; } /** * Build the global identifier used in user-facing output: "team/deploy". * This is the canonical way to print a skill across sources; the manifest * itself is keyed by local_slug, not this string. */ export function formatRef(sourceKey, sourceSlug) { return `${sourceKey}/${sourceSlug}`; } /** * Pick the single source a command should target. Used by every * command that operates on exactly one source (publish, source-scoped * install, etc.). * * Order: * 1. Explicit `--source <key>` → that source (or unknown_key error). * 2. Exactly one configured source → it. * 3. A source has `default: true` → it. * 4. Otherwise → ambiguous; caller decides whether to prompt or error. */ export function resolveTargetSource(flag, sources) { if (sources.length === 0) return { ok: false, reason: "no_sources" }; if (flag !== undefined) { const s = findByKey(sources, flag); return s ? { ok: true, source: s } : { ok: false, reason: "unknown_key", key: flag }; } if (sources.length === 1) return { ok: true, source: sources[0] }; const def = defaultSource(sources); if (def) return { ok: true, source: def }; return { ok: false, reason: "ambiguous", sources }; } /** * Sanitise a sources array: enforce at-most-one default, dedupe by key * (last wins), and dedupe by database_id (first wins). Used at scope- * read time so a hand-edited or partially-corrupt scope.json still * produces a coherent in-memory state. */ export function sanitiseSources(input) { const byKey = new Map(); const seenDbs = new Set(); for (const s of input) { if (seenDbs.has(s.database_id)) continue; seenDbs.add(s.database_id); byKey.set(s.key, s); } const out = [...byKey.values()]; // Collapse multiple defaults to the first. let seenDefault = false; for (const s of out) { if (s.default) { if (seenDefault) s.default = false; else seenDefault = true; } } return out; } //# sourceMappingURL=sources.js.map