@actiondock/builder 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -9
- package/dist/build.js +1 -1
- package/dist/export-archive.d.ts +20 -0
- package/dist/export-archive.js +52 -0
- package/dist/export-batch.d.ts +13 -0
- package/dist/export-batch.js +39 -0
- package/dist/export-composite.d.ts +13 -0
- package/dist/export-composite.js +225 -0
- package/dist/export-node.d.ts +11 -0
- package/dist/export-node.js +48 -0
- package/dist/export-source.d.ts +11 -0
- package/dist/export-source.js +134 -0
- package/dist/exporter.d.ts +10 -35
- package/dist/exporter.js +26 -645
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -2
- package/dist/pack.js +17 -3
- package/dist/planner.d.ts +0 -17
- package/dist/planner.js +0 -17
- package/dist/skill-md.d.ts +47 -0
- package/dist/skill-md.js +166 -0
- package/dist/types.d.ts +2 -6
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -12,11 +12,11 @@ ActionDock 2.0 构建规划、目录交付与技能导出包。
|
|
|
12
12
|
|
|
13
13
|
## 核心组件与能力
|
|
14
14
|
|
|
15
|
-
###
|
|
15
|
+
### SelectionPlanner 依赖闭包计算器
|
|
16
16
|
|
|
17
17
|
通过静态分析依赖关系与 Playbook 规程,无需执行任何业务代码,杜绝模块加载过程中的副作用:
|
|
18
18
|
|
|
19
|
-
- 规划模型:[SelectionPlanner](./src/planner.ts)
|
|
19
|
+
- 规划模型:[SelectionPlanner](./src/planner.ts) 读取 `actiondock.json` 与 Playbook 的 YAML 头部,提取 Actions 与 Playbooks 映射。
|
|
20
20
|
- 递归依赖闭包:自顶向下递归解析 Action 的 `uses` 声明列表,构建完整的静态调用图。
|
|
21
21
|
- 规程驱动按需剪枝:支持针对特定 Playbook 进行定向打包,自动计算并仅保留该 Playbook 直接或间接调用的最小 Action 集合,实现依赖按需裁剪。
|
|
22
22
|
- 静态资产收集:自动识别并关联声明的静态资产文件。
|
|
@@ -55,11 +55,11 @@ ActionDock 2.0 构建规划、目录交付与技能导出包。
|
|
|
55
55
|
## 编程调用示例
|
|
56
56
|
|
|
57
57
|
```ts
|
|
58
|
-
import { buildProject, packProject, SelectionPlanner, SkillExporter } from "@actiondock/builder";
|
|
58
|
+
import { buildProject, packProject, SelectionPlanner, SkillExporter, exportSkill } from "@actiondock/builder";
|
|
59
59
|
|
|
60
60
|
// 计算依赖规划闭包
|
|
61
61
|
const planner = new SelectionPlanner({ projectRoot: "/path/to/project" });
|
|
62
|
-
const plan = planner.
|
|
62
|
+
const plan = planner.plan({ projectRoot: "/path/to/project", playbooks: ["review-pr"] });
|
|
63
63
|
|
|
64
64
|
// 构建 Node.js 目录交付产物
|
|
65
65
|
const buildResult = await buildProject({
|
|
@@ -74,11 +74,19 @@ const packResult = await packProject({
|
|
|
74
74
|
outDir: "./dist/npm",
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
// 导出 Agent Skill
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
// 导出 Agent Skill 资产(顶层便捷入口,内部委托 SkillExporter 实例方法)
|
|
78
|
+
const skillResult = await exportSkill({
|
|
79
|
+
projectRoot: "/path/to/project",
|
|
80
|
+
outDir: "./dist/skills",
|
|
81
|
+
playbooks: ["review-pr"],
|
|
82
|
+
mode: "source",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 或直接实例化 SkillExporter 定制导出
|
|
86
|
+
const exporter = new SkillExporter();
|
|
87
|
+
const customResult = await exporter.export({
|
|
88
|
+
projectRoot: "/path/to/project",
|
|
89
|
+
outDir: "./dist/skills-custom",
|
|
82
90
|
mode: "source",
|
|
83
91
|
});
|
|
84
92
|
```
|
package/dist/build.js
CHANGED
|
@@ -178,7 +178,7 @@ const argv = process.argv.slice(2);
|
|
|
178
178
|
|
|
179
179
|
// 1. 监督进程接管参数校验,明确拒绝 --async
|
|
180
180
|
if (argv.includes("--async")) {
|
|
181
|
-
const isJson = argv.includes("--json")
|
|
181
|
+
const isJson = argv.includes("--json");
|
|
182
182
|
if (isJson) {
|
|
183
183
|
console.log(
|
|
184
184
|
JSON.stringify(
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ArchiveFormat } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 导出产物归档辅助层。
|
|
4
|
+
*
|
|
5
|
+
* 归档压缩能力的单一事实源在 ./archive,此处仅承载面向导出流程的
|
|
6
|
+
* 目标路径推导与临时文件清理语义,严禁重复实现压缩逻辑。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 执行归档压缩操作。
|
|
10
|
+
* 先压缩到临时路径,成功后原子重命名覆盖最终路径;失败时清理临时文件后透传原始异常,
|
|
11
|
+
* 避免旧档已删、新档半成品残留的中间态。
|
|
12
|
+
*/
|
|
13
|
+
export declare function createArchive(skillDir: string, format: ArchiveFormat): Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* 解析归档格式:显式 archiveFormat 优先,archive 字段直接携带格式时其次。
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveArchiveFormat(options: {
|
|
18
|
+
archive?: boolean | ArchiveFormat;
|
|
19
|
+
archiveFormat?: ArchiveFormat;
|
|
20
|
+
}): ArchiveFormat;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, rmSync, renameSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { createTarGzArchiveAsync, createZipArchiveAsync } from "./archive.js";
|
|
4
|
+
import { BuilderError } from "./errors.js";
|
|
5
|
+
/**
|
|
6
|
+
* 导出产物归档辅助层。
|
|
7
|
+
*
|
|
8
|
+
* 归档压缩能力的单一事实源在 ./archive,此处仅承载面向导出流程的
|
|
9
|
+
* 目标路径推导与临时文件清理语义,严禁重复实现压缩逻辑。
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* 执行归档压缩操作。
|
|
13
|
+
* 先压缩到临时路径,成功后原子重命名覆盖最终路径;失败时清理临时文件后透传原始异常,
|
|
14
|
+
* 避免旧档已删、新档半成品残留的中间态。
|
|
15
|
+
*/
|
|
16
|
+
export async function createArchive(skillDir, format) {
|
|
17
|
+
const parentDir = dirname(skillDir);
|
|
18
|
+
const folderName = basename(skillDir);
|
|
19
|
+
const archiveName = `${folderName}.${format === "tar.gz" ? "tar.gz" : "zip"}`;
|
|
20
|
+
const archivePath = join(parentDir, archiveName);
|
|
21
|
+
const tempArchivePath = `${archivePath}.tmp`;
|
|
22
|
+
if (existsSync(tempArchivePath)) {
|
|
23
|
+
// 清理上次失败可能残留的临时文件(兼容目录形态,避免阻塞本次压缩)
|
|
24
|
+
rmSync(tempArchivePath, { force: true, recursive: true, maxRetries: 3, retryDelay: 50 });
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
if (format === "tar.gz") {
|
|
28
|
+
await createTarGzArchiveAsync(skillDir, tempArchivePath);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
await createZipArchiveAsync(skillDir, tempArchivePath);
|
|
32
|
+
}
|
|
33
|
+
renameSync(tempArchivePath, archivePath);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
// 压缩失败:清理临时半成品,旧档保持原样,透传原始异常
|
|
37
|
+
if (existsSync(tempArchivePath)) {
|
|
38
|
+
rmSync(tempArchivePath, { force: true, recursive: true, maxRetries: 3, retryDelay: 50 });
|
|
39
|
+
}
|
|
40
|
+
throw new BuilderError(`Failed to create ${format} archive: ${err?.message || String(err)}`);
|
|
41
|
+
}
|
|
42
|
+
return archivePath;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 解析归档格式:显式 archiveFormat 优先,archive 字段直接携带格式时其次。
|
|
46
|
+
*/
|
|
47
|
+
export function resolveArchiveFormat(options) {
|
|
48
|
+
if (options.archiveFormat === "tar.gz" || options.archive === "tar.gz") {
|
|
49
|
+
return "tar.gz";
|
|
50
|
+
}
|
|
51
|
+
return "zip";
|
|
52
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BatchSkillExportOptions, BatchSkillExportResult, SkillExportResult, SkillExporterOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 批量 Skill 导出装配层。
|
|
4
|
+
*
|
|
5
|
+
* 职责:按项目根目录列表逐一执行单包导出并聚合统计结果,
|
|
6
|
+
* 目录名冲突时回退为原始 Package ID 的安全形式。
|
|
7
|
+
*/
|
|
8
|
+
/** 单包导出回调:由 SkillExporter 注入自身导出能力,避免模块间循环依赖 */
|
|
9
|
+
export type SingleSkillExporter = (options: SkillExporterOptions) => Promise<SkillExportResult>;
|
|
10
|
+
/**
|
|
11
|
+
* 执行批量导出多个 Skill。
|
|
12
|
+
*/
|
|
13
|
+
export declare function exportBatchImpl(options: BatchSkillExportOptions, exportSingle: SingleSkillExporter): Promise<BatchSkillExportResult>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { getPackageSlug, loadProjectConfig } from "@actiondock/core";
|
|
4
|
+
import { BuilderError } from "./errors.js";
|
|
5
|
+
/**
|
|
6
|
+
* 执行批量导出多个 Skill。
|
|
7
|
+
*/
|
|
8
|
+
export async function exportBatchImpl(options, exportSingle) {
|
|
9
|
+
if (!options.projectRoots || options.projectRoots.length === 0) {
|
|
10
|
+
throw new BuilderError("No project roots provided for batch skill export.");
|
|
11
|
+
}
|
|
12
|
+
const results = [];
|
|
13
|
+
const baseOutDir = resolve(options.outDir || join(process.cwd(), "dist", "skills"));
|
|
14
|
+
mkdirSync(baseOutDir, { recursive: true });
|
|
15
|
+
const usedDirNames = new Set();
|
|
16
|
+
for (const projectRoot of options.projectRoots) {
|
|
17
|
+
const config = loadProjectConfig(projectRoot);
|
|
18
|
+
let pkgSlug = getPackageSlug(config.id);
|
|
19
|
+
if (usedDirNames.has(pkgSlug)) {
|
|
20
|
+
pkgSlug = config.id.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/^-+|-+$/g, "");
|
|
21
|
+
}
|
|
22
|
+
usedDirNames.add(pkgSlug);
|
|
23
|
+
const pkgOutDir = join(baseOutDir, `${pkgSlug}-skill`);
|
|
24
|
+
const res = await exportSingle({
|
|
25
|
+
...options,
|
|
26
|
+
projectRoot,
|
|
27
|
+
outDir: pkgOutDir,
|
|
28
|
+
});
|
|
29
|
+
results.push(res);
|
|
30
|
+
}
|
|
31
|
+
const totalActions = results.reduce((sum, r) => sum + r.actionsCount, 0);
|
|
32
|
+
const totalPlaybooks = results.reduce((sum, r) => sum + r.playbooksCount, 0);
|
|
33
|
+
return {
|
|
34
|
+
results,
|
|
35
|
+
outDir: baseOutDir,
|
|
36
|
+
totalActions,
|
|
37
|
+
totalPlaybooks,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CompositeSkillExportOptions, CompositeSkillExportResult, SkillExporterOptions, SkillExportResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 复合套件导出装配层。
|
|
4
|
+
*
|
|
5
|
+
* 职责:将多个包聚合为一个统一的复合技能目录(mini-workspace 形态),
|
|
6
|
+
* 聚合各子包依赖生成复合根 package.json,并生成或复用复合 SKILL.md。
|
|
7
|
+
*/
|
|
8
|
+
/** 子包导出回调:由 SkillExporter 注入自身导出能力,避免模块间循环依赖 */
|
|
9
|
+
export type SubpackageExporter = (options: SkillExporterOptions) => Promise<SkillExportResult>;
|
|
10
|
+
/**
|
|
11
|
+
* 执行复合模式导出:将多个包聚合为一个统一的复合技能目录。
|
|
12
|
+
*/
|
|
13
|
+
export declare function exportCompositeImpl(options: CompositeSkillExportOptions, exportSubpackage: SubpackageExporter): Promise<CompositeSkillExportResult>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { generateCompositeSkillMd, getPackageSlug, loadManifest, loadPlaybooks, loadProjectConfig, } from "@actiondock/core";
|
|
4
|
+
import { BuilderError } from "./errors.js";
|
|
5
|
+
import { collectRelativeFiles, getInternalDependencyVersion, replaceDirAtomic } from "./fs-utils.js";
|
|
6
|
+
import { assertNoFileProtocolDeps, resolveWorkspaceDepVersion } from "./manifest.js";
|
|
7
|
+
import { SelectionPlanner } from "./planner.js";
|
|
8
|
+
import { findExistingCompositeSkillMd, resolveCustomSkillDeclaration, } from "./skill-md.js";
|
|
9
|
+
import { createArchive, resolveArchiveFormat } from "./export-archive.js";
|
|
10
|
+
/**
|
|
11
|
+
* 执行复合模式导出:将多个包聚合为一个统一的复合技能目录。
|
|
12
|
+
*/
|
|
13
|
+
export async function exportCompositeImpl(options, exportSubpackage) {
|
|
14
|
+
if (!options.projectRoots || options.projectRoots.length === 0) {
|
|
15
|
+
throw new BuilderError("No project roots provided for composite skill export.");
|
|
16
|
+
}
|
|
17
|
+
if (!options.bundleName || !options.bundleName.trim()) {
|
|
18
|
+
throw new BuilderError("bundleName is required for composite skill export.");
|
|
19
|
+
}
|
|
20
|
+
const skillMdOnly = options.skillMdOnly === true;
|
|
21
|
+
const customDeclaration = resolveCustomSkillDeclaration(options);
|
|
22
|
+
const bundleSlug = getPackageSlug(options.bundleName);
|
|
23
|
+
const defaultSkillDir = join(process.cwd(), "dist", `${bundleSlug}-skill`);
|
|
24
|
+
const targetSkillDir = resolve(options.outDir || defaultSkillDir);
|
|
25
|
+
const parentDir = dirname(targetSkillDir);
|
|
26
|
+
mkdirSync(parentDir, { recursive: true });
|
|
27
|
+
const stagingDir = skillMdOnly
|
|
28
|
+
? null
|
|
29
|
+
: join(parentDir, `.tmp-composite-${bundleSlug}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
30
|
+
if (stagingDir) {
|
|
31
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
const packagesDestDir = stagingDir ? join(stagingDir, "packages") : null;
|
|
34
|
+
if (packagesDestDir) {
|
|
35
|
+
mkdirSync(packagesDestDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
const packageInfos = [];
|
|
38
|
+
const packageSummaries = [];
|
|
39
|
+
let existingSkillPath;
|
|
40
|
+
try {
|
|
41
|
+
// 收集并展开所有包含在依赖闭包中的外部项目根目录,确保 bundle 整包携带齐全部依赖
|
|
42
|
+
const effectiveRoots = new Set();
|
|
43
|
+
for (const r of options.projectRoots) {
|
|
44
|
+
effectiveRoots.add(resolve(r));
|
|
45
|
+
if (!skillMdOnly) {
|
|
46
|
+
try {
|
|
47
|
+
const p = SelectionPlanner.plan({ projectRoot: r });
|
|
48
|
+
if (p.externalProjectRoots) {
|
|
49
|
+
for (const extRoot of p.externalProjectRoots) {
|
|
50
|
+
effectiveRoots.add(resolve(extRoot));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// 忽略规划解析异常,由主循环处理
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const usedDirNames = new Set();
|
|
60
|
+
for (const projectRoot of effectiveRoots) {
|
|
61
|
+
const config = loadProjectConfig(projectRoot);
|
|
62
|
+
let pkgSlug;
|
|
63
|
+
if (skillMdOnly) {
|
|
64
|
+
// 就地生成:规程链接基于工作区实际子目录名
|
|
65
|
+
pkgSlug = basename(resolve(projectRoot));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
pkgSlug = getPackageSlug(config.id);
|
|
69
|
+
if (usedDirNames.has(pkgSlug)) {
|
|
70
|
+
pkgSlug = config.id.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/^-+|-+$/g, "");
|
|
71
|
+
}
|
|
72
|
+
usedDirNames.add(pkgSlug);
|
|
73
|
+
}
|
|
74
|
+
let actionIds;
|
|
75
|
+
let playbookIds;
|
|
76
|
+
let manifest;
|
|
77
|
+
if (skillMdOnly) {
|
|
78
|
+
// 仅生成 SKILL.md:直接读取清单获取 Action 契约描述,不拷贝子包产物
|
|
79
|
+
manifest = loadManifest(projectRoot);
|
|
80
|
+
actionIds = manifest?.actions ? Object.keys(manifest.actions) : [];
|
|
81
|
+
playbookIds = Array.from(loadPlaybooks(projectRoot, config.playbooksDir).keys());
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const destPkgDir = join(packagesDestDir, pkgSlug);
|
|
85
|
+
const singleExport = await exportSubpackage({
|
|
86
|
+
projectRoot,
|
|
87
|
+
outDir: destPkgDir,
|
|
88
|
+
archive: false,
|
|
89
|
+
skipSkillMd: true,
|
|
90
|
+
mode: "source",
|
|
91
|
+
_isSubpackage: true,
|
|
92
|
+
});
|
|
93
|
+
manifest = loadManifest(destPkgDir) || loadManifest(projectRoot);
|
|
94
|
+
actionIds = singleExport.actions;
|
|
95
|
+
playbookIds = singleExport.playbooks;
|
|
96
|
+
}
|
|
97
|
+
const playbooks = loadPlaybooks(projectRoot, config.playbooksDir);
|
|
98
|
+
const actionEntries = actionIds.map((actId) => ({
|
|
99
|
+
id: actId,
|
|
100
|
+
description: manifest?.actions?.[actId]?.description,
|
|
101
|
+
}));
|
|
102
|
+
packageInfos.push({
|
|
103
|
+
config,
|
|
104
|
+
actions: actionEntries,
|
|
105
|
+
playbooks: Array.from(playbooks.values()),
|
|
106
|
+
packageDir: pkgSlug,
|
|
107
|
+
});
|
|
108
|
+
packageSummaries.push({
|
|
109
|
+
packageId: config.id,
|
|
110
|
+
actions: actionIds,
|
|
111
|
+
playbooks: playbookIds,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
existingSkillPath = skillMdOnly ? undefined : findExistingCompositeSkillMd(options);
|
|
115
|
+
const description = options.description ||
|
|
116
|
+
customDeclaration?.description ||
|
|
117
|
+
`ActionDock 复合技能套件,聚合 ${packageInfos.map((p) => p.config.name).join("、")}`;
|
|
118
|
+
const compositeSkillMd = generateCompositeSkillMd(options.bundleName, description, packageInfos, {
|
|
119
|
+
customSections: customDeclaration?.sections,
|
|
120
|
+
packagesBaseDir: skillMdOnly ? "." : "packages",
|
|
121
|
+
});
|
|
122
|
+
if (skillMdOnly) {
|
|
123
|
+
// 就地仅重生成 SKILL.md:始终重新生成(忽略已有 SKILL.md 复用逻辑),不产出完整套件目录
|
|
124
|
+
const targetFile = options.outDir && options.outDir.toLowerCase().endsWith(".md")
|
|
125
|
+
? resolve(options.outDir)
|
|
126
|
+
: resolve(options.outDir ? join(options.outDir, "SKILL.md") : join(process.cwd(), "SKILL.md"));
|
|
127
|
+
mkdirSync(dirname(targetFile), { recursive: true });
|
|
128
|
+
// 目标文件即将被覆盖时输出警告,避免无提示覆盖用户已有内容
|
|
129
|
+
if (existsSync(targetFile)) {
|
|
130
|
+
console.warn(`[WARN] Existing SKILL.md will be overwritten: ${targetFile}`);
|
|
131
|
+
}
|
|
132
|
+
writeFileSync(targetFile, compositeSkillMd, "utf-8");
|
|
133
|
+
return {
|
|
134
|
+
bundleName: options.bundleName,
|
|
135
|
+
skillDir: dirname(targetFile),
|
|
136
|
+
skillMdFile: targetFile,
|
|
137
|
+
packagesCount: packageInfos.length,
|
|
138
|
+
actionsCount: packageInfos.reduce((acc, p) => acc + p.actions.length, 0),
|
|
139
|
+
playbooksCount: packageInfos.reduce((acc, p) => acc + p.playbooks.length, 0),
|
|
140
|
+
packages: packageSummaries,
|
|
141
|
+
files: [basename(targetFile)],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (existingSkillPath) {
|
|
145
|
+
const destSkillMdPath = join(stagingDir, "SKILL.md");
|
|
146
|
+
if (resolve(existingSkillPath) !== resolve(destSkillMdPath)) {
|
|
147
|
+
copyFileSync(existingSkillPath, destSkillMdPath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
writeFileSync(join(stagingDir, "SKILL.md"), compositeSkillMd, "utf-8");
|
|
152
|
+
}
|
|
153
|
+
// 聚合所有子包依赖生成复合根目录 package.json
|
|
154
|
+
writeCompositePkgJson(stagingDir, packagesDestDir, packageInfos, bundleSlug, options);
|
|
155
|
+
// 原子替换目标复合技能目录
|
|
156
|
+
await replaceDirAtomic(stagingDir, targetSkillDir);
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
if (stagingDir && existsSync(stagingDir)) {
|
|
160
|
+
rmSync(stagingDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
let archivePath;
|
|
164
|
+
if (options.archive) {
|
|
165
|
+
archivePath = await createArchive(targetSkillDir, resolveArchiveFormat(options));
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
bundleName: options.bundleName,
|
|
169
|
+
skillDir: targetSkillDir,
|
|
170
|
+
archivePath,
|
|
171
|
+
packagesCount: packageInfos.length,
|
|
172
|
+
actionsCount: packageInfos.reduce((acc, p) => acc + p.actions.length, 0),
|
|
173
|
+
playbooksCount: packageInfos.reduce((acc, p) => acc + p.playbooks.length, 0),
|
|
174
|
+
packages: packageSummaries,
|
|
175
|
+
files: collectRelativeFiles(targetSkillDir),
|
|
176
|
+
// 复用中段已计算的路径,避免结尾重复扫描
|
|
177
|
+
usedExistingSkillMd: existingSkillPath,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 复合套件聚合各子包依赖并生成复合根目录 package.json。
|
|
182
|
+
*/
|
|
183
|
+
function writeCompositePkgJson(stagingDir, packagesDestDir, packageInfos, bundleSlug, options) {
|
|
184
|
+
const aggregatedDeps = {
|
|
185
|
+
"@actiondock/sdk": getInternalDependencyVersion(),
|
|
186
|
+
};
|
|
187
|
+
for (const info of packageInfos) {
|
|
188
|
+
const pkgJsonPath = join(packagesDestDir, info.packageDir, "package.json");
|
|
189
|
+
if (existsSync(pkgJsonPath)) {
|
|
190
|
+
try {
|
|
191
|
+
const parsed = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
192
|
+
if (parsed.dependencies && typeof parsed.dependencies === "object") {
|
|
193
|
+
assertNoFileProtocolDeps(parsed.dependencies, "exported skill packages");
|
|
194
|
+
for (const [dep, ver] of Object.entries(parsed.dependencies)) {
|
|
195
|
+
const verStr = String(ver);
|
|
196
|
+
if (dep.startsWith("@actiondock/")) {
|
|
197
|
+
aggregatedDeps[dep] = getInternalDependencyVersion();
|
|
198
|
+
}
|
|
199
|
+
else if (verStr.startsWith("workspace:")) {
|
|
200
|
+
aggregatedDeps[dep] = resolveWorkspaceDepVersion(join(packagesDestDir, info.packageDir), dep, verStr);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
aggregatedDeps[dep] = verStr;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
if (err instanceof BuilderError)
|
|
210
|
+
throw err;
|
|
211
|
+
// 复合导出缺依赖影响面大:读文件与解析失败必须显式报错,严禁静默吞掉导致聚合依赖缺失
|
|
212
|
+
throw new BuilderError(`Failed to read or parse package.json of exported subpackage '${info.packageDir}' (${pkgJsonPath}): ${err instanceof Error ? err.message : String(err)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const compositePkg = {
|
|
217
|
+
name: `${bundleSlug}-composite-skill`,
|
|
218
|
+
version: "0.1.0",
|
|
219
|
+
description: options.description || `Composite Skill suite: ${options.bundleName}`,
|
|
220
|
+
type: "module",
|
|
221
|
+
workspaces: ["packages/*"],
|
|
222
|
+
dependencies: aggregatedDeps,
|
|
223
|
+
};
|
|
224
|
+
writeFileSync(join(stagingDir, "package.json"), JSON.stringify(compositePkg, null, 2) + "\n", "utf-8");
|
|
225
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SelectionPlan, SkillExporterOptions, SkillExportResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Node 目录型 Skill 导出装配层。
|
|
4
|
+
*
|
|
5
|
+
* 职责:复用 Node 目录型构建产出可执行交付目录,
|
|
6
|
+
* 并在目录内生成调用该入口的 SKILL.md 说明书。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 执行 Node 目录型 Skill 导出。
|
|
10
|
+
*/
|
|
11
|
+
export declare function exportNodeSkill(root: string, targetSkillDir: string, plan: SelectionPlan, options: SkillExporterOptions, pkgSlug: string): Promise<SkillExportResult>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { generateStandaloneSkillMd } from "@actiondock/core";
|
|
2
|
+
import { buildProject } from "./build.js";
|
|
3
|
+
import { collectRelativeFiles } from "./fs-utils.js";
|
|
4
|
+
import { buildConfigForTemplates, toPlaybookDefinitions, toSkillActionItems, writeSkillMd, } from "./skill-md.js";
|
|
5
|
+
import { createArchive, resolveArchiveFormat } from "./export-archive.js";
|
|
6
|
+
/**
|
|
7
|
+
* Node 目录型 Skill 导出装配层。
|
|
8
|
+
*
|
|
9
|
+
* 职责:复用 Node 目录型构建产出可执行交付目录,
|
|
10
|
+
* 并在目录内生成调用该入口的 SKILL.md 说明书。
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* 执行 Node 目录型 Skill 导出。
|
|
14
|
+
*/
|
|
15
|
+
export async function exportNodeSkill(root, targetSkillDir, plan, options, pkgSlug) {
|
|
16
|
+
await buildProject({
|
|
17
|
+
projectRoot: root,
|
|
18
|
+
outDir: targetSkillDir,
|
|
19
|
+
actions: options.actions,
|
|
20
|
+
playbooks: options.playbooks,
|
|
21
|
+
config: options.config,
|
|
22
|
+
manifest: options.manifest,
|
|
23
|
+
vendorDeps: Boolean(options.vendorDeps),
|
|
24
|
+
allowInstallScripts: options.allowInstallScripts,
|
|
25
|
+
requireReproducible: options.requireReproducible,
|
|
26
|
+
});
|
|
27
|
+
const usedExistingSkillMd = writeSkillMd(root, targetSkillDir, plan, { skipSkillMd: options.skipSkillMd, skillMdPath: options.skillMdPath, pkgSlug }, () => {
|
|
28
|
+
const configForTemplates = buildConfigForTemplates(plan, plan.actionsDir || "actions", plan.playbooksDir || "playbooks");
|
|
29
|
+
return generateStandaloneSkillMd(configForTemplates, toSkillActionItems(plan.actions), toPlaybookDefinitions(plan.playbooks), "node ./entry.mjs");
|
|
30
|
+
});
|
|
31
|
+
let archivePath;
|
|
32
|
+
if (options.archive) {
|
|
33
|
+
archivePath = await createArchive(targetSkillDir, resolveArchiveFormat(options));
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
packageId: plan.packageId,
|
|
37
|
+
version: plan.version,
|
|
38
|
+
mode: "node",
|
|
39
|
+
skillDir: targetSkillDir,
|
|
40
|
+
archivePath,
|
|
41
|
+
actionsCount: plan.actions.length,
|
|
42
|
+
playbooksCount: plan.playbooks.length,
|
|
43
|
+
actions: plan.actions.map((a) => a.id),
|
|
44
|
+
playbooks: plan.playbooks.map((p) => p.id),
|
|
45
|
+
files: collectRelativeFiles(targetSkillDir),
|
|
46
|
+
usedExistingSkillMd,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SelectionPlan, SkillExporterOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 源码型 Skill 导出装配层。
|
|
4
|
+
*
|
|
5
|
+
* 职责:在暂存目录内完成 SKILL.md、actiondock.json、package.json
|
|
6
|
+
* 与全部源码/资产/Playbook 文件的物化,产物保持源码形态可直接编辑。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 源码型 Skill 的暂存阶段:生成 SKILL.md、actiondock.json、package.json 并拷贝源码。
|
|
10
|
+
*/
|
|
11
|
+
export declare function stageSourceSkill(root: string, skillDir: string, plan: SelectionPlan, options: SkillExporterOptions, pkgSlug: string): string | undefined;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { generateSourceSkillMd } from "@actiondock/core";
|
|
4
|
+
import { BuilderError } from "./errors.js";
|
|
5
|
+
import { getInternalDependencyVersion } from "./fs-utils.js";
|
|
6
|
+
import { assertValidManifestActionIds, resolveWorkspaceDepVersion, serializePlanManifest, } from "./manifest.js";
|
|
7
|
+
import { buildConfigForTemplates, toPlaybookDefinitions, toSkillActionItems, writeSkillMd, } from "./skill-md.js";
|
|
8
|
+
/**
|
|
9
|
+
* 源码型 Skill 导出装配层。
|
|
10
|
+
*
|
|
11
|
+
* 职责:在暂存目录内完成 SKILL.md、actiondock.json、package.json
|
|
12
|
+
* 与全部源码/资产/Playbook 文件的物化,产物保持源码形态可直接编辑。
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* 源码型 Skill 的暂存阶段:生成 SKILL.md、actiondock.json、package.json 并拷贝源码。
|
|
16
|
+
*/
|
|
17
|
+
export function stageSourceSkill(root, skillDir, plan, options, pkgSlug) {
|
|
18
|
+
const actionsDir = plan.actionsDir || "actions";
|
|
19
|
+
const playbooksDir = plan.playbooksDir || "playbooks";
|
|
20
|
+
const playbooksDestDir = join(skillDir, playbooksDir);
|
|
21
|
+
if (plan.playbooks.length > 0) {
|
|
22
|
+
mkdirSync(playbooksDestDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
const configForTemplates = buildConfigForTemplates(plan, actionsDir, playbooksDir);
|
|
25
|
+
const usedExistingSkillMd = writeSkillMd(root, skillDir, plan, { skipSkillMd: options.skipSkillMd, skillMdPath: options.skillMdPath, pkgSlug }, () => generateSourceSkillMd(configForTemplates, toSkillActionItems(plan.actions), toPlaybookDefinitions(plan.playbooks)));
|
|
26
|
+
// 导出精简后的 actiondock.json 项目配置(项目元数据与清单的单一事实源)
|
|
27
|
+
const exportedConfig = serializePlanManifest(plan, {
|
|
28
|
+
omitEmptyConfig: true,
|
|
29
|
+
includeDirs: { actionsDir, playbooksDir },
|
|
30
|
+
});
|
|
31
|
+
if (exportedConfig.actions && typeof exportedConfig.actions === "object") {
|
|
32
|
+
assertValidManifestActionIds(exportedConfig.actions);
|
|
33
|
+
}
|
|
34
|
+
writeFileSync(join(skillDir, "actiondock.json"), JSON.stringify(exportedConfig, null, 2) + "\n", "utf-8");
|
|
35
|
+
// 导出 package.json
|
|
36
|
+
writeSourceSkillPkgJson(root, skillDir, plan, pkgSlug);
|
|
37
|
+
// 拷贝 tsconfig.json(若存在)
|
|
38
|
+
const tsconfigPath = join(root, "tsconfig.json");
|
|
39
|
+
if (existsSync(tsconfigPath)) {
|
|
40
|
+
copyFileSync(tsconfigPath, join(skillDir, "tsconfig.json"));
|
|
41
|
+
}
|
|
42
|
+
// 拷贝 Action 源码文件,保留相对路径(仅拷贝自有 Action,跨包依赖不物化进消费包目录)
|
|
43
|
+
for (const act of plan.actions) {
|
|
44
|
+
if (act.isExternal || act.id.includes("/")) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (existsSync(act.resolvedPath)) {
|
|
48
|
+
const destFile = join(skillDir, act.entry);
|
|
49
|
+
mkdirSync(dirname(destFile), { recursive: true });
|
|
50
|
+
copyFileSync(act.resolvedPath, destFile);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 拷贝静态资产与代码模块文件,保留相对路径
|
|
54
|
+
for (const dep of plan.dependencies.modulesAndAssets) {
|
|
55
|
+
if ((dep.type === "asset" || dep.type === "module" || dep.type === "file") &&
|
|
56
|
+
existsSync(dep.resolvedPath)) {
|
|
57
|
+
const destAsset = join(skillDir, dep.path);
|
|
58
|
+
mkdirSync(dirname(destAsset), { recursive: true });
|
|
59
|
+
copyFileSync(dep.resolvedPath, destAsset);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 拷贝 Playbook 规程文件
|
|
63
|
+
for (const pb of plan.playbooks) {
|
|
64
|
+
if (existsSync(pb.filePath)) {
|
|
65
|
+
const destPb = join(playbooksDestDir, basename(pb.filePath));
|
|
66
|
+
copyFileSync(pb.filePath, destPb);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return usedExistingSkillMd;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 生成源码型 Skill 的 package.json(含依赖协议校验与 workspace 版本解析)。
|
|
73
|
+
*/
|
|
74
|
+
function writeSourceSkillPkgJson(root, skillDir, plan, pkgSlug) {
|
|
75
|
+
const sanitizeDependencies = (deps) => {
|
|
76
|
+
const result = {};
|
|
77
|
+
if (deps && typeof deps === "object") {
|
|
78
|
+
for (const [k, v] of Object.entries(deps)) {
|
|
79
|
+
const verStr = String(v);
|
|
80
|
+
if (verStr.startsWith("file:")) {
|
|
81
|
+
throw new BuilderError(`Unsupported file: dependency for '${k}'. Runtime dependencies must not use file: protocol in exported skill packages.`);
|
|
82
|
+
}
|
|
83
|
+
if (k.startsWith("@actiondock/")) {
|
|
84
|
+
result[k] = getInternalDependencyVersion();
|
|
85
|
+
}
|
|
86
|
+
else if (verStr.startsWith("workspace:")) {
|
|
87
|
+
result[k] = resolveWorkspaceDepVersion(root, k, verStr);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
result[k] = verStr;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!result["@actiondock/sdk"]) {
|
|
95
|
+
result["@actiondock/sdk"] = getInternalDependencyVersion();
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
};
|
|
99
|
+
let exportedPkg;
|
|
100
|
+
const projectPkgPath = join(root, "package.json");
|
|
101
|
+
if (existsSync(projectPkgPath)) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = readFileSync(projectPkgPath, "utf-8");
|
|
104
|
+
const parsed = JSON.parse(raw);
|
|
105
|
+
exportedPkg = {
|
|
106
|
+
name: parsed.name || pkgSlug,
|
|
107
|
+
version: plan.version || parsed.version || "0.1.0",
|
|
108
|
+
description: plan.description || parsed.description,
|
|
109
|
+
type: "module",
|
|
110
|
+
dependencies: sanitizeDependencies(parsed.dependencies),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (err instanceof BuilderError) {
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
exportedPkg = fallbackSkillPkg(plan, pkgSlug);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
exportedPkg = fallbackSkillPkg(plan, pkgSlug);
|
|
122
|
+
}
|
|
123
|
+
writeFileSync(join(skillDir, "package.json"), JSON.stringify(exportedPkg, null, 2) + "\n", "utf-8");
|
|
124
|
+
}
|
|
125
|
+
/** 无 package.json 或解析失败时的兜底导出描述 */
|
|
126
|
+
function fallbackSkillPkg(plan, pkgSlug) {
|
|
127
|
+
return {
|
|
128
|
+
name: pkgSlug,
|
|
129
|
+
version: plan.version,
|
|
130
|
+
description: plan.description,
|
|
131
|
+
type: "module",
|
|
132
|
+
dependencies: { "@actiondock/sdk": getInternalDependencyVersion() },
|
|
133
|
+
};
|
|
134
|
+
}
|