autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
687 lines (686 loc) • 30.5 kB
JavaScript
/**
* CursorDeliveryPipeline — 6 通道交付主入口
*
* 读取知识库 → 筛选 + 分类 + 排序 + 压缩 → 写入 6 个通道
*
* Channel A: .cursor/rules/autosnippet-project-rules.mdc (alwaysApply rules)
* Channel B: .cursor/rules/autosnippet-patterns-{topic}.mdc (smart rules)
* Channel C: .cursor/skills/ (project skills sync)
* Channel D: .cursor/skills/autosnippet-devdocs/ (dev documents)
* Channel F: AGENTS.md + CLAUDE.md + .github/copilot-instructions.md (agent instructions)
* + Mirror: .qoder/ .trae/ (IDE mirror)
*
* 触发时机:
* 1. bootstrap 完成后自动触发
* 2. `asd cursor-rules` CLI 命令手动触发
* 3. Recipe 状态变更(pending → active)后触发
* 4. `asd upgrade` 时作为升级步骤执行
*/
import fs from 'node:fs';
import path from 'node:path';
import { RawDbCallGraphAdapter, } from '../../repository/delivery/DeliveryRepoAdapter.js';
import { unwrapRawDb } from '../../repository/search/SearchRepoAdapter.js';
import { DELIVERY_RANK, KNOWLEDGE_CONFIDENCE } from '../../shared/constants.js';
import { DEFAULT_KNOWLEDGE_BASE_DIR } from '../../shared/ProjectMarkers.js';
import { AgentInstructionsGenerator } from './AgentInstructionsGenerator.js';
import { KnowledgeCompressor } from './KnowledgeCompressor.js';
import { RulesGenerator } from './RulesGenerator.js';
import { SkillsSyncer } from './SkillsSyncer.js';
import { BUDGET } from './TokenBudget.js';
import { TopicClassifier } from './TopicClassifier.js';
export class CursorDeliveryPipeline {
agentInstructions;
compressor;
database;
knowledgeService;
logger;
projectName;
projectRoot;
rulesGenerator;
skillsSyncer;
topicClassifier;
/**
* @param options.knowledgeService KnowledgeService 实例
* @param options.projectRoot 用户项目根目录
* @param [options.projectName] 项目名称
* @param [options.logger] 日志器
*/
constructor({ knowledgeService, projectRoot, projectName, logger, database, }) {
this.knowledgeService = knowledgeService;
this.projectRoot = projectRoot;
this.projectName = projectName || this._inferProjectName(projectRoot);
this.logger = logger || console;
this.database = database || null;
// 子模块
this.compressor = new KnowledgeCompressor();
this.topicClassifier = new TopicClassifier(this.projectName);
this.rulesGenerator = new RulesGenerator(projectRoot, this.projectName);
this.skillsSyncer = new SkillsSyncer(projectRoot, this.projectName, knowledgeService);
this.agentInstructions = new AgentInstructionsGenerator(projectRoot, this.projectName, logger);
}
/**
* 完整交付流程 — 生成 6 通道消费物料
* @returns >}
*/
async deliver() {
const startTime = Date.now();
const stats = {
channelA: { rulesCount: 0, tokensUsed: 0 },
channelB: {
topicCount: 0,
patternsCount: 0,
totalTokens: 0,
topics: {},
},
channelC: { synced: 0, skipped: 0, errors: 0 },
channelD: { documentsCount: 0, filesWritten: 0 },
channelF: { filesWritten: 0, totalTokens: 0 },
totalTokensUsed: 0,
duration: 0,
};
try {
// 1. 加载所有 active + pending 知识
const entries = await this._loadEntries();
this.logger.info?.(`[CursorDelivery] Loaded ${entries.length} knowledge entries`);
// 2. 分类:rules vs patterns vs facts vs documents
const { rules, patterns, facts, documents } = this._classify(entries);
this.logger.info?.(`[CursorDelivery] Classified: ${rules.length} rules, ${patterns.length} patterns, ${facts.length} facts, ${documents.length} documents`);
// 3. 清理旧的动态生成文件
this.rulesGenerator.cleanDynamicFiles();
// ── Channel A: Always-On Rules ──
const channelA = this._generateChannelA(rules);
stats.channelA = channelA;
// ── Baseline: 零知识库时注入基础引导 ──
if (entries.length === 0 && channelA.rulesCount === 0) {
const baseline = this.rulesGenerator.writeBaselineRules();
stats.channelA = { rulesCount: 0, tokensUsed: baseline.tokensUsed };
this.logger.info?.('[CursorDelivery] Baseline rules written (zero knowledge entries)');
}
// ── Channel B: Smart Rules (by topic) + Facts ──
const channelB = this._generateChannelB(patterns, facts);
stats.channelB = channelB;
// ── Channel B+: Call Graph Architecture Rules (Phase 5.2) ──
const archResult = this._generateCallGraphArchitectureRules();
if (archResult) {
stats.channelB.topicCount++;
stats.channelB.totalTokens += archResult.tokensUsed;
stats.channelB.topics = stats.channelB.topics || {};
stats.channelB.topics['call-architecture'] = {
patternsCount: archResult.insightsCount,
factsCount: 0,
tokensUsed: archResult.tokensUsed,
};
}
// ── Channel C: Skills Sync ──
const channelC = await this._generateChannelC();
stats.channelC = channelC;
// ── Channel D: Dev Documents → references ──
const channelD = this._generateChannelD(documents);
stats.channelD = channelD;
// ── Channel F: Agent Instructions (AGENTS.md / CLAUDE.md / copilot-instructions) ──
const channelF = this._generateChannelF(rules, patterns);
stats.channelF = channelF;
// NOTE: .qoder/ .trae/ 镜像不再自动执行,由 `asd mirror` 按需触发
stats.totalTokensUsed =
channelA.tokensUsed + channelB.totalTokens + (channelF.totalTokens || 0);
stats.duration = Date.now() - startTime;
this.logger.info?.(`[CursorDelivery] Done in ${stats.duration}ms — ` +
`A: ${channelA.rulesCount} rules (${channelA.tokensUsed} tokens), ` +
`B: ${channelB.topicCount} topics (${channelB.totalTokens} tokens), ` +
`C: ${channelC.synced} skills synced, ` +
`D: ${channelD.documentsCount} documents, ` +
`F: ${channelF.filesWritten} agent files`);
return { channelA, channelB, channelC, channelD, channelF, stats };
}
catch (error) {
this.logger.error?.(`[CursorDelivery] Error: ${error.message}`);
throw error;
}
}
// ─── 内部方法 ───────────────────────────────────────
/**
* 加载知识条目(active + staging + high-confidence pending)
*
* M2 六态状态机: staging/active/evolving 均为 CONSUMABLE 状态
*/
async _loadEntries() {
const allEntries = [];
// 加载 active + staging + evolving(CONSUMABLE 状态)
for (const state of ['active', 'staging', 'evolving']) {
try {
const result = await this.knowledgeService.list({ lifecycle: state }, { page: 1, pageSize: 200 });
const items = this._extractItems(result);
allEntries.push(...items);
}
catch (e) {
this.logger.warn?.(`[CursorDelivery] Failed to load ${state} entries: ${e.message}`);
}
}
// 加载 pending(高置信度的也纳入)
try {
const pending = await this.knowledgeService.list({ lifecycle: 'pending' }, { page: 1, pageSize: 200 });
const pendingItems = this._extractItems(pending);
const highConfPending = pendingItems.filter((e) => {
const qual = e.quality;
const conf = qual?.confidence;
return conf === undefined || conf === null || conf >= KNOWLEDGE_CONFIDENCE.PENDING_MIN;
});
allEntries.push(...highConfPending);
}
catch (e) {
this.logger.warn?.(`[CursorDelivery] Failed to load pending entries: ${e.message}`);
}
// 过滤掉历史遗留的 mock 条目(source=mock-bootstrap/mock-pipeline 或 createdBy=mock-ai)
const MOCK_SOURCES = new Set(['mock-bootstrap', 'mock-pipeline']);
const filtered = allEntries.filter((e) => {
if (MOCK_SOURCES.has(e.source)) {
return false;
}
if (e.createdBy === 'mock-ai') {
return false;
}
return true;
});
if (filtered.length < allEntries.length) {
this.logger.info?.(`[CursorDelivery] Filtered out ${allEntries.length - filtered.length} mock entries`);
}
return filtered;
}
/**
* 从 KnowledgeService.list() 返回值提取条目数组
*/
_extractItems(result) {
if (Array.isArray(result)) {
return result;
}
const obj = result;
if (obj?.items) {
return obj.items;
}
if (obj?.data) {
return obj.data;
}
return [];
}
/**
* 按 kind 分类知识条目
* dev-document 类型单独分流,不进入 Channel A/B 压缩
*/
_classify(entries) {
const rules = [], patterns = [], facts = [], documents = [];
for (const entry of entries) {
if (entry.knowledgeType === 'dev-document') {
documents.push(entry);
}
else if (entry.kind === 'rule') {
rules.push(entry);
}
else if (entry.kind === 'fact') {
facts.push(entry);
}
else {
patterns.push(entry); // 无 kind 或 kind='pattern' → pattern
}
}
return { rules, patterns, facts, documents };
}
/**
* 排序 — 质量分 + 统计使用量
*/
_rank(entries) {
return [...entries].sort((a, b) => {
const scoreA = this._rankScore(a);
const scoreB = this._rankScore(b);
return scoreB - scoreA;
});
}
/**
* 计算排名分
*/
_rankScore(entry) {
const qual = entry.quality;
const st = entry.stats;
let score = 0;
score +=
(qual?.confidence || KNOWLEDGE_CONFIDENCE.RANK_DEFAULT) * DELIVERY_RANK.CONFIDENCE_WEIGHT;
score += (qual?.authorityScore || 0) * DELIVERY_RANK.AUTHORITY_WEIGHT;
score +=
Math.min(st?.useCount || 0, DELIVERY_RANK.USE_COUNT_MAX) * DELIVERY_RANK.USE_COUNT_WEIGHT;
if (entry.lifecycle === 'active') {
score += DELIVERY_RANK.ACTIVE_BONUS;
}
return score;
}
/**
* Channel A 生成
*/
_generateChannelA(rules) {
const topRules = this._rank(rules).slice(0, BUDGET.CHANNEL_A_MAX_RULES);
const ruleLines = this.compressor.compressToRuleLine(topRules);
if (ruleLines.length === 0) {
this.logger.info?.('[CursorDelivery] Channel A: No rules to generate');
return { rulesCount: 0, tokensUsed: 0, filePath: null };
}
const result = this.rulesGenerator.writeAlwaysOnRules(ruleLines);
this.logger.info?.(`[CursorDelivery] Channel A: ${result.rulesCount} rules → ${result.filePath}`);
return result;
}
/**
* Channel B 生成(patterns + facts)
* @param patterns kind='pattern' 的知识条目
* @param [facts=[]] kind='fact' 的知识条目
*/
_generateChannelB(patterns, facts = []) {
const result = { topicCount: 0, patternsCount: 0, factsCount: 0, totalTokens: 0, topics: {} };
if (patterns.length === 0 && facts.length === 0) {
this.logger.info?.('[CursorDelivery] Channel B: No patterns or facts to generate');
return result;
}
// 按主题分组 patterns
const grouped = this.topicClassifier.group(patterns);
// 按主题分组 facts(复用同一分类器)
const groupedFacts = facts.length > 0 ? this.topicClassifier.group(facts) : {};
// 合并所有主题(patterns + facts 的并集)
const allTopics = new Set([...Object.keys(grouped), ...Object.keys(groupedFacts)]);
for (const topic of allTopics) {
const topicPatterns = grouped[topic] || [];
const topicFacts = groupedFacts[topic] || [];
// 压缩 patterns 为 When/Do/Don't
const top = this._rank(topicPatterns).slice(0, BUDGET.CHANNEL_B_MAX_PATTERNS);
const compressed = this.compressor.compressToWhenDoDont(top);
// 压缩 facts 为 Know 行
const factLines = this.compressor.compressToFactLines(topicFacts);
if (compressed.length === 0 && factLines.length === 0) {
continue;
}
// 格式化为 Markdown(patterns + facts)
let body = '';
if (compressed.length > 0) {
body += this.compressor.formatWhenDoDont(compressed);
}
if (factLines.length > 0) {
body += this.compressor.formatFactLines(factLines);
}
// 构建 description(合并 patterns 和 facts 条目用于关键词提取)
const allEntries = [...topicPatterns, ...topicFacts];
const description = this.topicClassifier.buildDescription(topic, allEntries);
// 写入 .mdc
const writeResult = this.rulesGenerator.writeSmartRules(topic, body, description);
result.topicCount++;
result.patternsCount += compressed.length;
result.factsCount += factLines.length;
result.totalTokens += writeResult.tokensUsed;
result.topics[topic] = {
patternsCount: compressed.length,
factsCount: factLines.length,
tokensUsed: writeResult.tokensUsed,
};
this.logger.info?.(`[CursorDelivery] Channel B: ${topic} — ${compressed.length} patterns + ${factLines.length} facts → ${writeResult.filePath}`);
}
return result;
}
/**
* Channel B+ — Call Graph Architecture Rules (Phase 5.2)
* 从调用图拓扑分析架构分层,生成 architecture smart rule
* @returns |null}
*/
_generateCallGraphArchitectureRules() {
if (!this.database) {
return null;
}
try {
const rawDb = unwrapRawDb(this.database);
const repo = new RawDbCallGraphAdapter(rawDb);
// 查询调用边中的跨目录调用模式
const callEdges = repo.findCallEdges();
if (!callEdges || callEdges.length < 5) {
return null;
}
// 提取 caller/callee 对应的文件路径
const entityFiles = new Map();
const entities = repo.findMethodEntities();
for (const e of entities) {
entityFiles.set(e.entity_id, e.file_path);
}
// 构建目录级调用矩阵
const dirCalls = new Map(); // 'src/controllers' → Map('src/services' → count)
for (const edge of callEdges) {
const callerFile = entityFiles.get(edge.from_id);
const calleeFile = entityFiles.get(edge.to_id);
if (!callerFile || !calleeFile || callerFile === calleeFile) {
continue;
}
const callerDir = this._extractLayerDir(callerFile);
const calleeDir = this._extractLayerDir(calleeFile);
if (!callerDir || !calleeDir || callerDir === calleeDir) {
continue;
}
if (!dirCalls.has(callerDir)) {
dirCalls.set(callerDir, new Map());
}
const targets = dirCalls.get(callerDir);
targets.set(calleeDir, (targets.get(calleeDir) || 0) + 1);
}
if (dirCalls.size === 0) {
return null;
}
// 检测架构层: 入度高(被调用多)的目录是底层服务, 出度高(调用多)的是上层
const dirInDegree = new Map();
const dirOutDegree = new Map();
for (const [from, targets] of dirCalls) {
for (const [to, count] of targets) {
dirOutDegree.set(from, (dirOutDegree.get(from) || 0) + count);
dirInDegree.set(to, (dirInDegree.get(to) || 0) + count);
}
}
// 生成架构洞察
const lines = [];
lines.push('## Call Graph Architecture');
lines.push('');
// 分层推断: 按 (inDegree - outDegree) 排序, 值越大 = 越底层
const allDirs = new Set([...dirInDegree.keys(), ...dirOutDegree.keys()]);
const layers = [...allDirs]
.map((dir) => ({
dir,
inDegree: dirInDegree.get(dir) || 0,
outDegree: dirOutDegree.get(dir) || 0,
layerScore: (dirInDegree.get(dir) || 0) - (dirOutDegree.get(dir) || 0),
}))
.sort((a, b) => b.layerScore - a.layerScore);
// 分层标签
const total = layers.length;
let insightsCount = 0;
if (total >= 2) {
lines.push('### Architecture Layers (inferred from call graph)');
lines.push('');
for (let i = 0; i < layers.length && i < 10; i++) {
const l = layers[i];
let layerLabel;
if (i < total * 0.33) {
layerLabel = '🔽 low-level (service/repository)';
}
else if (i < total * 0.66) {
layerLabel = '↔️ mid-level (business logic)';
}
else {
layerLabel = '🔼 high-level (controller/UI)';
}
lines.push(`- \`${l.dir}/\` — ${layerLabel} (in:${l.inDegree} out:${l.outDegree})`);
insightsCount++;
}
lines.push('');
}
// 核心调用链
const hotPaths = [...dirCalls.entries()]
.flatMap(([from, targets]) => [...targets.entries()].map(([to, count]) => ({ from, to, count })))
.sort((a, b) => b.count - a.count)
.slice(0, 8);
if (hotPaths.length > 0) {
lines.push('### Key Call Paths');
lines.push('');
for (const p of hotPaths) {
lines.push(`- \`${p.from}/\` → \`${p.to}/\` (${p.count} calls)`);
insightsCount++;
}
lines.push('');
}
if (insightsCount === 0) {
return null;
}
// 构建 description (用于 smart rule 关联性判断)
const dirList = layers.map((l) => l.dir).join(', ');
const description = `Architecture layer analysis for ${this.projectName}. ` +
`Relevant when editing files in: ${dirList}. ` +
`Call graph shows ${callEdges.length} cross-file call relationships.`;
const body = lines.join('\n');
const writeResult = this.rulesGenerator.writeSmartRules('call-architecture', body, description);
this.logger.info?.(`[CursorDelivery] Channel B+: call-architecture — ${insightsCount} insights → ${writeResult.filePath}`);
return {
insightsCount,
tokensUsed: writeResult.tokensUsed,
filePath: writeResult.filePath,
};
}
catch (err) {
this.logger.warn?.(`[CursorDelivery] Call graph architecture rules failed: ${err.message}`);
return null;
}
}
/**
* 从文件路径中提取层级目录 (第一或第二级有意义的目录)
*/
_extractLayerDir(filePath) {
if (!filePath) {
return null;
}
const parts = filePath.split('/').filter(Boolean);
// 跳过 src/ lib/ app/ 等通用前缀
const skipPrefixes = new Set(['src', 'lib', 'app', 'pkg', 'internal', 'cmd']);
let startIdx = 0;
if (parts.length > 1 && skipPrefixes.has(parts[0])) {
startIdx = 1;
}
// 取第一个有意义的目录
return parts[startIdx] || parts[0] || null;
}
/**
* Channel C 生成
*/
async _generateChannelC() {
try {
const syncResult = await this.skillsSyncer.sync();
this.logger.info?.(`[CursorDelivery] Channel C: ${syncResult.builtinSynced.length} builtin + ` +
`${syncResult.synced.length} project synced, ` +
`${syncResult.skipped.length} skipped, ${syncResult.errors.length} errors`);
return {
synced: syncResult.builtinSynced.length + syncResult.synced.length,
builtinSynced: syncResult.builtinSynced.length,
projectSynced: syncResult.synced.length,
skipped: syncResult.skipped.length,
errors: syncResult.errors.length,
details: syncResult,
};
}
catch (err) {
this.logger.error?.(`[CursorDelivery] Channel C error: ${err.message}`);
return {
synced: 0,
builtinSynced: 0,
projectSynced: 0,
skipped: 0,
errors: 1,
details: { synced: [], skipped: [], builtinSynced: [], errors: [err.message] },
};
}
}
/**
* Channel D — Dev Documents 生成
* 将 knowledgeType='dev-document' 的条目以原始 MD 写入
* .cursor/skills/autosnippet-devdocs/references/ 目录
*/
_generateChannelD(documents) {
const result = { documentsCount: 0, filesWritten: 0, filePaths: [] };
if (!documents || documents.length === 0) {
return result;
}
const devdocsDir = path.join(this.projectRoot, '.cursor', 'skills', 'autosnippet-devdocs');
const refsDir = path.join(devdocsDir, 'references');
fs.mkdirSync(refsDir, { recursive: true });
// 生成 SKILL.md(索引页)
const skillLines = [
'---',
'name: autosnippet-devdocs',
`description: "Development documents and knowledge artifacts for ${this.projectName}. Use when looking up architecture decisions, debug reports, design docs, or analysis notes."`,
'---',
'',
`# Dev Documents — ${this.projectName}`,
'',
'Use this skill when:',
'- Looking up architecture decisions or design rationale',
'- Reviewing debug reports or performance analysis',
'- Finding previous research or investigation notes',
'- Understanding project-specific decisions and trade-offs',
'',
'## Document Index',
'',
'| Title | Tags | Updated |',
'|-------|------|---------|',
];
for (const doc of documents) {
const tags = (doc.tags || []).join(', ') || '-';
const updated = doc.updatedAt
? new Date(doc.updatedAt * 1000).toISOString().split('T')[0]
: '-';
const slug = this._slugify(doc.title || doc.id || 'untitled');
skillLines.push(`| [${doc.title}](references/${slug}.md) | ${tags} | ${updated} |`);
// 写入单个文档 MD
const contentObj = doc.content;
const markdown = contentObj?.markdown || doc.description || '';
const docContent = [
`# ${doc.title || 'Untitled'}`,
'',
doc.description ? `> ${doc.description}` : '',
'',
`**Tags:** ${tags} `,
`**Scope:** ${doc.scope || 'universal'} `,
`**Created:** ${doc.createdAt ? new Date(doc.createdAt * 1000).toISOString().split('T')[0] : '-'}`,
'',
'---',
'',
markdown,
]
.filter(Boolean)
.join('\n');
const docPath = path.join(refsDir, `${slug}.md`);
fs.writeFileSync(docPath, docContent, 'utf8');
result.filePaths.push(docPath);
result.filesWritten++;
}
skillLines.push('');
skillLines.push('## Deeper Knowledge');
skillLines.push('');
skillLines.push('For full-text search across all documents:');
skillLines.push('- `autosnippet_search("your query")`');
fs.writeFileSync(path.join(devdocsDir, 'SKILL.md'), `${skillLines.join('\n')}\n`, 'utf8');
result.documentsCount = documents.length;
this.logger.info?.(`[CursorDelivery] Channel D: ${result.documentsCount} documents → ${refsDir}`);
return result;
}
/**
* Channel F — Agent Instructions 生成
* 生成 AGENTS.md / CLAUDE.md / .github/copilot-instructions.md
*/
_generateChannelF(rules, patterns) {
try {
// 收集可用 Skills 名称
const skillsDir = path.join(this.projectRoot, DEFAULT_KNOWLEDGE_BASE_DIR, 'skills');
let skills = [];
if (fs.existsSync(skillsDir)) {
skills = fs
.readdirSync(skillsDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
}
// 排序后传入
const rankedRules = this._rank(rules);
const rankedPatterns = this._rank(patterns);
const result = this.agentInstructions.generate({
rules: rankedRules,
patterns: rankedPatterns,
skills,
});
this.logger.info?.(`[CursorDelivery] Channel F: ${result.stats.filesWritten} agent instruction files ` +
`(${result.stats.totalTokens} tokens)`);
return {
filesWritten: result.stats.filesWritten,
totalTokens: result.stats.totalTokens,
files: {
agents: result.agents.filePath,
claude: result.claude.filePath,
copilot: result.copilot.filePath,
},
};
}
catch (err) {
this.logger.warn?.(`[CursorDelivery] Channel F error (non-blocking): ${err.message}`);
return { filesWritten: 0, totalTokens: 0, files: {} };
}
}
/**
* 文件名安全 slug 化
*/
_slugify(text) {
return (text
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
.replace(/^-+|-+$/g, '')
.substring(0, 80) || 'untitled');
}
/**
* 镜像 .cursor/ 交付物料到目标 IDE 目录(Qoder / Trae 等兼容 IDE)
* 只复制 autosnippet- 前缀的文件/目录,不触碰用户自定义内容
* @param targetDirName 目标目录名,如 '.qoder' 或 '.trae'
*/
_mirrorToIDE(targetDirName) {
try {
const cursorDir = path.join(this.projectRoot, '.cursor');
const targetDir = path.join(this.projectRoot, targetDirName);
// Mirror rules/ — 只复制 autosnippet-* 文件
const cursorRulesDir = path.join(cursorDir, 'rules');
if (fs.existsSync(cursorRulesDir)) {
const targetRulesDir = path.join(targetDir, 'rules');
fs.mkdirSync(targetRulesDir, { recursive: true });
for (const file of fs.readdirSync(cursorRulesDir)) {
if (!file.startsWith('autosnippet-')) {
continue;
}
const src = path.join(cursorRulesDir, file);
if (!fs.statSync(src).isFile()) {
continue;
}
// .mdc → .md
const destName = file.endsWith('.mdc') ? file.replace(/\.mdc$/, '.md') : file;
fs.copyFileSync(src, path.join(targetRulesDir, destName));
}
}
// Mirror skills/ — 只复制 autosnippet-* 子目录
const cursorSkillsDir = path.join(cursorDir, 'skills');
if (fs.existsSync(cursorSkillsDir)) {
const targetSkillsDir = path.join(targetDir, 'skills');
for (const entry of fs.readdirSync(cursorSkillsDir, { withFileTypes: true })) {
if (!entry.isDirectory() || !entry.name.startsWith('autosnippet-')) {
continue;
}
this._copyDirRecursive(path.join(cursorSkillsDir, entry.name), path.join(targetSkillsDir, entry.name));
}
}
this.logger.info?.(`[CursorDelivery] Mirrored autosnippet-* to ${targetDirName}/`);
}
catch (err) {
this.logger.warn?.(`[CursorDelivery] Mirror to ${targetDirName}/ failed: ${err.message}`);
}
}
/**
* 递归复制目录
*/
_copyDirRecursive(src, dest) {
fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
this._copyDirRecursive(srcPath, destPath);
}
else {
fs.copyFileSync(srcPath, destPath);
}
}
}
/**
* 从项目路径推断项目名称
*/
_inferProjectName(projectRoot) {
return path.basename(projectRoot);
}
}
export default CursorDeliveryPipeline;