@actiondock/builder 2.0.12 → 2.2.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.
@@ -0,0 +1,75 @@
1
+ import type { ActionDependency, PlaybookPlanEntry } from "./types.js";
2
+ /**
3
+ * 校验清单 actions 字典中的所有 Action ID 是否符合规范。
4
+ * 强制写入端契约自检,杜绝非法 Action ID 落盘。
5
+ */
6
+ export declare function assertValidManifestActionIds(actions: Record<string, unknown>): void;
7
+ /**
8
+ * actiondock.json 清单组装与依赖协议校验的单一实现。
9
+ * pack / build / exporter 三个模块的清单拼装与 file: 协议校验全部收敛至此,
10
+ * 字段顺序属于产物字节级契约的一部分,严禁调整。
11
+ */
12
+ /**
13
+ * 构造单个 Action 的清单声明项(字段顺序为产物契约的一部分)。
14
+ */
15
+ export declare function serializeManifestAction(action: ActionDependency): Record<string, unknown>;
16
+ /**
17
+ * 构造 Playbook 集合的清单字典。
18
+ */
19
+ export declare function serializeManifestPlaybooks(playbooks: PlaybookPlanEntry[], playbooksDir: string): Record<string, unknown>;
20
+ /**
21
+ * manifest 拼装选项。
22
+ */
23
+ export interface SerializePlanManifestOptions {
24
+ /** 是否包含 $schema 声明字段(npm pack 产物包含,目录型产物不包含) */
25
+ includeSchema?: boolean;
26
+ /** config 字段是否前置于 actions 之前(npm pack 产物的历史字段顺序) */
27
+ configBeforeActions?: boolean;
28
+ /** 附加 actionsDir / playbooksDir 目录字段(源码型 Skill 导出需要) */
29
+ includeDirs?: {
30
+ actionsDir?: string;
31
+ playbooksDir?: string;
32
+ };
33
+ /** Action 入口改写映射(npm pack 用于指向编译后的 .js/.mjs 入口) */
34
+ actionEntryOverride?: (action: ActionDependency) => string;
35
+ /** 是否省略空 config 键(源码型 Skill 导出保持旧版字节级行为:未声明时省略) */
36
+ omitEmptyConfig?: boolean;
37
+ /** 是否输出 playbooks 声明(npm pack 产物历史行为不输出,需显式关闭) */
38
+ includePlaybooks?: boolean;
39
+ }
40
+ /**
41
+ * 将 SelectionPlan 组装为 actiondock.json 清单对象。
42
+ * 标准字段顺序:schemaVersion、id、name、version、description、
43
+ * actionsDir、playbooksDir、actions、playbooks、config、files、assets。
44
+ */
45
+ export declare function serializePlanManifest(plan: {
46
+ packageId: string;
47
+ packageName: string;
48
+ version: string;
49
+ description?: string;
50
+ actions: ActionDependency[];
51
+ playbooks: PlaybookPlanEntry[];
52
+ playbooksDir?: string;
53
+ files?: string[];
54
+ assets?: string[];
55
+ configDefs?: Record<string, unknown>;
56
+ }, options?: SerializePlanManifestOptions): Record<string, unknown>;
57
+ /**
58
+ * 校验依赖字典中不存在 file: 本地协议声明,违规即抛出 BuilderError。
59
+ */
60
+ export declare function assertNoFileProtocolDeps(dependencies: Record<string, unknown> | undefined, context: string): void;
61
+ /**
62
+ * 解析 workspace: 依赖的真实版本号。
63
+ * @actiondock/* 内部依赖直接采用内部版本规则;显式约束直接剥离前缀;
64
+ * workspace:* 则在本地 node_modules 与 monorepo packages 目录中搜索目标包版本。
65
+ */
66
+ export declare function resolveWorkspaceDepVersion(root: string, depName: string, ver: string): string;
67
+ /**
68
+ * 读取并解析 package.json,解析失败抛出 BuilderError。
69
+ */
70
+ export declare function readPackageJson(pkgJsonPath: string): Record<string, any>;
71
+ /**
72
+ * 规范化导出用 package.json:导出 ./actiondock.json,设定 node 引擎约束与 ESM 类型。
73
+ * 返回新对象,不改动传入源。
74
+ */
75
+ export declare function normalizePkgExportsAndEngines(pkg: Record<string, any>): Record<string, any>;
@@ -0,0 +1,213 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { ACTION_ID_REGEX } from "@actiondock/core";
4
+ import { BuilderError } from "./errors.js";
5
+ import { getInternalDependencyVersion } from "./fs-utils.js";
6
+ /**
7
+ * 校验清单 actions 字典中的所有 Action ID 是否符合规范。
8
+ * 强制写入端契约自检,杜绝非法 Action ID 落盘。
9
+ */
10
+ export function assertValidManifestActionIds(actions) {
11
+ for (const actionId of Object.keys(actions)) {
12
+ if (!ACTION_ID_REGEX.test(actionId)) {
13
+ throw new BuilderError(`Invalid action ID '${actionId}' in generated manifest. Action IDs must match ${ACTION_ID_REGEX}`, "INVALID_ACTION_ID");
14
+ }
15
+ }
16
+ }
17
+ /**
18
+ * actiondock.json 清单组装与依赖协议校验的单一实现。
19
+ * pack / build / exporter 三个模块的清单拼装与 file: 协议校验全部收敛至此,
20
+ * 字段顺序属于产物字节级契约的一部分,严禁调整。
21
+ */
22
+ /**
23
+ * 构造单个 Action 的清单声明项(字段顺序为产物契约的一部分)。
24
+ */
25
+ export function serializeManifestAction(action) {
26
+ return {
27
+ entry: action.entry,
28
+ description: action.description,
29
+ inputSchema: action.inputSchema,
30
+ outputSchema: action.outputSchema,
31
+ uses: action.uses,
32
+ tags: action.tags,
33
+ annotations: action.annotations,
34
+ };
35
+ }
36
+ /**
37
+ * 构造 Playbook 集合的清单字典。
38
+ */
39
+ export function serializeManifestPlaybooks(playbooks, playbooksDir) {
40
+ const manifestPlaybooks = {};
41
+ for (const pb of playbooks) {
42
+ const fileName = pb.filePath.replace(/\\/g, "/").split("/").pop();
43
+ manifestPlaybooks[pb.id] = {
44
+ entry: `${playbooksDir}/${fileName}`,
45
+ ...(pb.description ? { description: pb.description } : {}),
46
+ ...(pb.actions && pb.actions.length > 0 ? { actions: pb.actions } : {}),
47
+ };
48
+ }
49
+ return manifestPlaybooks;
50
+ }
51
+ /**
52
+ * 将 SelectionPlan 组装为 actiondock.json 清单对象。
53
+ * 标准字段顺序:schemaVersion、id、name、version、description、
54
+ * actionsDir、playbooksDir、actions、playbooks、config、files、assets。
55
+ */
56
+ export function serializePlanManifest(plan, options = {}) {
57
+ const manifestActions = {};
58
+ for (const act of plan.actions) {
59
+ // 仅序列化包自有 Action,跨包依赖不写入包清单
60
+ if (act.isExternal || act.id.includes("/")) {
61
+ continue;
62
+ }
63
+ const serialized = serializeManifestAction(act);
64
+ if (options.actionEntryOverride) {
65
+ serialized.entry = options.actionEntryOverride(act);
66
+ }
67
+ manifestActions[act.id] = serialized;
68
+ }
69
+ // 写入端契约自检断言:校验所有 Action ID 必须符合规范
70
+ assertValidManifestActionIds(manifestActions);
71
+ const manifestPlaybooks = serializeManifestPlaybooks(plan.playbooks, options.includeDirs?.playbooksDir || plan.playbooksDir || "playbooks");
72
+ const manifest = {};
73
+ if (options.includeSchema) {
74
+ manifest.$schema = "https://actiondock.dev/schema/v2.json";
75
+ }
76
+ manifest.schemaVersion = 2;
77
+ manifest.id = plan.packageId;
78
+ manifest.name = plan.packageName;
79
+ manifest.version = plan.version;
80
+ manifest.description = plan.description;
81
+ if (options.includeDirs?.actionsDir) {
82
+ manifest.actionsDir = options.includeDirs.actionsDir;
83
+ }
84
+ if (options.includeDirs?.playbooksDir) {
85
+ manifest.playbooksDir = options.includeDirs.playbooksDir;
86
+ }
87
+ manifest.actions = manifestActions;
88
+ if (Object.keys(manifestPlaybooks).length > 0 && options.includePlaybooks !== false) {
89
+ manifest.playbooks = manifestPlaybooks;
90
+ }
91
+ if (plan.configDefs) {
92
+ manifest.config = plan.configDefs;
93
+ }
94
+ else if (!options.omitEmptyConfig) {
95
+ manifest.config = {};
96
+ }
97
+ if (plan.files && plan.files.length > 0) {
98
+ manifest.files = plan.files;
99
+ }
100
+ if (plan.assets && plan.assets.length > 0) {
101
+ manifest.assets = plan.assets;
102
+ }
103
+ if (options.configBeforeActions) {
104
+ // npm pack 产物的历史字段顺序:config 前置于 actions
105
+ const ordered = {};
106
+ for (const [key, value] of Object.entries(manifest)) {
107
+ if (key === "actions") {
108
+ ordered.config = manifest.config;
109
+ ordered.actions = value;
110
+ continue;
111
+ }
112
+ if (key === "config") {
113
+ continue;
114
+ }
115
+ ordered[key] = value;
116
+ }
117
+ return ordered;
118
+ }
119
+ return manifest;
120
+ }
121
+ /**
122
+ * 校验依赖字典中不存在 file: 本地协议声明,违规即抛出 BuilderError。
123
+ */
124
+ export function assertNoFileProtocolDeps(dependencies, context) {
125
+ if (!dependencies || typeof dependencies !== "object")
126
+ return;
127
+ for (const [depName, depVer] of Object.entries(dependencies)) {
128
+ if (String(depVer).startsWith("file:")) {
129
+ throw new BuilderError(`Unsupported file: dependency for '${depName}'. Runtime dependencies must not use file: protocol in ${context}.`);
130
+ }
131
+ }
132
+ }
133
+ /** 依赖名最后一段(scoped 包名去除命名空间前缀) */
134
+ function lastSegment(name) {
135
+ return name.split("/").pop();
136
+ }
137
+ /**
138
+ * 解析 workspace: 依赖的真实版本号。
139
+ * @actiondock/* 内部依赖直接采用内部版本规则;显式约束直接剥离前缀;
140
+ * workspace:* 则在本地 node_modules 与 monorepo packages 目录中搜索目标包版本。
141
+ */
142
+ export function resolveWorkspaceDepVersion(root, depName, ver) {
143
+ if (depName.startsWith("@actiondock/")) {
144
+ return getInternalDependencyVersion();
145
+ }
146
+ const stripped = ver.replace(/^workspace:/, "").trim();
147
+ if (stripped && stripped !== "*" && stripped !== "^" && stripped !== "~") {
148
+ return stripped;
149
+ }
150
+ const candidates = [
151
+ resolve(root, "node_modules", depName, "package.json"),
152
+ resolve(root, "packages", depName, "package.json"),
153
+ resolve(root, "packages", lastSegment(depName), "package.json"),
154
+ resolve(root, "..", depName, "package.json"),
155
+ resolve(root, "..", lastSegment(depName), "package.json"),
156
+ resolve(root, "..", "packages", depName, "package.json"),
157
+ resolve(root, "..", "packages", lastSegment(depName), "package.json"),
158
+ resolve(root, "..", "..", "packages", depName, "package.json"),
159
+ resolve(root, "..", "..", "packages", lastSegment(depName), "package.json"),
160
+ ];
161
+ for (const cand of candidates) {
162
+ if (existsSync(cand)) {
163
+ try {
164
+ const pkgData = JSON.parse(readFileSync(cand, "utf-8"));
165
+ if (pkgData.version) {
166
+ const prefix = stripped === "~" ? "~" : "^";
167
+ return `${prefix}${pkgData.version}`;
168
+ }
169
+ }
170
+ catch {
171
+ // 忽略单个依赖解析异常
172
+ }
173
+ }
174
+ }
175
+ throw new BuilderError(`Failed to resolve workspace dependency '${depName}' (${ver}) in '${root}'. Target package version could not be found.`);
176
+ }
177
+ /**
178
+ * 读取并解析 package.json,解析失败抛出 BuilderError。
179
+ */
180
+ export function readPackageJson(pkgJsonPath) {
181
+ try {
182
+ return JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
183
+ }
184
+ catch (err) {
185
+ throw new BuilderError(`Invalid package.json in ${pkgJsonPath}: ${err?.message || String(err)}`);
186
+ }
187
+ }
188
+ /**
189
+ * 规范化导出用 package.json:导出 ./actiondock.json,设定 node 引擎约束与 ESM 类型。
190
+ * 返回新对象,不改动传入源。
191
+ */
192
+ export function normalizePkgExportsAndEngines(pkg) {
193
+ const normalized = { ...pkg, type: "module" };
194
+ let currentExports = {};
195
+ if (typeof normalized.exports === "object" && normalized.exports !== null) {
196
+ currentExports = { ...normalized.exports };
197
+ }
198
+ else if (typeof normalized.exports === "string") {
199
+ currentExports["."] = normalized.exports;
200
+ }
201
+ currentExports["./actiondock.json"] = "./actiondock.json";
202
+ normalized.exports = currentExports;
203
+ const currentEngines = {
204
+ ...(typeof normalized.engines === "object" && normalized.engines !== null
205
+ ? normalized.engines
206
+ : {}),
207
+ };
208
+ if (!currentEngines.node) {
209
+ currentEngines.node = ">=24.12.0";
210
+ }
211
+ normalized.engines = currentEngines;
212
+ return normalized;
213
+ }
package/dist/pack.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { PackOptions, PackResult } from "./types.js";
2
+ /**
3
+ * 对 Action Package 执行打包并生成标准 npm Action 压缩包(.tgz)。
4
+ *
5
+ * 职责:
6
+ * - 调用项目本地 TypeScript 编译(Node 执行),输出声明文件(.d.ts)与纯 JavaScript ESM 产物到临时暂存目录。
7
+ * - 严格校验导出的 Manifest(入口全部指向编译后的 .js/.mjs,不得包含 actionsDir 或 playbooksDir 等废弃目录字段)。
8
+ * - 在暂存目录调用带有 --ignore-scripts 的 npm pack 生成标准 npm 压缩包(.tgz),严禁使用正则替换与自建 tar 压缩。
9
+ *
10
+ * @param options 打包参数选项
11
+ * @returns 打包结果元数据
12
+ */
13
+ export declare function packProject(options: PackOptions): Promise<PackResult>;
package/dist/pack.js ADDED
@@ -0,0 +1,339 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, join, relative, resolve } from "node:path";
7
+ import { getPackageSlug, loadProjectConfig } from "@actiondock/core";
8
+ import { BuilderError } from "./errors.js";
9
+ import { assertNoFileProtocolDeps, assertValidManifestActionIds, normalizePkgExportsAndEngines, readPackageJson, serializePlanManifest, } from "./manifest.js";
10
+ import { collectRelativeFiles } from "./fs-utils.js";
11
+ import { SelectionPlanner } from "./planner.js";
12
+ /**
13
+ * 对 Action Package 执行打包并生成标准 npm Action 压缩包(.tgz)。
14
+ *
15
+ * 职责:
16
+ * - 调用项目本地 TypeScript 编译(Node 执行),输出声明文件(.d.ts)与纯 JavaScript ESM 产物到临时暂存目录。
17
+ * - 严格校验导出的 Manifest(入口全部指向编译后的 .js/.mjs,不得包含 actionsDir 或 playbooksDir 等废弃目录字段)。
18
+ * - 在暂存目录调用带有 --ignore-scripts 的 npm pack 生成标准 npm 压缩包(.tgz),严禁使用正则替换与自建 tar 压缩。
19
+ *
20
+ * @param options 打包参数选项
21
+ * @returns 打包结果元数据
22
+ */
23
+ export async function packProject(options) {
24
+ const root = resolve(options.projectRoot);
25
+ const pkgConfig = options.config || loadProjectConfig(root);
26
+ const plan = SelectionPlanner.plan({
27
+ projectRoot: root,
28
+ config: pkgConfig,
29
+ manifest: options.manifest,
30
+ });
31
+ if (plan.actions.length === 0) {
32
+ throw new BuilderError("No actions resolved for packing");
33
+ }
34
+ const pkgSlug = getPackageSlug(plan.packageId);
35
+ const pkgJsonPath = join(root, "package.json");
36
+ if (!existsSync(pkgJsonPath)) {
37
+ throw new BuilderError(`package.json not found in project root: ${root}`);
38
+ }
39
+ const sourcePkgJson = readPackageJson(pkgJsonPath);
40
+ // 校验生产依赖合法性(禁止 file: 本地协议)
41
+ assertNoFileProtocolDeps(sourcePkgJson.dependencies, "packed packages");
42
+ // 创建干净临时目录,确保不改写源工程
43
+ const tempBase = mkdtempSync(join(tmpdir(), "ad-pack-"));
44
+ const stagingPkgDir = join(tempBase, "package");
45
+ mkdirSync(stagingPkgDir, { recursive: true });
46
+ try {
47
+ await compileTypeScript(root, stagingPkgDir, plan);
48
+ stageSources(root, stagingPkgDir, plan);
49
+ writeManifestAndPkgJson(stagingPkgDir, plan, sourcePkgJson);
50
+ // 收集打包文件相对清单与摘要
51
+ const relativeFiles = collectRelativeFiles(stagingPkgDir);
52
+ const manifestSummary = {
53
+ actionsCount: plan.actions.length,
54
+ actions: plan.actions.map((a) => a.id),
55
+ assetsCount: plan.assets.length,
56
+ filesCount: relativeFiles.length,
57
+ };
58
+ const tarballName = `${pkgSlug}-${plan.version}.tgz`;
59
+ const targetOutDir = resolve(options.outDir || join(root, "dist"));
60
+ if (options.dryRun) {
61
+ return {
62
+ packageId: plan.packageId,
63
+ packageName: plan.packageName,
64
+ version: plan.version,
65
+ tarballName,
66
+ sizeBytes: 0,
67
+ sha256: "",
68
+ files: relativeFiles,
69
+ manifestSummary,
70
+ };
71
+ }
72
+ mkdirSync(targetOutDir, { recursive: true });
73
+ const finalTarballPath = join(targetOutDir, tarballName);
74
+ if (existsSync(finalTarballPath)) {
75
+ rmSync(finalTarballPath, { force: true });
76
+ }
77
+ const generatedTarball = await runNpmPack(stagingPkgDir, tempBase, tarballName);
78
+ copyFileSync(generatedTarball, finalTarballPath);
79
+ rmSync(generatedTarball, { force: true });
80
+ const stat = statSync(finalTarballPath);
81
+ const fileBuf = readFileSync(finalTarballPath);
82
+ const sha256 = createHash("sha256").update(fileBuf).digest("hex");
83
+ return {
84
+ packageId: plan.packageId,
85
+ packageName: plan.packageName,
86
+ version: plan.version,
87
+ tarballPath: finalTarballPath,
88
+ tarballName,
89
+ sizeBytes: stat.size,
90
+ sha256,
91
+ files: relativeFiles,
92
+ manifestSummary,
93
+ };
94
+ }
95
+ finally {
96
+ if (existsSync(tempBase)) {
97
+ rmSync(tempBase, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
98
+ }
99
+ }
100
+ }
101
+ /**
102
+ * 判断文件路径是否为 TypeScript 源码。
103
+ */
104
+ function isTypeScriptSource(path) {
105
+ return path.endsWith(".ts") || path.endsWith(".mts");
106
+ }
107
+ /**
108
+ * 解析项目本地 TypeScript 编译器(回退到打包工具自身依赖)。
109
+ */
110
+ async function resolveTypeScriptCompiler(root) {
111
+ try {
112
+ const req = createRequire(join(root, "package.json"));
113
+ const tsPath = req.resolve("typescript");
114
+ const imported = await import(tsPath);
115
+ return imported.default || imported;
116
+ }
117
+ catch {
118
+ try {
119
+ const imported = await import("typescript");
120
+ return imported.default || imported;
121
+ }
122
+ catch {
123
+ throw new BuilderError(`TypeScript is required to pack TypeScript Action packages, but 'typescript' could not be resolved from ${root}.`);
124
+ }
125
+ }
126
+ }
127
+ /**
128
+ * 在暂存目录内执行 TypeScript 编译,产出 .js 与 .d.ts。
129
+ * 仅在存在 TypeScript 源码时执行。
130
+ */
131
+ async function compileTypeScript(root, stagingPkgDir, plan) {
132
+ const hasTypeScript = plan.actions.some((a) => isTypeScriptSource(a.entry)) ||
133
+ plan.dependencies.modulesAndAssets.some((m) => (m.type === "module" || m.type === "file") && isTypeScriptSource(m.path));
134
+ if (!hasTypeScript) {
135
+ return;
136
+ }
137
+ const ts = await resolveTypeScriptCompiler(root);
138
+ const tsSourceFiles = new Set();
139
+ for (const act of plan.actions) {
140
+ if (act.isExternal || act.id.includes("/"))
141
+ continue;
142
+ if (isTypeScriptSource(act.entry)) {
143
+ tsSourceFiles.add(act.resolvedPath);
144
+ }
145
+ }
146
+ for (const mod of plan.dependencies.modulesAndAssets) {
147
+ if ((mod.type === "module" || mod.type === "file") && isTypeScriptSource(mod.path)) {
148
+ tsSourceFiles.add(mod.resolvedPath);
149
+ }
150
+ }
151
+ const compilerOptions = {
152
+ target: ts.ScriptTarget.ES2022,
153
+ module: ts.ModuleKind.NodeNext,
154
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
155
+ declaration: true,
156
+ emitDeclarationOnly: false,
157
+ rewriteRelativeImportExtensions: true,
158
+ rootDir: root,
159
+ outDir: stagingPkgDir,
160
+ skipLibCheck: true,
161
+ esModuleInterop: true,
162
+ allowSyntheticDefaultImports: true,
163
+ allowJs: true,
164
+ };
165
+ const tsconfigPath = join(root, "tsconfig.json");
166
+ if (existsSync(tsconfigPath)) {
167
+ try {
168
+ const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
169
+ if (!configFile.error) {
170
+ const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, root);
171
+ Object.assign(compilerOptions, parsedConfig.options, {
172
+ outDir: stagingPkgDir,
173
+ rootDir: root,
174
+ declaration: true,
175
+ emitDeclarationOnly: false,
176
+ rewriteRelativeImportExtensions: true,
177
+ noEmit: false,
178
+ });
179
+ }
180
+ }
181
+ catch {
182
+ // 忽略 tsconfig 解析失败,回退到标准 compilerOptions
183
+ }
184
+ }
185
+ const host = ts.createCompilerHost(compilerOptions);
186
+ const program = ts.createProgram(Array.from(tsSourceFiles), compilerOptions, host);
187
+ const emitResult = program.emit();
188
+ const preEmitDiagnostics = ts.getPreEmitDiagnostics(program);
189
+ const allDiagnostics = [...preEmitDiagnostics, ...emitResult.diagnostics];
190
+ const errors = allDiagnostics.filter((d) => d.category === ts.DiagnosticCategory.Error);
191
+ if (errors.length > 0 || emitResult.emitSkipped) {
192
+ const diagnosticsToFormat = errors.length > 0 ? errors : allDiagnostics;
193
+ const formatted = ts.formatDiagnosticsWithColorAndContext(diagnosticsToFormat, {
194
+ getCanonicalFileName: (f) => f,
195
+ getCurrentDirectory: () => root,
196
+ getNewLine: () => "\n",
197
+ });
198
+ throw new BuilderError(`TypeScript compilation failed during pack:\n${formatted}`);
199
+ }
200
+ }
201
+ /**
202
+ * 拷贝声明的非 TypeScript 模块、静态资产、原生 JavaScript 入口、Playbook 与基础文档到暂存目录。
203
+ */
204
+ function stageSources(root, stagingPkgDir, plan) {
205
+ // 拷贝声明的非 TypeScript 模块与静态资产
206
+ for (const dep of plan.dependencies.modulesAndAssets) {
207
+ if ((dep.type === "asset" || dep.type === "module" || dep.type === "file") &&
208
+ existsSync(dep.resolvedPath)) {
209
+ if (isTypeScriptSource(dep.path)) {
210
+ continue; // 已由 TypeScript 编译生成 .js 与 .d.ts
211
+ }
212
+ const destPath = join(stagingPkgDir, dep.path);
213
+ mkdirSync(dirname(destPath), { recursive: true });
214
+ copyFileSync(dep.resolvedPath, destPath);
215
+ }
216
+ }
217
+ // 拷贝原生 JavaScript Action 入口(若存在)
218
+ for (const act of plan.actions) {
219
+ if (!isTypeScriptSource(act.entry)) {
220
+ const destPath = join(stagingPkgDir, act.entry);
221
+ mkdirSync(dirname(destPath), { recursive: true });
222
+ copyFileSync(act.resolvedPath, destPath);
223
+ }
224
+ }
225
+ // 拷贝 Playbook 规程文件
226
+ for (const pb of plan.playbooks) {
227
+ if (existsSync(pb.filePath)) {
228
+ const destPb = join(stagingPkgDir, relative(root, pb.filePath));
229
+ mkdirSync(dirname(destPb), { recursive: true });
230
+ copyFileSync(pb.filePath, destPb);
231
+ }
232
+ }
233
+ // 拷贝 README 等基础文档(若存在)
234
+ for (const doc of ["README.md", "readme.md", "LICENSE", "license"]) {
235
+ const srcDoc = join(root, doc);
236
+ if (existsSync(srcDoc)) {
237
+ copyFileSync(srcDoc, join(stagingPkgDir, doc));
238
+ }
239
+ }
240
+ }
241
+ /**
242
+ * 计算编译后的 Action 入口相对路径(.ts 转 .js、.mts 转 .mjs)。
243
+ */
244
+ function compiledEntryFor(action) {
245
+ if (action.entry.endsWith(".ts")) {
246
+ return action.entry.slice(0, -3) + ".js";
247
+ }
248
+ if (action.entry.endsWith(".mts")) {
249
+ return action.entry.slice(0, -4) + ".mjs";
250
+ }
251
+ return action.entry;
252
+ }
253
+ /**
254
+ * 生成并写入严格校验后的 actiondock.json 与规范化 package.json。
255
+ */
256
+ function writeManifestAndPkgJson(stagingPkgDir, plan, sourcePkgJson) {
257
+ // 构建编译后的 Action 清单字典并严格校验入口扩展名
258
+ const compiledEntries = new Map();
259
+ for (const act of plan.actions) {
260
+ if (act.isExternal || act.id.includes("/"))
261
+ continue;
262
+ const destEntry = compiledEntryFor(act);
263
+ if (!destEntry.endsWith(".js") && !destEntry.endsWith(".mjs")) {
264
+ throw new BuilderError(`Action '${act.id}' entry '${destEntry}' must point to compiled .js or .mjs`);
265
+ }
266
+ if (!existsSync(join(stagingPkgDir, destEntry))) {
267
+ throw new BuilderError(`Compiled action entry file not found: ${destEntry}`);
268
+ }
269
+ compiledEntries.set(act.id, destEntry);
270
+ }
271
+ // 严格生成与校验 actiondock.json(严禁包含废弃 actionsDir 或 playbooksDir 字段)
272
+ const packedConfig = serializePlanManifest(plan, {
273
+ includeSchema: true,
274
+ configBeforeActions: true,
275
+ includePlaybooks: false,
276
+ actionEntryOverride: (act) => compiledEntries.get(act.id),
277
+ });
278
+ if ("actionsDir" in packedConfig || "playbooksDir" in packedConfig) {
279
+ throw new BuilderError("Packed manifest must not contain deprecated fields 'actionsDir' or 'playbooksDir'.");
280
+ }
281
+ if (packedConfig.actions && typeof packedConfig.actions === "object") {
282
+ assertValidManifestActionIds(packedConfig.actions);
283
+ }
284
+ writeFileSync(join(stagingPkgDir, "actiondock.json"), JSON.stringify(packedConfig, null, 2) + "\n", "utf-8");
285
+ // 规范化 package.json:导出 ./actiondock.json,设定 node 引擎约束与 ESM 类型
286
+ const normalizedPkg = normalizePkgExportsAndEngines(sourcePkgJson);
287
+ writeFileSync(join(stagingPkgDir, "package.json"), JSON.stringify(normalizedPkg, null, 2) + "\n", "utf-8");
288
+ }
289
+ /**
290
+ * 在暂存目录调用带有 --ignore-scripts 的 npm pack 生成标准 npm 压缩包(.tgz)。
291
+ * 优先解析 --json 结构化输出,失败时回退到尾部行文件名解析。
292
+ * Windows 兼容:npm 是 .cmd 脚本,无 shell 直接 spawn 会 ENOENT/EINVAL。
293
+ */
294
+ async function runNpmPack(stagingPkgDir, packDestination, fallbackName) {
295
+ const { stdout } = await new Promise((resolvePromise, rejectPromise) => {
296
+ const child = spawn("npm", ["pack", "--ignore-scripts", "--json", "--pack-destination", packDestination], {
297
+ cwd: stagingPkgDir,
298
+ shell: process.platform === "win32",
299
+ });
300
+ let stdoutBuf = "";
301
+ let stderrBuf = "";
302
+ child.stdout.on("data", (chunk) => (stdoutBuf += chunk));
303
+ child.stderr.on("data", (chunk) => (stderrBuf += chunk));
304
+ child.on("error", rejectPromise);
305
+ child.on("close", (code) => {
306
+ if (code === 0) {
307
+ resolvePromise({ stdout: stdoutBuf, stderr: stderrBuf });
308
+ }
309
+ else {
310
+ rejectPromise(new BuilderError(`npm pack exited with code ${code}. Output:\n${stdoutBuf}\n${stderrBuf}`));
311
+ }
312
+ });
313
+ });
314
+ // 优先解析 --json 结构化输出中的压缩包文件名
315
+ let generatedName;
316
+ const trimmed = stdout.trim();
317
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
318
+ try {
319
+ const parsed = JSON.parse(trimmed);
320
+ const items = Array.isArray(parsed) ? parsed : [parsed];
321
+ const filename = items[0]?.filename;
322
+ if (typeof filename === "string" && filename.length > 0) {
323
+ generatedName = filename;
324
+ }
325
+ }
326
+ catch {
327
+ // 回退到尾部行解析
328
+ }
329
+ }
330
+ if (!generatedName) {
331
+ const packLines = trimmed.split("\n");
332
+ generatedName = packLines.pop()?.trim() || fallbackName;
333
+ }
334
+ const generatedTarball = join(packDestination, generatedName);
335
+ if (!existsSync(generatedTarball)) {
336
+ throw new BuilderError(`npm pack did not produce expected tarball at ${generatedTarball}. Output:\n${stdout}`);
337
+ }
338
+ return generatedTarball;
339
+ }