cc-devflow 4.1.1 → 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.
- package/.claude/commands/util/git-commit.md +1 -4
- package/CHANGELOG.md +41 -0
- package/bin/adapt.js +4 -0
- package/lib/compiler/CLAUDE.md +5 -3
- package/lib/compiler/__tests__/compile-regression.test.js +94 -0
- package/lib/compiler/__tests__/parser.test.js +46 -0
- package/lib/compiler/__tests__/resource-copier.test.js +26 -0
- package/lib/compiler/__tests__/skill-discovery.test.js +72 -0
- package/lib/compiler/emitters/antigravity-emitter.js +35 -36
- package/lib/compiler/emitters/base-emitter.js +3 -1
- package/lib/compiler/emitters/codex-emitter.js +34 -35
- package/lib/compiler/emitters/cursor-emitter.js +14 -34
- package/lib/compiler/emitters/qwen-emitter.js +14 -34
- package/lib/compiler/index.js +124 -0
- package/lib/compiler/parser.js +43 -8
- package/lib/compiler/resource-copier.js +6 -0
- package/lib/compiler/skill-discovery.js +68 -0
- package/lib/compiler/skills-registry.js +8 -41
- package/package.json +1 -1
|
@@ -4,9 +4,6 @@ description: Git Commit 工作流最佳实践 - Conventional Commits 规范、
|
|
|
4
4
|
version: 3.0.0
|
|
5
5
|
author: CC-DevFlow
|
|
6
6
|
updated_at: 2026-01-04
|
|
7
|
-
scripts:
|
|
8
|
-
review: .claude/scripts/git-commit-review.sh
|
|
9
|
-
pr_check: .claude/scripts/check-remote.sh
|
|
10
7
|
---
|
|
11
8
|
|
|
12
9
|
> **哲学核心**:Commit 是代码历史的节点,每个节点都应该清晰、独立、可追溯。
|
|
@@ -422,4 +419,4 @@ git show <commit-hash>
|
|
|
422
419
|
|
|
423
420
|
---
|
|
424
421
|
|
|
425
|
-
**记住**:好的 commit 历史是项目最好的文档。代码是写给人看的,只是顺便让机器运行。
|
|
422
|
+
**记住**:好的 commit 历史是项目最好的文档。代码是写给人看的,只是顺便让机器运行。
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [4.1.2] - 2026-02-07
|
|
11
|
+
|
|
12
|
+
### 🔧 Adapt Compiler Migration Reliability Fixes
|
|
13
|
+
|
|
14
|
+
v4.1.2 fixes regressions in multi-platform `adapt` output generation and adds regression coverage to prevent command/skill migration gaps.
|
|
15
|
+
|
|
16
|
+
#### Fixed
|
|
17
|
+
|
|
18
|
+
- **Nested command compilation restored**
|
|
19
|
+
- `adapt` now recursively scans `.claude/commands/**` instead of top-level only
|
|
20
|
+
- Preserves relative command paths (for example `flow/new` → platform subdirectories)
|
|
21
|
+
- Ensures command outputs are emitted to matching platform folder structures
|
|
22
|
+
|
|
23
|
+
- **Skills migration completeness restored**
|
|
24
|
+
- Added recursive Skill discovery for grouped and root-level skills
|
|
25
|
+
- Registry and platform emitters now discover the same full skill set
|
|
26
|
+
- Resolved missing Codex/Cursor/Qwen/Antigravity skill outputs
|
|
27
|
+
|
|
28
|
+
- **Module emission restored in main compile path**
|
|
29
|
+
- `adapt` main flow now emits skills/agents/rules/hooks in addition to commands
|
|
30
|
+
- CLI output now reports module-level emission statistics for easier verification
|
|
31
|
+
|
|
32
|
+
- **Resource copy reliability improved**
|
|
33
|
+
- Handles Skill resource symlinks more robustly during emitter copy
|
|
34
|
+
- Ignores glob-style inline paths to avoid false missing-file warnings
|
|
35
|
+
- Fixed broken workflow skill symlink targets under `.claude/skills/workflow/*`
|
|
36
|
+
|
|
37
|
+
#### Added
|
|
38
|
+
|
|
39
|
+
- **Regression tests for adapt migration pipeline**
|
|
40
|
+
- Parser recursive command discovery tests
|
|
41
|
+
- Skill discovery tests (grouped + root-level)
|
|
42
|
+
- Compile end-to-end regression test
|
|
43
|
+
- Resource copier glob-filter test
|
|
44
|
+
|
|
45
|
+
#### Benefits
|
|
46
|
+
|
|
47
|
+
- ✅ `cc-devflow adapt --platform <target>` now generates complete expected outputs
|
|
48
|
+
- ✅ Cross-platform command/skill migration behavior is consistent and test-protected
|
|
49
|
+
- ✅ Release/debug verification is faster with explicit module emission metrics
|
|
50
|
+
|
|
10
51
|
## [4.1.1] - 2026-02-07
|
|
11
52
|
|
|
12
53
|
### 🔧 Flow-Init Script Reliability + Flow Docs Alignment
|
package/bin/adapt.js
CHANGED
|
@@ -215,6 +215,10 @@ async function main() {
|
|
|
215
215
|
console.log(` Files skipped: ${result.filesSkipped}`);
|
|
216
216
|
console.log(` Resources copied: ${result.resourcesCopied}`);
|
|
217
217
|
console.log(` Resources skipped: ${result.resourcesSkipped}`);
|
|
218
|
+
console.log(` Skills emitted: ${result.skillsEmitted}`);
|
|
219
|
+
console.log(` Agents emitted: ${result.agentsEmitted}`);
|
|
220
|
+
console.log(` Module rules emitted: ${result.moduleRulesEmitted}`);
|
|
221
|
+
console.log(` Hooks emitted: ${result.hooksEmitted}`);
|
|
218
222
|
console.log(` Skills registered: ${result.skillsRegistered}`);
|
|
219
223
|
console.log(` Rules generated: ${result.rulesGenerated}`);
|
|
220
224
|
process.exit(0);
|
package/lib/compiler/CLAUDE.md
CHANGED
|
@@ -13,6 +13,8 @@ lib/compiler/
|
|
|
13
13
|
├── schemas.js # Zod validation schemas (CommandIR, Manifest)
|
|
14
14
|
├── errors.js # Custom error types (MissingFrontmatter, UnknownAlias, etc.)
|
|
15
15
|
├── skills-registry.js # Generate skills registry from .claude/skills/
|
|
16
|
+
├── skill-discovery.js # Recursive SKILL.md discovery (grouped + root skills)
|
|
17
|
+
├── resource-copier.js # Resource collection/copy and .claude path rewriting
|
|
16
18
|
├── platforms.js # Platform configuration registry (v2.0)
|
|
17
19
|
├── context-expander.js # context.jsonl expansion (v3.0)
|
|
18
20
|
├── index.js # Compiler entry point, orchestrates pipeline
|
|
@@ -69,7 +71,7 @@ Platform-specific output directories
|
|
|
69
71
|
## CLI Usage
|
|
70
72
|
|
|
71
73
|
```bash
|
|
72
|
-
#
|
|
74
|
+
# Compile commands + modules
|
|
73
75
|
npm run adapt # Compile all platforms
|
|
74
76
|
npm run adapt -- --platform codex # Compile single platform
|
|
75
77
|
npm run adapt -- --check # Drift detection only
|
|
@@ -81,8 +83,8 @@ npm run adapt -- --modules skills --platform cursor
|
|
|
81
83
|
|
|
82
84
|
## Key APIs
|
|
83
85
|
|
|
84
|
-
### compile(options) -
|
|
85
|
-
Compiles commands
|
|
86
|
+
### compile(options) - Main Flow
|
|
87
|
+
Compiles commands and emits platform modules (skills, agents, rules, hooks).
|
|
86
88
|
|
|
87
89
|
### compileMultiModule(options) - v3.0
|
|
88
90
|
Compiles all specified modules.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T043: Compile Regression Tests
|
|
3
|
+
*
|
|
4
|
+
* [INPUT]: 临时 .claude 项目
|
|
5
|
+
* [OUTPUT]: compile() 端到端回归验证
|
|
6
|
+
* [POS]: 修复 adapt 回归,确保命令与技能正确落盘
|
|
7
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
const { compile } = require('../index.js');
|
|
15
|
+
|
|
16
|
+
function writeFile(filePath, content) {
|
|
17
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
18
|
+
fs.writeFileSync(filePath, content);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function setupProjectFixture(rootDir) {
|
|
22
|
+
writeFile(path.join(rootDir, '.claude/commands/flow/new.md'), `---
|
|
23
|
+
name: flow-new
|
|
24
|
+
description: Create new requirement
|
|
25
|
+
---
|
|
26
|
+
# Flow New
|
|
27
|
+
|
|
28
|
+
Run flow new
|
|
29
|
+
`);
|
|
30
|
+
|
|
31
|
+
writeFile(path.join(rootDir, '.claude/skills/workflow/flow-dev/SKILL.md'), `---
|
|
32
|
+
name: flow-dev
|
|
33
|
+
description: Workflow dev skill
|
|
34
|
+
---
|
|
35
|
+
# flow-dev
|
|
36
|
+
`);
|
|
37
|
+
|
|
38
|
+
writeFile(path.join(rootDir, '.claude/skills/orchestrator/SKILL.md'), `---
|
|
39
|
+
name: orchestrator
|
|
40
|
+
description: Root level skill
|
|
41
|
+
---
|
|
42
|
+
# orchestrator
|
|
43
|
+
`);
|
|
44
|
+
|
|
45
|
+
writeFile(path.join(rootDir, '.claude/agents/planner.md'), `---
|
|
46
|
+
name: planner
|
|
47
|
+
description: Plan tasks
|
|
48
|
+
---
|
|
49
|
+
# planner
|
|
50
|
+
`);
|
|
51
|
+
|
|
52
|
+
writeFile(path.join(rootDir, '.claude/rules/base-rule.md'), `---
|
|
53
|
+
description: Base rule
|
|
54
|
+
alwaysApply: true
|
|
55
|
+
---
|
|
56
|
+
# rule
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('compile() regression', () => {
|
|
61
|
+
let tempDir;
|
|
62
|
+
let originalCwd;
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'compile-regression-'));
|
|
66
|
+
setupProjectFixture(tempDir);
|
|
67
|
+
originalCwd = process.cwd();
|
|
68
|
+
process.chdir(tempDir);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
process.chdir(originalCwd);
|
|
73
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should compile nested commands and emit skills for codex', async () => {
|
|
77
|
+
const result = await compile({
|
|
78
|
+
sourceDir: '.claude/commands',
|
|
79
|
+
skillsDir: '.claude/skills',
|
|
80
|
+
outputBaseDir: '.',
|
|
81
|
+
platforms: ['codex'],
|
|
82
|
+
verbose: false
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result.success).toBe(true);
|
|
86
|
+
expect(result.filesCompiled).toBe(1);
|
|
87
|
+
expect(result.skillsRegistered).toBe(2);
|
|
88
|
+
expect(result.skillsEmitted).toBe(2);
|
|
89
|
+
expect(fs.existsSync('.codex/prompts/flow/new.md')).toBe(true);
|
|
90
|
+
expect(fs.existsSync('.codex/skills/flow-dev/SKILL.md')).toBe(true);
|
|
91
|
+
expect(fs.existsSync('.codex/skills/orchestrator/SKILL.md')).toBe(true);
|
|
92
|
+
expect(fs.existsSync('AGENTS.md')).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -452,5 +452,51 @@ Body`);
|
|
|
452
452
|
fs.rmdirSync(tmpDir);
|
|
453
453
|
}
|
|
454
454
|
});
|
|
455
|
+
|
|
456
|
+
it('should parse nested command files recursively', async () => {
|
|
457
|
+
const tmpDir = path.join(os.tmpdir(), `test-nested-${Date.now()}`);
|
|
458
|
+
const flowDir = path.join(tmpDir, 'flow');
|
|
459
|
+
const utilDir = path.join(tmpDir, 'util');
|
|
460
|
+
fs.mkdirSync(flowDir, { recursive: true });
|
|
461
|
+
fs.mkdirSync(utilDir, { recursive: true });
|
|
462
|
+
|
|
463
|
+
fs.writeFileSync(path.join(flowDir, 'new.md'), `---
|
|
464
|
+
name: flow-new
|
|
465
|
+
description: Flow new command
|
|
466
|
+
---
|
|
467
|
+
Body`);
|
|
468
|
+
fs.writeFileSync(path.join(utilDir, 'code-review.md'), `---
|
|
469
|
+
name: code-review
|
|
470
|
+
description: Code review command
|
|
471
|
+
---
|
|
472
|
+
Body`);
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
const result = await parser.parseAllCommands(tmpDir);
|
|
476
|
+
expect(result).toHaveLength(2);
|
|
477
|
+
} finally {
|
|
478
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should preserve relative subpath in source filename', async () => {
|
|
483
|
+
const tmpDir = path.join(os.tmpdir(), `test-relpath-${Date.now()}`);
|
|
484
|
+
const flowDir = path.join(tmpDir, 'flow');
|
|
485
|
+
fs.mkdirSync(flowDir, { recursive: true });
|
|
486
|
+
|
|
487
|
+
fs.writeFileSync(path.join(flowDir, 'spec.md'), `---
|
|
488
|
+
name: flow-spec
|
|
489
|
+
description: Flow spec command
|
|
490
|
+
---
|
|
491
|
+
Body`);
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
const result = await parser.parseAllCommands(tmpDir);
|
|
495
|
+
expect(result).toHaveLength(1);
|
|
496
|
+
expect(result[0].source.filename).toBe('flow/spec');
|
|
497
|
+
} finally {
|
|
498
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
499
|
+
}
|
|
500
|
+
});
|
|
455
501
|
});
|
|
456
502
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T044: Resource Copier Tests
|
|
3
|
+
*
|
|
4
|
+
* [INPUT]: 内联路径字符串
|
|
5
|
+
* [OUTPUT]: scanInlineClaudePaths 扫描结果
|
|
6
|
+
* [POS]: 资源扫描单元测试,防止 glob 误判为文件
|
|
7
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { scanInlineClaudePaths } = require('../resource-copier.js');
|
|
11
|
+
|
|
12
|
+
describe('resource-copier', () => {
|
|
13
|
+
test('scanInlineClaudePaths should skip glob patterns', () => {
|
|
14
|
+
const content = 'Templates: .claude/docs/templates/context/*.jsonl.template';
|
|
15
|
+
const paths = scanInlineClaudePaths(content);
|
|
16
|
+
|
|
17
|
+
expect(paths).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('scanInlineClaudePaths should keep concrete file paths', () => {
|
|
21
|
+
const content = 'Use .claude/scripts/check-prerequisites.sh before execution.';
|
|
22
|
+
const paths = scanInlineClaudePaths(content);
|
|
23
|
+
|
|
24
|
+
expect(paths).toContain('.claude/scripts/check-prerequisites.sh');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T042: Skill Discovery Tests
|
|
3
|
+
*
|
|
4
|
+
* [INPUT]: 临时技能目录结构
|
|
5
|
+
* [OUTPUT]: 递归扫描结果断言
|
|
6
|
+
* [POS]: skill-discovery 单元测试,验证分组/非分组技能发现
|
|
7
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
const { discoverSkillEntries } = require('../skill-discovery.js');
|
|
15
|
+
const { generateSkillsRegistryV2 } = require('../skills-registry.js');
|
|
16
|
+
|
|
17
|
+
function setupSkillsFixture(rootDir) {
|
|
18
|
+
// 非分组技能
|
|
19
|
+
const rootSkillDir = path.join(rootDir, 'orchestrator');
|
|
20
|
+
fs.mkdirSync(rootSkillDir, { recursive: true });
|
|
21
|
+
fs.writeFileSync(path.join(rootSkillDir, 'SKILL.md'), `---
|
|
22
|
+
name: orchestrator
|
|
23
|
+
description: Root level skill
|
|
24
|
+
---`);
|
|
25
|
+
|
|
26
|
+
// 分组技能
|
|
27
|
+
const groupedSkillDir = path.join(rootDir, 'workflow', 'flow-dev');
|
|
28
|
+
fs.mkdirSync(groupedSkillDir, { recursive: true });
|
|
29
|
+
fs.writeFileSync(path.join(groupedSkillDir, 'SKILL.md'), `---
|
|
30
|
+
name: flow-dev
|
|
31
|
+
description: Grouped workflow skill
|
|
32
|
+
---`);
|
|
33
|
+
|
|
34
|
+
// 需要跳过的目录
|
|
35
|
+
const ignoredSkillDir = path.join(rootDir, '_reference', 'demo-skill');
|
|
36
|
+
fs.mkdirSync(ignoredSkillDir, { recursive: true });
|
|
37
|
+
fs.writeFileSync(path.join(ignoredSkillDir, 'SKILL.md'), `---
|
|
38
|
+
name: demo-skill
|
|
39
|
+
description: Should be ignored
|
|
40
|
+
---`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('skill-discovery', () => {
|
|
44
|
+
let tempDir;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-discovery-'));
|
|
48
|
+
setupSkillsFixture(tempDir);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('discoverSkillEntries should include grouped and root skills', () => {
|
|
56
|
+
const entries = discoverSkillEntries(tempDir);
|
|
57
|
+
const names = entries.map(entry => entry.name);
|
|
58
|
+
|
|
59
|
+
expect(names).toContain('orchestrator');
|
|
60
|
+
expect(names).toContain('flow-dev');
|
|
61
|
+
expect(names).not.toContain('demo-skill');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('generateSkillsRegistryV2 should register grouped and root skills', async () => {
|
|
65
|
+
const registry = await generateSkillsRegistryV2(tempDir);
|
|
66
|
+
const names = registry.skills.map(skill => skill.name);
|
|
67
|
+
|
|
68
|
+
expect(names).toContain('orchestrator');
|
|
69
|
+
expect(names).toContain('flow-dev');
|
|
70
|
+
expect(names).not.toContain('demo-skill');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -22,6 +22,7 @@ const yaml = require('js-yaml');
|
|
|
22
22
|
const matter = require('gray-matter');
|
|
23
23
|
const BaseEmitter = require('./base-emitter.js');
|
|
24
24
|
const { ContextExpander } = require('../context-expander.js');
|
|
25
|
+
const { discoverSkillEntries } = require('../skill-discovery.js');
|
|
25
26
|
|
|
26
27
|
const CONTENT_LIMIT = 12000;
|
|
27
28
|
const DESCRIPTION_LIMIT = 250;
|
|
@@ -196,40 +197,19 @@ class AntigravityEmitter extends BaseEmitter {
|
|
|
196
197
|
return results;
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const skillDir = path.join(groupDir, skillEntry.name);
|
|
216
|
-
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
217
|
-
|
|
218
|
-
if (!fs.existsSync(skillMdPath)) {
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
const result = await this._emitSingleSkill(
|
|
224
|
-
skillEntry.name,
|
|
225
|
-
skillDir,
|
|
226
|
-
skillMdPath,
|
|
227
|
-
targetDir
|
|
228
|
-
);
|
|
229
|
-
results.push(result);
|
|
230
|
-
} catch (error) {
|
|
231
|
-
console.warn(`Warning: Failed to emit skill ${skillEntry.name}: ${error.message}`);
|
|
232
|
-
}
|
|
200
|
+
const skills = discoverSkillEntries(sourceDir);
|
|
201
|
+
|
|
202
|
+
for (const skill of skills) {
|
|
203
|
+
try {
|
|
204
|
+
const result = await this._emitSingleSkill(
|
|
205
|
+
skill.name,
|
|
206
|
+
skill.skillDir,
|
|
207
|
+
skill.skillMdPath,
|
|
208
|
+
targetDir
|
|
209
|
+
);
|
|
210
|
+
results.push(result);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.warn(`Warning: Failed to emit skill ${skill.name}: ${error.message}`);
|
|
233
213
|
}
|
|
234
214
|
}
|
|
235
215
|
|
|
@@ -278,7 +258,12 @@ class AntigravityEmitter extends BaseEmitter {
|
|
|
278
258
|
const result = await this.emitToPath(targetPath, finalContent);
|
|
279
259
|
|
|
280
260
|
// 复制 scripts/ 和 references/ 目录
|
|
281
|
-
|
|
261
|
+
try {
|
|
262
|
+
await this._copySkillResources(skillDir, targetSkillDir);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
// 资源复制失败不阻塞主 SKILL.md 产物
|
|
265
|
+
console.warn(`Warning: Failed to copy resources for skill ${skillName}: ${error.message}`);
|
|
266
|
+
}
|
|
282
267
|
|
|
283
268
|
return { ...result, skillName };
|
|
284
269
|
}
|
|
@@ -287,7 +272,7 @@ class AntigravityEmitter extends BaseEmitter {
|
|
|
287
272
|
* 复制 Skill 资源目录
|
|
288
273
|
*/
|
|
289
274
|
async _copySkillResources(sourceSkillDir, targetSkillDir) {
|
|
290
|
-
const resourceDirs = ['scripts', 'references', 'examples', 'resources'];
|
|
275
|
+
const resourceDirs = ['scripts', 'references', 'assets', 'examples', 'resources'];
|
|
291
276
|
|
|
292
277
|
for (const dir of resourceDirs) {
|
|
293
278
|
const sourceResDir = path.join(sourceSkillDir, dir);
|
|
@@ -312,6 +297,20 @@ class AntigravityEmitter extends BaseEmitter {
|
|
|
312
297
|
|
|
313
298
|
if (entry.isDirectory()) {
|
|
314
299
|
await this._copyDir(srcPath, destPath);
|
|
300
|
+
} else if (entry.isSymbolicLink()) {
|
|
301
|
+
try {
|
|
302
|
+
const resolvedPath = await fs.promises.realpath(srcPath);
|
|
303
|
+
const resolvedStats = await fs.promises.stat(resolvedPath);
|
|
304
|
+
await fs.promises.rm(destPath, { force: true, recursive: true });
|
|
305
|
+
|
|
306
|
+
if (resolvedStats.isDirectory()) {
|
|
307
|
+
await this._copyDir(resolvedPath, destPath);
|
|
308
|
+
} else {
|
|
309
|
+
await fs.promises.copyFile(resolvedPath, destPath);
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.warn(`Warning: Skip broken symlink ${srcPath}: ${error.message}`);
|
|
313
|
+
}
|
|
315
314
|
} else {
|
|
316
315
|
await fs.promises.copyFile(srcPath, destPath);
|
|
317
316
|
}
|
|
@@ -70,9 +70,11 @@ class BaseEmitter {
|
|
|
70
70
|
const outputDir = this.outputDir;
|
|
71
71
|
const ext = this.fileExtension;
|
|
72
72
|
const filePath = path.join(outputDir, `${filename}${ext}`);
|
|
73
|
+
const fileDir = path.dirname(filePath);
|
|
73
74
|
|
|
74
75
|
// ✅ SECURITY FIX (FINDING-005): Set explicit directory permissions
|
|
75
|
-
|
|
76
|
+
// 支持带子路径的文件名(例如 flow/new)
|
|
77
|
+
await fs.promises.mkdir(fileDir, { recursive: true, mode: 0o755 });
|
|
76
78
|
|
|
77
79
|
// ✅ SECURITY FIX (FINDING-005): Set explicit file permissions
|
|
78
80
|
await fs.promises.writeFile(filePath, content, { encoding: 'utf8', mode: 0o644 });
|
|
@@ -20,6 +20,7 @@ const yaml = require('js-yaml');
|
|
|
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 CodexEmitter extends BaseEmitter {
|
|
25
26
|
get name() {
|
|
@@ -76,40 +77,19 @@ class CodexEmitter extends BaseEmitter {
|
|
|
76
77
|
return results;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const skillDir = path.join(groupDir, skillEntry.name);
|
|
96
|
-
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
97
|
-
|
|
98
|
-
if (!fs.existsSync(skillMdPath)) {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
const result = await this._emitSingleSkill(
|
|
104
|
-
skillEntry.name,
|
|
105
|
-
skillDir,
|
|
106
|
-
skillMdPath,
|
|
107
|
-
targetDir
|
|
108
|
-
);
|
|
109
|
-
results.push(result);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
console.warn(`Warning: Failed to emit skill ${skillEntry.name}: ${error.message}`);
|
|
112
|
-
}
|
|
80
|
+
const skills = discoverSkillEntries(sourceDir);
|
|
81
|
+
|
|
82
|
+
for (const skill of skills) {
|
|
83
|
+
try {
|
|
84
|
+
const result = await this._emitSingleSkill(
|
|
85
|
+
skill.name,
|
|
86
|
+
skill.skillDir,
|
|
87
|
+
skill.skillMdPath,
|
|
88
|
+
targetDir
|
|
89
|
+
);
|
|
90
|
+
results.push(result);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn(`Warning: Failed to emit skill ${skill.name}: ${error.message}`);
|
|
113
93
|
}
|
|
114
94
|
}
|
|
115
95
|
|
|
@@ -153,7 +133,12 @@ class CodexEmitter extends BaseEmitter {
|
|
|
153
133
|
const result = await this.emitToPath(targetPath, finalContent);
|
|
154
134
|
|
|
155
135
|
// 复制 scripts/ 和 references/ 目录
|
|
156
|
-
|
|
136
|
+
try {
|
|
137
|
+
await this._copySkillResources(skillDir, targetSkillDir);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// 资源复制失败不阻塞主 SKILL.md 产物
|
|
140
|
+
console.warn(`Warning: Failed to copy resources for skill ${skillName}: ${error.message}`);
|
|
141
|
+
}
|
|
157
142
|
|
|
158
143
|
return { ...result, skillName };
|
|
159
144
|
}
|
|
@@ -187,6 +172,20 @@ class CodexEmitter extends BaseEmitter {
|
|
|
187
172
|
|
|
188
173
|
if (entry.isDirectory()) {
|
|
189
174
|
await this._copyDir(srcPath, destPath);
|
|
175
|
+
} else if (entry.isSymbolicLink()) {
|
|
176
|
+
try {
|
|
177
|
+
const resolvedPath = await fs.promises.realpath(srcPath);
|
|
178
|
+
const resolvedStats = await fs.promises.stat(resolvedPath);
|
|
179
|
+
await fs.promises.rm(destPath, { force: true, recursive: true });
|
|
180
|
+
|
|
181
|
+
if (resolvedStats.isDirectory()) {
|
|
182
|
+
await this._copyDir(resolvedPath, destPath);
|
|
183
|
+
} else {
|
|
184
|
+
await fs.promises.copyFile(resolvedPath, destPath);
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.warn(`Warning: Skip broken symlink ${srcPath}: ${error.message}`);
|
|
188
|
+
}
|
|
190
189
|
} else {
|
|
191
190
|
await fs.promises.copyFile(srcPath, destPath);
|
|
192
191
|
}
|
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
package/lib/compiler/index.js
CHANGED
|
@@ -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
|
|
package/lib/compiler/parser.js
CHANGED
|
@@ -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
|
-
//
|
|
232
|
-
const mdFiles = files.filter(file => file.endsWith('.md'));
|
|
233
|
-
|
|
234
|
-
// 解析每个文件
|
|
230
|
+
// 解析每个文件(保留相对路径,支持分层输出)
|
|
235
231
|
const results = [];
|
|
236
|
-
for (const
|
|
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
|
|
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
|
-
|
|
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
|
}
|