@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
@@ -1,5 +1,5 @@
1
1
  import { findProjectRoot, writeActionTypes } from "@actiondock/core";
2
- import { ArgumentError, ExecutionError } from "../errors.js";
2
+ import { ExecutionError, notInProjectError } from "../errors.js";
3
3
  import { renderResult } from "../renderer.js";
4
4
  import { getEffectiveOptions } from "../utils/index.js";
5
5
  /**
@@ -16,12 +16,11 @@ export function registerGenerateCommands(program, context) {
16
16
  .command("types")
17
17
  .description("Generate .actiondock/generated/actions.d.ts from actiondock.json schema")
18
18
  .option("--json", "Output as JSON")
19
- .option("--envelope", "Wrap JSON output in standard envelope")
20
19
  .action(async (rawOptions, cmd) => {
21
20
  const options = getEffectiveOptions(rawOptions, cmd);
22
21
  const root = findProjectRoot();
23
22
  if (!root) {
24
- throw new ArgumentError("Not in an ActionDock project (actiondock.json not found)");
23
+ throw notInProjectError();
25
24
  }
26
25
  try {
27
26
  const { filePath, digest } = writeActionTypes(root);
@@ -32,7 +31,6 @@ export function registerGenerateCommands(program, context) {
32
31
  };
33
32
  renderResult(result, {
34
33
  json: options.json,
35
- envelope: options.envelope,
36
34
  humanFormatter: () => `[OK] Generated action types at ${filePath}\nManifest digest: ${digest}`,
37
35
  context,
38
36
  });
@@ -45,8 +45,8 @@ export const CLI_VERSION = readCliVersion();
45
45
  /**
46
46
  * 递归注入全局通用选项。
47
47
  *
48
- * 输出纪律契约:--json/--envelope 对所有命令统一注入;已实现 renderResult
49
- * 数据输出的命令会消费它们,未实现机器输出的命令(init、link、test、new、
48
+ * 输出纪律契约:--json 对所有命令统一注入;已实现 renderResult
49
+ * 数据输出的命令会消费它,未实现机器输出的命令(init、link、test、new、
50
50
  * serve、mcp 等)将其作为无操作标志忽略,不影响人类输出行为。
51
51
  * 错误信封由顶层 main 的 renderError 统一兜底,与命令实现解耦。
52
52
  */
@@ -55,9 +55,6 @@ function applyCommonOptions(cmd) {
55
55
  if (!hasOpt("json")) {
56
56
  cmd.option("--json", "Output as JSON");
57
57
  }
58
- if (!hasOpt("envelope")) {
59
- cmd.option("--envelope", "Wrap JSON output in standard envelope");
60
- }
61
58
  if (!hasOpt("dataDir") && !hasOpt("data-dir")) {
62
59
  cmd.option("--data-dir <path>", "Custom database storage directory");
63
60
  }
@@ -1,8 +1,8 @@
1
- import { fetchRemoteInfo, filterWithFallbackInfo, getRegistryStatus, resolvePackageRoot, resolveTarget, } from "@actiondock/core";
2
- import { ArgumentError, ExecutionError } from "../errors.js";
1
+ import { fetchRemoteInfo, filterWithFallbackInfo, getRegistryStatus, resolvePackageRoot, } from "@actiondock/core";
2
+ import { ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE, packageNotFoundError } from "../errors.js";
3
3
  import { projectDetailToJson, renderAggregatedPackages, renderProjectDetail, renderRegistryTree, renderResult, } from "../renderer.js";
4
4
  import { getProjectDetailInfo, scanLocalAggregatedPackages } from "../services/index.js";
5
- import { applyTargetOptions, getEffectiveOptions, resolveFallbackStrategy, resolveIntent } from "../utils/index.js";
5
+ import { applyTargetOptions, getEffectiveOptions, resolveFallbackStrategy, resolveIntent, resolveTargetFromOptions, } from "../utils/index.js";
6
6
  /**
7
7
  * 将远端信息响应归一为本地工程详情视图。
8
8
  */
@@ -29,7 +29,6 @@ function remoteInfoToDetail(remoteInfo) {
29
29
  function renderProjectDetailOutput(detail, options) {
30
30
  renderResult(projectDetailToJson(detail), {
31
31
  json: options.json,
32
- envelope: options.envelope,
33
32
  humanFormatter: () => renderProjectDetail(detail),
34
33
  context: options.context,
35
34
  });
@@ -51,20 +50,14 @@ export function registerInfoCommand(program, context) {
51
50
  .option("--fallback", "Enable fallback to full list when no items match intent")
52
51
  .option("--no-fallback", "Disable fallback to full list when no items match intent")
53
52
  .option("--json", "Output information as JSON")
54
- .option("--envelope", "Wrap JSON output in standard envelope")
55
53
  .option("--data-dir <path>", "Custom database storage directory")
56
54
  .action(async (patterns = [], rawOptions, cmd) => {
57
55
  const options = getEffectiveOptions(rawOptions, cmd);
58
56
  const effectiveIntent = resolveIntent(options.intent, patterns);
59
57
  const { isMachine, shouldFallback } = resolveFallbackStrategy(options);
60
- const outOpts = { json: options.json, envelope: options.envelope, context };
58
+ const outOpts = { json: options.json, context };
61
59
  // 远端服务目标分支
62
- const target = resolveTarget({
63
- profile: options.profile,
64
- server: options.server,
65
- token: options.token,
66
- insecure: options.insecure,
67
- }, context?.customHome);
60
+ const target = resolveTargetFromOptions(options, context);
68
61
  if (target.type === "remote") {
69
62
  const remoteInfo = await fetchRemoteInfo(target.serverUrl, target.token, {
70
63
  intent: effectiveIntent,
@@ -104,7 +97,6 @@ export function registerInfoCommand(program, context) {
104
97
  const status = getRegistryStatus(context?.customHome);
105
98
  renderResult(status, {
106
99
  json: options.json,
107
- envelope: options.envelope,
108
100
  humanFormatter: () => renderRegistryTree(status),
109
101
  context,
110
102
  });
@@ -118,7 +110,7 @@ export function registerInfoCommand(program, context) {
118
110
  renderProjectDetailOutput(detail, outOpts);
119
111
  return;
120
112
  }
121
- throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
113
+ throw packageNotFoundError(options.package);
122
114
  }
123
115
  // 扫描本地候选包(当前工程根目录 + 全局已链接包)
124
116
  const { currentRoot, aggregated } = scanLocalAggregatedPackages(context);
@@ -132,7 +124,7 @@ export function registerInfoCommand(program, context) {
132
124
  if (aggregated.length === 0) {
133
125
  renderResult({ linkedPackages: [] }, {
134
126
  ...outOpts,
135
- humanFormatter: () => "No ActionDock project in current directory, and no packages linked.\nRun 'ad link' inside an Action package to register it.",
127
+ humanFormatter: () => `${NO_PROJECT_NO_LINKED_MESSAGE}\nRun 'ad link' inside an Action package to register it.`,
136
128
  });
137
129
  return;
138
130
  }
@@ -1,5 +1,5 @@
1
1
  import { filterWithFallbackInfo } from "@actiondock/core";
2
- import { ArgumentError } from "../errors.js";
2
+ import { packageNotFoundError } from "../errors.js";
3
3
  import { renderActionList, renderResult } from "../renderer.js";
4
4
  import { applyTargetOptions, getEffectiveOptions, resolveFallbackStrategy, resolveIntent, resolveLocalPackageRoot, withTarget, } from "../utils/index.js";
5
5
  /**
@@ -18,7 +18,6 @@ export function attachListCommand(parent, context) {
18
18
  .option("--fallback", "Enable fallback to full list when no items match intent")
19
19
  .option("--no-fallback", "Disable fallback to full list when no items match intent")
20
20
  .option("--json", "Output as JSON")
21
- .option("--envelope", "Wrap JSON output in standard envelope")
22
21
  .option("--data-dir <path>", "Custom database storage directory")
23
22
  .action(async (patterns = [], rawOptions, cmd) => {
24
23
  const options = getEffectiveOptions(rawOptions, cmd);
@@ -27,7 +26,7 @@ export function attachListCommand(parent, context) {
27
26
  // 目标拓扑解析(仅 local 分支需要包寻址)
28
27
  const targetPackageRoot = resolveLocalPackageRoot(options.package);
29
28
  if (options.package && !targetPackageRoot) {
30
- throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
29
+ throw packageNotFoundError(options.package);
31
30
  }
32
31
  // 通过 Target 门面统一获取 Action 列表
33
32
  await withTarget(options, context, async (target, resolved) => {
@@ -56,7 +55,7 @@ export function attachListCommand(parent, context) {
56
55
  });
57
56
  const filterRes = filterWithFallbackInfo(rawList, effectiveIntent, [(a) => a.id, (a) => a.description], shouldFallback);
58
57
  if (filterRes.isFallback && isMachine) {
59
- renderResult({ items: filterRes.items, isFallback: true, matchedCount: 0 }, { json: options.json, envelope: options.envelope, context });
58
+ renderResult({ items: filterRes.items, isFallback: true, matchedCount: 0 }, { json: options.json, context });
60
59
  return;
61
60
  }
62
61
  const title = resolved.type === "remote"
@@ -66,7 +65,6 @@ export function attachListCommand(parent, context) {
66
65
  : "Available Actions";
67
66
  renderResult(filterRes.items, {
68
67
  json: options.json,
69
- envelope: options.envelope,
70
68
  humanFormatter: () => renderActionList(filterRes.items, title, filterRes.isFallback, effectiveIntent),
71
69
  context,
72
70
  });
@@ -1,7 +1,8 @@
1
1
  import { resolve } from "node:path";
2
2
  import { findProjectRoot, formatHostForUrl, loadProjectConfig, parseDuration } from "@actiondock/core";
3
+ import { createNodePlatform } from "@actiondock/runtime-node";
3
4
  import { ArgumentError, ExecutionError } from "../errors.js";
4
- import { writeStdout } from "../renderer.js";
5
+ import { writeStderr, writeStdout } from "../renderer.js";
5
6
  import { getEffectiveOptions, parseByteSize, parseListOption } from "../utils/index.js";
6
7
  /**
7
8
  * 注册 mcp 命令(STDIO 与 HTTP 传输服务模式)。
@@ -17,7 +18,7 @@ export function registerMcpCommands(program, context) {
17
18
  .option("--package <package-id>", "Specific linked package ID(s) to serve (can be specified multiple times or comma-separated)", parseListOption, [])
18
19
  .option("--all", "Serve all linked packages from global registry")
19
20
  .option("--timeout <duration>", "Execution timeout (e.g. 30s, 5m, 500ms)")
20
- .option("--allow-insecure-http", "Allow insecure HTTP connections with auth token (INSECURE)")
21
+ .option("--data-dir <path>", "Custom database storage directory")
21
22
  .action(async (rawOptions, cmd) => {
22
23
  const options = getEffectiveOptions(rawOptions, cmd);
23
24
  let timeoutMs;
@@ -41,6 +42,8 @@ export function registerMcpCommands(program, context) {
41
42
  packageIds,
42
43
  all,
43
44
  timeoutMs,
45
+ customHome: context?.customHome,
46
+ dataDir: options.dataDir || context?.dataDir,
44
47
  });
45
48
  }
46
49
  catch (err) {
@@ -63,6 +66,7 @@ export function registerMcpCommands(program, context) {
63
66
  .option("--package <package-id>", "Specific linked package ID(s) to serve (can be specified multiple times or comma-separated)", parseListOption, [])
64
67
  .option("--all", "Serve all linked packages from global registry")
65
68
  .option("--timeout <duration>", "Execution timeout (e.g. 30s, 5m, 500ms)")
69
+ .option("--data-dir <path>", "Custom database storage directory")
66
70
  .action(async (rawOptions, cmd) => {
67
71
  const options = getEffectiveOptions(rawOptions, cmd);
68
72
  const port = parseInt(options.port, 10) || 5178;
@@ -132,6 +136,11 @@ export function registerMcpCommands(program, context) {
132
136
  }
133
137
  }
134
138
  try {
139
+ const platform = createNodePlatform({
140
+ customHome: context?.customHome,
141
+ dataDir: options.dataDir || context?.dataDir,
142
+ rootDir: projectRoots?.[0],
143
+ });
135
144
  const { startMcpHttpServer } = await import("@actiondock/mcp");
136
145
  const server = await startMcpHttpServer({
137
146
  port,
@@ -146,6 +155,9 @@ export function registerMcpCommands(program, context) {
146
155
  packageIds,
147
156
  all,
148
157
  timeoutMs,
158
+ customHome: context?.customHome,
159
+ dataDir: options.dataDir || context?.dataDir,
160
+ platform,
149
161
  });
150
162
  const displayHost = formatHostForUrl(host);
151
163
  const actualMcpHost = formatHostForUrl(host === "0.0.0.0" ? "127.0.0.1" : host);
@@ -161,7 +173,15 @@ export function registerMcpCommands(program, context) {
161
173
  writeStdout(`Press Ctrl+C to terminate.\n`, context);
162
174
  const stopSignalHandler = () => {
163
175
  writeStdout("\nStopping MCP HTTP server...", context);
164
- server.stop();
176
+ Promise.resolve(server.stop())
177
+ .catch((err) => {
178
+ // 停止失败必须可见:写入 stderr 一行并标记失败退出码
179
+ writeStderr(`[ERROR] Failed to stop MCP HTTP server gracefully: ${err instanceof Error ? err.message : String(err)}`, context);
180
+ process.exitCode = 1;
181
+ })
182
+ .finally(() => {
183
+ process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
184
+ });
165
185
  };
166
186
  process.once("SIGINT", stopSignalHandler);
167
187
  process.once("SIGTERM", stopSignalHandler);
@@ -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 } from "../utils/index.js";
5
5
  export function registerPackCommand(program, context) {
@@ -17,11 +17,11 @@ export function registerPackCommand(program, context) {
17
17
  : findProjectRoot();
18
18
  if (!root) {
19
19
  if (options.package) {
20
- throw new ExecutionError(`Package '${options.package}' not found in linked packages or path`);
20
+ throw packageNotFoundError(options.package);
21
21
  }
22
- throw new ExecutionError("Not in an ActionDock project (actiondock.json not found).\nPlease specify -P, --package <id> or cd into a project directory.");
22
+ throw notInProjectError("Please specify -P, --package <id> or cd into a project directory.");
23
23
  }
24
- const isMachine = Boolean(options.json || options.envelope);
24
+ const isMachine = Boolean(options.json);
25
25
  if (!isMachine && !options.dryRun) {
26
26
  writeStdout("Packing Action package...");
27
27
  }
@@ -39,7 +39,6 @@ export function registerPackCommand(program, context) {
39
39
  }
40
40
  renderResult(result, {
41
41
  json: isMachine,
42
- envelope: options.envelope,
43
42
  humanFormatter: () => {
44
43
  const lines = [];
45
44
  if (options.dryRun) {
@@ -1,9 +1,9 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, relative, resolve } from "node:path";
3
- import { assertPathWithinRoot, fetchRemotePlaybooks, fetchRemotePlaybookShow, filterWithFallbackInfo, findProjectRoot, listLinkedPackages, loadActions, loadManifest, loadPlaybooks, loadProjectConfig, resolveActionProject, resolvePackageRoot, resolvePlaybookProject, resolveTarget, saveManifest, } from "@actiondock/core";
4
- import { ArgumentError, CliError, ExecutionError } from "../errors.js";
3
+ import { assertPathWithinRoot, fetchRemotePlaybooks, fetchRemotePlaybookShow, filterWithFallbackInfo, findProjectRoot, listLinkedPackages, loadActions, loadManifest, loadPlaybooks, loadProjectConfig, resolveActionProject, resolvePackageRoot, resolvePlaybookProject, saveManifest, } from "@actiondock/core";
4
+ import { ArgumentError, CliError, ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE, notInProjectError, packageNotFoundError, } from "../errors.js";
5
5
  import { renderPlaybookDetail, renderPlaybookList, renderResult, writeStdout, } from "../renderer.js";
6
- import { applyTargetOptions, getEffectiveOptions, resolveIntent } from "../utils/index.js";
6
+ import { applyTargetOptions, getEffectiveOptions, resolveIntent, resolveTargetFromOptions, } from "../utils/index.js";
7
7
  /**
8
8
  * 注册 Playbook 命令集合(list, show, validate, create)。
9
9
  *
@@ -22,20 +22,13 @@ export function registerPlaybookCommands(program, context) {
22
22
  .option("-P, --package <id>", "Target package ID or path"))
23
23
  .option("--no-fallback", "Disable fallback to full list when no items match intent")
24
24
  .option("--json", "Output as JSON")
25
- .option("--envelope", "Wrap JSON output in standard envelope")
26
25
  .action(async (patterns = [], rawOptions, cmd) => {
27
26
  const options = getEffectiveOptions(rawOptions, cmd);
28
27
  const effectiveIntent = resolveIntent(options.intent, patterns);
29
28
  const shouldFallback = options.fallback !== false;
30
- const isMachine = Boolean(options.json || options.envelope);
29
+ const isMachine = Boolean(options.json);
31
30
  // 1. 远端服务分支
32
- const target = resolveTarget({
33
- profile: options.profile,
34
- server: options.server,
35
- token: options.token,
36
- insecure: options.insecure,
37
- allowInsecureHttp: options.allowInsecureHttp,
38
- }, context?.customHome);
31
+ const target = resolveTargetFromOptions(options, context);
39
32
  if (target.type === "remote") {
40
33
  const remotePbs = await fetchRemotePlaybooks(target.serverUrl, target.token, {
41
34
  intent: effectiveIntent,
@@ -45,7 +38,6 @@ export function registerPlaybookCommands(program, context) {
45
38
  });
46
39
  renderResult(remotePbs, {
47
40
  json: options.json,
48
- envelope: options.envelope,
49
41
  humanFormatter: () => renderPlaybookList(remotePbs, `Playbooks on remote server ${target.serverUrl}${target.profileName ? ` (Profile: ${target.profileName})` : ""}`, false, effectiveIntent),
50
42
  context,
51
43
  });
@@ -56,7 +48,7 @@ export function registerPlaybookCommands(program, context) {
56
48
  if (options.package) {
57
49
  targetRoot = resolvePackageRoot(options.package);
58
50
  if (!targetRoot) {
59
- throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
51
+ throw packageNotFoundError(options.package);
60
52
  }
61
53
  }
62
54
  else {
@@ -72,12 +64,11 @@ export function registerPlaybookCommands(program, context) {
72
64
  }));
73
65
  const filterRes = filterWithFallbackInfo(rawList, effectiveIntent, [(p) => p.id, (p) => p.description], shouldFallback);
74
66
  if (filterRes.isFallback && isMachine) {
75
- renderResult({ items: filterRes.items, isFallback: true, matchedCount: 0 }, { json: options.json, envelope: options.envelope, context });
67
+ renderResult({ items: filterRes.items, isFallback: true, matchedCount: 0 }, { json: options.json, context });
76
68
  return;
77
69
  }
78
70
  renderResult(filterRes.items, {
79
71
  json: options.json,
80
- envelope: options.envelope,
81
72
  humanFormatter: () => renderPlaybookList(filterRes.items, `Playbooks in ${config.id} (${targetRoot})`, filterRes.isFallback, effectiveIntent),
82
73
  context,
83
74
  });
@@ -88,8 +79,7 @@ export function registerPlaybookCommands(program, context) {
88
79
  if (linkedList.length === 0) {
89
80
  renderResult([], {
90
81
  json: options.json,
91
- envelope: options.envelope,
92
- humanFormatter: () => "No ActionDock project in current directory, and no packages linked.",
82
+ humanFormatter: () => NO_PROJECT_NO_LINKED_MESSAGE,
93
83
  context,
94
84
  });
95
85
  return;
@@ -143,12 +133,11 @@ export function registerPlaybookCommands(program, context) {
143
133
  }
144
134
  }
145
135
  if (isFallback && isMachine) {
146
- renderResult({ packages: filteredPackages, isFallback: true, matchedCount: 0 }, { json: options.json, envelope: options.envelope, context });
136
+ renderResult({ packages: filteredPackages, isFallback: true, matchedCount: 0 }, { json: options.json, context });
147
137
  return;
148
138
  }
149
139
  renderResult(filteredPackages, {
150
140
  json: options.json,
151
- envelope: options.envelope,
152
141
  humanFormatter: () => {
153
142
  const lines = ["Linked Playbooks:\n"];
154
143
  for (const pkg of filteredPackages) {
@@ -168,20 +157,13 @@ export function registerPlaybookCommands(program, context) {
168
157
  .description("Show playbook content and metadata")
169
158
  .option("-P, --package <id>", "Target package ID or path"))
170
159
  .option("--json", "Output as JSON")
171
- .option("--envelope", "Wrap JSON output in standard envelope")
172
160
  .action(async (id, rawOptions, cmd) => {
173
161
  const options = getEffectiveOptions(rawOptions, cmd);
174
162
  if (!id) {
175
163
  throw new ArgumentError("Playbook ID is required for show");
176
164
  }
177
165
  // 1. 远端服务分支
178
- const target = resolveTarget({
179
- profile: options.profile,
180
- server: options.server,
181
- token: options.token,
182
- insecure: options.insecure,
183
- allowInsecureHttp: options.allowInsecureHttp,
184
- }, context?.customHome);
166
+ const target = resolveTargetFromOptions(options, context);
185
167
  if (target.type === "remote") {
186
168
  const detail = await fetchRemotePlaybookShow(target.serverUrl, id, target.token, {
187
169
  allowInsecureHttp: Boolean(options.allowInsecureHttp),
@@ -189,7 +171,6 @@ export function registerPlaybookCommands(program, context) {
189
171
  });
190
172
  renderResult(detail, {
191
173
  json: options.json,
192
- envelope: options.envelope,
193
174
  humanFormatter: () => renderPlaybookDetail(detail),
194
175
  context,
195
176
  });
@@ -200,7 +181,7 @@ export function registerPlaybookCommands(program, context) {
200
181
  if (options.package && !id.includes("/")) {
201
182
  const pkgRoot = resolvePackageRoot(options.package);
202
183
  if (!pkgRoot) {
203
- throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
184
+ throw packageNotFoundError(options.package);
204
185
  }
205
186
  showTarget = `${options.package}/${id}`;
206
187
  }
@@ -222,7 +203,6 @@ export function registerPlaybookCommands(program, context) {
222
203
  };
223
204
  renderResult(detail, {
224
205
  json: options.json,
225
- envelope: options.envelope,
226
206
  humanFormatter: () => renderPlaybookDetail(detail),
227
207
  context,
228
208
  });
@@ -233,14 +213,13 @@ export function registerPlaybookCommands(program, context) {
233
213
  .description("Validate playbook format and action references (in current project or linked packages)")
234
214
  .option("-P, --package <id>", "Target package ID or path")
235
215
  .option("--json", "Output as JSON")
236
- .option("--envelope", "Wrap JSON output in standard envelope")
237
216
  .action(async (id, rawOptions, cmd) => {
238
217
  const options = getEffectiveOptions(rawOptions, cmd);
239
218
  let root = null;
240
219
  if (options.package) {
241
220
  root = resolvePackageRoot(options.package);
242
221
  if (!root) {
243
- throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
222
+ throw packageNotFoundError(options.package);
244
223
  }
245
224
  }
246
225
  else {
@@ -278,7 +257,7 @@ export function registerPlaybookCommands(program, context) {
278
257
  else {
279
258
  const linkedList = listLinkedPackages(context?.customHome);
280
259
  if (linkedList.length === 0) {
281
- throw new ArgumentError("Not in an ActionDock project, and no packages linked.");
260
+ throw new ArgumentError(`${NO_PROJECT_NO_LINKED_MESSAGE} Please run 'ad link' inside a package first.`);
282
261
  }
283
262
  for (const pkg of linkedList) {
284
263
  if (!existsSync(pkg.path))
@@ -342,7 +321,6 @@ export function registerPlaybookCommands(program, context) {
342
321
  const allValid = results.every((r) => r.valid);
343
322
  renderResult({ valid: allValid, results }, {
344
323
  json: options.json,
345
- envelope: options.envelope,
346
324
  humanFormatter: () => {
347
325
  const lines = [];
348
326
  for (const r of results) {
@@ -378,7 +356,7 @@ export function registerPlaybookCommands(program, context) {
378
356
  export function handlePlaybookCreate(id, options, context) {
379
357
  const root = findProjectRoot();
380
358
  if (!root) {
381
- throw new ExecutionError("Not in an ActionDock project (actiondock.json not found)");
359
+ throw notInProjectError();
382
360
  }
383
361
  try {
384
362
  const config = loadProjectConfig(root);
@@ -1,6 +1,6 @@
1
1
  import { addProfile, checkRemoteHealth, filterWithFallbackInfo, getProfile, listProfiles, loadProfiles, maskSecretValue, removeProfile, resolveProfileToken, resolveTarget, toSnakeUpperCase, updateProfile, useProfile, } from "@actiondock/core";
2
2
  import { ArgumentError, CliError, ExecutionError, } from "../errors.js";
3
- import { renderResult, writeStdout } from "../renderer.js";
3
+ import { renderResult, writeStderr, writeStdout } from "../renderer.js";
4
4
  import { getEffectiveOptions, resolveFallbackStrategy, resolveIntent } from "../utils/index.js";
5
5
  export function registerProfileCommands(program) {
6
6
  const profileCmd = program
@@ -15,7 +15,6 @@ export function registerProfileCommands(program) {
15
15
  .option("--fallback", "Enable fallback to full list when no items match intent")
16
16
  .option("--no-fallback", "Disable fallback to full list when no items match intent")
17
17
  .option("--json", "Output as JSON")
18
- .option("--envelope", "Wrap JSON output in standard envelope")
19
18
  .action((patterns, rawOptions, cmd) => {
20
19
  try {
21
20
  const options = getEffectiveOptions(rawOptions, cmd);
@@ -39,12 +38,11 @@ export function registerProfileCommands(program) {
39
38
  });
40
39
  const filterRes = filterWithFallbackInfo(enriched, effectiveIntent, [(p) => p.name, (p) => p.serverUrl, (p) => p.description, (p) => p.tokenSource, (p) => (p.insecure ? "insecure" : "")], shouldFallback);
41
40
  if (filterRes.isFallback && isMachine) {
42
- renderResult({ items: filterRes.items, isFallback: true, matchedCount: 0 }, { json: options.json, envelope: options.envelope });
41
+ renderResult({ items: filterRes.items, isFallback: true, matchedCount: 0 }, { json: options.json });
43
42
  return;
44
43
  }
45
44
  renderResult(filterRes.items, {
46
45
  json: options.json,
47
- envelope: options.envelope,
48
46
  humanFormatter: () => {
49
47
  const lines = ["ActionDock Execution Profiles:\n"];
50
48
  if (filterRes.isFallback && effectiveIntent) {
@@ -95,7 +93,7 @@ export function registerProfileCommands(program) {
95
93
  .action((name, options) => {
96
94
  try {
97
95
  if (options.token) {
98
- console.warn("Warning: storing tokens directly in profiles.json is deprecated. Use --token-env or standard environment variables (e.g. ACTIONDOCK_<PROFILE>_TOKEN) instead.");
96
+ writeStderr("Warning: storing tokens directly in profiles.json is deprecated. Use --token-env or standard environment variables (e.g. ACTIONDOCK_<PROFILE>_TOKEN) instead.");
99
97
  }
100
98
  addProfile(name, {
101
99
  serverUrl: options.server,
@@ -123,7 +121,7 @@ export function registerProfileCommands(program) {
123
121
  .action((name, options) => {
124
122
  try {
125
123
  if (options.token) {
126
- console.warn("Warning: storing tokens directly in profiles.json is deprecated. Use --token-env or standard environment variables (e.g. ACTIONDOCK_<PROFILE>_TOKEN) instead.");
124
+ writeStderr("Warning: storing tokens directly in profiles.json is deprecated. Use --token-env or standard environment variables (e.g. ACTIONDOCK_<PROFILE>_TOKEN) instead.");
127
125
  }
128
126
  let insecure;
129
127
  if (options.insecure === true) {
@@ -164,7 +162,6 @@ export function registerProfileCommands(program) {
164
162
  .description("Display details of a profile (defaults to active profile)")
165
163
  .option("--reveal, --show-secrets", "Reveal plain text values for tokens")
166
164
  .option("--json", "Output as JSON")
167
- .option("--envelope", "Wrap JSON output in standard envelope")
168
165
  .action((name, rawOptions, cmd) => {
169
166
  try {
170
167
  const options = getEffectiveOptions(rawOptions, cmd);
@@ -192,7 +189,6 @@ export function registerProfileCommands(program) {
192
189
  };
193
190
  renderResult(data, {
194
191
  json: options.json,
195
- envelope: options.envelope,
196
192
  humanFormatter: () => {
197
193
  const lines = [];
198
194
  lines.push(`Profile: ${data.name}${data.isCurrent ? " (Active)" : ""}`);
@@ -261,7 +257,6 @@ export function registerProfileCommands(program) {
261
257
  .option("-k, --insecure", "Allow insecure TLS connections (skip TLS certificate validation)")
262
258
  .option("--allow-insecure-http", "Allow insecure HTTP connections with auth token")
263
259
  .option("--json", "Output as JSON")
264
- .option("--envelope", "Wrap JSON output in standard envelope")
265
260
  .action(async (name, rawOptions, cmd) => {
266
261
  try {
267
262
  const options = getEffectiveOptions(rawOptions, cmd);
@@ -273,7 +268,6 @@ export function registerProfileCommands(program) {
273
268
  if (target.type === "local") {
274
269
  renderResult({ ok: true, type: "local", message: "Local execution" }, {
275
270
  json: options.json,
276
- envelope: options.envelope,
277
271
  humanFormatter: () => `Target profile '${target.profileName || "local"}' is local (runs in local runtime).`,
278
272
  });
279
273
  return;
@@ -284,7 +278,6 @@ export function registerProfileCommands(program) {
284
278
  });
285
279
  renderResult(health, {
286
280
  json: options.json,
287
- envelope: options.envelope,
288
281
  humanFormatter: () => {
289
282
  if (health.ok) {
290
283
  return `[OK] Connected to ${target.serverUrl} (${health.latencyMs}ms) - Version: ${health.version}, Status: ${health.status}`;
@@ -1,8 +1,8 @@
1
1
  import { resolve } from "node:path";
2
2
  import { beginTransaction, findProjectRoot, getInstallCommand, loadLockfile, loadManifest, saveLockfile, saveManifest, } from "@actiondock/core";
3
- import { ArgumentError, ExecutionError } from "../errors.js";
4
- import { renderResult } from "../renderer.js";
5
- import { getEffectiveOptions, spawnAsync } from "../utils/index.js";
3
+ import { ArgumentError, ExecutionError, notInProjectError } from "../errors.js";
4
+ import { renderResult, writeStderr } from "../renderer.js";
5
+ import { assertSafePackageSpec, getEffectiveOptions, spawnAsync } from "../utils/index.js";
6
6
  /**
7
7
  * 注册 ad remove 依赖移除命令。
8
8
  * 遵循规范:移除依赖并更新锁文件,保留该包的历史存储数据,检查反向引用防止误删。
@@ -13,12 +13,11 @@ export function registerRemoveCommand(program, context) {
13
13
  .description("Remove an Action package dependency and update actiondock.lock.json")
14
14
  .option("-P, --package <path>", "Target project directory path")
15
15
  .option("--json", "Output as JSON")
16
- .option("--envelope", "Wrap JSON output in standard envelope")
17
16
  .action(async (packageIdentifier, rawOptions, cmd) => {
18
17
  const options = getEffectiveOptions(rawOptions, cmd);
19
18
  const root = options.package ? resolve(options.package) : findProjectRoot();
20
19
  if (!root) {
21
- throw new ArgumentError("Not in an ActionDock project (actiondock.json not found).\nPlease specify -P, --package <path> or cd into a project directory.");
20
+ throw notInProjectError("Please specify -P, --package <path> or cd into a project directory.");
22
21
  }
23
22
  const manifest = loadManifest(root);
24
23
  if (!manifest) {
@@ -86,7 +85,8 @@ export function registerRemoveCommand(program, context) {
86
85
  delete lockfile.packages[targetPackageId];
87
86
  saveLockfile(root, lockfile);
88
87
  }
89
- // 调用包管理器执行卸载
88
+ // 调用包管理器执行卸载(说明符白名单校验,消除 win32 shell 拼接注入面)
89
+ assertSafePackageSpec(npmPackageName);
90
90
  const installCmd = getInstallCommand(root);
91
91
  const pm = installCmd[0];
92
92
  const args = pm === "bun" ? ["remove", npmPackageName] : ["uninstall", npmPackageName, "--ignore-scripts"];
@@ -94,18 +94,22 @@ export function registerRemoveCommand(program, context) {
94
94
  cwd: root,
95
95
  stdio: "pipe",
96
96
  });
97
- // 卸载失败不阻断事务提交(与原同步实现一致:忽略退出码)
98
- void uninstallProc;
97
+ // 卸载失败不阻断事务提交,但必须可见:非零退出码输出 WARN 并在结果中标记
98
+ const uninstallFailed = uninstallProc.status !== 0;
99
+ if (uninstallFailed) {
100
+ const errMsg = uninstallProc.stderr || uninstallProc.stdout || "unknown error";
101
+ writeStderr(`[WARN] Package manager '${pm}' reported failure while uninstalling '${npmPackageName}' (exit code ${uninstallProc.status ?? "null"}): ${errMsg.trim().split("\n").pop() || ""}`, context);
102
+ }
99
103
  // 提交事务
100
104
  await tx.commit();
101
105
  const resultPayload = {
102
106
  packageId: targetPackageId,
103
107
  npmPackage: npmPackageName,
104
108
  retainedNamespace: targetPackageId,
109
+ ...(uninstallFailed ? { uninstallFailed: true } : {}),
105
110
  };
106
111
  renderResult(resultPayload, {
107
112
  json: options.json,
108
- envelope: options.envelope,
109
113
  humanFormatter: () => `Successfully removed '${resultPayload.packageId}'.\n[INFO] Retained configuration and state namespace for package '${resultPayload.packageId}'.`,
110
114
  context,
111
115
  });
@@ -1,5 +1,13 @@
1
+ import type { ExecutionResult } from "@actiondock/sdk";
1
2
  import { Command } from "commander";
2
3
  import type { CliContext } from "../types.js";
4
+ /**
5
+ * 以原始纯文本形式渲染 Action 执行终态结果(默认纯文本模式)。
6
+ *
7
+ * 设计契约:
8
+ * 1. stdout: 仅承载业务有效载荷(payload)。保持原生排版、未转义多行与真实换行,供用户阅读或下游管道直接消费。
9
+ */
10
+ export declare function renderRawExecutionResult(targetRef: string, result: ExecutionResult, context?: CliContext): void;
3
11
  /**
4
12
  * 统一执行 Action 核心逻辑(使用 ActionDockTarget 门面)。
5
13
  */