@actiondock/cli 2.7.0 → 2.9.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 +6 -7
  2. package/bin/ad.js +52 -23
  3. package/dist/commands/action/create.js +4 -5
  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 +10 -9
  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 +5 -2
  27. package/dist/commands/run.js +19 -20
  28. package/dist/commands/runs.js +26 -35
  29. package/dist/commands/serve.js +177 -57
  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 +46 -2
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.js +3 -3
  40. package/dist/renderer.d.ts +1 -1
  41. package/dist/renderer.js +1 -1
  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 +4 -14
  46. package/dist/standalone/index.js +5 -44
  47. package/dist/types.d.ts +2 -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 +9 -1
  55. package/dist/utils/input.js +12 -1
  56. package/dist/utils/server-launch.d.ts +114 -0
  57. package/dist/utils/server-launch.js +275 -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,4 +1,4 @@
1
- import { type InvocationControl } from "@actiondock/core";
1
+ import { type InvocationControl } from "@actiondock/core/server";
2
2
  import type { ExecutionResult } from "@actiondock/sdk";
3
3
  import { Command } from "commander";
4
4
  import type { CliContext } from "../types.js";
@@ -10,7 +10,10 @@ import type { CliContext } from "../types.js";
10
10
  */
11
11
  export declare function renderRawExecutionResult(targetRef: string, result: ExecutionResult, context?: CliContext): void;
12
12
  /**
13
- * 统一执行 Action 核心逻辑(使用 ActionDockTarget 门面)。
13
+ * 统一执行 Action 核心逻辑(使用 ActionDockService 服务端口)。
14
+ *
15
+ * @param id 目标 Action 标识
16
+ * @param options 执行选项(由 Command 统一构造)
14
17
  */
15
18
  export declare function executeAction(id: string, options: any, context?: CliContext, flatArgs?: string[], control?: InvocationControl): Promise<void>;
16
19
  /**
@@ -1,7 +1,8 @@
1
- import { parseDuration, resolveTarget, validateActionInputValue, mapInputValidationFailure, } from "@actiondock/core";
1
+ import { validateActionInputValue, mapInputValidationFailure, } from "@actiondock/core/project";
2
+ import { resolveTarget, } from "@actiondock/core/profile";
2
3
  import { ArgumentError, ExecutionError, SigintError, packageNotFoundError } from "../errors.js";
3
4
  import { writeStderr, writeStdout } from "../renderer.js";
4
- import { applyTargetOptions, getEffectiveOptions, resolveActionInput, resolveLocalPackageRoot, withTarget, } from "../utils/index.js";
5
+ import { applyTargetOptions, getEffectiveOptions, resolveActionInput, resolveLocalPackageRoot, resolveTimeoutMs, withService, } from "../utils/index.js";
5
6
  /**
6
7
  * 以原始纯文本形式渲染 Action 执行终态结果(默认纯文本模式)。
7
8
  *
@@ -95,7 +96,10 @@ export function renderRawExecutionResult(targetRef, result, context) {
95
96
  }
96
97
  }
97
98
  /**
98
- * 统一执行 Action 核心逻辑(使用 ActionDockTarget 门面)。
99
+ * 统一执行 Action 核心逻辑(使用 ActionDockService 服务端口)。
100
+ *
101
+ * @param id 目标 Action 标识
102
+ * @param options 执行选项(由 Command 统一构造)
99
103
  */
100
104
  export async function executeAction(id, options, context, flatArgs, control) {
101
105
  if (!id) {
@@ -114,15 +118,7 @@ export async function executeAction(id, options, context, flatArgs, control) {
114
118
  if (!check.valid) {
115
119
  throw mapInputValidationFailure("cli-pre-target", check);
116
120
  }
117
- let timeoutMs;
118
- if (options.timeout) {
119
- try {
120
- timeoutMs = parseDuration(options.timeout);
121
- }
122
- catch (err) {
123
- throw new ArgumentError(`Invalid timeout format: ${err.message}`);
124
- }
125
- }
121
+ const timeoutMs = resolveTimeoutMs(options.timeout);
126
122
  const configOverrides = {};
127
123
  if (options.config) {
128
124
  const list = Array.isArray(options.config) ? options.config : [options.config];
@@ -149,14 +145,14 @@ export async function executeAction(id, options, context, flatArgs, control) {
149
145
  throw packageNotFoundError(options.package);
150
146
  }
151
147
  let targetRef = id;
152
- if (options.package && !id.includes("/") && !id.includes(":")) {
148
+ if (options.package && !id.includes("/")) {
153
149
  targetRef = `${options.package}/${id}`;
154
150
  }
155
- // 通过 Target 统一执行
156
- await withTarget(options, context, async (target) => {
151
+ // 通过 Service 统一执行
152
+ await withService(options, context, async (service) => {
157
153
  const isMachine = Boolean(options.json);
158
154
  if (options.async) {
159
- const ticket = await target.startAction(targetRef, input, {
155
+ const ticket = await service.execution.start(targetRef, input, {
160
156
  signal: effectiveSignal,
161
157
  timeoutMs,
162
158
  config: configOverrides,
@@ -182,7 +178,7 @@ export async function executeAction(id, options, context, flatArgs, control) {
182
178
  }
183
179
  }
184
180
  else {
185
- const result = await target.runAction(targetRef, input, {
181
+ const result = await service.execution.run(targetRef, input, {
186
182
  signal: effectiveSignal,
187
183
  timeoutMs,
188
184
  config: configOverrides,
@@ -208,10 +204,13 @@ export async function executeAction(id, options, context, flatArgs, control) {
208
204
  }, { localRoot: targetPackageRoot || undefined, scanLinkedPackages: true, ownDataDir: true });
209
205
  }
210
206
  catch (err) {
207
+ // sigint 取消源、中断链路特征错误码或 SigintError 本身,均统一归并为 SigintError
208
+ // 原 if 中额外的「cancellationSource 为 sigint 且 AbortError/aborted/ACTION_CANCELLED」
209
+ // 子句被首项 cancellationSource === "sigint" 完全蕴含,等价化简后移除
211
210
  const isSigint = effectiveControl?.cancellationSource === "sigint" ||
212
- err?.message?.includes("Interrupted by SIGINT");
213
- if (isSigint &&
214
- (err?.name === "AbortError" || effectiveSignal?.aborted || err?.message?.includes("SIGINT") || err instanceof SigintError)) {
211
+ err?.code === "SIGINT_INTERRUPTED" ||
212
+ err instanceof SigintError;
213
+ if (isSigint) {
215
214
  throw new SigintError();
216
215
  }
217
216
  throw err;
@@ -1,7 +1,9 @@
1
- import { filterWithFallbackInfo, findProjectRoot, listLinkedPackages, loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
2
- import { ArgumentError, ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE, notInProjectError, packageNotFoundError, } from "../errors.js";
1
+ import { findProjectRoot, loadProjectConfig, } from "@actiondock/core";
2
+ import { filterWithFallbackInfo, } from "@actiondock/core/project";
3
+ import { listLinkedPackages, resolvePackageRoot, } from "@actiondock/core/registry";
4
+ import { ArgumentError, ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE, packageNotFoundError, } from "../errors.js";
3
5
  import { renderResult, renderRunDetail, renderRunsList } from "../renderer.js";
4
- import { applyTargetOptions, getEffectiveOptions, resolveIntent, resolveTargetFromOptions, withRemoteTarget, withTarget, } from "../utils/index.js";
6
+ import { applyTargetOptions, getEffectiveOptions, remoteTargetSuffix, requirePackageRoot, resolveFallbackStrategy, resolveIntent, withService, } from "../utils/index.js";
5
7
  /**
6
8
  * 解析本地目标包根目录与工程配置(仅在 local 分支调用)。
7
9
  * 显式指定包且寻址失败时抛出参数错误;工程清单损坏时降级为链接包视图。
@@ -50,21 +52,26 @@ export function registerRunsCommands(program, context) {
50
52
  .option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
51
53
  .option("-a, --action <actionId>", "Filter by action ID")
52
54
  .option("-n, --limit <count>", "Maximum number of records to return", "20"))
55
+ .option("--fallback", "Enable fallback to full list when no items match intent")
53
56
  .option("--no-fallback", "Disable fallback to full list when no items match intent")
54
57
  .option("--data-dir <path>", "Custom database storage directory")
55
58
  .option("--json", "Output as JSON")
56
59
  .action(async (patterns = [], rawOptions, cmd) => {
57
60
  const options = getEffectiveOptions(rawOptions, cmd);
58
61
  const effectiveIntent = resolveIntent(options.intent, patterns);
59
- const shouldFallback = options.fallback !== false;
62
+ const { shouldFallback } = resolveFallbackStrategy(options);
60
63
  const limit = Number.parseInt(options.limit, 10) || 20;
61
64
  const scope = resolveLocalRunScope(options.package);
62
- // 通过 Target 门面统一访问
63
- await withTarget(options, context, async (target, resolved) => {
64
- const records = await target.listRuns({
65
+ // 通过 Service 门面统一访问
66
+ await withService(options, context, async (service, resolved) => {
67
+ // limit 下推查询层:intent 过滤发生在内存(过滤后仍需截断到用户值),
68
+ // 下推时附加合理裕量,保证过滤后可截断数量不少于用户请求
69
+ const FETCH_HEADROOM = 200;
70
+ const queryLimit = effectiveIntent ? limit + FETCH_HEADROOM : limit;
71
+ const records = await service.runs.list({
65
72
  packageId: options.package,
66
73
  actionId: options.action,
67
- limit: 500,
74
+ limit: queryLimit,
68
75
  });
69
76
  // 若处于本地无项目环境且无任何软链接包,则直接输出友好提示
70
77
  if (resolved.type === "local" && !scope.targetPackageRoot) {
@@ -85,7 +92,7 @@ export function registerRunsCommands(program, context) {
85
92
  humanFormatter: () => {
86
93
  let title = "Execution Runs";
87
94
  if (resolved.type === "remote") {
88
- title = `Execution Runs on remote server ${resolved.serverUrl}${resolved.profileName ? ` (Profile: ${resolved.profileName})` : ""}`;
95
+ title = `Execution Runs ${remoteTargetSuffix(resolved)}`;
89
96
  }
90
97
  else if (scope.projConfig) {
91
98
  title = `Execution Runs in ${scope.projConfig.name} (${scope.projConfig.id})`;
@@ -112,9 +119,9 @@ export function registerRunsCommands(program, context) {
112
119
  throw new ArgumentError("Run ID is required");
113
120
  }
114
121
  const scope = resolveLocalRunScope(options.package);
115
- // 通过 Target 门面统一查询
116
- await withTarget(options, context, async (target, resolved) => {
117
- const run = await target.getRun(id);
122
+ // 通过 Service 门面统一查询
123
+ await withService(options, context, async (service, resolved) => {
124
+ const run = await service.runs.get(id);
118
125
  if (!run) {
119
126
  if (resolved.type === "remote") {
120
127
  throw new ExecutionError(`Run record '${id}' not found on remote server`);
@@ -144,12 +151,8 @@ export function registerRunsCommands(program, context) {
144
151
  if (!id) {
145
152
  throw new ArgumentError("Run ID is required for cancel");
146
153
  }
147
- const resolved = resolveTargetFromOptions(options, context);
148
- if (resolved.type === "local") {
149
- throw new ArgumentError("'ad runs cancel' is only supported for remote execution targets. Use --profile <name> or --server <url>.");
150
- }
151
- await withRemoteTarget(options, context, async (target) => {
152
- const result = await target.cancelRun(id, options.reason);
154
+ await withService(options, context, async (service) => {
155
+ const result = await service.runs.cancel(id, options.reason);
153
156
  const isErrorOutcome = result.outcome === "not_found" || result.outcome === "not_owner";
154
157
  renderResult(result, {
155
158
  json: options.json,
@@ -167,7 +170,7 @@ export function registerRunsCommands(program, context) {
167
170
  if (isErrorOutcome) {
168
171
  process.exitCode = 1;
169
172
  }
170
- });
173
+ }, { requireRemote: true });
171
174
  });
172
175
  // runs clear
173
176
  applyTargetOptions(runsCmd
@@ -182,19 +185,7 @@ export function registerRunsCommands(program, context) {
182
185
  let targetPackageRoot;
183
186
  let packageId = options.package;
184
187
  if (!options.profile && !options.server) {
185
- if (options.package) {
186
- const root = resolvePackageRoot(options.package);
187
- if (!root) {
188
- throw packageNotFoundError(options.package);
189
- }
190
- targetPackageRoot = root;
191
- }
192
- else {
193
- targetPackageRoot = findProjectRoot() || undefined;
194
- if (!targetPackageRoot) {
195
- throw notInProjectError("Please specify -P, --package <id> or cd into a project directory.");
196
- }
197
- }
188
+ targetPackageRoot = requirePackageRoot(options.package).root;
198
189
  try {
199
190
  packageId = loadProjectConfig(targetPackageRoot).id;
200
191
  }
@@ -202,9 +193,9 @@ export function registerRunsCommands(program, context) {
202
193
  // 清单损坏时保留原始 -P 参数作为包标识
203
194
  }
204
195
  }
205
- await withTarget(options, context, async (target, resolved) => {
206
- const count = target.clearRuns
207
- ? await target.clearRuns({ packageId, actionId: options.action })
196
+ await withService(options, context, async (service, resolved) => {
197
+ const count = service.runs.clear
198
+ ? await service.runs.clear({ packageId, actionId: options.action })
208
199
  : 0;
209
200
  const payload = { ok: true, clearedCount: count };
210
201
  renderResult(payload, {
@@ -1,10 +1,12 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { createActionDockTarget, findProjectRoot, formatHostForUrl, loadProjectConfig, startActionDockServer, } from "@actiondock/core";
4
- import { createNodePlatform } from "@actiondock/runtime-node";
5
- import { ArgumentError, ExecutionError } from "../errors.js";
3
+ import { createActionDock, createNodePlatform, findProjectRoot, loadProjectConfig, startActionDockServer, } from "@actiondock/core";
4
+ import { resolvePackageRoot } from "@actiondock/core/registry";
5
+ import { parseActionRef } from "@actiondock/core/graph";
6
+ import { formatHostForUrl, } from "@actiondock/core/server";
7
+ import { ArgumentError, ExecutionError, packageNotFoundError } from "../errors.js";
6
8
  import { writeStderr, writeStdout } from "../renderer.js";
7
- import { ensureSelfSignedCertificate, getEffectiveOptions, parseByteSize } from "../utils/index.js";
9
+ import { ensureSelfSignedCertificate, getEffectiveOptions, loadViewsFromFile, mergeServerViews, normalizeCorsOrigins, registerStopSignalHandler, printServerBanner, resolveMaxBodyBytes, resolveServerHost, resolveServerPort, resolveServerToken, parseListOption, } from "../utils/index.js";
8
10
  /**
9
11
  * 注册 serve HTTP 服务启动命令。
10
12
  *
@@ -31,17 +33,20 @@ export function registerServeCommand(program, context) {
31
33
  .option("--tls-ca <path>", "Path to TLS CA certificate file (or set ACTIONDOCK_TLS_CA)")
32
34
  .option("--tls-passphrase <passphrase>", "Passphrase for TLS private key (or set ACTIONDOCK_TLS_PASSPHRASE)")
33
35
  .option("-d, --dir <path>", "Project root directory (default: current working directory)")
36
+ .option("-P, --package <package-id>", "Specific package ID(s) to serve (can be specified multiple times or comma-separated)", parseListOption, [])
37
+ .option("-A, --action <action-ref>", "Specific action ref(s) to serve (can be specified multiple times or comma-separated)", parseListOption, [])
38
+ .option("--views-file <path>", "Path to JSON file configuring virtual projection views")
39
+ .option("--views <json>", "JSON string configuring virtual projection views")
34
40
  .option("--data-dir <path>", "Custom database storage directory")
35
41
  .action(async (rawOptions, cmd) => {
36
42
  const options = getEffectiveOptions(rawOptions, cmd);
37
- const parsedPort = parseInt(options.port, 10);
38
- const port = Number.isNaN(parsedPort) ? 5177 : parsedPort;
39
- const host = options.host || "127.0.0.1";
40
- const token = options.token || (typeof process !== "undefined" ? process.env?.ACTIONDOCK_TOKEN : undefined);
43
+ const port = resolveServerPort(options.port, 5177);
44
+ const host = resolveServerHost(options.host);
45
+ const token = resolveServerToken(options.token);
41
46
  const allowInsecureNoAuth = Boolean(options.allowInsecureNoAuth);
42
47
  const allowQueryToken = Boolean(options.allowQueryToken);
43
48
  const enableManagement = Boolean(options.management);
44
- const corsOrigins = options.corsOrigin && options.corsOrigin.length > 0 ? options.corsOrigin : undefined;
49
+ const corsOrigins = normalizeCorsOrigins(options.corsOrigin);
45
50
  const exposeDebugInfo = Boolean(options.exposeDebugInfo);
46
51
  const httpsEnabled = Boolean(options.https ||
47
52
  options.tlsCert ||
@@ -82,42 +87,153 @@ export function registerServeCommand(program, context) {
82
87
  };
83
88
  }
84
89
  }
85
- let maxBodyBytes;
86
- if (options.maxBody) {
87
- try {
88
- maxBodyBytes = parseByteSize(options.maxBody);
89
- }
90
- catch (err) {
91
- throw new ArgumentError(`Invalid max-body format: ${err.message}`);
92
- }
93
- }
90
+ const maxBodyBytes = resolveMaxBodyBytes(options.maxBody);
94
91
  const projectRoot = options.dir
95
92
  ? resolve(options.dir)
96
93
  : findProjectRoot(process.cwd());
94
+ let projectConfig;
97
95
  let projectName = "Global Registry Mode";
98
96
  if (projectRoot) {
99
97
  try {
100
- const config = loadProjectConfig(projectRoot);
101
- projectName = `${config.name} (${config.id})`;
98
+ projectConfig = loadProjectConfig(projectRoot);
99
+ projectName = `${projectConfig.name} (${projectConfig.id})`;
102
100
  }
103
101
  catch {
104
102
  // 降级处理
105
103
  }
106
104
  }
105
+ const fileViews = options.viewsFile
106
+ ? loadViewsFromFile(options.viewsFile)
107
+ : undefined;
108
+ let cliViews;
109
+ if (options.views) {
110
+ try {
111
+ const parsed = typeof options.views === "string" ? JSON.parse(options.views) : options.views;
112
+ cliViews =
113
+ parsed && typeof parsed === "object" && "views" in parsed && (typeof parsed.views === "object" || Array.isArray(parsed.views))
114
+ ? parsed.views
115
+ : parsed;
116
+ }
117
+ catch (err) {
118
+ const msg = err instanceof Error ? err.message : String(err);
119
+ throw new ArgumentError(`Failed to parse --views option: ${msg}`);
120
+ }
121
+ }
122
+ const configViews = projectConfig?.server?.views;
123
+ const viewsConfig = mergeServerViews(mergeServerViews(configViews, fileViews), cliViews);
124
+ const rawPackages = options.package || [];
125
+ const packageAllowlist = rawPackages.length > 0 ? [] : undefined;
126
+ const resolvedPackageIds = [];
127
+ const explicitPackages = [];
128
+ const rawActions = options.action || [];
129
+ const actionAllowlist = rawActions.length > 0 ? [...rawActions] : undefined;
130
+ if (rawPackages.length > 0) {
131
+ for (const pkgId of rawPackages) {
132
+ const root = resolvePackageRoot(pkgId, projectRoot || undefined, context?.customHome);
133
+ if (!root) {
134
+ throw packageNotFoundError(pkgId);
135
+ }
136
+ if (!projectRoot || root !== projectRoot) {
137
+ if (!explicitPackages.some((p) => p.packageRoot === root)) {
138
+ explicitPackages.push({ packageRoot: root });
139
+ }
140
+ }
141
+ let resolvedId = pkgId;
142
+ try {
143
+ const config = loadProjectConfig(root);
144
+ if (config.id) {
145
+ resolvedId = config.id;
146
+ if (!packageAllowlist.includes(config.id)) {
147
+ packageAllowlist.push(config.id);
148
+ }
149
+ }
150
+ }
151
+ catch {
152
+ // 降级使用原始标识符
153
+ }
154
+ if (!packageAllowlist.includes(pkgId)) {
155
+ packageAllowlist.push(pkgId);
156
+ }
157
+ if (!resolvedPackageIds.includes(resolvedId)) {
158
+ resolvedPackageIds.push(resolvedId);
159
+ }
160
+ }
161
+ }
162
+ if (rawActions.length > 0) {
163
+ for (const actRef of rawActions) {
164
+ try {
165
+ const parsed = parseActionRef(actRef);
166
+ if (parsed.packageId) {
167
+ const root = resolvePackageRoot(parsed.packageId, projectRoot || undefined, context?.customHome);
168
+ if (root) {
169
+ if (!projectRoot || root !== projectRoot) {
170
+ if (!explicitPackages.some((p) => p.packageRoot === root)) {
171
+ explicitPackages.push({ packageRoot: root });
172
+ }
173
+ }
174
+ }
175
+ else {
176
+ writeStderr(`Warning: Package '${parsed.packageId}' specified in action '${actRef}' was not found.`, context);
177
+ }
178
+ }
179
+ }
180
+ catch {
181
+ // 忽略非标准或短引用形态
182
+ }
183
+ }
184
+ }
185
+ if (viewsConfig) {
186
+ const viewItems = Array.isArray(viewsConfig) ? viewsConfig : Object.values(viewsConfig);
187
+ for (const item of viewItems) {
188
+ if (!item || typeof item !== "object")
189
+ continue;
190
+ if (Array.isArray(item.packageAllowlist)) {
191
+ for (const pkgId of item.packageAllowlist) {
192
+ if (typeof pkgId === "string") {
193
+ const root = resolvePackageRoot(pkgId, projectRoot || undefined, context?.customHome);
194
+ if (root && (!projectRoot || root !== projectRoot)) {
195
+ if (!explicitPackages.some((p) => p.packageRoot === root)) {
196
+ explicitPackages.push({ packageRoot: root });
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ if (Array.isArray(item.actionAllowlist)) {
203
+ for (const actRef of item.actionAllowlist) {
204
+ if (typeof actRef === "string") {
205
+ try {
206
+ const parsed = parseActionRef(actRef);
207
+ if (parsed.packageId) {
208
+ const root = resolvePackageRoot(parsed.packageId, projectRoot || undefined, context?.customHome);
209
+ if (root && (!projectRoot || root !== projectRoot)) {
210
+ if (!explicitPackages.some((p) => p.packageRoot === root)) {
211
+ explicitPackages.push({ packageRoot: root });
212
+ }
213
+ }
214
+ }
215
+ }
216
+ catch { }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
107
222
  const platform = createNodePlatform({
108
223
  customHome: context?.customHome,
109
224
  dataDir: options.dataDir || context?.dataDir,
110
225
  rootDir: projectRoot || undefined,
111
226
  });
112
- const target = await createActionDockTarget({
227
+ const service = await createActionDock({
113
228
  type: "local",
114
229
  projectRoot: projectRoot || undefined,
230
+ packages: explicitPackages.length > 0 ? explicitPackages : undefined,
115
231
  customHome: context?.customHome,
116
232
  dataDir: options.dataDir || context?.dataDir,
117
233
  platform,
118
- scanLinkedPackages: !projectRoot,
234
+ scanLinkedPackages: !projectRoot || rawPackages.length > 0 || rawActions.length > 0 || Boolean(viewsConfig),
119
235
  });
120
- let mcpHandler;
236
+ let mcpHandlerFactory;
121
237
  const enableMcp = options.mcp !== false;
122
238
  if (enableMcp) {
123
239
  // MCP 处理器创建失败时直接终止启动,避免横幅宣称不存在的端点
@@ -126,21 +242,26 @@ export function registerServeCommand(program, context) {
126
242
  import("@actiondock/mcp"),
127
243
  import("@modelcontextprotocol/server"),
128
244
  ]);
129
- const handler = createMcpHandler(() => {
130
- return createActionDockMcpServer({
131
- target,
245
+ mcpHandlerFactory = (view) => {
246
+ const handler = createMcpHandler(() => {
247
+ return createActionDockMcpServer({
248
+ service,
249
+ packageAllowlist: view.packageAllowlist,
250
+ packageIds: view.packageAllowlist,
251
+ actionAllowlist: view.actionAllowlist,
252
+ });
253
+ }, {
254
+ onerror: (err) => {
255
+ writeStderr(`[MCP HTTP Error] ${err?.message || String(err)}`, context);
256
+ },
132
257
  });
133
- }, {
134
- onerror: (err) => {
135
- writeStderr(`[MCP HTTP Error] ${err?.message || String(err)}`, context);
136
- },
137
- });
138
- mcpHandler = async (req) => {
139
- return handler.fetch(req);
258
+ return async (req) => {
259
+ return handler.fetch(req);
260
+ };
140
261
  };
141
262
  }
142
263
  catch (err) {
143
- await target.close().catch(() => { });
264
+ await service.close().catch(() => { });
144
265
  throw new ExecutionError(`Failed to initialize MCP endpoint: ${err?.message || String(err)}`, err);
145
266
  }
146
267
  }
@@ -156,19 +277,31 @@ export function registerServeCommand(program, context) {
156
277
  exposeDebugInfo,
157
278
  enableMcp,
158
279
  enableManagement,
159
- mcpHandler,
280
+ mcpHandlerFactory,
160
281
  tls,
161
282
  projectRoot: projectRoot || undefined,
162
- target,
283
+ service,
284
+ packageAllowlist,
285
+ actionAllowlist,
286
+ views: viewsConfig,
163
287
  });
164
288
  const displayHost = formatHostForUrl(host);
165
289
  const actualEndpointHost = formatHostForUrl(host === "0.0.0.0" ? "127.0.0.1" : host);
166
290
  const scheme = tls ? "https" : "http";
167
- writeStdout(`\n======================================================`, context);
168
- writeStdout(` ActionDock 2.0 HTTP Runner Server`, context);
169
- writeStdout(`======================================================`, context);
170
- writeStdout(` * Listening on: ${scheme}://${displayHost}:${server.port}`, context);
171
- writeStdout(` * Project: ${projectName}`, context);
291
+ printServerBanner(`ActionDock 2.0 HTTP Runner Server`, scheme, displayHost, server.port, context, {
292
+ views: viewsConfig,
293
+ endpointHost: actualEndpointHost,
294
+ enableMcp,
295
+ });
296
+ if (resolvedPackageIds.length > 0) {
297
+ writeStdout(` * Packages: ${resolvedPackageIds.join(", ")}`, context);
298
+ }
299
+ else {
300
+ writeStdout(` * Project: ${projectName}`, context);
301
+ }
302
+ if (actionAllowlist && actionAllowlist.length > 0) {
303
+ writeStdout(` * Actions: ${actionAllowlist.join(", ")}`, context);
304
+ }
172
305
  if (projectRoot && exposeDebugInfo) {
173
306
  writeStdout(` * Root Path: ${projectRoot}`, context);
174
307
  }
@@ -183,30 +316,17 @@ export function registerServeCommand(program, context) {
183
316
  writeStdout(` * Management APIs: ${enableManagement ? "Enabled" : "Disabled (Default)"}`, context);
184
317
  writeStdout(` * CORS Origins: ${corsOrigins ? corsOrigins.join(", ") : "Disabled (Default)"}`, context);
185
318
  writeStdout(` * Max Body Size: ${options.maxBody || "1mb"}`, context);
186
- writeStdout(` * Health Endpoint: ${scheme}://${actualEndpointHost}:${server.port}/api/v1/health`, context);
319
+ writeStdout(` * Health Endpoint: ${scheme}://${actualEndpointHost}:${server.port}/api/v2/health`, context);
187
320
  if (enableMcp) {
188
321
  writeStdout(` * MCP Endpoint: ${scheme}://${actualEndpointHost}:${server.port}/mcp`, context);
189
322
  }
190
323
  writeStdout(`======================================================\n`, context);
191
324
  writeStdout(`Server is ready to accept remote requests.`, context);
192
325
  writeStdout(`Press Ctrl+C to terminate.\n`, context);
193
- const stopSignalHandler = () => {
194
- writeStdout("\nStopping ActionDock server...", context);
195
- Promise.resolve(server.stop())
196
- .catch((err) => {
197
- // 停止失败必须可见:写入 stderr 一行并标记失败退出码
198
- writeStderr(`[ERROR] Failed to stop ActionDock server gracefully: ${err instanceof Error ? err.message : String(err)}`, context);
199
- process.exitCode = 1;
200
- })
201
- .finally(() => {
202
- process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
203
- });
204
- };
205
- process.once("SIGINT", stopSignalHandler);
206
- process.once("SIGTERM", stopSignalHandler);
326
+ registerStopSignalHandler(server, `ActionDock server`, context);
207
327
  }
208
328
  catch (err) {
209
- await target.close().catch(() => { });
329
+ await service.close().catch(() => { });
210
330
  throw new ExecutionError(`Failed to start ActionDock server: ${err.message}`, err);
211
331
  }
212
332
  });
@@ -1,10 +1,10 @@
1
- import type { ActionDockTarget } from "@actiondock/core";
1
+ import { type ActionDockService } from "@actiondock/core";
2
2
  import type { CliContext } from "../types.js";
3
3
  /**
4
4
  * 状态键列表渲染参数视图。
5
5
  */
6
6
  export interface StateListRenderArgs {
7
- target: ActionDockTarget;
7
+ service: ActionDockService;
8
8
  actionId: string;
9
9
  prefix: string;
10
10
  options: any;
@@ -1,4 +1,6 @@
1
- import { filterWithFallbackInfo, listLinkedPackages, loadProjectConfig, } from "@actiondock/core";
1
+ import { loadProjectConfig, } from "@actiondock/core";
2
+ import { filterWithFallbackInfo, } from "@actiondock/core/project";
3
+ import { listLinkedPackages, } from "@actiondock/core/registry";
2
4
  import { existsSync } from "node:fs";
3
5
  import { NO_PROJECT_NO_LINKED_MESSAGE } from "../errors.js";
4
6
  import { renderResult, renderStateList } from "../renderer.js";
@@ -6,16 +8,16 @@ import { renderResult, renderStateList } from "../renderer.js";
6
8
  * 渲染单个项目作用域的状态键列表(含 --detail JSON 明细分支)。
7
9
  */
8
10
  export async function renderProjectScopedStateList(args) {
9
- const { target, targetRoot, actionId, prefix, options, effectiveIntent, shouldFallback, context } = args;
11
+ const { service, targetRoot, actionId, prefix, options, effectiveIntent, shouldFallback, context } = args;
10
12
  const projConfig = loadProjectConfig(targetRoot);
11
- const allKeys = await target.listStateKeys(projConfig.id, actionId, {
13
+ const allKeys = (await service.management?.state.list(projConfig.id, actionId, {
12
14
  namespace: options.namespace !== undefined ? options.namespace : null,
13
15
  prefix,
14
- });
16
+ })) ?? [];
15
17
  if (options.detail && options.json) {
16
18
  const entries = [];
17
19
  for (const k of allKeys) {
18
- const entry = await target.getState(projConfig.id, actionId, k, {
20
+ const entry = await service.management?.state.get(projConfig.id, actionId, k, {
19
21
  namespace: options.namespace,
20
22
  detail: true,
21
23
  });
@@ -41,7 +43,7 @@ export async function renderProjectScopedStateList(args) {
41
43
  * 渲染全部链接包聚合的状态键列表(跳过磁盘失效与清单损坏的链接项)。
42
44
  */
43
45
  export async function renderLinkedPackagesStateList(args) {
44
- const { target, actionId, prefix, options, effectiveIntent, shouldFallback, context } = args;
46
+ const { service, actionId, prefix, options, effectiveIntent, shouldFallback, context } = args;
45
47
  // 扫描所有链接的包状态
46
48
  const linked = listLinkedPackages();
47
49
  if (linked.length === 0) {
@@ -58,10 +60,10 @@ export async function renderLinkedPackagesStateList(args) {
58
60
  continue;
59
61
  try {
60
62
  const config = loadProjectConfig(pkg.path);
61
- const keys = await target.listStateKeys(config.id, actionId, {
63
+ const keys = (await service.management?.state.list(config.id, actionId, {
62
64
  namespace: options.namespace !== undefined ? options.namespace : null,
63
65
  prefix,
64
- });
66
+ })) ?? [];
65
67
  for (const k of keys) {
66
68
  aggregatedKeys.push(`${config.id}/${k}`);
67
69
  }
@@ -3,7 +3,7 @@ import type { CliContext } from "../types.js";
3
3
  /**
4
4
  * 注册 state 状态管理命令(get、set、delete、clear、keys、list)。
5
5
  *
6
- * @param program Commander 实例
7
- * @param context 命令行上下文
6
+ * @param program Commander 根程序对象
7
+ * @param context CLI 上下文
8
8
  */
9
9
  export declare function registerStateCommands(program: Command, context?: CliContext): void;