@fitlab-ai/agent-infra 0.5.5 → 0.5.7

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 (110) hide show
  1. package/README.md +182 -1
  2. package/README.zh-CN.md +182 -1
  3. package/bin/cli.js +28 -4
  4. package/lib/defaults.json +1 -0
  5. package/lib/init.js +68 -4
  6. package/lib/prompt.js +28 -1
  7. package/lib/render.js +1 -1
  8. package/lib/sandbox/commands/create.js +7 -3
  9. package/lib/sandbox/commands/rm.js +6 -4
  10. package/lib/sandbox/commands/vm.js +43 -16
  11. package/lib/sandbox/config.js +5 -0
  12. package/lib/sandbox/engine.js +125 -16
  13. package/lib/sandbox/shell.js +47 -7
  14. package/lib/sandbox/task-resolver.js +13 -6
  15. package/lib/sandbox/tools.js +18 -14
  16. package/package.json +2 -2
  17. package/templates/.agents/QUICKSTART.en.md +17 -0
  18. package/templates/.agents/QUICKSTART.zh-CN.md +17 -0
  19. package/templates/.agents/README.en.md +121 -0
  20. package/templates/.agents/README.zh-CN.md +121 -0
  21. package/templates/.agents/rules/issue-pr-commands.en.md +5 -0
  22. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +5 -0
  23. package/templates/.agents/rules/issue-sync.en.md +5 -0
  24. package/templates/.agents/rules/issue-sync.zh-CN.md +5 -0
  25. package/templates/.agents/rules/label-milestone-setup.en.md +5 -0
  26. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +5 -0
  27. package/templates/.agents/rules/milestone-inference.en.md +5 -0
  28. package/templates/.agents/rules/milestone-inference.github.en.md +6 -5
  29. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +6 -5
  30. package/templates/.agents/rules/milestone-inference.zh-CN.md +5 -0
  31. package/templates/.agents/rules/pr-sync.en.md +5 -0
  32. package/templates/.agents/rules/pr-sync.zh-CN.md +5 -0
  33. package/templates/.agents/rules/release-commands.en.md +5 -0
  34. package/templates/.agents/rules/release-commands.zh-CN.md +5 -0
  35. package/templates/.agents/rules/security-alerts.en.md +5 -0
  36. package/templates/.agents/rules/security-alerts.zh-CN.md +5 -0
  37. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +47 -12
  38. package/templates/.agents/scripts/platform-adapters/platform-sync.js +6 -0
  39. package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
  40. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
  41. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  42. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  43. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  44. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  45. package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
  46. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
  47. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  48. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
  49. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  50. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
  51. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  52. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  53. package/templates/.agents/skills/create-issue/SKILL.en.md +2 -2
  54. package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +2 -2
  55. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  56. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  57. package/templates/.agents/skills/create-release-note/SKILL.en.md +8 -1
  58. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +8 -1
  59. package/templates/.agents/skills/create-task/SKILL.en.md +2 -2
  60. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +2 -2
  61. package/templates/.agents/skills/implement-task/SKILL.en.md +3 -3
  62. package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +3 -3
  63. package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
  64. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +2 -2
  65. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  66. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
  67. package/templates/.agents/skills/import-issue/SKILL.en.md +12 -4
  68. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +12 -4
  69. package/templates/.agents/skills/import-issue/config/verify.json +2 -1
  70. package/templates/.agents/skills/init-labels/SKILL.en.md +1 -1
  71. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
  72. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +6 -0
  73. package/templates/.agents/skills/init-milestones/SKILL.en.md +1 -1
  74. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +1 -1
  75. package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +6 -0
  76. package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
  77. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
  78. package/templates/.agents/skills/post-release/SKILL.en.md +95 -0
  79. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +95 -0
  80. package/templates/.agents/skills/refine-task/SKILL.en.md +2 -2
  81. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +2 -2
  82. package/templates/.agents/skills/refine-title/SKILL.en.md +1 -1
  83. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +1 -1
  84. package/templates/.agents/skills/release/SKILL.en.md +6 -1
  85. package/templates/.agents/skills/release/SKILL.zh-CN.md +6 -1
  86. package/templates/.agents/skills/release/scripts/manage-milestones.sh +6 -0
  87. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  88. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
  89. package/templates/.agents/skills/review-task/SKILL.en.md +3 -3
  90. package/templates/.agents/skills/review-task/SKILL.zh-CN.md +3 -3
  91. package/templates/.agents/skills/test/SKILL.en.md +1 -1
  92. package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
  93. package/templates/.agents/skills/test-integration/SKILL.en.md +1 -1
  94. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +1 -1
  95. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +10 -2
  96. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +4 -2
  97. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +598 -7
  98. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +1 -1
  99. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +1 -1
  100. package/templates/.agents/templates/task.en.md +2 -2
  101. package/templates/.agents/templates/task.zh-CN.md +2 -2
  102. package/templates/.claude/commands/post-release.en.md +8 -0
  103. package/templates/.claude/commands/post-release.zh-CN.md +8 -0
  104. package/templates/.gemini/commands/_project_/post-release.en.toml +6 -0
  105. package/templates/.gemini/commands/_project_/post-release.zh-CN.toml +6 -0
  106. package/templates/.github/workflows/metadata-sync.yml +1 -1
  107. package/templates/.github/workflows/pr-label.yml +1 -1
  108. package/templates/.github/workflows/status-label.yml +1 -1
  109. package/templates/.opencode/commands/post-release.en.md +9 -0
  110. package/templates/.opencode/commands/post-release.zh-CN.md +9 -0
@@ -15,6 +15,7 @@
15
15
 
16
16
  import childProcess from 'node:child_process';
17
17
  import fs from 'node:fs';
18
+ import os from 'node:os';
18
19
  import path from 'node:path';
19
20
  import { fileURLToPath } from 'node:url';
20
21
 
@@ -23,6 +24,7 @@ const DEFAULTS = {
23
24
  "type": "github"
24
25
  },
25
26
  "sandbox": {
27
+ "engine": null,
26
28
  "runtimes": [
27
29
  "node20"
28
30
  ],
@@ -77,7 +79,7 @@ const DEFAULTS = {
77
79
  }
78
80
  };
79
81
 
80
- const INSTALLER_VERSION = "v0.5.5";
82
+ const INSTALLER_VERSION = "v0.5.7";
81
83
  const PACKAGE_NAME = '@fitlab-ai/agent-infra';
82
84
  // Add a new identifier here only after shipping matching .{platform}. template variants.
83
85
  const KNOWN_PLATFORMS = new Set(['github']);
@@ -85,6 +87,21 @@ const KNOWN_LANGUAGES = new Set(['en', 'zh-CN']);
85
87
 
86
88
  function norm(p) { return p.replace(/\\/g, '/'); }
87
89
 
90
+ function normDir(p) {
91
+ return norm(p).replace(/^\.\//, '').replace(/\/+$/, '');
92
+ }
93
+
94
+ function isInsideProject(projectRoot, relativePath) {
95
+ if (typeof relativePath !== 'string' || relativePath.trim() === '' || path.isAbsolute(relativePath)) {
96
+ return false;
97
+ }
98
+
99
+ const root = path.resolve(projectRoot);
100
+ const resolved = path.resolve(projectRoot, relativePath);
101
+ const rel = path.relative(root, resolved);
102
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
103
+ }
104
+
88
105
  function globMatch(pattern, filePath) {
89
106
  const p = norm(pattern), f = norm(filePath);
90
107
  const globstarDir = '__GLOBSTAR_DIR__';
@@ -124,6 +141,528 @@ function removeEmptyDirs(dir) {
124
141
  }
125
142
  }
126
143
 
144
+ function parseSkillFrontmatter(filePath) {
145
+ const content = fs.readFileSync(filePath, 'utf8');
146
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
147
+ if (!match) return {};
148
+
149
+ const result = {};
150
+ const lines = match[1].split(/\r?\n/);
151
+ const normalizeValue = (value) => value.replace(/^["']|["']$/g, '').trim();
152
+
153
+ for (let index = 0; index < lines.length; index += 1) {
154
+ const line = lines[index];
155
+ const pair = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
156
+ if (!pair) continue;
157
+
158
+ const [, key, rawValue] = pair;
159
+ if (rawValue === '>') {
160
+ const block = [];
161
+ for (let offset = index + 1; offset < lines.length; offset += 1) {
162
+ const nextLine = lines[offset];
163
+ if (!/^\s+/.test(nextLine)) break;
164
+
165
+ block.push(nextLine.trim());
166
+ index = offset;
167
+ }
168
+ result[key] = block.join(' ').trim();
169
+ continue;
170
+ }
171
+
172
+ result[key] = normalizeValue(rawValue);
173
+ }
174
+
175
+ return result;
176
+ }
177
+
178
+ function listTemplateSkillNames(templateRoot) {
179
+ const templateSkillsDir = path.join(templateRoot, '.agents/skills');
180
+ if (!fs.existsSync(templateSkillsDir)) return new Set();
181
+
182
+ return new Set(
183
+ fs.readdirSync(templateSkillsDir, { withFileTypes: true })
184
+ .filter((entry) => entry.isDirectory())
185
+ .map((entry) => entry.name)
186
+ );
187
+ }
188
+
189
+ function detectCustomSkills(projectRoot, templateSkillNames) {
190
+ const skillsDir = path.join(projectRoot, '.agents/skills');
191
+ if (!fs.existsSync(skillsDir)) return [];
192
+
193
+ return fs.readdirSync(skillsDir, { withFileTypes: true })
194
+ .filter((entry) => entry.isDirectory() && !templateSkillNames.has(entry.name))
195
+ .map((entry) => {
196
+ const skillMd = path.join(skillsDir, entry.name, 'SKILL.md');
197
+ if (!fs.existsSync(skillMd)) return null;
198
+
199
+ const meta = parseSkillFrontmatter(skillMd);
200
+ return {
201
+ dirName: entry.name,
202
+ name: meta.name || entry.name,
203
+ description: meta.description || '',
204
+ args: meta.args || null
205
+ };
206
+ })
207
+ .filter(Boolean)
208
+ .sort((left, right) => left.dirName.localeCompare(right.dirName));
209
+ }
210
+
211
+ function isCustomProtected(targetPath, customSkills, project, customTUICommandTargets) {
212
+ const normalized = norm(targetPath);
213
+
214
+ return customSkills.some(({ dirName }) => (
215
+ normalized.startsWith(`.agents/skills/${dirName}/`) ||
216
+ normalized === `.claude/commands/${dirName}.md` ||
217
+ normalized === `.opencode/commands/${dirName}.md` ||
218
+ normalized === '.gemini/commands/' + project + '/' + dirName + '.toml' ||
219
+ customTUICommandTargets.has(normalized)
220
+ ));
221
+ }
222
+
223
+ function recordCustomTUISkipped(report, entry) {
224
+ report?.custom?.customTUIs?.skipped?.push(entry);
225
+ }
226
+
227
+ function recordCustomTUISkippedRef(report, entry) {
228
+ report?.custom?.customTUIs?.skippedRefs?.push(entry);
229
+ }
230
+
231
+ function expandHome(inputPath) {
232
+ if (inputPath === '~') return os.homedir();
233
+ if (inputPath.startsWith('~/')) {
234
+ return path.join(os.homedir(), inputPath.slice(2));
235
+ }
236
+
237
+ return path.resolve(inputPath);
238
+ }
239
+
240
+ function mergeTemplateSources(baseRoot, sources, report) {
241
+ const sourceMap = new Map();
242
+ const sourceMeta = new Map();
243
+ const conflictsByRel = new Map();
244
+ const baseRels = walkDir(baseRoot).map((filePath) => norm(path.relative(baseRoot, filePath)));
245
+
246
+ for (const rel of baseRels) {
247
+ sourceMap.set(rel, baseRoot);
248
+ sourceMeta.set(rel, { type: 'builtin' });
249
+ }
250
+
251
+ const recordConflict = (rel, winner, ignored) => {
252
+ const existing = conflictsByRel.get(rel);
253
+ if (existing) {
254
+ existing.winner = winner;
255
+ existing.ignored.push(...ignored);
256
+ return;
257
+ }
258
+
259
+ const conflict = { rel, winner, ignored: [...ignored] };
260
+ conflictsByRel.set(rel, conflict);
261
+ report.templateSources.conflicts.push(conflict);
262
+ };
263
+
264
+ const templateSources = Array.isArray(sources) ? sources : [];
265
+ for (const [index, source] of templateSources.entries()) {
266
+ if (source?.type !== 'local') continue;
267
+ if (typeof source.path !== 'string' || source.path.trim() === '') {
268
+ report.templateSources.errors.push({
269
+ index,
270
+ type: String(source?.type || ''),
271
+ path: String(source?.path || ''),
272
+ reason: 'invalid path'
273
+ });
274
+ continue;
275
+ }
276
+
277
+ const srcDir = expandHome(source.path);
278
+ if (!fs.existsSync(srcDir) || !fs.statSync(srcDir).isDirectory()) {
279
+ report.templateSources.errors.push({
280
+ index,
281
+ type: source.type,
282
+ path: source.path,
283
+ reason: 'directory not found'
284
+ });
285
+ continue;
286
+ }
287
+
288
+ const extRels = walkDir(srcDir).map((filePath) => norm(path.relative(srcDir, filePath)));
289
+ const sourceInfo = { type: source.type, path: source.path };
290
+ for (const rel of extRels) {
291
+ const existing = sourceMeta.get(rel);
292
+ if (existing?.type === 'builtin') {
293
+ recordConflict(rel, existing, [sourceInfo]);
294
+ continue;
295
+ }
296
+
297
+ if (existing) {
298
+ recordConflict(rel, sourceInfo, [existing]);
299
+ }
300
+
301
+ sourceMap.set(rel, srcDir);
302
+ sourceMeta.set(rel, sourceInfo);
303
+ }
304
+
305
+ report.templateSources.loaded += 1;
306
+ report.templateSources.files += extRels.length;
307
+ }
308
+
309
+ return {
310
+ mergedRels: [...sourceMap.keys()],
311
+ sourceMap
312
+ };
313
+ }
314
+
315
+ function writeIfChanged(projectRoot, targetPath, content, reportBucket) {
316
+ const fullPath = path.join(projectRoot, targetPath);
317
+ const exists = fs.existsSync(fullPath);
318
+
319
+ if (exists && fs.readFileSync(fullPath, 'utf8') === content) {
320
+ reportBucket.unchanged.push(targetPath);
321
+ return;
322
+ }
323
+
324
+ const dir = path.dirname(fullPath);
325
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
326
+ fs.writeFileSync(fullPath, content, 'utf8');
327
+
328
+ (exists ? reportBucket.updated : reportBucket.generated).push(targetPath);
329
+ }
330
+
331
+ function syncCustomSkillSources(projectRoot, sources, report, templateSkillNames) {
332
+ const skillsDir = path.join(projectRoot, '.agents/skills');
333
+ const syncedSkills = new Map();
334
+
335
+ for (const source of sources) {
336
+ if (source?.type !== 'local') continue;
337
+ if (typeof source.path !== 'string' || source.path.trim() === '') {
338
+ report.custom.sourceErrors.push({ source: String(source?.path || ''), reason: 'invalid path' });
339
+ continue;
340
+ }
341
+
342
+ const srcDir = expandHome(source.path);
343
+ if (!fs.existsSync(srcDir) || !fs.statSync(srcDir).isDirectory()) {
344
+ report.custom.sourceErrors.push({ source: source.path, reason: 'directory not found' });
345
+ continue;
346
+ }
347
+
348
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
349
+ if (!entry.isDirectory()) continue;
350
+ if (templateSkillNames.has(entry.name)) {
351
+ report.custom.sourceErrors.push({
352
+ source: source.path,
353
+ reason: `skill ${entry.name} conflicts with built-in skill`
354
+ });
355
+ continue;
356
+ }
357
+
358
+ const skillSrcDir = path.join(srcDir, entry.name);
359
+ const skillMd = path.join(skillSrcDir, 'SKILL.md');
360
+ if (!fs.existsSync(skillMd)) continue;
361
+
362
+ const skillDstDir = path.join(skillsDir, entry.name);
363
+ const trackedFiles = syncedSkills.get(entry.name) || new Set();
364
+ syncedSkills.set(entry.name, trackedFiles);
365
+
366
+ for (const srcFile of walkDir(skillSrcDir)) {
367
+ const relPath = norm(path.relative(skillSrcDir, srcFile));
368
+ const dstFile = path.join(skillDstDir, relPath);
369
+ const projectPath = norm(path.relative(projectRoot, dstFile));
370
+ const srcContent = fs.readFileSync(srcFile);
371
+ const existed = fs.existsSync(dstFile);
372
+
373
+ trackedFiles.add(relPath);
374
+
375
+ if (existed) {
376
+ const dstContent = fs.readFileSync(dstFile);
377
+ if (srcContent.equals(dstContent)) {
378
+ report.custom.unchanged.push(projectPath);
379
+ continue;
380
+ }
381
+ }
382
+
383
+ const dir = path.dirname(dstFile);
384
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
385
+ fs.writeFileSync(dstFile, srcContent);
386
+
387
+ (existed ? report.custom.updated : report.custom.generated).push(projectPath);
388
+ }
389
+ }
390
+ }
391
+
392
+ return syncedSkills;
393
+ }
394
+
395
+ function cleanStaleSyncedFiles(projectRoot, syncedSkills, report) {
396
+ const skillsDir = path.join(projectRoot, '.agents/skills');
397
+
398
+ for (const [skillName, expectedFiles] of syncedSkills) {
399
+ const skillDir = path.join(skillsDir, skillName);
400
+ if (!fs.existsSync(skillDir)) continue;
401
+
402
+ const actualFiles = walkDir(skillDir).map((filePath) => norm(path.relative(skillDir, filePath)));
403
+ const removedBefore = report.custom.removed.length;
404
+
405
+ for (const actualFile of actualFiles) {
406
+ if (expectedFiles.has(actualFile)) continue;
407
+
408
+ const staleFile = path.join(skillDir, actualFile);
409
+ fs.unlinkSync(staleFile);
410
+ report.custom.removed.push(norm(path.relative(projectRoot, staleFile)));
411
+ }
412
+
413
+ if (report.custom.removed.length > removedBefore) {
414
+ removeEmptyDirs(skillDir);
415
+ }
416
+ }
417
+ }
418
+
419
+ function generateClaudeCommand(skill, lang) {
420
+ const isZhCN = lang === 'zh-CN';
421
+ const lines = ['---', `description: ${JSON.stringify(skill.description)}`];
422
+
423
+ if (skill.args) {
424
+ lines.push(`usage: ${JSON.stringify(`/${skill.dirName} ${skill.args}`)}`);
425
+ }
426
+
427
+ lines.push('---', '');
428
+ lines.push(
429
+ isZhCN
430
+ ? `读取并执行 \`.agents/skills/${skill.dirName}/SKILL.md\` 中的 ${skill.dirName} 技能。`
431
+ : `Read and execute the ${skill.dirName} skill from \`.agents/skills/${skill.dirName}/SKILL.md\`.`
432
+ );
433
+ lines.push('');
434
+ lines.push(isZhCN ? '严格按照技能中定义的所有步骤执行。' : 'Follow all steps defined in the skill exactly.');
435
+
436
+ return `${lines.join('\n')}\n`;
437
+ }
438
+
439
+ function generateGeminiCommand(skill, lang) {
440
+ const isZhCN = lang === 'zh-CN';
441
+ const promptLines = [];
442
+
443
+ if (skill.args) {
444
+ promptLines.push(isZhCN ? '参数:{{args}}' : 'Arguments: {{args}}');
445
+ promptLines.push('');
446
+ }
447
+
448
+ promptLines.push(
449
+ isZhCN
450
+ ? `读取并执行 \`.agents/skills/${skill.dirName}/SKILL.md\` 中的 ${skill.dirName} 技能。`
451
+ : `Read and execute the ${skill.dirName} skill from \`.agents/skills/${skill.dirName}/SKILL.md\`.`
452
+ );
453
+ promptLines.push('');
454
+ promptLines.push(isZhCN ? '严格按照技能中定义的所有步骤执行。' : 'Follow all steps defined in the skill exactly.');
455
+
456
+ return [
457
+ `description = ${JSON.stringify(skill.description)}`,
458
+ 'prompt = """',
459
+ ...promptLines,
460
+ '"""'
461
+ ].join('\n') + '\n';
462
+ }
463
+
464
+ function generateOpenCodeCommand(skill, lang) {
465
+ const isZhCN = lang === 'zh-CN';
466
+ const lines = [
467
+ '---',
468
+ `description: ${JSON.stringify(skill.description)}`,
469
+ 'agent: general',
470
+ 'subtask: false',
471
+ '---',
472
+ ''
473
+ ];
474
+
475
+ if (skill.args) {
476
+ lines.push(isZhCN ? '参数:$ARGUMENTS' : 'Arguments: $ARGUMENTS');
477
+ lines.push('');
478
+ }
479
+
480
+ lines.push(
481
+ isZhCN
482
+ ? `读取并执行 \`.agents/skills/${skill.dirName}/SKILL.md\` 中的 ${skill.dirName} 技能。`
483
+ : `Read and execute the ${skill.dirName} skill from \`.agents/skills/${skill.dirName}/SKILL.md\`.`
484
+ );
485
+ lines.push('');
486
+ lines.push(isZhCN ? '严格按照技能中定义的所有步骤执行。' : 'Follow all steps defined in the skill exactly.');
487
+
488
+ return `${lines.join('\n')}\n`;
489
+ }
490
+
491
+ function validateCustomTUIs(projectRoot, customTUIs, report) {
492
+ const tools = Array.isArray(customTUIs) ? customTUIs : [];
493
+ return tools
494
+ .map((tool, index) => {
495
+ if (typeof tool?.dir !== 'string' || tool.dir.trim() === '') {
496
+ recordCustomTUISkipped(report, {
497
+ index,
498
+ name: String(tool?.name || ''),
499
+ dir: String(tool?.dir || ''),
500
+ reason: 'invalid dir'
501
+ });
502
+ return null;
503
+ }
504
+
505
+ if (!isInsideProject(projectRoot, tool.dir)) {
506
+ recordCustomTUISkipped(report, {
507
+ index,
508
+ name: String(tool?.name || ''),
509
+ dir: tool.dir,
510
+ reason: 'dir must be a relative path inside the project root'
511
+ });
512
+ return null;
513
+ }
514
+
515
+ return { ...tool, index, dir: normDir(tool.dir) };
516
+ })
517
+ .filter(Boolean);
518
+ }
519
+
520
+ function customTUITargetPath(tool, refFile, refSkillName, skillName) {
521
+ const targetFile = refFile.includes(refSkillName)
522
+ ? refFile.replaceAll(refSkillName, skillName)
523
+ : `${skillName}${path.extname(refFile)}`;
524
+ return norm(path.join(tool.dir, targetFile));
525
+ }
526
+
527
+ function findCustomTUIReference(projectRoot, tool, templateSkillNames, report, logSkipped = false) {
528
+ const cmdDir = path.join(projectRoot, tool.dir);
529
+ if (!fs.existsSync(cmdDir) || !fs.statSync(cmdDir).isDirectory()) {
530
+ if (logSkipped) {
531
+ recordCustomTUISkipped(report, {
532
+ index: tool.index,
533
+ name: String(tool.name || ''),
534
+ dir: tool.dir,
535
+ reason: 'directory not found'
536
+ });
537
+ }
538
+ return null;
539
+ }
540
+
541
+ const cmdFiles = fs.readdirSync(cmdDir)
542
+ .filter((file) => fs.statSync(path.join(cmdDir, file)).isFile())
543
+ .sort((left, right) => left.localeCompare(right));
544
+ if (cmdFiles.length === 0) {
545
+ if (logSkipped) {
546
+ recordCustomTUISkipped(report, {
547
+ index: tool.index,
548
+ name: String(tool.name || ''),
549
+ dir: tool.dir,
550
+ reason: 'no command files'
551
+ });
552
+ }
553
+ return null;
554
+ }
555
+
556
+ let sawKnownSkillReference = false;
557
+
558
+ for (const file of cmdFiles) {
559
+ const content = fs.readFileSync(path.join(cmdDir, file), 'utf8');
560
+ const match = content.match(/\.agents\/skills\/([^/]+)\/SKILL\.md/);
561
+ if (!match) continue;
562
+
563
+ const skillName = match[1];
564
+ if (!templateSkillNames.has(skillName)) continue;
565
+
566
+ const skillMd = path.join(projectRoot, '.agents/skills', skillName, 'SKILL.md');
567
+ if (!fs.existsSync(skillMd)) continue;
568
+
569
+ const meta = parseSkillFrontmatter(skillMd);
570
+ if (!meta.description) continue;
571
+
572
+ sawKnownSkillReference = true;
573
+ if (!content.includes(meta.description)) {
574
+ if (logSkipped) {
575
+ recordCustomTUISkippedRef(report, {
576
+ index: tool.index,
577
+ name: String(tool.name || ''),
578
+ dir: tool.dir,
579
+ file,
580
+ skill: skillName,
581
+ reason: 'description not found in reference command file'
582
+ });
583
+ }
584
+ continue;
585
+ }
586
+
587
+ return { content, file, skillName, skillDesc: meta.description };
588
+ }
589
+
590
+ if (logSkipped) {
591
+ recordCustomTUISkipped(report, {
592
+ index: tool.index,
593
+ name: String(tool.name || ''),
594
+ dir: tool.dir,
595
+ reason: sawKnownSkillReference
596
+ ? 'no reference command file with matching description'
597
+ : 'no usable reference command file'
598
+ });
599
+ }
600
+
601
+ return null;
602
+ }
603
+
604
+ function buildCustomTUICommandTargets(projectRoot, customSkills, customTUIs, templateSkillNames) {
605
+ const targets = new Set();
606
+ for (const tool of customTUIs) {
607
+ const ref = findCustomTUIReference(projectRoot, tool, templateSkillNames, null, false);
608
+ if (!ref) continue;
609
+
610
+ for (const skill of customSkills) {
611
+ targets.add(customTUITargetPath(tool, ref.file, ref.skillName, skill.dirName));
612
+ }
613
+ }
614
+
615
+ return targets;
616
+ }
617
+
618
+ function learnAndGenerateCommands(projectRoot, customSkills, tool, templateSkillNames, report) {
619
+ const ref = findCustomTUIReference(projectRoot, tool, templateSkillNames, report, true);
620
+ if (!ref) return;
621
+
622
+ for (const skill of customSkills) {
623
+ const descToken = '__AGENT_INFRA_CUSTOM_SKILL_DESCRIPTION__';
624
+ const generated = ref.content
625
+ .replaceAll(ref.skillDesc, descToken)
626
+ .replaceAll(ref.skillName, skill.dirName)
627
+ .replaceAll(descToken, skill.description);
628
+
629
+ writeIfChanged(
630
+ projectRoot,
631
+ customTUITargetPath(tool, ref.file, ref.skillName, skill.dirName),
632
+ generated,
633
+ report.custom.commands
634
+ );
635
+ }
636
+ }
637
+
638
+ function generateCustomCommands(projectRoot, customSkills, project, lang, report, customTUIs, templateSkillNames) {
639
+ for (const skill of customSkills) {
640
+ writeIfChanged(
641
+ projectRoot,
642
+ `.claude/commands/${skill.dirName}.md`,
643
+ generateClaudeCommand(skill, lang),
644
+ report.custom.commands
645
+ );
646
+ writeIfChanged(
647
+ projectRoot,
648
+ '.gemini/commands/' + project + '/' + skill.dirName + '.toml',
649
+ generateGeminiCommand(skill, lang),
650
+ report.custom.commands
651
+ );
652
+ writeIfChanged(
653
+ projectRoot,
654
+ `.opencode/commands/${skill.dirName}.md`,
655
+ generateOpenCodeCommand(skill, lang),
656
+ report.custom.commands
657
+ );
658
+ }
659
+
660
+ const tools = Array.isArray(customTUIs) ? customTUIs : [];
661
+ for (const tool of tools) {
662
+ learnAndGenerateCommands(projectRoot, customSkills, tool, templateSkillNames, report);
663
+ }
664
+ }
665
+
127
666
  function matchesAny(rel, patterns) {
128
667
  const n = norm(rel);
129
668
  return patterns.some(p => norm(p) === n || globMatch(p, n));
@@ -411,7 +950,10 @@ function syncTemplates(projectRoot, templateRootOverride) {
411
950
 
412
951
  const { project, org, language: lang = 'en' } = cfg;
413
952
  const platformType = cfg.platform?.type || DEFAULTS.platform.type;
953
+ const customTUIsConfig = Array.isArray(cfg.customTUIs) ? cfg.customTUIs : [];
414
954
  const vars = { project, org };
955
+ const templateSkillNames = listTemplateSkillNames(templateRoot);
956
+ const protectedCustomSkills = detectCustomSkills(projectRoot, templateSkillNames);
415
957
 
416
958
  const managed = [...(cfg.files.managed || [])];
417
959
  const merged = [...(cfg.files.merged || [])];
@@ -421,12 +963,36 @@ function syncTemplates(projectRoot, templateRootOverride) {
421
963
  templateVersion: version,
422
964
  templateRoot: norm(templateRoot),
423
965
  registryAdded: [],
966
+ templateSources: {
967
+ configured: 0,
968
+ loaded: 0,
969
+ files: 0,
970
+ errors: [],
971
+ conflicts: []
972
+ },
424
973
  managed: { written: [], created: [], unchanged: [], skippedMerged: [], removed: [] },
974
+ custom: {
975
+ detected: [],
976
+ generated: [],
977
+ updated: [],
978
+ unchanged: [],
979
+ removed: [],
980
+ sourceErrors: [],
981
+ customTUIs: { skipped: [], skippedRefs: [] },
982
+ commands: { generated: [], updated: [], unchanged: [] }
983
+ },
425
984
  ejected: { created: [], skipped: [] },
426
985
  merged: { pending: [] },
427
986
  configUpdated: false,
428
987
  selfUpdate: false
429
988
  };
989
+ const customTUIs = validateCustomTUIs(projectRoot, customTUIsConfig, report);
990
+ const customTUICommandTargets = buildCustomTUICommandTargets(
991
+ projectRoot,
992
+ protectedCustomSkills,
993
+ customTUIs,
994
+ templateSkillNames
995
+ );
430
996
 
431
997
  const known = new Set([...managed, ...merged, ...ejected]);
432
998
  for (const e of (DEFAULTS.files.managed || [])) {
@@ -436,7 +1002,10 @@ function syncTemplates(projectRoot, templateRootOverride) {
436
1002
  if (!known.has(e)) { merged.push(e); known.add(e); report.registryAdded.push({ entry: e, list: 'merged' }); }
437
1003
  }
438
1004
 
439
- const allRels = walkDir(templateRoot).map(f => norm(path.relative(templateRoot, f)));
1005
+ const templateSources = Array.isArray(cfg.templates?.sources) ? cfg.templates.sources : [];
1006
+ report.templateSources.configured = templateSources.length;
1007
+ const { mergedRels, sourceMap } = mergeTemplateSources(templateRoot, templateSources, report);
1008
+ const allRels = mergedRels;
440
1009
  const allSet = new Set(allRels);
441
1010
  for (const entry of managed) {
442
1011
  const isDir = entry.endsWith('/');
@@ -445,10 +1014,14 @@ function syncTemplates(projectRoot, templateRootOverride) {
445
1014
 
446
1015
  if (isDir) {
447
1016
  const dir = path.join(templateRoot, entry);
448
- if (!fs.existsSync(dir)) continue;
449
- entryRels = walkDir(dir).map(f => norm(path.relative(templateRoot, f)));
1017
+ const builtinRels = fs.existsSync(dir)
1018
+ ? walkDir(dir).map((filePath) => norm(path.relative(templateRoot, filePath)))
1019
+ : [];
1020
+ const prefix = norm(entry);
1021
+ const externalRels = allRels.filter((rel) => rel.startsWith(prefix) && !builtinRels.includes(rel));
1022
+ entryRels = [...builtinRels, ...externalRels];
1023
+ if (!entryRels.length) continue;
450
1024
  } else {
451
- entryRels = [];
452
1025
  entryRels = entryVariantRels(entry, allSet, platformType);
453
1026
  if (!entryRels.length) continue;
454
1027
  }
@@ -463,7 +1036,8 @@ function syncTemplates(projectRoot, templateRootOverride) {
463
1036
  continue;
464
1037
  }
465
1038
 
466
- const srcFull = path.join(templateRoot, src);
1039
+ const srcRoot = sourceMap.get(src) || templateRoot;
1040
+ const srcFull = path.join(srcRoot, src);
467
1041
  const dstFull = path.join(projectRoot, tgt);
468
1042
  const bin = isBinary(srcFull);
469
1043
  const content = bin
@@ -497,6 +1071,7 @@ function syncTemplates(projectRoot, templateRootOverride) {
497
1071
  for (const projFile of projFiles) {
498
1072
  if (expectedTargets.has(projFile)) continue;
499
1073
  if (projFile === configPathRel) continue;
1074
+ if (isCustomProtected(projFile, protectedCustomSkills, project, customTUICommandTargets)) continue;
500
1075
  if (matchesAny(projFile, merged) || matchesAny(projFile, ejected)) continue;
501
1076
 
502
1077
  fs.unlinkSync(path.join(projectRoot, projFile));
@@ -509,6 +1084,16 @@ function syncTemplates(projectRoot, templateRootOverride) {
509
1084
  }
510
1085
  }
511
1086
 
1087
+ const sources = Array.isArray(cfg.skills?.sources) ? cfg.skills.sources : [];
1088
+ if (sources.length > 0) {
1089
+ const syncedSkills = syncCustomSkillSources(projectRoot, sources, report, templateSkillNames);
1090
+ cleanStaleSyncedFiles(projectRoot, syncedSkills, report);
1091
+ }
1092
+
1093
+ const customSkills = detectCustomSkills(projectRoot, templateSkillNames);
1094
+ report.custom.detected = customSkills.map((skill) => skill.dirName);
1095
+ generateCustomCommands(projectRoot, customSkills, project, lang, report, customTUIs, templateSkillNames);
1096
+
512
1097
  for (const entry of ejected) {
513
1098
  const dstFull = path.join(projectRoot, entry);
514
1099
  if (fs.existsSync(dstFull)) {
@@ -521,7 +1106,8 @@ function syncTemplates(projectRoot, templateRootOverride) {
521
1106
  const src = selected.get(target);
522
1107
  if (!src) continue;
523
1108
 
524
- const content = renderContent(fs.readFileSync(path.join(templateRoot, src), 'utf8'), vars);
1109
+ const srcRoot = sourceMap.get(src) || templateRoot;
1110
+ const content = renderContent(fs.readFileSync(path.join(srcRoot, src), 'utf8'), vars);
525
1111
  const dir = path.dirname(dstFull);
526
1112
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
527
1113
  fs.writeFileSync(dstFull, content);
@@ -557,6 +1143,11 @@ function syncTemplates(projectRoot, templateRootOverride) {
557
1143
  report.managed.written.length +
558
1144
  report.managed.created.length +
559
1145
  report.managed.removed.length +
1146
+ report.custom.generated.length +
1147
+ report.custom.updated.length +
1148
+ report.custom.removed.length +
1149
+ report.custom.commands.generated.length +
1150
+ report.custom.commands.updated.length +
560
1151
  report.ejected.created.length +
561
1152
  report.registryAdded.length
562
1153
  ) > 0;