@actiondock/cli 2.14.0 → 2.15.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.
@@ -1,10 +1,11 @@
1
- import { fetchRemoteInfo, } from "@actiondock/core/profile";
1
+ import { isExplicitPath, } from "@actiondock/core";
2
2
  import { filterWithFallbackInfo, } from "@actiondock/core/project";
3
+ import { fetchRemoteInfo, } from "@actiondock/core/profile";
3
4
  import { getRegistryStatus, resolvePackageRoot, } from "@actiondock/core/registry";
4
- import { ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE, packageNotFoundError } from "../errors.js";
5
- import { projectDetailToJson, renderAggregatedPackages, renderProjectDetail, renderRegistryTree, renderResult, } from "../renderer.js";
6
- import { getProjectDetailInfo, scanLocalAggregatedPackages } from "../services/index.js";
7
- import { applyTargetOptions, getEffectiveOptions, resolveFallbackStrategy, resolveIntent, resolveTargetFromOptions, } from "../utils/index.js";
5
+ import { ArgumentError, ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE, packageNotFoundError } from "../errors.js";
6
+ import { projectDetailToJson, renderAggregatedPackages, renderCompositeSkillDetail, renderProjectDetail, renderRegistryTree, renderResult, } from "../renderer.js";
7
+ import { buildCompositeDetails, filterProjectDetails, getProjectDetailInfo, scanLocalAggregatedPackages, } from "../services/index.js";
8
+ import { applyTargetOptions, getEffectiveOptions, resolveCommandTarget, resolveFallbackStrategy, resolveIntent, } from "../utils/index.js";
8
9
  /**
9
10
  * 将远端信息响应归一为本地工程详情视图。
10
11
  */
@@ -51,9 +52,49 @@ function remoteInfoToDetail(remoteInfo) {
51
52
  * 渲染工程详情输出(机器模式输出视图转换结果,人类模式渲染详情)。
52
53
  */
53
54
  function renderProjectDetailOutput(detail, options) {
54
- renderResult(projectDetailToJson(detail), {
55
+ const { matchedCount, isFallback } = filterProjectDetails([detail], options.effectiveIntent, Boolean(options.shouldFallback));
56
+ if (matchedCount === 0 && !isFallback) {
57
+ if (options.isMachine) {
58
+ renderResult({ root: detail.projectRoot, packages: [], matchedCount: 0, isFallback: false }, options);
59
+ return;
60
+ }
61
+ throw new ExecutionError(`No packages matched intent '${options.effectiveIntent}'`);
62
+ }
63
+ renderResult({
64
+ ...projectDetailToJson(detail),
65
+ ...(options.effectiveIntent ? { matchedCount, isFallback } : {}),
66
+ }, {
67
+ json: options.json,
68
+ humanFormatter: () => (isFallback ? `(No packages matched intent '${options.effectiveIntent}', showing selected package)\n\n` : "") +
69
+ renderProjectDetail(detail),
70
+ context: options.context,
71
+ });
72
+ }
73
+ /**
74
+ * 渲染复合技能整体输出(机器模式输出聚合结构,人类模式渲染各子包卡片)。
75
+ */
76
+ function renderCompositeSkillOutput(root, details, options) {
77
+ const outOpts = { json: options.json, context: options.context };
78
+ const { items: filteredDetails, isFallback, matchedCount } = filterProjectDetails(details, options.effectiveIntent, Boolean(options.shouldFallback));
79
+ if (options.effectiveIntent && matchedCount === 0 && !isFallback) {
80
+ if (options.isMachine) {
81
+ renderResult({ root, packages: [], matchedCount: 0, isFallback: false }, outOpts);
82
+ return;
83
+ }
84
+ throw new ExecutionError(`No packages matched intent '${options.effectiveIntent}' in skill directory`);
85
+ }
86
+ if (options.isMachine) {
87
+ const machineResult = {
88
+ root,
89
+ packages: filteredDetails.map(projectDetailToJson),
90
+ ...(options.effectiveIntent ? { matchedCount, isFallback } : {}),
91
+ };
92
+ renderResult(machineResult, outOpts);
93
+ return;
94
+ }
95
+ renderResult(filteredDetails, {
55
96
  json: options.json,
56
- humanFormatter: () => renderProjectDetail(detail),
97
+ humanFormatter: () => renderCompositeSkillDetail(root, filteredDetails, isFallback, options.effectiveIntent),
57
98
  context: options.context,
58
99
  });
59
100
  }
@@ -77,18 +118,25 @@ export function registerInfoCommand(program, context) {
77
118
  .option("--data-dir <path>", "Custom database storage directory")
78
119
  .action(async (patterns = [], rawOptions, cmd) => {
79
120
  const options = getEffectiveOptions(rawOptions, cmd);
121
+ // 唯一显式路径位置参数属于寻址目标,必须先于远端与树状图分支裁决。
122
+ if (!options.package && !options.intent && patterns.length === 1 && isExplicitPath(patterns[0])) {
123
+ options.package = patterns[0];
124
+ patterns = [];
125
+ }
80
126
  const effectiveIntent = resolveIntent(options.intent, patterns);
81
127
  const { isMachine, shouldFallback } = resolveFallbackStrategy(options);
82
128
  const outOpts = { json: options.json, context };
129
+ const detailOpts = { ...outOpts, effectiveIntent, shouldFallback, isMachine };
130
+ // info 仅查询单包清单,复合目录仍保留封闭集合校验。
131
+ const target = resolveCommandTarget(options, context, { metadataOnly: true });
83
132
  // 远端服务目标分支
84
- const target = resolveTargetFromOptions(options, context);
85
- if (target.type === "remote") {
86
- const remoteInfo = await fetchRemoteInfo(target.serverUrl, target.token, {
133
+ if (target.isRemote) {
134
+ const remoteInfo = await fetchRemoteInfo(target.resolved.serverUrl, target.resolved.token, {
87
135
  intent: effectiveIntent,
88
136
  package: options.package,
89
137
  tree: Boolean(options.tree),
90
138
  allowInsecureHttp: Boolean(options.allowInsecureHttp),
91
- insecure: target.insecure,
139
+ insecure: target.resolved.insecure,
92
140
  });
93
141
  if (isMachine) {
94
142
  renderResult(remoteInfo, outOpts);
@@ -116,8 +164,11 @@ export function registerInfoCommand(program, context) {
116
164
  });
117
165
  return;
118
166
  }
119
- // 本地注册表树状图分支
167
+ // 本地注册表树状图分支(若与显式技能或包组合,给出明确 ArgumentError)
120
168
  if (options.tree) {
169
+ if (options.package || target.localSkill) {
170
+ throw new ArgumentError("Tree view (--tree) is not supported with explicit skill directories or packages; use 'ad info -P <path>' without --tree.");
171
+ }
121
172
  const status = getRegistryStatus(context?.customHome);
122
173
  renderResult(status, {
123
174
  json: options.json,
@@ -126,12 +177,30 @@ export function registerInfoCommand(program, context) {
126
177
  });
127
178
  return;
128
179
  }
129
- // 显式指定 Package 参数分支
180
+ // 显式复合技能分支
181
+ if (target.localSkill?.type === "composite") {
182
+ const details = await buildCompositeDetails(target.localSkill);
183
+ renderCompositeSkillOutput(target.localSkill.root, details, {
184
+ json: options.json,
185
+ context,
186
+ effectiveIntent,
187
+ shouldFallback,
188
+ isMachine,
189
+ });
190
+ return;
191
+ }
192
+ // 显式单包技能分支
193
+ if (target.localSkill?.type === "single") {
194
+ const pkg = target.localSkill.packages[0];
195
+ const detail = await getProjectDetailInfo(pkg.packageRoot);
196
+ renderProjectDetailOutput(detail, detailOpts);
197
+ return;
198
+ }
199
+ // 显式指定 Package 参数分支(普通包 ID 或路径)
130
200
  if (options.package) {
131
- const directRoot = resolvePackageRoot(options.package);
132
- if (directRoot) {
133
- const detail = await getProjectDetailInfo(directRoot);
134
- renderProjectDetailOutput(detail, outOpts);
201
+ if (target.targetPackageRoot) {
202
+ const detail = await getProjectDetailInfo(target.targetPackageRoot);
203
+ renderProjectDetailOutput(detail, detailOpts);
135
204
  return;
136
205
  }
137
206
  throw packageNotFoundError(options.package);
@@ -163,8 +232,18 @@ export function registerInfoCommand(program, context) {
163
232
  }
164
233
  // 有关键字过滤场景:单一位置参数且非显式 --intent 时优先尝试包寻址
165
234
  if (patterns.length === 1 && !options.intent) {
166
- const directRoot = resolvePackageRoot(patterns[0]);
235
+ const directRoot = resolvePackageRoot(patterns[0], undefined, context?.customHome);
167
236
  if (directRoot) {
237
+ const directTarget = resolveCommandTarget({ ...options, package: directRoot }, context, { metadataOnly: true });
238
+ if (directTarget.localSkill?.type === "composite") {
239
+ const details = await buildCompositeDetails(directTarget.localSkill);
240
+ renderCompositeSkillOutput(directTarget.localSkill.root, details, {
241
+ ...outOpts,
242
+ shouldFallback,
243
+ isMachine,
244
+ });
245
+ return;
246
+ }
168
247
  const detail = await getProjectDetailInfo(directRoot);
169
248
  renderProjectDetailOutput(detail, outOpts);
170
249
  return;
@@ -1,13 +1,13 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, relative, resolve } from "node:path";
3
- import { assertPathWithinRoot, loadActions, loadManifest, loadPlaybooks, saveManifest, filterWithFallbackInfo, } from "@actiondock/core/project";
4
- import { DefaultActionCatalog, PackageGraphBuilder, resolveAction, resolvePlaybook, } from "@actiondock/core/graph";
3
+ import { assertPathWithinRoot, loadManifest, loadPlaybooks, saveManifest, filterWithFallbackInfo, } from "@actiondock/core/project";
4
+ import { DefaultActionCatalog, resolveAction, resolvePlaybook, } from "@actiondock/core/graph";
5
5
  import { fetchRemotePlaybooks, fetchRemotePlaybookShow, } from "@actiondock/core/profile";
6
6
  import { loadProjectConfig, } from "@actiondock/core";
7
7
  import { listLinkedPackages, } from "@actiondock/core/registry";
8
8
  import { ArgumentError, CliError, ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE, wrapAsExecutionError, } from "../errors.js";
9
- import { renderPlaybookDetail, renderPlaybookList, renderResult, writeStdout, } from "../renderer.js";
10
- import { applyTargetOptions, getEffectiveOptions, remoteTargetSuffix, requirePackageRoot, resolveFallbackStrategy, resolveIntent, resolveLocalPackageRoot, resolveTargetFromOptions, } from "../utils/index.js";
9
+ import { renderPlaybookDetail, renderPlaybookList, renderResult, writeStderr, writeStdout, } from "../renderer.js";
10
+ import { applyTargetOptions, buildScopedPackageGraph, getEffectiveOptions, remoteTargetSuffix, requirePackageRoot, resolveCommandTarget, resolveFallbackStrategy, resolveIntent, } from "../utils/index.js";
11
11
  /**
12
12
  * 注册 Playbook 命令集合(list, show, validate, create)。
13
13
  *
@@ -34,14 +34,15 @@ export function registerPlaybookCommands(program, context) {
34
34
  const hints = [
35
35
  "Tip: Run 'ad playbook show <id>' to inspect procedure steps before execution.",
36
36
  ];
37
- // 1. 远端服务分支
38
- const target = resolveTargetFromOptions(options, context);
39
- if (target.type === "remote") {
40
- const remotePbs = await fetchRemotePlaybooks(target.serverUrl, target.token, {
37
+ // 1. 目标拓扑统一解析
38
+ const target = resolveCommandTarget(options, context);
39
+ // 2. 远端服务分支
40
+ if (target.isRemote) {
41
+ const remotePbs = await fetchRemotePlaybooks(target.resolved.serverUrl, target.resolved.token, {
41
42
  intent: effectiveIntent,
42
43
  package: options.package,
43
44
  allowInsecureHttp: Boolean(options.allowInsecureHttp),
44
- insecure: target.insecure,
45
+ insecure: target.resolved.insecure,
45
46
  });
46
47
  if (isMachine) {
47
48
  renderResult({ items: remotePbs, hints }, { json: options.json, context });
@@ -49,24 +50,44 @@ export function registerPlaybookCommands(program, context) {
49
50
  }
50
51
  renderResult(remotePbs, {
51
52
  json: options.json,
52
- humanFormatter: () => renderPlaybookList(remotePbs, `Playbooks ${remoteTargetSuffix(target)}`, false, effectiveIntent),
53
+ humanFormatter: () => renderPlaybookList(remotePbs, `Playbooks ${remoteTargetSuffix(target.resolved)}`, false, effectiveIntent),
53
54
  context,
54
55
  });
55
56
  return;
56
57
  }
57
- // 2. 本地工程模式
58
- const targetRoot = options.package
59
- ? requirePackageRoot(options.package).root
60
- : resolveLocalPackageRoot();
61
- if (targetRoot) {
62
- const config = loadProjectConfig(targetRoot);
63
- const playbooks = loadPlaybooks(targetRoot, config.playbooksDir);
64
- const rawList = Array.from(playbooks.values()).map((pb) => ({
65
- id: pb.id,
66
- description: pb.description,
67
- packageId: config.id,
68
- }));
69
- const filterRes = filterWithFallbackInfo(rawList, effectiveIntent, [(p) => p.id, (p) => p.description], shouldFallback);
58
+ // 3. 本地模式(单包技能、复合技能或本地工程)
59
+ if (target.localSkill || target.targetPackageRoot) {
60
+ const rawList = [];
61
+ let title;
62
+ if (target.localSkill) {
63
+ const isComposite = target.localSkill.type === "composite";
64
+ for (const pkg of target.localSkill.packages) {
65
+ const playbooks = loadPlaybooks(pkg.packageRoot, pkg.projectConfig.playbooksDir, pkg.projectConfig);
66
+ for (const pb of playbooks.values()) {
67
+ rawList.push({
68
+ id: isComposite ? `${pkg.packageId}/${pb.id}` : pb.id,
69
+ description: pb.description,
70
+ packageId: pkg.packageId,
71
+ });
72
+ }
73
+ }
74
+ title = isComposite
75
+ ? `Playbooks in ${options.package || target.localSkill.root}`
76
+ : `Playbooks in ${target.localSkill.packages[0].packageId} (${target.localSkill.packages[0].packageRoot})`;
77
+ }
78
+ else {
79
+ const config = loadProjectConfig(target.targetPackageRoot);
80
+ const playbooks = loadPlaybooks(target.targetPackageRoot, config.playbooksDir, config);
81
+ for (const pb of playbooks.values()) {
82
+ rawList.push({
83
+ id: pb.id,
84
+ description: pb.description,
85
+ packageId: config.id,
86
+ });
87
+ }
88
+ title = `Playbooks in ${config.id} (${target.targetPackageRoot})`;
89
+ }
90
+ const filterRes = filterWithFallbackInfo(rawList, effectiveIntent, [(p) => p.id, (p) => p.description || ""], shouldFallback);
70
91
  if (isMachine) {
71
92
  if (filterRes.isFallback) {
72
93
  renderResult({ items: filterRes.items, isFallback: true, matchedCount: 0, hints }, { json: options.json, context });
@@ -81,7 +102,7 @@ export function registerPlaybookCommands(program, context) {
81
102
  }
82
103
  renderResult(filterRes.items, {
83
104
  json: options.json,
84
- humanFormatter: () => renderPlaybookList(filterRes.items, `Playbooks in ${config.id} (${targetRoot})`, filterRes.isFallback, effectiveIntent),
105
+ humanFormatter: () => renderPlaybookList(filterRes.items, title, filterRes.isFallback, effectiveIntent),
85
106
  context,
86
107
  });
87
108
  return;
@@ -102,25 +123,22 @@ export function registerPlaybookCommands(program, context) {
102
123
  }
103
124
  const aggregated = [];
104
125
  for (const pkg of linkedList) {
105
- if (!existsSync(pkg.path))
126
+ if (!existsSync(pkg.path)) {
127
+ writeStderr(`Skipping inaccessible linked package '${pkg.id}' at '${pkg.path}'`, context);
106
128
  continue;
107
- try {
108
- const config = loadProjectConfig(pkg.path);
109
- const pbs = loadPlaybooks(pkg.path, config.playbooksDir);
110
- const list = Array.from(pbs.values()).map((p) => ({
111
- id: p.id,
112
- description: p.description,
113
- }));
114
- aggregated.push({
115
- packageId: pkg.id,
116
- packageName: pkg.name,
117
- path: pkg.path,
118
- playbooks: list,
119
- });
120
- }
121
- catch {
122
- // 单个链接包清单损坏或 Playbook 加载失败时跳过,继续聚合其余包
123
129
  }
130
+ const config = loadProjectConfig(pkg.path);
131
+ const pbs = loadPlaybooks(pkg.path, config.playbooksDir, config);
132
+ const list = Array.from(pbs.values()).map((p) => ({
133
+ id: p.id,
134
+ description: p.description,
135
+ }));
136
+ aggregated.push({
137
+ packageId: pkg.id,
138
+ packageName: pkg.name,
139
+ path: pkg.path,
140
+ playbooks: list,
141
+ });
124
142
  }
125
143
  let filteredPackages = [];
126
144
  let isFallback = false;
@@ -183,13 +201,14 @@ export function registerPlaybookCommands(program, context) {
183
201
  if (!id) {
184
202
  throw new ArgumentError("Playbook ID is required for show");
185
203
  }
186
- // 1. 远端服务分支
187
- const target = resolveTargetFromOptions(options, context);
188
- if (target.type === "remote") {
204
+ // 1. 目标拓扑统一解析
205
+ const target = resolveCommandTarget(options, context);
206
+ // 2. 远端服务分支
207
+ if (target.isRemote) {
189
208
  try {
190
- const detail = await fetchRemotePlaybookShow(target.serverUrl, id, target.token, {
209
+ const detail = await fetchRemotePlaybookShow(target.resolved.serverUrl, target.resolvePlaybookRef(id), target.resolved.token, {
191
210
  allowInsecureHttp: Boolean(options.allowInsecureHttp),
192
- insecure: target.insecure,
211
+ insecure: target.resolved.insecure,
193
212
  });
194
213
  renderResult(detail, {
195
214
  json: options.json,
@@ -203,20 +222,15 @@ export function registerPlaybookCommands(program, context) {
203
222
  throw new ArgumentError(`${err.message}\n${hint}`, err?.details, err?.code, hint);
204
223
  }
205
224
  }
206
- // 2. 本地项目模式
207
- let showTarget = id;
208
- if (options.package && !id.includes("/")) {
209
- requirePackageRoot(options.package);
210
- showTarget = `${options.package}/${id}`;
211
- }
225
+ // 3. 本地分支(单包技能、复合技能、本地工程或已链接包)
226
+ const showTarget = target.resolvePlaybookRef(id);
212
227
  let resolved;
213
228
  try {
214
- const graph = new PackageGraphBuilder({
215
- root: resolveLocalPackageRoot() || undefined,
216
- customHome: context?.customHome,
217
- allowDevLinks: true,
218
- }).buildSync();
219
- resolved = resolvePlaybook(showTarget, { graph });
229
+ const graph = buildScopedPackageGraph(target, context);
230
+ const caller = target.localSkill
231
+ ? (target.localSkill.type === "single" ? target.localSkill.packages[0].packageId : undefined)
232
+ : (target.targetPackageRoot ? loadProjectConfig(target.targetPackageRoot).id : undefined);
233
+ resolved = resolvePlaybook(showTarget, { graph, caller });
220
234
  }
221
235
  catch (err) {
222
236
  const hint = "Tip: Run 'ad playbook list' to discover available playbooks.";
@@ -249,36 +263,73 @@ export function registerPlaybookCommands(program, context) {
249
263
  .option("--json", "Output as JSON")
250
264
  .action(async (id, rawOptions, cmd) => {
251
265
  const options = getEffectiveOptions(rawOptions, cmd);
252
- const root = options.package
253
- ? requirePackageRoot(options.package).root
254
- : resolveLocalPackageRoot();
266
+ // 保持本地校验定位,profile 始终为 local
267
+ const target = resolveCommandTarget({ ...options, profile: "local" }, context);
268
+ const graph = buildScopedPackageGraph(target, context);
269
+ const catalog = new DefaultActionCatalog(graph);
255
270
  const targets = [];
256
- if (root) {
271
+ if (target.localSkill) {
272
+ if (id) {
273
+ const showTarget = target.resolvePlaybookRef(id);
274
+ const caller = target.localSkill.type === "single" ? target.localSkill.packages[0].packageId : undefined;
275
+ let resolved;
276
+ try {
277
+ resolved = resolvePlaybook(showTarget, { graph, caller });
278
+ }
279
+ catch (err) {
280
+ throw new ArgumentError(err.message, err?.details, err?.code);
281
+ }
282
+ targets.push({
283
+ root: resolved.projectRoot,
284
+ packageId: resolved.packageId,
285
+ playbooks: [resolved.playbook],
286
+ });
287
+ }
288
+ else {
289
+ for (const pkg of target.localSkill.packages) {
290
+ const playbooks = loadPlaybooks(pkg.packageRoot, pkg.projectConfig.playbooksDir);
291
+ targets.push({
292
+ root: pkg.packageRoot,
293
+ packageId: pkg.packageId,
294
+ playbooks: Array.from(playbooks.values()),
295
+ });
296
+ }
297
+ }
298
+ }
299
+ else if (target.targetPackageRoot) {
300
+ const root = target.targetPackageRoot;
257
301
  const config = loadProjectConfig(root);
258
- const playbooks = loadPlaybooks(root, config.playbooksDir);
259
302
  if (id) {
260
- const pb = playbooks.get(id);
261
- if (!pb) {
262
- throw new ArgumentError(`Playbook '${id}' not found in package '${config.id}'`);
303
+ const showTarget = target.resolvePlaybookRef(id);
304
+ let resolved;
305
+ try {
306
+ resolved = resolvePlaybook(showTarget, { graph, caller: config.id });
263
307
  }
264
- targets.push({ root, packageId: config.id, playbooks: [pb] });
308
+ catch (err) {
309
+ throw new ArgumentError(err.message, err?.details, err?.code);
310
+ }
311
+ targets.push({
312
+ root: resolved.projectRoot,
313
+ packageId: resolved.packageId,
314
+ playbooks: [resolved.playbook],
315
+ });
265
316
  }
266
317
  else {
267
- targets.push({ root, packageId: config.id, playbooks: Array.from(playbooks.values()) });
318
+ const playbooks = loadPlaybooks(root, config.playbooksDir);
319
+ targets.push({
320
+ root,
321
+ packageId: config.id,
322
+ playbooks: Array.from(playbooks.values()),
323
+ });
268
324
  }
269
325
  }
270
326
  else if (id) {
271
327
  let resolved;
272
328
  try {
273
- const graph = new PackageGraphBuilder({
274
- root: resolveLocalPackageRoot() || undefined,
275
- customHome: context?.customHome,
276
- allowDevLinks: true,
277
- }).buildSync();
278
329
  resolved = resolvePlaybook(id, { graph });
279
330
  }
280
331
  catch (err) {
281
- throw new ArgumentError(err.message);
332
+ throw new ArgumentError(err.message, err?.details, err?.code);
282
333
  }
283
334
  targets.push({
284
335
  root: resolved.projectRoot,
@@ -292,34 +343,22 @@ export function registerPlaybookCommands(program, context) {
292
343
  throw new ArgumentError(`${NO_PROJECT_NO_LINKED_MESSAGE} Please run 'ad link' inside a package first.`);
293
344
  }
294
345
  for (const pkg of linkedList) {
295
- if (!existsSync(pkg.path))
346
+ if (!existsSync(pkg.path)) {
347
+ writeStderr(`Skipping inaccessible linked package '${pkg.id}' at '${pkg.path}'`, context);
296
348
  continue;
297
- try {
298
- const config = loadProjectConfig(pkg.path);
299
- const playbooks = loadPlaybooks(pkg.path, config.playbooksDir);
300
- targets.push({
301
- root: pkg.path,
302
- packageId: pkg.id,
303
- playbooks: Array.from(playbooks.values()),
304
- });
305
- }
306
- catch {
307
- // 单个链接包清单损坏或 Playbook 加载失败时跳过,不阻断整体校验
308
349
  }
350
+ const config = loadProjectConfig(pkg.path);
351
+ const playbooks = loadPlaybooks(pkg.path, config.playbooksDir, config);
352
+ targets.push({
353
+ root: pkg.path,
354
+ packageId: pkg.id,
355
+ playbooks: Array.from(playbooks.values()),
356
+ });
309
357
  }
310
358
  }
311
359
  const results = [];
312
- const graph = new PackageGraphBuilder({
313
- root: resolveLocalPackageRoot() || undefined,
314
- customHome: context?.customHome,
315
- allowDevLinks: true,
316
- }).buildSync();
317
- const catalog = new DefaultActionCatalog(graph);
318
- for (const target of targets) {
319
- const config = loadProjectConfig(target.root);
320
- const manifest = loadManifest(target.root);
321
- const actions = await loadActions(target.root, config.actionsDir);
322
- for (const pb of target.playbooks) {
360
+ for (const targetItem of targets) {
361
+ for (const pb of targetItem.playbooks) {
323
362
  const warnings = [];
324
363
  const errors = [];
325
364
  if (!pb.id) {
@@ -330,26 +369,30 @@ export function registerPlaybookCommands(program, context) {
330
369
  }
331
370
  if (pb.actions && Array.isArray(pb.actions)) {
332
371
  for (const actRef of pb.actions) {
333
- if (actRef.includes("/")) {
372
+ if (typeof actRef !== "string" || !actRef.trim()) {
373
+ errors.push(`Referenced action must be a non-empty string, received ${JSON.stringify(actRef)}`);
374
+ continue;
375
+ }
376
+ const trimmed = actRef.trim();
377
+ if (trimmed.includes("/")) {
334
378
  try {
335
- resolveAction(actRef, { graph, catalog, caller: target.packageId });
379
+ resolveAction(trimmed, { graph, catalog, caller: targetItem.packageId });
336
380
  }
337
381
  catch (e) {
338
- errors.push(`Referenced cross-package action '${actRef}' not resolvable: ${e.message}. Hint: Action '${actRef}' not found. Run 'ad action create ${actRef}' to create local action, or 'ad add <pkg>' to install external dependency.`);
382
+ errors.push(`Referenced cross-package action '${trimmed}' not resolvable: ${e.message}. Hint: Action '${trimmed}' not found. Run 'ad action create ${trimmed}' to create local action, or 'ad add <pkg>' to install external dependency.`);
339
383
  }
340
384
  }
341
385
  else {
342
- const foundInActions = actions.has(actRef);
343
- const foundInManifest = Boolean(manifest?.actions?.[actRef]);
344
- if (!foundInActions && !foundInManifest) {
345
- errors.push(`Referenced action '${actRef}' not found in project actions. Hint: Action '${actRef}' not found. Run 'ad action create ${actRef}' to create local action, or 'ad add <pkg>' to install external dependency.`);
386
+ // 短引用只接受所属包清单登记的 Action,不能由源码文件或其他包补位。
387
+ if (!catalog.get(targetItem.packageId, trimmed)) {
388
+ errors.push(`Referenced action '${trimmed}' not found in project actions. Hint: Action '${trimmed}' not found. Run 'ad action create ${trimmed}' to create local action, or 'ad add <pkg>' to install external dependency.`);
346
389
  }
347
390
  }
348
391
  }
349
392
  }
350
393
  results.push({
351
394
  id: pb.id,
352
- packageId: target.packageId,
395
+ packageId: targetItem.packageId,
353
396
  valid: errors.length === 0,
354
397
  warnings,
355
398
  errors,
@@ -43,3 +43,7 @@ export type RegistryTreeView = RegistryStatusReport | (Record<string, unknown> &
43
43
  * 格式化渲染注册表层级树结构。
44
44
  */
45
45
  export declare function renderRegistryTree(status: RegistryTreeView): string;
46
+ /**
47
+ * 格式化渲染复合技能各子包的自省卡片列表。
48
+ */
49
+ export declare function renderCompositeSkillDetail(root: string, filteredDetails: ProjectDetailInfo[], isFallback?: boolean, effectiveIntent?: string): string;
@@ -194,3 +194,19 @@ export function renderRegistryTree(status) {
194
194
  }
195
195
  return lines.join("\n");
196
196
  }
197
+ /**
198
+ * 格式化渲染复合技能各子包的自省卡片列表。
199
+ */
200
+ export function renderCompositeSkillDetail(root, filteredDetails, isFallback, effectiveIntent) {
201
+ const lines = [];
202
+ if (isFallback) {
203
+ lines.push(`(No packages in skill matched intent '${effectiveIntent}', showing all packages)\n`);
204
+ }
205
+ lines.push(`ActionDock Composite Skill: ${root}`);
206
+ lines.push(`Packages (${filteredDetails.length}):\n`);
207
+ for (const d of filteredDetails) {
208
+ lines.push(renderProjectDetail(d));
209
+ lines.push("");
210
+ }
211
+ return lines.join("\n").trimEnd();
212
+ }
@@ -1,3 +1,4 @@
1
+ import { type LocalSkillDirectory } from "@actiondock/core/project";
1
2
  import type { AggregatedPackage, CliContext, ProjectDetailInfo } from "../types.js";
2
3
  /**
3
4
  * 装配单个工程的详情自省信息(清单、动作映射与 Playbook 映射)。
@@ -5,6 +6,18 @@ import type { AggregatedPackage, CliContext, ProjectDetailInfo } from "../types.
5
6
  * @param root 工程根目录
6
7
  */
7
8
  export declare function getProjectDetailInfo(root: string): Promise<ProjectDetailInfo>;
9
+ /**
10
+ * 装配复合技能各子包的自省详情,将 Action 与 Playbook 的调用索引提升为完全限定 ID。
11
+ */
12
+ export declare function buildCompositeDetails(localSkill: LocalSkillDirectory): Promise<ProjectDetailInfo[]>;
13
+ /**
14
+ * 单包与复合技能共享相同的详情过滤规则(单一事实源)。
15
+ */
16
+ export declare function filterProjectDetails(details: ProjectDetailInfo[], effectiveIntent?: string, shouldFallback?: boolean): {
17
+ items: ProjectDetailInfo[];
18
+ matchedCount: number;
19
+ isFallback: boolean;
20
+ };
8
21
  /**
9
22
  * 装配单个包的摘要聚合信息(不含动作与 Playbook 明细映射)。
10
23
  *
@@ -13,7 +26,7 @@ export declare function getProjectDetailInfo(root: string): Promise<ProjectDetai
13
26
  export declare function getPackageSummary(root: string): AggregatedPackage;
14
27
  /**
15
28
  * 扫描本地候选包集合(当前工程根目录优先,叠加全局已链接包,按物理路径去重)。
16
- * 清单损坏或磁盘失效的候选项跳过,不影响其余条目聚合。
29
+ * 失效或损坏链接给出明确诊断记录,不静默吞没也不中断其余条目聚合。
17
30
  *
18
31
  * @param context CLI 上下文(提供 customHome 定位全局注册表)
19
32
  */
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { findProjectRoot, loadProjectConfig, } from "@actiondock/core";
3
3
  import { listLinkedPackages, } from "@actiondock/core/registry";
4
- import { loadManifest, loadPlaybooks, } from "@actiondock/core/project";
4
+ import { filterWithFallbackInfo, loadManifest, loadPlaybooks, } from "@actiondock/core/project";
5
5
  /**
6
6
  * 装配单个工程的详情自省信息(清单、动作映射与 Playbook 映射)。
7
7
  *
@@ -38,6 +38,51 @@ export async function getProjectDetailInfo(root) {
38
38
  playbooksMap: playbooks,
39
39
  };
40
40
  }
41
+ /**
42
+ * 装配复合技能各子包的自省详情,将 Action 与 Playbook 的调用索引提升为完全限定 ID。
43
+ */
44
+ export async function buildCompositeDetails(localSkill) {
45
+ const details = [];
46
+ for (const pkg of localSkill.packages) {
47
+ const detail = await getProjectDetailInfo(pkg.packageRoot);
48
+ const qualifiedActionsMap = new Map();
49
+ if (detail.actionsMap) {
50
+ for (const [actId, act] of detail.actionsMap.entries()) {
51
+ const fullId = `${pkg.packageId}/${actId}`;
52
+ qualifiedActionsMap.set(fullId, { id: fullId, description: act.description });
53
+ }
54
+ }
55
+ const qualifiedPlaybooksMap = new Map();
56
+ if (detail.playbooksMap) {
57
+ for (const [pbId, pb] of detail.playbooksMap.entries()) {
58
+ const fullId = `${pkg.packageId}/${pbId}`;
59
+ qualifiedPlaybooksMap.set(fullId, { ...pb, id: fullId });
60
+ }
61
+ }
62
+ detail.actionsMap = qualifiedActionsMap;
63
+ detail.actions = Array.from(qualifiedActionsMap.keys());
64
+ detail.playbooksMap = qualifiedPlaybooksMap;
65
+ detail.playbooks = Array.from(qualifiedPlaybooksMap.keys());
66
+ details.push(detail);
67
+ }
68
+ return details;
69
+ }
70
+ /**
71
+ * 单包与复合技能共享相同的详情过滤规则(单一事实源)。
72
+ */
73
+ export function filterProjectDetails(details, effectiveIntent, shouldFallback = false) {
74
+ if (!effectiveIntent) {
75
+ return { items: details, matchedCount: details.length, isFallback: false };
76
+ }
77
+ return filterWithFallbackInfo(details, effectiveIntent, [
78
+ (d) => d.id,
79
+ (d) => d.name,
80
+ (d) => d.description || "",
81
+ (d) => d.projectRoot,
82
+ (d) => d.actions.join(" "),
83
+ (d) => d.playbooks.join(" "),
84
+ ], Boolean(shouldFallback));
85
+ }
41
86
  /**
42
87
  * 装配单个包的摘要聚合信息(不含动作与 Playbook 明细映射)。
43
88
  *
@@ -63,7 +108,7 @@ export function getPackageSummary(root) {
63
108
  }
64
109
  /**
65
110
  * 扫描本地候选包集合(当前工程根目录优先,叠加全局已链接包,按物理路径去重)。
66
- * 清单损坏或磁盘失效的候选项跳过,不影响其余条目聚合。
111
+ * 失效或损坏链接给出明确诊断记录,不静默吞没也不中断其余条目聚合。
67
112
  *
68
113
  * @param context CLI 上下文(提供 customHome 定位全局注册表)
69
114
  */
@@ -73,25 +118,22 @@ export function scanLocalAggregatedPackages(context) {
73
118
  const aggregated = [];
74
119
  const seenPaths = new Set();
75
120
  if (currentRoot) {
76
- try {
77
- aggregated.push(getPackageSummary(currentRoot));
78
- seenPaths.add(currentRoot);
79
- }
80
- catch {
81
- // 忽略异常工程根目录(清单损坏)
82
- }
121
+ aggregated.push(getPackageSummary(currentRoot));
122
+ seenPaths.add(currentRoot);
83
123
  }
84
124
  for (const pkg of linkedList) {
85
- if (!existsSync(pkg.path))
125
+ if (!existsSync(pkg.path)) {
126
+ process.stderr.write(`[actiondock] Diagnostic: Skipping inaccessible linked package '${pkg.id}' at '${pkg.path}'\n`);
86
127
  continue;
128
+ }
87
129
  if (seenPaths.has(pkg.path))
88
130
  continue;
89
131
  try {
90
132
  aggregated.push(getPackageSummary(pkg.path));
91
133
  seenPaths.add(pkg.path);
92
134
  }
93
- catch {
94
- // 忽略失效链接(清单损坏或磁盘异常)
135
+ catch (err) {
136
+ process.stderr.write(`[actiondock] Diagnostic: Skipping invalid linked package '${pkg.id}' at '${pkg.path}': ${err?.message || String(err)}\n`);
95
137
  }
96
138
  }
97
139
  return { currentRoot, aggregated };
@@ -1,5 +1,6 @@
1
1
  import { loadProjectConfig, type ActionDockService, type LocalSkillDirectory } from "@actiondock/core";
2
2
  import { type ResolvedTarget } from "@actiondock/core/profile";
3
+ import { type PackageGraph } from "@actiondock/core/graph";
3
4
  import type { Command } from "commander";
4
5
  import type { CliContext } from "../types.js";
5
6
  /**
@@ -30,6 +31,10 @@ export interface TargetResolutionOptions {
30
31
  /** 是否允许向非回环地址发送明文 HTTP 请求(对应 --allow-insecure-http) */
31
32
  allowInsecureHttp?: boolean;
32
33
  }
34
+ /** 仅供清单查询使用的内部解析策略;执行命令默认保持封闭校验。 */
35
+ export interface TargetResolutionStrategy {
36
+ readonly metadataOnly?: boolean;
37
+ }
33
38
  /**
34
39
  * 目标拓扑解析单一入口:从命令选项视图解析远端/本地目标拓扑信息。
35
40
  *
@@ -44,9 +49,37 @@ export interface TargetResolutionOptions {
44
49
  *
45
50
  * @param options 命令选项视图(支持直接传入 Commander 解析后的 options 对象)
46
51
  * @param context CLI 上下文
52
+ * @param strategy 内部解析策略;仅清单查询可跳过单包闭包校验
47
53
  * @returns 解析后的目标拓扑信息(仅信息解析,不创建 Service 实例)
48
54
  */
49
- export declare function resolveTargetFromOptions(options: TargetResolutionOptions, context?: CliContext): ResolvedTarget;
55
+ export declare function resolveTargetFromOptions(options: TargetResolutionOptions, context?: CliContext, strategy?: TargetResolutionStrategy): ResolvedTarget;
56
+ /**
57
+ * 统一计算目标资源引用(Action 或 Playbook)的辅助函数(单一事实源)。
58
+ *
59
+ * 规则:
60
+ * - 远端分支:options.package 存在且资源标识未限定时,前缀拼接 options.package;
61
+ * - 本地单包技能:资源标识未限定时,前缀拼接真实 packageId(而非路径目录名);
62
+ * - 本地复合技能:保持短名,供后续图解析裁决唯一匹配或多包歧义;完全限定名直接透传;
63
+ * - 普通工程包:非技能且 options.package 存在时,未限定则拼接 options.package。
64
+ *
65
+ * @param resourceId 资源标识符(动作名或规程名)
66
+ * @param options 命令选项视图
67
+ * @param localSkill 本地技能目录视图
68
+ */
69
+ export declare function resolveResourceRef(resourceId: string, options: TargetResolutionOptions, localSkill?: LocalSkillDirectory | null): string;
70
+ /**
71
+ * 统一计算目标 Playbook 引用的辅助函数。
72
+ */
73
+ export declare function resolvePlaybookRef(playbookId: string, options: TargetResolutionOptions, localSkill?: LocalSkillDirectory | null): string;
74
+ /**
75
+ * 构造与目标作用域严格对应的 PackageGraph(单一事实源)。
76
+ *
77
+ * 封闭保证:
78
+ * - 当目标为 LocalSkillDirectory 时,仅以 localSkill.packages 显式包集合构建封闭图;
79
+ * 绝不扫描 cwd 也不扫描全局 Registry/开发软链,防止外部同名包或污染泄漏;
80
+ * - 当目标为普通本地工程或包 ID 时,维持原有的基于项目根与全局链接表的拓扑图构建模式。
81
+ */
82
+ export declare function buildScopedPackageGraph(target: CommandTargetResolution, context?: CliContext): PackageGraph;
50
83
  /**
51
84
  * 命令目标与动作引用解析结果。
52
85
  */
@@ -55,12 +88,14 @@ export interface CommandTargetResolution {
55
88
  resolved: ResolvedTarget;
56
89
  /** 是否为远端目标 */
57
90
  isRemote: boolean;
58
- /** 本地封闭技能目录视图(非本地技能目录时为 null) */
91
+ /** 本地技能目录或单包元数据作用域(未寻址本地包时为 null) */
59
92
  localSkill: LocalSkillDirectory | null;
60
93
  /** 本地包根目录路径(远端分支为 undefined,未找到为 undefined) */
61
94
  targetPackageRoot?: string;
62
95
  /** 统一计算目标 Action 引用的辅助函数 */
63
96
  resolveActionRef: (actionId: string) => string;
97
+ /** 统一计算目标 Playbook 引用的辅助函数 */
98
+ resolvePlaybookRef: (playbookId: string) => string;
64
99
  }
65
100
  /**
66
101
  * 统一解析 CLI 命令的目标拓扑与包作用域(run, describe, list 等命令的单一事实源)。
@@ -68,10 +103,11 @@ export interface CommandTargetResolution {
68
103
  * 保证:
69
104
  * - 优先判定目标拓扑;
70
105
  * - 远端目标(remote)完全隔离:不执行任何本地目录探测、清单加载或包根寻址;
71
- * - 远端目标动作引用保持原始词法包 ID 拼接(${options.package}/${actionId}),绝不被本地同名目录或清单 ID 篡改;
72
- * - 本地目标按本地封闭技能目录或工程包严格解析,不存在时透明抛出 packageNotFoundError。
106
+ * - 远端目标动作与规程引用保持原始词法包 ID 拼接(${options.package}/${id}),绝不被本地同名目录或清单 ID 篡改;
107
+ * - 本地目标默认按封闭技能目录或工程包严格解析;元数据策略仅跳过单包闭包校验;
108
+ * - 显式目标不存在时透明抛出 packageNotFoundError。
73
109
  */
74
- export declare function resolveCommandTarget(options: TargetResolutionOptions, context?: CliContext): CommandTargetResolution;
110
+ export declare function resolveCommandTarget(options: TargetResolutionOptions, context?: CliContext, strategy?: TargetResolutionStrategy): CommandTargetResolution;
75
111
  /**
76
112
  * 配置与状态管理目标作用域。
77
113
  */
@@ -1,6 +1,7 @@
1
- import { connectActionDock, createActionDock, createNodePlatform, findProjectRoot, inspectLocalPackageDirectory, isExplicitPath, loadProjectConfig, } from "@actiondock/core";
1
+ import { connectActionDock, createActionDock, createNodePlatform, findProjectRoot, inspectLocalPackageDirectory, isExplicitPath, loadProjectConfig, queryLocalPackageDirectory, } from "@actiondock/core";
2
2
  import { resolvePackageRoot, } from "@actiondock/core/registry";
3
3
  import { resolveTarget, } from "@actiondock/core/profile";
4
+ import { PackageGraphBuilder, } from "@actiondock/core/graph";
4
5
  import { ArgumentError, packageNotFoundError } from "../errors.js";
5
6
  /**
6
7
  * 统一为 Commander 命令节点挂载目标环境解析相关选项(单一事实源)。
@@ -17,23 +18,20 @@ export function applyTargetOptions(command) {
17
18
  .option("-k, --insecure", "Allow insecure server connections (skip TLS certificate validation)")
18
19
  .option("--allow-insecure-http", "Allow sending auth token over unencrypted HTTP to non-loopback hosts");
19
20
  }
21
+ /** 本地寻址策略只有一个实现入口,查询与严格闭包均委托核心目录能力。 */
22
+ function inspectCommandDirectory(packageOption, context, strategy) {
23
+ if (!strategy?.metadataOnly)
24
+ return inspectLocalPackageDirectory(packageOption);
25
+ const directory = queryLocalPackageDirectory(packageOption);
26
+ if (directory)
27
+ return directory;
28
+ const root = resolvePackageRoot(packageOption, undefined, context?.customHome);
29
+ return root ? queryLocalPackageDirectory(root) : null;
30
+ }
20
31
  /**
21
- * 目标拓扑解析单一入口:从命令选项视图解析远端/本地目标拓扑信息。
22
- *
23
- * 显式路径与包标识符分类规则:
24
- * - 显式路径形态(以 .、/、~ 开头或包含非 scoped 分隔符):
25
- * 严格作为本地技能或包目录解析;禁止与显式远端选项(--server 或非 local 的 --profile)组合,违规抛出 ArgumentError;
26
- * 不受隐式 Profile 或 ACTIONDOCK_SERVER_URL 环境变量改变目标,本地执行优先;
27
- * 若本地目录不存在或不合法,抛出 packageNotFoundError;
28
- * - 纯词法包标识符(如 pkg.a、@scope/pkg):
29
- * 若存在远端目标(显式 --server、非 local profile 或默认远端环境),严格作为远端包标识解析,绝不因本地 cwd 存在同名目录而劫持为本地执行;
30
- * 若无远端目标,正常按本地目标解析。
31
- *
32
- * @param options 命令选项视图(支持直接传入 Commander 解析后的 options 对象)
33
- * @param context CLI 上下文
34
- * @returns 解析后的目标拓扑信息(仅信息解析,不创建 Service 实例)
32
+ * 内部统一解析目标拓扑与包作用域,单次调用闭包完成全部发现,无跨调用缓存。
35
33
  */
36
- export function resolveTargetFromOptions(options, context) {
34
+ function resolveCommandTargetInternal(options, context, strategy) {
37
35
  const isExplicit = options.package ? isExplicitPath(options.package) : false;
38
36
  if (isExplicit) {
39
37
  const hasExplicitServer = Boolean(options.server && options.server.trim());
@@ -41,22 +39,138 @@ export function resolveTargetFromOptions(options, context) {
41
39
  if (hasExplicitServer || hasExplicitProfile) {
42
40
  throw new ArgumentError("Cannot combine local package/skill directory with remote target options (--server or --profile). Specify a package ID for remote targets, or remove remote target options for local directory execution.");
43
41
  }
44
- const localSkill = inspectLocalPackageDirectory(options.package);
42
+ const localSkill = inspectCommandDirectory(options.package, context, strategy);
45
43
  if (!localSkill) {
46
44
  throw packageNotFoundError(options.package);
47
45
  }
48
46
  return {
49
- type: "local",
50
- profileName: "local",
47
+ resolved: {
48
+ type: "local",
49
+ profileName: "local",
50
+ },
51
+ localSkill,
52
+ targetPackageRoot: localSkill.root,
51
53
  };
52
54
  }
53
- return resolveTarget({
55
+ const resolved = resolveTarget({
54
56
  profile: options.profile,
55
57
  server: options.server,
56
58
  token: options.token,
57
59
  insecure: options.insecure,
58
60
  allowInsecureHttp: options.allowInsecureHttp,
59
61
  }, context?.customHome);
62
+ if (resolved.type === "remote") {
63
+ return {
64
+ resolved,
65
+ localSkill: null,
66
+ targetPackageRoot: undefined,
67
+ };
68
+ }
69
+ // 本地分支(options.package 为短名或未传)
70
+ if (options.package) {
71
+ const localSkill = inspectCommandDirectory(options.package, context, strategy);
72
+ const targetPackageRoot = localSkill
73
+ ? localSkill.root
74
+ : strategy?.metadataOnly
75
+ ? resolvePackageRoot(options.package, undefined, context?.customHome)
76
+ : resolveLocalPackageRoot(options.package);
77
+ if (!targetPackageRoot) {
78
+ throw packageNotFoundError(options.package);
79
+ }
80
+ return {
81
+ resolved,
82
+ localSkill,
83
+ targetPackageRoot,
84
+ };
85
+ }
86
+ const projectRoot = findProjectRoot();
87
+ return {
88
+ resolved,
89
+ localSkill: null,
90
+ targetPackageRoot: projectRoot || undefined,
91
+ };
92
+ }
93
+ /**
94
+ * 目标拓扑解析单一入口:从命令选项视图解析远端/本地目标拓扑信息。
95
+ *
96
+ * 显式路径与包标识符分类规则:
97
+ * - 显式路径形态(以 .、/、~ 开头或包含非 scoped 分隔符):
98
+ * 严格作为本地技能或包目录解析;禁止与显式远端选项(--server 或非 local 的 --profile)组合,违规抛出 ArgumentError;
99
+ * 不受隐式 Profile 或 ACTIONDOCK_SERVER_URL 环境变量改变目标,本地执行优先;
100
+ * 若本地目录不存在或不合法,抛出 packageNotFoundError;
101
+ * - 纯词法包标识符(如 pkg.a、@scope/pkg):
102
+ * 若存在远端目标(显式 --server、非 local profile 或默认远端环境),严格作为远端包标识解析,绝不因本地 cwd 存在同名目录而劫持为本地执行;
103
+ * 若无远端目标,正常按本地目标解析。
104
+ *
105
+ * @param options 命令选项视图(支持直接传入 Commander 解析后的 options 对象)
106
+ * @param context CLI 上下文
107
+ * @param strategy 内部解析策略;仅清单查询可跳过单包闭包校验
108
+ * @returns 解析后的目标拓扑信息(仅信息解析,不创建 Service 实例)
109
+ */
110
+ export function resolveTargetFromOptions(options, context, strategy) {
111
+ return resolveCommandTargetInternal(options, context, strategy).resolved;
112
+ }
113
+ /**
114
+ * 统一计算目标资源引用(Action 或 Playbook)的辅助函数(单一事实源)。
115
+ *
116
+ * 规则:
117
+ * - 远端分支:options.package 存在且资源标识未限定时,前缀拼接 options.package;
118
+ * - 本地单包技能:资源标识未限定时,前缀拼接真实 packageId(而非路径目录名);
119
+ * - 本地复合技能:保持短名,供后续图解析裁决唯一匹配或多包歧义;完全限定名直接透传;
120
+ * - 普通工程包:非技能且 options.package 存在时,未限定则拼接 options.package。
121
+ *
122
+ * @param resourceId 资源标识符(动作名或规程名)
123
+ * @param options 命令选项视图
124
+ * @param localSkill 本地技能目录视图
125
+ */
126
+ export function resolveResourceRef(resourceId, options, localSkill) {
127
+ if (!options.package)
128
+ return resourceId;
129
+ if (localSkill) {
130
+ if (localSkill.type === "single" && !resourceId.includes("/")) {
131
+ return `${localSkill.packages[0].packageId}/${resourceId}`;
132
+ }
133
+ // 复合技能短名不拼目录名,保持 resourceId 供后续裁决唯一匹配或多包歧义
134
+ return resourceId;
135
+ }
136
+ if (!resourceId.includes("/")) {
137
+ return `${options.package}/${resourceId}`;
138
+ }
139
+ return resourceId;
140
+ }
141
+ /**
142
+ * 统一计算目标 Playbook 引用的辅助函数。
143
+ */
144
+ export function resolvePlaybookRef(playbookId, options, localSkill) {
145
+ return resolveResourceRef(playbookId, options, localSkill);
146
+ }
147
+ /**
148
+ * 构造与目标作用域严格对应的 PackageGraph(单一事实源)。
149
+ *
150
+ * 封闭保证:
151
+ * - 当目标为 LocalSkillDirectory 时,仅以 localSkill.packages 显式包集合构建封闭图;
152
+ * 绝不扫描 cwd 也不扫描全局 Registry/开发软链,防止外部同名包或污染泄漏;
153
+ * - 当目标为普通本地工程或包 ID 时,维持原有的基于项目根与全局链接表的拓扑图构建模式。
154
+ */
155
+ export function buildScopedPackageGraph(target, context) {
156
+ if (target.localSkill) {
157
+ const isSingle = target.localSkill.type === "single";
158
+ const discovered = target.localSkill.packages.map((pkg) => ({
159
+ id: pkg.packageId,
160
+ root: pkg.packageRoot,
161
+ manifest: pkg.projectConfig,
162
+ isCurrentProject: isSingle,
163
+ }));
164
+ return new PackageGraphBuilder({
165
+ packages: discovered,
166
+ root: isSingle ? target.localSkill.packages[0].packageId : undefined,
167
+ }).buildSync();
168
+ }
169
+ return new PackageGraphBuilder({
170
+ root: target.targetPackageRoot,
171
+ customHome: context?.customHome,
172
+ allowDevLinks: true,
173
+ }).buildSync();
60
174
  }
61
175
  /**
62
176
  * 统一解析 CLI 命令的目标拓扑与包作用域(run, describe, list 等命令的单一事实源)。
@@ -64,55 +178,35 @@ export function resolveTargetFromOptions(options, context) {
64
178
  * 保证:
65
179
  * - 优先判定目标拓扑;
66
180
  * - 远端目标(remote)完全隔离:不执行任何本地目录探测、清单加载或包根寻址;
67
- * - 远端目标动作引用保持原始词法包 ID 拼接(${options.package}/${actionId}),绝不被本地同名目录或清单 ID 篡改;
68
- * - 本地目标按本地封闭技能目录或工程包严格解析,不存在时透明抛出 packageNotFoundError。
181
+ * - 远端目标动作与规程引用保持原始词法包 ID 拼接(${options.package}/${id}),绝不被本地同名目录或清单 ID 篡改;
182
+ * - 本地目标默认按封闭技能目录或工程包严格解析;元数据策略仅跳过单包闭包校验;
183
+ * - 显式目标不存在时透明抛出 packageNotFoundError。
69
184
  */
70
- export function resolveCommandTarget(options, context) {
71
- const resolved = resolveTargetFromOptions(options, context);
72
- if (resolved.type === "remote") {
185
+ export function resolveCommandTarget(options, context, strategy) {
186
+ const result = resolveCommandTargetInternal(options, context, strategy);
187
+ if (result.resolved.type === "remote") {
188
+ const resolveRemoteRef = (id) => {
189
+ if (options.package && !id.includes("/")) {
190
+ return `${options.package}/${id}`;
191
+ }
192
+ return id;
193
+ };
73
194
  return {
74
- resolved,
195
+ resolved: result.resolved,
75
196
  isRemote: true,
76
197
  localSkill: null,
77
198
  targetPackageRoot: undefined,
78
- resolveActionRef: (actionId) => {
79
- if (options.package && !actionId.includes("/")) {
80
- return `${options.package}/${actionId}`;
81
- }
82
- return actionId;
83
- },
199
+ resolveActionRef: resolveRemoteRef,
200
+ resolvePlaybookRef: resolveRemoteRef,
84
201
  };
85
202
  }
86
- // 本地分支
87
- const localSkill = options.package ? inspectLocalPackageDirectory(options.package) : null;
88
- const targetPackageRoot = localSkill
89
- ? localSkill.root
90
- : options.package
91
- ? resolveLocalPackageRoot(options.package)
92
- : findProjectRoot();
93
- if (options.package && !targetPackageRoot) {
94
- throw packageNotFoundError(options.package);
95
- }
96
203
  return {
97
- resolved,
204
+ resolved: result.resolved,
98
205
  isRemote: false,
99
- localSkill,
100
- targetPackageRoot: targetPackageRoot || undefined,
101
- resolveActionRef: (actionId) => {
102
- if (!options.package)
103
- return actionId;
104
- if (localSkill) {
105
- if (localSkill.type === "single" && !actionId.includes("/")) {
106
- return `${localSkill.packages[0].packageId}/${actionId}`;
107
- }
108
- // 复合技能短名不拼目录名,保持 actionId 供 Host resolveAction 裁决唯一匹配或多包歧义
109
- return actionId;
110
- }
111
- if (!actionId.includes("/")) {
112
- return `${options.package}/${actionId}`;
113
- }
114
- return actionId;
115
- },
206
+ localSkill: result.localSkill,
207
+ targetPackageRoot: result.targetPackageRoot,
208
+ resolveActionRef: (actionId) => resolveResourceRef(actionId, options, result.localSkill),
209
+ resolvePlaybookRef: (playbookId) => resolvePlaybookRef(playbookId, options, result.localSkill),
116
210
  };
117
211
  }
118
212
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actiondock/cli",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "description": "ActionDock CLI facade for developing, testing, building, and exporting standalone Agent Actions and Skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,10 +34,10 @@
34
34
  "test": "node ../../scripts/run-tests.ts"
35
35
  },
36
36
  "dependencies": {
37
- "@actiondock/builder": "^2.14.0",
38
- "@actiondock/core": "^2.14.0",
39
- "@actiondock/mcp": "^2.14.0",
40
- "@actiondock/sdk": "^2.14.0",
37
+ "@actiondock/builder": "^2.15.0",
38
+ "@actiondock/core": "^2.15.0",
39
+ "@actiondock/mcp": "^2.15.0",
40
+ "@actiondock/sdk": "^2.15.0",
41
41
  "commander": "^13.1.0",
42
42
  "selfsigned": "^5.5.0"
43
43
  },