@actiondock/builder 2.0.9 → 2.0.11-beta.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/package.json +3 -3
- package/src/build.ts +106 -0
- package/src/exporter.ts +349 -76
- package/src/index.ts +1 -0
- package/src/planner.ts +284 -6
- package/src/types.ts +75 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actiondock/builder",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11-beta.0",
|
|
4
4
|
"description": "ActionDock Build Planner, Bun standalone compiler, and Skill exporter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"test": "bun test"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@actiondock/core": "
|
|
31
|
-
"@actiondock/sdk": "
|
|
30
|
+
"@actiondock/core": "2.0.11-beta.0",
|
|
31
|
+
"@actiondock/sdk": "2.0.11-beta.0",
|
|
32
32
|
"yaml": "^2.7.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { generateStandaloneEntrypoint, getPackageSlug } from "@actiondock/core";
|
|
4
|
+
import { BunCompiler } from "./compiler";
|
|
5
|
+
import { BuilderError } from "./errors";
|
|
6
|
+
import { BuildPlanner } from "./planner";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 独立二进制可执行文件构建选项。
|
|
10
|
+
*/
|
|
11
|
+
export interface BuildOptions {
|
|
12
|
+
/** 目标项目根目录 */
|
|
13
|
+
projectRoot: string;
|
|
14
|
+
/** 目标架构(如 "bun-linux-x64", "bun-darwin-arm64", "bun-windows-x64" 等) */
|
|
15
|
+
target?: string;
|
|
16
|
+
/** 输出可执行文件的目标路径(默认输出到 dist/ 目录) */
|
|
17
|
+
outfile?: string;
|
|
18
|
+
/** 是否开启代码压缩混淆(默认 true) */
|
|
19
|
+
minify?: boolean;
|
|
20
|
+
/** 是否编译为 V8/JavaScriptCore 字节码(默认 true) */
|
|
21
|
+
bytecode?: boolean;
|
|
22
|
+
/** 显式挑选打包的 Action ID 清单(用于按需子集打包) */
|
|
23
|
+
actions?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 独立二进制构建完成后的元数据结果对象。
|
|
28
|
+
*/
|
|
29
|
+
export interface BuildResult {
|
|
30
|
+
/** 所属 Package ID */
|
|
31
|
+
packageId: string;
|
|
32
|
+
/** 打包的项目版本号 */
|
|
33
|
+
version: string;
|
|
34
|
+
/** 编译的目标平台架构 */
|
|
35
|
+
target: string;
|
|
36
|
+
/** 生成的独立二进制可执行文件绝对路径 */
|
|
37
|
+
executablePath: string;
|
|
38
|
+
/** 生成的 sidecar 元数据 JSON 文件绝对路径 */
|
|
39
|
+
metadataPath: string;
|
|
40
|
+
/** 打包内置的 Action ID 列表 */
|
|
41
|
+
actions: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 调用 Bun 原生编译引擎(Bun.build --compile)将 Action Package 打包为零外部依赖的独立二进制可执行文件。
|
|
46
|
+
*
|
|
47
|
+
* @param options 构建参数
|
|
48
|
+
* @returns 构建产物结果元数据
|
|
49
|
+
*/
|
|
50
|
+
export async function buildProject(options: BuildOptions): Promise<BuildResult> {
|
|
51
|
+
const root = resolve(options.projectRoot);
|
|
52
|
+
const plan = BuildPlanner.plan({
|
|
53
|
+
projectRoot: root,
|
|
54
|
+
actions: options.actions,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (plan.actions.length === 0) {
|
|
58
|
+
throw new BuilderError("No actions resolved for standalone compilation");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const buildDir = join(root, ".actiondock", ".build");
|
|
62
|
+
mkdirSync(buildDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
const entryCode = generateStandaloneEntrypoint(
|
|
65
|
+
plan.packageId,
|
|
66
|
+
plan.version,
|
|
67
|
+
plan.description,
|
|
68
|
+
plan.actions.map((a) => ({ id: a.id, filePath: a.resolvedPath })),
|
|
69
|
+
plan.configDefs
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const entryFileName = `entry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.ts`;
|
|
73
|
+
const entryPath = join(buildDir, entryFileName);
|
|
74
|
+
writeFileSync(entryPath, entryCode, "utf-8");
|
|
75
|
+
|
|
76
|
+
const binaryName = getPackageSlug(plan.packageId);
|
|
77
|
+
const defaultOutfile = join(root, "dist", binaryName);
|
|
78
|
+
const outfile = resolve(options.outfile || defaultOutfile);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const result = await BunCompiler.compile({
|
|
82
|
+
entrypoint: entryPath,
|
|
83
|
+
outfile,
|
|
84
|
+
target: options.target,
|
|
85
|
+
minify: options.minify,
|
|
86
|
+
bytecode: options.bytecode,
|
|
87
|
+
cwd: root,
|
|
88
|
+
packageId: plan.packageId,
|
|
89
|
+
version: plan.version,
|
|
90
|
+
actions: plan.actions.map((a) => a.id),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
packageId: result.packageId || plan.packageId,
|
|
95
|
+
version: result.version || plan.version,
|
|
96
|
+
target: result.target,
|
|
97
|
+
executablePath: result.executablePath,
|
|
98
|
+
metadataPath: result.metadataPath || "",
|
|
99
|
+
actions: plan.actions.map((a) => a.id),
|
|
100
|
+
};
|
|
101
|
+
} finally {
|
|
102
|
+
if (existsSync(entryPath)) {
|
|
103
|
+
rmSync(entryPath, { force: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/exporter.ts
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
generateCompositeSkillMd,
|
|
5
5
|
generateSourceSkillMd,
|
|
6
6
|
generateStandaloneEntrypoint,
|
|
7
7
|
generateStandaloneSkillMd,
|
|
8
8
|
getPackageSlug,
|
|
9
|
+
loadManifest,
|
|
10
|
+
loadPlaybooks,
|
|
11
|
+
loadProjectConfig,
|
|
12
|
+
type CompositeSkillPackageInfo,
|
|
9
13
|
type ProjectConfig,
|
|
10
14
|
} from "@actiondock/core";
|
|
11
15
|
import { createTarGzArchive, createZipArchive } from "./archive";
|
|
12
16
|
import { BunCompiler } from "./compiler";
|
|
13
17
|
import { BuilderError } from "./errors";
|
|
14
18
|
import { BuildPlanner } from "./planner";
|
|
15
|
-
import type {
|
|
19
|
+
import type {
|
|
20
|
+
ArchiveFormat,
|
|
21
|
+
BatchSkillExportOptions,
|
|
22
|
+
BatchSkillExportResult,
|
|
23
|
+
CompositeSkillExportOptions,
|
|
24
|
+
CompositeSkillExportResult,
|
|
25
|
+
SkillExporterOptions,
|
|
26
|
+
SkillExportResult,
|
|
27
|
+
} from "./types";
|
|
16
28
|
|
|
17
29
|
/**
|
|
18
30
|
* 递归扫描生成目录中的所有相对文件路径。
|
|
@@ -65,6 +77,105 @@ function createArchive(
|
|
|
65
77
|
return archivePath;
|
|
66
78
|
}
|
|
67
79
|
|
|
80
|
+
/**
|
|
81
|
+
* 检索单个 Action 项目目录中已存在的 SKILL.md 文件。
|
|
82
|
+
*/
|
|
83
|
+
export function findExistingSingleSkillMd(
|
|
84
|
+
projectRoot: string,
|
|
85
|
+
explicitPath?: string,
|
|
86
|
+
pkgSlug?: string
|
|
87
|
+
): string | undefined {
|
|
88
|
+
if (explicitPath && existsSync(explicitPath)) {
|
|
89
|
+
try {
|
|
90
|
+
if (statSync(explicitPath).isFile()) return resolve(explicitPath);
|
|
91
|
+
} catch {
|
|
92
|
+
// 忽略文件属性读取异常
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const candidates = [
|
|
97
|
+
join(projectRoot, "SKILL.md"),
|
|
98
|
+
join(projectRoot, "skill.md"),
|
|
99
|
+
join(projectRoot, "skills", "SKILL.md"),
|
|
100
|
+
join(projectRoot, "skills", "skill.md"),
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
if (pkgSlug) {
|
|
104
|
+
candidates.push(
|
|
105
|
+
join(projectRoot, "skills", pkgSlug, "SKILL.md"),
|
|
106
|
+
join(projectRoot, "skills", pkgSlug, "skill.md")
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const candidate of candidates) {
|
|
111
|
+
if (existsSync(candidate)) {
|
|
112
|
+
try {
|
|
113
|
+
if (statSync(candidate).isFile()) return resolve(candidate);
|
|
114
|
+
} catch {
|
|
115
|
+
// 忽略
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 检索复合套件工作区中已存在的 SKILL.md 文件。
|
|
125
|
+
*/
|
|
126
|
+
export function findExistingCompositeSkillMd(
|
|
127
|
+
options: CompositeSkillExportOptions
|
|
128
|
+
): string | undefined {
|
|
129
|
+
if (options.skillMdPath && existsSync(options.skillMdPath)) {
|
|
130
|
+
try {
|
|
131
|
+
if (statSync(options.skillMdPath).isFile()) return resolve(options.skillMdPath);
|
|
132
|
+
} catch {
|
|
133
|
+
// 忽略
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const bundleSlug = getPackageSlug(options.bundleName);
|
|
138
|
+
const searchDirs = new Set<string>();
|
|
139
|
+
|
|
140
|
+
if (options.workspaceRoot && existsSync(options.workspaceRoot)) {
|
|
141
|
+
searchDirs.add(resolve(options.workspaceRoot));
|
|
142
|
+
}
|
|
143
|
+
searchDirs.add(process.cwd());
|
|
144
|
+
|
|
145
|
+
for (const root of options.projectRoots) {
|
|
146
|
+
const absRoot = resolve(root);
|
|
147
|
+
const parentDir = dirname(absRoot);
|
|
148
|
+
searchDirs.add(parentDir);
|
|
149
|
+
searchDirs.add(dirname(parentDir));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const candidateRelativePaths = [
|
|
153
|
+
join("skills", options.bundleName, "SKILL.md"),
|
|
154
|
+
join("skills", bundleSlug, "SKILL.md"),
|
|
155
|
+
join("skills", "SKILL.md"),
|
|
156
|
+
"SKILL.md",
|
|
157
|
+
join("skills", options.bundleName, "skill.md"),
|
|
158
|
+
join("skills", bundleSlug, "skill.md"),
|
|
159
|
+
join("skills", "skill.md"),
|
|
160
|
+
"skill.md",
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
for (const dir of searchDirs) {
|
|
164
|
+
for (const rel of candidateRelativePaths) {
|
|
165
|
+
const fullPath = join(dir, rel);
|
|
166
|
+
if (existsSync(fullPath)) {
|
|
167
|
+
try {
|
|
168
|
+
if (statSync(fullPath).isFile()) return fullPath;
|
|
169
|
+
} catch {
|
|
170
|
+
// 忽略
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
68
179
|
/**
|
|
69
180
|
* Agent Skill 导出器。
|
|
70
181
|
* 负责源码型 Skill 与独立二进制型 Skill 的构建、打包与归档分发。
|
|
@@ -115,51 +226,32 @@ export class SkillExporter {
|
|
|
115
226
|
config: plan.configDefs as any,
|
|
116
227
|
};
|
|
117
228
|
|
|
229
|
+
let usedExistingSkillMd: string | undefined;
|
|
230
|
+
|
|
118
231
|
if (mode === "source") {
|
|
119
232
|
// ----------------------------------------------------
|
|
120
233
|
// 源码 Skill 导出 (Source Skill Export)
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
actions: plan.actions.map((a) => ({
|
|
141
|
-
id: a.id,
|
|
142
|
-
entry: a.entry,
|
|
143
|
-
description: a.description,
|
|
144
|
-
inputSchema: a.inputSchema,
|
|
145
|
-
outputSchema: a.outputSchema,
|
|
146
|
-
uses: a.uses,
|
|
147
|
-
tags: a.tags,
|
|
148
|
-
})),
|
|
149
|
-
playbooks: plan.playbooks.map((p) => ({
|
|
150
|
-
id: p.id,
|
|
151
|
-
description: p.description,
|
|
152
|
-
entry: `playbooks/${basename(p.filePath)}`,
|
|
153
|
-
})),
|
|
154
|
-
exportedAt: new Date().toISOString(),
|
|
155
|
-
};
|
|
156
|
-
writeFileSync(
|
|
157
|
-
join(skillDir, "actiondock.skill.json"),
|
|
158
|
-
JSON.stringify(skillJsonContent, null, 2) + "\n",
|
|
159
|
-
"utf-8"
|
|
160
|
-
);
|
|
234
|
+
// 优先复用当前动作目录下已有的 SKILL.md;若不存在且未显式跳过,则动态生成
|
|
235
|
+
const existingSkill = !options.skipSkillMd
|
|
236
|
+
? findExistingSingleSkillMd(root, options.skillMdPath, pkgSlug)
|
|
237
|
+
: undefined;
|
|
238
|
+
|
|
239
|
+
if (existingSkill) {
|
|
240
|
+
usedExistingSkillMd = existingSkill;
|
|
241
|
+
const destSkillMd = join(skillDir, "SKILL.md");
|
|
242
|
+
if (resolve(existingSkill) !== resolve(destSkillMd)) {
|
|
243
|
+
copyFileSync(existingSkill, destSkillMd);
|
|
244
|
+
}
|
|
245
|
+
} else if (!options.skipSkillMd) {
|
|
246
|
+
const skillMd = generateSourceSkillMd(
|
|
247
|
+
configForTemplates,
|
|
248
|
+
plan.actions as any,
|
|
249
|
+
plan.playbooks as any
|
|
250
|
+
);
|
|
251
|
+
writeFileSync(join(skillDir, "SKILL.md"), skillMd, "utf-8");
|
|
252
|
+
}
|
|
161
253
|
|
|
162
|
-
//
|
|
254
|
+
// - 导出精简后的 actiondock.manifest.json 清单
|
|
163
255
|
const manifestActions: Record<string, unknown> = {};
|
|
164
256
|
for (const a of plan.actions) {
|
|
165
257
|
manifestActions[a.id] = {
|
|
@@ -171,6 +263,12 @@ export class SkillExporter {
|
|
|
171
263
|
tags: a.tags,
|
|
172
264
|
annotations: a.annotations,
|
|
173
265
|
};
|
|
266
|
+
if (a.id.includes("/")) {
|
|
267
|
+
const shortId = a.id.slice(a.id.lastIndexOf("/") + 1);
|
|
268
|
+
if (!manifestActions[shortId]) {
|
|
269
|
+
manifestActions[shortId] = manifestActions[a.id];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
174
272
|
}
|
|
175
273
|
const exportedManifest = {
|
|
176
274
|
schemaVersion: 1,
|
|
@@ -183,7 +281,7 @@ export class SkillExporter {
|
|
|
183
281
|
"utf-8"
|
|
184
282
|
);
|
|
185
283
|
|
|
186
|
-
//
|
|
284
|
+
// - 导出精简后的 actiondock.json 项目配置
|
|
187
285
|
const exportedConfig = {
|
|
188
286
|
id: plan.packageId,
|
|
189
287
|
name: plan.packageName,
|
|
@@ -199,7 +297,7 @@ export class SkillExporter {
|
|
|
199
297
|
"utf-8"
|
|
200
298
|
);
|
|
201
299
|
|
|
202
|
-
//
|
|
300
|
+
// - 导出 package.json
|
|
203
301
|
const projectPkgPath = join(root, "package.json");
|
|
204
302
|
let exportedPkg: any;
|
|
205
303
|
if (existsSync(projectPkgPath)) {
|
|
@@ -240,13 +338,13 @@ export class SkillExporter {
|
|
|
240
338
|
"utf-8"
|
|
241
339
|
);
|
|
242
340
|
|
|
243
|
-
//
|
|
341
|
+
// - 拷贝 tsconfig.json(若存在)
|
|
244
342
|
const tsconfigPath = join(root, "tsconfig.json");
|
|
245
343
|
if (existsSync(tsconfigPath)) {
|
|
246
344
|
copyFileSync(tsconfigPath, join(skillDir, "tsconfig.json"));
|
|
247
345
|
}
|
|
248
346
|
|
|
249
|
-
//
|
|
347
|
+
// - 拷贝 Action 源码文件,完整保留相对路径
|
|
250
348
|
for (const act of plan.actions) {
|
|
251
349
|
if (existsSync(act.resolvedPath)) {
|
|
252
350
|
const destFile = join(skillDir, act.entry);
|
|
@@ -255,16 +353,16 @@ export class SkillExporter {
|
|
|
255
353
|
}
|
|
256
354
|
}
|
|
257
355
|
|
|
258
|
-
//
|
|
356
|
+
// - 拷贝静态资产与代码模块文件,完整保留相对路径
|
|
259
357
|
for (const dep of plan.dependencies.modulesAndAssets) {
|
|
260
|
-
if (dep.type === "asset" && existsSync(dep.resolvedPath)) {
|
|
358
|
+
if ((dep.type === "asset" || dep.type === "module") && existsSync(dep.resolvedPath)) {
|
|
261
359
|
const destAsset = join(skillDir, dep.path);
|
|
262
360
|
mkdirSync(dirname(destAsset), { recursive: true });
|
|
263
361
|
copyFileSync(dep.resolvedPath, destAsset);
|
|
264
362
|
}
|
|
265
363
|
}
|
|
266
364
|
|
|
267
|
-
//
|
|
365
|
+
// - 拷贝 Playbook 规程文件
|
|
268
366
|
for (const pb of plan.playbooks) {
|
|
269
367
|
if (existsSync(pb.filePath)) {
|
|
270
368
|
const destPb = join(playbooksDestDir, basename(pb.filePath));
|
|
@@ -274,7 +372,7 @@ export class SkillExporter {
|
|
|
274
372
|
} else {
|
|
275
373
|
// ----------------------------------------------------
|
|
276
374
|
// 独立二进制 Skill 导出 (Standalone Binary Skill Export)
|
|
277
|
-
// 包含: SKILL.md,
|
|
375
|
+
// 包含: SKILL.md, bin/<binary>, playbooks/*, assets/*
|
|
278
376
|
// ----------------------------------------------------
|
|
279
377
|
const binDir = join(skillDir, "bin");
|
|
280
378
|
mkdirSync(binDir, { recursive: true });
|
|
@@ -319,31 +417,28 @@ export class SkillExporter {
|
|
|
319
417
|
|
|
320
418
|
const actualBinaryName = basename(compileRes.executablePath);
|
|
321
419
|
|
|
322
|
-
//
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
JSON.stringify(parsedSkillJson, null, 2) + "\n",
|
|
343
|
-
"utf-8"
|
|
344
|
-
);
|
|
420
|
+
// - 生成独立模式 SKILL.md(若未显式跳过)
|
|
421
|
+
const existingSkill = !options.skipSkillMd
|
|
422
|
+
? findExistingSingleSkillMd(root, options.skillMdPath, pkgSlug)
|
|
423
|
+
: undefined;
|
|
424
|
+
|
|
425
|
+
if (existingSkill) {
|
|
426
|
+
usedExistingSkillMd = existingSkill;
|
|
427
|
+
const destSkillMd = join(skillDir, "SKILL.md");
|
|
428
|
+
if (resolve(existingSkill) !== resolve(destSkillMd)) {
|
|
429
|
+
copyFileSync(existingSkill, destSkillMd);
|
|
430
|
+
}
|
|
431
|
+
} else if (!options.skipSkillMd) {
|
|
432
|
+
const skillMd = generateStandaloneSkillMd(
|
|
433
|
+
configForTemplates,
|
|
434
|
+
plan.actions as any,
|
|
435
|
+
plan.playbooks as any,
|
|
436
|
+
`./bin/${actualBinaryName}`
|
|
437
|
+
);
|
|
438
|
+
writeFileSync(join(skillDir, "SKILL.md"), skillMd, "utf-8");
|
|
439
|
+
}
|
|
345
440
|
|
|
346
|
-
//
|
|
441
|
+
// - 拷贝 Playbook 规程文件
|
|
347
442
|
for (const pb of plan.playbooks) {
|
|
348
443
|
if (existsSync(pb.filePath)) {
|
|
349
444
|
const destPb = join(playbooksDestDir, basename(pb.filePath));
|
|
@@ -351,7 +446,7 @@ export class SkillExporter {
|
|
|
351
446
|
}
|
|
352
447
|
}
|
|
353
448
|
|
|
354
|
-
//
|
|
449
|
+
// - 拷贝静态资产
|
|
355
450
|
for (const dep of plan.dependencies.modulesAndAssets) {
|
|
356
451
|
if (dep.type === "asset" && existsSync(dep.resolvedPath)) {
|
|
357
452
|
const destAsset = join(skillDir, dep.path);
|
|
@@ -387,9 +482,172 @@ export class SkillExporter {
|
|
|
387
482
|
actions: plan.actions.map((a) => a.id),
|
|
388
483
|
playbooks: plan.playbooks.map((p) => p.id),
|
|
389
484
|
files,
|
|
485
|
+
usedExistingSkillMd,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* 批量导出多个 Skill。
|
|
491
|
+
*/
|
|
492
|
+
public async exportBatch(options: BatchSkillExportOptions): Promise<BatchSkillExportResult> {
|
|
493
|
+
if (!options.projectRoots || options.projectRoots.length === 0) {
|
|
494
|
+
throw new BuilderError("No project roots provided for batch skill export.");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const results: SkillExportResult[] = [];
|
|
498
|
+
const baseOutDir = resolve(options.outDir || join(process.cwd(), "dist", "skills"));
|
|
499
|
+
mkdirSync(baseOutDir, { recursive: true });
|
|
500
|
+
|
|
501
|
+
const usedDirNames = new Set<string>();
|
|
502
|
+
for (const projectRoot of options.projectRoots) {
|
|
503
|
+
const config = loadProjectConfig(projectRoot);
|
|
504
|
+
let pkgSlug = getPackageSlug(config.id);
|
|
505
|
+
if (usedDirNames.has(pkgSlug)) {
|
|
506
|
+
pkgSlug = config.id.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/^-+|-+$/g, "");
|
|
507
|
+
}
|
|
508
|
+
usedDirNames.add(pkgSlug);
|
|
509
|
+
|
|
510
|
+
const pkgOutDir = join(baseOutDir, `${pkgSlug}-skill`);
|
|
511
|
+
|
|
512
|
+
const res = await this.export({
|
|
513
|
+
...options,
|
|
514
|
+
projectRoot,
|
|
515
|
+
outDir: pkgOutDir,
|
|
516
|
+
});
|
|
517
|
+
results.push(res);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const totalActions = results.reduce((sum, r) => sum + r.actionsCount, 0);
|
|
521
|
+
const totalPlaybooks = results.reduce((sum, r) => sum + r.playbooksCount, 0);
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
results,
|
|
525
|
+
outDir: baseOutDir,
|
|
526
|
+
totalActions,
|
|
527
|
+
totalPlaybooks,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* 静态辅助调用批量导出方法。
|
|
533
|
+
*/
|
|
534
|
+
public static async exportBatch(options: BatchSkillExportOptions): Promise<BatchSkillExportResult> {
|
|
535
|
+
const exporter = new SkillExporter();
|
|
536
|
+
return exporter.exportBatch(options);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* 复合模式导出:将多个包聚合为一个统一的复合技能目录。
|
|
541
|
+
*/
|
|
542
|
+
public async exportComposite(options: CompositeSkillExportOptions): Promise<CompositeSkillExportResult> {
|
|
543
|
+
if (!options.projectRoots || options.projectRoots.length === 0) {
|
|
544
|
+
throw new BuilderError("No project roots provided for composite skill export.");
|
|
545
|
+
}
|
|
546
|
+
if (!options.bundleName || !options.bundleName.trim()) {
|
|
547
|
+
throw new BuilderError("bundleName is required for composite skill export.");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const bundleSlug = getPackageSlug(options.bundleName);
|
|
551
|
+
const defaultSkillDir = join(process.cwd(), "dist", `${bundleSlug}-skill`);
|
|
552
|
+
const skillDir = resolve(options.outDir || defaultSkillDir);
|
|
553
|
+
|
|
554
|
+
mkdirSync(skillDir, { recursive: true });
|
|
555
|
+
const packagesDestDir = join(skillDir, "packages");
|
|
556
|
+
mkdirSync(packagesDestDir, { recursive: true });
|
|
557
|
+
|
|
558
|
+
const packageInfos: CompositeSkillPackageInfo[] = [];
|
|
559
|
+
const packageSummaries: Array<{ packageId: string; actions: string[]; playbooks: string[] }> = [];
|
|
560
|
+
|
|
561
|
+
const usedDirNames = new Set<string>();
|
|
562
|
+
for (const projectRoot of options.projectRoots) {
|
|
563
|
+
const config = loadProjectConfig(projectRoot);
|
|
564
|
+
let pkgSlug = getPackageSlug(config.id);
|
|
565
|
+
if (usedDirNames.has(pkgSlug)) {
|
|
566
|
+
pkgSlug = config.id.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/^-+|-+$/g, "");
|
|
567
|
+
}
|
|
568
|
+
usedDirNames.add(pkgSlug);
|
|
569
|
+
|
|
570
|
+
const destPkgDir = join(packagesDestDir, pkgSlug);
|
|
571
|
+
|
|
572
|
+
const singleExport = await this.export({
|
|
573
|
+
projectRoot,
|
|
574
|
+
outDir: destPkgDir,
|
|
575
|
+
archive: false,
|
|
576
|
+
skipSkillMd: true,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const manifest = loadManifest(destPkgDir) || loadManifest(projectRoot);
|
|
580
|
+
const playbooks = loadPlaybooks(projectRoot, config.playbooksDir);
|
|
581
|
+
|
|
582
|
+
const actionEntries = singleExport.actions.map((actId) => ({
|
|
583
|
+
id: actId,
|
|
584
|
+
description: manifest?.actions?.[actId]?.description,
|
|
585
|
+
}));
|
|
586
|
+
|
|
587
|
+
packageInfos.push({
|
|
588
|
+
config,
|
|
589
|
+
actions: actionEntries,
|
|
590
|
+
playbooks: Array.from(playbooks.values()),
|
|
591
|
+
packageDir: pkgSlug,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
packageSummaries.push({
|
|
595
|
+
packageId: config.id,
|
|
596
|
+
actions: singleExport.actions,
|
|
597
|
+
playbooks: singleExport.playbooks,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const existingSkillPath = findExistingCompositeSkillMd(options);
|
|
602
|
+
if (existingSkillPath) {
|
|
603
|
+
const destSkillMdPath = join(skillDir, "SKILL.md");
|
|
604
|
+
if (resolve(existingSkillPath) !== resolve(destSkillMdPath)) {
|
|
605
|
+
copyFileSync(existingSkillPath, destSkillMdPath);
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
const description =
|
|
609
|
+
options.description ||
|
|
610
|
+
`ActionDock 复合技能套件,聚合 ${packageInfos.map((p) => p.config.name).join("、")}`;
|
|
611
|
+
const compositeSkillMd = generateCompositeSkillMd(options.bundleName, description, packageInfos);
|
|
612
|
+
writeFileSync(join(skillDir, "SKILL.md"), compositeSkillMd, "utf-8");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
let archivePath: string | undefined;
|
|
616
|
+
if (options.archive) {
|
|
617
|
+
let format: ArchiveFormat = "zip";
|
|
618
|
+
if (options.archiveFormat === "tar.gz" || options.archive === "tar.gz") {
|
|
619
|
+
format = "tar.gz";
|
|
620
|
+
} else if (options.archiveFormat === "zip" || options.archive === "zip") {
|
|
621
|
+
format = "zip";
|
|
622
|
+
}
|
|
623
|
+
archivePath = createArchive(skillDir, format);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const files = scanRelativeFiles(skillDir);
|
|
627
|
+
const totalActions = packageSummaries.reduce((sum, p) => sum + p.actions.length, 0);
|
|
628
|
+
const totalPlaybooks = packageSummaries.reduce((sum, p) => sum + p.playbooks.length, 0);
|
|
629
|
+
|
|
630
|
+
return {
|
|
631
|
+
bundleName: options.bundleName,
|
|
632
|
+
skillDir,
|
|
633
|
+
archivePath,
|
|
634
|
+
packagesCount: packageInfos.length,
|
|
635
|
+
actionsCount: totalActions,
|
|
636
|
+
playbooksCount: totalPlaybooks,
|
|
637
|
+
packages: packageSummaries,
|
|
638
|
+
files,
|
|
639
|
+
usedExistingSkillMd: existingSkillPath,
|
|
390
640
|
};
|
|
391
641
|
}
|
|
392
642
|
|
|
643
|
+
/**
|
|
644
|
+
* 静态辅助调用复合导出方法。
|
|
645
|
+
*/
|
|
646
|
+
public static async exportComposite(options: CompositeSkillExportOptions): Promise<CompositeSkillExportResult> {
|
|
647
|
+
const exporter = new SkillExporter();
|
|
648
|
+
return exporter.exportComposite(options);
|
|
649
|
+
}
|
|
650
|
+
|
|
393
651
|
/**
|
|
394
652
|
* 静态辅助调用方法。
|
|
395
653
|
*/
|
|
@@ -405,3 +663,18 @@ export class SkillExporter {
|
|
|
405
663
|
export async function exportSkill(options: SkillExporterOptions): Promise<SkillExportResult> {
|
|
406
664
|
return SkillExporter.export(options);
|
|
407
665
|
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* 快捷批量导出多个 Skill 产物函数。
|
|
669
|
+
*/
|
|
670
|
+
export async function exportSkillBatch(options: BatchSkillExportOptions): Promise<BatchSkillExportResult> {
|
|
671
|
+
return SkillExporter.exportBatch(options);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* 快捷复合模式导出 Skill 产物函数。
|
|
676
|
+
*/
|
|
677
|
+
export async function exportCompositeSkill(options: CompositeSkillExportOptions): Promise<CompositeSkillExportResult> {
|
|
678
|
+
return SkillExporter.exportComposite(options);
|
|
679
|
+
}
|
|
680
|
+
|
package/src/index.ts
CHANGED
package/src/planner.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import YAML from "yaml";
|
|
4
4
|
import {
|
|
5
5
|
type ActionDockManifest,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
loadProjectConfig,
|
|
9
9
|
type PlaybookDefinition,
|
|
10
10
|
type ProjectConfig,
|
|
11
|
+
resolveActionProjectSync,
|
|
11
12
|
validateManifest,
|
|
12
13
|
} from "@actiondock/core";
|
|
13
14
|
import { PlannerError } from "./errors";
|
|
@@ -40,6 +41,199 @@ function walkDirectory(dir: string): string[] {
|
|
|
40
41
|
return results;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 忽略的模块路径与文件模式判断(排除测试文件、类型声明文件以及构建/版本控制等私有目录)。
|
|
46
|
+
*/
|
|
47
|
+
function isIgnoredModulePath(relPath: string): boolean {
|
|
48
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
49
|
+
return (
|
|
50
|
+
normalized.startsWith("node_modules/") ||
|
|
51
|
+
normalized.includes("/node_modules/") ||
|
|
52
|
+
normalized.startsWith(".git/") ||
|
|
53
|
+
normalized.startsWith("dist/") ||
|
|
54
|
+
normalized.startsWith(".actiondock/") ||
|
|
55
|
+
normalized.endsWith(".test.ts") ||
|
|
56
|
+
normalized.endsWith(".test.js") ||
|
|
57
|
+
normalized.endsWith(".test.tsx") ||
|
|
58
|
+
normalized.endsWith(".test.jsx") ||
|
|
59
|
+
normalized.endsWith(".spec.ts") ||
|
|
60
|
+
normalized.endsWith(".spec.js") ||
|
|
61
|
+
normalized.endsWith(".spec.tsx") ||
|
|
62
|
+
normalized.endsWith(".spec.jsx") ||
|
|
63
|
+
normalized.endsWith(".d.ts")
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 提取源码文本中的所有相对路径模块引用说明符(如 ./... 与 ../...)。
|
|
69
|
+
* 采用纯正则静态提取,杜绝动态 import 与代码执行。
|
|
70
|
+
*/
|
|
71
|
+
function extractRelativeImports(source: string): string[] {
|
|
72
|
+
// 移除多行注释与单行注释,避免注释中的无效相对路径被误提取
|
|
73
|
+
const stripped = source
|
|
74
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
75
|
+
.replace(/\/\/[^\n]*/g, "");
|
|
76
|
+
|
|
77
|
+
const specifiers = new Set<string>();
|
|
78
|
+
|
|
79
|
+
// 匹配 from "..."、from '...'、from `...`
|
|
80
|
+
const fromRegex = /\bfrom\s*["'`]([^"'`]+)["'`]/g;
|
|
81
|
+
let match: RegExpExecArray | null;
|
|
82
|
+
while ((match = fromRegex.exec(stripped)) !== null) {
|
|
83
|
+
const spec = match[1].trim();
|
|
84
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
85
|
+
specifiers.add(spec);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 匹配 import(...) 或 import "..."
|
|
90
|
+
const importRegex = /\bimport\s*(?:\(\s*)?["'`]([^"'`]+)["'`]/g;
|
|
91
|
+
while ((match = importRegex.exec(stripped)) !== null) {
|
|
92
|
+
const spec = match[1].trim();
|
|
93
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
94
|
+
specifiers.add(spec);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 匹配 require(...)
|
|
99
|
+
const requireRegex = /\brequire\s*\(\s*["'`]([^"'`]+)["'`]/g;
|
|
100
|
+
while ((match = requireRegex.exec(stripped)) !== null) {
|
|
101
|
+
const spec = match[1].trim();
|
|
102
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
103
|
+
specifiers.add(spec);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return Array.from(specifiers);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const CANDIDATE_EXTENSIONS = [
|
|
111
|
+
"",
|
|
112
|
+
".ts",
|
|
113
|
+
".tsx",
|
|
114
|
+
".js",
|
|
115
|
+
".jsx",
|
|
116
|
+
".mjs",
|
|
117
|
+
".cjs",
|
|
118
|
+
".mts",
|
|
119
|
+
".cts",
|
|
120
|
+
".json",
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 在文件系统上探测解析相对导入说明符所对应的物理源文件。
|
|
125
|
+
* 兼容 NodeNext 规范中将 .js / .mjs / .cjs 说明符解析映射到 .ts / .mts / .cts 源文件。
|
|
126
|
+
*/
|
|
127
|
+
function resolveLocalModulePath(baseDir: string, specifier: string): string | null {
|
|
128
|
+
const candidate = resolve(baseDir, specifier);
|
|
129
|
+
|
|
130
|
+
// 1. 若候选路径直接存在且为文件
|
|
131
|
+
if (existsSync(candidate)) {
|
|
132
|
+
try {
|
|
133
|
+
if (statSync(candidate).isFile()) {
|
|
134
|
+
return candidate;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// 忽略文件属性读取异常
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2. NodeNext / ESM 规范映射:若导入说明符以 .js / .mjs / .cjs 结尾,优先尝试对应 TypeScript 源码文件
|
|
142
|
+
if (candidate.endsWith(".js")) {
|
|
143
|
+
const tsPath = candidate.slice(0, -3) + ".ts";
|
|
144
|
+
if (existsSync(tsPath)) return tsPath;
|
|
145
|
+
const tsxPath = candidate.slice(0, -3) + ".tsx";
|
|
146
|
+
if (existsSync(tsxPath)) return tsxPath;
|
|
147
|
+
} else if (candidate.endsWith(".mjs")) {
|
|
148
|
+
const mtsPath = candidate.slice(0, -4) + ".mts";
|
|
149
|
+
if (existsSync(mtsPath)) return mtsPath;
|
|
150
|
+
} else if (candidate.endsWith(".cjs")) {
|
|
151
|
+
const ctsPath = candidate.slice(0, -4) + ".cts";
|
|
152
|
+
if (existsSync(ctsPath)) return ctsPath;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 3. 尝试追加常见扩展名
|
|
156
|
+
for (const ext of CANDIDATE_EXTENSIONS) {
|
|
157
|
+
if (!ext) continue;
|
|
158
|
+
const withExt = candidate + ext;
|
|
159
|
+
if (existsSync(withExt)) {
|
|
160
|
+
try {
|
|
161
|
+
if (statSync(withExt).isFile()) {
|
|
162
|
+
return withExt;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// 忽略
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 4. 若为目录,尝试 index 文件
|
|
171
|
+
if (existsSync(candidate)) {
|
|
172
|
+
try {
|
|
173
|
+
if (statSync(candidate).isDirectory()) {
|
|
174
|
+
for (const ext of [".ts", ".tsx", ".js", ".mjs", ".json"]) {
|
|
175
|
+
const indexFile = join(candidate, `index${ext}`);
|
|
176
|
+
if (existsSync(indexFile)) {
|
|
177
|
+
return indexFile;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// 忽略
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 从一组入口文件出发,静态深度追踪所有相对路径引用的本地源码模块闭包。
|
|
191
|
+
*/
|
|
192
|
+
function traceLocalModuleDependencies(
|
|
193
|
+
entryFiles: string[],
|
|
194
|
+
projectRoot: string
|
|
195
|
+
): string[] {
|
|
196
|
+
const visited = new Set<string>();
|
|
197
|
+
const modules = new Set<string>();
|
|
198
|
+
const queue = [...entryFiles];
|
|
199
|
+
|
|
200
|
+
while (queue.length > 0) {
|
|
201
|
+
const current = queue.shift()!;
|
|
202
|
+
if (visited.has(current)) continue;
|
|
203
|
+
visited.add(current);
|
|
204
|
+
|
|
205
|
+
const relToRoot = relative(projectRoot, current);
|
|
206
|
+
if (relToRoot.startsWith("..") || isAbsolute(relToRoot)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let content = "";
|
|
211
|
+
try {
|
|
212
|
+
content = readFileSync(current, "utf-8");
|
|
213
|
+
} catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const specifiers = extractRelativeImports(content);
|
|
218
|
+
const dir = dirname(current);
|
|
219
|
+
|
|
220
|
+
for (const spec of specifiers) {
|
|
221
|
+
const resolved = resolveLocalModulePath(dir, spec);
|
|
222
|
+
if (resolved) {
|
|
223
|
+
const rel = relative(projectRoot, resolved);
|
|
224
|
+
if (!rel.startsWith("..") && !isAbsolute(rel) && !isIgnoredModulePath(rel)) {
|
|
225
|
+
modules.add(resolved);
|
|
226
|
+
if (!visited.has(resolved)) {
|
|
227
|
+
queue.push(resolved);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return Array.from(modules);
|
|
235
|
+
}
|
|
236
|
+
|
|
43
237
|
/**
|
|
44
238
|
* 纯文本解析 Playbook Markdown 文件,提取 YAML Frontmatter,不执行任何业务代码。
|
|
45
239
|
*/
|
|
@@ -281,9 +475,11 @@ export class BuildPlanner {
|
|
|
281
475
|
}
|
|
282
476
|
}
|
|
283
477
|
|
|
284
|
-
// 5. 进行依赖闭包(uses
|
|
478
|
+
// 5. 进行依赖闭包(uses)解析计算,自动处理环形依赖与传递依赖(支持当前清单与已链接外部包)
|
|
285
479
|
const resolvedActionIds = new Set<string>();
|
|
286
480
|
const queue = Array.from(initialActionIds);
|
|
481
|
+
const externalActionRoots = new Map<string, string>();
|
|
482
|
+
const externalActionEntries = new Map<string, ActionManifestEntry>();
|
|
287
483
|
|
|
288
484
|
while (queue.length > 0) {
|
|
289
485
|
const currentId = queue.shift()!;
|
|
@@ -291,14 +487,37 @@ export class BuildPlanner {
|
|
|
291
487
|
continue;
|
|
292
488
|
}
|
|
293
489
|
|
|
294
|
-
|
|
490
|
+
let entry = manifest.actions[currentId];
|
|
491
|
+
let entryRoot = root;
|
|
492
|
+
|
|
493
|
+
if (!entry) {
|
|
494
|
+
try {
|
|
495
|
+
const resolvedExternal = resolveActionProjectSync(currentId, root);
|
|
496
|
+
if (resolvedExternal && existsSync(resolvedExternal.projectRoot)) {
|
|
497
|
+
const externalManifest = loadManifest(resolvedExternal.projectRoot);
|
|
498
|
+
const externalEntry = externalManifest?.actions?.[resolvedExternal.actionId];
|
|
499
|
+
if (externalEntry) {
|
|
500
|
+
entry = externalEntry;
|
|
501
|
+
entryRoot = resolvedExternal.projectRoot;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} catch {
|
|
505
|
+
// 忽略
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
295
509
|
if (!entry) {
|
|
296
510
|
throw new PlannerError(
|
|
297
|
-
`Action '${currentId}' referenced in dependency closure (uses) was not found in manifest`,
|
|
511
|
+
`Action '${currentId}' referenced in dependency closure (uses) was not found in manifest or linked packages`,
|
|
298
512
|
"MISSING_DEPENDENCY"
|
|
299
513
|
);
|
|
300
514
|
}
|
|
301
515
|
|
|
516
|
+
if (entryRoot !== root) {
|
|
517
|
+
externalActionRoots.set(currentId, entryRoot);
|
|
518
|
+
externalActionEntries.set(currentId, entry);
|
|
519
|
+
}
|
|
520
|
+
|
|
302
521
|
resolvedActionIds.add(currentId);
|
|
303
522
|
|
|
304
523
|
if (entry.uses && Array.isArray(entry.uses)) {
|
|
@@ -321,8 +540,15 @@ export class BuildPlanner {
|
|
|
321
540
|
// 7. 构造 Action 依赖结构
|
|
322
541
|
const actionDependencies: ActionDependency[] = [];
|
|
323
542
|
for (const actId of resolvedActionIds) {
|
|
324
|
-
const entry = manifest.actions[actId];
|
|
325
|
-
const
|
|
543
|
+
const entry = manifest.actions[actId] || externalActionEntries.get(actId);
|
|
544
|
+
const entryRoot = externalActionRoots.get(actId) || root;
|
|
545
|
+
if (!entry) {
|
|
546
|
+
throw new PlannerError(
|
|
547
|
+
`Action '${actId}' entry not found`,
|
|
548
|
+
"ENTRY_FILE_NOT_FOUND"
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
const resolvedPath = resolve(entryRoot, entry.entry);
|
|
326
552
|
if (!existsSync(resolvedPath)) {
|
|
327
553
|
throw new PlannerError(
|
|
328
554
|
`Action '${actId}' entry file not found on disk: ${entry.entry}`,
|
|
@@ -345,6 +571,58 @@ export class BuildPlanner {
|
|
|
345
571
|
// 8. 构造模块与资产依赖结构
|
|
346
572
|
const modulesAndAssets: AssetDependency[] = [];
|
|
347
573
|
const assetPathSet = new Set<string>();
|
|
574
|
+
const modulePathSet = new Set<string>();
|
|
575
|
+
const actionPathSet = new Set(actionDependencies.map((a) => a.resolvedPath));
|
|
576
|
+
|
|
577
|
+
// 静态递归追踪 Action 源码引用的本地模块代码(如 lib/、辅助工具等)
|
|
578
|
+
const rootActionsMap = new Map<string, string[]>();
|
|
579
|
+
for (const act of actionDependencies) {
|
|
580
|
+
const actRoot = externalActionRoots.get(act.id) || root;
|
|
581
|
+
if (!rootActionsMap.has(actRoot)) {
|
|
582
|
+
rootActionsMap.set(actRoot, []);
|
|
583
|
+
}
|
|
584
|
+
rootActionsMap.get(actRoot)!.push(act.resolvedPath);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
for (const [actRoot, files] of rootActionsMap) {
|
|
588
|
+
const tracedModulePaths = traceLocalModuleDependencies(files, actRoot);
|
|
589
|
+
for (const modPath of tracedModulePaths) {
|
|
590
|
+
if (!actionPathSet.has(modPath)) {
|
|
591
|
+
const rel = relative(actRoot, modPath).replace(/\\/g, "/");
|
|
592
|
+
if (!modulePathSet.has(rel) && !isIgnoredModulePath(rel)) {
|
|
593
|
+
modulePathSet.add(rel);
|
|
594
|
+
modulesAndAssets.push({
|
|
595
|
+
path: rel,
|
|
596
|
+
resolvedPath: modPath,
|
|
597
|
+
type: "module",
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 若未显式过滤 Action 与 Playbook(全量构建模式),且存在根目录 lib 目录,自动全量扫描 lib 源码
|
|
605
|
+
const isSelective =
|
|
606
|
+
Boolean(options?.actions && options.actions.length > 0) ||
|
|
607
|
+
Boolean(options?.playbooks && options.playbooks.length > 0);
|
|
608
|
+
|
|
609
|
+
if (!isSelective) {
|
|
610
|
+
const defaultLibDir = join(root, "lib");
|
|
611
|
+
if (existsSync(defaultLibDir)) {
|
|
612
|
+
const libFiles = walkDirectory(defaultLibDir);
|
|
613
|
+
for (const file of libFiles) {
|
|
614
|
+
const rel = relative(root, file).replace(/\\/g, "/");
|
|
615
|
+
if (!isIgnoredModulePath(rel) && !actionPathSet.has(file) && !modulePathSet.has(rel)) {
|
|
616
|
+
modulePathSet.add(rel);
|
|
617
|
+
modulesAndAssets.push({
|
|
618
|
+
path: rel,
|
|
619
|
+
resolvedPath: file,
|
|
620
|
+
type: "module",
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
348
626
|
|
|
349
627
|
// 清单声明资产
|
|
350
628
|
if (manifest.assets && Array.isArray(manifest.assets)) {
|
package/src/types.ts
CHANGED
|
@@ -231,6 +231,10 @@ export interface SkillExporterOptions {
|
|
|
231
231
|
config?: ProjectConfig;
|
|
232
232
|
/** 预置的声明式清单(可选) */
|
|
233
233
|
manifest?: ActionDockManifest;
|
|
234
|
+
/** 是否跳过生成 SKILL.md(多包聚合时避免子包重复生成,对外保持唯一定义) */
|
|
235
|
+
skipSkillMd?: boolean;
|
|
236
|
+
/** 显式指定的已有 SKILL.md 文件路径(若提供或存在则直接使用,不再动态生成) */
|
|
237
|
+
skillMdPath?: string;
|
|
234
238
|
}
|
|
235
239
|
|
|
236
240
|
/**
|
|
@@ -259,4 +263,75 @@ export interface SkillExportResult {
|
|
|
259
263
|
playbooks: string[];
|
|
260
264
|
/** 导出的文件相对路径清单 */
|
|
261
265
|
files: string[];
|
|
266
|
+
/** 若复用了已有的 SKILL.md 文件,返回该文件的绝对路径 */
|
|
267
|
+
usedExistingSkillMd?: string;
|
|
262
268
|
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 批量 Skill 导出选项。
|
|
272
|
+
*/
|
|
273
|
+
export interface BatchSkillExportOptions extends Omit<SkillExporterOptions, "projectRoot"> {
|
|
274
|
+
/** 待导出的项目根目录绝对路径列表 */
|
|
275
|
+
projectRoots: string[];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 批量 Skill 导出结果。
|
|
280
|
+
*/
|
|
281
|
+
export interface BatchSkillExportResult {
|
|
282
|
+
/** 各包独立导出结果列表 */
|
|
283
|
+
results: SkillExportResult[];
|
|
284
|
+
/** 批量导出根目录 */
|
|
285
|
+
outDir: string;
|
|
286
|
+
/** 导出的总 Action 数量 */
|
|
287
|
+
totalActions: number;
|
|
288
|
+
/** 导出的总 Playbook 数量 */
|
|
289
|
+
totalPlaybooks: number;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 复合 Skill 套件导出选项。
|
|
294
|
+
*/
|
|
295
|
+
export interface CompositeSkillExportOptions {
|
|
296
|
+
/** 复合技能套件名称 */
|
|
297
|
+
bundleName: string;
|
|
298
|
+
/** 参与聚合的项目根目录绝对路径列表 */
|
|
299
|
+
projectRoots: string[];
|
|
300
|
+
/** 复合套件输出目标目录(默认输出至 dist 目录) */
|
|
301
|
+
outDir?: string;
|
|
302
|
+
/** 复合技能描述信息 */
|
|
303
|
+
description?: string;
|
|
304
|
+
/** 是否执行归档压缩,亦可直接指定归档格式 */
|
|
305
|
+
archive?: boolean | ArchiveFormat;
|
|
306
|
+
/** 归档压缩格式(zip 或 tar.gz) */
|
|
307
|
+
archiveFormat?: ArchiveFormat;
|
|
308
|
+
/** 工作区根目录(用于检索工作区现有的 SKILL.md 等资产,默认基于 cwd 与项目目录推导) */
|
|
309
|
+
workspaceRoot?: string;
|
|
310
|
+
/** 显式指定的已有 SKILL.md 文件路径(若存在则直接使用,不再动态生成) */
|
|
311
|
+
skillMdPath?: string;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 复合 Skill 套件导出结果。
|
|
316
|
+
*/
|
|
317
|
+
export interface CompositeSkillExportResult {
|
|
318
|
+
/** 复合技能套件名称 */
|
|
319
|
+
bundleName: string;
|
|
320
|
+
/** 生成的复合 Skill 目录绝对路径 */
|
|
321
|
+
skillDir: string;
|
|
322
|
+
/** 生成的归档文件绝对路径(若开启压缩) */
|
|
323
|
+
archivePath?: string;
|
|
324
|
+
/** 包含的 Package 数量 */
|
|
325
|
+
packagesCount: number;
|
|
326
|
+
/** 包含的总 Action 数量 */
|
|
327
|
+
actionsCount: number;
|
|
328
|
+
/** 包含的总 Playbook 数量 */
|
|
329
|
+
playbooksCount: number;
|
|
330
|
+
/** 各包的摘要明细 */
|
|
331
|
+
packages: Array<{ packageId: string; actions: string[]; playbooks: string[] }>;
|
|
332
|
+
/** 生成的文件清单 */
|
|
333
|
+
files: string[];
|
|
334
|
+
/** 若复用了已有的 SKILL.md 文件,返回该文件的绝对路径 */
|
|
335
|
+
usedExistingSkillMd?: string;
|
|
336
|
+
}
|
|
337
|
+
|