cc-devflow 4.1.1 → 4.1.3

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.
Files changed (32) hide show
  1. package/.claude/commands/flow/dev.md +1 -1
  2. package/.claude/commands/flow/quality.md +5 -0
  3. package/.claude/commands/flow/release.md +3 -3
  4. package/.claude/commands/flow/restart.md +1 -1
  5. package/.claude/commands/flow/status.md +2 -2
  6. package/.claude/commands/flow/update.md +1 -1
  7. package/.claude/commands/util/git-commit.md +1 -4
  8. package/.claude/scripts/flow-quality-full.sh +32 -1
  9. package/.claude/scripts/flow-quality-quick.sh +57 -2
  10. package/.claude/scripts/generate-status-report.sh +5 -5
  11. package/.claude/scripts/recover-workflow.sh +24 -24
  12. package/.claude/skills/workflow/flow-quality/SKILL.md +4 -0
  13. package/.claude/skills/workflow/flow-release/SKILL.md +1 -1
  14. package/CHANGELOG.md +70 -0
  15. package/bin/adapt.js +4 -0
  16. package/lib/compiler/CLAUDE.md +5 -3
  17. package/lib/compiler/__tests__/compile-regression.test.js +103 -0
  18. package/lib/compiler/__tests__/multi-module-emitters.test.js +37 -11
  19. package/lib/compiler/__tests__/parser.test.js +46 -0
  20. package/lib/compiler/__tests__/resource-copier.test.js +26 -0
  21. package/lib/compiler/__tests__/skill-discovery.test.js +72 -0
  22. package/lib/compiler/emitters/antigravity-emitter.js +57 -39
  23. package/lib/compiler/emitters/base-emitter.js +81 -1
  24. package/lib/compiler/emitters/codex-emitter.js +71 -69
  25. package/lib/compiler/emitters/cursor-emitter.js +14 -34
  26. package/lib/compiler/emitters/qwen-emitter.js +14 -34
  27. package/lib/compiler/index.js +124 -0
  28. package/lib/compiler/parser.js +43 -8
  29. package/lib/compiler/resource-copier.js +6 -0
  30. package/lib/compiler/skill-discovery.js +68 -0
  31. package/lib/compiler/skills-registry.js +8 -41
  32. package/package.json +1 -1
@@ -21,6 +21,7 @@ const yaml = require('js-yaml');
21
21
  const matter = require('gray-matter');
22
22
  const BaseEmitter = require('./base-emitter.js');
23
23
  const { ContextExpander } = require('../context-expander.js');
24
+ const { discoverSkillEntries } = require('../skill-discovery.js');
24
25
 
25
26
  class CursorEmitter extends BaseEmitter {
26
27
  get name() {
@@ -58,40 +59,19 @@ class CursorEmitter extends BaseEmitter {
58
59
  return results;
59
60
  }
60
61
 
61
- // 扫描技能分组目录
62
- const groupDirs = await fs.promises.readdir(sourceDir, { withFileTypes: true });
63
-
64
- for (const groupEntry of groupDirs) {
65
- if (!groupEntry.isDirectory() || groupEntry.name.startsWith('_')) {
66
- continue;
67
- }
68
-
69
- const groupDir = path.join(sourceDir, groupEntry.name);
70
- const skillEntries = await fs.promises.readdir(groupDir, { withFileTypes: true });
71
-
72
- for (const skillEntry of skillEntries) {
73
- if (!skillEntry.isDirectory() || skillEntry.name.startsWith('_')) {
74
- continue;
75
- }
76
-
77
- const skillDir = path.join(groupDir, skillEntry.name);
78
- const skillMdPath = path.join(skillDir, 'SKILL.md');
79
-
80
- if (!fs.existsSync(skillMdPath)) {
81
- continue;
82
- }
83
-
84
- try {
85
- const result = await this._emitSkillAsRule(
86
- skillEntry.name,
87
- skillDir,
88
- skillMdPath,
89
- targetDir
90
- );
91
- results.push(result);
92
- } catch (error) {
93
- console.warn(`Warning: Failed to emit skill ${skillEntry.name}: ${error.message}`);
94
- }
62
+ const skills = discoverSkillEntries(sourceDir);
63
+
64
+ for (const skill of skills) {
65
+ try {
66
+ const result = await this._emitSkillAsRule(
67
+ skill.name,
68
+ skill.skillDir,
69
+ skill.skillMdPath,
70
+ targetDir
71
+ );
72
+ results.push(result);
73
+ } catch (error) {
74
+ console.warn(`Warning: Failed to emit skill ${skill.name}: ${error.message}`);
95
75
  }
96
76
  }
97
77
 
@@ -20,6 +20,7 @@ const toml = require('@iarna/toml');
20
20
  const matter = require('gray-matter');
21
21
  const BaseEmitter = require('./base-emitter.js');
22
22
  const { ContextExpander } = require('../context-expander.js');
23
+ const { discoverSkillEntries } = require('../skill-discovery.js');
23
24
 
24
25
  class QwenEmitter extends BaseEmitter {
25
26
  get name() {
@@ -63,40 +64,19 @@ class QwenEmitter extends BaseEmitter {
63
64
  return results;
64
65
  }
65
66
 
66
- // 扫描技能分组目录
67
- const groupDirs = await fs.promises.readdir(sourceDir, { withFileTypes: true });
68
-
69
- for (const groupEntry of groupDirs) {
70
- if (!groupEntry.isDirectory() || groupEntry.name.startsWith('_')) {
71
- continue;
72
- }
73
-
74
- const groupDir = path.join(sourceDir, groupEntry.name);
75
- const skillEntries = await fs.promises.readdir(groupDir, { withFileTypes: true });
76
-
77
- for (const skillEntry of skillEntries) {
78
- if (!skillEntry.isDirectory() || skillEntry.name.startsWith('_')) {
79
- continue;
80
- }
81
-
82
- const skillDir = path.join(groupDir, skillEntry.name);
83
- const skillMdPath = path.join(skillDir, 'SKILL.md');
84
-
85
- if (!fs.existsSync(skillMdPath)) {
86
- continue;
87
- }
88
-
89
- try {
90
- const result = await this._emitSkillAsToml(
91
- skillEntry.name,
92
- skillDir,
93
- skillMdPath,
94
- targetDir
95
- );
96
- results.push(result);
97
- } catch (error) {
98
- console.warn(`Warning: Failed to emit skill ${skillEntry.name}: ${error.message}`);
99
- }
67
+ const skills = discoverSkillEntries(sourceDir);
68
+
69
+ for (const skill of skills) {
70
+ try {
71
+ const result = await this._emitSkillAsToml(
72
+ skill.name,
73
+ skill.skillDir,
74
+ skill.skillMdPath,
75
+ targetDir
76
+ );
77
+ results.push(result);
78
+ } catch (error) {
79
+ console.warn(`Warning: Failed to emit skill ${skill.name}: ${error.message}`);
100
80
  }
101
81
  }
102
82
 
@@ -230,6 +230,110 @@ function getRulesTargetPath(platform, config) {
230
230
  return path.join(config.folder, rulesConfig.dir || 'rules');
231
231
  }
232
232
 
233
+ function getHooksTargetDir(config) {
234
+ return config.folder;
235
+ }
236
+
237
+ // ============================================================
238
+ // emitPlatformModules - 编译 Skills/Agents/Rules/Hooks 模块
239
+ // ============================================================
240
+ async function emitPlatformModules(sourceBaseDir, platforms, options = {}) {
241
+ const {
242
+ includeSkills = true,
243
+ includeAgents = true,
244
+ includeRules = true,
245
+ includeHooks = true,
246
+ verbose = false
247
+ } = options;
248
+
249
+ const summary = {
250
+ skillsEmitted: 0,
251
+ agentsEmitted: 0,
252
+ moduleRulesEmitted: 0,
253
+ hooksEmitted: 0,
254
+ errors: []
255
+ };
256
+
257
+ for (const platform of platforms) {
258
+ const emitter = getEmitter(platform);
259
+ const platformConfig = getPlatformConfig(platform);
260
+
261
+ if (includeSkills) {
262
+ try {
263
+ const skillsSourceDir = path.join(sourceBaseDir, 'skills');
264
+ const skillsTargetDir = getSkillsTargetDir(platform, platformConfig);
265
+
266
+ if (fs.existsSync(skillsSourceDir)) {
267
+ const skillResults = await emitter.emitSkills(skillsSourceDir, skillsTargetDir);
268
+ summary.skillsEmitted += skillResults.length;
269
+
270
+ if (verbose && skillResults.length > 0) {
271
+ console.log(`Emitted skills: ${platform} (${skillResults.length})`);
272
+ }
273
+ }
274
+ } catch (error) {
275
+ summary.errors.push(`${platform}/skills: ${error.message}`);
276
+ }
277
+ }
278
+
279
+ if (includeAgents) {
280
+ try {
281
+ const agentsSourceDir = path.join(sourceBaseDir, 'agents');
282
+ const agentsTargetPath = getAgentsTargetPath(platform, platformConfig);
283
+
284
+ if (fs.existsSync(agentsSourceDir)) {
285
+ const agentResults = await emitter.emitAgents(agentsSourceDir, agentsTargetPath);
286
+ summary.agentsEmitted += agentResults.length;
287
+
288
+ if (verbose && agentResults.length > 0) {
289
+ console.log(`Emitted agents: ${platform} (${agentResults.length})`);
290
+ }
291
+ }
292
+ } catch (error) {
293
+ summary.errors.push(`${platform}/agents: ${error.message}`);
294
+ }
295
+ }
296
+
297
+ if (includeRules) {
298
+ try {
299
+ const rulesSourceDir = path.join(sourceBaseDir, 'rules');
300
+ const rulesTargetPath = getRulesTargetPath(platform, platformConfig);
301
+
302
+ if (fs.existsSync(rulesSourceDir)) {
303
+ const ruleResults = await emitter.emitRules(rulesSourceDir, rulesTargetPath);
304
+ summary.moduleRulesEmitted += ruleResults.length;
305
+
306
+ if (verbose && ruleResults.length > 0) {
307
+ console.log(`Emitted module rules: ${platform} (${ruleResults.length})`);
308
+ }
309
+ }
310
+ } catch (error) {
311
+ summary.errors.push(`${platform}/rules: ${error.message}`);
312
+ }
313
+ }
314
+
315
+ if (includeHooks && isModuleSupported(platform, 'hooks')) {
316
+ try {
317
+ const hooksSourceDir = path.join(sourceBaseDir, 'hooks');
318
+ const hooksTargetDir = getHooksTargetDir(platformConfig);
319
+
320
+ if (fs.existsSync(hooksSourceDir)) {
321
+ const hookResults = await emitter.emitHooks(hooksSourceDir, hooksTargetDir);
322
+ summary.hooksEmitted += hookResults.length;
323
+
324
+ if (verbose && hookResults.length > 0) {
325
+ console.log(`Emitted hooks: ${platform} (${hookResults.length})`);
326
+ }
327
+ }
328
+ } catch (error) {
329
+ summary.errors.push(`${platform}/hooks: ${error.message}`);
330
+ }
331
+ }
332
+ }
333
+
334
+ return summary;
335
+ }
336
+
233
337
  // ============================================================
234
338
  // compile - 编译主函数
235
339
  // ============================================================
@@ -259,6 +363,10 @@ async function compile(options = {}) {
259
363
  filesSkipped: 0,
260
364
  resourcesCopied: 0,
261
365
  resourcesSkipped: 0,
366
+ skillsEmitted: 0,
367
+ agentsEmitted: 0,
368
+ moduleRulesEmitted: 0,
369
+ hooksEmitted: 0,
262
370
  rulesGenerated: 0,
263
371
  skillsRegistered: 0,
264
372
  errors: []
@@ -307,6 +415,21 @@ async function compile(options = {}) {
307
415
  }
308
416
  }
309
417
 
418
+ // 编译多模块产物(skills / agents / rules / hooks)
419
+ const sourceBaseDir = path.resolve(path.dirname(sourceDir));
420
+ const moduleSummary = await emitPlatformModules(sourceBaseDir, platforms, {
421
+ includeSkills: skills,
422
+ includeAgents: true,
423
+ includeRules: rules,
424
+ includeHooks: true,
425
+ verbose
426
+ });
427
+ result.skillsEmitted = moduleSummary.skillsEmitted;
428
+ result.agentsEmitted = moduleSummary.agentsEmitted;
429
+ result.moduleRulesEmitted = moduleSummary.moduleRulesEmitted;
430
+ result.hooksEmitted = moduleSummary.hooksEmitted;
431
+ result.errors.push(...moduleSummary.errors);
432
+
310
433
  // 解析所有命令文件
311
434
  const absoluteSourceDir = path.resolve(sourceDir);
312
435
  let irs = [];
@@ -440,6 +563,7 @@ async function compile(options = {}) {
440
563
  manifest.generatedAt = new Date().toISOString();
441
564
  await saveManifest(manifest, manifestPath);
442
565
 
566
+ result.success = result.errors.length === 0;
443
567
  return result;
444
568
  }
445
569
 
@@ -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.1",
3
+ "version": "4.1.3",
4
4
  "description": "DevFlow CLI tool",
5
5
  "main": "bin/cc-devflow.js",
6
6
  "bin": {