@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.
- package/README.md +182 -1
- package/README.zh-CN.md +182 -1
- package/bin/cli.js +28 -4
- package/lib/defaults.json +1 -0
- package/lib/init.js +68 -4
- package/lib/prompt.js +28 -1
- package/lib/render.js +1 -1
- package/lib/sandbox/commands/create.js +7 -3
- package/lib/sandbox/commands/rm.js +6 -4
- package/lib/sandbox/commands/vm.js +43 -16
- package/lib/sandbox/config.js +5 -0
- package/lib/sandbox/engine.js +125 -16
- package/lib/sandbox/shell.js +47 -7
- package/lib/sandbox/task-resolver.js +13 -6
- package/lib/sandbox/tools.js +18 -14
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +17 -0
- package/templates/.agents/QUICKSTART.zh-CN.md +17 -0
- package/templates/.agents/README.en.md +121 -0
- package/templates/.agents/README.zh-CN.md +121 -0
- package/templates/.agents/rules/issue-pr-commands.en.md +5 -0
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +5 -0
- package/templates/.agents/rules/issue-sync.en.md +5 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +5 -0
- package/templates/.agents/rules/label-milestone-setup.en.md +5 -0
- package/templates/.agents/rules/label-milestone-setup.zh-CN.md +5 -0
- package/templates/.agents/rules/milestone-inference.en.md +5 -0
- package/templates/.agents/rules/milestone-inference.github.en.md +6 -5
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +6 -5
- package/templates/.agents/rules/milestone-inference.zh-CN.md +5 -0
- package/templates/.agents/rules/pr-sync.en.md +5 -0
- package/templates/.agents/rules/pr-sync.zh-CN.md +5 -0
- package/templates/.agents/rules/release-commands.en.md +5 -0
- package/templates/.agents/rules/release-commands.zh-CN.md +5 -0
- package/templates/.agents/rules/security-alerts.en.md +5 -0
- package/templates/.agents/rules/security-alerts.zh-CN.md +5 -0
- package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +47 -12
- package/templates/.agents/scripts/platform-adapters/platform-sync.js +6 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-issue/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-issue/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-release-note/SKILL.en.md +8 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +8 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/implement-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/implement-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/import-issue/SKILL.en.md +12 -4
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +12 -4
- package/templates/.agents/skills/import-issue/config/verify.json +2 -1
- package/templates/.agents/skills/init-labels/SKILL.en.md +1 -1
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-labels/scripts/init-labels.sh +6 -0
- package/templates/.agents/skills/init-milestones/SKILL.en.md +1 -1
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +6 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/post-release/SKILL.en.md +95 -0
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +95 -0
- package/templates/.agents/skills/refine-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/refine-title/SKILL.en.md +1 -1
- package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/release/SKILL.en.md +6 -1
- package/templates/.agents/skills/release/SKILL.zh-CN.md +6 -1
- package/templates/.agents/skills/release/scripts/manage-milestones.sh +6 -0
- package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/test/SKILL.en.md +1 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +1 -1
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +10 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +4 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +598 -7
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +1 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.claude/commands/post-release.en.md +8 -0
- package/templates/.claude/commands/post-release.zh-CN.md +8 -0
- package/templates/.gemini/commands/_project_/post-release.en.toml +6 -0
- package/templates/.gemini/commands/_project_/post-release.zh-CN.toml +6 -0
- package/templates/.github/workflows/metadata-sync.yml +1 -1
- package/templates/.github/workflows/pr-label.yml +1 -1
- package/templates/.github/workflows/status-label.yml +1 -1
- package/templates/.opencode/commands/post-release.en.md +9 -0
- 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.
|
|
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
|
|
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
|
-
|
|
449
|
-
|
|
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
|
|
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
|
|
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;
|