@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,8 +1,7 @@
1
- import { resolve } from "node:path";
2
- import { beginTransaction, findProjectRoot, getInstallCommand, loadLockfile, loadManifest, saveLockfile, saveManifest, } from "@actiondock/core";
3
- import { ArgumentError, ExecutionError, notInProjectError } from "../errors.js";
1
+ import { beginTransaction, getInstallCommand, loadLockfile, saveLockfile, saveManifest, } from "@actiondock/core/project";
2
+ import { ArgumentError, ExecutionError } from "../errors.js";
4
3
  import { renderResult, writeStderr } from "../renderer.js";
5
- import { assertSafePackageSpec, getEffectiveOptions, spawnAsync } from "../utils/index.js";
4
+ import { assertSafePackageSpec, getEffectiveOptions, requireProjectManifestRoot, rollbackAndRethrow, spawnAsync, } from "../utils/index.js";
6
5
  /**
7
6
  * 注册 ad remove 依赖移除命令。
8
7
  * 遵循规范:移除依赖并更新锁文件,保留该包的历史存储数据,检查反向引用防止误删。
@@ -15,14 +14,7 @@ export function registerRemoveCommand(program, context) {
15
14
  .option("--json", "Output as JSON")
16
15
  .action(async (packageIdentifier, rawOptions, cmd) => {
17
16
  const options = getEffectiveOptions(rawOptions, cmd);
18
- const root = options.package ? resolve(options.package) : findProjectRoot();
19
- if (!root) {
20
- throw notInProjectError("Please specify -P, --package <path> or cd into a project directory.");
21
- }
22
- const manifest = loadManifest(root);
23
- if (!manifest) {
24
- throw new ArgumentError(`actiondock.json not found in ${root}`);
25
- }
17
+ const { root, manifest } = requireProjectManifestRoot(options.package);
26
18
  // 寻址逻辑包标识
27
19
  let targetPackageId = null;
28
20
  let npmPackageName = packageIdentifier;
@@ -115,8 +107,7 @@ export function registerRemoveCommand(program, context) {
115
107
  });
116
108
  }
117
109
  catch (err) {
118
- await tx.rollback({ frozenInstall: false });
119
- throw new ExecutionError(err.message, err, err.code || "REMOVE_DEPENDENCY_FAILED");
110
+ await rollbackAndRethrow(tx, err, "REMOVE_DEPENDENCY_FAILED");
120
111
  }
121
112
  });
122
113
  }
@@ -1,3 +1,4 @@
1
+ import { type InvocationControl } from "@actiondock/core/server";
1
2
  import type { ExecutionResult } from "@actiondock/sdk";
2
3
  import { Command } from "commander";
3
4
  import type { CliContext } from "../types.js";
@@ -9,9 +10,12 @@ import type { CliContext } from "../types.js";
9
10
  */
10
11
  export declare function renderRawExecutionResult(targetRef: string, result: ExecutionResult, context?: CliContext): void;
11
12
  /**
12
- * 统一执行 Action 核心逻辑(使用 ActionDockTarget 门面)。
13
+ * 统一执行 Action 核心逻辑(使用 ActionDockService 服务端口)。
14
+ *
15
+ * @param id 目标 Action 标识
16
+ * @param options 执行选项(由 Command 统一构造)
13
17
  */
14
- export declare function executeAction(id: string, options: any, context?: CliContext): Promise<void>;
18
+ export declare function executeAction(id: string, options: any, context?: CliContext, flatArgs?: string[], control?: InvocationControl): Promise<void>;
15
19
  /**
16
20
  * 挂载 run 子命令至指定 Commander 节点。
17
21
  *
@@ -1,7 +1,8 @@
1
- import { parseDuration, resolveTarget } 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
  *
@@ -86,30 +87,38 @@ export function renderRawExecutionResult(targetRef, result, context) {
86
87
  ? result.error.details
87
88
  : JSON.stringify(result.error.details, null, 2), context);
88
89
  }
89
- process.exitCode = 1;
90
+ if (context) {
91
+ context.exitCode = 1;
92
+ }
93
+ if (!context?.control) {
94
+ process.exitCode = 1;
95
+ }
90
96
  }
91
97
  }
92
98
  /**
93
- * 统一执行 Action 核心逻辑(使用 ActionDockTarget 门面)。
99
+ * 统一执行 Action 核心逻辑(使用 ActionDockService 服务端口)。
100
+ *
101
+ * @param id 目标 Action 标识
102
+ * @param options 执行选项(由 Command 统一构造)
94
103
  */
95
- export async function executeAction(id, options, context) {
104
+ export async function executeAction(id, options, context, flatArgs, control) {
96
105
  if (!id) {
97
106
  throw new ArgumentError("Action ID is required for run");
98
107
  }
108
+ const effectiveControl = control ?? context?.control;
109
+ const effectiveSignal = effectiveControl?.signal;
110
+ const effectiveFlatArgs = flatArgs ?? options.flatArgs;
99
111
  const input = await resolveActionInput({
100
112
  input: options.input,
101
113
  inputFile: options.inputFile,
114
+ flatArgs: effectiveFlatArgs && effectiveFlatArgs.length > 0 ? effectiveFlatArgs : undefined,
102
115
  stdin: context?.stdin,
103
116
  });
104
- let timeoutMs;
105
- if (options.timeout) {
106
- try {
107
- timeoutMs = parseDuration(options.timeout);
108
- }
109
- catch (err) {
110
- throw new ArgumentError(`Invalid timeout format: ${err.message}`);
111
- }
117
+ const check = validateActionInputValue(input);
118
+ if (!check.valid) {
119
+ throw mapInputValidationFailure("cli-pre-target", check);
112
120
  }
121
+ const timeoutMs = resolveTimeoutMs(options.timeout);
113
122
  const configOverrides = {};
114
123
  if (options.config) {
115
124
  const list = Array.isArray(options.config) ? options.config : [options.config];
@@ -119,13 +128,6 @@ export async function executeAction(id, options, context) {
119
128
  configOverrides[k] = v.join("=");
120
129
  }
121
130
  }
122
- const controller = new AbortController();
123
- let receivedSigint = false;
124
- const sigintHandler = () => {
125
- receivedSigint = true;
126
- controller.abort(new Error("Interrupted by SIGINT"));
127
- };
128
- process.once("SIGINT", sigintHandler);
129
131
  try {
130
132
  // 异步执行仅支持远端目标,本地模式直接拒绝
131
133
  const resolvedTopology = resolveTarget({
@@ -143,15 +145,15 @@ export async function executeAction(id, options, context) {
143
145
  throw packageNotFoundError(options.package);
144
146
  }
145
147
  let targetRef = id;
146
- if (options.package && !id.includes("/") && !id.includes(":")) {
148
+ if (options.package && !id.includes("/")) {
147
149
  targetRef = `${options.package}/${id}`;
148
150
  }
149
- // 通过 Target 统一执行
150
- await withTarget(options, context, async (target) => {
151
+ // 通过 Service 统一执行
152
+ await withService(options, context, async (service) => {
151
153
  const isMachine = Boolean(options.json);
152
154
  if (options.async) {
153
- const ticket = await target.startAction(targetRef, input, {
154
- signal: controller.signal,
155
+ const ticket = await service.execution.start(targetRef, input, {
156
+ signal: effectiveSignal,
155
157
  timeoutMs,
156
158
  config: configOverrides,
157
159
  requestId: options.requestId,
@@ -169,17 +171,23 @@ export async function executeAction(id, options, context) {
169
171
  writeStdout(ticket.runId, context);
170
172
  }
171
173
  if (ticket.status === "failed") {
172
- process.exitCode = 1;
174
+ if (context) {
175
+ context.exitCode = 1;
176
+ }
173
177
  return;
174
178
  }
175
179
  }
176
180
  else {
177
- const result = await target.runAction(targetRef, input, {
178
- signal: controller.signal,
181
+ const result = await service.execution.run(targetRef, input, {
182
+ signal: effectiveSignal,
179
183
  timeoutMs,
180
184
  config: configOverrides,
181
185
  requestId: options.requestId,
182
186
  });
187
+ if (effectiveControl?.cancellationSource === "sigint" &&
188
+ (effectiveSignal?.aborted || (!result.ok && result.error?.code === "ACTION_CANCELLED"))) {
189
+ throw new SigintError();
190
+ }
183
191
  if (isMachine) {
184
192
  writeStdout(JSON.stringify(result, null, 2), context);
185
193
  }
@@ -187,21 +195,26 @@ export async function executeAction(id, options, context) {
187
195
  renderRawExecutionResult(targetRef, result, context);
188
196
  }
189
197
  if (!result.ok) {
190
- process.exitCode = 1;
198
+ if (context) {
199
+ context.exitCode = 1;
200
+ }
191
201
  return;
192
202
  }
193
203
  }
194
204
  }, { localRoot: targetPackageRoot || undefined, scanLinkedPackages: true, ownDataDir: true });
195
205
  }
196
206
  catch (err) {
197
- if (receivedSigint || err?.name === "AbortError" || err?.message?.includes("SIGINT")) {
207
+ // sigint 取消源、中断链路特征错误码或 SigintError 本身,均统一归并为 SigintError
208
+ // 原 if 中额外的「cancellationSource 为 sigint 且 AbortError/aborted/ACTION_CANCELLED」
209
+ // 子句被首项 cancellationSource === "sigint" 完全蕴含,等价化简后移除
210
+ const isSigint = effectiveControl?.cancellationSource === "sigint" ||
211
+ err?.code === "SIGINT_INTERRUPTED" ||
212
+ err instanceof SigintError;
213
+ if (isSigint) {
198
214
  throw new SigintError();
199
215
  }
200
216
  throw err;
201
217
  }
202
- finally {
203
- process.removeListener("SIGINT", sigintHandler);
204
- }
205
218
  }
206
219
  /**
207
220
  * 挂载 run 子命令至指定 Commander 节点。
@@ -211,7 +224,7 @@ export async function executeAction(id, options, context) {
211
224
  */
212
225
  export function attachRunCommand(parent, context) {
213
226
  const cmd = parent
214
- .command("run <id>")
227
+ .command("run <id> [params...]")
215
228
  .description("Execute an action (from current project, linked packages, or remote profile)")
216
229
  .option("-P, --package <id>", "Target package ID or path")
217
230
  .option("-i, --input <json>", "Action input as inline JSON")
@@ -223,9 +236,19 @@ export function attachRunCommand(parent, context) {
223
236
  .option("--async", "Execute asynchronously in background (requires remote server or profile)")
224
237
  .option("--data-dir <path>", "Custom database directory")
225
238
  .option("--json", "Output as JSON")
226
- .action(async (id, rawOptions, cmd) => {
227
- const options = getEffectiveOptions(rawOptions, cmd);
228
- await executeAction(id, options, context);
239
+ .action(async (id, params, rawOptions, cmd) => {
240
+ let flatArgs;
241
+ let effectiveRawOptions = rawOptions;
242
+ let effectiveCmd = cmd;
243
+ if (Array.isArray(params)) {
244
+ flatArgs = params.length > 0 ? params : undefined;
245
+ }
246
+ else {
247
+ effectiveCmd = rawOptions;
248
+ effectiveRawOptions = params;
249
+ }
250
+ const options = getEffectiveOptions(effectiveRawOptions, effectiveCmd);
251
+ await executeAction(id, { ...options, flatArgs }, context, undefined, context?.control);
229
252
  });
230
253
  }
231
254
  /**
@@ -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,11 @@
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 { formatHostForUrl, } from "@actiondock/core/server";
6
+ import { ArgumentError, ExecutionError, packageNotFoundError } from "../errors.js";
6
7
  import { writeStderr, writeStdout } from "../renderer.js";
7
- import { ensureSelfSignedCertificate, getEffectiveOptions, parseByteSize } from "../utils/index.js";
8
+ import { ensureSelfSignedCertificate, getEffectiveOptions, normalizeCorsOrigins, registerStopSignalHandler, printServerBanner, resolveMaxBodyBytes, resolveServerHost, resolveServerPort, resolveServerToken, parseListOption, } from "../utils/index.js";
8
9
  /**
9
10
  * 注册 serve HTTP 服务启动命令。
10
11
  *
@@ -31,17 +32,17 @@ export function registerServeCommand(program, context) {
31
32
  .option("--tls-ca <path>", "Path to TLS CA certificate file (or set ACTIONDOCK_TLS_CA)")
32
33
  .option("--tls-passphrase <passphrase>", "Passphrase for TLS private key (or set ACTIONDOCK_TLS_PASSPHRASE)")
33
34
  .option("-d, --dir <path>", "Project root directory (default: current working directory)")
35
+ .option("-P, --package <package-id>", "Specific package ID(s) to serve (can be specified multiple times or comma-separated)", parseListOption, [])
34
36
  .option("--data-dir <path>", "Custom database storage directory")
35
37
  .action(async (rawOptions, cmd) => {
36
38
  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);
39
+ const port = resolveServerPort(options.port, 5177);
40
+ const host = resolveServerHost(options.host);
41
+ const token = resolveServerToken(options.token);
41
42
  const allowInsecureNoAuth = Boolean(options.allowInsecureNoAuth);
42
43
  const allowQueryToken = Boolean(options.allowQueryToken);
43
44
  const enableManagement = Boolean(options.management);
44
- const corsOrigins = options.corsOrigin && options.corsOrigin.length > 0 ? options.corsOrigin : undefined;
45
+ const corsOrigins = normalizeCorsOrigins(options.corsOrigin);
45
46
  const exposeDebugInfo = Boolean(options.exposeDebugInfo);
46
47
  const httpsEnabled = Boolean(options.https ||
47
48
  options.tlsCert ||
@@ -82,18 +83,46 @@ export function registerServeCommand(program, context) {
82
83
  };
83
84
  }
84
85
  }
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
- }
86
+ const maxBodyBytes = resolveMaxBodyBytes(options.maxBody);
94
87
  const projectRoot = options.dir
95
88
  ? resolve(options.dir)
96
89
  : findProjectRoot(process.cwd());
90
+ const rawPackages = options.package || [];
91
+ const packageAllowlist = rawPackages.length > 0 ? [] : undefined;
92
+ const resolvedPackageIds = [];
93
+ const explicitPackages = [];
94
+ if (rawPackages.length > 0) {
95
+ for (const pkgId of rawPackages) {
96
+ const root = resolvePackageRoot(pkgId, projectRoot || undefined, context?.customHome);
97
+ if (!root) {
98
+ throw packageNotFoundError(pkgId);
99
+ }
100
+ if (!projectRoot || root !== projectRoot) {
101
+ if (!explicitPackages.some((p) => p.packageRoot === root)) {
102
+ explicitPackages.push({ packageRoot: root });
103
+ }
104
+ }
105
+ let resolvedId = pkgId;
106
+ try {
107
+ const config = loadProjectConfig(root);
108
+ if (config.id) {
109
+ resolvedId = config.id;
110
+ if (!packageAllowlist.includes(config.id)) {
111
+ packageAllowlist.push(config.id);
112
+ }
113
+ }
114
+ }
115
+ catch {
116
+ // 降级使用原始标识符
117
+ }
118
+ if (!packageAllowlist.includes(pkgId)) {
119
+ packageAllowlist.push(pkgId);
120
+ }
121
+ if (!resolvedPackageIds.includes(resolvedId)) {
122
+ resolvedPackageIds.push(resolvedId);
123
+ }
124
+ }
125
+ }
97
126
  let projectName = "Global Registry Mode";
98
127
  if (projectRoot) {
99
128
  try {
@@ -109,13 +138,14 @@ export function registerServeCommand(program, context) {
109
138
  dataDir: options.dataDir || context?.dataDir,
110
139
  rootDir: projectRoot || undefined,
111
140
  });
112
- const target = await createActionDockTarget({
141
+ const service = await createActionDock({
113
142
  type: "local",
114
143
  projectRoot: projectRoot || undefined,
144
+ packages: explicitPackages.length > 0 ? explicitPackages : undefined,
115
145
  customHome: context?.customHome,
116
146
  dataDir: options.dataDir || context?.dataDir,
117
147
  platform,
118
- scanLinkedPackages: !projectRoot,
148
+ scanLinkedPackages: !projectRoot || rawPackages.length > 0,
119
149
  });
120
150
  let mcpHandler;
121
151
  const enableMcp = options.mcp !== false;
@@ -128,7 +158,9 @@ export function registerServeCommand(program, context) {
128
158
  ]);
129
159
  const handler = createMcpHandler(() => {
130
160
  return createActionDockMcpServer({
131
- target,
161
+ service,
162
+ packageAllowlist,
163
+ packageIds: packageAllowlist,
132
164
  });
133
165
  }, {
134
166
  onerror: (err) => {
@@ -140,7 +172,7 @@ export function registerServeCommand(program, context) {
140
172
  };
141
173
  }
142
174
  catch (err) {
143
- await target.close().catch(() => { });
175
+ await service.close().catch(() => { });
144
176
  throw new ExecutionError(`Failed to initialize MCP endpoint: ${err?.message || String(err)}`, err);
145
177
  }
146
178
  }
@@ -159,16 +191,19 @@ export function registerServeCommand(program, context) {
159
191
  mcpHandler,
160
192
  tls,
161
193
  projectRoot: projectRoot || undefined,
162
- target,
194
+ service,
195
+ packageAllowlist,
163
196
  });
164
197
  const displayHost = formatHostForUrl(host);
165
198
  const actualEndpointHost = formatHostForUrl(host === "0.0.0.0" ? "127.0.0.1" : host);
166
199
  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);
200
+ printServerBanner(`ActionDock 2.0 HTTP Runner Server`, scheme, displayHost, server.port, context);
201
+ if (resolvedPackageIds.length > 0) {
202
+ writeStdout(` * Packages: ${resolvedPackageIds.join(", ")}`, context);
203
+ }
204
+ else {
205
+ writeStdout(` * Project: ${projectName}`, context);
206
+ }
172
207
  if (projectRoot && exposeDebugInfo) {
173
208
  writeStdout(` * Root Path: ${projectRoot}`, context);
174
209
  }
@@ -183,30 +218,17 @@ export function registerServeCommand(program, context) {
183
218
  writeStdout(` * Management APIs: ${enableManagement ? "Enabled" : "Disabled (Default)"}`, context);
184
219
  writeStdout(` * CORS Origins: ${corsOrigins ? corsOrigins.join(", ") : "Disabled (Default)"}`, context);
185
220
  writeStdout(` * Max Body Size: ${options.maxBody || "1mb"}`, context);
186
- writeStdout(` * Health Endpoint: ${scheme}://${actualEndpointHost}:${server.port}/api/v1/health`, context);
221
+ writeStdout(` * Health Endpoint: ${scheme}://${actualEndpointHost}:${server.port}/api/v2/health`, context);
187
222
  if (enableMcp) {
188
223
  writeStdout(` * MCP Endpoint: ${scheme}://${actualEndpointHost}:${server.port}/mcp`, context);
189
224
  }
190
225
  writeStdout(`======================================================\n`, context);
191
226
  writeStdout(`Server is ready to accept remote requests.`, context);
192
227
  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);
228
+ registerStopSignalHandler(server, `ActionDock server`, context);
207
229
  }
208
230
  catch (err) {
209
- await target.close().catch(() => { });
231
+ await service.close().catch(() => { });
210
232
  throw new ExecutionError(`Failed to start ActionDock server: ${err.message}`, err);
211
233
  }
212
234
  });
@@ -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;