@actiondock/builder 2.18.0 → 2.20.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.
@@ -3,6 +3,8 @@ import type { ExternalDependency } from "./types.js";
3
3
  * 测试与类型声明文件后缀常量(供 fallback 清单过滤与 isIgnoredPath 共享同一口径)。
4
4
  */
5
5
  export declare const TEST_FILE_SUFFIXES: string[];
6
+ /** 运行环境配置不得进入分发产物;普通业务 bindings.json 不受影响。 */
7
+ export declare function isRuntimeConfigFile(relPath: string): boolean;
6
8
  /**
7
9
  * 忽略的文件模式判断(排除测试文件、类型声明文件以及构建/版本控制等私有目录)。
8
10
  */
@@ -15,6 +15,11 @@ export const TEST_FILE_SUFFIXES = [
15
15
  ".spec.jsx",
16
16
  ".d.ts",
17
17
  ];
18
+ /** 运行环境配置不得进入分发产物;普通业务 bindings.json 不受影响。 */
19
+ export function isRuntimeConfigFile(relPath) {
20
+ const segments = relPath.replace(/\\/g, "/").split("/");
21
+ return segments.includes(".actiondock") || segments.at(-1) === "profiles.json";
22
+ }
18
23
  /**
19
24
  * 忽略的文件模式判断(排除测试文件、类型声明文件以及构建/版本控制等私有目录)。
20
25
  */
@@ -24,7 +29,7 @@ export function isIgnoredPath(relPath) {
24
29
  normalized.includes("/node_modules/") ||
25
30
  normalized.startsWith(".git/") ||
26
31
  normalized.startsWith("dist/") ||
27
- normalized.startsWith(".actiondock/") ||
32
+ isRuntimeConfigFile(normalized) ||
28
33
  TEST_FILE_SUFFIXES.some((suffix) => normalized.endsWith(suffix)));
29
34
  }
30
35
  /**
package/dist/planner.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { basename, join, relative, resolve } from "node:path";
3
- import { ActionDockError, ACTION_NOT_FOUND, UNDECLARED_ACTION_DEPENDENCY, loadProjectConfig, } from "@actiondock/core";
4
- import { assertPathWithinRoot, traverseDirectory, loadManifest, loadPlaybooks, validateManifest, } from "@actiondock/core/project";
3
+ import { ActionDockError, ACTION_NOT_FOUND, PACKAGE_NOT_FOUND, UNDECLARED_ACTION_DEPENDENCY, loadProjectConfig, } from "@actiondock/core";
4
+ import { assertPathWithinRoot, traverseDirectory, loadManifest, loadPlaybooks, validateManifest, loadActionBindings, hasRemoteActionBinding, validateCanonicalActionRef, } from "@actiondock/core/project";
5
5
  import { DefaultActionCatalog, PackageGraphBuilder, resolveAction, } from "@actiondock/core/graph";
6
6
  import { PlannerError } from "./errors.js";
7
7
  import { assertRelativeDependenciesIntegrity } from "./dependency-check.js";
8
8
  import { computeLockfileInfo } from "./lockfile-resolver.js";
9
- import { extractExternalDependencies, isIgnoredPath } from "./dependency-resolver.js";
9
+ import { extractExternalDependencies, isIgnoredPath, isRuntimeConfigFile } from "./dependency-resolver.js";
10
10
  /**
11
11
  * 递归扫描指定目录下的文件,返回绝对路径列表。
12
12
  * 目录遍历统一复用 core 的 traverseDirectory 单一事实源,
@@ -15,6 +15,19 @@ import { extractExternalDependencies, isIgnoredPath } from "./dependency-resolve
15
15
  function walkDirectory(dir) {
16
16
  return traverseDirectory(dir).map((entry) => entry.fullPath);
17
17
  }
18
+ /**
19
+ * 判断动作引用是否为完全限定标识符(包含包命名空间与动作名,且不含通配符与非法字符)。
20
+ * 统一复用 core 的 validateCanonicalActionRef 单一事实源。
21
+ */
22
+ function isFullyQualifiedActionRef(ref) {
23
+ try {
24
+ validateCanonicalActionRef(ref);
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
18
31
  /**
19
32
  * 构造备用清单映射,仅读取文件系统条目与配置声明,杜绝 AST 源码分析与动态代码执行。
20
33
  */
@@ -181,81 +194,138 @@ export class SelectionPlanner {
181
194
  */
182
195
  resolveClosure(root, manifest, config, initialActionIds) {
183
196
  const resolvedActionIds = new Set();
184
- // queued 与 resolvedActionIds 联合判重,避免高入度节点在 uses 图中被重复入队导致 O(边数) 重复解析
185
- const queued = new Set(initialActionIds);
186
- const queue = Array.from(initialActionIds);
197
+ const remoteActionRefs = new Set();
198
+ const visitedEdges = new Set();
199
+ const queuedEdges = new Set();
200
+ const queue = [];
201
+ const enqueueDependency = (ref, callerRoot, callerPkgId) => {
202
+ const edgeKey = `${callerRoot}:::${ref}`;
203
+ if (!visitedEdges.has(edgeKey) && !queuedEdges.has(edgeKey)) {
204
+ queuedEdges.add(edgeKey);
205
+ queue.push({ ref, callerRoot, callerPkgId });
206
+ }
207
+ };
208
+ for (const id of initialActionIds) {
209
+ enqueueDependency(id, root, config?.id);
210
+ }
187
211
  const externalActionRoots = new Map();
188
212
  const externalActionEntries = new Map();
189
213
  const manifestActions = manifest.actions || {};
190
214
  const graph = new PackageGraphBuilder({ root, allowDevLinks: true }).buildSync();
191
215
  const catalog = new DefaultActionCatalog(graph);
192
216
  while (queue.length > 0) {
193
- const currentId = queue.shift();
194
- queued.delete(currentId);
195
- if (resolvedActionIds.has(currentId)) {
196
- continue;
197
- }
198
- let entry = manifestActions[currentId] || config?.actions?.[currentId];
199
- let entryRoot = root;
217
+ const currentItem = queue.shift();
218
+ const currentId = currentItem.ref;
219
+ const currentCallerRoot = currentItem.callerRoot;
220
+ const currentCallerPkgId = currentItem.callerPkgId;
221
+ const currentEdgeKey = `${currentCallerRoot}:::${currentId}`;
222
+ queuedEdges.delete(currentEdgeKey);
223
+ visitedEdges.add(currentEdgeKey);
224
+ let entry = currentCallerRoot === root
225
+ ? (manifestActions[currentId] || config?.actions?.[currentId])
226
+ : undefined;
227
+ let entryRoot = currentCallerRoot;
228
+ let targetPkgId = currentCallerRoot === root ? config?.id : undefined;
200
229
  if (!entry) {
201
- const resolved = resolveAction(currentId, { graph, catalog, caller: config?.id });
202
- const resolvedPkgId = resolved.package.id;
203
- const resolvedActionId = resolved.ref.actionId;
204
- const extNode = graph.packages.get(resolvedPkgId);
205
- if (extNode && existsSync(extNode.root)) {
206
- const extRoot = extNode.root;
207
- let extConfig;
208
- try {
209
- extConfig = loadProjectConfig(extRoot);
210
- }
211
- catch {
212
- extConfig = {
213
- id: resolvedPkgId,
214
- name: resolvedPkgId,
215
- version: "0.1.0",
216
- };
230
+ let localResolved = null;
231
+ let resolveError = null;
232
+ try {
233
+ localResolved = resolveAction(currentId, {
234
+ graph,
235
+ catalog,
236
+ caller: currentCallerPkgId || config?.id,
237
+ });
238
+ }
239
+ catch (err) {
240
+ resolveError = err;
241
+ }
242
+ if (localResolved) {
243
+ const resolvedPkgId = localResolved.package.id;
244
+ const resolvedActionId = localResolved.ref.actionId;
245
+ const extNode = graph.packages.get(resolvedPkgId);
246
+ if (extNode && existsSync(extNode.root)) {
247
+ const extRoot = extNode.root;
248
+ let extConfig;
249
+ try {
250
+ extConfig = loadProjectConfig(extRoot);
251
+ }
252
+ catch {
253
+ extConfig = {
254
+ id: resolvedPkgId,
255
+ name: resolvedPkgId,
256
+ version: "0.1.0",
257
+ };
258
+ }
259
+ const externalManifest = loadManifest(extRoot);
260
+ const externalEntry = externalManifest?.actions?.[resolvedActionId] ||
261
+ extConfig?.actions?.[resolvedActionId];
262
+ if (!externalEntry) {
263
+ resolveError = new ActionDockError(UNDECLARED_ACTION_DEPENDENCY, `UNDECLARED_ACTION_DEPENDENCY: Action '${resolvedActionId}' is not declared in package '${resolvedPkgId}' manifest`);
264
+ }
265
+ else {
266
+ entry = externalEntry;
267
+ entryRoot = extRoot;
268
+ targetPkgId = resolvedPkgId;
269
+ // 依赖包 actiondock.json 中声明的传递依赖闭包(按外部包自身作用域入队,不继承根配置)
270
+ const extPkgUses = extConfig?.uses;
271
+ if (Array.isArray(extPkgUses)) {
272
+ for (const u of extPkgUses) {
273
+ const trimmedUse = typeof u === "string" ? u.trim() : "";
274
+ if (trimmedUse) {
275
+ enqueueDependency(trimmedUse, extRoot, resolvedPkgId);
276
+ }
277
+ }
278
+ }
279
+ }
217
280
  }
218
- const externalManifest = loadManifest(extRoot);
219
- const externalEntry = externalManifest?.actions?.[resolvedActionId] ||
220
- extConfig?.actions?.[resolvedActionId];
221
- if (!externalEntry) {
222
- throw new ActionDockError(UNDECLARED_ACTION_DEPENDENCY, `UNDECLARED_ACTION_DEPENDENCY: Action '${resolvedActionId}' is not declared in package '${resolvedPkgId}' manifest`);
281
+ }
282
+ if (!entry) {
283
+ // 仅在明确为包未找到或动作未找到时允许回退至远端绑定检查;严禁掩盖损坏清单、无效引用或版本冲突等非缺失异常
284
+ const isMissing = !resolveError ||
285
+ (resolveError instanceof ActionDockError &&
286
+ (resolveError.code === PACKAGE_NOT_FOUND ||
287
+ resolveError.code === ACTION_NOT_FOUND));
288
+ if (!isMissing) {
289
+ throw resolveError;
223
290
  }
224
- entry = externalEntry;
225
- entryRoot = extRoot;
226
- // 依赖包 actiondock.json 中声明的传递依赖闭包(入队前查重,避免重复膨胀)
227
- const extPkgUses = extConfig?.uses;
228
- if (Array.isArray(extPkgUses)) {
229
- for (const u of extPkgUses) {
230
- const trimmedUse = typeof u === "string" ? u.trim() : "";
231
- if (trimmedUse &&
232
- !resolvedActionIds.has(trimmedUse) &&
233
- !queued.has(trimmedUse)) {
234
- queued.add(trimmedUse);
235
- queue.push(trimmedUse);
236
- }
291
+ // 仅对完全限定依赖(pkg/action),且发起调用的当前包根目录绑定配置中包含精确 remote 条目时,识别为远端依赖
292
+ if (isFullyQualifiedActionRef(currentId)) {
293
+ const bindings = loadActionBindings(currentCallerRoot);
294
+ if (hasRemoteActionBinding(bindings, currentId)) {
295
+ remoteActionRefs.add(currentId);
296
+ continue;
237
297
  }
238
298
  }
299
+ if (resolveError) {
300
+ throw resolveError;
301
+ }
302
+ throw new ActionDockError(ACTION_NOT_FOUND, `Action '${currentId}' referenced in dependency closure (uses) was not found in manifest or linked packages`);
239
303
  }
240
304
  }
241
- if (!entry) {
242
- throw new ActionDockError(ACTION_NOT_FOUND, `Action '${currentId}' referenced in dependency closure (uses) was not found in manifest or linked packages`);
305
+ // 验证本地 entry 文件物理存在,不得跳过本地 entry 缺失
306
+ const resolvedPath = resolve(entryRoot, entry.entry);
307
+ if (!existsSync(resolvedPath)) {
308
+ throw new PlannerError(`Action '${currentId}' entry file not found on disk: ${entry.entry}`, "ENTRY_FILE_NOT_FOUND");
243
309
  }
244
310
  if (entryRoot !== root) {
245
311
  externalActionRoots.set(currentId, entryRoot);
246
312
  externalActionEntries.set(currentId, entry);
247
313
  }
314
+ // 物化 action 按既有唯一集合去重,若已物化则跳过重复 uses 展开
315
+ if (resolvedActionIds.has(currentId)) {
316
+ continue;
317
+ }
248
318
  resolvedActionIds.add(currentId);
249
319
  if (entry.uses && Array.isArray(entry.uses)) {
250
320
  for (const depId of entry.uses) {
251
- if (!resolvedActionIds.has(depId) && !queued.has(depId)) {
252
- queued.add(depId);
253
- queue.push(depId);
321
+ const trimmedDep = typeof depId === "string" ? depId.trim() : "";
322
+ if (trimmedDep) {
323
+ enqueueDependency(trimmedDep, entryRoot, targetPkgId || currentCallerPkgId);
254
324
  }
255
325
  }
256
326
  }
257
327
  }
258
- return { resolvedActionIds, externalActionRoots, externalActionEntries };
328
+ return { resolvedActionIds, externalActionRoots, externalActionEntries, remoteActionRefs };
259
329
  }
260
330
  /**
261
331
  * 构造 Action 依赖结构(闭包结果物化为 ActionDependency 列表)。
@@ -312,6 +382,9 @@ export class SelectionPlanner {
312
382
  declaredFiles.add(f);
313
383
  }
314
384
  for (const declaredRel of declaredFiles) {
385
+ if (isRuntimeConfigFile(declaredRel)) {
386
+ throw new PlannerError(`Runtime configuration file '${declaredRel}' cannot be explicitly declared as build asset or file`, "RESTRICTED_CONFIG_ASSET");
387
+ }
315
388
  const resolvedFile = resolve(root, declaredRel);
316
389
  assertPathWithinRoot(root, resolvedFile, "files");
317
390
  if (!existsSync(resolvedFile)) {
@@ -322,7 +395,10 @@ export class SelectionPlanner {
322
395
  const walked = walkDirectory(resolvedFile);
323
396
  for (const f of walked) {
324
397
  const rel = relative(root, f).replace(/\\/g, "/");
325
- if (!isIgnoredPath(rel) && !actionPathSet.has(f) && !filePathSet.has(rel)) {
398
+ if (!isIgnoredPath(rel) &&
399
+ !isRuntimeConfigFile(rel) &&
400
+ !actionPathSet.has(f) &&
401
+ !filePathSet.has(rel)) {
326
402
  filePathSet.add(rel);
327
403
  modulesAndAssets.push({
328
404
  path: rel,
@@ -334,7 +410,10 @@ export class SelectionPlanner {
334
410
  }
335
411
  else if (stat.isFile()) {
336
412
  const rel = relative(root, resolvedFile).replace(/\\/g, "/");
337
- if (!actionPathSet.has(resolvedFile) && !filePathSet.has(rel)) {
413
+ if (!isIgnoredPath(rel) &&
414
+ !isRuntimeConfigFile(rel) &&
415
+ !actionPathSet.has(resolvedFile) &&
416
+ !filePathSet.has(rel)) {
338
417
  filePathSet.add(rel);
339
418
  modulesAndAssets.push({
340
419
  path: rel,
@@ -359,6 +438,9 @@ export class SelectionPlanner {
359
438
  declaredAssets.add(a);
360
439
  }
361
440
  for (const declaredRel of declaredAssets) {
441
+ if (isRuntimeConfigFile(declaredRel)) {
442
+ throw new PlannerError(`Runtime configuration file '${declaredRel}' cannot be explicitly declared as build asset or file`, "RESTRICTED_CONFIG_ASSET");
443
+ }
362
444
  const resolvedAsset = resolve(root, declaredRel);
363
445
  assertPathWithinRoot(root, resolvedAsset, "assets");
364
446
  if (!existsSync(resolvedAsset)) {
@@ -369,7 +451,7 @@ export class SelectionPlanner {
369
451
  const walked = walkDirectory(resolvedAsset);
370
452
  for (const f of walked) {
371
453
  const rel = relative(root, f).replace(/\\/g, "/");
372
- if (!assetPathSet.has(rel)) {
454
+ if (!isIgnoredPath(rel) && !isRuntimeConfigFile(rel) && !assetPathSet.has(rel)) {
373
455
  assetPathSet.add(rel);
374
456
  modulesAndAssets.push({
375
457
  path: rel,
@@ -381,7 +463,7 @@ export class SelectionPlanner {
381
463
  }
382
464
  else if (stat.isFile()) {
383
465
  const rel = relative(root, resolvedAsset).replace(/\\/g, "/");
384
- if (!assetPathSet.has(rel)) {
466
+ if (!isIgnoredPath(rel) && !isRuntimeConfigFile(rel) && !assetPathSet.has(rel)) {
385
467
  assetPathSet.add(rel);
386
468
  modulesAndAssets.push({
387
469
  path: rel,
@@ -397,7 +479,7 @@ export class SelectionPlanner {
397
479
  const assetFiles = walkDirectory(defaultAssetsDir);
398
480
  for (const file of assetFiles) {
399
481
  const rel = relative(root, file).replace(/\\/g, "/");
400
- if (!assetPathSet.has(rel)) {
482
+ if (!isIgnoredPath(rel) && !isRuntimeConfigFile(rel) && !assetPathSet.has(rel)) {
401
483
  assetPathSet.add(rel);
402
484
  modulesAndAssets.push({
403
485
  path: rel,
@@ -458,7 +540,7 @@ export class SelectionPlanner {
458
540
  finalPlaybooks = selectedPlaybooks.filter((pb) => {
459
541
  if (!pb.actions || pb.actions.length === 0)
460
542
  return true;
461
- return pb.actions.every((a) => closure.resolvedActionIds.has(a));
543
+ return pb.actions.every((a) => closure.resolvedActionIds.has(a) || closure.remoteActionRefs.has(a));
462
544
  });
463
545
  }
464
546
  const actionDependencies = this.buildActionDependencies(root, closure, manifest);
@@ -498,6 +580,7 @@ export class SelectionPlanner {
498
580
  configDefs: config.config,
499
581
  lockfile: lockfileInfo,
500
582
  externalProjectRoots: externalRoots,
583
+ remoteActionRefs: Array.from(closure.remoteActionRefs || []).sort(),
501
584
  metadata: {
502
585
  plannedAt: new Date().toISOString(),
503
586
  schemaVersion: 2,
package/dist/types.d.ts CHANGED
@@ -131,6 +131,8 @@ export interface BuildPlan {
131
131
  configDefs?: Record<string, unknown>;
132
132
  /** 依赖闭包包含的外部已链接包项目根目录列表(去重) */
133
133
  externalProjectRoots?: string[];
134
+ /** 经由远端绑定配置解析的外部依赖 Action 引用列表(只读展示,不加入本地源码构建闭包) */
135
+ remoteActionRefs?: string[];
134
136
  /** 检测到的锁文件信息 */
135
137
  lockfile?: LockfileInfo;
136
138
  /** 规划元数据 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actiondock/builder",
3
- "version": "2.18.0",
3
+ "version": "2.20.0",
4
4
  "description": "ActionDock Build Planner, distribution packaging, and Skill exporter",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,8 +29,8 @@
29
29
  "test": "node ../../scripts/run-tests.ts"
30
30
  },
31
31
  "dependencies": {
32
- "@actiondock/core": "^2.18.0",
33
- "@actiondock/sdk": "^2.18.0"
32
+ "@actiondock/core": "^2.20.0",
33
+ "@actiondock/sdk": "^2.20.0"
34
34
  },
35
35
  "keywords": [
36
36
  "actiondock",