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