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.
@@ -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
- const groupDirs = await fs.promises.readdir(sourceDir, { withFileTypes: true });
201
-
202
- for (const groupEntry of groupDirs) {
203
- if (!groupEntry.isDirectory() || groupEntry.name.startsWith('_')) {
204
- continue;
205
- }
206
-
207
- const groupDir = path.join(sourceDir, groupEntry.name);
208
- const skillEntries = await fs.promises.readdir(groupDir, { withFileTypes: true });
209
-
210
- for (const skillEntry of skillEntries) {
211
- if (!skillEntry.isDirectory() || skillEntry.name.startsWith('_')) {
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
- await this._copySkillResources(skillDir, targetSkillDir);
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
- await fs.promises.mkdir(outputDir, { recursive: true, mode: 0o755 });
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
- // 扫描技能分组目录 (workflow/, domain/, utility/, guardrail/)
80
- const groupDirs = await fs.promises.readdir(sourceDir, { withFileTypes: true });
81
-
82
- for (const groupEntry of groupDirs) {
83
- if (!groupEntry.isDirectory() || groupEntry.name.startsWith('_')) {
84
- continue;
85
- }
86
-
87
- const groupDir = path.join(sourceDir, groupEntry.name);
88
- const skillEntries = await fs.promises.readdir(groupDir, { withFileTypes: true });
89
-
90
- for (const skillEntry of skillEntries) {
91
- if (!skillEntry.isDirectory() || skillEntry.name.startsWith('_')) {
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
- await this._copySkillResources(skillDir, targetSkillDir);
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
- 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