@actiondock/cli 2.7.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +6 -7
  2. package/bin/ad.js +52 -23
  3. package/dist/commands/action/create.js +4 -5
  4. package/dist/commands/add.js +9 -17
  5. package/dist/commands/build.js +3 -18
  6. package/dist/commands/config/delete.js +8 -7
  7. package/dist/commands/config/env.js +7 -11
  8. package/dist/commands/config/get.js +8 -5
  9. package/dist/commands/config/list.js +16 -22
  10. package/dist/commands/config/schema.d.ts +7 -1
  11. package/dist/commands/config/schema.js +44 -67
  12. package/dist/commands/config/set.js +9 -7
  13. package/dist/commands/describe.js +10 -9
  14. package/dist/commands/doctor.js +4 -3
  15. package/dist/commands/export.js +5 -13
  16. package/dist/commands/generate.js +4 -3
  17. package/dist/commands/info.js +3 -1
  18. package/dist/commands/init.js +3 -3
  19. package/dist/commands/link.js +4 -4
  20. package/dist/commands/list.js +19 -10
  21. package/dist/commands/mcp.js +27 -66
  22. package/dist/commands/pack.js +3 -12
  23. package/dist/commands/playbook.js +30 -10
  24. package/dist/commands/profile.js +10 -19
  25. package/dist/commands/remove.js +5 -14
  26. package/dist/commands/run.d.ts +5 -2
  27. package/dist/commands/run.js +19 -20
  28. package/dist/commands/runs.js +26 -35
  29. package/dist/commands/serve.js +177 -57
  30. package/dist/commands/state-list.d.ts +2 -2
  31. package/dist/commands/state-list.js +10 -8
  32. package/dist/commands/state.d.ts +2 -2
  33. package/dist/commands/state.js +110 -103
  34. package/dist/commands/test.js +1 -1
  35. package/dist/commands/validate.js +4 -17
  36. package/dist/errors.d.ts +14 -0
  37. package/dist/errors.js +46 -2
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.js +3 -3
  40. package/dist/renderer.d.ts +1 -1
  41. package/dist/renderer.js +1 -1
  42. package/dist/services/config-merge.d.ts +6 -5
  43. package/dist/services/config-merge.js +10 -8
  44. package/dist/services/inspect.js +3 -1
  45. package/dist/standalone/index.d.ts +4 -14
  46. package/dist/standalone/index.js +5 -44
  47. package/dist/types.d.ts +2 -1
  48. package/dist/types.js +1 -1
  49. package/dist/utils/cert.js +1 -1
  50. package/dist/utils/duration.d.ts +1 -1
  51. package/dist/utils/duration.js +1 -1
  52. package/dist/utils/index.d.ts +1 -2
  53. package/dist/utils/index.js +1 -2
  54. package/dist/utils/input.d.ts +9 -1
  55. package/dist/utils/input.js +12 -1
  56. package/dist/utils/server-launch.d.ts +114 -0
  57. package/dist/utils/server-launch.js +275 -0
  58. package/dist/utils/target.d.ts +14 -13
  59. package/dist/utils/target.js +20 -39
  60. package/dist/utils/utils.d.ts +101 -1
  61. package/dist/utils/utils.js +97 -7
  62. package/package.json +5 -6
  63. package/dist/utils/bytes.d.ts +0 -1
  64. package/dist/utils/bytes.js +0 -1
  65. package/dist/utils/filter.d.ts +0 -1
  66. package/dist/utils/filter.js +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @actiondock/cli
2
2
 
3
- ActionDock 2.0 官方命令行门面工具链。
3
+ ActionDock 2.x 官方命令行门面工具链。
4
4
 
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D24.12.0-green?logo=node.js)](https://nodejs.org/)
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?logo=typescript)](https://www.typescriptlang.org/)
@@ -15,7 +15,6 @@ ActionDock 2.0 官方命令行门面工具链。
15
15
  - 原生基于 Node.js >=24.12.0 运行,充分依托原生类型擦除、内置 SQLite 与原生 HTTP 特性。
16
16
  - 支持 npm、pnpm 与 yarn 标准包管理器进行全局安装与项目级管理。
17
17
  - 日常开发、调试、测试、构建、打包、MCP 服务启动与 HTTP 部署完全基于 Node.js 与 npm 标准工作流,无需安装外部编译器。
18
- - 单文件独立二进制构建(`--target`、`--bytecode`、`--standalone`)与 `actiondock.manifest.json` 已在 2.0 中彻底移除,由标准的 Node.js 目录交付产物与 npm 打包取代。
19
18
 
20
19
  ---
21
20
 
@@ -109,6 +108,7 @@ ad pack
109
108
 
110
109
  - 严格目标定位:通过 `-P, --package <id|path>` 指定目标包。系统严格区分物理路径与注册表包标识符,若目标不存在则不向当前目录或父级目录隐式回退,直接以参数校验错误退出。
111
110
  - 多目标检索契约:在执行多目标检索(如 `ad info` 或 `ad list`)时,若无任何匹配项,在机器模式下始终返回确定性空数组结构并以状态码 0 退出,不因空搜索产生异常中断。
111
+ - 服务端细粒度白名单:通过 `ad serve` 启动微服务时,支持通过 `-P, --package <package-id>` 与 `-A, --action <action-ref>` 分别指定包白名单与动作白名单(支持短名与全限定名,可多次指定或逗号分隔)。同时配置时取权限交集。未授权动作的详情调阅与执行均被阻断并返回 403 错误(错误码 `ACTION_FORBIDDEN`),历史运行记录与 MCP 协议工具暴露同步过滤。
112
112
 
113
113
  ### Action 执行入参契约
114
114
 
@@ -181,7 +181,7 @@ $data | ConvertTo-Json -Depth 100 | ad run complex-action --input-file -
181
181
  | `ad export skill` | 导出 Agent Skill 技能包(支持 `--mode source` 与 `--mode node`) |
182
182
  | `ad link` / `unlink` | 注册或注销工作区全局路由与符号链接 |
183
183
  | `ad profile` | 管理远程执行节点凭证与环境配置 |
184
- | `ad serve` | 启动远程 HTTP/HTTPS 执行调度微服务(原生支持 `--https` 自签名与生产证书) |
184
+ | `ad serve` | 启动远程 HTTP/HTTPS 执行调度微服务(支持 `-P` 包白名单与 `-A` 动作白名单细粒度权限控制,原生支持 `--https` 自签名与生产证书) |
185
185
  | `ad mcp` | 以 STDIO 或 HTTP 协议启动 MCP 服务 |
186
186
 
187
187
  ---
@@ -190,10 +190,9 @@ $data | ConvertTo-Json -Depth 100 | ad run complex-action --input-file -
190
190
 
191
191
  作为顶层门面与独立运行分发器,`@actiondock/cli` 串联以下子包:
192
192
 
193
- - 运行时适配:依赖 [@actiondock/runtime-node](../runtime-node/README.md),在启动时自动注入基于 Node.js 原生能力的驱动实现。
194
- - 领域内核:依赖 [@actiondock/core](../core/README.md),调度统一调用门面 ActionDockTarget、数据目录锁 DataDirLock 与依赖事务管理器。
195
- - 构建与打包:依赖 [@actiondock/builder](../builder/README.md),完成目录交付构建、npm 打包与技能导出。
196
- - 协议服务:通过 [@actiondock/mcp](../mcp/README.md) 启动协议监听。
193
+ - 领域内核与运行时:依赖 [@actiondock/core](../core/README.md),统一对接标准服务端口体系(`DiscoveryPort`、`ExecutionPort`、`RunsPort`、`ConfigPort`、`StatePort`)与统一服务门面(`createActionDock`、`connectActionDock`),承载原生运行时、存储驱动与受管进程治理。
194
+ - 构建与打包:依赖 [@actiondock/builder](../builder/README.md),完成依赖闭包规划、Node 目录交付产物构建、npm 打包与 Agent Skill 资产导出。
195
+ - 协议服务:通过 [@actiondock/mcp](../mcp/README.md) 启动 Model Context Protocol 协议监听与适配。
197
196
 
198
197
  ---
199
198
 
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
  }
@@ -1,7 +1,8 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
- import { assertPathWithinRoot, findProjectRoot, getPackageSlug, loadManifest, loadProjectConfig, saveManifest, writeActionTypes, } from "@actiondock/core";
4
- import { ExecutionError, notInProjectError } from "../../errors.js";
3
+ import { assertPathWithinRoot, getPackageSlug, loadManifest, saveManifest, writeActionTypes, } from "@actiondock/core/project";
4
+ import { findProjectRoot, loadProjectConfig, } from "@actiondock/core";
5
+ import { ExecutionError, notInProjectError, wrapAsExecutionError } from "../../errors.js";
5
6
  import { writeStdout } from "../../renderer.js";
6
7
  import { attachDescribeCommand } from "../describe.js";
7
8
  import { attachListCommand } from "../list.js";
@@ -210,8 +211,6 @@ ${returnBody}
210
211
  writeStdout(` 复杂输入或大段文本:ad run ${id} --json --input-file input.json`, context);
211
212
  }
212
213
  catch (err) {
213
- if (err instanceof ExecutionError)
214
- throw err;
215
- throw new ExecutionError(err.message);
214
+ throw wrapAsExecutionError(err);
216
215
  }
217
216
  }
@@ -1,9 +1,9 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
3
- import { ActionPackageResolver, beginTransaction, computeManifestDigest, findProjectRoot, getInstallCommand, loadLockfile, loadManifest, MANIFEST_FILE_NAME, parseJsonWithoutDuplicates, saveLockfile, saveManifest, } from "@actiondock/core";
4
- import { ArgumentError, ExecutionError, notInProjectError } from "../errors.js";
2
+ import { join } from "node:path";
3
+ import { PackageGraphBuilder, } from "@actiondock/core/graph";
4
+ import { beginTransaction, computeManifestDigest, getInstallCommand, loadLockfile, MANIFEST_FILE_NAME, parseJsonWithoutDuplicates, saveLockfile, saveManifest, } from "@actiondock/core/project";
5
5
  import { renderResult } from "../renderer.js";
6
- import { assertSafePackageSpec, getEffectiveOptions, spawnAsync } from "../utils/index.js";
6
+ import { assertSafePackageSpec, getEffectiveOptions, requireProjectManifestRoot, rollbackAndRethrow, spawnAsync, } from "../utils/index.js";
7
7
  /**
8
8
  * 从安装说明符中提取基础 npm 包名(去除版本号及前缀范围)。
9
9
  */
@@ -36,14 +36,7 @@ export function registerAddCommand(program, context) {
36
36
  .option("--json", "Output as JSON")
37
37
  .action(async (packageSpec, rawOptions, cmd) => {
38
38
  const options = getEffectiveOptions(rawOptions, cmd);
39
- const root = options.package ? resolve(options.package) : findProjectRoot();
40
- if (!root) {
41
- throw notInProjectError("Please specify -P, --package <path> or cd into a project directory.");
42
- }
43
- const manifest = loadManifest(root);
44
- if (!manifest) {
45
- throw new ArgumentError(`actiondock.json not found in ${root}`);
46
- }
39
+ const { root, manifest } = requireProjectManifestRoot(options.package);
47
40
  // 包说明符白名单前置校验:拒绝 shell 元字符,消除 win32 shell 拼接注入面
48
41
  assertSafePackageSpec(packageSpec);
49
42
  const npmPackageName = extractNpmPackageName(packageSpec);
@@ -97,9 +90,9 @@ export function registerAddCommand(program, context) {
97
90
  manifest.dependencies = manifest.dependencies || {};
98
91
  manifest.dependencies[targetPackageId] =
99
92
  targetPackageId === npmPackageName ? `^${targetVersion}` : npmPackageName;
100
- // 触发依赖图解析及版本冲突检测
101
- const resolver = new ActionPackageResolver({ projectRoot: root, manifest });
102
- const graph = resolver.resolveSync();
93
+ // 触发依赖图构建及版本冲突检测
94
+ const builder = new PackageGraphBuilder({ projectRoot: root, manifest });
95
+ const graph = builder.buildSync();
103
96
  // 更新 actiondock.lock.json
104
97
  const lockfile = loadLockfile(root) || {
105
98
  lockfileVersion: 1,
@@ -137,8 +130,7 @@ export function registerAddCommand(program, context) {
137
130
  });
138
131
  }
139
132
  catch (err) {
140
- await tx.rollback({ frozenInstall: false });
141
- throw new ExecutionError(err.message, err, err.code || "ADD_DEPENDENCY_FAILED");
133
+ await rollbackAndRethrow(tx, err, "ADD_DEPENDENCY_FAILED");
142
134
  }
143
135
  });
144
136
  }
@@ -1,7 +1,6 @@
1
- import { findProjectRoot, resolvePackageRoot } from "@actiondock/core";
2
- import { ExecutionError, notInProjectError, packageNotFoundError } from "../errors.js";
1
+ import { ExecutionError } from "../errors.js";
3
2
  import { renderResult, writeStdout } from "../renderer.js";
4
- import { getEffectiveOptions, parseListOption } from "../utils/index.js";
3
+ import { getEffectiveOptions, parseListOption, requirePackageRoot } from "../utils/index.js";
5
4
  export function registerBuildCommand(program, context) {
6
5
  program
7
6
  .command("build")
@@ -14,23 +13,9 @@ export function registerBuildCommand(program, context) {
14
13
  .option("--vendor-deps", "Materialize locked production dependencies into the output")
15
14
  .option("--allow-install-scripts", "Allow lifecycle install scripts to run during vendor-deps")
16
15
  .option("--require-reproducible", "Require reproducible build and fail if install scripts must run")
17
- .option("-t, --target <target>", "Target compilation platform (removed)")
18
- .option("--bytecode", "Bytecode compilation (removed)")
19
16
  .action(async (rawOptions, cmd) => {
20
17
  const options = getEffectiveOptions(rawOptions, cmd);
21
- // 彻底删除原有单文件二进制输出语义
22
- if (options.target !== undefined || options.bytecode !== undefined) {
23
- throw new ExecutionError("Unsupported build mode: '--target' and '--bytecode' standalone single-file binary compilation have been removed in ActionDock 2.0. Directory-based Node.js builds are now the standard distribution format.", undefined, "UNSUPPORTED_BUILD_MODE");
24
- }
25
- const root = options.package
26
- ? resolvePackageRoot(options.package)
27
- : findProjectRoot();
28
- if (!root) {
29
- if (options.package) {
30
- throw packageNotFoundError(options.package);
31
- }
32
- throw notInProjectError("Please specify -P, --package <id> or cd into a project directory.");
33
- }
18
+ const { root } = requirePackageRoot(options.package);
34
19
  const isMachine = Boolean(options.json);
35
20
  if (!isMachine) {
36
21
  writeStdout("Building Node.js delivery artifact...");
@@ -1,7 +1,8 @@
1
- import { loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
1
+ import { loadProjectConfig, } from "@actiondock/core";
2
+ import { resolvePackageRoot, } from "@actiondock/core/registry";
2
3
  import { ArgumentError, packageNotFoundError } from "../../errors.js";
3
4
  import { writeStdout } from "../../renderer.js";
4
- import { applyTargetOptions, getEffectiveOptions, withTarget } from "../../utils/index.js";
5
+ import { applyTargetOptions, getEffectiveOptions, withService } from "../../utils/index.js";
5
6
  /**
6
7
  * 注册 config delete 子命令:删除配置项。
7
8
  *
@@ -22,14 +23,14 @@ export function registerConfigDeleteCommand(configCmd, context) {
22
23
  throw new ArgumentError("Configuration key is required");
23
24
  }
24
25
  const root = resolvePackageRoot(options.package);
25
- await withTarget(options, context, async (target, resolved) => {
26
+ await withService(options, context, async (service, resolved) => {
26
27
  if (resolved.type === "remote") {
27
- await target.deleteConfig(options.package || "", key);
28
+ await service.management?.config.delete(options.package || "", key);
28
29
  writeStdout(`[OK] Configuration '${key}' deleted from remote server`, context);
29
30
  return;
30
31
  }
31
32
  if (options.global) {
32
- await target.deleteConfig("global", key);
33
+ await service.management?.config.delete("global", key);
33
34
  writeStdout(`[OK] Global configuration '${key}' deleted`, context);
34
35
  return;
35
36
  }
@@ -37,12 +38,12 @@ export function registerConfigDeleteCommand(configCmd, context) {
37
38
  if (options.package) {
38
39
  throw packageNotFoundError(options.package);
39
40
  }
40
- await target.deleteConfig("global", key);
41
+ await service.management?.config.delete("global", key);
41
42
  writeStdout(`[OK] Global configuration '${key}' deleted`, context);
42
43
  return;
43
44
  }
44
45
  const projConfig = loadProjectConfig(root);
45
- await target.deleteConfig(projConfig.id, key);
46
+ await service.management?.config.delete(projConfig.id, key);
46
47
  writeStdout(`[OK] Configuration '${key}' deleted for package '${projConfig.id}'`, context);
47
48
  });
48
49
  });
@@ -1,7 +1,7 @@
1
- import { fetchRemoteConfigEnv, isSecretConfigKey, loadProjectConfig, resolveEnvValue, resolvePackageRoot, } from "@actiondock/core";
2
- import { notInProjectError, packageNotFoundError } from "../../errors.js";
1
+ import { fetchRemoteConfigEnv, resolveEnvValue, } from "@actiondock/core/profile";
2
+ import { isSecretConfigKey, } from "@actiondock/core/project";
3
3
  import { renderConfigEnv, renderResult } from "../../renderer.js";
4
- import { applyTargetOptions, getEffectiveOptions, resolveTargetFromOptions } from "../../utils/index.js";
4
+ import { applyTargetOptions, getEffectiveOptions, requirePackageRoot, resolveTargetFromOptions } from "../../utils/index.js";
5
5
  /**
6
6
  * 注册 config env 子命令:诊断环境变量对声明配置的满足率。
7
7
  *
@@ -36,14 +36,10 @@ export function registerConfigEnvCommand(configCmd, context) {
36
36
  return;
37
37
  }
38
38
  // 本地项目分支
39
- const root = resolvePackageRoot(targetPkg);
40
- if (!root) {
41
- if (targetPkg) {
42
- throw packageNotFoundError(targetPkg);
43
- }
44
- throw notInProjectError("Usage: ad config env [package-id] or cd into a project directory.");
45
- }
46
- const projConfig = loadProjectConfig(root);
39
+ const { root, projConfig } = requirePackageRoot(targetPkg, {
40
+ loadConfig: true,
41
+ hint: "Usage: ad config env [package-id] or cd into a project directory.",
42
+ });
47
43
  const declared = projConfig.config || {};
48
44
  const envChecks = [];
49
45
  for (const [key, itemDef] of Object.entries(declared)) {
@@ -1,8 +1,11 @@
1
- import { fetchRemoteConfig, isSecretConfigKey, loadProjectConfig, maskSecretValue, resolvePackageRoot, } from "@actiondock/core";
1
+ import { fetchRemoteConfig, } from "@actiondock/core/profile";
2
+ import { isSecretConfigKey, maskSecretValue, } from "@actiondock/core/project";
3
+ import { loadProjectConfig, } from "@actiondock/core";
4
+ import { resolvePackageRoot, } from "@actiondock/core/registry";
2
5
  import { ArgumentError, packageNotFoundError } from "../../errors.js";
3
6
  import { renderResult } from "../../renderer.js";
4
7
  import { resolveMergedConfigEntry } from "../../services/config-merge.js";
5
- import { applyTargetOptions, getEffectiveOptions, resolveTargetFromOptions, withTarget, } from "../../utils/index.js";
8
+ import { applyTargetOptions, getEffectiveOptions, resolveTargetFromOptions, withService, } from "../../utils/index.js";
6
9
  /**
7
10
  * 注册 config get 子命令:查询指定配置键的生效值。
8
11
  *
@@ -49,12 +52,12 @@ export function registerConfigGetCommand(configCmd, context) {
49
52
  }
50
53
  // 本地分支
51
54
  const root = resolvePackageRoot(options.package);
52
- await withTarget(options, context, async (localTarget) => {
55
+ await withService(options, context, async (service) => {
53
56
  if (options.global || !root) {
54
57
  if (!options.global && options.package && !root) {
55
58
  throw packageNotFoundError(options.package);
56
59
  }
57
- const confView = await localTarget.getConfig("global", key);
60
+ const confView = await service.management?.config.get("global", key);
58
61
  const val = confView?.value;
59
62
  const isSecret = confView?.secret ?? isSecretConfigKey(key);
60
63
  const displayValue = !reveal && isSecret && val !== undefined ? maskSecretValue(val) : val;
@@ -74,7 +77,7 @@ export function registerConfigGetCommand(configCmd, context) {
74
77
  const projConfig = loadProjectConfig(root);
75
78
  const declaredItem = projConfig.config?.[key];
76
79
  // 合并优先级链单一事实源:项目包级 > 全局持久化 > 环境变量 > 声明默认值
77
- const { value: displayValue, source, secret: isSecret } = await resolveMergedConfigEntry(key, projConfig.id, declaredItem, localTarget, reveal);
80
+ const { value: displayValue, source, secret: isSecret } = await resolveMergedConfigEntry(key, projConfig.id, declaredItem, service, reveal);
78
81
  const payload = {
79
82
  key,
80
83
  value: displayValue,
@@ -1,8 +1,11 @@
1
- import { fetchRemoteConfig, filterWithFallbackInfo, findProjectRoot, isSecretConfigKey, loadProjectConfig, maskSecretValue, resolvePackageRoot, } from "@actiondock/core";
1
+ import { fetchRemoteConfig, } from "@actiondock/core/profile";
2
+ import { findProjectRoot, loadProjectConfig, } from "@actiondock/core";
3
+ import { filterWithFallbackInfo, isSecretConfigKey, maskSecretValue, } from "@actiondock/core/project";
4
+ import { resolvePackageRoot, } from "@actiondock/core/registry";
2
5
  import { packageNotFoundError } from "../../errors.js";
3
6
  import { renderConfigList, renderResult } from "../../renderer.js";
4
7
  import { buildMergedConfigEntries } from "../../services/config-merge.js";
5
- import { applyTargetOptions, getEffectiveOptions, resolveIntent, resolveTargetFromOptions, withTarget, } from "../../utils/index.js";
8
+ import { applyTargetOptions, getEffectiveOptions, remoteTargetLabel, resolveFallbackStrategy, resolveIntent, resolveTargetFromOptions, withService, } from "../../utils/index.js";
6
9
  /**
7
10
  * 构造单条配置项的展示视图(含掩码决策,reveal 为假且为敏感键时打码)。
8
11
  * 掩码决策收敛于命令层,renderer 仅负责值的字符串格式化。
@@ -31,13 +34,14 @@ export function registerConfigListCommand(configCmd, context) {
31
34
  .option("-g, --global", "Show only global configurations"))
32
35
  .option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
33
36
  .option("--reveal, --show-secrets", "Reveal plain text values for secrets")
37
+ .option("--fallback", "Enable fallback to full list when no items match intent")
34
38
  .option("--no-fallback", "Disable fallback to full list when no items match intent")
35
39
  .option("--data-dir <path>", "Custom database storage directory")
36
40
  .option("--json", "Output as JSON")
37
41
  .action(async (patterns = [], rawOptions, cmd) => {
38
42
  const options = getEffectiveOptions(rawOptions, cmd);
39
43
  const effectiveIntent = resolveIntent(options.intent, patterns);
40
- const shouldFallback = options.fallback !== false;
44
+ const { shouldFallback } = resolveFallbackStrategy(options);
41
45
  const reveal = Boolean(options.reveal || options.showSecrets);
42
46
  // 远端服务分支不经过 Target 门面,直接查询远端配置接口
43
47
  const remoteTargetInfo = resolveTargetFromOptions(options, context);
@@ -49,42 +53,32 @@ export function registerConfigListCommand(configCmd, context) {
49
53
  const entries = Object.entries(res.values || {}).map(([k, v]) => toDisplayEntry(k, v, "remote", isSecretConfigKey(k, res.declared?.[k]), reveal, res.declared?.[k]?.description || ""));
50
54
  renderResult(entries, {
51
55
  json: options.json,
52
- humanFormatter: () => renderConfigList(entries, `Remote Server ${remoteTargetInfo.serverUrl}${remoteTargetInfo.profileName ? ` (Profile: ${remoteTargetInfo.profileName})` : ""}`, false, effectiveIntent),
56
+ humanFormatter: () => renderConfigList(entries, remoteTargetLabel(remoteTargetInfo), false, effectiveIntent),
53
57
  context,
54
58
  });
55
59
  return;
56
60
  }
57
- // 本地分支(通过 Target 门面统一访问)
58
- await withTarget(options, context, async (localTarget) => {
61
+ // 本地分支(通过 Service 门面统一访问)
62
+ await withService(options, context, async (service) => {
59
63
  const root = options.package ? resolvePackageRoot(options.package) : findProjectRoot();
60
- if (options.global) {
61
- const all = await localTarget.listConfig("global");
62
- const entries = all.map((item) => toDisplayEntry(item.key, item.value, "global", item.secret || isSecretConfigKey(item.key), reveal));
63
- const filterRes = filterWithFallbackInfo(entries, effectiveIntent, [(c) => c.key, (c) => c.value], shouldFallback);
64
- renderResult(filterRes.items, {
65
- json: options.json,
66
- humanFormatter: () => renderConfigList(filterRes.items, "Global Scope", filterRes.isFallback, effectiveIntent),
67
- context,
68
- });
69
- return;
70
- }
71
- if (!root) {
72
- if (options.package) {
64
+ // 全局作用域分支:显式 --global 或无工程回退时仅列举全局配置
65
+ if (options.global || !root) {
66
+ if (!options.global && options.package) {
73
67
  throw packageNotFoundError(options.package);
74
68
  }
75
- const all = await localTarget.listConfig("global");
69
+ const all = (await service.management?.config.list("global")) ?? [];
76
70
  const entries = all.map((item) => toDisplayEntry(item.key, item.value, "global", item.secret || isSecretConfigKey(item.key), reveal));
77
71
  const filterRes = filterWithFallbackInfo(entries, effectiveIntent, [(c) => c.key, (c) => c.value], shouldFallback);
78
72
  renderResult(filterRes.items, {
79
73
  json: options.json,
80
- humanFormatter: () => renderConfigList(filterRes.items, "Global Scope (No project found)", filterRes.isFallback, effectiveIntent),
74
+ humanFormatter: () => renderConfigList(filterRes.items, options.global ? "Global Scope" : "Global Scope (No project found)", filterRes.isFallback, effectiveIntent),
81
75
  context,
82
76
  });
83
77
  return;
84
78
  }
85
79
  const projConfig = loadProjectConfig(root);
86
80
  // 跨作用域合并视图单一事实源:项目包级 > 全局持久化 > 环境变量 > 声明默认值
87
- const merged = await buildMergedConfigEntries(root, localTarget, reveal);
81
+ const merged = await buildMergedConfigEntries(root, service, reveal);
88
82
  const filterRes = filterWithFallbackInfo(merged, effectiveIntent, [(c) => c.key, (c) => c.value, (c) => c.description, (c) => c.source], shouldFallback);
89
83
  renderResult(filterRes.items, {
90
84
  json: options.json,
@@ -3,7 +3,13 @@ import type { CliContext } from "../../types.js";
3
3
  /**
4
4
  * 注册 config schema 子命令:检查声明配置的解析状态。
5
5
  *
6
+ * 状态推导复用 config-merge 的跨作用域合并视图单一事实源
7
+ * (项目包级 > 全局持久化 > 环境变量 > 声明默认值):
8
+ * - source 命中 project、global 或 env 时状态为 SET;
9
+ * - source 为 default(仅声明默认值兜底)时状态为 DEFAULT;
10
+ * - 键无任何来源(不在合并视图中,即未声明默认值也未持久化)时状态为 MISSING。
11
+ *
6
12
  * @param configCmd config 命令实例
7
- * @param context 命令行上下文
13
+ * @param context CLI 上下文
8
14
  */
9
15
  export declare function registerConfigSchemaCommand(configCmd: Command, context?: CliContext): void;
@@ -1,80 +1,63 @@
1
- import { createActionDockTarget, isSecretConfigKey, loadProjectConfig, resolveEnvValue, resolvePackageRoot, } from "@actiondock/core";
2
- import { ArgumentError, ExecutionError, notInProjectError, packageNotFoundError } from "../../errors.js";
1
+ import { isSecretConfigKey, } from "@actiondock/core/project";
2
+ import { notInProjectError } from "../../errors.js";
3
3
  import { renderConfigSchema, renderResult, writeStdout } from "../../renderer.js";
4
- import { getEffectiveOptions } from "../../utils/index.js";
4
+ import { buildMergedConfigEntries } from "../../services/config-merge.js";
5
+ import { applyTargetOptions, getEffectiveOptions, requirePackageRoot, withService } from "../../utils/index.js";
5
6
  /**
6
7
  * 注册 config schema 子命令:检查声明配置的解析状态。
7
8
  *
9
+ * 状态推导复用 config-merge 的跨作用域合并视图单一事实源
10
+ * (项目包级 > 全局持久化 > 环境变量 > 声明默认值):
11
+ * - source 命中 project、global 或 env 时状态为 SET;
12
+ * - source 为 default(仅声明默认值兜底)时状态为 DEFAULT;
13
+ * - 键无任何来源(不在合并视图中,即未声明默认值也未持久化)时状态为 MISSING。
14
+ *
8
15
  * @param configCmd config 命令实例
9
- * @param context 命令行上下文
16
+ * @param context CLI 上下文
10
17
  */
11
18
  export function registerConfigSchemaCommand(configCmd, context) {
12
- configCmd
19
+ applyTargetOptions(configCmd
13
20
  .command("schema [identifier]")
14
21
  .alias("check")
15
22
  .description("Inspect declared configuration requirements and check resolution status")
16
- .option("-P, --package <id>", "Target package ID or path")
23
+ .option("-P, --package <id>", "Target package ID or path"))
17
24
  .option("--data-dir <path>", "Custom database storage directory")
18
25
  .option("--json", "Output as JSON")
19
26
  .action(async (identifier, rawOptions, cmd) => {
20
- try {
21
- const options = getEffectiveOptions(rawOptions, cmd);
22
- const targetPkg = identifier || options.package;
23
- const root = resolvePackageRoot(targetPkg);
24
- if (!root) {
25
- if (targetPkg) {
26
- throw packageNotFoundError(targetPkg);
27
- }
28
- throw notInProjectError("Usage: ad config schema [package-id] or cd into a project directory.");
29
- }
30
- const projConfig = loadProjectConfig(root);
31
- const declared = projConfig.config || {};
32
- const declaredKeys = Object.keys(declared);
33
- const target = await createActionDockTarget({
34
- type: "local",
35
- projectRoot: root,
36
- customHome: context?.customHome,
37
- dataDir: options.dataDir || context?.dataDir,
38
- });
39
- let globalConfig = [];
40
- let projectConfig = [];
41
- try {
42
- globalConfig = await target.listConfig("global");
43
- projectConfig = await target.listConfig(projConfig.id);
44
- }
45
- finally {
46
- await target.close();
47
- }
48
- const projectConfigMap = new Map(projectConfig.map((c) => [c.key, c]));
49
- const globalConfigMap = new Map(globalConfig.map((c) => [c.key, c]));
50
- const items = declaredKeys.map((key) => {
27
+ const options = getEffectiveOptions(rawOptions, cmd);
28
+ const targetPkg = identifier || options.package;
29
+ const { root, projConfig } = requirePackageRoot(targetPkg, {
30
+ loadConfig: true,
31
+ hint: "Usage: ad config schema [package-id] or cd into a project directory.",
32
+ });
33
+ const declared = projConfig.config || {};
34
+ // 远端目标显式拒绝:schema 状态推导依赖本地工程声明与环境变量解析,
35
+ // 远端作用域无法获得等价视图,静默落到本地会误导调用方
36
+ if (options.profile || options.server) {
37
+ throw notInProjectError("'ad config schema' only supports local projects. Remote targets are not supported for schema inspection.");
38
+ }
39
+ // 跨作用域合并视图单一事实源:与 config list 共享同一条优先级链
40
+ await withService(options, context, async (service) => {
41
+ const merged = await buildMergedConfigEntries(root, service, true);
42
+ const mergedByKey = new Map(merged.map((entry) => [entry.key, entry]));
43
+ const items = Object.keys(declared).map((key) => {
51
44
  const itemDef = declared[key];
52
45
  const isSecret = isSecretConfigKey(key, itemDef);
53
- let resolvedValue;
46
+ const mergedEntry = mergedByKey.get(key);
54
47
  let source = "missing";
55
48
  let status = "MISSING";
56
- const envResolved = resolveEnvValue(key, itemDef, projConfig.id);
57
- const projItem = projectConfigMap.get(key);
58
- const globItem = globalConfigMap.get(key);
59
- if (projItem && projItem.configured && projItem.source === "package") {
60
- resolvedValue = projItem.value;
61
- source = "project";
62
- status = "SET";
63
- }
64
- else if (globItem && globItem.configured) {
65
- resolvedValue = globItem.value;
66
- source = "global";
67
- status = "SET";
68
- }
69
- else if (envResolved !== undefined) {
70
- resolvedValue = envResolved.value;
71
- source = "env";
72
- status = "SET";
73
- }
74
- else if (itemDef.default !== undefined) {
75
- resolvedValue = itemDef.default;
76
- source = "default";
77
- status = "DEFAULT";
49
+ let resolvedValue;
50
+ if (mergedEntry) {
51
+ source = mergedEntry.source;
52
+ resolvedValue = mergedEntry.value;
53
+ // 合并视图的 default 来源仅是链尾兑底标签:
54
+ // 真正的 DEFAULT 状态要求声明确实携带默认值,否则视为 MISSING
55
+ if (mergedEntry.source !== "default") {
56
+ status = "SET";
57
+ }
58
+ else if (itemDef.default !== undefined) {
59
+ status = "DEFAULT";
60
+ }
78
61
  }
79
62
  return {
80
63
  key,
@@ -108,12 +91,6 @@ export function registerConfigSchemaCommand(configCmd, context) {
108
91
  if (!ok) {
109
92
  process.exitCode = 1;
110
93
  }
111
- }
112
- catch (err) {
113
- if (err instanceof ArgumentError || err instanceof ExecutionError) {
114
- throw err;
115
- }
116
- throw new ExecutionError(err.message);
117
- }
94
+ }, { localRoot: root });
118
95
  });
119
96
  }