@actiondock/cli 2.6.0 → 2.8.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.
Files changed (66) hide show
  1. package/README.md +34 -22
  2. package/bin/ad.js +4 -4
  3. package/dist/commands/action/create.js +9 -25
  4. package/dist/commands/add.js +9 -17
  5. package/dist/commands/build.js +3 -18
  6. package/dist/commands/config/delete.js +8 -7
  7. package/dist/commands/config/env.js +7 -11
  8. package/dist/commands/config/get.js +8 -5
  9. package/dist/commands/config/list.js +16 -22
  10. package/dist/commands/config/schema.d.ts +7 -1
  11. package/dist/commands/config/schema.js +44 -67
  12. package/dist/commands/config/set.js +9 -7
  13. package/dist/commands/describe.js +15 -26
  14. package/dist/commands/doctor.js +4 -3
  15. package/dist/commands/export.js +5 -13
  16. package/dist/commands/generate.js +4 -3
  17. package/dist/commands/info.js +3 -1
  18. package/dist/commands/init.js +3 -3
  19. package/dist/commands/link.js +4 -4
  20. package/dist/commands/list.js +19 -10
  21. package/dist/commands/mcp.js +27 -66
  22. package/dist/commands/pack.js +3 -12
  23. package/dist/commands/playbook.js +30 -10
  24. package/dist/commands/profile.js +10 -19
  25. package/dist/commands/remove.js +5 -14
  26. package/dist/commands/run.d.ts +6 -2
  27. package/dist/commands/run.js +60 -37
  28. package/dist/commands/runs.js +26 -35
  29. package/dist/commands/serve.js +66 -44
  30. package/dist/commands/state-list.d.ts +2 -2
  31. package/dist/commands/state-list.js +10 -8
  32. package/dist/commands/state.d.ts +2 -2
  33. package/dist/commands/state.js +110 -103
  34. package/dist/commands/test.js +1 -1
  35. package/dist/commands/validate.js +4 -17
  36. package/dist/errors.d.ts +14 -0
  37. package/dist/errors.js +78 -2
  38. package/dist/index.d.ts +8 -1
  39. package/dist/index.js +60 -8
  40. package/dist/renderer.d.ts +3 -2
  41. package/dist/renderer.js +4 -19
  42. package/dist/services/config-merge.d.ts +6 -5
  43. package/dist/services/config-merge.js +10 -8
  44. package/dist/services/inspect.js +3 -1
  45. package/dist/standalone/index.d.ts +7 -11
  46. package/dist/standalone/index.js +8 -17
  47. package/dist/types.d.ts +7 -1
  48. package/dist/types.js +1 -1
  49. package/dist/utils/cert.js +1 -1
  50. package/dist/utils/duration.d.ts +1 -1
  51. package/dist/utils/duration.js +1 -1
  52. package/dist/utils/index.d.ts +1 -2
  53. package/dist/utils/index.js +1 -2
  54. package/dist/utils/input.d.ts +5 -42
  55. package/dist/utils/input.js +7 -84
  56. package/dist/utils/server-launch.d.ts +61 -0
  57. package/dist/utils/server-launch.js +114 -0
  58. package/dist/utils/target.d.ts +14 -13
  59. package/dist/utils/target.js +20 -39
  60. package/dist/utils/utils.d.ts +101 -1
  61. package/dist/utils/utils.js +97 -7
  62. package/package.json +5 -6
  63. package/dist/utils/bytes.d.ts +0 -1
  64. package/dist/utils/bytes.js +0 -1
  65. package/dist/utils/filter.d.ts +0 -1
  66. package/dist/utils/filter.js +0 -1
@@ -1,32 +1,78 @@
1
- import { decodeStateKey, findProjectRoot, loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
1
+ import { decodeStateKey, } from "@actiondock/sdk";
2
+ import { findProjectRoot, loadProjectConfig, } from "@actiondock/core";
3
+ import { resolvePackageRoot, } from "@actiondock/core/registry";
2
4
  import { ArgumentError, ExecutionError, packageNotFoundError } from "../errors.js";
3
5
  import { renderResult, renderStateList, writeStderr, writeStdout } from "../renderer.js";
4
- import { applyTargetOptions, getEffectiveOptions, getTargetRoot, resolveIntent, withTarget, } from "../utils/index.js";
6
+ import { applyTargetOptions, getEffectiveOptions, getTargetRoot, remoteTargetLabel, resolveFallbackStrategy, resolveIntent, resolveTargetFromOptions, withService, } from "../utils/index.js";
5
7
  import { renderLinkedPackagesStateList, renderProjectScopedStateList, } from "./state-list.js";
6
8
  /**
7
- * 解析复合状态键为命名空间与裸键视图。
9
+ * 统一预解码状态键寻址(get、set、delete 共享的单一事实源)。
8
10
  *
9
- * 多冒号歧义键(如 `review:owner/repo:42`)不再直接崩溃:
11
+ * 三命令在 CLI 层即完成 `ns:key` 复合键解耦:
12
+ * - 显式 `-n/--namespace` 指定时命名空间以其为准,键保持原串不拆解;
13
+ * - 未显式指定时按复合键语法预解码出命名空间与裸键;
14
+ * - 多冒号歧义键降级为整串裸键并提示。
15
+ *
16
+ * 歧义键(如 `review:owner/repo:42`)不再直接崩溃:
10
17
  * 降级为「整串作为裸 key」并在 stderr 提示可用 -n/--namespace 显式指定,
11
18
  * 保持与本地存储层歧义兜底(deleteStateSmart 的纯键回退)一致的容错语义。
19
+ *
20
+ * 本地与远端分支都消费解耦后的 namespace + key,
21
+ * 消除「set 预解码而 get/delete 依赖存储层回退」的语义不对称。
12
22
  */
13
- function decodeStateKeyWithFallback(rawKey, hasExplicitNamespace, context) {
23
+ function resolveStateAddress(rawKey, options, context) {
24
+ if (options.namespace !== undefined) {
25
+ return { namespace: options.namespace, key: rawKey };
26
+ }
14
27
  try {
15
28
  const decoded = decodeStateKey(rawKey);
16
29
  return { namespace: decoded.namespace || undefined, key: decoded.key };
17
30
  }
18
31
  catch {
19
- if (!hasExplicitNamespace) {
20
- writeStderr(`[WARN] State key '${rawKey}' contains multiple colon delimiters; treating it as a bare key. Use -n/--namespace to specify the namespace explicitly.`, context);
21
- }
32
+ writeStderr(`[WARN] State key '${rawKey}' contains multiple colon delimiters; treating it as a bare key. Use -n/--namespace to specify the namespace explicitly.`, context);
22
33
  return { namespace: undefined, key: rawKey };
23
34
  }
24
35
  }
36
+ /**
37
+ * 解析状态命令目标作用域(remote 与 local 双分支模板的单一事实源)。
38
+ *
39
+ * 远端分支直接以 -P 参数寻址,消息后缀固定为 "on remote server";
40
+ * 本地分支基于 getTargetRoot 完成包寻址(含从键中剥离 pkg/ 前缀),
41
+ * 以工程配置 id 寻址,消息后缀为 "in/from package '...'"。
42
+ *
43
+ * @param resolved 目标拓扑解析结果
44
+ * @param options 命令选项视图
45
+ * @param rawKey 原始键(本地分支用于剥离 pkg/ 前缀)
46
+ * @returns 作用域解析结果(含包标识、消息后缀、本地根目录与工程配置)
47
+ */
48
+ function resolveStateScope(resolved, options, rawKey) {
49
+ if (resolved.type === "remote") {
50
+ return {
51
+ packageId: options.package || "",
52
+ scopeSuffix: "on remote server",
53
+ deleteSuffix: "on remote server",
54
+ effectiveKey: rawKey || "",
55
+ projConfig: undefined,
56
+ localRoot: undefined,
57
+ };
58
+ }
59
+ // 本地项目模式:getTargetRoot 会从键中剥离 pkg/ 前缀,寻址基于解耦后的裸键
60
+ const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
61
+ const projConfig = loadProjectConfig(root);
62
+ return {
63
+ packageId: projConfig.id,
64
+ scopeSuffix: `in package '${projConfig.id}'`,
65
+ deleteSuffix: `from package '${projConfig.id}'`,
66
+ effectiveKey,
67
+ projConfig,
68
+ localRoot: root,
69
+ };
70
+ }
25
71
  /**
26
72
  * 注册 state 状态管理命令(get、set、delete、clear、keys、list)。
27
73
  *
28
- * @param program Commander 实例
29
- * @param context 命令行上下文
74
+ * @param program Commander 根程序对象
75
+ * @param context CLI 上下文
30
76
  */
31
77
  export function registerStateCommands(program, context) {
32
78
  const stateCmd = program
@@ -36,18 +82,18 @@ export function registerStateCommands(program, context) {
36
82
  const handleListKeys = async (prefix = "", rawOptions, cmd) => {
37
83
  const options = getEffectiveOptions(rawOptions, cmd);
38
84
  const effectiveIntent = resolveIntent(options.intent, prefix ? [prefix] : []);
39
- const shouldFallback = options.fallback !== false;
40
- await withTarget(options, context, async (target, resolved) => {
85
+ const { shouldFallback } = resolveFallbackStrategy(options);
86
+ await withService(options, context, async (service, resolved) => {
41
87
  const actionId = options.action || "";
42
88
  // 远端服务模式:直接列举远端包作用域键
43
89
  if (resolved.type === "remote") {
44
- const keys = await target.listStateKeys(options.package || "", actionId, {
90
+ const keys = (await service.management?.state.list(options.package || "", actionId, {
45
91
  namespace: options.namespace,
46
92
  prefix,
47
- });
93
+ })) ?? [];
48
94
  renderResult(keys, {
49
95
  json: options.json,
50
- humanFormatter: () => renderStateList(keys, `Remote Server ${resolved.serverUrl}${resolved.profileName ? ` (Profile: ${resolved.profileName})` : ""}`, false, effectiveIntent),
96
+ humanFormatter: () => renderStateList(keys, remoteTargetLabel(resolved), false, effectiveIntent),
51
97
  context,
52
98
  });
53
99
  return;
@@ -65,7 +111,7 @@ export function registerStateCommands(program, context) {
65
111
  }
66
112
  if (targetRoot) {
67
113
  await renderProjectScopedStateList({
68
- target,
114
+ service,
69
115
  targetRoot,
70
116
  actionId,
71
117
  prefix,
@@ -77,7 +123,7 @@ export function registerStateCommands(program, context) {
77
123
  return;
78
124
  }
79
125
  await renderLinkedPackagesStateList({
80
- target,
126
+ service,
81
127
  actionId,
82
128
  prefix,
83
129
  options,
@@ -96,6 +142,7 @@ export function registerStateCommands(program, context) {
96
142
  .option("-n, --namespace <ns>", "Filter keys under specific namespace (omit to list all namespaces)"))
97
143
  .option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
98
144
  .option("--detail", "Include metadata (ttl, expiresAt, size, updatedAt) in JSON output")
145
+ .option("--fallback", "Enable fallback to full list when no items match intent")
99
146
  .option("--no-fallback", "Disable fallback to full list when no items match intent")
100
147
  .option("--data-dir <path>", "Custom database storage directory")
101
148
  .option("--json", "Output as JSON")
@@ -109,6 +156,7 @@ export function registerStateCommands(program, context) {
109
156
  .option("-n, --namespace <ns>", "Filter keys under specific namespace"))
110
157
  .option("-i, --intent <pattern>", "Regex or fuzzy intent filter")
111
158
  .option("--detail", "Include metadata in JSON output")
159
+ .option("--fallback", "Enable fallback to full list when no items match intent")
112
160
  .option("--no-fallback", "Disable fallback")
113
161
  .option("--data-dir <path>", "Custom database storage directory")
114
162
  .option("--json", "Output as JSON")
@@ -127,45 +175,28 @@ export function registerStateCommands(program, context) {
127
175
  if (!rawKey) {
128
176
  throw new ArgumentError("State key is required");
129
177
  }
130
- await withTarget(options, context, async (target, resolved) => {
178
+ // 惰性作用域缓存:仅本地分支求值一次,避免远端模式触发包寻址副作用
179
+ let scope;
180
+ const resolveScope = (resolved) => (scope ??= resolveStateScope(resolved, options, rawKey));
181
+ await withService(options, context, async (service, resolved) => {
131
182
  const actionId = options.action || "";
132
- if (resolved.type === "remote") {
133
- const decoded = decodeStateKeyWithFallback(rawKey, options.namespace !== undefined, context);
134
- const effectiveNamespace = options.namespace || decoded.namespace;
135
- const actualKey = options.namespace ? rawKey : decoded.key;
136
- const entry = await target.getState(options.package || "", actionId, actualKey, {
137
- namespace: effectiveNamespace,
138
- detail: true,
139
- });
140
- if (entry === undefined || entry.value === undefined) {
141
- throw new ExecutionError(`State key '${rawKey}' not found on remote server`);
142
- }
143
- const val = entry.value;
144
- renderResult({ key: rawKey, value: val, namespace: entry.namespace || effectiveNamespace }, {
145
- json: options.json,
146
- humanFormatter: () => (typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)),
147
- context,
148
- });
149
- return;
150
- }
151
- // 本地项目模式
152
- const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
153
- const projConfig = loadProjectConfig(root);
154
- const entry = await target.getState(projConfig.id, actionId, effectiveKey, {
155
- namespace: options.namespace,
183
+ const targetScope = resolveScope(resolved);
184
+ const address = resolveStateAddress(targetScope.effectiveKey, options, context);
185
+ const entry = await service.management?.state.get(targetScope.packageId, actionId, address.key, {
186
+ namespace: address.namespace,
156
187
  detail: true,
157
188
  });
158
189
  if (entry === undefined || entry.value === undefined) {
159
- throw new ExecutionError(`State key '${rawKey}' not found in package '${projConfig.id}'`);
190
+ throw new ExecutionError(`State key '${rawKey}' not found ${targetScope.scopeSuffix}`);
160
191
  }
161
192
  const val = entry.value;
162
- const matchedNamespace = entry.namespace;
193
+ const matchedNamespace = resolved.type === "remote" ? (entry.namespace || address.namespace) : entry.namespace;
163
194
  renderResult({ key: rawKey, value: val, namespace: matchedNamespace }, {
164
195
  json: options.json,
165
196
  humanFormatter: () => (typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)),
166
197
  context,
167
198
  });
168
- }, { localRoot: () => getTargetRoot(options.package, rawKey).root });
199
+ }, { localRoot: () => resolveScope(resolveTargetFromOptions(options, context)).localRoot });
169
200
  });
170
201
  // state set <key> <value>
171
202
  applyTargetOptions(stateCmd
@@ -192,36 +223,21 @@ export function registerStateCommands(program, context) {
192
223
  if (options.ttl && (isNaN(ttlSec) || ttlSec <= 0)) {
193
224
  throw new ArgumentError(`Invalid --ttl value: '${options.ttl}'. Must be a positive integer.`);
194
225
  }
195
- await withTarget(options, context, async (target, resolved) => {
226
+ // 惰性作用域缓存:仅本地分支求值一次,避免远端模式触发包寻址副作用
227
+ let scope;
228
+ const resolveScope = (resolved) => (scope ??= resolveStateScope(resolved, options, rawKey));
229
+ await withService(options, context, async (service, resolved) => {
196
230
  const actionId = options.action || "";
197
- if (resolved.type === "remote") {
198
- const decoded = decodeStateKeyWithFallback(rawKey, options.namespace !== undefined, context);
199
- const effectiveNamespace = options.namespace || decoded.namespace;
200
- const actualKey = options.namespace ? rawKey : decoded.key;
201
- await target.setState(options.package || "", actionId, actualKey, parsedVal, {
202
- namespace: effectiveNamespace,
203
- ttl: ttlSec,
204
- });
205
- writeStdout(`[OK] State '${rawKey}' updated on remote server`, context);
206
- return;
207
- }
208
- // 本地项目模式
209
- const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
210
- const projConfig = loadProjectConfig(root);
211
- let actualNamespace = options.namespace;
212
- let finalKey = effectiveKey;
213
- if (options.namespace === undefined && effectiveKey.includes(":")) {
214
- const decoded = decodeStateKeyWithFallback(effectiveKey, false, context);
215
- actualNamespace = decoded.namespace;
216
- finalKey = decoded.key;
217
- }
218
- await target.setState(projConfig.id, actionId, finalKey, parsedVal, {
219
- namespace: actualNamespace,
231
+ const targetScope = resolveScope(resolved);
232
+ const address = resolveStateAddress(targetScope.effectiveKey, options, context);
233
+ await service.management?.state.set(targetScope.packageId, actionId, address.key, parsedVal, {
234
+ namespace: address.namespace,
220
235
  ttl: ttlSec,
221
236
  });
222
- const displayKey = actualNamespace ? `${actualNamespace}:${finalKey}` : finalKey;
223
- writeStdout(`[OK] State '${displayKey}' updated in package '${projConfig.id}'`, context);
224
- }, { localRoot: () => getTargetRoot(options.package, rawKey).root });
237
+ // 远端回显原始键;本地回显解耦后的 ns:key 复合视图
238
+ const displayKey = resolved.type === "remote" ? rawKey : (address.namespace ? `${address.namespace}:${address.key}` : address.key);
239
+ writeStdout(`[OK] State '${displayKey}' updated ${targetScope.scopeSuffix}`, context);
240
+ }, { localRoot: () => resolveScope(resolveTargetFromOptions(options, context)).localRoot });
225
241
  });
226
242
  // state delete <key>
227
243
  applyTargetOptions(stateCmd
@@ -237,32 +253,21 @@ export function registerStateCommands(program, context) {
237
253
  if (!rawKey) {
238
254
  throw new ArgumentError("State key is required");
239
255
  }
240
- await withTarget(options, context, async (target, resolved) => {
256
+ // 惰性作用域缓存:仅本地分支求值一次,避免远端模式触发包寻址副作用
257
+ let scope;
258
+ const resolveScope = (resolved) => (scope ??= resolveStateScope(resolved, options, rawKey));
259
+ await withService(options, context, async (service, resolved) => {
241
260
  const actionId = options.action || "";
242
- if (resolved.type === "remote") {
243
- const decoded = decodeStateKeyWithFallback(rawKey, options.namespace !== undefined, context);
244
- const effectiveNamespace = options.namespace || decoded.namespace;
245
- const actualKey = options.namespace ? rawKey : decoded.key;
246
- const deleted = await target.deleteState(options.package || "", actionId, actualKey, {
247
- namespace: effectiveNamespace,
248
- });
249
- if (!deleted) {
250
- throw new ExecutionError(`State key '${rawKey}' not found on remote server`);
251
- }
252
- writeStdout(`[OK] State '${rawKey}' deleted on remote server`, context);
253
- return;
254
- }
255
- // 本地项目模式
256
- const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
257
- const projConfig = loadProjectConfig(root);
258
- const deleted = await target.deleteState(projConfig.id, actionId, effectiveKey, {
259
- namespace: options.namespace,
261
+ const targetScope = resolveScope(resolved);
262
+ const address = resolveStateAddress(targetScope.effectiveKey, options, context);
263
+ const deleted = await service.management?.state.delete(targetScope.packageId, actionId, address.key, {
264
+ namespace: address.namespace,
260
265
  });
261
266
  if (!deleted) {
262
- throw new ExecutionError(`State key '${rawKey}' not found in package '${projConfig.id}'`);
267
+ throw new ExecutionError(`State key '${rawKey}' not found ${targetScope.scopeSuffix}`);
263
268
  }
264
- writeStdout(`[OK] State '${rawKey}' deleted from package '${projConfig.id}'`, context);
265
- }, { localRoot: () => getTargetRoot(options.package, rawKey).root });
269
+ writeStdout(`[OK] State '${rawKey}' deleted ${targetScope.deleteSuffix}`, context);
270
+ }, { localRoot: () => resolveScope(resolveTargetFromOptions(options, context)).localRoot });
266
271
  });
267
272
  // state clear
268
273
  applyTargetOptions(stateCmd
@@ -279,27 +284,29 @@ export function registerStateCommands(program, context) {
279
284
  if (!options.all && options.namespace === undefined) {
280
285
  throw new ArgumentError("Must specify either -n/--namespace <name> to clear specific namespace, or --all to clear entire package state store.");
281
286
  }
282
- await withTarget(options, context, async (target, resolved) => {
287
+ // 惰性作用域缓存:仅本地分支求值一次,避免远端模式触发包寻址副作用
288
+ let scope;
289
+ const resolveScope = (resolved) => (scope ??= resolveStateScope(resolved, options));
290
+ await withService(options, context, async (service, resolved) => {
283
291
  const actionId = options.action || "";
284
292
  if (resolved.type === "remote") {
285
- const count = await target.clearState(options.package || "", actionId, {
293
+ const count = (await service.management?.state.clear(options.package || "", actionId, {
286
294
  namespace: options.namespace,
287
295
  all: Boolean(options.all),
288
- });
296
+ })) ?? 0;
289
297
  writeStdout(`[OK] Cleared ${count} state entry(s) on remote server`, context);
290
298
  return;
291
299
  }
292
300
  // 本地项目模式
293
- const { root } = getTargetRoot(options.package);
294
- const projConfig = loadProjectConfig(root);
295
- const count = await target.clearState(projConfig.id, actionId, {
301
+ const targetScope = resolveScope(resolved);
302
+ const count = (await service.management?.state.clear(targetScope.packageId, actionId, {
296
303
  namespace: options.namespace,
297
304
  all: Boolean(options.all),
298
- });
305
+ })) ?? 0;
299
306
  const scopeDesc = options.all
300
307
  ? "all namespaces"
301
308
  : `namespace '${options.namespace}'`;
302
- writeStdout(`[OK] Cleared ${count} state entry(s) in ${scopeDesc} for package '${projConfig.id}'`, context);
303
- }, { localRoot: () => getTargetRoot(options.package).root });
309
+ writeStdout(`[OK] Cleared ${count} state entry(s) in ${scopeDesc} for package '${targetScope.projConfig.id}'`, context);
310
+ }, { localRoot: () => resolveScope(resolveTargetFromOptions(options, context)).localRoot });
304
311
  });
305
312
  }
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { findProjectRoot } from "@actiondock/core";
3
+ import { findProjectRoot, } from "@actiondock/core";
4
4
  import { writeStderr } from "../renderer.js";
5
5
  import { spawnAsync } from "../utils/index.js";
6
6
  export function registerTestCommand(program) {
@@ -1,7 +1,7 @@
1
- import { checkGeneratedTypes, findProjectRoot, GENERATED_TYPES_OUTDATED_CODE, loadActions, loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
2
- import { ArgumentError, ExecutionError, notInProjectError, packageNotFoundError } from "../errors.js";
1
+ import { checkGeneratedTypes, GENERATED_TYPES_OUTDATED_CODE, loadActions, } from "@actiondock/core/project";
2
+ import { ArgumentError, ExecutionError } from "../errors.js";
3
3
  import { renderActionValidation, renderResult } from "../renderer.js";
4
- import { getEffectiveOptions } from "../utils/index.js";
4
+ import { getEffectiveOptions, requirePackageRoot } from "../utils/index.js";
5
5
  /**
6
6
  * 挂载 validate 子命令至指定 Commander 节点。
7
7
  *
@@ -17,20 +17,7 @@ export function attachValidateCommand(parent, context) {
17
17
  .option("--data-dir <path>", "Custom database storage directory")
18
18
  .action(async (id, rawOptions, cmd) => {
19
19
  const options = getEffectiveOptions(rawOptions, cmd);
20
- let root = null;
21
- if (options.package) {
22
- root = resolvePackageRoot(options.package);
23
- if (!root) {
24
- throw packageNotFoundError(options.package);
25
- }
26
- }
27
- else {
28
- root = findProjectRoot();
29
- }
30
- if (!root) {
31
- throw notInProjectError("Please specify -P, --package <id> or cd into a project directory.");
32
- }
33
- const config = loadProjectConfig(root);
20
+ const { root, projConfig: config } = requirePackageRoot(options.package, { loadConfig: true });
34
21
  const actions = await loadActions(root, config.actionsDir);
35
22
  let toValidate = [];
36
23
  if (id) {
package/dist/errors.d.ts CHANGED
@@ -20,6 +20,20 @@ export declare class ArgumentError extends CliError {
20
20
  export declare class ExecutionError extends CliError {
21
21
  constructor(message: string, details?: unknown, code?: string);
22
22
  }
23
+ /**
24
+ * 将任意异常统一包裹为 CLI 执行错误(错误码保留辅助)。
25
+ *
26
+ * 包裹规则:
27
+ * - CliError 与 ActionDockError 原样透传(保留原始 code、details 与退出码映射);
28
+ * - 其余异常包裹为 ExecutionError,并保留 cause 链接与数字型 code 字段,
29
+ * 避免 core 抛出的结构化错误码被一律降级为 EXECUTION_FAILURE。
30
+ * - messagePrefix 仅对需要包裹的分支生效(附加命令语境说明);
31
+ * 透传分支保持原始实例不变,不重建消息。
32
+ *
33
+ * @param err 待包裹的原始异常
34
+ * @param messagePrefix 可选的消息前缀(仅包裹分支生效)
35
+ */
36
+ export declare function wrapAsExecutionError(err: unknown, messagePrefix?: string): Error;
23
37
  /**
24
38
  * 用户中断信号错误(退出码为 130)。
25
39
  */
package/dist/errors.js CHANGED
@@ -1,4 +1,21 @@
1
+ import { ActionDockError, INVALID_ARGUMENT, INVALID_PACKAGE_ID, PACKAGE_NOT_FOUND, ACTION_NOT_FOUND, NOT_FOUND, PATH_TRAVERSAL, INVALID_FLAT_ARGUMENT, INVALID_JSON, INVALID_JSON_LITERAL, INPUT_PATH_CONFLICT, FLAT_INPUT_LIMIT_EXCEEDED, INPUT_LIMIT_EXCEEDED, INPUT_POLICY_VIOLATION, INPUT_CONFLICT, INPUT_FILE_NOT_FOUND, INPUT_FILE_READ_FAILED, } from "@actiondock/core";
2
+ import { InputError } from "@actiondock/core/project";
1
3
  import { ExitCode } from "./types.js";
4
+ /**
5
+ * 扁平与解析输入错误码集合。
6
+ */
7
+ const FLAT_ERROR_CODES = new Set([
8
+ INVALID_FLAT_ARGUMENT,
9
+ INVALID_JSON,
10
+ INVALID_JSON_LITERAL,
11
+ INPUT_PATH_CONFLICT,
12
+ FLAT_INPUT_LIMIT_EXCEEDED,
13
+ INPUT_LIMIT_EXCEEDED,
14
+ INPUT_POLICY_VIOLATION,
15
+ INPUT_CONFLICT,
16
+ INPUT_FILE_NOT_FOUND,
17
+ INPUT_FILE_READ_FAILED,
18
+ ]);
2
19
  /**
3
20
  * CLI 信封错误码常量(仅 CLI 输出协议使用,属包内部码,就地常量化管理)。
4
21
  */
@@ -39,6 +56,33 @@ export class ExecutionError extends CliError {
39
56
  this.name = "ExecutionError";
40
57
  }
41
58
  }
59
+ /**
60
+ * 将任意异常统一包裹为 CLI 执行错误(错误码保留辅助)。
61
+ *
62
+ * 包裹规则:
63
+ * - CliError 与 ActionDockError 原样透传(保留原始 code、details 与退出码映射);
64
+ * - 其余异常包裹为 ExecutionError,并保留 cause 链接与数字型 code 字段,
65
+ * 避免 core 抛出的结构化错误码被一律降级为 EXECUTION_FAILURE。
66
+ * - messagePrefix 仅对需要包裹的分支生效(附加命令语境说明);
67
+ * 透传分支保持原始实例不变,不重建消息。
68
+ *
69
+ * @param err 待包裹的原始异常
70
+ * @param messagePrefix 可选的消息前缀(仅包裹分支生效)
71
+ */
72
+ export function wrapAsExecutionError(err, messagePrefix) {
73
+ if (err instanceof CliError || err instanceof ActionDockError) {
74
+ return err;
75
+ }
76
+ const originalMessage = err instanceof Error ? err.message : String(err);
77
+ const message = messagePrefix ? `${messagePrefix}${originalMessage}` : originalMessage;
78
+ const rawCode = err?.code;
79
+ const code = typeof rawCode === "string" && rawCode ? rawCode : "EXECUTION_FAILURE";
80
+ const wrapped = new ExecutionError(message, undefined, code);
81
+ if (err instanceof Error) {
82
+ wrapped.cause = err;
83
+ }
84
+ return wrapped;
85
+ }
42
86
  /**
43
87
  * 用户中断信号错误(退出码为 130)。
44
88
  */
@@ -80,10 +124,41 @@ export function formatError(err) {
80
124
  details: err.details,
81
125
  };
82
126
  }
127
+ if (err instanceof InputError) {
128
+ return {
129
+ code: err.code,
130
+ message: err.message,
131
+ exitCode: ExitCode.INVALID_ARGUMENT,
132
+ details: err.details,
133
+ };
134
+ }
135
+ if (err instanceof ActionDockError) {
136
+ const isArgument = err.code === INVALID_ARGUMENT ||
137
+ err.code === INVALID_PACKAGE_ID ||
138
+ err.code === PACKAGE_NOT_FOUND ||
139
+ err.code === ACTION_NOT_FOUND ||
140
+ err.code === NOT_FOUND ||
141
+ err.code === PATH_TRAVERSAL ||
142
+ FLAT_ERROR_CODES.has(err.code);
143
+ return {
144
+ code: err.code,
145
+ message: err.message,
146
+ exitCode: isArgument ? ExitCode.INVALID_ARGUMENT : ExitCode.FAILURE,
147
+ details: err.details,
148
+ };
149
+ }
83
150
  // Commander.js 原生错误处理
84
151
  if (typeof err === "object" && err !== null && "code" in err && typeof err.code === "string") {
152
+ const code = err.code;
153
+ if (FLAT_ERROR_CODES.has(code)) {
154
+ return {
155
+ code,
156
+ message: err.message || String(err),
157
+ exitCode: ExitCode.INVALID_ARGUMENT,
158
+ details: err.details,
159
+ };
160
+ }
85
161
  const commanderErr = err;
86
- const code = commanderErr.code;
87
162
  // 参数类异常
88
163
  if (code.startsWith("commander.unknownOption") ||
89
164
  code.startsWith("commander.missingArgument") ||
@@ -108,9 +183,10 @@ export function formatError(err) {
108
183
  }
109
184
  if (err instanceof Error) {
110
185
  return {
111
- code: CLI_CODE_ERROR,
186
+ code: err.code || CLI_CODE_ERROR,
112
187
  message: err.message,
113
188
  exitCode: ExitCode.FAILURE,
189
+ details: err.details,
114
190
  };
115
191
  }
116
192
  return {
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type InvocationControl, type CliContext } from "./types.js";
1
2
  export * from "./types.js";
2
3
  export * from "./errors.js";
3
4
  export * from "./renderer.js";
@@ -10,6 +11,12 @@ export * from "./commands/index.js";
10
11
  * ActionDock CLI 主执行入口函数。
11
12
  *
12
13
  * @param argv 命令行参数数组(默认使用 process.argv)
14
+ * @param control 可选的中断与取消控制契约
13
15
  * @returns 进程退出码
14
16
  */
15
- export declare function main(argv?: string[]): Promise<number>;
17
+ export declare function main(argv?: string[], control?: InvocationControl, contextOptions?: Partial<CliContext>): Promise<number>;
18
+ /**
19
+ * 命令行独立进程适配器。
20
+ * 仅在命令行可执行入口调用,负责全局 SIGINT 监听与退出状态码写入。
21
+ */
22
+ export declare function runCliProcess(argv?: string[]): Promise<void>;
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { CommanderError } from "commander";
2
2
  import { createCliProgram } from "./commands/index.js";
3
- import { formatError } from "./errors.js";
3
+ import { formatError, SigintError } from "./errors.js";
4
4
  import { renderError } from "./renderer.js";
5
+ import { ExitCode } from "./types.js";
5
6
  export * from "./types.js";
6
7
  export * from "./errors.js";
7
8
  export * from "./renderer.js";
@@ -14,16 +15,37 @@ export * from "./commands/index.js";
14
15
  * ActionDock CLI 主执行入口函数。
15
16
  *
16
17
  * @param argv 命令行参数数组(默认使用 process.argv)
18
+ * @param control 可选的中断与取消控制契约
17
19
  * @returns 进程退出码
18
20
  */
19
- export async function main(argv = process.argv) {
20
- const program = createCliProgram();
21
+ export async function main(argv = process.argv, control, contextOptions) {
22
+ const context = { control, exitCode: 0, ...contextOptions };
23
+ const program = createCliProgram(context);
21
24
  const isMachine = argv.includes("--json");
25
+ const normalizedArgv = argv.length >= 2 &&
26
+ (argv[0].endsWith("node") ||
27
+ argv[0].endsWith("bun") ||
28
+ argv[0].endsWith("tsx") ||
29
+ argv[1].includes("/") ||
30
+ argv[1].includes("\\") ||
31
+ argv[1].endsWith(".js") ||
32
+ argv[1].endsWith(".ts"))
33
+ ? argv
34
+ : ["node", "ad", ...argv];
22
35
  try {
23
- await program.parseAsync(argv);
24
- return typeof process.exitCode === "number" ? process.exitCode : 0;
36
+ await program.parseAsync(normalizedArgv);
37
+ if (control?.cancellationSource === "sigint" && control?.signal?.aborted) {
38
+ return ExitCode.SIGINT;
39
+ }
40
+ return (context.exitCode && context.exitCode !== 0)
41
+ ? context.exitCode
42
+ : (typeof process.exitCode === "number" ? process.exitCode : 0);
25
43
  }
26
44
  catch (err) {
45
+ if (control?.cancellationSource === "sigint" &&
46
+ (control?.signal?.aborted || err?.name === "AbortError" || err?.code === "SIGINT_INTERRUPTED" || err instanceof SigintError)) {
47
+ return ExitCode.SIGINT;
48
+ }
27
49
  if (err instanceof CommanderError) {
28
50
  if (err.exitCode === 0) {
29
51
  return 0;
@@ -32,9 +54,39 @@ export async function main(argv = process.argv) {
32
54
  // 复用 renderer 的 renderError 统一错误信封,消除手工拼装的双实现
33
55
  renderError(err, {
34
56
  json: isMachine,
57
+ context,
35
58
  });
36
- process.exitCode = formatError(err).exitCode;
37
- return process.exitCode;
59
+ return formatError(err).exitCode;
60
+ }
61
+ }
62
+ /**
63
+ * 命令行独立进程适配器。
64
+ * 仅在命令行可执行入口调用,负责全局 SIGINT 监听与退出状态码写入。
65
+ */
66
+ export async function runCliProcess(argv = process.argv) {
67
+ const controller = new AbortController();
68
+ const control = {
69
+ signal: controller.signal,
70
+ };
71
+ let sigintCount = 0;
72
+ const sigintHandler = () => {
73
+ sigintCount++;
74
+ if (sigintCount === 1) {
75
+ control.cancellationSource = "sigint";
76
+ controller.abort(new Error("Interrupted by SIGINT"));
77
+ }
78
+ else {
79
+ process.exitCode = 130;
80
+ process.exit(130);
81
+ }
82
+ };
83
+ process.on("SIGINT", sigintHandler);
84
+ try {
85
+ const exitCode = await main(argv, control);
86
+ process.exitCode = exitCode;
87
+ }
88
+ finally {
89
+ process.removeListener("SIGINT", sigintHandler);
38
90
  }
39
91
  }
40
92
  import { fileURLToPath } from "node:url";
@@ -42,7 +94,7 @@ import { resolve } from "node:path";
42
94
  const isDirectRun = Boolean(process.argv[1]) &&
43
95
  resolve(process.argv[1]) === fileURLToPath(import.meta.url);
44
96
  if (isDirectRun) {
45
- main().catch((err) => {
97
+ runCliProcess().catch((err) => {
46
98
  console.error("Fatal error:", err);
47
99
  process.exitCode = 1;
48
100
  });
@@ -1,5 +1,5 @@
1
1
  import type { RunRecord } from "@actiondock/sdk";
2
- import type { RegistryStatusReport } from "@actiondock/core";
2
+ import { type RegistryStatusReport } from "@actiondock/core/registry";
3
3
  import type { Envelope, ProjectDetailInfo, AggregatedPackage, EnvCheckItem, CliContext } from "./types.js";
4
4
  /**
5
5
  * 构造标准成功结果信封。
@@ -112,7 +112,8 @@ export declare function renderActionList(items: Array<{
112
112
  packageId?: string;
113
113
  }>, title?: string, isFallback?: boolean, intent?: string): string;
114
114
  /**
115
- * 格式化渲染 Action 详情。
115
+ * 格式化渲染 Action 详情(编码顾问 Encoding Advisor)。
116
+ * 统一复用 core 下沉的 formatActionDetail 实现,与独立分发器保持完全一致。
116
117
  */
117
118
  export declare function renderActionDetail(action: {
118
119
  id: string;