@actiondock/builder 2.0.11 → 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.
- package/README.md +46 -30
- package/dist/archive.d.ts +47 -0
- package/dist/archive.js +519 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +576 -0
- package/dist/dependency-check.d.ts +14 -0
- package/dist/dependency-check.js +246 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +22 -0
- package/dist/exporter.d.ts +69 -0
- package/dist/exporter.js +761 -0
- package/dist/fs-utils.d.ts +16 -0
- package/dist/fs-utils.js +146 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +21 -0
- package/dist/manifest.d.ts +75 -0
- package/dist/manifest.js +213 -0
- package/dist/pack.d.ts +13 -0
- package/dist/pack.js +339 -0
- package/dist/planner.d.ts +68 -0
- package/dist/planner.js +681 -0
- package/dist/types.d.ts +442 -0
- package/dist/types.js +1 -0
- package/package.json +15 -17
- package/src/archive.ts +0 -248
- package/src/build.ts +0 -106
- package/src/compiler.ts +0 -303
- package/src/errors.ts +0 -67
- package/src/exporter.ts +0 -680
- package/src/index.ts +0 -6
- package/src/planner.ts +0 -731
- package/src/types.ts +0 -337
package/dist/planner.js
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
4
|
+
import { assertPathWithinRoot, loadManifest, loadPlaybooks, loadProjectConfig, resolveActionProjectSync, validateManifest, } from "@actiondock/core";
|
|
5
|
+
import { PlannerError } from "./errors.js";
|
|
6
|
+
import { assertRelativeDependenciesIntegrity } from "./dependency-check.js";
|
|
7
|
+
/**
|
|
8
|
+
* 递归扫描指定目录下的文件,返回绝对路径列表。
|
|
9
|
+
* 通过安全路径边界能力校验,阻止越界并避免符号链接逃逸。
|
|
10
|
+
*/
|
|
11
|
+
function walkDirectory(dir, rootDir = dir, visited = new Set()) {
|
|
12
|
+
if (!existsSync(dir))
|
|
13
|
+
return [];
|
|
14
|
+
const results = [];
|
|
15
|
+
const entries = readdirSync(dir);
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const fullPath = join(dir, entry);
|
|
18
|
+
if (rootDir) {
|
|
19
|
+
try {
|
|
20
|
+
assertPathWithinRoot(rootDir, fullPath, "planner path");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
let real;
|
|
27
|
+
try {
|
|
28
|
+
real = existsSync(fullPath) ? realpathSync(fullPath) : fullPath;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (rootDir) {
|
|
34
|
+
try {
|
|
35
|
+
assertPathWithinRoot(rootDir, real, "planner path");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// 忽略并跳过指向项目根目录外部的软链接
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (visited.has(real)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
visited.add(real);
|
|
46
|
+
try {
|
|
47
|
+
const stat = statSync(fullPath);
|
|
48
|
+
if (stat.isDirectory()) {
|
|
49
|
+
results.push(...walkDirectory(fullPath, rootDir, visited));
|
|
50
|
+
}
|
|
51
|
+
else if (stat.isFile()) {
|
|
52
|
+
results.push(fullPath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// 忽略无法访问或损坏的文件/符号链接
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 忽略的文件模式判断(排除测试文件、类型声明文件以及构建/版本控制等私有目录)。
|
|
64
|
+
*/
|
|
65
|
+
function isIgnoredPath(relPath) {
|
|
66
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
67
|
+
return (normalized.startsWith("node_modules/") ||
|
|
68
|
+
normalized.includes("/node_modules/") ||
|
|
69
|
+
normalized.startsWith(".git/") ||
|
|
70
|
+
normalized.startsWith("dist/") ||
|
|
71
|
+
normalized.startsWith(".actiondock/") ||
|
|
72
|
+
normalized.endsWith(".test.ts") ||
|
|
73
|
+
normalized.endsWith(".test.js") ||
|
|
74
|
+
normalized.endsWith(".test.tsx") ||
|
|
75
|
+
normalized.endsWith(".test.jsx") ||
|
|
76
|
+
normalized.endsWith(".spec.ts") ||
|
|
77
|
+
normalized.endsWith(".spec.js") ||
|
|
78
|
+
normalized.endsWith(".spec.tsx") ||
|
|
79
|
+
normalized.endsWith(".spec.jsx") ||
|
|
80
|
+
normalized.endsWith(".d.ts"));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 支持的包管理器锁文件候选列表。
|
|
84
|
+
*/
|
|
85
|
+
const KNOWN_LOCKFILES = [
|
|
86
|
+
"package-lock.json",
|
|
87
|
+
"npm-shrinkwrap.json",
|
|
88
|
+
"bun.lockb",
|
|
89
|
+
"bun.lock",
|
|
90
|
+
"pnpm-lock.yaml",
|
|
91
|
+
"yarn.lock",
|
|
92
|
+
];
|
|
93
|
+
/**
|
|
94
|
+
* 读取并计算项目锁文件元数据及 SHA-256 摘要。
|
|
95
|
+
*/
|
|
96
|
+
function computeLockfileInfo(projectRoot, preferredLockfile) {
|
|
97
|
+
if (preferredLockfile) {
|
|
98
|
+
const lockPath = resolve(projectRoot, preferredLockfile);
|
|
99
|
+
assertPathWithinRoot(projectRoot, lockPath, "lockfile");
|
|
100
|
+
if (existsSync(lockPath) && statSync(lockPath).isFile()) {
|
|
101
|
+
const content = readFileSync(lockPath);
|
|
102
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
103
|
+
return {
|
|
104
|
+
name: basename(lockPath),
|
|
105
|
+
path: lockPath,
|
|
106
|
+
sha256,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
throw new PlannerError(`Specified lockfile not found on disk: ${preferredLockfile}`, "LOCKFILE_NOT_FOUND");
|
|
110
|
+
}
|
|
111
|
+
for (const lockFileName of KNOWN_LOCKFILES) {
|
|
112
|
+
const lockPath = join(projectRoot, lockFileName);
|
|
113
|
+
if (existsSync(lockPath)) {
|
|
114
|
+
try {
|
|
115
|
+
if (statSync(lockPath).isFile()) {
|
|
116
|
+
const content = readFileSync(lockPath);
|
|
117
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
118
|
+
return {
|
|
119
|
+
name: lockFileName,
|
|
120
|
+
path: lockPath,
|
|
121
|
+
sha256,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 构造备用清单映射,仅读取文件系统条目与配置声明,杜绝 AST 源码分析与动态代码执行。
|
|
134
|
+
*/
|
|
135
|
+
function generateFallbackManifest(projectRoot, actionsDir = "actions", config) {
|
|
136
|
+
const dir = join(projectRoot, actionsDir);
|
|
137
|
+
const actions = {};
|
|
138
|
+
// 若 actiondock.json 中声明了 actions 字典,读取显式声明
|
|
139
|
+
const configActions = config?.actions;
|
|
140
|
+
if (configActions && typeof configActions === "object") {
|
|
141
|
+
for (const [id, rawEntry] of Object.entries(configActions)) {
|
|
142
|
+
if (typeof rawEntry === "object" && rawEntry !== null) {
|
|
143
|
+
const entry = rawEntry;
|
|
144
|
+
actions[id] = {
|
|
145
|
+
entry: entry.entry || join(actionsDir, `${id}.ts`).replace(/\\/g, "/"),
|
|
146
|
+
description: entry.description || `Action ${id}`,
|
|
147
|
+
uses: Array.isArray(entry.uses) ? entry.uses : [],
|
|
148
|
+
inputSchema: entry.inputSchema,
|
|
149
|
+
outputSchema: entry.outputSchema,
|
|
150
|
+
tags: entry.tags,
|
|
151
|
+
annotations: entry.annotations,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// 若 actions 目录存在,基于文件名建立默认映射
|
|
157
|
+
if (existsSync(dir)) {
|
|
158
|
+
const files = walkDirectory(dir, projectRoot).filter((f) => (f.endsWith(".ts") || f.endsWith(".js")) &&
|
|
159
|
+
!f.endsWith(".d.ts") &&
|
|
160
|
+
!f.endsWith(".test.ts") &&
|
|
161
|
+
!f.endsWith(".spec.ts"));
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
const relPath = relative(projectRoot, file).replace(/\\/g, "/");
|
|
164
|
+
const filename = basename(file);
|
|
165
|
+
const actionId = filename.replace(/\.(ts|js)$/, "");
|
|
166
|
+
if (!actions[actionId]) {
|
|
167
|
+
actions[actionId] = {
|
|
168
|
+
entry: relPath,
|
|
169
|
+
description: `Action ${actionId}`,
|
|
170
|
+
uses: [],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
schemaVersion: 2,
|
|
177
|
+
id: config?.id || basename(projectRoot),
|
|
178
|
+
actions,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 收集项目根目录 package.json 中声明的外部 npm 依赖。
|
|
183
|
+
*/
|
|
184
|
+
function extractExternalDependencies(projectRoot) {
|
|
185
|
+
const pkgPath = join(projectRoot, "package.json");
|
|
186
|
+
if (!existsSync(pkgPath))
|
|
187
|
+
return [];
|
|
188
|
+
try {
|
|
189
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
190
|
+
const parsed = JSON.parse(raw);
|
|
191
|
+
const deps = [];
|
|
192
|
+
if (parsed.dependencies && typeof parsed.dependencies === "object") {
|
|
193
|
+
for (const [name, versionRange] of Object.entries(parsed.dependencies)) {
|
|
194
|
+
deps.push({
|
|
195
|
+
name,
|
|
196
|
+
versionRange: String(versionRange),
|
|
197
|
+
isDev: false,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (parsed.devDependencies && typeof parsed.devDependencies === "object") {
|
|
202
|
+
for (const [name, versionRange] of Object.entries(parsed.devDependencies)) {
|
|
203
|
+
deps.push({
|
|
204
|
+
name,
|
|
205
|
+
versionRange: String(versionRange),
|
|
206
|
+
isDev: true,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return deps;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 声明式选择集与构建规划器 (SelectionPlanner)。
|
|
218
|
+
* 仅读取 actiondock.json、锁文件与显式构建参数,彻底移除 TypeScript AST 源码扫描。
|
|
219
|
+
*/
|
|
220
|
+
export class SelectionPlanner {
|
|
221
|
+
projectRoot;
|
|
222
|
+
constructor(options) {
|
|
223
|
+
this.projectRoot = resolve(options?.projectRoot || process.cwd());
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* 解析项目配置:优先使用传入对象,缺失则读取 actiondock.json。
|
|
227
|
+
*/
|
|
228
|
+
resolveConfig(root, options) {
|
|
229
|
+
if (options?.config) {
|
|
230
|
+
return options.config;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
return loadProjectConfig(root);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
throw new PlannerError(`Failed to load project config: ${err.message}`, "CONFIG_LOAD_ERROR");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 解析声明式清单:优先使用传入清单,缺失则读取 actiondock.json,仍未提供则采用安全备用清单。
|
|
241
|
+
*/
|
|
242
|
+
resolveManifest(root, config, manifest) {
|
|
243
|
+
if (manifest) {
|
|
244
|
+
return manifest;
|
|
245
|
+
}
|
|
246
|
+
const loaded = loadManifest(root);
|
|
247
|
+
if (loaded) {
|
|
248
|
+
return loaded;
|
|
249
|
+
}
|
|
250
|
+
return generateFallbackManifest(root, config.actionsDir || "actions", config);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* 收集 Playbook 规程定义(统一复用 core 的 loadPlaybooks,清单为唯一事实源)。
|
|
254
|
+
*/
|
|
255
|
+
collectPlaybooks(root, config, manifest) {
|
|
256
|
+
const effectiveManifest = {
|
|
257
|
+
...(config || {}),
|
|
258
|
+
...(manifest || {}),
|
|
259
|
+
playbooks: {
|
|
260
|
+
...(config?.playbooks || {}),
|
|
261
|
+
...(manifest?.playbooks || {}),
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
const playbooksDefinitions = loadPlaybooks(root, config.playbooksDir || manifest?.playbooksDir || "playbooks", effectiveManifest);
|
|
265
|
+
const playbooksMap = new Map();
|
|
266
|
+
for (const [id, def] of playbooksDefinitions.entries()) {
|
|
267
|
+
playbooksMap.set(id, {
|
|
268
|
+
id: def.id,
|
|
269
|
+
filePath: def.filePath,
|
|
270
|
+
actions: def.actions,
|
|
271
|
+
description: def.description,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return playbooksMap;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* 计算初始 Action 与 Playbook 集合(显式声明过滤、uses 顶层依赖与默认全集)。
|
|
278
|
+
*/
|
|
279
|
+
resolveInitialSelection(options, manifest, config, playbooksMap) {
|
|
280
|
+
const initialActionIds = new Set();
|
|
281
|
+
let selectedPlaybooks = [];
|
|
282
|
+
// 若指定了 Playbook 列表,进行过滤并提取关联 Action
|
|
283
|
+
if (options?.playbooks && options.playbooks.length > 0) {
|
|
284
|
+
for (const pbId of options.playbooks) {
|
|
285
|
+
const pb = playbooksMap.get(pbId);
|
|
286
|
+
if (!pb) {
|
|
287
|
+
throw new PlannerError(`Playbook '${pbId}' specified in build options was not found in project`, "PLAYBOOK_NOT_FOUND");
|
|
288
|
+
}
|
|
289
|
+
selectedPlaybooks.push(pb);
|
|
290
|
+
if (pb.actions && pb.actions.length > 0) {
|
|
291
|
+
for (const act of pb.actions) {
|
|
292
|
+
initialActionIds.add(act);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
selectedPlaybooks = Array.from(playbooksMap.values());
|
|
299
|
+
}
|
|
300
|
+
const manifestActions = manifest.actions || {};
|
|
301
|
+
// 若显式指定了 Action 列表,加入集合
|
|
302
|
+
if (options?.actions && options.actions.length > 0) {
|
|
303
|
+
for (const actId of options.actions) {
|
|
304
|
+
if (!manifestActions[actId]) {
|
|
305
|
+
throw new PlannerError(`Action '${actId}' specified in build options was not found in manifest`, "ACTION_NOT_FOUND");
|
|
306
|
+
}
|
|
307
|
+
initialActionIds.add(actId);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// 若均未显式指定,默认包含清单中的全部 Action
|
|
311
|
+
if ((!options?.actions || options.actions.length === 0) && (!options?.playbooks || options.playbooks.length === 0)) {
|
|
312
|
+
for (const actId of Object.keys(manifestActions)) {
|
|
313
|
+
initialActionIds.add(actId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// 当前包在 actiondock.json 中声明的顶层 uses 直接依赖
|
|
317
|
+
const currentPkgUses = config?.uses;
|
|
318
|
+
if (Array.isArray(currentPkgUses)) {
|
|
319
|
+
for (const depId of currentPkgUses) {
|
|
320
|
+
if (typeof depId === "string" && depId.trim()) {
|
|
321
|
+
initialActionIds.add(depId.trim());
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return { initialActionIds, selectedPlaybooks };
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 进行依赖闭包(uses)解析计算,自动处理环形依赖与传递依赖(支持当前清单与已链接外部包)。
|
|
329
|
+
*/
|
|
330
|
+
resolveClosure(root, manifest, config, initialActionIds) {
|
|
331
|
+
const resolvedActionIds = new Set();
|
|
332
|
+
const queue = Array.from(initialActionIds);
|
|
333
|
+
const externalActionRoots = new Map();
|
|
334
|
+
const externalActionEntries = new Map();
|
|
335
|
+
const manifestActions = manifest.actions || {};
|
|
336
|
+
while (queue.length > 0) {
|
|
337
|
+
const currentId = queue.shift();
|
|
338
|
+
if (resolvedActionIds.has(currentId)) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
let entry = manifestActions[currentId] || config?.actions?.[currentId];
|
|
342
|
+
let entryRoot = root;
|
|
343
|
+
if (!entry) {
|
|
344
|
+
try {
|
|
345
|
+
const resolvedExternal = resolveActionProjectSync(currentId, root);
|
|
346
|
+
if (resolvedExternal && existsSync(resolvedExternal.projectRoot)) {
|
|
347
|
+
const extRoot = resolvedExternal.projectRoot;
|
|
348
|
+
let extConfig;
|
|
349
|
+
try {
|
|
350
|
+
extConfig = loadProjectConfig(extRoot);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
extConfig = {
|
|
354
|
+
id: resolvedExternal.packageId,
|
|
355
|
+
name: resolvedExternal.packageId,
|
|
356
|
+
version: "0.1.0",
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const externalManifest = loadManifest(extRoot);
|
|
360
|
+
let externalEntry = externalManifest?.actions?.[resolvedExternal.actionId] ||
|
|
361
|
+
extConfig?.actions?.[resolvedExternal.actionId];
|
|
362
|
+
if (!externalEntry) {
|
|
363
|
+
const staticManifest = generateFallbackManifest(extRoot, extConfig.actionsDir || "actions", extConfig);
|
|
364
|
+
externalEntry = staticManifest.actions?.[resolvedExternal.actionId];
|
|
365
|
+
}
|
|
366
|
+
if (externalEntry) {
|
|
367
|
+
entry = externalEntry;
|
|
368
|
+
entryRoot = extRoot;
|
|
369
|
+
// 依赖包 actiondock.json 中声明的传递依赖闭包
|
|
370
|
+
const extPkgUses = extConfig?.uses;
|
|
371
|
+
if (Array.isArray(extPkgUses)) {
|
|
372
|
+
for (const u of extPkgUses) {
|
|
373
|
+
if (typeof u === "string" && u.trim() && !resolvedActionIds.has(u.trim())) {
|
|
374
|
+
queue.push(u.trim());
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// 忽略
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (!entry) {
|
|
386
|
+
throw new PlannerError(`Action '${currentId}' referenced in dependency closure (uses) was not found in manifest or linked packages`, "MISSING_DEPENDENCY");
|
|
387
|
+
}
|
|
388
|
+
if (entryRoot !== root) {
|
|
389
|
+
externalActionRoots.set(currentId, entryRoot);
|
|
390
|
+
externalActionEntries.set(currentId, entry);
|
|
391
|
+
}
|
|
392
|
+
resolvedActionIds.add(currentId);
|
|
393
|
+
if (entry.uses && Array.isArray(entry.uses)) {
|
|
394
|
+
for (const depId of entry.uses) {
|
|
395
|
+
if (!resolvedActionIds.has(depId)) {
|
|
396
|
+
queue.push(depId);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return { resolvedActionIds, externalActionRoots, externalActionEntries };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 构造 Action 依赖结构(闭包结果物化为 ActionDependency 列表)。
|
|
405
|
+
*/
|
|
406
|
+
buildActionDependencies(root, closure, manifest) {
|
|
407
|
+
const manifestActions = manifest.actions || {};
|
|
408
|
+
const actionDependencies = [];
|
|
409
|
+
for (const actId of closure.resolvedActionIds) {
|
|
410
|
+
const entry = manifestActions[actId] || closure.externalActionEntries.get(actId);
|
|
411
|
+
const entryRoot = closure.externalActionRoots.get(actId) || root;
|
|
412
|
+
if (!entry) {
|
|
413
|
+
throw new PlannerError(`Action '${actId}' entry not found`, "ENTRY_FILE_NOT_FOUND");
|
|
414
|
+
}
|
|
415
|
+
const resolvedPath = resolve(entryRoot, entry.entry);
|
|
416
|
+
if (!existsSync(resolvedPath)) {
|
|
417
|
+
throw new PlannerError(`Action '${actId}' entry file not found on disk: ${entry.entry}`, "ENTRY_FILE_NOT_FOUND");
|
|
418
|
+
}
|
|
419
|
+
const isExternal = closure.externalActionRoots.has(actId) || actId.includes("/");
|
|
420
|
+
actionDependencies.push({
|
|
421
|
+
id: actId,
|
|
422
|
+
entry: entry.entry,
|
|
423
|
+
resolvedPath,
|
|
424
|
+
isExternal,
|
|
425
|
+
uses: entry.uses || [],
|
|
426
|
+
description: entry.description,
|
|
427
|
+
inputSchema: entry.inputSchema,
|
|
428
|
+
outputSchema: entry.outputSchema,
|
|
429
|
+
tags: entry.tags,
|
|
430
|
+
annotations: entry.annotations,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return actionDependencies;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* 收集声明式文件与资产依赖结构(仅依据显式声明,杜绝 AST 依赖扫描与未声明模块猜测)。
|
|
437
|
+
*/
|
|
438
|
+
collectDeclaredFilesAndAssets(root, options, manifest, config, actionDependencies, selectedPlaybooks) {
|
|
439
|
+
const modulesAndAssets = [];
|
|
440
|
+
const assetPathSet = new Set();
|
|
441
|
+
const filePathSet = new Set();
|
|
442
|
+
const actionPathSet = new Set(actionDependencies.map((a) => a.resolvedPath));
|
|
443
|
+
// 收集 files 声明(来源:options.files、manifest.files、config.files)
|
|
444
|
+
const declaredFiles = new Set();
|
|
445
|
+
if (options?.files && Array.isArray(options.files)) {
|
|
446
|
+
for (const f of options.files)
|
|
447
|
+
declaredFiles.add(f);
|
|
448
|
+
}
|
|
449
|
+
if (manifest?.files && Array.isArray(manifest.files)) {
|
|
450
|
+
for (const f of manifest.files)
|
|
451
|
+
declaredFiles.add(f);
|
|
452
|
+
}
|
|
453
|
+
if (config?.files && Array.isArray(config.files)) {
|
|
454
|
+
for (const f of config.files)
|
|
455
|
+
declaredFiles.add(f);
|
|
456
|
+
}
|
|
457
|
+
for (const declaredRel of declaredFiles) {
|
|
458
|
+
const resolvedFile = resolve(root, declaredRel);
|
|
459
|
+
assertPathWithinRoot(root, resolvedFile, "files");
|
|
460
|
+
if (!existsSync(resolvedFile)) {
|
|
461
|
+
throw new PlannerError(`File or directory declared in 'files' not found: ${declaredRel}`, "FILE_NOT_FOUND");
|
|
462
|
+
}
|
|
463
|
+
const stat = statSync(resolvedFile);
|
|
464
|
+
if (stat.isDirectory()) {
|
|
465
|
+
const walked = walkDirectory(resolvedFile, root);
|
|
466
|
+
for (const f of walked) {
|
|
467
|
+
const rel = relative(root, f).replace(/\\/g, "/");
|
|
468
|
+
if (!isIgnoredPath(rel) && !actionPathSet.has(f) && !filePathSet.has(rel)) {
|
|
469
|
+
filePathSet.add(rel);
|
|
470
|
+
modulesAndAssets.push({
|
|
471
|
+
path: rel,
|
|
472
|
+
resolvedPath: f,
|
|
473
|
+
type: "module",
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else if (stat.isFile()) {
|
|
479
|
+
const rel = relative(root, resolvedFile).replace(/\\/g, "/");
|
|
480
|
+
if (!actionPathSet.has(resolvedFile) && !filePathSet.has(rel)) {
|
|
481
|
+
filePathSet.add(rel);
|
|
482
|
+
modulesAndAssets.push({
|
|
483
|
+
path: rel,
|
|
484
|
+
resolvedPath: resolvedFile,
|
|
485
|
+
type: "module",
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// 收集 assets 声明(来源:options.assets、manifest.assets、config.assets)
|
|
491
|
+
const declaredAssets = new Set();
|
|
492
|
+
if (options?.assets && Array.isArray(options.assets)) {
|
|
493
|
+
for (const a of options.assets)
|
|
494
|
+
declaredAssets.add(a);
|
|
495
|
+
}
|
|
496
|
+
if (manifest.assets && Array.isArray(manifest.assets)) {
|
|
497
|
+
for (const a of manifest.assets)
|
|
498
|
+
declaredAssets.add(a);
|
|
499
|
+
}
|
|
500
|
+
if (config?.assets && Array.isArray(config.assets)) {
|
|
501
|
+
for (const a of config.assets)
|
|
502
|
+
declaredAssets.add(a);
|
|
503
|
+
}
|
|
504
|
+
for (const declaredRel of declaredAssets) {
|
|
505
|
+
const resolvedAsset = resolve(root, declaredRel);
|
|
506
|
+
assertPathWithinRoot(root, resolvedAsset, "assets");
|
|
507
|
+
if (!existsSync(resolvedAsset)) {
|
|
508
|
+
throw new PlannerError(`Asset declared in 'assets' not found: ${declaredRel}`, "ASSET_NOT_FOUND");
|
|
509
|
+
}
|
|
510
|
+
const stat = statSync(resolvedAsset);
|
|
511
|
+
if (stat.isDirectory()) {
|
|
512
|
+
const walked = walkDirectory(resolvedAsset, root);
|
|
513
|
+
for (const f of walked) {
|
|
514
|
+
const rel = relative(root, f).replace(/\\/g, "/");
|
|
515
|
+
if (!assetPathSet.has(rel)) {
|
|
516
|
+
assetPathSet.add(rel);
|
|
517
|
+
modulesAndAssets.push({
|
|
518
|
+
path: rel,
|
|
519
|
+
resolvedPath: f,
|
|
520
|
+
type: "asset",
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else if (stat.isFile()) {
|
|
526
|
+
const rel = relative(root, resolvedAsset).replace(/\\/g, "/");
|
|
527
|
+
if (!assetPathSet.has(rel)) {
|
|
528
|
+
assetPathSet.add(rel);
|
|
529
|
+
modulesAndAssets.push({
|
|
530
|
+
path: rel,
|
|
531
|
+
resolvedPath: resolvedAsset,
|
|
532
|
+
type: "asset",
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// 默认 assets 目录扫描
|
|
538
|
+
const defaultAssetsDir = join(root, "assets");
|
|
539
|
+
if (existsSync(defaultAssetsDir)) {
|
|
540
|
+
const assetFiles = walkDirectory(defaultAssetsDir, root);
|
|
541
|
+
for (const file of assetFiles) {
|
|
542
|
+
const rel = relative(root, file).replace(/\\/g, "/");
|
|
543
|
+
if (!assetPathSet.has(rel)) {
|
|
544
|
+
assetPathSet.add(rel);
|
|
545
|
+
modulesAndAssets.push({
|
|
546
|
+
path: rel,
|
|
547
|
+
resolvedPath: file,
|
|
548
|
+
type: "asset",
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Playbook 规程文档依赖
|
|
554
|
+
for (const pb of selectedPlaybooks) {
|
|
555
|
+
const rel = relative(root, pb.filePath).replace(/\\/g, "/");
|
|
556
|
+
modulesAndAssets.push({
|
|
557
|
+
path: rel,
|
|
558
|
+
resolvedPath: pb.filePath,
|
|
559
|
+
type: "playbook",
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
// 项目核心配置文件
|
|
563
|
+
const configPath = join(root, "actiondock.json");
|
|
564
|
+
if (existsSync(configPath)) {
|
|
565
|
+
modulesAndAssets.push({
|
|
566
|
+
path: "actiondock.json",
|
|
567
|
+
resolvedPath: configPath,
|
|
568
|
+
type: "config",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
modulesAndAssets,
|
|
573
|
+
assets: Array.from(assetPathSet),
|
|
574
|
+
files: Array.from(filePathSet),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* 执行依赖闭包裁剪与构建规划。
|
|
579
|
+
* 主流程仅做编排,各阶段职责由私有方法承担。
|
|
580
|
+
*
|
|
581
|
+
* @param options 规划参数
|
|
582
|
+
* @returns 完整的 SelectionPlan / BuildPlan 结构
|
|
583
|
+
*/
|
|
584
|
+
plan(options) {
|
|
585
|
+
const root = resolve(options?.projectRoot || this.projectRoot);
|
|
586
|
+
// 解析项目配置与声明式清单
|
|
587
|
+
const config = this.resolveConfig(root, options);
|
|
588
|
+
const manifest = this.resolveManifest(root, config, options?.manifest);
|
|
589
|
+
const validation = validateManifest(manifest, { projectRoot: root });
|
|
590
|
+
if (!validation.valid) {
|
|
591
|
+
throw new PlannerError(`Invalid actiondock.json: ${(validation.errors || []).join("; ")}`, "INVALID_MANIFEST");
|
|
592
|
+
}
|
|
593
|
+
// 读取 Playbook 规程定义并计算初始选择集
|
|
594
|
+
const playbooksMap = this.collectPlaybooks(root, config, manifest);
|
|
595
|
+
const { initialActionIds, selectedPlaybooks } = this.resolveInitialSelection(options, manifest, config, playbooksMap);
|
|
596
|
+
// 依赖闭包解析与 Action 依赖结构物化
|
|
597
|
+
const closure = this.resolveClosure(root, manifest, config, initialActionIds);
|
|
598
|
+
let finalPlaybooks = selectedPlaybooks;
|
|
599
|
+
// 若指定了 Action 但未指定 Playbook,反向裁剪排除无法满足依赖的 Playbook
|
|
600
|
+
if (options?.actions && options.actions.length > 0 && (!options.playbooks || options.playbooks.length === 0)) {
|
|
601
|
+
finalPlaybooks = selectedPlaybooks.filter((pb) => {
|
|
602
|
+
if (!pb.actions || pb.actions.length === 0)
|
|
603
|
+
return true;
|
|
604
|
+
return pb.actions.every((a) => closure.resolvedActionIds.has(a));
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
const actionDependencies = this.buildActionDependencies(root, closure, manifest);
|
|
608
|
+
// 收集锁文件信息并执行校验
|
|
609
|
+
const lockfileInfo = computeLockfileInfo(root, options?.lockfile);
|
|
610
|
+
if (options?.expectedLockfileDigest) {
|
|
611
|
+
if (!lockfileInfo) {
|
|
612
|
+
throw new PlannerError(`Lockfile not found in project but expected digest was specified: ${options.expectedLockfileDigest}`, "LOCKFILE_NOT_FOUND");
|
|
613
|
+
}
|
|
614
|
+
if (lockfileInfo.sha256 !== options.expectedLockfileDigest) {
|
|
615
|
+
throw new PlannerError(`Lockfile digest mismatch: expected ${options.expectedLockfileDigest} but got ${lockfileInfo.sha256}`, "LOCKFILE_DIGEST_MISMATCH");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// 收集声明式文件与资产依赖
|
|
619
|
+
const declaredFiles = this.collectDeclaredFilesAndAssets(root, options, manifest, config, actionDependencies, finalPlaybooks);
|
|
620
|
+
// 外部 npm 依赖解析与最终规划产物组装
|
|
621
|
+
const externalDependencies = extractExternalDependencies(root);
|
|
622
|
+
const dependencies = {
|
|
623
|
+
actions: actionDependencies,
|
|
624
|
+
modulesAndAssets: declaredFiles.modulesAndAssets,
|
|
625
|
+
external: externalDependencies,
|
|
626
|
+
};
|
|
627
|
+
const externalRoots = Array.from(new Set(closure.externalActionRoots.values()));
|
|
628
|
+
const planResult = {
|
|
629
|
+
packageId: config.id,
|
|
630
|
+
packageName: config.name || config.id,
|
|
631
|
+
version: config.version || "0.1.0",
|
|
632
|
+
description: config.description,
|
|
633
|
+
projectRoot: root,
|
|
634
|
+
actionsDir: config.actionsDir || "actions",
|
|
635
|
+
playbooksDir: config.playbooksDir || "playbooks",
|
|
636
|
+
actions: actionDependencies,
|
|
637
|
+
playbooks: finalPlaybooks,
|
|
638
|
+
dependencies,
|
|
639
|
+
assets: declaredFiles.assets,
|
|
640
|
+
files: declaredFiles.files,
|
|
641
|
+
configDefs: config.config,
|
|
642
|
+
lockfile: lockfileInfo,
|
|
643
|
+
externalProjectRoots: externalRoots,
|
|
644
|
+
metadata: {
|
|
645
|
+
plannedAt: new Date().toISOString(),
|
|
646
|
+
schemaVersion: 2,
|
|
647
|
+
actionCount: actionDependencies.length,
|
|
648
|
+
playbookCount: finalPlaybooks.length,
|
|
649
|
+
lockfileDigest: lockfileInfo?.sha256,
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
if (!options?.skipDependencyValidation) {
|
|
653
|
+
assertRelativeDependenciesIntegrity(root, planResult);
|
|
654
|
+
}
|
|
655
|
+
return planResult;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* 静态辅助调用方法。
|
|
659
|
+
*/
|
|
660
|
+
static plan(options) {
|
|
661
|
+
const planner = new SelectionPlanner({ projectRoot: options.projectRoot });
|
|
662
|
+
return planner.plan(options);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* 构建规划器标准导出。
|
|
667
|
+
* @deprecated SelectionPlanner 的历史别名,请直接使用 SelectionPlanner,将在两个版本后移除。
|
|
668
|
+
*/
|
|
669
|
+
export const BuildPlanner = SelectionPlanner;
|
|
670
|
+
/**
|
|
671
|
+
* 快捷选择规划函数。
|
|
672
|
+
* @deprecated 便捷快捷函数,请直接使用 SelectionPlanner.plan,将在两个版本后移除。
|
|
673
|
+
*/
|
|
674
|
+
export function selectionPlan(options) {
|
|
675
|
+
return SelectionPlanner.plan(options);
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* 快捷构建规划函数标准导出。
|
|
679
|
+
* @deprecated selectionPlan 的历史别名,请改用 SelectionPlanner.plan,将在两个版本后移除。
|
|
680
|
+
*/
|
|
681
|
+
export const buildPlan = selectionPlan;
|