Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions bin/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,12 @@ function scanInvocableSkills(skillsDir) {

/**
* 根据 SKILL.md 元数据生成 command .md 内容
*
* 设计原则:
* - 读取 SKILL.md + 执行脚本合并为一气呵成的指令流
* - 禁止「先…然后…」的分步模式,避免 Claude 在步骤间停顿
* - 无脚本的 skill:仅读取 SKILL.md 作为知识库提供指导
*
* @param {Object} meta - parseFrontmatter 返回的元数据
* @param {string} skillRelPath - 相对于 skills/ 的路径(如 'tools/gen-docs')
* @param {boolean} hasScripts - 是否有可执行脚本
Expand All @@ -253,24 +259,23 @@ function generateCommandContent(meta, skillRelPath, hasScripts) {
lines.push(`allowed-tools: ${tools}`);
lines.push('---');
lines.push('');
lines.push('## 执行指南');
lines.push('');
lines.push('先读取完整的 Skill 定义获取详细规范:');
lines.push('');
lines.push('```');
lines.push(skillPath);
lines.push('```');

if (hasScripts) {
// ── 有脚本的 skill:读取规范 + 执行脚本,一气呵成 ──
lines.push('以下所有步骤一气呵成,不要在步骤间停顿等待用户输入:');
lines.push('');
lines.push('然后执行:');
lines.push(`1. 读取规范:${skillPath}`);
lines.push(`2. 执行命令:\`node ~/.claude/skills/run_skill.js ${name} $ARGUMENTS\``);
lines.push('3. 按规范分析输出,完成后续动作');
lines.push('');
lines.push('```bash');
lines.push(`node ~/.claude/skills/run_skill.js ${name} $ARGUMENTS`);
lines.push('```');
lines.push('全程不要停顿,不要询问是否继续。');
} else {
// ── 无脚本的 skill:知识库模式 ──
lines.push('读取以下秘典,根据内容为用户提供专业指导:');
lines.push('');
lines.push('根据秘典内容为用户提供专业指导。');
lines.push('```');
lines.push(skillPath);
lines.push('```');
}

lines.push('');
Expand Down
58 changes: 56 additions & 2 deletions skills/tools/gen-docs/scripts/doc_generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,66 @@ const path = require('path');

// --- Utilities ---

function rglob(dir, filter) {
function parseGitignore(modPath) {
const patterns = [];
const hardcoded = ['node_modules', '.git', '__pycache__', '.vscode', '.idea', 'dist', 'build', '.DS_Store'];

// 硬编码常见排除
hardcoded.forEach(p => patterns.push({ pattern: p, negate: false }));

// 解析 .gitignore
try {
const gitignorePath = path.join(modPath, '.gitignore');
const content = fs.readFileSync(gitignorePath, 'utf8');
content.split('\n').forEach(line => {
line = line.trim();
if (line && !line.startsWith('#')) {
const negate = line.startsWith('!');
if (negate) line = line.slice(1);
patterns.push({ pattern: line, negate });
}
});
} catch {}

return patterns;
}

function shouldIgnore(filePath, basePath, patterns) {
const relPath = path.relative(basePath, filePath);
const name = path.basename(filePath);

let ignored = false;
for (const {pattern, negate} of patterns) {
let match = false;

if (pattern.includes('*')) {
// 通配符匹配
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
match = regex.test(name) || regex.test(relPath);
} else if (pattern.includes('/')) {
// 路径匹配
match = relPath.includes(pattern) || relPath.startsWith(pattern);
} else {
// 文件名匹配
match = name === pattern || relPath.includes(`/${pattern}`) || relPath.startsWith(pattern);
}

if (match) ignored = !negate;
}
return ignored;
}

function rglob(dir, filter, basePath = dir) {
const patterns = parseGitignore(basePath);
const results = [];

for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);

if (shouldIgnore(full, basePath, patterns)) continue;

if (entry.isDirectory()) {
results.push(...rglob(full, filter));
results.push(...rglob(full, filter, basePath));
} else if (!filter || filter(entry.name, full)) {
results.push(full);
}
Expand Down
83 changes: 83 additions & 0 deletions test/gen-docs.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict';

const fs = require('fs');
const path = require('path');
const os = require('os');
const { spawn } = require('child_process');

// 集成测试:通过实际运行验证功能
describe('gen-docs gitignore 支持', () => {
let tempDir;

beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gen-docs-test-'));
});

afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});

function runGenDocs(targetPath, args = []) {
return new Promise((resolve, reject) => {
const scriptPath = path.join(__dirname, '../skills/tools/gen-docs/scripts/doc_generator.js');
const child = spawn('node', [scriptPath, targetPath, '--json', ...args], {
stdio: 'pipe'
});

let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => stdout += data);
child.stderr.on('data', (data) => stderr += data);

child.on('close', (code) => {
if (code === 0) {
try {
resolve(JSON.parse(stdout));
} catch {
resolve({ stdout, stderr });
}
} else {
reject(new Error(`Exit ${code}: ${stderr}`));
}
});
});
}

test('排除 node_modules 目录', async () => {
// 创建测试结构
fs.mkdirSync(path.join(tempDir, 'node_modules'));
fs.mkdirSync(path.join(tempDir, 'src'));
fs.writeFileSync(path.join(tempDir, 'src/main.js'), 'console.log("test");');
fs.writeFileSync(path.join(tempDir, 'node_modules/package.json'), '{}');

const result = await runGenDocs(tempDir, ['--force']);

expect(result.status).toBe('success');

const readme = fs.readFileSync(path.join(tempDir, 'README.md'), 'utf8');
expect(readme).toContain('src/main.js');
expect(readme).not.toContain('node_modules');
});

test('支持 .gitignore 规则排除代码目录', async () => {
// 创建 .gitignore
fs.writeFileSync(path.join(tempDir, '.gitignore'), 'dist/\n.cache/');

// 创建测试文件 - 用 .js 文件确保进入目录结构
fs.mkdirSync(path.join(tempDir, 'src'));
fs.mkdirSync(path.join(tempDir, 'dist'));
fs.mkdirSync(path.join(tempDir, '.cache'));
fs.writeFileSync(path.join(tempDir, 'src/main.js'), 'export default {}');
fs.writeFileSync(path.join(tempDir, 'dist/bundle.js'), 'built');
fs.writeFileSync(path.join(tempDir, '.cache/cache.js'), 'cached');

const result = await runGenDocs(tempDir, ['--force']);

expect(result.status).toBe('success');

const readme = fs.readFileSync(path.join(tempDir, 'README.md'), 'utf8');
expect(readme).toContain('src/main.js'); // 正常文件应出现
expect(readme).not.toContain('dist/bundle.js'); // 被 gitignore 排除
expect(readme).not.toContain('.cache/cache.js');// 被 gitignore 排除
});
});
Loading