feat(mosaic): migrate install wizard from v0 to v1 (#103)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
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:
96
packages/mosaic/src/skills/catalog.ts
Normal file
96
packages/mosaic/src/skills/catalog.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
143
packages/mosaic/src/skills/categories.ts
Normal file
143
packages/mosaic/src/skills/categories.ts
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user