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.
- package/.claude/commands/flow/dev.md +1 -1
- package/.claude/commands/flow/quality.md +5 -0
- package/.claude/commands/flow/release.md +3 -3
- package/.claude/commands/flow/restart.md +1 -1
- package/.claude/commands/flow/status.md +2 -2
- package/.claude/commands/flow/update.md +1 -1
- package/.claude/commands/util/git-commit.md +1 -4
- package/.claude/scripts/flow-quality-full.sh +32 -1
- package/.claude/scripts/flow-quality-quick.sh +57 -2
- package/.claude/scripts/generate-status-report.sh +5 -5
- package/.claude/scripts/recover-workflow.sh +24 -24
- package/.claude/skills/workflow/flow-quality/SKILL.md +4 -0
- package/.claude/skills/workflow/flow-release/SKILL.md +1 -1
- package/CHANGELOG.md +70 -0
- package/bin/adapt.js +4 -0
- package/lib/compiler/CLAUDE.md +5 -3
- package/lib/compiler/__tests__/compile-regression.test.js +103 -0
- package/lib/compiler/__tests__/multi-module-emitters.test.js +37 -11
- 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 +57 -39
- package/lib/compiler/emitters/base-emitter.js +81 -1
- package/lib/compiler/emitters/codex-emitter.js +71 -69
- 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
|
@@ -213,9 +213,10 @@ describe('CodexEmitter Multi-Module', () => {
|
|
|
213
213
|
expect(content).toContain('## Required Context');
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
test('emitAgents
|
|
216
|
+
test('emitAgents writes compact managed block and preserves existing content', async () => {
|
|
217
217
|
const sourceDir = path.join(tempDir, '.claude', 'agents');
|
|
218
218
|
const targetPath = path.join(tempDir, 'AGENTS.md');
|
|
219
|
+
fs.writeFileSync(targetPath, '# User Memory\n\nDo not overwrite this.\n');
|
|
219
220
|
|
|
220
221
|
const results = await emitter.emitAgents(sourceDir, targetPath);
|
|
221
222
|
|
|
@@ -223,25 +224,32 @@ describe('CodexEmitter Multi-Module', () => {
|
|
|
223
224
|
expect(fs.existsSync(targetPath)).toBe(true);
|
|
224
225
|
|
|
225
226
|
const content = fs.readFileSync(targetPath, 'utf8');
|
|
226
|
-
expect(content).toContain('#
|
|
227
|
-
expect(content).toContain('
|
|
227
|
+
expect(content).toContain('# User Memory');
|
|
228
|
+
expect(content).toContain('<!-- cc-devflow:codex-agents:start -->');
|
|
229
|
+
expect(content).toContain('## CC-DevFlow Agents');
|
|
230
|
+
expect(content).toContain('- Count: 1');
|
|
231
|
+
expect(content).not.toContain('This agent is for testing.');
|
|
228
232
|
});
|
|
229
233
|
|
|
230
|
-
test('emitRules
|
|
234
|
+
test('emitRules writes compact managed block and remains idempotent', async () => {
|
|
231
235
|
const sourceDir = path.join(tempDir, '.claude', 'rules');
|
|
232
236
|
const targetPath = path.join(tempDir, 'AGENTS.md');
|
|
233
237
|
|
|
234
|
-
|
|
235
|
-
fs.writeFileSync(targetPath, '# Agents\n\nExisting content.\n');
|
|
238
|
+
fs.writeFileSync(targetPath, '# User Memory\n\nKeep me.\n');
|
|
236
239
|
|
|
237
|
-
const
|
|
240
|
+
const first = await emitter.emitRules(sourceDir, targetPath);
|
|
241
|
+
const second = await emitter.emitRules(sourceDir, targetPath);
|
|
238
242
|
|
|
239
|
-
expect(
|
|
243
|
+
expect(first.length).toBe(1);
|
|
244
|
+
expect(second.length).toBe(1);
|
|
240
245
|
|
|
241
246
|
const content = fs.readFileSync(targetPath, 'utf8');
|
|
242
|
-
expect(content).toContain('#
|
|
243
|
-
expect(content).toContain('
|
|
244
|
-
expect(content).toContain('
|
|
247
|
+
expect(content).toContain('# User Memory');
|
|
248
|
+
expect(content).toContain('<!-- cc-devflow:codex-rules:start -->');
|
|
249
|
+
expect(content).toContain('## CC-DevFlow Rules');
|
|
250
|
+
expect(content).toContain('- Count: 1');
|
|
251
|
+
expect(content).not.toContain('This rule is for testing.');
|
|
252
|
+
expect(content.split('<!-- cc-devflow:codex-rules:start -->').length - 1).toBe(1);
|
|
245
253
|
});
|
|
246
254
|
});
|
|
247
255
|
|
|
@@ -428,6 +436,24 @@ describe('AntigravityEmitter Multi-Module', () => {
|
|
|
428
436
|
const targetPath = path.join(targetDir, 'test-rule.md');
|
|
429
437
|
expect(fs.existsSync(targetPath)).toBe(true);
|
|
430
438
|
});
|
|
439
|
+
|
|
440
|
+
test('emitAgents writes compact managed block and preserves existing content', async () => {
|
|
441
|
+
const sourceDir = path.join(tempDir, '.claude', 'agents');
|
|
442
|
+
const targetPath = path.join(tempDir, 'AGENTS.md');
|
|
443
|
+
fs.writeFileSync(targetPath, '# User Memory\n\nKeep this section.\n');
|
|
444
|
+
|
|
445
|
+
const results = await emitter.emitAgents(sourceDir, targetPath);
|
|
446
|
+
|
|
447
|
+
expect(results.length).toBe(1);
|
|
448
|
+
expect(fs.existsSync(targetPath)).toBe(true);
|
|
449
|
+
|
|
450
|
+
const content = fs.readFileSync(targetPath, 'utf8');
|
|
451
|
+
expect(content).toContain('# User Memory');
|
|
452
|
+
expect(content).toContain('<!-- cc-devflow:antigravity-agents:start -->');
|
|
453
|
+
expect(content).toContain('## CC-DevFlow Agents');
|
|
454
|
+
expect(content).toContain('- Count: 1');
|
|
455
|
+
expect(content).not.toContain('This agent is for testing.');
|
|
456
|
+
});
|
|
431
457
|
});
|
|
432
458
|
|
|
433
459
|
// ============================================================
|
|
@@ -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
|
+
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 输出格式:
|
|
10
10
|
* - Commands: Markdown + YAML frontmatter -> .agent/workflows/
|
|
11
11
|
* - Skills: SKILL.md (YAML frontmatter) -> .agent/skills/
|
|
12
|
-
* - Agents:
|
|
12
|
+
* - Agents: 索引摘要写入 AGENTS.md(保留用户内容)
|
|
13
13
|
* - Rules: Markdown -> .agent/rules/
|
|
14
14
|
*
|
|
15
15
|
* 限制: 单文件 <= 12,000 字符,超限时自动拆分
|
|
@@ -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
|
}
|
|
@@ -320,10 +319,29 @@ class AntigravityEmitter extends BaseEmitter {
|
|
|
320
319
|
|
|
321
320
|
/**
|
|
322
321
|
* 编译 Agents 模块
|
|
323
|
-
* .claude/agents/[name].md -> AGENTS.md (
|
|
322
|
+
* .claude/agents/[name].md -> AGENTS.md (短索引,受管块)
|
|
324
323
|
*/
|
|
325
324
|
async emitAgents(sourceDir, targetPath) {
|
|
326
|
-
|
|
325
|
+
const results = [];
|
|
326
|
+
const agentNames = await this.readMarkdownEntryNames(sourceDir);
|
|
327
|
+
|
|
328
|
+
if (agentNames.length === 0) {
|
|
329
|
+
return results;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const summary = [
|
|
333
|
+
'## CC-DevFlow Agents',
|
|
334
|
+
'',
|
|
335
|
+
'- Scope: `.claude/agents/*.md`',
|
|
336
|
+
`- Count: ${agentNames.length}`,
|
|
337
|
+
`- Entries: ${this.formatCompactList(agentNames)}`,
|
|
338
|
+
'- Policy: Keep AGENTS.md concise; detailed agent instructions stay in source files.'
|
|
339
|
+
].join('\n');
|
|
340
|
+
|
|
341
|
+
const result = await this.upsertManagedBlock(targetPath, 'antigravity-agents', summary);
|
|
342
|
+
results.push({ ...result, count: agentNames.length });
|
|
343
|
+
|
|
344
|
+
return results;
|
|
327
345
|
}
|
|
328
346
|
|
|
329
347
|
/**
|
|
@@ -23,6 +23,7 @@ const crypto = require('crypto');
|
|
|
23
23
|
// SECURITY CONFIGURATION (FINDING-003)
|
|
24
24
|
// ============================================================
|
|
25
25
|
const MAX_OUTPUT_SIZE = 2 * 1024 * 1024; // 2MB limit
|
|
26
|
+
const MANAGED_BLOCK_PREFIX = 'cc-devflow';
|
|
26
27
|
|
|
27
28
|
// ============================================================
|
|
28
29
|
// MODULE_TYPES - 支持的模块类型
|
|
@@ -70,9 +71,11 @@ class BaseEmitter {
|
|
|
70
71
|
const outputDir = this.outputDir;
|
|
71
72
|
const ext = this.fileExtension;
|
|
72
73
|
const filePath = path.join(outputDir, `${filename}${ext}`);
|
|
74
|
+
const fileDir = path.dirname(filePath);
|
|
73
75
|
|
|
74
76
|
// ✅ SECURITY FIX (FINDING-005): Set explicit directory permissions
|
|
75
|
-
|
|
77
|
+
// 支持带子路径的文件名(例如 flow/new)
|
|
78
|
+
await fs.promises.mkdir(fileDir, { recursive: true, mode: 0o755 });
|
|
76
79
|
|
|
77
80
|
// ✅ SECURITY FIX (FINDING-005): Set explicit file permissions
|
|
78
81
|
await fs.promises.writeFile(filePath, content, { encoding: 'utf8', mode: 0o644 });
|
|
@@ -282,6 +285,83 @@ class BaseEmitter {
|
|
|
282
285
|
hashContent(content) {
|
|
283
286
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
284
287
|
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 读取目录下的 Markdown 文件名(去掉 .md)
|
|
291
|
+
*/
|
|
292
|
+
async readMarkdownEntryNames(sourceDir) {
|
|
293
|
+
if (!fs.existsSync(sourceDir)) {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const entries = await fs.promises.readdir(sourceDir, { withFileTypes: true });
|
|
298
|
+
|
|
299
|
+
return entries
|
|
300
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.md'))
|
|
301
|
+
.map(entry => entry.name.replace(/\.md$/, ''))
|
|
302
|
+
.sort((a, b) => a.localeCompare(b));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 格式化短列表,避免全局记忆文件过长
|
|
307
|
+
*/
|
|
308
|
+
formatCompactList(items, maxItems = 12) {
|
|
309
|
+
if (!items || items.length === 0) {
|
|
310
|
+
return '`(none)`';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const visible = items.slice(0, maxItems).map(item => `\`${item}\``);
|
|
314
|
+
const hiddenCount = items.length - visible.length;
|
|
315
|
+
|
|
316
|
+
if (hiddenCount > 0) {
|
|
317
|
+
visible.push(`... (+${hiddenCount} more)`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return visible.join(', ');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 受管块写入:仅替换指定区块,保留用户原有内容
|
|
325
|
+
*/
|
|
326
|
+
async upsertManagedBlock(targetPath, blockId, body) {
|
|
327
|
+
const startMarker = `<!-- ${MANAGED_BLOCK_PREFIX}:${blockId}:start -->`;
|
|
328
|
+
const endMarker = `<!-- ${MANAGED_BLOCK_PREFIX}:${blockId}:end -->`;
|
|
329
|
+
const managedBlock = `${startMarker}\n${body.trim()}\n${endMarker}`;
|
|
330
|
+
|
|
331
|
+
let existing = '';
|
|
332
|
+
if (fs.existsSync(targetPath)) {
|
|
333
|
+
existing = await fs.promises.readFile(targetPath, 'utf8');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const merged = this._replaceManagedBlock(existing, startMarker, endMarker, managedBlock);
|
|
337
|
+
return this.emitToPath(targetPath, merged);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_replaceManagedBlock(existing, startMarker, endMarker, managedBlock) {
|
|
341
|
+
const startIndex = existing.indexOf(startMarker);
|
|
342
|
+
const endIndex = existing.indexOf(endMarker);
|
|
343
|
+
|
|
344
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
345
|
+
const before = existing.slice(0, startIndex).trimEnd();
|
|
346
|
+
const after = existing.slice(endIndex + endMarker.length).trimStart();
|
|
347
|
+
|
|
348
|
+
return this._joinBlocks(before, managedBlock, after);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!existing.trim()) {
|
|
352
|
+
return `${managedBlock}\n`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return `${existing.trimEnd()}\n\n${managedBlock}\n`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_joinBlocks(...blocks) {
|
|
359
|
+
const validBlocks = blocks.filter(block => block && block.trim().length > 0);
|
|
360
|
+
if (validBlocks.length === 0) {
|
|
361
|
+
return '';
|
|
362
|
+
}
|
|
363
|
+
return `${validBlocks.join('\n\n')}\n`;
|
|
364
|
+
}
|
|
285
365
|
}
|
|
286
366
|
|
|
287
367
|
module.exports = BaseEmitter;
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* 输出格式:
|
|
10
10
|
* - Commands: Markdown + YAML frontmatter → .codex/prompts/
|
|
11
11
|
* - Skills: SKILL.md (YAML frontmatter) → .codex/skills/
|
|
12
|
-
* - Agents:
|
|
13
|
-
* - Rules:
|
|
12
|
+
* - Agents: 索引摘要写入 AGENTS.md(保留用户内容)
|
|
13
|
+
* - Rules: 索引摘要写入 AGENTS.md(保留用户内容)
|
|
14
14
|
*
|
|
15
15
|
* v2.0: 支持多模块编译
|
|
16
16
|
*/
|
|
@@ -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
|
}
|
|
@@ -195,51 +194,54 @@ class CodexEmitter extends BaseEmitter {
|
|
|
195
194
|
|
|
196
195
|
/**
|
|
197
196
|
* 编译 Agents 模块
|
|
198
|
-
* .claude/agents/[name].md -> AGENTS.md (
|
|
197
|
+
* .claude/agents/[name].md -> AGENTS.md (短索引,受管块)
|
|
199
198
|
*/
|
|
200
199
|
async emitAgents(sourceDir, targetPath) {
|
|
201
|
-
|
|
200
|
+
const results = [];
|
|
201
|
+
const agentNames = await this.readMarkdownEntryNames(sourceDir);
|
|
202
|
+
|
|
203
|
+
if (agentNames.length === 0) {
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const summary = [
|
|
208
|
+
'## CC-DevFlow Agents',
|
|
209
|
+
'',
|
|
210
|
+
'- Scope: `.claude/agents/*.md`',
|
|
211
|
+
`- Count: ${agentNames.length}`,
|
|
212
|
+
`- Entries: ${this.formatCompactList(agentNames)}`,
|
|
213
|
+
'- Policy: Keep AGENTS.md concise as global memory; full agent specs stay in source files.'
|
|
214
|
+
].join('\n');
|
|
215
|
+
|
|
216
|
+
const result = await this.upsertManagedBlock(targetPath, 'codex-agents', summary);
|
|
217
|
+
results.push({ ...result, count: agentNames.length });
|
|
218
|
+
|
|
219
|
+
return results;
|
|
202
220
|
}
|
|
203
221
|
|
|
204
222
|
/**
|
|
205
223
|
* 编译 Rules 模块
|
|
206
|
-
* .claude/rules/[name].md -> AGENTS.md (
|
|
224
|
+
* .claude/rules/[name].md -> AGENTS.md (短索引,受管块)
|
|
207
225
|
*/
|
|
208
226
|
async emitRules(sourceDir, targetPath) {
|
|
209
227
|
const results = [];
|
|
228
|
+
const ruleNames = await this.readMarkdownEntryNames(sourceDir);
|
|
210
229
|
|
|
211
|
-
if (
|
|
230
|
+
if (ruleNames.length === 0) {
|
|
212
231
|
return results;
|
|
213
232
|
}
|
|
214
233
|
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
sections.push(`### ${ruleName}\n\n${content}`);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (sections.length > 0) {
|
|
231
|
-
// 追加到现有 AGENTS.md
|
|
232
|
-
let existingContent = '';
|
|
233
|
-
if (fs.existsSync(targetPath)) {
|
|
234
|
-
existingContent = await fs.promises.readFile(targetPath, 'utf8');
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const rulesSection = `\n\n## Rules\n\n${sections.join('\n\n---\n\n')}`;
|
|
238
|
-
const merged = existingContent + rulesSection;
|
|
239
|
-
|
|
240
|
-
const result = await this.emitToPath(targetPath, merged);
|
|
241
|
-
results.push(result);
|
|
242
|
-
}
|
|
234
|
+
const summary = [
|
|
235
|
+
'## CC-DevFlow Rules',
|
|
236
|
+
'',
|
|
237
|
+
'- Scope: `.claude/rules/*.md`',
|
|
238
|
+
`- Count: ${ruleNames.length}`,
|
|
239
|
+
`- Entries: ${this.formatCompactList(ruleNames)}`,
|
|
240
|
+
'- Policy: AGENTS.md stores only memory-level constraints, not full rule bodies.'
|
|
241
|
+
].join('\n');
|
|
242
|
+
|
|
243
|
+
const result = await this.upsertManagedBlock(targetPath, 'codex-rules', summary);
|
|
244
|
+
results.push({ ...result, count: ruleNames.length });
|
|
243
245
|
|
|
244
246
|
return results;
|
|
245
247
|
}
|