@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actiondock/builder",
3
- "version": "2.0.9",
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": "^2.0.9",
31
- "@actiondock/sdk": "^2.0.9",
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
- generateSkillJson,
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 { ArchiveFormat, SkillExporterOptions, SkillExportResult } from "./types";
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
- // 包含: SKILL.md, actiondock.skill.json, actiondock.manifest.json, actiondock.json, package.json, 源码与资产
122
- // ----------------------------------------------------
123
-
124
- // 1. 生成 SKILL.md
125
- const skillMd = generateSourceSkillMd(
126
- configForTemplates,
127
- plan.actions as any,
128
- plan.playbooks as any
129
- );
130
- writeFileSync(join(skillDir, "SKILL.md"), skillMd, "utf-8");
131
-
132
- // 2. 生成 actiondock.skill.json 工具清单
133
- const skillJsonContent = {
134
- schemaVersion: "2.0.0",
135
- packageId: plan.packageId,
136
- name: plan.packageName,
137
- version: plan.version,
138
- description: plan.description,
139
- mode: "source",
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
- // 3. 导出精简后的 actiondock.manifest.json 清单
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
- // 4. 导出精简后的 actiondock.json 项目配置
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
- // 5. 导出 package.json
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
- // 6. 拷贝 tsconfig.json(若存在)
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
- // 7. 拷贝 Action 源码文件,完整保留相对路径
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
- // 8. 拷贝静态资产文件,完整保留相对路径
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
- // 9. 拷贝 Playbook 文件
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, actiondock.skill.json, bin/<binary>, playbooks/*, assets/*
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
- // 1. 生成独立模式 SKILL.md
323
- const skillMd = generateStandaloneSkillMd(
324
- configForTemplates,
325
- plan.actions as any,
326
- plan.playbooks as any,
327
- `./bin/${actualBinaryName}`
328
- );
329
- writeFileSync(join(skillDir, "SKILL.md"), skillMd, "utf-8");
330
-
331
- // 2. 生成 actiondock.skill.json
332
- const rawSkillJson = generateSkillJson(
333
- configForTemplates,
334
- plan.actions as any,
335
- actualBinaryName,
336
- target
337
- );
338
- const parsedSkillJson = JSON.parse(rawSkillJson);
339
- parsedSkillJson.mode = "standalone";
340
- writeFileSync(
341
- join(skillDir, "actiondock.skill.json"),
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
- // 3. 拷贝 Playbook
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
- // 4. 拷贝静态资产
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
@@ -1,5 +1,6 @@
1
1
  export * from "./types";
2
2
  export * from "./errors";
3
+ export * from "./build";
3
4
  export * from "./planner";
4
5
  export * from "./compiler";
5
6
  export * from "./exporter";
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
- const entry = manifest.actions[currentId];
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 resolvedPath = resolve(root, entry.entry);
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
+