@actiondock/builder 2.5.1 → 2.6.0
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 +17 -9
- package/dist/build.js +1 -1
- package/dist/export-archive.d.ts +20 -0
- package/dist/export-archive.js +52 -0
- package/dist/export-batch.d.ts +13 -0
- package/dist/export-batch.js +39 -0
- package/dist/export-composite.d.ts +13 -0
- package/dist/export-composite.js +225 -0
- package/dist/export-node.d.ts +11 -0
- package/dist/export-node.js +48 -0
- package/dist/export-source.d.ts +11 -0
- package/dist/export-source.js +134 -0
- package/dist/exporter.d.ts +10 -35
- package/dist/exporter.js +26 -645
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -2
- package/dist/pack.js +17 -3
- package/dist/planner.d.ts +0 -17
- package/dist/planner.js +0 -17
- package/dist/skill-md.d.ts +47 -0
- package/dist/skill-md.js +166 -0
- package/dist/types.d.ts +2 -6
- package/package.json +4 -4
package/dist/exporter.js
CHANGED
|
@@ -1,387 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { createTarGzArchiveAsync, createZipArchiveAsync } from "./archive.js";
|
|
5
|
-
import { buildProject } from "./build.js";
|
|
3
|
+
import { getPackageSlug } from "@actiondock/core";
|
|
6
4
|
import { BuilderError } from "./errors.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { replaceDirAtomic } from "./fs-utils.js";
|
|
6
|
+
import { collectRelativeFiles } from "./fs-utils.js";
|
|
9
7
|
import { SelectionPlanner } from "./planner.js";
|
|
8
|
+
import { createArchive, resolveArchiveFormat } from "./export-archive.js";
|
|
9
|
+
import { stageSourceSkill } from "./export-source.js";
|
|
10
|
+
import { exportNodeSkill } from "./export-node.js";
|
|
11
|
+
import { exportCompositeImpl } from "./export-composite.js";
|
|
12
|
+
import { exportBatchImpl } from "./export-batch.js";
|
|
10
13
|
/**
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return actions;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* 将规划产物中的 Playbook 列表适配为模板所需的 PlaybookDefinition 视图。
|
|
18
|
-
*/
|
|
19
|
-
function toPlaybookDefinitions(playbooks) {
|
|
20
|
-
return playbooks;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* 执行归档压缩操作。
|
|
24
|
-
* 先压缩到临时路径,成功后原子重命名覆盖最终路径;失败时清理临时文件后透传原始异常,
|
|
25
|
-
* 避免旧档已删、新档半成品残留的中间态。
|
|
26
|
-
*/
|
|
27
|
-
async function createArchive(skillDir, format) {
|
|
28
|
-
const parentDir = dirname(skillDir);
|
|
29
|
-
const folderName = basename(skillDir);
|
|
30
|
-
const archiveName = `${folderName}.${format === "tar.gz" ? "tar.gz" : "zip"}`;
|
|
31
|
-
const archivePath = join(parentDir, archiveName);
|
|
32
|
-
const tempArchivePath = `${archivePath}.tmp`;
|
|
33
|
-
if (existsSync(tempArchivePath)) {
|
|
34
|
-
// 清理上次失败可能残留的临时文件(兼容目录形态,避免阻塞本次压缩)
|
|
35
|
-
rmSync(tempArchivePath, { force: true, recursive: true, maxRetries: 3, retryDelay: 50 });
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
if (format === "tar.gz") {
|
|
39
|
-
await createTarGzArchiveAsync(skillDir, tempArchivePath);
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
await createZipArchiveAsync(skillDir, tempArchivePath);
|
|
43
|
-
}
|
|
44
|
-
renameSync(tempArchivePath, archivePath);
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
// 压缩失败:清理临时半成品,旧档保持原样,透传原始异常
|
|
48
|
-
if (existsSync(tempArchivePath)) {
|
|
49
|
-
rmSync(tempArchivePath, { force: true, recursive: true, maxRetries: 3, retryDelay: 50 });
|
|
50
|
-
}
|
|
51
|
-
throw new BuilderError(`Failed to create ${format} archive: ${err?.message || String(err)}`);
|
|
52
|
-
}
|
|
53
|
-
return archivePath;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* 解析归档格式:显式 archiveFormat 优先,archive 字段直接携带格式时其次。
|
|
57
|
-
*/
|
|
58
|
-
function resolveArchiveFormat(options) {
|
|
59
|
-
if (options.archiveFormat === "tar.gz" || options.archive === "tar.gz") {
|
|
60
|
-
return "tar.gz";
|
|
61
|
-
}
|
|
62
|
-
return "zip";
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* 检索单个 Action 项目目录中已存在的 SKILL.md 文件。
|
|
66
|
-
*/
|
|
67
|
-
export function findExistingSingleSkillMd(projectRoot, explicitPath, pkgSlug) {
|
|
68
|
-
if (explicitPath && existsSync(explicitPath)) {
|
|
69
|
-
try {
|
|
70
|
-
if (statSync(explicitPath).isFile())
|
|
71
|
-
return resolve(explicitPath);
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// 忽略文件属性读取异常
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const candidates = [
|
|
78
|
-
join(projectRoot, "SKILL.md"),
|
|
79
|
-
join(projectRoot, "skill.md"),
|
|
80
|
-
join(projectRoot, "skills", "SKILL.md"),
|
|
81
|
-
join(projectRoot, "skills", "skill.md"),
|
|
82
|
-
];
|
|
83
|
-
if (pkgSlug) {
|
|
84
|
-
candidates.push(join(projectRoot, "skills", pkgSlug, "SKILL.md"), join(projectRoot, "skills", pkgSlug, "skill.md"));
|
|
85
|
-
}
|
|
86
|
-
for (const candidate of candidates) {
|
|
87
|
-
if (existsSync(candidate)) {
|
|
88
|
-
try {
|
|
89
|
-
if (statSync(candidate).isFile())
|
|
90
|
-
return resolve(candidate);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
// 忽略文件属性读取异常
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* 检索复合套件工作区中已存在的 SKILL.md 文件。
|
|
101
|
-
*/
|
|
102
|
-
export function findExistingCompositeSkillMd(options) {
|
|
103
|
-
if (options.skillMdPath && existsSync(options.skillMdPath)) {
|
|
104
|
-
try {
|
|
105
|
-
if (statSync(options.skillMdPath).isFile())
|
|
106
|
-
return resolve(options.skillMdPath);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// 忽略文件属性读取异常
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const bundleSlug = getPackageSlug(options.bundleName);
|
|
113
|
-
const searchDirs = new Set();
|
|
114
|
-
if (options.workspaceRoot && existsSync(options.workspaceRoot)) {
|
|
115
|
-
searchDirs.add(resolve(options.workspaceRoot));
|
|
116
|
-
}
|
|
117
|
-
searchDirs.add(process.cwd());
|
|
118
|
-
for (const root of options.projectRoots) {
|
|
119
|
-
const absRoot = resolve(root);
|
|
120
|
-
// 仅搜索父目录(工作区根)一级;祖父目录搜索可能命中毫不相关的全局 SKILL.md 并复制进产物
|
|
121
|
-
searchDirs.add(dirname(absRoot));
|
|
122
|
-
}
|
|
123
|
-
const candidateRelativePaths = [
|
|
124
|
-
join("skills", options.bundleName, "SKILL.md"),
|
|
125
|
-
join("skills", bundleSlug, "SKILL.md"),
|
|
126
|
-
join("skills", "SKILL.md"),
|
|
127
|
-
"SKILL.md",
|
|
128
|
-
join("skills", options.bundleName, "skill.md"),
|
|
129
|
-
join("skills", bundleSlug, "skill.md"),
|
|
130
|
-
join("skills", "skill.md"),
|
|
131
|
-
"skill.md",
|
|
132
|
-
];
|
|
133
|
-
for (const dir of searchDirs) {
|
|
134
|
-
for (const rel of candidateRelativePaths) {
|
|
135
|
-
const fullPath = join(dir, rel);
|
|
136
|
-
if (existsSync(fullPath)) {
|
|
137
|
-
try {
|
|
138
|
-
if (statSync(fullPath).isFile())
|
|
139
|
-
return fullPath;
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
// 忽略文件属性读取异常
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return undefined;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* 解析复合技能自定义说明书:显式 customMdPath 优先(缺失即报错),
|
|
151
|
-
* 否则按 `SKILL.custom.md` 约定名在工作区根目录与当前目录自动发现。
|
|
152
|
-
*/
|
|
153
|
-
function resolveCustomSkillDeclaration(options) {
|
|
154
|
-
let filePath;
|
|
155
|
-
if (options.customMdPath) {
|
|
156
|
-
if (!existsSync(options.customMdPath)) {
|
|
157
|
-
throw new BuilderError(`Custom skill declaration file not found: '${options.customMdPath}'`);
|
|
158
|
-
}
|
|
159
|
-
filePath = resolve(options.customMdPath);
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
const searchDirs = [options.workspaceRoot, process.cwd()].filter(Boolean);
|
|
163
|
-
for (const dir of searchDirs) {
|
|
164
|
-
const candidate = join(resolve(dir), COMPOSITE_CUSTOM_DECLARATION_FILE);
|
|
165
|
-
if (existsSync(candidate)) {
|
|
166
|
-
filePath = candidate;
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
if (!filePath) {
|
|
172
|
-
return undefined;
|
|
173
|
-
}
|
|
174
|
-
return {
|
|
175
|
-
file: filePath,
|
|
176
|
-
...parseCustomSkillDeclaration(readFileSync(filePath, "utf-8")),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* 复制或生成 SKILL.md 到目标目录。
|
|
181
|
-
* 返回实际复用的已有 SKILL.md 路径(若发生复用)。
|
|
182
|
-
*/
|
|
183
|
-
function writeSkillMd(root, destDir, plan, options, generate) {
|
|
184
|
-
const existingSkill = options.skipSkillMd
|
|
185
|
-
? undefined
|
|
186
|
-
: findExistingSingleSkillMd(root, options.skillMdPath, options.pkgSlug);
|
|
187
|
-
if (existingSkill) {
|
|
188
|
-
const destSkillMd = join(destDir, "SKILL.md");
|
|
189
|
-
if (resolve(existingSkill) !== resolve(destSkillMd)) {
|
|
190
|
-
copyFileSync(existingSkill, destSkillMd);
|
|
191
|
-
}
|
|
192
|
-
return existingSkill;
|
|
193
|
-
}
|
|
194
|
-
if (!options.skipSkillMd) {
|
|
195
|
-
writeFileSync(join(destDir, "SKILL.md"), generate(), "utf-8");
|
|
196
|
-
}
|
|
197
|
-
return undefined;
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* 构造模板生成所需的 ProjectConfig 视图。
|
|
201
|
-
*/
|
|
202
|
-
function buildConfigForTemplates(plan, actionsDir, playbooksDir) {
|
|
203
|
-
return {
|
|
204
|
-
id: plan.packageId,
|
|
205
|
-
name: plan.packageName,
|
|
206
|
-
version: plan.version,
|
|
207
|
-
description: plan.description,
|
|
208
|
-
actionsDir,
|
|
209
|
-
playbooksDir,
|
|
210
|
-
// 声明性配置字典在规划侧为宽松结构,模板仅透传展示
|
|
211
|
-
config: plan.configDefs,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* 源码型 Skill 的暂存阶段:生成 SKILL.md、actiondock.json、package.json 并拷贝源码。
|
|
216
|
-
*/
|
|
217
|
-
function stageSourceSkill(root, skillDir, plan, options, pkgSlug) {
|
|
218
|
-
const actionsDir = plan.actionsDir || "actions";
|
|
219
|
-
const playbooksDir = plan.playbooksDir || "playbooks";
|
|
220
|
-
const playbooksDestDir = join(skillDir, playbooksDir);
|
|
221
|
-
if (plan.playbooks.length > 0) {
|
|
222
|
-
mkdirSync(playbooksDestDir, { recursive: true });
|
|
223
|
-
}
|
|
224
|
-
const configForTemplates = buildConfigForTemplates(plan, actionsDir, playbooksDir);
|
|
225
|
-
const usedExistingSkillMd = writeSkillMd(root, skillDir, plan, { skipSkillMd: options.skipSkillMd, skillMdPath: options.skillMdPath, pkgSlug }, () => generateSourceSkillMd(configForTemplates, toSkillActionItems(plan.actions), toPlaybookDefinitions(plan.playbooks)));
|
|
226
|
-
// 导出精简后的 actiondock.json 项目配置(项目元数据与清单的单一事实源)
|
|
227
|
-
const exportedConfig = serializePlanManifest(plan, {
|
|
228
|
-
omitEmptyConfig: true,
|
|
229
|
-
includeDirs: { actionsDir, playbooksDir },
|
|
230
|
-
});
|
|
231
|
-
if (exportedConfig.actions && typeof exportedConfig.actions === "object") {
|
|
232
|
-
assertValidManifestActionIds(exportedConfig.actions);
|
|
233
|
-
}
|
|
234
|
-
writeFileSync(join(skillDir, "actiondock.json"), JSON.stringify(exportedConfig, null, 2) + "\n", "utf-8");
|
|
235
|
-
// 导出 package.json
|
|
236
|
-
writeSourceSkillPkgJson(root, skillDir, plan, pkgSlug);
|
|
237
|
-
// 拷贝 tsconfig.json(若存在)
|
|
238
|
-
const tsconfigPath = join(root, "tsconfig.json");
|
|
239
|
-
if (existsSync(tsconfigPath)) {
|
|
240
|
-
copyFileSync(tsconfigPath, join(skillDir, "tsconfig.json"));
|
|
241
|
-
}
|
|
242
|
-
// 拷贝 Action 源码文件,保留相对路径(仅拷贝自有 Action,跨包依赖不物化进消费包目录)
|
|
243
|
-
for (const act of plan.actions) {
|
|
244
|
-
if (act.isExternal || act.id.includes("/")) {
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
if (existsSync(act.resolvedPath)) {
|
|
248
|
-
const destFile = join(skillDir, act.entry);
|
|
249
|
-
mkdirSync(dirname(destFile), { recursive: true });
|
|
250
|
-
copyFileSync(act.resolvedPath, destFile);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
// 拷贝静态资产与代码模块文件,保留相对路径
|
|
254
|
-
for (const dep of plan.dependencies.modulesAndAssets) {
|
|
255
|
-
if ((dep.type === "asset" || dep.type === "module" || dep.type === "file") &&
|
|
256
|
-
existsSync(dep.resolvedPath)) {
|
|
257
|
-
const destAsset = join(skillDir, dep.path);
|
|
258
|
-
mkdirSync(dirname(destAsset), { recursive: true });
|
|
259
|
-
copyFileSync(dep.resolvedPath, destAsset);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
// 拷贝 Playbook 规程文件
|
|
263
|
-
for (const pb of plan.playbooks) {
|
|
264
|
-
if (existsSync(pb.filePath)) {
|
|
265
|
-
const destPb = join(playbooksDestDir, basename(pb.filePath));
|
|
266
|
-
copyFileSync(pb.filePath, destPb);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return usedExistingSkillMd;
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* 生成源码型 Skill 的 package.json(含依赖协议校验与 workspace 版本解析)。
|
|
273
|
-
*/
|
|
274
|
-
function writeSourceSkillPkgJson(root, skillDir, plan, pkgSlug) {
|
|
275
|
-
const sanitizeDependencies = (deps) => {
|
|
276
|
-
const result = {};
|
|
277
|
-
if (deps && typeof deps === "object") {
|
|
278
|
-
for (const [k, v] of Object.entries(deps)) {
|
|
279
|
-
const verStr = String(v);
|
|
280
|
-
if (verStr.startsWith("file:")) {
|
|
281
|
-
throw new BuilderError(`Unsupported file: dependency for '${k}'. Runtime dependencies must not use file: protocol in exported skill packages.`);
|
|
282
|
-
}
|
|
283
|
-
if (k.startsWith("@actiondock/")) {
|
|
284
|
-
result[k] = getInternalDependencyVersion();
|
|
285
|
-
}
|
|
286
|
-
else if (verStr.startsWith("workspace:")) {
|
|
287
|
-
result[k] = resolveWorkspaceDepVersion(root, k, verStr);
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
result[k] = verStr;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
if (!result["@actiondock/sdk"]) {
|
|
295
|
-
result["@actiondock/sdk"] = getInternalDependencyVersion();
|
|
296
|
-
}
|
|
297
|
-
return result;
|
|
298
|
-
};
|
|
299
|
-
let exportedPkg;
|
|
300
|
-
const projectPkgPath = join(root, "package.json");
|
|
301
|
-
if (existsSync(projectPkgPath)) {
|
|
302
|
-
try {
|
|
303
|
-
const raw = readFileSync(projectPkgPath, "utf-8");
|
|
304
|
-
const parsed = JSON.parse(raw);
|
|
305
|
-
exportedPkg = {
|
|
306
|
-
name: parsed.name || pkgSlug,
|
|
307
|
-
version: plan.version || parsed.version || "0.1.0",
|
|
308
|
-
description: plan.description || parsed.description,
|
|
309
|
-
type: "module",
|
|
310
|
-
dependencies: sanitizeDependencies(parsed.dependencies),
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
catch (err) {
|
|
314
|
-
if (err instanceof BuilderError) {
|
|
315
|
-
throw err;
|
|
316
|
-
}
|
|
317
|
-
exportedPkg = fallbackSkillPkg(plan, pkgSlug);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
exportedPkg = fallbackSkillPkg(plan, pkgSlug);
|
|
322
|
-
}
|
|
323
|
-
writeFileSync(join(skillDir, "package.json"), JSON.stringify(exportedPkg, null, 2) + "\n", "utf-8");
|
|
324
|
-
}
|
|
325
|
-
/** 无 package.json 或解析失败时的兜底导出描述 */
|
|
326
|
-
function fallbackSkillPkg(plan, pkgSlug) {
|
|
327
|
-
return {
|
|
328
|
-
name: pkgSlug,
|
|
329
|
-
version: plan.version,
|
|
330
|
-
description: plan.description,
|
|
331
|
-
type: "module",
|
|
332
|
-
dependencies: { "@actiondock/sdk": getInternalDependencyVersion() },
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* 复合套件聚合各子包依赖并生成复合根目录 package.json。
|
|
337
|
-
*/
|
|
338
|
-
function writeCompositePkgJson(stagingDir, packagesDestDir, packageInfos, bundleSlug, options) {
|
|
339
|
-
const aggregatedDeps = {
|
|
340
|
-
"@actiondock/sdk": getInternalDependencyVersion(),
|
|
341
|
-
};
|
|
342
|
-
for (const info of packageInfos) {
|
|
343
|
-
const pkgJsonPath = join(packagesDestDir, info.packageDir, "package.json");
|
|
344
|
-
if (existsSync(pkgJsonPath)) {
|
|
345
|
-
try {
|
|
346
|
-
const raw = readFileSync(pkgJsonPath, "utf-8");
|
|
347
|
-
const parsed = JSON.parse(raw);
|
|
348
|
-
if (parsed.dependencies && typeof parsed.dependencies === "object") {
|
|
349
|
-
assertNoFileProtocolDeps(parsed.dependencies, "exported skill packages");
|
|
350
|
-
for (const [dep, ver] of Object.entries(parsed.dependencies)) {
|
|
351
|
-
const verStr = String(ver);
|
|
352
|
-
if (dep.startsWith("@actiondock/")) {
|
|
353
|
-
aggregatedDeps[dep] = getInternalDependencyVersion();
|
|
354
|
-
}
|
|
355
|
-
else if (verStr.startsWith("workspace:")) {
|
|
356
|
-
aggregatedDeps[dep] = resolveWorkspaceDepVersion(join(packagesDestDir, info.packageDir), dep, verStr);
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
aggregatedDeps[dep] = verStr;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
catch (err) {
|
|
365
|
-
if (err instanceof BuilderError)
|
|
366
|
-
throw err;
|
|
367
|
-
// 复合导出缺依赖影响面大:读文件与解析失败必须显式报错,严禁静默吞掉导致聚合依赖缺失
|
|
368
|
-
throw new BuilderError(`Failed to read or parse package.json of exported subpackage '${info.packageDir}' (${pkgJsonPath}): ${err instanceof Error ? err.message : String(err)}`);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
const compositePkg = {
|
|
373
|
-
name: `${bundleSlug}-composite-skill`,
|
|
374
|
-
version: "0.1.0",
|
|
375
|
-
description: options.description || `Composite Skill suite: ${options.bundleName}`,
|
|
376
|
-
type: "module",
|
|
377
|
-
workspaces: ["packages/*"],
|
|
378
|
-
dependencies: aggregatedDeps,
|
|
379
|
-
};
|
|
380
|
-
writeFileSync(join(stagingDir, "package.json"), JSON.stringify(compositePkg, null, 2) + "\n", "utf-8");
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Agent Skill 导出器。
|
|
384
|
-
* 负责源码型 Skill 与 Node 目录型 Skill 的构建、打包与分发。
|
|
14
|
+
* Agent Skill 导出器门面。
|
|
15
|
+
* 仅承担编排职责:单包模式分派、复合/批量流程装配与结果组装,
|
|
16
|
+
* 各导出模式的具体装配逻辑由 export-* 单一职责模块承担。
|
|
385
17
|
*/
|
|
386
18
|
export class SkillExporter {
|
|
387
19
|
/**
|
|
@@ -451,47 +83,10 @@ export class SkillExporter {
|
|
|
451
83
|
};
|
|
452
84
|
}
|
|
453
85
|
if (mode === "node") {
|
|
454
|
-
return
|
|
86
|
+
return exportNodeSkill(root, targetSkillDir, plan, options, pkgSlug);
|
|
455
87
|
}
|
|
456
88
|
return this.exportSourceSkill(root, targetSkillDir, plan, options, pkgSlug);
|
|
457
89
|
}
|
|
458
|
-
/**
|
|
459
|
-
* Node 目录型 Skill 导出:复用 Node 目录型构建,并在 Skill 内生成调用该入口的 SKILL.md。
|
|
460
|
-
*/
|
|
461
|
-
async exportNodeSkill(root, targetSkillDir, plan, options, pkgSlug) {
|
|
462
|
-
await buildProject({
|
|
463
|
-
projectRoot: root,
|
|
464
|
-
outDir: targetSkillDir,
|
|
465
|
-
actions: options.actions,
|
|
466
|
-
playbooks: options.playbooks,
|
|
467
|
-
config: options.config,
|
|
468
|
-
manifest: options.manifest,
|
|
469
|
-
vendorDeps: Boolean(options.vendorDeps),
|
|
470
|
-
allowInstallScripts: options.allowInstallScripts,
|
|
471
|
-
requireReproducible: options.requireReproducible,
|
|
472
|
-
});
|
|
473
|
-
const usedExistingSkillMd = writeSkillMd(root, targetSkillDir, plan, { skipSkillMd: options.skipSkillMd, skillMdPath: options.skillMdPath, pkgSlug }, () => {
|
|
474
|
-
const configForTemplates = buildConfigForTemplates(plan, plan.actionsDir || "actions", plan.playbooksDir || "playbooks");
|
|
475
|
-
return generateStandaloneSkillMd(configForTemplates, toSkillActionItems(plan.actions), toPlaybookDefinitions(plan.playbooks), "node ./entry.mjs");
|
|
476
|
-
});
|
|
477
|
-
let archivePath;
|
|
478
|
-
if (options.archive) {
|
|
479
|
-
archivePath = await createArchive(targetSkillDir, resolveArchiveFormat(options));
|
|
480
|
-
}
|
|
481
|
-
return {
|
|
482
|
-
packageId: plan.packageId,
|
|
483
|
-
version: plan.version,
|
|
484
|
-
mode: "node",
|
|
485
|
-
skillDir: targetSkillDir,
|
|
486
|
-
archivePath,
|
|
487
|
-
actionsCount: plan.actions.length,
|
|
488
|
-
playbooksCount: plan.playbooks.length,
|
|
489
|
-
actions: plan.actions.map((a) => a.id),
|
|
490
|
-
playbooks: plan.playbooks.map((p) => p.id),
|
|
491
|
-
files: collectRelativeFiles(targetSkillDir),
|
|
492
|
-
usedExistingSkillMd,
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
90
|
/**
|
|
496
91
|
* 源码型 Skill 导出:暂存目录内完成清单、说明书与源码装配后原子替换目标目录。
|
|
497
92
|
*/
|
|
@@ -529,255 +124,41 @@ export class SkillExporter {
|
|
|
529
124
|
usedExistingSkillMd,
|
|
530
125
|
};
|
|
531
126
|
}
|
|
532
|
-
/**
|
|
533
|
-
* 静态辅助调用单个 Skill 导出方法。
|
|
534
|
-
* @deprecated 实例方法的历史便捷入口,请直接使用实例方法,将在两个版本后移除。
|
|
535
|
-
*/
|
|
536
|
-
static async export(options) {
|
|
537
|
-
const exporter = new SkillExporter();
|
|
538
|
-
return exporter.export(options);
|
|
539
|
-
}
|
|
540
127
|
/**
|
|
541
128
|
* 批量导出多个 Skill。
|
|
542
129
|
*/
|
|
543
130
|
async exportBatch(options) {
|
|
544
|
-
|
|
545
|
-
throw new BuilderError("No project roots provided for batch skill export.");
|
|
546
|
-
}
|
|
547
|
-
const results = [];
|
|
548
|
-
const baseOutDir = resolve(options.outDir || join(process.cwd(), "dist", "skills"));
|
|
549
|
-
mkdirSync(baseOutDir, { recursive: true });
|
|
550
|
-
const usedDirNames = new Set();
|
|
551
|
-
for (const projectRoot of options.projectRoots) {
|
|
552
|
-
const config = loadProjectConfig(projectRoot);
|
|
553
|
-
let pkgSlug = getPackageSlug(config.id);
|
|
554
|
-
if (usedDirNames.has(pkgSlug)) {
|
|
555
|
-
pkgSlug = config.id.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/^-+|-+$/g, "");
|
|
556
|
-
}
|
|
557
|
-
usedDirNames.add(pkgSlug);
|
|
558
|
-
const pkgOutDir = join(baseOutDir, `${pkgSlug}-skill`);
|
|
559
|
-
const res = await this.export({
|
|
560
|
-
...options,
|
|
561
|
-
projectRoot,
|
|
562
|
-
outDir: pkgOutDir,
|
|
563
|
-
});
|
|
564
|
-
results.push(res);
|
|
565
|
-
}
|
|
566
|
-
const totalActions = results.reduce((sum, r) => sum + r.actionsCount, 0);
|
|
567
|
-
const totalPlaybooks = results.reduce((sum, r) => sum + r.playbooksCount, 0);
|
|
568
|
-
return {
|
|
569
|
-
results,
|
|
570
|
-
outDir: baseOutDir,
|
|
571
|
-
totalActions,
|
|
572
|
-
totalPlaybooks,
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
/**
|
|
576
|
-
* 静态辅助调用批量导出方法。
|
|
577
|
-
* @deprecated 实例方法的历史便捷入口,请直接使用实例方法,将在两个版本后移除。
|
|
578
|
-
*/
|
|
579
|
-
static async exportBatch(options) {
|
|
580
|
-
const exporter = new SkillExporter();
|
|
581
|
-
return exporter.exportBatch(options);
|
|
131
|
+
return exportBatchImpl(options, (opts) => this.export(opts));
|
|
582
132
|
}
|
|
583
133
|
/**
|
|
584
134
|
* 复合模式导出:将多个包聚合为一个统一的复合技能目录。
|
|
585
135
|
*/
|
|
586
136
|
async exportComposite(options) {
|
|
587
|
-
|
|
588
|
-
throw new BuilderError("No project roots provided for composite skill export.");
|
|
589
|
-
}
|
|
590
|
-
if (!options.bundleName || !options.bundleName.trim()) {
|
|
591
|
-
throw new BuilderError("bundleName is required for composite skill export.");
|
|
592
|
-
}
|
|
593
|
-
const skillMdOnly = options.skillMdOnly === true;
|
|
594
|
-
const customDeclaration = resolveCustomSkillDeclaration(options);
|
|
595
|
-
const bundleSlug = getPackageSlug(options.bundleName);
|
|
596
|
-
const defaultSkillDir = join(process.cwd(), "dist", `${bundleSlug}-skill`);
|
|
597
|
-
const targetSkillDir = resolve(options.outDir || defaultSkillDir);
|
|
598
|
-
const parentDir = dirname(targetSkillDir);
|
|
599
|
-
mkdirSync(parentDir, { recursive: true });
|
|
600
|
-
const stagingDir = skillMdOnly
|
|
601
|
-
? null
|
|
602
|
-
: join(parentDir, `.tmp-composite-${bundleSlug}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
603
|
-
if (stagingDir) {
|
|
604
|
-
mkdirSync(stagingDir, { recursive: true });
|
|
605
|
-
}
|
|
606
|
-
const packagesDestDir = stagingDir ? join(stagingDir, "packages") : null;
|
|
607
|
-
if (packagesDestDir) {
|
|
608
|
-
mkdirSync(packagesDestDir, { recursive: true });
|
|
609
|
-
}
|
|
610
|
-
const packageInfos = [];
|
|
611
|
-
const packageSummaries = [];
|
|
612
|
-
let existingSkillPath;
|
|
613
|
-
try {
|
|
614
|
-
// 收集并展开所有包含在依赖闭包中的外部项目根目录,确保 bundle 整包携带齐全部依赖
|
|
615
|
-
const effectiveRoots = new Set();
|
|
616
|
-
for (const r of options.projectRoots) {
|
|
617
|
-
effectiveRoots.add(resolve(r));
|
|
618
|
-
if (!skillMdOnly) {
|
|
619
|
-
try {
|
|
620
|
-
const p = SelectionPlanner.plan({ projectRoot: r });
|
|
621
|
-
if (p.externalProjectRoots) {
|
|
622
|
-
for (const extRoot of p.externalProjectRoots) {
|
|
623
|
-
effectiveRoots.add(resolve(extRoot));
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
catch {
|
|
628
|
-
// 忽略规划解析异常,由主循环处理
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
const usedDirNames = new Set();
|
|
633
|
-
for (const projectRoot of effectiveRoots) {
|
|
634
|
-
const config = loadProjectConfig(projectRoot);
|
|
635
|
-
let pkgSlug;
|
|
636
|
-
if (skillMdOnly) {
|
|
637
|
-
// 就地生成:规程链接基于工作区实际子目录名
|
|
638
|
-
pkgSlug = basename(resolve(projectRoot));
|
|
639
|
-
}
|
|
640
|
-
else {
|
|
641
|
-
pkgSlug = getPackageSlug(config.id);
|
|
642
|
-
if (usedDirNames.has(pkgSlug)) {
|
|
643
|
-
pkgSlug = config.id.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/^-+|-+$/g, "");
|
|
644
|
-
}
|
|
645
|
-
usedDirNames.add(pkgSlug);
|
|
646
|
-
}
|
|
647
|
-
let actionIds;
|
|
648
|
-
let playbookIds;
|
|
649
|
-
let manifest;
|
|
650
|
-
if (skillMdOnly) {
|
|
651
|
-
// 仅生成 SKILL.md:直接读取清单获取 Action 契约描述,不拷贝子包产物
|
|
652
|
-
manifest = loadManifest(projectRoot);
|
|
653
|
-
actionIds = manifest?.actions ? Object.keys(manifest.actions) : [];
|
|
654
|
-
playbookIds = Array.from(loadPlaybooks(projectRoot, config.playbooksDir).keys());
|
|
655
|
-
}
|
|
656
|
-
else {
|
|
657
|
-
const destPkgDir = join(packagesDestDir, pkgSlug);
|
|
658
|
-
const singleExport = await this.export({
|
|
659
|
-
projectRoot,
|
|
660
|
-
outDir: destPkgDir,
|
|
661
|
-
archive: false,
|
|
662
|
-
skipSkillMd: true,
|
|
663
|
-
mode: "source",
|
|
664
|
-
_isSubpackage: true,
|
|
665
|
-
});
|
|
666
|
-
manifest = loadManifest(destPkgDir) || loadManifest(projectRoot);
|
|
667
|
-
actionIds = singleExport.actions;
|
|
668
|
-
playbookIds = singleExport.playbooks;
|
|
669
|
-
}
|
|
670
|
-
const playbooks = loadPlaybooks(projectRoot, config.playbooksDir);
|
|
671
|
-
const actionEntries = actionIds.map((actId) => ({
|
|
672
|
-
id: actId,
|
|
673
|
-
description: manifest?.actions?.[actId]?.description,
|
|
674
|
-
}));
|
|
675
|
-
packageInfos.push({
|
|
676
|
-
config,
|
|
677
|
-
actions: actionEntries,
|
|
678
|
-
playbooks: Array.from(playbooks.values()),
|
|
679
|
-
packageDir: pkgSlug,
|
|
680
|
-
});
|
|
681
|
-
packageSummaries.push({
|
|
682
|
-
packageId: config.id,
|
|
683
|
-
actions: actionIds,
|
|
684
|
-
playbooks: playbookIds,
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
existingSkillPath = skillMdOnly ? undefined : findExistingCompositeSkillMd(options);
|
|
688
|
-
const description = options.description ||
|
|
689
|
-
customDeclaration?.description ||
|
|
690
|
-
`ActionDock 复合技能套件,聚合 ${packageInfos.map((p) => p.config.name).join("、")}`;
|
|
691
|
-
const compositeSkillMd = generateCompositeSkillMd(options.bundleName, description, packageInfos, {
|
|
692
|
-
customSections: customDeclaration?.sections,
|
|
693
|
-
packagesBaseDir: skillMdOnly ? "." : "packages",
|
|
694
|
-
});
|
|
695
|
-
if (skillMdOnly) {
|
|
696
|
-
// 就地仅重生成 SKILL.md:始终重新生成(忽略已有 SKILL.md 复用逻辑),不产出完整套件目录
|
|
697
|
-
const targetFile = options.outDir && options.outDir.toLowerCase().endsWith(".md")
|
|
698
|
-
? resolve(options.outDir)
|
|
699
|
-
: resolve(options.outDir ? join(options.outDir, "SKILL.md") : join(process.cwd(), "SKILL.md"));
|
|
700
|
-
mkdirSync(dirname(targetFile), { recursive: true });
|
|
701
|
-
// 目标文件即将被覆盖时输出警告,避免无提示覆盖用户已有内容
|
|
702
|
-
if (existsSync(targetFile)) {
|
|
703
|
-
console.warn(`[WARN] Existing SKILL.md will be overwritten: ${targetFile}`);
|
|
704
|
-
}
|
|
705
|
-
writeFileSync(targetFile, compositeSkillMd, "utf-8");
|
|
706
|
-
return {
|
|
707
|
-
bundleName: options.bundleName,
|
|
708
|
-
skillDir: dirname(targetFile),
|
|
709
|
-
skillMdFile: targetFile,
|
|
710
|
-
packagesCount: packageInfos.length,
|
|
711
|
-
actionsCount: packageInfos.reduce((acc, p) => acc + p.actions.length, 0),
|
|
712
|
-
playbooksCount: packageInfos.reduce((acc, p) => acc + p.playbooks.length, 0),
|
|
713
|
-
packages: packageSummaries,
|
|
714
|
-
files: [basename(targetFile)],
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
if (existingSkillPath) {
|
|
718
|
-
const destSkillMdPath = join(stagingDir, "SKILL.md");
|
|
719
|
-
if (resolve(existingSkillPath) !== resolve(destSkillMdPath)) {
|
|
720
|
-
copyFileSync(existingSkillPath, destSkillMdPath);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
else {
|
|
724
|
-
writeFileSync(join(stagingDir, "SKILL.md"), compositeSkillMd, "utf-8");
|
|
725
|
-
}
|
|
726
|
-
// 聚合所有子包依赖生成复合根目录 package.json
|
|
727
|
-
writeCompositePkgJson(stagingDir, packagesDestDir, packageInfos, bundleSlug, options);
|
|
728
|
-
// 原子替换目标复合技能目录
|
|
729
|
-
await replaceDirAtomic(stagingDir, targetSkillDir);
|
|
730
|
-
}
|
|
731
|
-
finally {
|
|
732
|
-
if (stagingDir && existsSync(stagingDir)) {
|
|
733
|
-
rmSync(stagingDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
let archivePath;
|
|
737
|
-
if (options.archive) {
|
|
738
|
-
archivePath = await createArchive(targetSkillDir, resolveArchiveFormat(options));
|
|
739
|
-
}
|
|
740
|
-
return {
|
|
741
|
-
bundleName: options.bundleName,
|
|
742
|
-
skillDir: targetSkillDir,
|
|
743
|
-
archivePath,
|
|
744
|
-
packagesCount: packageInfos.length,
|
|
745
|
-
actionsCount: packageInfos.reduce((acc, p) => acc + p.actions.length, 0),
|
|
746
|
-
playbooksCount: packageInfos.reduce((acc, p) => acc + p.playbooks.length, 0),
|
|
747
|
-
packages: packageSummaries,
|
|
748
|
-
files: collectRelativeFiles(targetSkillDir),
|
|
749
|
-
// 复用中段已计算的路径,避免结尾重复扫描
|
|
750
|
-
usedExistingSkillMd: existingSkillPath,
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
/**
|
|
754
|
-
* 静态辅助调用复合导出方法。
|
|
755
|
-
* @deprecated 实例方法的历史便捷入口,请直接使用实例方法,将在两个版本后移除。
|
|
756
|
-
*/
|
|
757
|
-
static async exportComposite(options) {
|
|
758
|
-
const exporter = new SkillExporter();
|
|
759
|
-
return exporter.exportComposite(options);
|
|
137
|
+
return exportCompositeImpl(options, (opts) => this.export(opts));
|
|
760
138
|
}
|
|
761
139
|
}
|
|
762
140
|
/**
|
|
763
|
-
* 导出单个 Skill
|
|
764
|
-
*
|
|
141
|
+
* 导出单个 Skill 产物的顶层公共入口函数。
|
|
142
|
+
* 内部创建 SkillExporter 实例并委托其实例导出方法,适合常规单次导出场景;
|
|
143
|
+
* 需要复用导出器或定制行为时可直接实例化 SkillExporter。
|
|
765
144
|
*/
|
|
766
145
|
export async function exportSkill(options) {
|
|
767
146
|
const exporter = new SkillExporter();
|
|
768
147
|
return exporter.export(options);
|
|
769
148
|
}
|
|
770
149
|
/**
|
|
771
|
-
* 批量导出多个 Skill
|
|
772
|
-
*
|
|
150
|
+
* 批量导出多个 Skill 产物的顶层公共入口函数。
|
|
151
|
+
* 内部创建 SkillExporter 实例并委托其实例批量导出方法。
|
|
773
152
|
*/
|
|
774
153
|
export async function exportSkillBatch(options) {
|
|
775
|
-
|
|
154
|
+
const exporter = new SkillExporter();
|
|
155
|
+
return exporter.exportBatch(options);
|
|
776
156
|
}
|
|
777
157
|
/**
|
|
778
|
-
*
|
|
779
|
-
*
|
|
158
|
+
* 导出复合技能套件的顶层公共入口函数。
|
|
159
|
+
* 内部创建 SkillExporter 实例并委托其实例复合导出方法。
|
|
780
160
|
*/
|
|
781
161
|
export async function exportCompositeSkill(options) {
|
|
782
|
-
|
|
162
|
+
const exporter = new SkillExporter();
|
|
163
|
+
return exporter.exportComposite(options);
|
|
783
164
|
}
|