@actiondock/cli 2.8.0 → 2.9.1

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.
package/README.md CHANGED
@@ -108,6 +108,7 @@ ad pack
108
108
 
109
109
  - 严格目标定位:通过 `-P, --package <id|path>` 指定目标包。系统严格区分物理路径与注册表包标识符,若目标不存在则不向当前目录或父级目录隐式回退,直接以参数校验错误退出。
110
110
  - 多目标检索契约:在执行多目标检索(如 `ad info` 或 `ad list`)时,若无任何匹配项,在机器模式下始终返回确定性空数组结构并以状态码 0 退出,不因空搜索产生异常中断。
111
+ - 服务端细粒度白名单:通过 `ad serve` 启动微服务时,支持通过 `-P, --package <package-id>` 与 `-A, --action <action-ref>` 分别指定包白名单与动作白名单(支持短名与全限定名,可多次指定或逗号分隔)。同时配置时取权限交集。未授权动作的详情调阅与执行均被阻断并返回 403 错误(错误码 `ACTION_FORBIDDEN`),历史运行记录与 MCP 协议工具暴露同步过滤。
111
112
 
112
113
  ### Action 执行入参契约
113
114
 
@@ -173,14 +174,14 @@ $data | ConvertTo-Json -Depth 100 | ad run complex-action --input-file -
173
174
  | `ad playbook list` / `show` | 查看智能体操作规程 Playbook |
174
175
  | `ad config list` / `get` / `set` | 管理包运行时配置与环境变量绑定 |
175
176
  | `ad state list` / `get` / `set` / `delete` / `clear` | 查看与维护 SQLite 持久化状态 |
176
- | `ad runs list` / `show` | 查询任务执行历史与追踪记录 |
177
+ | `ad runs list` / `show` / `clear` | 查询任务执行历史与追踪记录,或按时间与数量策略清理历史数据 |
177
178
  | `ad test` | 执行快速单元测试 |
178
179
  | `ad build` | 构建 Node.js 目录交付产物(支持 `--vendor-deps` 与 `--archive`) |
179
180
  | `ad pack` | 打包为标准 npm tarball(`.tgz`),支持 `--dry-run` |
180
181
  | `ad export skill` | 导出 Agent Skill 技能包(支持 `--mode source` 与 `--mode node`) |
181
182
  | `ad link` / `unlink` | 注册或注销工作区全局路由与符号链接 |
182
183
  | `ad profile` | 管理远程执行节点凭证与环境配置 |
183
- | `ad serve` | 启动远程 HTTP/HTTPS 执行调度微服务(原生支持 `--https` 自签名与生产证书) |
184
+ | `ad serve` | 启动远程 HTTP/HTTPS 执行调度微服务(支持 `-P` 包白名单与 `-A` 动作白名单细粒度权限控制,原生支持 `--https` 自签名与生产证书) |
184
185
  | `ad mcp` | 以 STDIO 或 HTTP 协议启动 MCP 服务 |
185
186
 
186
187
  ---
package/bin/ad.js CHANGED
@@ -1,42 +1,71 @@
1
1
  #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
2
+ import { spawn } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
4
  import { createRequire } from "node:module";
5
5
  import { resolve } from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
- const distEntry = resolve(import.meta.dirname, "../dist/index.js");
8
+ const isBun = typeof process.versions.bun !== "undefined";
9
+ const hasTsx =
10
+ process.execArgv.some((arg, i) => arg === "--import" && process.execArgv[i + 1]?.includes("tsx")) ||
11
+ process.execArgv.some((arg) => arg.includes("tsx"));
9
12
 
10
- if (existsSync(distEntry)) {
11
- const { runCliProcess } = await import(pathToFileURL(distEntry).href);
12
- await runCliProcess(process.argv);
13
- } else {
14
- const isBun = typeof process.versions.bun !== "undefined";
15
- const hasTsx =
16
- process.execArgv.some((arg, i) => arg === "--import" && process.execArgv[i + 1]?.includes("tsx")) ||
17
- process.execArgv.some((arg) => arg.includes("tsx"));
18
-
19
- if (!isBun && !hasTsx) {
20
- const require = createRequire(import.meta.url);
21
- let tsxSpecifier = null;
13
+ if (!isBun && !hasTsx) {
14
+ const require = createRequire(import.meta.url);
15
+ let tsxSpecifier = null;
16
+ try {
17
+ tsxSpecifier = pathToFileURL(require.resolve("tsx", { paths: [import.meta.dirname, process.cwd()] })).href;
18
+ } catch {
22
19
  try {
23
20
  tsxSpecifier = pathToFileURL(require.resolve("tsx")).href;
24
21
  } catch {
25
22
  // 未检测到 tsx 模块
26
23
  }
24
+ }
25
+
26
+ if (tsxSpecifier) {
27
+ const child = spawn(
28
+ process.execPath,
29
+ ["--import", tsxSpecifier, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
30
+ {
31
+ stdio: "inherit",
32
+ }
33
+ );
34
+
35
+ const forwardSignal = (sig) => {
36
+ if (child.pid && !child.killed) {
37
+ try {
38
+ child.kill(sig);
39
+ } catch {}
40
+ }
41
+ };
42
+
43
+ process.on("SIGTERM", () => forwardSignal("SIGTERM"));
44
+ process.on("SIGINT", () => forwardSignal("SIGINT"));
45
+ process.on("SIGHUP", () => forwardSignal("SIGHUP"));
27
46
 
28
- if (tsxSpecifier) {
29
- const res = spawnSync(
30
- process.execPath,
31
- ["--import", tsxSpecifier, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
32
- {
33
- stdio: "inherit",
47
+ await new Promise((resolvePromise) => {
48
+ child.on("exit", (code, signal) => {
49
+ if (signal) {
50
+ process.removeListener("SIGTERM", forwardSignal);
51
+ process.removeListener("SIGINT", forwardSignal);
52
+ process.removeListener("SIGHUP", forwardSignal);
53
+ process.kill(process.pid, signal);
54
+ } else {
55
+ process.exit(code ?? 0);
34
56
  }
35
- );
36
- process.exit(res.status ?? (res.signal ? 1 : 0));
37
- }
57
+ resolvePromise();
58
+ });
59
+ });
38
60
  }
61
+ }
62
+
63
+ const distEntry = resolve(import.meta.dirname, "../dist/index.js");
39
64
 
65
+ if (existsSync(distEntry)) {
66
+ const { runCliProcess } = await import(pathToFileURL(distEntry).href);
67
+ await runCliProcess(process.argv);
68
+ } else {
40
69
  const { runCliProcess } = await import("../src/index.ts");
41
70
  await runCliProcess(process.argv);
42
71
  }
@@ -216,6 +216,36 @@ export async function executeAction(id, options, context, flatArgs, control) {
216
216
  throw err;
217
217
  }
218
218
  }
219
+ const RUN_HELP_AFTER_TEXT = `
220
+ Introspection & Guidance:
221
+ If uncertain about an action's inputs or schemas:
222
+ ad describe <id> Inspect action schema, required fields, and assignment examples
223
+ If handling multi-step tasks or complex workflows:
224
+ ad playbook list List available Standard Operating Procedures (SOPs)
225
+ ad playbook show <id> View step-by-step procedure and execution rules
226
+
227
+ Flat Input Syntax & Examples:
228
+ Standard invocation syntax:
229
+ ad run <id> [control-options] [-- <assignments...>]
230
+
231
+ Flat parameter assignments:
232
+ # String assignment (= keeps raw string, no type coercion)
233
+ ad run <id> -- key="value" path="src/index.ts"
234
+
235
+ # Typed / JSON assignment (:= parses numbers, booleans, arrays, objects)
236
+ ad run <id> -- count:=10 enabled:=true timeoutMs:=5000
237
+
238
+ # Sequential array indexing (zero-indexed, consecutive)
239
+ ad run <id> -- paths.0="src" paths.1="docs"
240
+ # Or direct JSON array
241
+ ad run <id> -- paths:='["src", "docs"]'
242
+
243
+ # Nested object properties
244
+ ad run <id> -- user.name="alice" user.age:=30
245
+
246
+ Complex or multiline inputs:
247
+ # Pass via JSON file with --input-file to avoid shell escaping issues
248
+ ad run <id> --input-file input.json`;
219
249
  /**
220
250
  * 挂载 run 子命令至指定 Commander 节点。
221
251
  *
@@ -236,6 +266,7 @@ export function attachRunCommand(parent, context) {
236
266
  .option("--async", "Execute asynchronously in background (requires remote server or profile)")
237
267
  .option("--data-dir <path>", "Custom database directory")
238
268
  .option("--json", "Output as JSON")
269
+ .addHelpText("after", RUN_HELP_AFTER_TEXT)
239
270
  .action(async (id, params, rawOptions, cmd) => {
240
271
  let flatArgs;
241
272
  let effectiveRawOptions = rawOptions;
@@ -1,4 +1,4 @@
1
- import { findProjectRoot, loadProjectConfig, } from "@actiondock/core";
1
+ import { findProjectRoot, loadProjectConfig, parseDuration, } from "@actiondock/core";
2
2
  import { filterWithFallbackInfo, } from "@actiondock/core/project";
3
3
  import { listLinkedPackages, resolvePackageRoot, } from "@actiondock/core/registry";
4
4
  import { ArgumentError, ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE, packageNotFoundError, } from "../errors.js";
@@ -177,7 +177,10 @@ export function registerRunsCommands(program, context) {
177
177
  .command("clear")
178
178
  .description("Clear execution run records")
179
179
  .option("-P, --package <id>", "Target package ID or path")
180
- .option("-a, --action <actionId>", "Filter by action ID"))
180
+ .option("-a, --action <actionId>", "Filter by action ID")
181
+ .option("--status <status>", "Filter by run status (e.g. success, failed)")
182
+ .option("--older-than <duration>", "Only clear runs older than specified duration (e.g. 14d, 7d, 24h, 30m)")
183
+ .option("--keep <count>", "Keep the most recent N runs and clear older ones"))
181
184
  .option("--data-dir <path>", "Custom database storage directory")
182
185
  .option("--json", "Output as JSON")
183
186
  .action(async (rawOptions, cmd) => {
@@ -193,16 +196,49 @@ export function registerRunsCommands(program, context) {
193
196
  // 清单损坏时保留原始 -P 参数作为包标识
194
197
  }
195
198
  }
199
+ let olderThanMs;
200
+ if (options.olderThan) {
201
+ try {
202
+ olderThanMs = parseDuration(options.olderThan);
203
+ }
204
+ catch (err) {
205
+ throw new ArgumentError(`Invalid --older-than argument: ${err.message}`);
206
+ }
207
+ }
208
+ let keep;
209
+ if (options.keep !== undefined) {
210
+ const parsed = parseInt(String(options.keep), 10);
211
+ if (isNaN(parsed) || parsed < 0) {
212
+ throw new ArgumentError("Invalid --keep argument: must be a non-negative integer");
213
+ }
214
+ keep = parsed;
215
+ }
196
216
  await withService(options, context, async (service, resolved) => {
197
217
  const count = service.runs.clear
198
- ? await service.runs.clear({ packageId, actionId: options.action })
218
+ ? await service.runs.clear({
219
+ packageId,
220
+ actionId: options.action,
221
+ status: options.status,
222
+ olderThanMs,
223
+ keep,
224
+ })
199
225
  : 0;
226
+ const filters = [];
227
+ if (options.action)
228
+ filters.push(`action='${options.action}'`);
229
+ if (options.status)
230
+ filters.push(`status='${options.status}'`);
231
+ if (options.olderThan)
232
+ filters.push(`older than ${options.olderThan}`);
233
+ if (keep !== undefined)
234
+ filters.push(`keeping newest ${keep}`);
235
+ const filterDesc = filters.length > 0 ? ` (${filters.join(", ")})` : "";
200
236
  const payload = { ok: true, clearedCount: count };
201
237
  renderResult(payload, {
202
238
  json: options.json,
203
239
  humanFormatter: () => resolved.type === "remote"
204
- ? `Cleared ${count} execution run(s) on remote server.`
205
- : `Cleared ${count} execution run(s) in package '${packageId}'.`,
240
+ ? `Cleared ${count} execution run(s) on remote server${filterDesc}.`
241
+ : `Cleared ${count} execution run(s) in package '${packageId}'${filterDesc}.`,
206
242
  context,
207
243
  });
208
244
  }, { localRoot: targetPackageRoot });
@@ -2,10 +2,11 @@ import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { createActionDock, createNodePlatform, findProjectRoot, loadProjectConfig, startActionDockServer, } from "@actiondock/core";
4
4
  import { resolvePackageRoot } from "@actiondock/core/registry";
5
+ import { parseActionRef } from "@actiondock/core/graph";
5
6
  import { formatHostForUrl, } from "@actiondock/core/server";
6
7
  import { ArgumentError, ExecutionError, packageNotFoundError } from "../errors.js";
7
8
  import { writeStderr, writeStdout } from "../renderer.js";
8
- import { ensureSelfSignedCertificate, getEffectiveOptions, normalizeCorsOrigins, registerStopSignalHandler, printServerBanner, resolveMaxBodyBytes, resolveServerHost, resolveServerPort, resolveServerToken, parseListOption, } from "../utils/index.js";
9
+ import { ensureSelfSignedCertificate, getEffectiveOptions, loadViewsFromFile, mergeServerViews, normalizeCorsOrigins, registerStopSignalHandler, printServerBanner, resolveMaxBodyBytes, resolveServerHost, resolveServerPort, resolveServerToken, parseListOption, } from "../utils/index.js";
9
10
  /**
10
11
  * 注册 serve HTTP 服务启动命令。
11
12
  *
@@ -33,6 +34,9 @@ export function registerServeCommand(program, context) {
33
34
  .option("--tls-passphrase <passphrase>", "Passphrase for TLS private key (or set ACTIONDOCK_TLS_PASSPHRASE)")
34
35
  .option("-d, --dir <path>", "Project root directory (default: current working directory)")
35
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")
36
40
  .option("--data-dir <path>", "Custom database storage directory")
37
41
  .action(async (rawOptions, cmd) => {
38
42
  const options = getEffectiveOptions(rawOptions, cmd);
@@ -87,10 +91,42 @@ export function registerServeCommand(program, context) {
87
91
  const projectRoot = options.dir
88
92
  ? resolve(options.dir)
89
93
  : findProjectRoot(process.cwd());
94
+ let projectConfig;
95
+ let projectName = "Global Registry Mode";
96
+ if (projectRoot) {
97
+ try {
98
+ projectConfig = loadProjectConfig(projectRoot);
99
+ projectName = `${projectConfig.name} (${projectConfig.id})`;
100
+ }
101
+ catch {
102
+ // 降级处理
103
+ }
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);
90
124
  const rawPackages = options.package || [];
91
125
  const packageAllowlist = rawPackages.length > 0 ? [] : undefined;
92
126
  const resolvedPackageIds = [];
93
127
  const explicitPackages = [];
128
+ const rawActions = options.action || [];
129
+ const actionAllowlist = rawActions.length > 0 ? [...rawActions] : undefined;
94
130
  if (rawPackages.length > 0) {
95
131
  for (const pkgId of rawPackages) {
96
132
  const root = resolvePackageRoot(pkgId, projectRoot || undefined, context?.customHome);
@@ -123,14 +159,64 @@ export function registerServeCommand(program, context) {
123
159
  }
124
160
  }
125
161
  }
126
- let projectName = "Global Registry Mode";
127
- if (projectRoot) {
128
- try {
129
- const config = loadProjectConfig(projectRoot);
130
- projectName = `${config.name} (${config.id})`;
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
+ }
131
183
  }
132
- catch {
133
- // 降级处理
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
+ }
134
220
  }
135
221
  }
136
222
  const platform = createNodePlatform({
@@ -145,9 +231,9 @@ export function registerServeCommand(program, context) {
145
231
  customHome: context?.customHome,
146
232
  dataDir: options.dataDir || context?.dataDir,
147
233
  platform,
148
- scanLinkedPackages: !projectRoot || rawPackages.length > 0,
234
+ scanLinkedPackages: !projectRoot || rawPackages.length > 0 || rawActions.length > 0 || Boolean(viewsConfig),
149
235
  });
150
- let mcpHandler;
236
+ let mcpHandlerFactory;
151
237
  const enableMcp = options.mcp !== false;
152
238
  if (enableMcp) {
153
239
  // MCP 处理器创建失败时直接终止启动,避免横幅宣称不存在的端点
@@ -156,19 +242,22 @@ export function registerServeCommand(program, context) {
156
242
  import("@actiondock/mcp"),
157
243
  import("@modelcontextprotocol/server"),
158
244
  ]);
159
- const handler = createMcpHandler(() => {
160
- return createActionDockMcpServer({
161
- service,
162
- packageAllowlist,
163
- packageIds: packageAllowlist,
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
+ },
164
257
  });
165
- }, {
166
- onerror: (err) => {
167
- writeStderr(`[MCP HTTP Error] ${err?.message || String(err)}`, context);
168
- },
169
- });
170
- mcpHandler = async (req) => {
171
- return handler.fetch(req);
258
+ return async (req) => {
259
+ return handler.fetch(req);
260
+ };
172
261
  };
173
262
  }
174
263
  catch (err) {
@@ -188,22 +277,31 @@ export function registerServeCommand(program, context) {
188
277
  exposeDebugInfo,
189
278
  enableMcp,
190
279
  enableManagement,
191
- mcpHandler,
280
+ mcpHandlerFactory,
192
281
  tls,
193
282
  projectRoot: projectRoot || undefined,
194
283
  service,
195
284
  packageAllowlist,
285
+ actionAllowlist,
286
+ views: viewsConfig,
196
287
  });
197
288
  const displayHost = formatHostForUrl(host);
198
289
  const actualEndpointHost = formatHostForUrl(host === "0.0.0.0" ? "127.0.0.1" : host);
199
290
  const scheme = tls ? "https" : "http";
200
- printServerBanner(`ActionDock 2.0 HTTP Runner Server`, scheme, displayHost, server.port, context);
291
+ printServerBanner(`ActionDock 2.0 HTTP Runner Server`, scheme, displayHost, server.port, context, {
292
+ views: viewsConfig,
293
+ endpointHost: actualEndpointHost,
294
+ enableMcp,
295
+ });
201
296
  if (resolvedPackageIds.length > 0) {
202
297
  writeStdout(` * Packages: ${resolvedPackageIds.join(", ")}`, context);
203
298
  }
204
299
  else {
205
300
  writeStdout(` * Project: ${projectName}`, context);
206
301
  }
302
+ if (actionAllowlist && actionAllowlist.length > 0) {
303
+ writeStdout(` * Actions: ${actionAllowlist.join(", ")}`, context);
304
+ }
207
305
  if (projectRoot && exposeDebugInfo) {
208
306
  writeStdout(` * Root Path: ${projectRoot}`, context);
209
307
  }
@@ -1,3 +1,4 @@
1
+ import type { ServerViewOptions } from "@actiondock/core/server";
1
2
  import type { CliContext } from "../types.js";
2
3
  /**
3
4
  * 服务类命令(ad serve、ad mcp serve、ad mcp、ad run)的共享启动脚手架(单一事实源)。
@@ -51,11 +52,63 @@ export declare function registerStopSignalHandler(server: {
51
52
  stop(): Promise<unknown> | unknown;
52
53
  }, serverName: string, context?: CliContext): void;
53
54
  /**
54
- * 打印服务就绪横幅(顶部固定段,serve 与 mcp serve 共享排版)。
55
+ * 虚拟视图简要展示信息。
56
+ */
57
+ export interface ServerViewSummary {
58
+ name: string;
59
+ enableMcp?: boolean;
60
+ }
61
+ /**
62
+ * 打印服务横幅的可选配置参数。
63
+ */
64
+ export interface PrintServerBannerOptions {
65
+ views?: Record<string, ServerViewOptions> | ServerViewOptions[];
66
+ endpointHost?: string;
67
+ enableMcp?: boolean;
68
+ }
69
+ /**
70
+ * 从指定文件路径加载虚拟视图配置字典。
71
+ *
72
+ * @param filePath 视图配置文件路径
73
+ * @returns 解析后的视图字典或数组
74
+ */
75
+ export declare function loadViewsFromFile(filePath: string): Record<string, ServerViewOptions> | ServerViewOptions[];
76
+ /**
77
+ * 合并项目配置与独立配置文件中的虚拟视图定义。
78
+ * 命令行 --views-file 优先级高于项目配置文件 actiondock.json 中的 server.views。
79
+ *
80
+ * @param configViews 项目配置中的 views
81
+ * @param fileViews 独立配置文件中的 views
82
+ */
83
+ export declare function mergeServerViews(configViews?: Record<string, ServerViewOptions> | ServerViewOptions[], fileViews?: Record<string, ServerViewOptions> | ServerViewOptions[]): Record<string, ServerViewOptions> | ServerViewOptions[] | undefined;
84
+ /**
85
+ * 从服务视图配置中提取所有命名空间视图列表(排除 default 默认视图)。
86
+ *
87
+ * @param views 视图字典或数组
88
+ * @returns 命名空间视图简要信息列表
89
+ */
90
+ export declare function extractNamespacedViews(views?: Record<string, ServerViewOptions> | ServerViewOptions[]): ServerViewSummary[];
91
+ /**
92
+ * 打印服务命名空间虚拟视图列表。
93
+ *
94
+ * @param views 视图配置字典或数组
95
+ * @param scheme 访问协议(http 或 https)
96
+ * @param endpointHost 面向调用的端点主机地址
97
+ * @param port 监听端口
98
+ * @param context CLI 上下文
99
+ * @param options 额外选项(如全局 enableMcp)
100
+ */
101
+ export declare function printServerViews(views: Record<string, ServerViewOptions> | ServerViewOptions[] | undefined, scheme: string, endpointHost: string, port: number, context?: CliContext, options?: {
102
+ enableMcp?: boolean;
103
+ }): void;
104
+ /**
105
+ * 打印服务就绪横幅(顶部固定段,支持展示命名空间视图)。
55
106
  *
56
107
  * @param title 横幅主体标题(如 ActionDock 2.0 HTTP Runner Server)
57
108
  * @param scheme 端点协议(http 或 https)
58
109
  * @param displayHost 面向展示的主机地址
59
110
  * @param port 实际监听端口
111
+ * @param contextOrOptions 命令行上下文或横幅配置选项
112
+ * @param maybeOptions 横幅配置选项(包含 views 等信息)
60
113
  */
61
- export declare function printServerBanner(title: string, scheme: string, displayHost: string, port: number, context?: CliContext): void;
114
+ export declare function printServerBanner(title: string, scheme: string, displayHost: string, port: number, contextOrOptions?: CliContext | PrintServerBannerOptions, maybeOptions?: PrintServerBannerOptions): void;
@@ -1,3 +1,5 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
1
3
  import { parseDuration, } from "@actiondock/core/project";
2
4
  import { ArgumentError } from "../errors.js";
3
5
  import { writeStderr, writeStdout } from "../renderer.js";
@@ -99,16 +101,175 @@ export function registerStopSignalHandler(server, serverName, context) {
99
101
  process.once("SIGTERM", stopSignalHandler);
100
102
  }
101
103
  /**
102
- * 打印服务就绪横幅(顶部固定段,serve 与 mcp serve 共享排版)。
104
+ * 从指定文件路径加载虚拟视图配置字典。
105
+ *
106
+ * @param filePath 视图配置文件路径
107
+ * @returns 解析后的视图字典或数组
108
+ */
109
+ export function loadViewsFromFile(filePath) {
110
+ const resolvedPath = resolve(filePath);
111
+ if (!existsSync(resolvedPath)) {
112
+ throw new ArgumentError(`Views file not found: ${resolvedPath}`);
113
+ }
114
+ let parsed;
115
+ try {
116
+ const content = readFileSync(resolvedPath, "utf-8");
117
+ parsed = JSON.parse(content);
118
+ }
119
+ catch (err) {
120
+ const msg = err instanceof Error ? err.message : String(err);
121
+ throw new ArgumentError(`Failed to read views file: ${msg}`);
122
+ }
123
+ if (!parsed || typeof parsed !== "object") {
124
+ throw new ArgumentError("Invalid views file: expected a JSON object or array");
125
+ }
126
+ if (Array.isArray(parsed)) {
127
+ return parsed;
128
+ }
129
+ const obj = parsed;
130
+ if (obj.views && typeof obj.views === "object") {
131
+ return obj.views;
132
+ }
133
+ if (obj.server && typeof obj.server === "object" && obj.server.views) {
134
+ const srv = obj.server;
135
+ return srv.views;
136
+ }
137
+ return parsed;
138
+ }
139
+ /**
140
+ * 合并项目配置与独立配置文件中的虚拟视图定义。
141
+ * 命令行 --views-file 优先级高于项目配置文件 actiondock.json 中的 server.views。
142
+ *
143
+ * @param configViews 项目配置中的 views
144
+ * @param fileViews 独立配置文件中的 views
145
+ */
146
+ export function mergeServerViews(configViews, fileViews) {
147
+ if (!configViews && !fileViews) {
148
+ return undefined;
149
+ }
150
+ if (!configViews) {
151
+ return fileViews;
152
+ }
153
+ if (!fileViews) {
154
+ return configViews;
155
+ }
156
+ if (!Array.isArray(configViews) && !Array.isArray(fileViews)) {
157
+ return { ...configViews, ...fileViews };
158
+ }
159
+ const merged = {};
160
+ const addEntry = (item, fallbackKey) => {
161
+ const key = item.name?.trim() || fallbackKey;
162
+ merged[key] = { ...item, name: key };
163
+ };
164
+ if (Array.isArray(configViews)) {
165
+ configViews.forEach((item, index) => addEntry(item, item.name || `view_${index}`));
166
+ }
167
+ else {
168
+ Object.entries(configViews).forEach(([k, v]) => addEntry(v, k));
169
+ }
170
+ if (Array.isArray(fileViews)) {
171
+ fileViews.forEach((item, index) => addEntry(item, item.name || `view_${index}`));
172
+ }
173
+ else {
174
+ Object.entries(fileViews).forEach(([k, v]) => addEntry(v, k));
175
+ }
176
+ return merged;
177
+ }
178
+ /**
179
+ * 从服务视图配置中提取所有命名空间视图列表(排除 default 默认视图)。
180
+ *
181
+ * @param views 视图字典或数组
182
+ * @returns 命名空间视图简要信息列表
183
+ */
184
+ export function extractNamespacedViews(views) {
185
+ if (!views)
186
+ return [];
187
+ const result = [];
188
+ if (Array.isArray(views)) {
189
+ for (let i = 0; i < views.length; i++) {
190
+ const item = views[i];
191
+ if (!item)
192
+ continue;
193
+ const name = item.name?.trim() || `view_${i}`;
194
+ if (name === "default")
195
+ continue;
196
+ result.push({
197
+ name,
198
+ enableMcp: item.enableMcp,
199
+ });
200
+ }
201
+ }
202
+ else if (typeof views === "object") {
203
+ for (const [key, item] of Object.entries(views)) {
204
+ if (!item)
205
+ continue;
206
+ const name = item.name?.trim() || key.trim();
207
+ if (name === "default")
208
+ continue;
209
+ result.push({
210
+ name,
211
+ enableMcp: item.enableMcp,
212
+ });
213
+ }
214
+ }
215
+ return result;
216
+ }
217
+ /**
218
+ * 打印服务命名空间虚拟视图列表。
219
+ *
220
+ * @param views 视图配置字典或数组
221
+ * @param scheme 访问协议(http 或 https)
222
+ * @param endpointHost 面向调用的端点主机地址
223
+ * @param port 监听端口
224
+ * @param context CLI 上下文
225
+ * @param options 额外选项(如全局 enableMcp)
226
+ */
227
+ export function printServerViews(views, scheme, endpointHost, port, context, options) {
228
+ const namespaced = extractNamespacedViews(views);
229
+ if (namespaced.length === 0) {
230
+ return;
231
+ }
232
+ writeStdout(` * Views:`, context);
233
+ for (const v of namespaced) {
234
+ const encodedName = encodeURIComponent(v.name);
235
+ writeStdout(` - ${v.name}:`, context);
236
+ writeStdout(` - HTTP Root: ${scheme}://${endpointHost}:${port}/views/${encodedName}`, context);
237
+ const mcpEnabled = options?.enableMcp !== false && v.enableMcp !== false;
238
+ const mcpDesc = mcpEnabled
239
+ ? `${scheme}://${endpointHost}:${port}/views/${encodedName}/mcp`
240
+ : "Disabled";
241
+ writeStdout(` - MCP Endpoint: ${mcpDesc}`, context);
242
+ }
243
+ }
244
+ /**
245
+ * 打印服务就绪横幅(顶部固定段,支持展示命名空间视图)。
103
246
  *
104
247
  * @param title 横幅主体标题(如 ActionDock 2.0 HTTP Runner Server)
105
248
  * @param scheme 端点协议(http 或 https)
106
249
  * @param displayHost 面向展示的主机地址
107
250
  * @param port 实际监听端口
251
+ * @param contextOrOptions 命令行上下文或横幅配置选项
252
+ * @param maybeOptions 横幅配置选项(包含 views 等信息)
108
253
  */
109
- export function printServerBanner(title, scheme, displayHost, port, context) {
254
+ export function printServerBanner(title, scheme, displayHost, port, contextOrOptions, maybeOptions) {
255
+ let context;
256
+ let options;
257
+ if (contextOrOptions &&
258
+ ("views" in contextOrOptions || "endpointHost" in contextOrOptions || "enableMcp" in contextOrOptions)) {
259
+ options = contextOrOptions;
260
+ }
261
+ else {
262
+ context = contextOrOptions;
263
+ options = maybeOptions;
264
+ }
110
265
  writeStdout(`\n======================================================`, context);
111
266
  writeStdout(` ${title}`, context);
112
267
  writeStdout(`======================================================`, context);
113
268
  writeStdout(` * Listening on: ${scheme}://${displayHost}:${port}`, context);
269
+ if (options?.views) {
270
+ const endpointHost = options.endpointHost || displayHost;
271
+ printServerViews(options.views, scheme, endpointHost, port, context, {
272
+ enableMcp: options.enableMcp,
273
+ });
274
+ }
114
275
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actiondock/cli",
3
- "version": "2.8.0",
3
+ "version": "2.9.1",
4
4
  "description": "ActionDock CLI facade for developing, testing, building, and exporting standalone Agent Actions and Skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,10 +34,10 @@
34
34
  "test": "node ../../scripts/run-tests.ts"
35
35
  },
36
36
  "dependencies": {
37
- "@actiondock/builder": "^2.8.0",
38
- "@actiondock/core": "^2.8.0",
39
- "@actiondock/mcp": "^2.8.0",
40
- "@actiondock/sdk": "^2.8.0",
37
+ "@actiondock/builder": "^2.9.1",
38
+ "@actiondock/core": "^2.9.1",
39
+ "@actiondock/mcp": "^2.9.1",
40
+ "@actiondock/sdk": "^2.9.1",
41
41
  "commander": "^13.1.0",
42
42
  "selfsigned": "^5.5.0"
43
43
  },