feat(mosaic): migrate install wizard from v0 to v1 (#103)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #103.
This commit is contained in:
2026-03-15 00:59:42 +00:00
committed by jason.woltje
parent 84e1868028
commit c4e52085e3
31 changed files with 2272 additions and 2 deletions

View File

@@ -0,0 +1,96 @@
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { parse as parseYaml } from 'yaml';
import { RECOMMENDED_SKILLS } from '../constants.js';
export interface SkillEntry {
name: string;
description: string;
version?: string;
recommended: boolean;
source: 'canonical' | 'local';
}
export function loadSkillsCatalog(mosaicHome: string): SkillEntry[] {
const skills: SkillEntry[] = [];
// Load canonical skills
const canonicalDir = join(mosaicHome, 'skills');
if (existsSync(canonicalDir)) {
skills.push(...loadSkillsFromDir(canonicalDir, 'canonical'));
}
// Fallback to source repo
const sourceDir = join(mosaicHome, 'sources', 'agent-skills', 'skills');
if (skills.length === 0 && existsSync(sourceDir)) {
skills.push(...loadSkillsFromDir(sourceDir, 'canonical'));
}
// Load local skills
const localDir = join(mosaicHome, 'skills-local');
if (existsSync(localDir)) {
skills.push(...loadSkillsFromDir(localDir, 'local'));
}
return skills.sort((a, b) => a.name.localeCompare(b.name));
}
function loadSkillsFromDir(dir: string, source: 'canonical' | 'local'): SkillEntry[] {
const entries: SkillEntry[] = [];
let dirEntries;
try {
dirEntries = readdirSync(dir, { withFileTypes: true });
} catch {
return entries;
}
for (const entry of dirEntries) {
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
const skillMdPath = join(dir, entry.name, 'SKILL.md');
if (!existsSync(skillMdPath)) continue;
try {
const content = readFileSync(skillMdPath, 'utf-8');
const frontmatter = parseFrontmatter(content);
entries.push({
name: (frontmatter['name'] as string | undefined) ?? entry.name,
description: (frontmatter['description'] as string | undefined) ?? '',
version: frontmatter['version'] as string | undefined,
recommended: RECOMMENDED_SKILLS.has(entry.name),
source,
});
} catch {
// Skip malformed skills
entries.push({
name: entry.name,
description: '',
recommended: RECOMMENDED_SKILLS.has(entry.name),
source,
});
}
}
return entries;
}
function parseFrontmatter(content: string): Record<string, unknown> {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match?.[1]) return {};
try {
return (parseYaml(match[1]) as Record<string, unknown>) ?? {};
} catch {
// Fallback: simple key-value parsing
const result: Record<string, string> = {};
for (const line of match[1].split('\n')) {
const kv = line.match(/^(\w[\w-]*)\s*:\s*(.+)/);
if (kv?.[1] !== undefined && kv[2] !== undefined) {
result[kv[1]] = kv[2].replace(/^['"]|['"]$/g, '');
}
}
return result;
}
}

View File

@@ -0,0 +1,143 @@
/**
* Skill category definitions and mapping.
* Skills are assigned to categories by name, with keyword fallback.
*/
export const SKILL_CATEGORIES: Record<string, string[]> = {
'Frontend & UI': [
'ai-sdk',
'algorithmic-art',
'antfu',
'canvas-design',
'frontend-design',
'next-best-practices',
'nuxt',
'pinia',
'shadcn-ui',
'slidev',
'tailwind-design-system',
'theme-factory',
'ui-animation',
'unocss',
'vercel-composition-patterns',
'vercel-react-best-practices',
'vercel-react-native-skills',
'vue',
'vue-best-practices',
'vue-router-best-practices',
'vueuse-functions',
'web-artifacts-builder',
'web-design-guidelines',
'vite',
'vitepress',
],
'Backend & Infrastructure': [
'architecture-patterns',
'fastapi',
'mcp-builder',
'nestjs-best-practices',
'python-performance-optimization',
'tsdown',
'turborepo',
'pnpm',
'dispatching-parallel-agents',
'subagent-driven-development',
'create-agent',
'proactive-agent',
'using-superpowers',
'kickstart',
'executing-plans',
],
'Testing & Quality': [
'code-review-excellence',
'lint',
'pr-reviewer',
'receiving-code-review',
'requesting-code-review',
'systematic-debugging',
'test-driven-development',
'verification-before-completion',
'vitest',
'vue-testing-best-practices',
'webapp-testing',
],
'Marketing & Growth': [
'ab-test-setup',
'analytics-tracking',
'competitor-alternatives',
'copy-editing',
'copywriting',
'email-sequence',
'form-cro',
'free-tool-strategy',
'launch-strategy',
'marketing-ideas',
'marketing-psychology',
'onboarding-cro',
'page-cro',
'paid-ads',
'paywall-upgrade-cro',
'popup-cro',
'pricing-strategy',
'product-marketing-context',
'programmatic-seo',
'referral-program',
'schema-markup',
'seo-audit',
'signup-flow-cro',
'social-content',
],
'Product & Strategy': [
'brainstorming',
'brand-guidelines',
'content-strategy',
'writing-plans',
'skill-creator',
'writing-skills',
'prd',
],
'Developer Practices': ['finishing-a-development-branch', 'using-git-worktrees'],
'Auth & Security': [
'better-auth-best-practices',
'create-auth-skill',
'email-and-password-best-practices',
'organization-best-practices',
'two-factor-authentication-best-practices',
],
'Content & Documentation': [
'doc-coauthoring',
'docx',
'internal-comms',
'pdf',
'pptx',
'slack-gif-creator',
'xlsx',
],
};
// Reverse lookup: skill name -> category
const SKILL_TO_CATEGORY = new Map<string, string>();
for (const [category, skills] of Object.entries(SKILL_CATEGORIES)) {
for (const skill of skills) {
SKILL_TO_CATEGORY.set(skill, category);
}
}
export function categorizeSkill(name: string, description: string): string {
const mapped = SKILL_TO_CATEGORY.get(name);
if (mapped) return mapped;
return inferCategoryFromDescription(description);
}
function inferCategoryFromDescription(desc: string): string {
const lower = desc.toLowerCase();
if (/\b(react|vue|css|frontend|ui|component|tailwind|design)\b/.test(lower))
return 'Frontend & UI';
if (/\b(api|backend|server|docker|infra|deploy)\b/.test(lower)) return 'Backend & Infrastructure';
if (/\b(test|lint|review|debug|quality)\b/.test(lower)) return 'Testing & Quality';
if (/\b(marketing|seo|copy|ads|cro|conversion|email)\b/.test(lower)) return 'Marketing & Growth';
if (/\b(auth|security|2fa|password|credential)\b/.test(lower)) return 'Auth & Security';
if (/\b(doc|pdf|word|sheet|writing|comms)\b/.test(lower)) return 'Content & Documentation';
if (/\b(product|strategy|brainstorm|plan|prd)\b/.test(lower)) return 'Product & Strategy';
return 'Developer Practices';
}