#!/usr/bin/env node/** * Scan and Generate Mapping Documents * 扫描 ~/.claude/ 目录并生成映射文档 */import{ readFileSync, writeFileSync, mkdirSync, existsSync, lstatSync, readdirSync,}from"fs";import{ globSync }from"glob";import{ dirname, basename, join }from"path";import{ fileURLToPath }from"url";const __dirname =dirname(fileURLToPath(import.meta.url));constROOT_DIR= process.env.HOME+"/.claude";constOUTPUT_DIR=join(__dirname,"output");constTEMPLATES_DIR=join(__dirname,"templates");// 确保输出目录存在if(!existsSync(OUTPUT_DIR)){mkdirSync(OUTPUT_DIR,{recursive:true});}// 扫描配置constCONFIG={mappings:[{id:"commands",name:"Local Commands",outputFile:"commands-mapping.md",sourceDir:ROOT_DIR+"/commands/",sourcePattern:"**/*.md",exclude:["CLAUDE.md"],frontmatterFields:["description","argument-hint","allowed-tools"],groupBy:(file)=>{const relative = file.replace(ROOT_DIR+"/commands/","");const parts = relative.split("/");return parts.length >1?capitalize(parts[0]):"General";},getShortcut:(file, frontmatter)=>{const relative = file .replace(ROOT_DIR+"/commands/","").replace(".md","").replace(/\//g,":");return relative ?"/"+ relative :"/";},getName:(frontmatter, title, file)=>{return frontmatter.name || title ||basename(file,".md");},},{id:"plugins-commands",name:"Plugin Commands",outputFile:"plugins-commands-mapping.md",sourceDir:ROOT_DIR+"/plugins/cache/",sourcePattern:["**/commands/*.md","**/.claude/commands/*.md"],exclude:["CLAUDE.md"],frontmatterFields:["description","name"],groupBy:(file)=>{const match = file.match(/plugins\/cache\/([^\/]+)\/([^\/]+)/);if(match){returnformatPluginName(match[1], match[2]);}return"Unknown";},getShortcut:(file, frontmatter)=>{const cmdMatch = file.match(/[\/\.](claude\/)commands\/([^\/]+)\.md$/);const cmdName = cmdMatch ? cmdMatch[2]:basename(file,".md");const orgMatch = file.match(/plugins\/cache\/([^\/]+)\//);const org = orgMatch ? orgMatch[1]:"";return"/"+(org ? org.replace("-plugins","")+":":"")+ cmdName;},getName:(frontmatter, title, file)=>{const match = file.match(/[\/\.](claude\/)commands\/([^\/]+)\.md$/);return frontmatter.name || match?.[2]||basename(file,".md");},},{id:"plugins-agents",name:"Plugin Agents",outputFile:"plugins-agents-mapping.md",sourceDir:ROOT_DIR+"/plugins/cache/",sourcePattern:["**/agents/**/*.md","**/.claude/agents/**/*.md"],exclude:["CLAUDE.md"],frontmatterFields:["description","name"],groupBy:(file)=>{const match = file.match(/plugins\/cache\/([^\/]+)\/([^\/]+)/);if(match){returnformatPluginName(match[1], match[2]);}return"Unknown";},subgroupBy:(file)=>{const relative = file.split(/agents\//)[1];if(!relative)return"Agents";const parts = relative.split("/");return parts.length >1?capitalize(parts[0]):"Agents";},getShortcut:null,getName:(frontmatter, title, file)=>{return frontmatter.name || title ||basename(file,".md");},},{id:"plugins-skills",name:"Plugin Skills",outputFile:"plugins-skills-mapping.md",sourceDir:ROOT_DIR+"/plugins/cache/",sourcePattern:"**/.claude/skills/*.md",exclude:["CLAUDE.md","SKILL.md"],frontmatterFields:["description","name"],groupBy:(file)=>{const match = file.match(/plugins\/cache\/([^\/]+)\/([^\/]+)/);if(match){returnformatPluginName(match[1], match[2]);}return"Unknown";},getShortcut:null,getName:(frontmatter, title, file)=>{return frontmatter.name || title ||basename(file,".md");},},{id:"skills",name:"Local Skills",outputFile:"skills-mapping.md",sourceDir:ROOT_DIR+"/skills/",sourcePattern:"*/SKILL.md",exclude:[],frontmatterFields:["description","name"],groupBy:(file)=>{const relative = file.replace(ROOT_DIR+"/skills/","");const skillName = relative.split("/")[0];returngetSkillCategory(skillName);},getShortcut:null,getName:(frontmatter, title, file)=>{return frontmatter.name || title ||basename(file,".md");},},{id:"agents",name:"Local Agents",outputFile:"agents-mapping.md",sourceDir:ROOT_DIR+"/agents/",sourcePattern:"**/*.md",exclude:["CLAUDE.md"],frontmatterFields:["description","name"],groupBy:(file)=>{const relative = file.replace(ROOT_DIR+"/agents/","");const parts = relative.split("/");return parts.length >1?capitalize(parts[0]):"General";},getShortcut:null,getName:(frontmatter, title, file)=>{return frontmatter.name || title ||basename(file,".md");},},],};// 工具函数functionformatPluginName(org, plugin){const displayNames ={thedotmack:"Claude Mem","nyldn-plugins":"Claude Octopus","claude-plugins-official":"Official Plugins","planning-with-files":"Planning With Files",};const orgDisplay = displayNames[org]|| org;const pluginDisplay = plugin === org ?"":` (${org})`;return orgDisplay + pluginDisplay;}functioncapitalize(str){if(!str)return"";return str.charAt(0).toUpperCase()+ str.slice(1);}functiongetSkillCategory(skillName){const categories ={"eslint-auto-fix":"Development","ast-grep":"Development","frontend-design":"Development","code-refactor-flow":"Development",zustand:"Development","migrating-js-to-ts":"Development","logging-best-practices":"Development","code-analyze":"Code Analysis","code-review":"Code Analysis","git-diff-report":"Code Analysis","prompt-optimizer":"AI & Prompt",codeagent:"AI & Prompt","codex-subagent":"AI & Prompt",claudeception:"Skill Management","skill-creator":"Skill Management","skill-validator":"Skill Management","skill-review":"Skill Management","tech-blog":"Content & Writing","stop-slop":"Content & Writing","task-manager":"Project Management",figma2code:"Project Management",};return categories[skillName]||"Other";}functionparseFrontmatter(content){const result ={};const match = content.match(/^---\n([\s\S]*?)\n---/);if(!match)return result;const yamlContent = match[1];const lines = yamlContent.split("\n");let currentKey =null;let multilineValue =[];let isMultiline =false;for(let i =0; i < lines.length; i++){const line = lines[i];const colonIndex = line.indexOf(":");// 检查是否是新的键值对if(colonIndex >0&&!line.startsWith(" ")&&!line.startsWith("\t")){// 保存之前的多行值if(isMultiline && currentKey){ result[currentKey]= multilineValue.join(" ").trim(); multilineValue =[]; isMultiline =false;}const key = line.slice(0, colonIndex).trim();const value = line.slice(colonIndex +1).trim();// 检查是否是多行格式 (> 或 |)if(value ===">"|| value ==="|"){ currentKey = key; isMultiline =true; multilineValue =[];}else{// 单行值 currentKey = key; result[key]= value.replace(/^\[|\]$/g,"").replace(/,\s*/g,", ");}}elseif(isMultiline && line.trim()){// 多行值的内容行(忽略空行) multilineValue.push(line.trim());}}// 保存最后一个多行值if(isMultiline && currentKey){ result[currentKey]= multilineValue.join(" ").trim();}return result;}functiongetTitle(content){const lines = content.split("\n");for(const line of lines){if(line.startsWith("# ")){return line.replace("# ","").trim();}}returnnull;}functionscanFiles(pattern, exclude =[]){const patterns = Array.isArray(pattern)? pattern :[pattern];let files =[];for(const p of patterns){ files = files.concat(globSync(p,{ignore: exclude }));}return[...newSet(files.filter((f)=>!lstatSync(f).isDirectory()))];}// 获取插件目录下的最新版本号functiongetLatestVersion(versions){if(versions.length ===0)returnnull;// 分离语义版本和非语义版本const semverVersions =[];const nonSemverVersions =[];for(const v of versions){// 语义版本格式: x.y.z (可能带有 pre-release 标签)if(/^\d+\.\d+(\.\d+)?(-[a-zA-Z0-9.-]+)?$/.test(v)){ semverVersions.push(v);}else{ nonSemverVersions.push(v);}}// 如果有语义版本,使用 semver 比较if(semverVersions.length >0){// 简单的 semver 比较函数constcompareSemver=(a, b)=>{constparse=(v)=>{const parts = v.split("-")[0].split(".").map(Number);const preRelease = v.split("-")[1]||"";return{major: parts[0]||0,minor: parts[1]||0,patch: parts[2]||0, preRelease,};};const pa =parse(a);const pb =parse(b);if(pa.major !== pb.major)return pb.major - pa.major;if(pa.minor !== pb.minor)return pb.minor - pa.minor;if(pa.patch !== pb.patch)return pb.patch - pa.patch;// 处理 pre-release: 正式版 > pre-releaseif(!pa.preRelease && pb.preRelease)return-1;if(pa.preRelease &&!pb.preRelease)return1;return0;};return semverVersions.sort(compareSemver)[0];}// 否则使用字母序最后一个return nonSemverVersions.sort().pop();}// 获取插件目录下所有版本目录functiongetPluginVersions(pluginPath){if(!existsSync(pluginPath))return[];const entries =readdirSync(pluginPath,{withFileTypes:true});return entries .filter((entry)=> entry.isDirectory()).map((entry)=> entry.name);}// 过滤文件,只保留每个插件最新版本的内容functionfilterLatestVersionFiles(files, patterns, exclude =[]){if(files.length ===0)return files;// 解析文件路径,提取插件标识和版本// 格式: plugins/cache/{org}/{plugin}/{version}/...const pluginVersions =newMap();// key: "org/plugin", value: versionfor(const file of files){const match = file.match(/plugins\/cache\/([^\/]+)\/([^\/]+)\/([^\/]+)/);if(match){const org = match[1];const plugin = match[2];const version = match[3];const key =`${org}/${plugin}`;if(!pluginVersions.has(key)){ pluginVersions.set(key, version);}else{const currentLatest = pluginVersions.get(key);const candidateVersions =[currentLatest, version];const latest =getLatestVersion(candidateVersions); pluginVersions.set(key, latest);}}}// 重新扫描获取最新版本的实际文件const latestFiles =newSet();const sourceDir =ROOT_DIR+"/plugins/cache/";for(const[key, version]of pluginVersions){const[org, plugin]= key.split("/");const versionPath =join(sourceDir, org, plugin, version);if(existsSync(versionPath)){for(const pattern of patterns){const fullPattern =join(versionPath, pattern);const matched =globSync(fullPattern,{ignore: exclude,}); matched.forEach((f)=> latestFiles.add(f));}}}return[...latestFiles];}functiongroupFiles(files, groupBy, subgroupBy =null){const groups ={};for(const file of files){const category =groupBy(file);if(!groups[category]){ groups[category]= subgroupBy ?{}:[];}if(subgroupBy){const subcategory =subgroupBy(file);if(!groups[category][subcategory]){ groups[category][subcategory]=[];} groups[category][subcategory].push(file);}else{ groups[category].push(file);}}return groups;}functiongetAgentUsageInstructions(mappingId){// 模板文件映射const templateFiles ={commands:"commands-usage.md","plugins-commands":"plugins-commands-usage.md","plugins-agents":"plugins-agents-usage.md","plugins-skills":"plugins-skills-usage.md",skills:"skills-usage.md",agents:"agents-usage.md",};const templateFile = templateFiles[mappingId];if(!templateFile){return"";}const templatePath =join(TEMPLATES_DIR, templateFile);if(!existsSync(templatePath)){ console.warn(`Warning: Template file not found: ${templatePath}`);return"";}try{returnreadFileSync(templatePath,"utf-8");}catch(error){ console.warn(`Warning: Failed to read template file: ${templatePath}`, error,);return"";}}functiongenerateMarkdown(mapping, groups){let md =`---\nversion: 1.0\nlastUpdated: ${newDate().toISOString().split("T")[0]}\n---\n\n`; md +=`# ${mapping.name} 映射表\n\n`; md +=`本文件从 \`${mapping.sourceDir}\` 目录自动扫描生成。\n\n`;// 添加 Agent 使用流程说明const usageInstructions =getAgentUsageInstructions(mapping.id);if(usageInstructions){ md += usageInstructions;} md +=`---\n\n`;const categories = Object.keys(groups).sort();let totalCount =0;for(const category of categories){const group = groups[category]; md +=`## ${category}\n\n`;if(mapping.subgroupBy &&typeof group ==="object"){const subcategories = Object.keys(group).sort();for(const subcategory of subcategories){const files = group[subcategory]; md +=`### ${subcategory}\n\n`; md +=generateTable(mapping, files, totalCount); totalCount += files.length;}}else{const files = Array.isArray(group)? group : group[category]||[]; md +=generateTable(mapping, files, totalCount); totalCount += files.length;}} md +=`---\n\n`; md +=`*最后更新:${newDate().toLocaleDateString("zh-CN")}*\n`;return md;}functiongenerateTable(mapping, files, startIndex){if(files.length ===0)return"";let md ="";const hasShortcut = mapping.getShortcut !==null;if(hasShortcut){ md +=`| 快捷方式 | 名称 | 描述 | 完整路径 |\n`; md +=`|----------|------|------|----------|\n`;}else{ md +=`| 名称 | 描述 | 完整路径 |\n`; md +=`|------|------|----------|\n`;}for(const file of files.sort()){ md +=generateRow(mapping, file)+"\n";} md +="\n";return md;}functiongenerateRow(mapping, filePath){const content =readFileSync(filePath,"utf-8");const frontmatter =parseFrontmatter(content);const title =getTitle(content);const shortPath = filePath.replace(process.env.HOME,"~");const name = mapping.getName(frontmatter, title, filePath);const shortcut = mapping.getShortcut ? mapping.getShortcut(filePath, frontmatter):"";let description = frontmatter.description ||"-";// Clean up description - remove surrounding quotes if presentif(description.startsWith('"')&& description.endsWith('"')){ description = description.slice(1,-1);}if(shortcut){return`| ${shortcut} | ${name} | ${description} | \`${shortPath}\` |`;}return`| ${name} | ${description} | \`${shortPath}\` |`;}functionrun(){ console.log("Scanning and generating mapping documents...\n");for(const mapping ofCONFIG.mappings){ console.log(` Processing: ${mapping.name}...`);const patterns = Array.isArray(mapping.sourcePattern)? mapping.sourcePattern :[mapping.sourcePattern];let files =[];for(const pattern of patterns){// Prepend sourceDir to all patternsconst fullPattern =join(mapping.sourceDir, pattern); files = files.concat(globSync(fullPattern,{ignore: mapping.exclude }));} files =[...newSet(files.filter((f)=>!lstatSync(f).isDirectory()))];// 插件 mapping 需要过滤只保留最新版本if(mapping.id.startsWith("plugins-")){ files =filterLatestVersionFiles(files, patterns, mapping.exclude);}if(files.length ===0){ console.log(` Warning: No files found for ${mapping.name}`);continue;}const groups =groupFiles(files, mapping.groupBy, mapping.subgroupBy);const markdown =generateMarkdown(mapping, groups);const outputPath =join(OUTPUT_DIR, mapping.outputFile);writeFileSync(outputPath, markdown,"utf-8");const totalItems = Object.values(groups).reduce((sum, group)=>{if(mapping.subgroupBy &&typeof group ==="object"){return sum + Object.values(group).reduce((s, f)=> s + f.length,0);}return sum +(Array.isArray(group)? group.length :0);},0); console.log(` Generated: ${mapping.outputFile} (${totalItems} items)`);} console.log("\nAll mapping documents generated successfully!");}run();