@actiondock/cli 2.5.0 → 2.6.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 (51) hide show
  1. package/README.md +44 -3
  2. package/dist/commands/action/create.js +2 -2
  3. package/dist/commands/add.js +5 -5
  4. package/dist/commands/build.js +4 -5
  5. package/dist/commands/config/delete.js +2 -2
  6. package/dist/commands/config/env.js +6 -15
  7. package/dist/commands/config/get.js +8 -39
  8. package/dist/commands/config/list.js +8 -55
  9. package/dist/commands/config/schema.js +4 -6
  10. package/dist/commands/config/set.js +2 -2
  11. package/dist/commands/describe.js +2 -4
  12. package/dist/commands/doctor.js +3 -10
  13. package/dist/commands/export.js +4 -7
  14. package/dist/commands/generate.js +2 -4
  15. package/dist/commands/index.js +2 -5
  16. package/dist/commands/info.js +7 -15
  17. package/dist/commands/list.js +3 -5
  18. package/dist/commands/mcp.js +23 -3
  19. package/dist/commands/pack.js +4 -5
  20. package/dist/commands/playbook.js +14 -36
  21. package/dist/commands/profile.js +4 -11
  22. package/dist/commands/remove.js +13 -9
  23. package/dist/commands/run.d.ts +8 -0
  24. package/dist/commands/run.js +118 -33
  25. package/dist/commands/runs.js +10 -25
  26. package/dist/commands/serve.js +31 -4
  27. package/dist/commands/state-list.js +2 -5
  28. package/dist/commands/state.js +29 -16
  29. package/dist/commands/test.js +6 -2
  30. package/dist/commands/validate.js +3 -5
  31. package/dist/errors.d.ts +14 -1
  32. package/dist/errors.js +20 -2
  33. package/dist/index.js +1 -2
  34. package/dist/renderer.d.ts +0 -2
  35. package/dist/renderer.js +3 -15
  36. package/dist/services/config-merge.d.ts +41 -0
  37. package/dist/services/config-merge.js +93 -0
  38. package/dist/services/index.d.ts +1 -0
  39. package/dist/services/index.js +1 -0
  40. package/dist/types.d.ts +0 -2
  41. package/dist/utils/index.d.ts +1 -0
  42. package/dist/utils/index.js +1 -0
  43. package/dist/utils/input.d.ts +46 -0
  44. package/dist/utils/input.js +89 -0
  45. package/dist/utils/spawn.d.ts +13 -0
  46. package/dist/utils/spawn.js +31 -0
  47. package/dist/utils/target.d.ts +21 -0
  48. package/dist/utils/target.js +29 -14
  49. package/dist/utils/utils.d.ts +2 -3
  50. package/dist/utils/utils.js +6 -6
  51. package/package.json +6 -6
package/README.md CHANGED
@@ -51,7 +51,14 @@ ad add @actiondock/example-tools
51
51
 
52
52
  - 本地执行 Action:
53
53
  ```bash
54
+ # 简单参数直接内联传递
54
55
  ad run sample.greet --input '{"name": "Alice"}'
56
+
57
+ # 复杂参数或对象从文件读取
58
+ ad run sample.greet --input-file input.json
59
+
60
+ # 自动化与脚本通过标准输入传递
61
+ cat input.json | ad run sample.greet --input-file -
55
62
  ```
56
63
 
57
64
  - 运行单元测试:
@@ -92,14 +99,48 @@ ad pack
92
99
  - `-v, -V, --version`:打印版本号并退出。
93
100
  - `-h, --help`:打印命令帮助并退出。
94
101
  - `--json`:以标准 JSON 格式输出结果。
95
- - `--envelope`:将输出包装为标准信封结构对象(包含 `ok: true, data: T` 或 `ok: false, error: { code, message, details }`)。
96
- - `--data-dir <path>`:指定自定义数据存储目录,实现多测试或多任务数据隔离。
102
+ - `--data-dir <path>`:指定运行时数据库存储目录(包级 `runtime.db`、全局 `global.db` 与目录排他锁),实现多测试或多任务运行数据隔离。注意:本选项仅隔离 SQLite 数据文件,环境配置(profile)与全局注册表(registry)属于用户根目录资产,不受 `--data-dir` 影响。
103
+ - `ACTIONDOCK_HOME=<path>`(环境变量):指定 ActionDock 用户根目录基准(默认对应操作系统的用户主目录)。如需隔离环境配置 `profiles.json`、全局注册表 `registry.json` 或本地证书,请通过设置此环境变量实现沙箱与自动化测试隔离。
97
104
 
98
105
  ### 严格目标解析机制
99
106
 
100
107
  - 严格目标定位:通过 `-P, --package <id|path>` 指定目标包。系统严格区分物理路径与注册表包标识符,若目标不存在则不向当前目录或父级目录隐式回退,直接以参数校验错误退出。
101
108
  - 多目标检索契约:在执行多目标检索(如 `ad info` 或 `ad list`)时,若无任何匹配项,在机器模式下始终返回确定性空数组结构并以状态码 0 退出,不因空搜索产生异常中断。
102
109
 
110
+ ### Action 执行入参契约
111
+
112
+ 执行 Action 支持两种互斥的传参方式:
113
+
114
+ - 简单内联参数:使用 `--input <json>` 直接解析 JSON 字符串,适合简易标量入参。
115
+ - 复杂对象文件:使用 `--input-file <path>` 读取文件并解析 JSON,避免各类终端的引号转义损坏。
116
+ - 标准输入管道:使用 `--input-file -` 从标准输入读取全部数据并解析 JSON,适合跨进程协同与 CI 自动化脚本。
117
+ - 默认无输入:未指定 `--input` 与 `--input-file` 时,入参默认提供 `{}`。
118
+ - 严格互斥:`--input` 与 `--input-file` 互斥,同时提供时直接报错并退出。
119
+ - 统一解析:无论内联参数、文件还是标准输入,解析前均自动剔除 UTF-8 BOM 标记,且不设人为大小上限。
120
+
121
+ ### Windows 与多终端传参建议
122
+
123
+ 针对 Windows PowerShell、cmd 以及各终端中复杂 JSON 容易遇到的双引号转义问题,推荐按以下规范传参:
124
+
125
+ - 简单入参:使用 `--input`,如 `ad run greet --input '{"name":"Alice"}'`。
126
+ - 复杂结构:推荐先保存为 JSON 文件并使用 `--input-file`,如 `ad run complex-action --input-file input.json`。
127
+ - 动态生成输入:通过管道输出配合 `--input-file -` 传递,在 PowerShell 中可执行:
128
+
129
+ ```powershell
130
+ $data = @{
131
+ name = "Alice"
132
+ options = @{
133
+ lang = "zh-CN"
134
+ }
135
+ }
136
+ $data | ConvertTo-Json -Depth 100 | ad run complex-action --input-file -
137
+ ```
138
+
139
+ ### Action 输出模式与机器模式
140
+
141
+ - **默认原始纯文本输出**:`ad run` 默认采用面向终端人类阅读与 LLM Agent 上下文消费的原始输出模式。直接将执行结果正文内容(如文件 `content`、`text`、`message` 或标量字符串)输出至 stdout(保留真实换行且无 JSON 转义),相关元数据(如文件路径、行号范围、截断标记)独立输出至 stderr,符合 Unix 管道安全原则。执行失败时在 stderr 输出错误详情并以退出码 1 退出。
142
+ - **机器 JSON 格式 (`--json`)**:当需要以程序化方式消费、获取完整结构化数据或被外部系统集成时,传入 `--json`。此时输出标准 JSON 结果(包含 `ok`、`runId`、`data` 或 `error`)。
143
+
103
144
  ---
104
145
 
105
146
  ## 常用命令速查
@@ -112,7 +153,7 @@ ad pack
112
153
  | `ad info [patterns...]` | 检索包元数据与能力清单,支持模式匹配与树形展示 |
113
154
  | `ad list [patterns...]` | 列出包内所有已注册的 Action |
114
155
  | `ad describe <id>` | 查看 Action 的详情、参数与模式规范(别名 `ad show`) |
115
- | `ad run <id>` | 本地或远程执行指定 Action 并输出标准信封结果 |
156
+ | `ad run <id>` | 本地或远程执行指定 Action(默认输出原始纯文本,支持 `--json` 输出标准信封) |
116
157
  | `ad validate [id]` | 校验 Action 规范与模式规范 |
117
158
  | `ad doctor` | 执行运行环境与项目结构健康诊断 |
118
159
  | `ad action create <id>` | 创建新 Action 源码(别名 `ad action new`) |
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { assertPathWithinRoot, findProjectRoot, getPackageSlug, loadManifest, loadProjectConfig, saveManifest, writeActionTypes, } from "@actiondock/core";
4
- import { ExecutionError } from "../../errors.js";
4
+ import { ExecutionError, notInProjectError } from "../../errors.js";
5
5
  import { writeStdout } from "../../renderer.js";
6
6
  import { attachDescribeCommand } from "../describe.js";
7
7
  import { attachListCommand } from "../list.js";
@@ -89,7 +89,7 @@ export function registerActionCommands(program, context) {
89
89
  export async function handleActionCreate(id, options, context) {
90
90
  const root = findProjectRoot();
91
91
  if (!root) {
92
- throw new ExecutionError("Not in an ActionDock project (actiondock.json not found)");
92
+ throw notInProjectError();
93
93
  }
94
94
  try {
95
95
  const config = loadProjectConfig(root);
@@ -1,9 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { ActionPackageResolver, beginTransaction, computeManifestDigest, findProjectRoot, getInstallCommand, loadLockfile, loadManifest, MANIFEST_FILE_NAME, parseJsonWithoutDuplicates, saveLockfile, saveManifest, } from "@actiondock/core";
4
- import { ArgumentError, ExecutionError } from "../errors.js";
4
+ import { ArgumentError, ExecutionError, notInProjectError } from "../errors.js";
5
5
  import { renderResult } from "../renderer.js";
6
- import { getEffectiveOptions, spawnAsync } from "../utils/index.js";
6
+ import { assertSafePackageSpec, getEffectiveOptions, spawnAsync } from "../utils/index.js";
7
7
  /**
8
8
  * 从安装说明符中提取基础 npm 包名(去除版本号及前缀范围)。
9
9
  */
@@ -34,17 +34,18 @@ export function registerAddCommand(program, context) {
34
34
  .option("-D, --dev", "Install package as development dependency")
35
35
  .option("-P, --package <path>", "Target project directory path")
36
36
  .option("--json", "Output as JSON")
37
- .option("--envelope", "Wrap JSON output in standard envelope")
38
37
  .action(async (packageSpec, rawOptions, cmd) => {
39
38
  const options = getEffectiveOptions(rawOptions, cmd);
40
39
  const root = options.package ? resolve(options.package) : findProjectRoot();
41
40
  if (!root) {
42
- throw new ArgumentError("Not in an ActionDock project (actiondock.json not found).\nPlease specify -P, --package <path> or cd into a project directory.");
41
+ throw notInProjectError("Please specify -P, --package <path> or cd into a project directory.");
43
42
  }
44
43
  const manifest = loadManifest(root);
45
44
  if (!manifest) {
46
45
  throw new ArgumentError(`actiondock.json not found in ${root}`);
47
46
  }
47
+ // 包说明符白名单前置校验:拒绝 shell 元字符,消除 win32 shell 拼接注入面
48
+ assertSafePackageSpec(packageSpec);
48
49
  const npmPackageName = extractNpmPackageName(packageSpec);
49
50
  // 获取排他修改锁并创建事务快照
50
51
  const tx = await beginTransaction(root, `ad add ${packageSpec}`);
@@ -131,7 +132,6 @@ export function registerAddCommand(program, context) {
131
132
  };
132
133
  renderResult(resultPayload, {
133
134
  json: options.json,
134
- envelope: options.envelope,
135
135
  humanFormatter: () => `Successfully added and locked '${resultPayload.packageId}' (v${resultPayload.version}) from ${resultPayload.npmPackage}.`,
136
136
  context,
137
137
  });
@@ -1,5 +1,5 @@
1
1
  import { findProjectRoot, resolvePackageRoot } from "@actiondock/core";
2
- import { ExecutionError } from "../errors.js";
2
+ import { ExecutionError, notInProjectError, packageNotFoundError } from "../errors.js";
3
3
  import { renderResult, writeStdout } from "../renderer.js";
4
4
  import { getEffectiveOptions, parseListOption } from "../utils/index.js";
5
5
  export function registerBuildCommand(program, context) {
@@ -27,11 +27,11 @@ export function registerBuildCommand(program, context) {
27
27
  : findProjectRoot();
28
28
  if (!root) {
29
29
  if (options.package) {
30
- throw new ExecutionError(`Package '${options.package}' not found in linked packages or path`);
30
+ throw packageNotFoundError(options.package);
31
31
  }
32
- throw new ExecutionError("Not in an ActionDock project (actiondock.json not found).\nPlease specify -P, --package <id> or cd into a project directory.");
32
+ throw notInProjectError("Please specify -P, --package <id> or cd into a project directory.");
33
33
  }
34
- const isMachine = Boolean(options.json || options.envelope);
34
+ const isMachine = Boolean(options.json);
35
35
  if (!isMachine) {
36
36
  writeStdout("Building Node.js delivery artifact...");
37
37
  }
@@ -57,7 +57,6 @@ export function registerBuildCommand(program, context) {
57
57
  }
58
58
  renderResult(result, {
59
59
  json: isMachine,
60
- envelope: options.envelope,
61
60
  humanFormatter: () => {
62
61
  const lines = [];
63
62
  lines.push(`[OK] Successfully built ${result.packageId} (v${result.version})`);
@@ -1,5 +1,5 @@
1
1
  import { loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
2
- import { ArgumentError } from "../../errors.js";
2
+ import { ArgumentError, packageNotFoundError } from "../../errors.js";
3
3
  import { writeStdout } from "../../renderer.js";
4
4
  import { applyTargetOptions, getEffectiveOptions, withTarget } from "../../utils/index.js";
5
5
  /**
@@ -35,7 +35,7 @@ export function registerConfigDeleteCommand(configCmd, context) {
35
35
  }
36
36
  if (!root) {
37
37
  if (options.package) {
38
- throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
38
+ throw packageNotFoundError(options.package);
39
39
  }
40
40
  await target.deleteConfig("global", key);
41
41
  writeStdout(`[OK] Global configuration '${key}' deleted`, context);
@@ -1,7 +1,7 @@
1
- import { fetchRemoteConfigEnv, isSecretConfigKey, loadProjectConfig, resolveEnvValue, resolvePackageRoot, resolveTarget, } from "@actiondock/core";
2
- import { ArgumentError } from "../../errors.js";
1
+ import { fetchRemoteConfigEnv, isSecretConfigKey, loadProjectConfig, resolveEnvValue, resolvePackageRoot, } from "@actiondock/core";
2
+ import { notInProjectError, packageNotFoundError } from "../../errors.js";
3
3
  import { renderConfigEnv, renderResult } from "../../renderer.js";
4
- import { applyTargetOptions, getEffectiveOptions } from "../../utils/index.js";
4
+ import { applyTargetOptions, getEffectiveOptions, resolveTargetFromOptions } from "../../utils/index.js";
5
5
  /**
6
6
  * 注册 config env 子命令:诊断环境变量对声明配置的满足率。
7
7
  *
@@ -14,18 +14,11 @@ export function registerConfigEnvCommand(configCmd, context) {
14
14
  .description("Diagnose environment variable satisfaction for declared configuration")
15
15
  .option("-P, --package <id>", "Target package ID or path"))
16
16
  .option("--json", "Output as JSON")
17
- .option("--envelope", "Wrap JSON output in standard envelope")
18
17
  .action(async (identifier, rawOptions, cmd) => {
19
18
  const options = getEffectiveOptions(rawOptions, cmd);
20
19
  const targetPkg = identifier || options.package;
21
20
  // 远端服务分支直接查询远端配置环境诊断接口
22
- const target = resolveTarget({
23
- profile: options.profile,
24
- server: options.server,
25
- token: options.token,
26
- insecure: options.insecure,
27
- allowInsecureHttp: options.allowInsecureHttp,
28
- }, context?.customHome);
21
+ const target = resolveTargetFromOptions(options, context);
29
22
  if (target.type === "remote") {
30
23
  const res = await fetchRemoteConfigEnv(target.serverUrl, target.token, targetPkg, {
31
24
  allowInsecureHttp: Boolean(options.allowInsecureHttp),
@@ -33,7 +26,6 @@ export function registerConfigEnvCommand(configCmd, context) {
33
26
  });
34
27
  renderResult(res, {
35
28
  json: options.json,
36
- envelope: options.envelope,
37
29
  humanFormatter: () => renderConfigEnv(res.envChecks || [], res.packageId),
38
30
  context,
39
31
  });
@@ -47,9 +39,9 @@ export function registerConfigEnvCommand(configCmd, context) {
47
39
  const root = resolvePackageRoot(targetPkg);
48
40
  if (!root) {
49
41
  if (targetPkg) {
50
- throw new ArgumentError(`Package '${targetPkg}' not found in linked packages or path`);
42
+ throw packageNotFoundError(targetPkg);
51
43
  }
52
- throw new ArgumentError("Not in an ActionDock project.\nUsage: ad config env [package-id] or cd into a project directory.");
44
+ throw notInProjectError("Usage: ad config env [package-id] or cd into a project directory.");
53
45
  }
54
46
  const projConfig = loadProjectConfig(root);
55
47
  const declared = projConfig.config || {};
@@ -76,7 +68,6 @@ export function registerConfigEnvCommand(configCmd, context) {
76
68
  };
77
69
  renderResult(payload, {
78
70
  json: options.json,
79
- envelope: options.envelope,
80
71
  humanFormatter: () => renderConfigEnv(envChecks, projConfig.id),
81
72
  context,
82
73
  });
@@ -1,19 +1,8 @@
1
- import { fetchRemoteConfig, isSecretConfigKey, loadProjectConfig, maskSecretValue, resolveEnvValue, resolvePackageRoot, resolveTarget, } from "@actiondock/core";
2
- import { ArgumentError } from "../../errors.js";
1
+ import { fetchRemoteConfig, isSecretConfigKey, loadProjectConfig, maskSecretValue, resolvePackageRoot, } from "@actiondock/core";
2
+ import { ArgumentError, packageNotFoundError } from "../../errors.js";
3
3
  import { renderResult } from "../../renderer.js";
4
- import { applyTargetOptions, getEffectiveOptions, withTarget } from "../../utils/index.js";
5
- /**
6
- * 解析远端目标拓扑信息(仅信息解析,不创建 Target 实例)。
7
- */
8
- function resolveRemoteTargetInfo(options, context) {
9
- return resolveTarget({
10
- profile: options.profile,
11
- server: options.server,
12
- token: options.token,
13
- insecure: options.insecure,
14
- allowInsecureHttp: options.allowInsecureHttp,
15
- }, context?.customHome);
16
- }
4
+ import { resolveMergedConfigEntry } from "../../services/config-merge.js";
5
+ import { applyTargetOptions, getEffectiveOptions, resolveTargetFromOptions, withTarget, } from "../../utils/index.js";
17
6
  /**
18
7
  * 注册 config get 子命令:查询指定配置键的生效值。
19
8
  *
@@ -29,7 +18,6 @@ export function registerConfigGetCommand(configCmd, context) {
29
18
  .option("--reveal, --show-secrets", "Reveal plain text values for secrets")
30
19
  .option("--data-dir <path>", "Custom database storage directory")
31
20
  .option("--json", "Output as JSON")
32
- .option("--envelope", "Wrap JSON output in standard envelope")
33
21
  .action(async (key, rawOptions, cmd) => {
34
22
  const options = getEffectiveOptions(rawOptions, cmd);
35
23
  if (!key) {
@@ -37,7 +25,7 @@ export function registerConfigGetCommand(configCmd, context) {
37
25
  }
38
26
  const reveal = Boolean(options.reveal || options.showSecrets);
39
27
  // 远端服务分支不经过 Target 门面,直接查询远端配置接口
40
- const target = resolveRemoteTargetInfo(options, context);
28
+ const target = resolveTargetFromOptions(options, context);
41
29
  if (target.type === "remote") {
42
30
  const res = await fetchRemoteConfig(target.serverUrl, target.token, options.package, {
43
31
  allowInsecureHttp: Boolean(options.allowInsecureHttp),
@@ -54,7 +42,6 @@ export function registerConfigGetCommand(configCmd, context) {
54
42
  };
55
43
  renderResult(payload, {
56
44
  json: options.json,
57
- envelope: options.envelope,
58
45
  humanFormatter: () => (displayValue !== undefined ? String(displayValue) : ""),
59
46
  context,
60
47
  });
@@ -65,7 +52,7 @@ export function registerConfigGetCommand(configCmd, context) {
65
52
  await withTarget(options, context, async (localTarget) => {
66
53
  if (options.global || !root) {
67
54
  if (!options.global && options.package && !root) {
68
- throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
55
+ throw packageNotFoundError(options.package);
69
56
  }
70
57
  const confView = await localTarget.getConfig("global", key);
71
58
  const val = confView?.value;
@@ -79,7 +66,6 @@ export function registerConfigGetCommand(configCmd, context) {
79
66
  };
80
67
  renderResult(payload, {
81
68
  json: options.json,
82
- envelope: options.envelope,
83
69
  humanFormatter: () => (displayValue !== undefined ? String(displayValue) : ""),
84
70
  context,
85
71
  });
@@ -87,24 +73,8 @@ export function registerConfigGetCommand(configCmd, context) {
87
73
  }
88
74
  const projConfig = loadProjectConfig(root);
89
75
  const declaredItem = projConfig.config?.[key];
90
- const confView = await localTarget.getConfig(projConfig.id, key);
91
- let resolvedVal = confView?.value;
92
- let source = confView?.source === "package" ? "project" : (confView?.source || "default");
93
- const envResolved = resolveEnvValue(key, declaredItem, projConfig.id);
94
- if (confView && confView.configured) {
95
- resolvedVal = confView.value;
96
- source = confView.source === "package" ? "project" : confView.source;
97
- }
98
- else if (envResolved !== undefined) {
99
- resolvedVal = envResolved.value;
100
- source = "env";
101
- }
102
- else {
103
- resolvedVal = declaredItem?.default;
104
- source = "default";
105
- }
106
- const isSecret = confView?.secret ?? isSecretConfigKey(key, declaredItem);
107
- const displayValue = !reveal && isSecret && resolvedVal !== undefined ? maskSecretValue(resolvedVal) : resolvedVal;
76
+ // 合并优先级链单一事实源:项目包级 > 全局持久化 > 环境变量 > 声明默认值
77
+ const { value: displayValue, source, secret: isSecret } = await resolveMergedConfigEntry(key, projConfig.id, declaredItem, localTarget, reveal);
108
78
  const payload = {
109
79
  key,
110
80
  value: displayValue,
@@ -113,7 +83,6 @@ export function registerConfigGetCommand(configCmd, context) {
113
83
  };
114
84
  renderResult(payload, {
115
85
  json: options.json,
116
- envelope: options.envelope,
117
86
  humanFormatter: () => (displayValue !== undefined ? String(displayValue) : ""),
118
87
  context,
119
88
  });
@@ -1,7 +1,8 @@
1
- import { fetchRemoteConfig, filterWithFallbackInfo, findProjectRoot, isSecretConfigKey, loadProjectConfig, maskSecretValue, resolveEnvValue, resolvePackageRoot, resolveTarget, } from "@actiondock/core";
2
- import { ArgumentError } from "../../errors.js";
1
+ import { fetchRemoteConfig, filterWithFallbackInfo, findProjectRoot, isSecretConfigKey, loadProjectConfig, maskSecretValue, resolvePackageRoot, } from "@actiondock/core";
2
+ import { packageNotFoundError } from "../../errors.js";
3
3
  import { renderConfigList, renderResult } from "../../renderer.js";
4
- import { applyTargetOptions, getEffectiveOptions, resolveIntent, withTarget } from "../../utils/index.js";
4
+ import { buildMergedConfigEntries } from "../../services/config-merge.js";
5
+ import { applyTargetOptions, getEffectiveOptions, resolveIntent, resolveTargetFromOptions, withTarget, } from "../../utils/index.js";
5
6
  /**
6
7
  * 构造单条配置项的展示视图(含掩码决策,reveal 为假且为敏感键时打码)。
7
8
  * 掩码决策收敛于命令层,renderer 仅负责值的字符串格式化。
@@ -33,20 +34,13 @@ export function registerConfigListCommand(configCmd, context) {
33
34
  .option("--no-fallback", "Disable fallback to full list when no items match intent")
34
35
  .option("--data-dir <path>", "Custom database storage directory")
35
36
  .option("--json", "Output as JSON")
36
- .option("--envelope", "Wrap JSON output in standard envelope")
37
37
  .action(async (patterns = [], rawOptions, cmd) => {
38
38
  const options = getEffectiveOptions(rawOptions, cmd);
39
39
  const effectiveIntent = resolveIntent(options.intent, patterns);
40
40
  const shouldFallback = options.fallback !== false;
41
41
  const reveal = Boolean(options.reveal || options.showSecrets);
42
42
  // 远端服务分支不经过 Target 门面,直接查询远端配置接口
43
- const remoteTargetInfo = resolveTarget({
44
- profile: options.profile,
45
- server: options.server,
46
- token: options.token,
47
- insecure: options.insecure,
48
- allowInsecureHttp: options.allowInsecureHttp,
49
- }, context?.customHome);
43
+ const remoteTargetInfo = resolveTargetFromOptions(options, context);
50
44
  if (remoteTargetInfo.type === "remote") {
51
45
  const res = await fetchRemoteConfig(remoteTargetInfo.serverUrl, remoteTargetInfo.token, options.package, {
52
46
  allowInsecureHttp: Boolean(options.allowInsecureHttp),
@@ -55,7 +49,6 @@ export function registerConfigListCommand(configCmd, context) {
55
49
  const entries = Object.entries(res.values || {}).map(([k, v]) => toDisplayEntry(k, v, "remote", isSecretConfigKey(k, res.declared?.[k]), reveal, res.declared?.[k]?.description || ""));
56
50
  renderResult(entries, {
57
51
  json: options.json,
58
- envelope: options.envelope,
59
52
  humanFormatter: () => renderConfigList(entries, `Remote Server ${remoteTargetInfo.serverUrl}${remoteTargetInfo.profileName ? ` (Profile: ${remoteTargetInfo.profileName})` : ""}`, false, effectiveIntent),
60
53
  context,
61
54
  });
@@ -70,7 +63,6 @@ export function registerConfigListCommand(configCmd, context) {
70
63
  const filterRes = filterWithFallbackInfo(entries, effectiveIntent, [(c) => c.key, (c) => c.value], shouldFallback);
71
64
  renderResult(filterRes.items, {
72
65
  json: options.json,
73
- envelope: options.envelope,
74
66
  humanFormatter: () => renderConfigList(filterRes.items, "Global Scope", filterRes.isFallback, effectiveIntent),
75
67
  context,
76
68
  });
@@ -78,63 +70,24 @@ export function registerConfigListCommand(configCmd, context) {
78
70
  }
79
71
  if (!root) {
80
72
  if (options.package) {
81
- throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
73
+ throw packageNotFoundError(options.package);
82
74
  }
83
75
  const all = await localTarget.listConfig("global");
84
76
  const entries = all.map((item) => toDisplayEntry(item.key, item.value, "global", item.secret || isSecretConfigKey(item.key), reveal));
85
77
  const filterRes = filterWithFallbackInfo(entries, effectiveIntent, [(c) => c.key, (c) => c.value], shouldFallback);
86
78
  renderResult(filterRes.items, {
87
79
  json: options.json,
88
- envelope: options.envelope,
89
80
  humanFormatter: () => renderConfigList(filterRes.items, "Global Scope (No project found)", filterRes.isFallback, effectiveIntent),
90
81
  context,
91
82
  });
92
83
  return;
93
84
  }
94
85
  const projConfig = loadProjectConfig(root);
95
- const declared = projConfig.config || {};
96
- const globalConfigList = await localTarget.listConfig("global");
97
- const projectConfigList = await localTarget.listConfig(projConfig.id);
98
- const projectConfigMap = new Map(projectConfigList.map((c) => [c.key, c]));
99
- const globalConfigMap = new Map(globalConfigList.map((c) => [c.key, c]));
100
- const allKeys = new Set([
101
- ...Object.keys(declared),
102
- ...projectConfigList.map((c) => c.key),
103
- ...globalConfigList.map((c) => c.key),
104
- ]);
105
- // 配置合并优先级:项目包级 > 全局持久化 > 环境变量 > 声明默认值。
106
- // core 的 RuntimeConfig.describe 已是五层链事实源,但 Target 门面仅按作用域暴露
107
- // listConfig(scope),不提供跨作用域合并视图;此处保持命令层合并逻辑,
108
- // 与 app.getConfig 的 describe 委托链行为语义一致(overrides 层仅存在于 run 链路)。
109
- const merged = Array.from(allKeys).map((k) => {
110
- let rawValue;
111
- let source = "default";
112
- const envResolved = resolveEnvValue(k, declared[k], projConfig.id);
113
- const projItem = projectConfigMap.get(k);
114
- const globItem = globalConfigMap.get(k);
115
- if (projItem && projItem.configured && projItem.source === "package") {
116
- rawValue = projItem.value;
117
- source = "project";
118
- }
119
- else if (globItem && globItem.configured) {
120
- rawValue = globItem.value;
121
- source = "global";
122
- }
123
- else if (envResolved !== undefined) {
124
- rawValue = envResolved.value;
125
- source = "env";
126
- }
127
- else {
128
- rawValue = declared[k]?.default;
129
- source = "default";
130
- }
131
- const isSecret = isSecretConfigKey(k, declared[k]);
132
- return toDisplayEntry(k, rawValue, source, isSecret, reveal, declared[k]?.description || "");
133
- });
86
+ // 跨作用域合并视图单一事实源:项目包级 > 全局持久化 > 环境变量 > 声明默认值
87
+ const merged = await buildMergedConfigEntries(root, localTarget, reveal);
134
88
  const filterRes = filterWithFallbackInfo(merged, effectiveIntent, [(c) => c.key, (c) => c.value, (c) => c.description, (c) => c.source], shouldFallback);
135
89
  renderResult(filterRes.items, {
136
90
  json: options.json,
137
- envelope: options.envelope,
138
91
  humanFormatter: () => renderConfigList(filterRes.items, `${projConfig.name} (${projConfig.id})`, filterRes.isFallback, effectiveIntent),
139
92
  context,
140
93
  });
@@ -1,5 +1,5 @@
1
1
  import { createActionDockTarget, isSecretConfigKey, loadProjectConfig, resolveEnvValue, resolvePackageRoot, } from "@actiondock/core";
2
- import { ArgumentError, ExecutionError } from "../../errors.js";
2
+ import { ArgumentError, ExecutionError, notInProjectError, packageNotFoundError } from "../../errors.js";
3
3
  import { renderConfigSchema, renderResult, writeStdout } from "../../renderer.js";
4
4
  import { getEffectiveOptions } from "../../utils/index.js";
5
5
  /**
@@ -16,7 +16,6 @@ export function registerConfigSchemaCommand(configCmd, context) {
16
16
  .option("-P, --package <id>", "Target package ID or path")
17
17
  .option("--data-dir <path>", "Custom database storage directory")
18
18
  .option("--json", "Output as JSON")
19
- .option("--envelope", "Wrap JSON output in standard envelope")
20
19
  .action(async (identifier, rawOptions, cmd) => {
21
20
  try {
22
21
  const options = getEffectiveOptions(rawOptions, cmd);
@@ -24,9 +23,9 @@ export function registerConfigSchemaCommand(configCmd, context) {
24
23
  const root = resolvePackageRoot(targetPkg);
25
24
  if (!root) {
26
25
  if (targetPkg) {
27
- throw new ArgumentError(`Package '${targetPkg}' not found in linked packages or path`);
26
+ throw packageNotFoundError(targetPkg);
28
27
  }
29
- throw new ArgumentError("Not in an ActionDock project.\nUsage: ad config schema [package-id] or cd into a project directory.");
28
+ throw notInProjectError("Usage: ad config schema [package-id] or cd into a project directory.");
30
29
  }
31
30
  const projConfig = loadProjectConfig(root);
32
31
  const declared = projConfig.config || {};
@@ -97,10 +96,9 @@ export function registerConfigSchemaCommand(configCmd, context) {
97
96
  missingCount: missingRequired.length,
98
97
  configs: items,
99
98
  };
100
- if (options.json || options.envelope) {
99
+ if (options.json) {
101
100
  renderResult(result, {
102
101
  json: options.json,
103
- envelope: options.envelope,
104
102
  context,
105
103
  });
106
104
  }
@@ -1,5 +1,5 @@
1
1
  import { isSecretConfigKey, loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
2
- import { ArgumentError } from "../../errors.js";
2
+ import { ArgumentError, packageNotFoundError } from "../../errors.js";
3
3
  import { writeStdout } from "../../renderer.js";
4
4
  import { applyTargetOptions, getEffectiveOptions, withTarget } from "../../utils/index.js";
5
5
  import { resolveConfigValueInput } from "../../prompt.js";
@@ -69,7 +69,7 @@ export function registerConfigSetCommand(configCmd, context) {
69
69
  }
70
70
  if (!root) {
71
71
  if (options.package) {
72
- throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
72
+ throw packageNotFoundError(options.package);
73
73
  }
74
74
  await target.setConfig("global", key, parsedVal);
75
75
  writeStdout(`[OK] Global configuration '${key}' updated (no project in current directory)`, context);
@@ -1,4 +1,4 @@
1
- import { ArgumentError, ExecutionError } from "../errors.js";
1
+ import { ArgumentError, ExecutionError, packageNotFoundError } from "../errors.js";
2
2
  import { renderActionDetail, renderResult } from "../renderer.js";
3
3
  import { applyTargetOptions, getEffectiveOptions, resolveLocalPackageRoot, withTarget, } from "../utils/index.js";
4
4
  /**
@@ -15,7 +15,6 @@ export function attachDescribeCommand(parent, context) {
15
15
  .option("-P, --package <id>", "Target package ID or path");
16
16
  return applyTargetOptions(cmd)
17
17
  .option("--json", "Output as JSON")
18
- .option("--envelope", "Wrap JSON output in standard envelope")
19
18
  .option("--data-dir <path>", "Custom database storage directory")
20
19
  .action(async (id, rawOptions, cmd) => {
21
20
  const options = getEffectiveOptions(rawOptions, cmd);
@@ -25,7 +24,7 @@ export function attachDescribeCommand(parent, context) {
25
24
  // 目标拓扑解析(仅 local 分支需要包寻址)
26
25
  const targetPackageRoot = resolveLocalPackageRoot(options.package);
27
26
  if (options.package && !targetPackageRoot) {
28
- throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
27
+ throw packageNotFoundError(options.package);
29
28
  }
30
29
  let targetRef = id;
31
30
  if (options.package && !id.includes("/") && !id.includes(":")) {
@@ -57,7 +56,6 @@ export function attachDescribeCommand(parent, context) {
57
56
  };
58
57
  renderResult(detail, {
59
58
  json: options.json,
60
- envelope: options.envelope,
61
59
  humanFormatter: () => renderActionDetail(detail),
62
60
  context,
63
61
  });
@@ -1,7 +1,7 @@
1
- import { fetchRemoteDoctor, resolveTarget, runDoctorChecks } from "@actiondock/core";
1
+ import { fetchRemoteDoctor, runDoctorChecks } from "@actiondock/core";
2
2
  import { ExecutionError } from "../errors.js";
3
3
  import { renderResult } from "../renderer.js";
4
- import { applyTargetOptions, getEffectiveOptions } from "../utils/index.js";
4
+ import { applyTargetOptions, getEffectiveOptions, resolveTargetFromOptions } from "../utils/index.js";
5
5
  export function registerDoctorCommand(program, context) {
6
6
  const cmd = program
7
7
  .command("doctor")
@@ -9,16 +9,10 @@ export function registerDoctorCommand(program, context) {
9
9
  .option("-P, --package <id|path>", "Target package ID or directory path for project diagnostics");
10
10
  applyTargetOptions(cmd)
11
11
  .option("--json", "Output diagnostics report in JSON format")
12
- .option("--envelope", "Wrap JSON output in standard envelope")
13
12
  .action(async (rawOptions, cmd) => {
14
13
  try {
15
14
  const options = getEffectiveOptions(rawOptions, cmd);
16
- const target = resolveTarget({
17
- profile: options.profile,
18
- server: options.server,
19
- token: options.token,
20
- insecure: options.insecure,
21
- }, context?.customHome);
15
+ const target = resolveTargetFromOptions(options, context);
22
16
  let report;
23
17
  if (target.type === "remote") {
24
18
  const remoteRes = await fetchRemoteDoctor(target.serverUrl, target.token, options.package, {
@@ -35,7 +29,6 @@ export function registerDoctorCommand(program, context) {
35
29
  }
36
30
  renderResult(report, {
37
31
  json: options.json,
38
- envelope: options.envelope,
39
32
  humanFormatter: () => {
40
33
  const lines = [];
41
34
  const title = target.type === "remote"
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { basename } from "node:path";
3
3
  import { discoverProjects, findProjectRoot, listLinkedPackages, resolvePackageRoot } from "@actiondock/core";
4
- import { ExecutionError } from "../errors.js";
4
+ import { ExecutionError, notInProjectError, packageNotFoundError } from "../errors.js";
5
5
  import { renderResult, writeStdout } from "../renderer.js";
6
6
  import { getEffectiveOptions, parseListOption } from "../utils/index.js";
7
7
  export function registerExportCommand(program, context) {
@@ -68,7 +68,7 @@ export function registerExportCommand(program, context) {
68
68
  for (const pkgIdOrPath of options.package) {
69
69
  const root = resolvePackageRoot(pkgIdOrPath);
70
70
  if (!root) {
71
- throw new ExecutionError(`Package '${pkgIdOrPath}' not found in linked packages or path`);
71
+ throw packageNotFoundError(pkgIdOrPath);
72
72
  }
73
73
  if (!roots.includes(root)) {
74
74
  roots.push(root);
@@ -81,13 +81,13 @@ export function registerExportCommand(program, context) {
81
81
  roots.push(root);
82
82
  }
83
83
  else {
84
- throw new ExecutionError("Not in an ActionDock project (actiondock.json not found).\nPlease specify -P, --package <id>, --workspace, --all, or cd into a project directory.");
84
+ throw notInProjectError("Please specify -P, --package <id>, --workspace, --all, or cd into a project directory.");
85
85
  }
86
86
  }
87
87
  if (roots.length > 1 && (options.actions || options.playbook)) {
88
88
  throw new ExecutionError("Filtering options (--actions, --playbook) can only be used when exporting a single package.");
89
89
  }
90
- const isMachine = Boolean(options.json || options.envelope);
90
+ const isMachine = Boolean(options.json);
91
91
  try {
92
92
  const { exportCompositeSkill, exportSkill, exportSkillBatch } = await import("@actiondock/builder");
93
93
  if (options.bundle !== undefined) {
@@ -109,7 +109,6 @@ export function registerExportCommand(program, context) {
109
109
  });
110
110
  renderResult(result, {
111
111
  json: isMachine,
112
- envelope: options.envelope,
113
112
  humanFormatter: () => {
114
113
  const lines = [];
115
114
  lines.push(`[OK] Successfully exported Composite Skill: ${result.bundleName}`);
@@ -150,7 +149,6 @@ export function registerExportCommand(program, context) {
150
149
  });
151
150
  renderResult(batchRes, {
152
151
  json: isMachine,
153
- envelope: options.envelope,
154
152
  humanFormatter: () => {
155
153
  const lines = [];
156
154
  lines.push(`[OK] Successfully batch exported ${batchRes.results.length} Skill packages to: ${batchRes.outDir}`);
@@ -183,7 +181,6 @@ export function registerExportCommand(program, context) {
183
181
  });
184
182
  renderResult(result, {
185
183
  json: isMachine,
186
- envelope: options.envelope,
187
184
  humanFormatter: () => {
188
185
  const lines = [];
189
186
  lines.push(`[OK] Successfully exported ${result.mode === "node" ? "Node Directory" : "Source"} Skill: ${result.packageId} (v${result.version})`);