autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
315 lines (314 loc) • 14.7 kB
JavaScript
/**
* SkillsSyncer — AutoSnippet Skills to .cursor/skills/ 同步器
*
* Channel C: 将内置 Skills 和项目级 Skills 统一同步到
* .cursor/skills/ 目录,适配 Cursor Agent Skills 标准格式。
*
* - 内置 Skills:从 AutoSnippet 包 skills/ 目录直接复制(autosnippet-create 等)
* - 项目级 Skills:从 AutoSnippet/skills/ 转换格式后写入(project-* → autosnippet-*)
* - 同时为项目级 Skill 生成 references/RECIPES.md(相关 Recipe 摘要)
*/
import fs from 'node:fs';
import path from 'node:path';
import { DEFAULT_KNOWLEDGE_BASE_DIR } from '../../shared/ProjectMarkers.js';
import { SKILLS_DIR as BUILTIN_SKILLS_DIR } from '../../shared/package-root.js';
/**
* 技能名称映射:AutoSnippet/skills/ → .cursor/skills/
* AutoSnippet/skills/ 下面是 bootstrap 动态生成的项目级 skills,
* 如 project-architecture/, project-code-standard/ 等。
*/
const SKILL_NAME_MAP = {
'project-architecture': 'autosnippet-architecture',
'project-coding-standards': 'autosnippet-coding-standards',
'project-agent-guidelines': 'autosnippet-guidelines',
'project-data-event-flow': 'autosnippet-data-flow',
'project-design-patterns': 'autosnippet-design-patterns',
'project-error-resilience': 'autosnippet-error-resilience',
'project-swift-objc-idiom': 'autosnippet-swift-objc-idiom',
// 语言维度
'project-ts-js-module': 'autosnippet-ts-js-module',
'project-react-patterns': 'autosnippet-react-patterns',
'project-python-structure': 'autosnippet-python-structure',
'project-jvm-annotation': 'autosnippet-jvm-annotation',
};
/** 用途描述模板(英文,Cursor 优先) */
const SKILL_DESC_MAP = {
'autosnippet-architecture': 'Architecture patterns, module boundaries, and dependency rules for {project}. Use when creating new modules, reviewing architecture, or understanding dependencies.',
'autosnippet-coding-standards': 'Coding standards and style conventions for {project}. Use when writing new code, reviewing formatting, or enforcing naming conventions.',
'autosnippet-guidelines': 'Agent interaction guidelines for {project}. Use when understanding how to work with this specific project.',
'autosnippet-data-flow': 'Event and data flow patterns for {project}. Use when working with events, state management, or data pipelines.',
'autosnippet-design-patterns': 'Common code patterns and idioms for {project}. Use when implementing features following project conventions.',
'autosnippet-error-resilience': 'Error handling, resilience patterns and defensive coding for {project}. Use when making design decisions or code review.',
'autosnippet-swift-objc-idiom': 'Swift/ObjC idioms, categories, method swizzling and interop for {project}. Use when working with Swift or Objective-C code.',
'autosnippet-ts-js-module': 'Module export structure, barrel exports, and public API surface for {project}. Use when working with imports/exports or module boundaries.',
'autosnippet-react-patterns': 'React component patterns, state management conventions and routing for {project}. Use when following framework patterns.',
'autosnippet-python-structure': 'Python package structure, __init__.py exports, import patterns and type hint coverage for {project}. Use when working with Python modules.',
'autosnippet-jvm-annotation': 'Annotation patterns (DI, ORM, API, custom) and meta-programming for {project}. Use when working with Spring, Jakarta, or framework annotations.',
};
export class SkillsSyncer {
knowledgeService;
projectName;
projectRoot;
sourceDir;
targetDir;
/**
* @param projectRoot 用户项目根目录
* @param projectName 项目名称
* @param [knowledgeService] 可选,用于生成 references/RECIPES.md
*/
constructor(projectRoot, projectName = 'Project', knowledgeService = null) {
this.projectRoot = projectRoot;
this.projectName = projectName;
this.knowledgeService = knowledgeService;
this.sourceDir = path.join(projectRoot, DEFAULT_KNOWLEDGE_BASE_DIR, 'skills');
this.targetDir = path.join(projectRoot, '.cursor', 'skills');
}
/**
* 执行完整同步流程
* @returns >}
*/
async sync() {
const result = {
synced: [],
skipped: [],
errors: [],
builtinSynced: [],
};
// ── Phase 1: 同步内置 Skills ──
this._syncBuiltinSkills(result);
// ── Phase 2: 同步项目级 Skills ──
await this._syncProjectSkills(result);
return result;
}
/**
* 同步内置 Skills:从 AutoSnippet 包 skills/ 目录直接复制到 .cursor/skills/
*/
_syncBuiltinSkills(result) {
if (!fs.existsSync(BUILTIN_SKILLS_DIR)) {
return;
}
const builtinDirs = fs
.readdirSync(BUILTIN_SKILLS_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
for (const name of builtinDirs) {
try {
const src = path.join(BUILTIN_SKILLS_DIR, name);
const dest = path.join(this.targetDir, name);
fs.cpSync(src, dest, { recursive: true, force: true });
result.builtinSynced.push(name);
}
catch (err) {
result.errors.push(`builtin/${name}: ${err.message}`);
}
}
}
/**
* 同步项目级 Skills:从 AutoSnippet/skills/ 转换格式后写入 .cursor/skills/
*/
async _syncProjectSkills(result) {
// 检查源目录是否存在
if (!fs.existsSync(this.sourceDir)) {
return;
}
// 扫描源目录
const skillDirs = fs
.readdirSync(this.sourceDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
for (const dirName of skillDirs) {
try {
const sourceSkillPath = path.join(this.sourceDir, dirName, 'SKILL.md');
if (!fs.existsSync(sourceSkillPath)) {
result.skipped.push(dirName);
continue;
}
const targetName = SKILL_NAME_MAP[dirName] ||
`autosnippet-${dirName.replace(/^project-/, '')}`;
const targetSkillDir = path.join(this.targetDir, targetName);
// 创建目标目录
fs.mkdirSync(targetSkillDir, { recursive: true });
// 读取源 SKILL.md
const sourceContent = fs.readFileSync(sourceSkillPath, 'utf8');
// 转换格式
const targetContent = this._convertSkillMd(sourceContent, targetName, dirName);
// 写入目标 SKILL.md
fs.writeFileSync(path.join(targetSkillDir, 'SKILL.md'), targetContent, 'utf8');
// 生成 references/RECIPES.md
await this._generateRecipes(targetSkillDir, dirName);
result.synced.push(targetName);
}
catch (err) {
result.errors.push(`${dirName}: ${err.message}`);
}
}
}
/**
* 转换 SKILL.md 格式 — 从 AutoSnippet 格式到 Cursor Agent Skills 标准
*/
_convertSkillMd(source, targetName, sourceDirName) {
// 提取原始内容(去掉 frontmatter)
const bodyMatch = source.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
const body = bodyMatch ? bodyMatch[1].trim() : source.trim();
// 获取描述
const descTemplate = SKILL_DESC_MAP[targetName] ||
`Knowledge and patterns from {project}. Use when working with ${sourceDirName.replace(/^project-/, '')} related code.`;
const description = descTemplate.replace(/\{project\}/g, this.projectName);
// 构建 Cursor 标准格式
const dimensionLabel = sourceDirName.replace(/^project-/, '').replace(/-/g, ' ');
const lines = [
'---',
`name: ${targetName}`,
`description: "${description}"`,
'---',
'',
`# ${this._capitalizeWords(dimensionLabel)} — ${this.projectName}`,
'',
'Use this skill when:',
...this._generateUseCases(sourceDirName),
'',
'## Instructions',
'',
body,
'',
'## Deeper Knowledge',
'',
`For detailed recipes and code examples:`,
`- \`autosnippet_search("${dimensionLabel}")\``,
'',
'## Referenced Files',
'',
'See `references/RECIPES.md` for related recipe summaries.',
];
return `${lines.join('\n')}\n`;
}
/**
* 生成 references/RECIPES.md
*/
async _generateRecipes(targetSkillDir, sourceDirName) {
const refsDir = path.join(targetSkillDir, 'references');
fs.mkdirSync(refsDir, { recursive: true });
// 如果有 knowledgeService,查询该维度的 recipes
let recipes = [];
if (this.knowledgeService) {
try {
const dimension = sourceDirName.replace(/^project-/, '');
const result = await this.knowledgeService.list({ lifecycle: 'active', category: dimension }, { page: 1, pageSize: 50 });
const resultObj = result;
if (Array.isArray(resultObj)) {
recipes = resultObj;
}
else if (resultObj && typeof resultObj === 'object') {
recipes = (resultObj.items ||
resultObj.data ||
[]);
}
}
catch {
// 忽略查询错误
}
}
// 生成 RECIPES.md
const dimensionLabel = sourceDirName.replace(/^project-/, '').replace(/-/g, ' ');
const lines = [`# ${this._capitalizeWords(dimensionLabel)} Recipes`, ''];
if (recipes.length > 0) {
lines.push('| Title | Trigger | Kind | Lang | Summary |');
lines.push('|---|---|---|---|---|');
for (const entry of recipes.slice(0, 20)) {
const title = (entry.title || '').replace(/\|/g, '/');
const trigger = entry.trigger || '-';
const kind = entry.kind || '-';
const lang = entry.language || '-';
const summary = (entry.summaryCn || entry.description || '')
.replace(/\|/g, '/')
.slice(0, 80);
lines.push(`| ${title} | ${trigger} | ${kind} | ${lang} | ${summary} |`);
}
}
else {
lines.push('No recipes available yet. Run `asd bootstrap` to generate knowledge.');
}
lines.push('');
lines.push(`For full content, use: \`autosnippet_search("${dimensionLabel}")\``);
fs.writeFileSync(path.join(refsDir, 'RECIPES.md'), `${lines.join('\n')}\n`, 'utf8');
}
/**
* 生成使用场景列表
*/
_generateUseCases(sourceDirName) {
const casesMap = {
'project-architecture': [
'- Creating new modules, services, or managers',
'- Reviewing architectural decisions',
'- Understanding module boundaries and dependency rules',
],
'project-code-standard': [
'- Writing new code and need to follow coding standards',
'- Reviewing code formatting and naming conventions',
'- Setting up new files with proper structure',
],
'project-profile': [
'- Need background on the project and tech stack',
'- Understanding the overall project structure',
'- Onboarding or getting project context',
],
'project-agent-guidelines': [
'- Understanding project-specific workflow requirements',
'- Following project conventions for AI-assisted coding',
],
'project-event-and-data-flow': [
'- Working with events, notifications, or callbacks',
'- Implementing data flow or state management',
'- Understanding how data moves through the system',
],
'project-code-pattern': [
'- Implementing features using project conventions',
'- Looking for common code patterns and idioms',
'- Need a code template for a typical operation',
],
'project-objc-deep-scan': [
'- Working with Objective-C runtime features',
'- Understanding method swizzling or hook registries',
'- Modifying sensitive Objective-C code',
],
'project-category-scan': [
'- Looking for existing utility methods',
'- Working with categories or extensions',
'- Avoiding duplicate implementations',
],
'project-best-practice': [
'- Making design decisions',
'- Code review and quality improvements',
'- Choosing between implementation approaches',
],
'project-module-exports': [
'- Working with module imports and exports',
'- Understanding the public API surface',
'- Refactoring barrel exports or re-export chains',
],
'project-framework-conventions': [
'- Following framework-specific patterns (React/Vue/Angular)',
'- Organizing components and routes',
'- Implementing state management patterns',
],
'project-python-structure': [
'- Working with Python modules and packages',
'- Understanding import patterns and __init__.py exports',
'- Adding type hints or decorators',
],
'project-jvm-annotations': [
'- Working with dependency injection annotations',
'- Configuring ORM entities and API endpoints',
'- Using or creating custom annotations',
],
};
return (casesMap[sourceDirName] || [
'- Working with code related to this dimension',
'- Need guidance on project-specific patterns',
]);
}
_capitalizeWords(str) {
return str.replace(/\b\w/g, (c) => c.toUpperCase());
}
}
export default SkillsSyncer;