cc-devflow 4.1.0 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -225,18 +225,18 @@ function parseCommand(filePath) {
225
225
  async function parseAllCommands(dirPath) {
226
226
  const absoluteDir = path.resolve(dirPath);
227
227
 
228
- // 读取目录中的所有文件
229
- const files = fs.readdirSync(absoluteDir);
228
+ const mdFiles = collectMarkdownFiles(absoluteDir, absoluteDir);
230
229
 
231
- // 过滤 .md 文件
232
- const mdFiles = files.filter(file => file.endsWith('.md'));
233
-
234
- // 解析每个文件
230
+ // 解析每个文件(保留相对路径,支持分层输出)
235
231
  const results = [];
236
- for (const file of mdFiles) {
237
- const filePath = path.join(absoluteDir, file);
232
+ for (const filePath of mdFiles) {
238
233
  try {
239
234
  const ir = parseCommand(filePath);
235
+
236
+ // 输出文件名使用 commands 根目录相对路径(不含扩展名)
237
+ const relativePath = path.relative(absoluteDir, filePath).replace(/\\/g, '/');
238
+ ir.source.filename = relativePath.replace(/\.md$/i, '');
239
+
240
240
  results.push(ir);
241
241
  } catch (error) {
242
242
  // 重新抛出以便调用者处理
@@ -247,9 +247,44 @@ async function parseAllCommands(dirPath) {
247
247
  return results;
248
248
  }
249
249
 
250
+ // ============================================================
251
+ // collectMarkdownFiles - 递归收集 Markdown 文件
252
+ // ============================================================
253
+ function collectMarkdownFiles(rootDir, currentDir) {
254
+ const results = [];
255
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
256
+
257
+ for (const entry of entries) {
258
+ // 跳过隐藏目录与 "_" 前缀目录
259
+ if (entry.isDirectory()) {
260
+ if (entry.name.startsWith('.') || entry.name.startsWith('_')) {
261
+ continue;
262
+ }
263
+
264
+ const childFiles = collectMarkdownFiles(rootDir, path.join(currentDir, entry.name));
265
+ results.push(...childFiles);
266
+ continue;
267
+ }
268
+
269
+ if (!entry.isFile() || !entry.name.endsWith('.md')) {
270
+ continue;
271
+ }
272
+
273
+ results.push(path.join(currentDir, entry.name));
274
+ }
275
+
276
+ // 统一排序,确保输出稳定
277
+ if (currentDir === rootDir) {
278
+ results.sort((a, b) => a.localeCompare(b));
279
+ }
280
+
281
+ return results;
282
+ }
283
+
250
284
  module.exports = {
251
285
  parseCommand,
252
286
  parseAllCommands,
287
+ collectMarkdownFiles,
253
288
  hashContent,
254
289
  detectPlaceholders,
255
290
  validateScriptAliases,
@@ -120,6 +120,12 @@ function scanInlineClaudePaths(content) {
120
120
  let cleanPath = match[0];
121
121
  // 移除末尾的常见标点
122
122
  cleanPath = cleanPath.replace(/[,;:.\s]+$/, '');
123
+
124
+ // 跳过 glob 模式(例如 *.jsonl.template),避免误判为单文件资源
125
+ if (cleanPath.includes('*')) {
126
+ continue;
127
+ }
128
+
123
129
  paths.add(cleanPath);
124
130
  }
125
131
 
@@ -0,0 +1,68 @@
1
+ /**
2
+ * T041: Skill Discovery Utility
3
+ *
4
+ * [INPUT]: .claude/skills/ 目录
5
+ * [OUTPUT]: SkillEntry[] (name, skillDir, skillMdPath)
6
+ * [POS]: Skills 扫描工具,统一发现分组与非分组 Skill
7
+ * [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
8
+ *
9
+ * 规则:
10
+ * - 递归扫描所有子目录
11
+ * - 目录内存在 SKILL.md 即判定为 Skill
12
+ * - 跳过以下划线开头的目录(例如 _reference-implementations)
13
+ * - 扫描结果按 skillMdPath 排序,保证输出稳定
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ // ============================================================
20
+ // discoverSkillEntries - 递归扫描 Skill 目录
21
+ // ============================================================
22
+ function discoverSkillEntries(skillsDir) {
23
+ const absoluteDir = path.resolve(skillsDir);
24
+
25
+ if (!fs.existsSync(absoluteDir)) {
26
+ return [];
27
+ }
28
+
29
+ const entries = [];
30
+ walkSkillDirs(absoluteDir, entries);
31
+
32
+ // 保证输出稳定,避免不同文件系统顺序差异
33
+ entries.sort((a, b) => a.skillMdPath.localeCompare(b.skillMdPath));
34
+ return entries;
35
+ }
36
+
37
+ // ============================================================
38
+ // walkSkillDirs - 深度优先扫描
39
+ // ============================================================
40
+ function walkSkillDirs(dirPath, entries) {
41
+ const dirName = path.basename(dirPath);
42
+ if (dirName.startsWith('_')) {
43
+ return;
44
+ }
45
+
46
+ const skillMdPath = path.join(dirPath, 'SKILL.md');
47
+ if (fs.existsSync(skillMdPath)) {
48
+ entries.push({
49
+ name: dirName,
50
+ skillDir: dirPath,
51
+ skillMdPath
52
+ });
53
+ return;
54
+ }
55
+
56
+ const children = fs.readdirSync(dirPath, { withFileTypes: true });
57
+ for (const child of children) {
58
+ if (!child.isDirectory()) {
59
+ continue;
60
+ }
61
+
62
+ walkSkillDirs(path.join(dirPath, child.name), entries);
63
+ }
64
+ }
65
+
66
+ module.exports = {
67
+ discoverSkillEntries
68
+ };
@@ -11,6 +11,7 @@
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
13
  const matter = require('gray-matter');
14
+ const { discoverSkillEntries } = require('./skill-discovery.js');
14
15
 
15
16
  // ============================================================
16
17
  // Constants
@@ -23,35 +24,18 @@ const REGISTRY_OUTPUT_PATH = 'devflow/.generated/skills-registry.json';
23
24
  // ============================================================
24
25
  function generateSkillsRegistry(skillsDir) {
25
26
  const registry = [];
26
- const absoluteDir = path.resolve(skillsDir);
27
-
28
- if (!fs.existsSync(absoluteDir)) {
29
- return registry;
30
- }
31
-
32
- const entries = fs.readdirSync(absoluteDir, { withFileTypes: true });
33
-
34
- for (const entry of entries) {
35
- if (!entry.isDirectory() || entry.name.startsWith('_')) {
36
- continue;
37
- }
38
-
39
- const skillDir = path.join(absoluteDir, entry.name);
40
- const skillMdPath = path.join(skillDir, 'SKILL.md');
41
-
42
- if (!fs.existsSync(skillMdPath)) {
43
- continue;
44
- }
27
+ const skillEntries = discoverSkillEntries(skillsDir);
45
28
 
29
+ for (const entry of skillEntries) {
46
30
  try {
47
- const skillMdContent = fs.readFileSync(skillMdPath, 'utf8');
31
+ const skillMdContent = fs.readFileSync(entry.skillMdPath, 'utf8');
48
32
  const parsed = matter(skillMdContent);
49
33
 
50
34
  registry.push({
51
35
  name: parsed.data.name || entry.name,
52
36
  description: parsed.data.description || '',
53
37
  type: parsed.data.type || 'utility',
54
- path: skillDir,
38
+ path: entry.skillDir,
55
39
  triggers: []
56
40
  });
57
41
  } catch (error) {
@@ -82,29 +66,12 @@ async function generateSkillsRegistryV2(skillsDir) {
82
66
  }
83
67
  }
84
68
 
85
- // 扫描技能目录
86
- if (!fs.existsSync(absoluteDir)) {
87
- return createRegistry(skills);
88
- }
89
-
90
- const entries = fs.readdirSync(absoluteDir, { withFileTypes: true });
69
+ // 扫描技能目录(支持分组与非分组结构)
70
+ const entries = discoverSkillEntries(absoluteDir);
91
71
 
92
72
  for (const entry of entries) {
93
- // 跳过非目录和 _ 前缀目录
94
- if (!entry.isDirectory() || entry.name.startsWith('_')) {
95
- continue;
96
- }
97
-
98
- const skillDir = path.join(absoluteDir, entry.name);
99
- const skillMdPath = path.join(skillDir, 'SKILL.md');
100
-
101
- // 必须有 SKILL.md
102
- if (!fs.existsSync(skillMdPath)) {
103
- continue;
104
- }
105
-
106
73
  try {
107
- const skillEntry = parseSkillEntry(entry.name, skillDir, skillMdPath, skillRules);
74
+ const skillEntry = parseSkillEntry(entry.name, entry.skillDir, entry.skillMdPath, skillRules);
108
75
  if (skillEntry) {
109
76
  skills.push(skillEntry);
110
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-devflow",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "DevFlow CLI tool",
5
5
  "main": "bin/cc-devflow.js",
6
6
  "bin": {