#!/usr/bin/env node
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));
const ROOT_DIR = process.env.HOME + "/.claude";
const OUTPUT_DIR = join(__dirname, "output");
const TEMPLATES_DIR = join(__dirname, "templates");
if (!existsSync(OUTPUT_DIR)) {
mkdirSync(OUTPUT_DIR, { recursive: true });
}
const CONFIG = {
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");
},
},
],
};
function formatPluginName(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;
}
function capitalize(str) {
if (!str) return "";
return str.charAt(0).toUpperCase() + str.slice(1);
}
function getSkillCategory(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";
}
function parseFrontmatter(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*/, ", ");
}
} else if (isMultiline && line.trim()) {
multilineValue.push(line.trim());
}
}
if (isMultiline && currentKey) {
result[currentKey] = multilineValue.join(" ").trim();
}
return result;
}
function getTitle(content) {
const lines = content.split("\n");
for (const line of lines) {
if (line.startsWith("# ")) {
return line.replace("# ", "").trim();
}
}
return null;
}
function scanFiles(pattern, exclude = []) {
const patterns = Array.isArray(pattern) ? pattern : [pattern];
let files = [];
for (const p of patterns) {
files = files.concat(globSync(p, { ignore: exclude }));
}
return [...new Set(files.filter((f) => !lstatSync(f).isDirectory()))];
}
function getLatestVersion(versions) {
if (versions.length === 0) return null;
const semverVersions = [];
const nonSemverVersions = [];
for (const v of versions) {
if (/^\d+\.\d+(\.\d+)?(-[a-zA-Z0-9.-]+)?$/.test(v)) {
semverVersions.push(v);
} else {
nonSemverVersions.push(v);
}
}
if (semverVersions.length > 0) {
const compareSemver = (a, b) => {
const parse = (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;
if (!pa.preRelease && pb.preRelease) return -1;
if (pa.preRelease && !pb.preRelease) return 1;
return 0;
};
return semverVersions.sort(compareSemver)[0];
}
return nonSemverVersions.sort().pop();
}
function getPluginVersions(pluginPath) {
if (!existsSync(pluginPath)) return [];
const entries = readdirSync(pluginPath, { withFileTypes: true });
return entries
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name);
}
function filterLatestVersionFiles(files, patterns, exclude = []) {
if (files.length === 0) return files;
const pluginVersions = new Map();
for (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 = new Set();
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];
}
function groupFiles(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;
}
function getAgentUsageInstructions(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 {
return readFileSync(templatePath, "utf-8");
} catch (error) {
console.warn(`Warning: Failed to read template file: ${templatePath}`, error);
return "";
}
}
function generateMarkdown(mapping, groups) {
let md = `---\nversion: 1.0\nlastUpdated: ${new Date().toISOString().split("T")[0]}\n---\n\n`;
md += `# ${mapping.name} 映射表\n\n`;
md += `本文件从 \`${mapping.sourceDir}\` 目录自动扫描生成。\n\n`;
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 += `*最后更新:${new Date().toLocaleDateString("zh-CN")}*\n`;
return md;
}
function generateTable(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;
}
function generateRow(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 || "-";
if (description.startsWith('"') && description.endsWith('"')) {
description = description.slice(1, -1);
}
if (shortcut) {
return `| ${shortcut} | ${name} | ${description} | \`${shortPath}\` |`;
}
return `| ${name} | ${description} | \`${shortPath}\` |`;
}
function run() {
console.log("Scanning and generating mapping documents...\n");
for (const mapping of CONFIG.mappings) {
console.log(` Processing: ${mapping.name}...`);
const patterns = Array.isArray(mapping.sourcePattern) ? mapping.sourcePattern : [mapping.sourcePattern];
let files = [];
for (const pattern of patterns) {
const fullPattern = join(mapping.sourceDir, pattern);
files = files.concat(globSync(fullPattern, { ignore: mapping.exclude }));
}
files = [...new Set(files.filter((f) => !lstatSync(f).isDirectory()))];
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();