@actiondock/cli 2.5.1 → 2.7.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.
- package/README.md +60 -5
- package/bin/ad.js +4 -4
- package/dist/commands/action/create.js +7 -22
- package/dist/commands/add.js +5 -5
- package/dist/commands/build.js +4 -5
- package/dist/commands/config/delete.js +2 -2
- package/dist/commands/config/env.js +6 -15
- package/dist/commands/config/get.js +8 -39
- package/dist/commands/config/list.js +8 -55
- package/dist/commands/config/schema.js +4 -6
- package/dist/commands/config/set.js +2 -2
- package/dist/commands/describe.js +8 -22
- package/dist/commands/doctor.js +3 -10
- package/dist/commands/export.js +4 -7
- package/dist/commands/generate.js +2 -4
- package/dist/commands/index.js +2 -5
- package/dist/commands/info.js +7 -15
- package/dist/commands/list.js +3 -5
- package/dist/commands/mcp.js +10 -3
- package/dist/commands/pack.js +4 -5
- package/dist/commands/playbook.js +14 -36
- package/dist/commands/profile.js +4 -11
- package/dist/commands/remove.js +13 -9
- package/dist/commands/run.d.ts +10 -1
- package/dist/commands/run.js +161 -52
- package/dist/commands/runs.js +10 -25
- package/dist/commands/serve.js +9 -1
- package/dist/commands/state-list.js +2 -5
- package/dist/commands/state.js +29 -16
- package/dist/commands/test.js +6 -2
- package/dist/commands/validate.js +3 -5
- package/dist/errors.d.ts +14 -1
- package/dist/errors.js +53 -3
- package/dist/index.d.ts +8 -1
- package/dist/index.js +61 -10
- package/dist/renderer.d.ts +3 -4
- package/dist/renderer.js +7 -34
- package/dist/services/config-merge.d.ts +41 -0
- package/dist/services/config-merge.js +93 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/standalone/index.d.ts +8 -2
- package/dist/standalone/index.js +32 -2
- package/dist/types.d.ts +6 -3
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/input.d.ts +1 -0
- package/dist/utils/input.js +1 -0
- package/dist/utils/spawn.d.ts +13 -0
- package/dist/utils/spawn.js +31 -0
- package/dist/utils/target.d.ts +21 -0
- package/dist/utils/target.js +29 -14
- package/dist/utils/utils.d.ts +2 -3
- package/dist/utils/utils.js +6 -6
- package/package.json +6 -6
package/dist/commands/run.d.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
import { type InvocationControl } from "@actiondock/core";
|
|
2
|
+
import type { ExecutionResult } from "@actiondock/sdk";
|
|
1
3
|
import { Command } from "commander";
|
|
2
4
|
import type { CliContext } from "../types.js";
|
|
5
|
+
/**
|
|
6
|
+
* 以原始纯文本形式渲染 Action 执行终态结果(默认纯文本模式)。
|
|
7
|
+
*
|
|
8
|
+
* 设计契约:
|
|
9
|
+
* 1. stdout: 仅承载业务有效载荷(payload)。保持原生排版、未转义多行与真实换行,供用户阅读或下游管道直接消费。
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderRawExecutionResult(targetRef: string, result: ExecutionResult, context?: CliContext): void;
|
|
3
12
|
/**
|
|
4
13
|
* 统一执行 Action 核心逻辑(使用 ActionDockTarget 门面)。
|
|
5
14
|
*/
|
|
6
|
-
export declare function executeAction(id: string, options: any, context?: CliContext): Promise<void>;
|
|
15
|
+
export declare function executeAction(id: string, options: any, context?: CliContext, flatArgs?: string[], control?: InvocationControl): Promise<void>;
|
|
7
16
|
/**
|
|
8
17
|
* 挂载 run 子命令至指定 Commander 节点。
|
|
9
18
|
*
|
package/dist/commands/run.js
CHANGED
|
@@ -1,32 +1,119 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { applyTargetOptions, getEffectiveOptions, resolveLocalPackageRoot, withTarget, } from "../utils/index.js";
|
|
1
|
+
import { parseDuration, resolveTarget, validateActionInputValue, mapInputValidationFailure, } from "@actiondock/core";
|
|
2
|
+
import { ArgumentError, ExecutionError, SigintError, packageNotFoundError } from "../errors.js";
|
|
3
|
+
import { writeStderr, writeStdout } from "../renderer.js";
|
|
4
|
+
import { applyTargetOptions, getEffectiveOptions, resolveActionInput, resolveLocalPackageRoot, withTarget, } from "../utils/index.js";
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
6
|
+
* 以原始纯文本形式渲染 Action 执行终态结果(默认纯文本模式)。
|
|
7
|
+
*
|
|
8
|
+
* 设计契约:
|
|
9
|
+
* 1. stdout: 仅承载业务有效载荷(payload)。保持原生排版、未转义多行与真实换行,供用户阅读或下游管道直接消费。
|
|
8
10
|
*/
|
|
9
|
-
export
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
input = JSON.parse(options.input);
|
|
11
|
+
export function renderRawExecutionResult(targetRef, result, context) {
|
|
12
|
+
if (result.ok) {
|
|
13
|
+
const data = result.data;
|
|
14
|
+
let rawText;
|
|
15
|
+
let metaInfo;
|
|
16
|
+
if (typeof data === "string") {
|
|
17
|
+
rawText = data;
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
else if (data !== null && typeof data === "object") {
|
|
20
|
+
if ("content" in data && data.content !== undefined) {
|
|
21
|
+
rawText =
|
|
22
|
+
typeof data.content === "object" && data.content !== null
|
|
23
|
+
? JSON.stringify(data.content, null, 2)
|
|
24
|
+
: String(data.content);
|
|
25
|
+
const { content, ...rest } = data;
|
|
26
|
+
if (Object.keys(rest).length > 0) {
|
|
27
|
+
metaInfo = rest;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if ("text" in data && typeof data.text === "string") {
|
|
31
|
+
rawText = data.text;
|
|
32
|
+
const { text, ...rest } = data;
|
|
33
|
+
if (Object.keys(rest).length > 0) {
|
|
34
|
+
metaInfo = rest;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if ("message" in data && typeof data.message === "string") {
|
|
38
|
+
rawText = data.message;
|
|
39
|
+
const { message, ...rest } = data;
|
|
40
|
+
if (Object.keys(rest).length > 0) {
|
|
41
|
+
metaInfo = rest;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
rawText = JSON.stringify(data, null, 2);
|
|
46
|
+
}
|
|
20
47
|
}
|
|
48
|
+
else if (data !== undefined) {
|
|
49
|
+
rawText = String(data);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
rawText = "";
|
|
53
|
+
}
|
|
54
|
+
if (metaInfo) {
|
|
55
|
+
const parts = [];
|
|
56
|
+
const title = metaInfo.path ? String(metaInfo.path) : targetRef;
|
|
57
|
+
parts.push(title);
|
|
58
|
+
if (metaInfo.startLine !== undefined && metaInfo.endLine !== undefined) {
|
|
59
|
+
parts.push(`lines ${metaInfo.startLine}-${metaInfo.endLine}`);
|
|
60
|
+
}
|
|
61
|
+
else if (metaInfo.line !== undefined) {
|
|
62
|
+
parts.push(`line ${metaInfo.line}`);
|
|
63
|
+
}
|
|
64
|
+
if (metaInfo.hasMore !== undefined) {
|
|
65
|
+
parts.push(`hasMore: ${metaInfo.hasMore}`);
|
|
66
|
+
}
|
|
67
|
+
if (metaInfo.truncated) {
|
|
68
|
+
parts.push("truncated: true");
|
|
69
|
+
}
|
|
70
|
+
const handled = new Set(["path", "startLine", "endLine", "line", "hasMore", "truncated"]);
|
|
71
|
+
for (const [k, v] of Object.entries(metaInfo)) {
|
|
72
|
+
if (!handled.has(k) && v !== undefined) {
|
|
73
|
+
parts.push(`${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (parts.length > 0) {
|
|
77
|
+
writeStderr(`[${parts.join(" | ")}]`, context);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
writeStdout(rawText, context);
|
|
21
81
|
}
|
|
22
|
-
else
|
|
23
|
-
|
|
24
|
-
|
|
82
|
+
else {
|
|
83
|
+
writeStderr(`Error [${result.error.code}]: ${result.error.message}`, context);
|
|
84
|
+
if (result.error.details) {
|
|
85
|
+
writeStderr(typeof result.error.details === "string"
|
|
86
|
+
? result.error.details
|
|
87
|
+
: JSON.stringify(result.error.details, null, 2), context);
|
|
25
88
|
}
|
|
26
|
-
|
|
27
|
-
|
|
89
|
+
if (context) {
|
|
90
|
+
context.exitCode = 1;
|
|
91
|
+
}
|
|
92
|
+
if (!context?.control) {
|
|
93
|
+
process.exitCode = 1;
|
|
28
94
|
}
|
|
29
95
|
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 统一执行 Action 核心逻辑(使用 ActionDockTarget 门面)。
|
|
99
|
+
*/
|
|
100
|
+
export async function executeAction(id, options, context, flatArgs, control) {
|
|
101
|
+
if (!id) {
|
|
102
|
+
throw new ArgumentError("Action ID is required for run");
|
|
103
|
+
}
|
|
104
|
+
const effectiveControl = control ?? context?.control;
|
|
105
|
+
const effectiveSignal = effectiveControl?.signal;
|
|
106
|
+
const effectiveFlatArgs = flatArgs ?? options.flatArgs;
|
|
107
|
+
const input = await resolveActionInput({
|
|
108
|
+
input: options.input,
|
|
109
|
+
inputFile: options.inputFile,
|
|
110
|
+
flatArgs: effectiveFlatArgs && effectiveFlatArgs.length > 0 ? effectiveFlatArgs : undefined,
|
|
111
|
+
stdin: context?.stdin,
|
|
112
|
+
});
|
|
113
|
+
const check = validateActionInputValue(input);
|
|
114
|
+
if (!check.valid) {
|
|
115
|
+
throw mapInputValidationFailure("cli-pre-target", check);
|
|
116
|
+
}
|
|
30
117
|
let timeoutMs;
|
|
31
118
|
if (options.timeout) {
|
|
32
119
|
try {
|
|
@@ -45,13 +132,6 @@ export async function executeAction(id, options, context) {
|
|
|
45
132
|
configOverrides[k] = v.join("=");
|
|
46
133
|
}
|
|
47
134
|
}
|
|
48
|
-
const controller = new AbortController();
|
|
49
|
-
let receivedSigint = false;
|
|
50
|
-
const sigintHandler = () => {
|
|
51
|
-
receivedSigint = true;
|
|
52
|
-
controller.abort(new Error("Interrupted by SIGINT"));
|
|
53
|
-
};
|
|
54
|
-
process.once("SIGINT", sigintHandler);
|
|
55
135
|
try {
|
|
56
136
|
// 异步执行仅支持远端目标,本地模式直接拒绝
|
|
57
137
|
const resolvedTopology = resolveTarget({
|
|
@@ -66,7 +146,7 @@ export async function executeAction(id, options, context) {
|
|
|
66
146
|
// 目标拓扑解析(仅 local 分支需要包寻址)
|
|
67
147
|
const targetPackageRoot = resolveLocalPackageRoot(options.package);
|
|
68
148
|
if (options.package && !targetPackageRoot) {
|
|
69
|
-
throw
|
|
149
|
+
throw packageNotFoundError(options.package);
|
|
70
150
|
}
|
|
71
151
|
let targetRef = id;
|
|
72
152
|
if (options.package && !id.includes("/") && !id.includes(":")) {
|
|
@@ -74,48 +154,68 @@ export async function executeAction(id, options, context) {
|
|
|
74
154
|
}
|
|
75
155
|
// 通过 Target 统一执行
|
|
76
156
|
await withTarget(options, context, async (target) => {
|
|
157
|
+
const isMachine = Boolean(options.json);
|
|
77
158
|
if (options.async) {
|
|
78
159
|
const ticket = await target.startAction(targetRef, input, {
|
|
79
|
-
signal:
|
|
160
|
+
signal: effectiveSignal,
|
|
80
161
|
timeoutMs,
|
|
81
162
|
config: configOverrides,
|
|
82
163
|
requestId: options.requestId,
|
|
83
164
|
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
165
|
+
if (isMachine) {
|
|
166
|
+
const asyncOutput = {
|
|
167
|
+
ok: ticket.status !== "failed",
|
|
168
|
+
runId: ticket.runId,
|
|
169
|
+
status: ticket.status,
|
|
170
|
+
};
|
|
171
|
+
writeStdout(JSON.stringify(asyncOutput, null, 2), context);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
writeStderr(`[${ticket.status}] runId: ${ticket.runId}`, context);
|
|
175
|
+
writeStdout(ticket.runId, context);
|
|
176
|
+
}
|
|
90
177
|
if (ticket.status === "failed") {
|
|
91
|
-
|
|
178
|
+
if (context) {
|
|
179
|
+
context.exitCode = 1;
|
|
180
|
+
}
|
|
92
181
|
return;
|
|
93
182
|
}
|
|
94
183
|
}
|
|
95
184
|
else {
|
|
96
185
|
const result = await target.runAction(targetRef, input, {
|
|
97
|
-
signal:
|
|
186
|
+
signal: effectiveSignal,
|
|
98
187
|
timeoutMs,
|
|
99
188
|
config: configOverrides,
|
|
100
189
|
requestId: options.requestId,
|
|
101
190
|
});
|
|
102
|
-
|
|
191
|
+
if (effectiveControl?.cancellationSource === "sigint" &&
|
|
192
|
+
(effectiveSignal?.aborted || (!result.ok && result.error?.code === "ACTION_CANCELLED"))) {
|
|
193
|
+
throw new SigintError();
|
|
194
|
+
}
|
|
195
|
+
if (isMachine) {
|
|
196
|
+
writeStdout(JSON.stringify(result, null, 2), context);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
renderRawExecutionResult(targetRef, result, context);
|
|
200
|
+
}
|
|
103
201
|
if (!result.ok) {
|
|
104
|
-
|
|
202
|
+
if (context) {
|
|
203
|
+
context.exitCode = 1;
|
|
204
|
+
}
|
|
105
205
|
return;
|
|
106
206
|
}
|
|
107
207
|
}
|
|
108
|
-
}, { localRoot: targetPackageRoot || undefined, scanLinkedPackages: true });
|
|
208
|
+
}, { localRoot: targetPackageRoot || undefined, scanLinkedPackages: true, ownDataDir: true });
|
|
109
209
|
}
|
|
110
210
|
catch (err) {
|
|
111
|
-
|
|
211
|
+
const isSigint = effectiveControl?.cancellationSource === "sigint" ||
|
|
212
|
+
err?.message?.includes("Interrupted by SIGINT");
|
|
213
|
+
if (isSigint &&
|
|
214
|
+
(err?.name === "AbortError" || effectiveSignal?.aborted || err?.message?.includes("SIGINT") || err instanceof SigintError)) {
|
|
112
215
|
throw new SigintError();
|
|
113
216
|
}
|
|
114
217
|
throw err;
|
|
115
218
|
}
|
|
116
|
-
finally {
|
|
117
|
-
process.removeListener("SIGINT", sigintHandler);
|
|
118
|
-
}
|
|
119
219
|
}
|
|
120
220
|
/**
|
|
121
221
|
* 挂载 run 子命令至指定 Commander 节点。
|
|
@@ -125,11 +225,11 @@ export async function executeAction(id, options, context) {
|
|
|
125
225
|
*/
|
|
126
226
|
export function attachRunCommand(parent, context) {
|
|
127
227
|
const cmd = parent
|
|
128
|
-
.command("run <id>")
|
|
228
|
+
.command("run <id> [params...]")
|
|
129
229
|
.description("Execute an action (from current project, linked packages, or remote profile)")
|
|
130
230
|
.option("-P, --package <id>", "Target package ID or path")
|
|
131
|
-
.option("-i, --input <json>", "
|
|
132
|
-
.option("-f, --input-file <path>", "
|
|
231
|
+
.option("-i, --input <json>", "Action input as inline JSON")
|
|
232
|
+
.option("-f, --input-file <path>", "Action input from JSON file, or '-' for stdin")
|
|
133
233
|
.option("-c, --config <key=value...>", "Temporary config override (repeatable)");
|
|
134
234
|
return applyTargetOptions(cmd)
|
|
135
235
|
.option("--timeout <duration>", "Execution timeout (e.g. 30s, 5m, 500ms)")
|
|
@@ -137,10 +237,19 @@ export function attachRunCommand(parent, context) {
|
|
|
137
237
|
.option("--async", "Execute asynchronously in background (requires remote server or profile)")
|
|
138
238
|
.option("--data-dir <path>", "Custom database directory")
|
|
139
239
|
.option("--json", "Output as JSON")
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
240
|
+
.action(async (id, params, rawOptions, cmd) => {
|
|
241
|
+
let flatArgs;
|
|
242
|
+
let effectiveRawOptions = rawOptions;
|
|
243
|
+
let effectiveCmd = cmd;
|
|
244
|
+
if (Array.isArray(params)) {
|
|
245
|
+
flatArgs = params.length > 0 ? params : undefined;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
effectiveCmd = rawOptions;
|
|
249
|
+
effectiveRawOptions = params;
|
|
250
|
+
}
|
|
251
|
+
const options = getEffectiveOptions(effectiveRawOptions, effectiveCmd);
|
|
252
|
+
await executeAction(id, { ...options, flatArgs }, context, undefined, context?.control);
|
|
144
253
|
});
|
|
145
254
|
}
|
|
146
255
|
/**
|
package/dist/commands/runs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { filterWithFallbackInfo, findProjectRoot, listLinkedPackages, loadProjectConfig, resolvePackageRoot,
|
|
2
|
-
import { ArgumentError, ExecutionError } from "../errors.js";
|
|
1
|
+
import { filterWithFallbackInfo, findProjectRoot, listLinkedPackages, loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
|
|
2
|
+
import { ArgumentError, ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE, notInProjectError, packageNotFoundError, } from "../errors.js";
|
|
3
3
|
import { renderResult, renderRunDetail, renderRunsList } from "../renderer.js";
|
|
4
|
-
import { applyTargetOptions, getEffectiveOptions, resolveIntent, withRemoteTarget, withTarget, } from "../utils/index.js";
|
|
4
|
+
import { applyTargetOptions, getEffectiveOptions, resolveIntent, resolveTargetFromOptions, withRemoteTarget, withTarget, } from "../utils/index.js";
|
|
5
5
|
/**
|
|
6
6
|
* 解析本地目标包根目录与工程配置(仅在 local 分支调用)。
|
|
7
7
|
* 显式指定包且寻址失败时抛出参数错误;工程清单损坏时降级为链接包视图。
|
|
@@ -10,7 +10,7 @@ function resolveLocalRunScope(packageOption) {
|
|
|
10
10
|
if (packageOption) {
|
|
11
11
|
const root = resolvePackageRoot(packageOption);
|
|
12
12
|
if (!root) {
|
|
13
|
-
throw
|
|
13
|
+
throw packageNotFoundError(packageOption);
|
|
14
14
|
}
|
|
15
15
|
try {
|
|
16
16
|
return { targetPackageRoot: root, projConfig: loadProjectConfig(root) };
|
|
@@ -53,7 +53,6 @@ export function registerRunsCommands(program, context) {
|
|
|
53
53
|
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
54
54
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
55
55
|
.option("--json", "Output as JSON")
|
|
56
|
-
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
57
56
|
.action(async (patterns = [], rawOptions, cmd) => {
|
|
58
57
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
59
58
|
const effectiveIntent = resolveIntent(options.intent, patterns);
|
|
@@ -63,10 +62,9 @@ export function registerRunsCommands(program, context) {
|
|
|
63
62
|
// 通过 Target 门面统一访问
|
|
64
63
|
await withTarget(options, context, async (target, resolved) => {
|
|
65
64
|
const records = await target.listRuns({
|
|
66
|
-
packageId:
|
|
65
|
+
packageId: options.package,
|
|
67
66
|
actionId: options.action,
|
|
68
|
-
|
|
69
|
-
limit,
|
|
67
|
+
limit: 500,
|
|
70
68
|
});
|
|
71
69
|
// 若处于本地无项目环境且无任何软链接包,则直接输出友好提示
|
|
72
70
|
if (resolved.type === "local" && !scope.targetPackageRoot) {
|
|
@@ -74,8 +72,7 @@ export function registerRunsCommands(program, context) {
|
|
|
74
72
|
if (linked.length === 0) {
|
|
75
73
|
renderResult([], {
|
|
76
74
|
json: options.json,
|
|
77
|
-
|
|
78
|
-
humanFormatter: () => "No ActionDock project in current directory, and no packages linked.",
|
|
75
|
+
humanFormatter: () => NO_PROJECT_NO_LINKED_MESSAGE,
|
|
79
76
|
context,
|
|
80
77
|
});
|
|
81
78
|
return;
|
|
@@ -85,7 +82,6 @@ export function registerRunsCommands(program, context) {
|
|
|
85
82
|
const capped = filterRes.items.slice(0, limit);
|
|
86
83
|
renderResult(capped, {
|
|
87
84
|
json: options.json,
|
|
88
|
-
envelope: options.envelope,
|
|
89
85
|
humanFormatter: () => {
|
|
90
86
|
let title = "Execution Runs";
|
|
91
87
|
if (resolved.type === "remote") {
|
|
@@ -110,7 +106,6 @@ export function registerRunsCommands(program, context) {
|
|
|
110
106
|
.option("-P, --package <id>", "Target package ID or path"))
|
|
111
107
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
112
108
|
.option("--json", "Output as JSON")
|
|
113
|
-
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
114
109
|
.action(async (id, rawOptions, cmd) => {
|
|
115
110
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
116
111
|
if (!id) {
|
|
@@ -133,7 +128,6 @@ export function registerRunsCommands(program, context) {
|
|
|
133
128
|
}
|
|
134
129
|
renderResult(run, {
|
|
135
130
|
json: options.json,
|
|
136
|
-
envelope: options.envelope,
|
|
137
131
|
humanFormatter: () => renderRunDetail(run),
|
|
138
132
|
context,
|
|
139
133
|
});
|
|
@@ -145,18 +139,12 @@ export function registerRunsCommands(program, context) {
|
|
|
145
139
|
.description("Cancel a running action execution on a remote server"))
|
|
146
140
|
.option("-r, --reason <reason>", "Reason for cancellation")
|
|
147
141
|
.option("--json", "Output as JSON")
|
|
148
|
-
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
149
142
|
.action(async (id, rawOptions, cmd) => {
|
|
150
143
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
151
144
|
if (!id) {
|
|
152
145
|
throw new ArgumentError("Run ID is required for cancel");
|
|
153
146
|
}
|
|
154
|
-
const resolved =
|
|
155
|
-
profile: options.profile,
|
|
156
|
-
server: options.server,
|
|
157
|
-
token: options.token,
|
|
158
|
-
insecure: options.insecure,
|
|
159
|
-
}, context?.customHome);
|
|
147
|
+
const resolved = resolveTargetFromOptions(options, context);
|
|
160
148
|
if (resolved.type === "local") {
|
|
161
149
|
throw new ArgumentError("'ad runs cancel' is only supported for remote execution targets. Use --profile <name> or --server <url>.");
|
|
162
150
|
}
|
|
@@ -165,7 +153,6 @@ export function registerRunsCommands(program, context) {
|
|
|
165
153
|
const isErrorOutcome = result.outcome === "not_found" || result.outcome === "not_owner";
|
|
166
154
|
renderResult(result, {
|
|
167
155
|
json: options.json,
|
|
168
|
-
envelope: options.envelope,
|
|
169
156
|
humanFormatter: () => {
|
|
170
157
|
if (result.outcome === "not_found") {
|
|
171
158
|
return `Error: Run record '${id}' not found on remote server.`;
|
|
@@ -190,7 +177,6 @@ export function registerRunsCommands(program, context) {
|
|
|
190
177
|
.option("-a, --action <actionId>", "Filter by action ID"))
|
|
191
178
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
192
179
|
.option("--json", "Output as JSON")
|
|
193
|
-
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
194
180
|
.action(async (rawOptions, cmd) => {
|
|
195
181
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
196
182
|
let targetPackageRoot;
|
|
@@ -199,14 +185,14 @@ export function registerRunsCommands(program, context) {
|
|
|
199
185
|
if (options.package) {
|
|
200
186
|
const root = resolvePackageRoot(options.package);
|
|
201
187
|
if (!root) {
|
|
202
|
-
throw
|
|
188
|
+
throw packageNotFoundError(options.package);
|
|
203
189
|
}
|
|
204
190
|
targetPackageRoot = root;
|
|
205
191
|
}
|
|
206
192
|
else {
|
|
207
193
|
targetPackageRoot = findProjectRoot() || undefined;
|
|
208
194
|
if (!targetPackageRoot) {
|
|
209
|
-
throw
|
|
195
|
+
throw notInProjectError("Please specify -P, --package <id> or cd into a project directory.");
|
|
210
196
|
}
|
|
211
197
|
}
|
|
212
198
|
try {
|
|
@@ -223,7 +209,6 @@ export function registerRunsCommands(program, context) {
|
|
|
223
209
|
const payload = { ok: true, clearedCount: count };
|
|
224
210
|
renderResult(payload, {
|
|
225
211
|
json: options.json,
|
|
226
|
-
envelope: options.envelope,
|
|
227
212
|
humanFormatter: () => resolved.type === "remote"
|
|
228
213
|
? `Cleared ${count} execution run(s) on remote server.`
|
|
229
214
|
: `Cleared ${count} execution run(s) in package '${packageId}'.`,
|
package/dist/commands/serve.js
CHANGED
|
@@ -192,7 +192,15 @@ export function registerServeCommand(program, context) {
|
|
|
192
192
|
writeStdout(`Press Ctrl+C to terminate.\n`, context);
|
|
193
193
|
const stopSignalHandler = () => {
|
|
194
194
|
writeStdout("\nStopping ActionDock server...", context);
|
|
195
|
-
server.stop()
|
|
195
|
+
Promise.resolve(server.stop())
|
|
196
|
+
.catch((err) => {
|
|
197
|
+
// 停止失败必须可见:写入 stderr 一行并标记失败退出码
|
|
198
|
+
writeStderr(`[ERROR] Failed to stop ActionDock server gracefully: ${err instanceof Error ? err.message : String(err)}`, context);
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
})
|
|
201
|
+
.finally(() => {
|
|
202
|
+
process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
|
|
203
|
+
});
|
|
196
204
|
};
|
|
197
205
|
process.once("SIGINT", stopSignalHandler);
|
|
198
206
|
process.once("SIGTERM", stopSignalHandler);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { filterWithFallbackInfo, listLinkedPackages, loadProjectConfig, } from "@actiondock/core";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { NO_PROJECT_NO_LINKED_MESSAGE } from "../errors.js";
|
|
3
4
|
import { renderResult, renderStateList } from "../renderer.js";
|
|
4
5
|
/**
|
|
5
6
|
* 渲染单个项目作用域的状态键列表(含 --detail JSON 明细分支)。
|
|
@@ -25,7 +26,6 @@ export async function renderProjectScopedStateList(args) {
|
|
|
25
26
|
const filterRes = filterWithFallbackInfo(entries, effectiveIntent, [(e) => e.fullKey, (e) => e.key, (e) => e.namespace], shouldFallback);
|
|
26
27
|
renderResult(filterRes.items, {
|
|
27
28
|
json: true,
|
|
28
|
-
envelope: options.envelope,
|
|
29
29
|
context,
|
|
30
30
|
});
|
|
31
31
|
return;
|
|
@@ -33,7 +33,6 @@ export async function renderProjectScopedStateList(args) {
|
|
|
33
33
|
const filterRes = filterWithFallbackInfo(allKeys, effectiveIntent, [(k) => k], shouldFallback);
|
|
34
34
|
renderResult(filterRes.items, {
|
|
35
35
|
json: options.json,
|
|
36
|
-
envelope: options.envelope,
|
|
37
36
|
humanFormatter: () => renderStateList(filterRes.items, `${projConfig.name} (${projConfig.id})`, filterRes.isFallback, effectiveIntent),
|
|
38
37
|
context,
|
|
39
38
|
});
|
|
@@ -48,8 +47,7 @@ export async function renderLinkedPackagesStateList(args) {
|
|
|
48
47
|
if (linked.length === 0) {
|
|
49
48
|
renderResult([], {
|
|
50
49
|
json: options.json,
|
|
51
|
-
|
|
52
|
-
humanFormatter: () => "No ActionDock project in current directory, and no packages linked.",
|
|
50
|
+
humanFormatter: () => NO_PROJECT_NO_LINKED_MESSAGE,
|
|
53
51
|
context,
|
|
54
52
|
});
|
|
55
53
|
return;
|
|
@@ -75,7 +73,6 @@ export async function renderLinkedPackagesStateList(args) {
|
|
|
75
73
|
const filterRes = filterWithFallbackInfo(aggregatedKeys, effectiveIntent, [(k) => k], shouldFallback);
|
|
76
74
|
renderResult(filterRes.items, {
|
|
77
75
|
json: options.json,
|
|
78
|
-
envelope: options.envelope,
|
|
79
76
|
humanFormatter: () => renderStateList(filterRes.items, "Linked Packages State Store", filterRes.isFallback, effectiveIntent),
|
|
80
77
|
context,
|
|
81
78
|
});
|
package/dist/commands/state.js
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { decodeStateKey, findProjectRoot, loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
|
|
2
|
-
import { ArgumentError, ExecutionError } from "../errors.js";
|
|
3
|
-
import { renderResult, renderStateList, writeStdout } from "../renderer.js";
|
|
2
|
+
import { ArgumentError, ExecutionError, packageNotFoundError } from "../errors.js";
|
|
3
|
+
import { renderResult, renderStateList, writeStderr, writeStdout } from "../renderer.js";
|
|
4
4
|
import { applyTargetOptions, getEffectiveOptions, getTargetRoot, resolveIntent, withTarget, } from "../utils/index.js";
|
|
5
5
|
import { renderLinkedPackagesStateList, renderProjectScopedStateList, } from "./state-list.js";
|
|
6
|
+
/**
|
|
7
|
+
* 解析复合状态键为命名空间与裸键视图。
|
|
8
|
+
*
|
|
9
|
+
* 多冒号歧义键(如 `review:owner/repo:42`)不再直接崩溃:
|
|
10
|
+
* 降级为「整串作为裸 key」并在 stderr 提示可用 -n/--namespace 显式指定,
|
|
11
|
+
* 保持与本地存储层歧义兜底(deleteStateSmart 的纯键回退)一致的容错语义。
|
|
12
|
+
*/
|
|
13
|
+
function decodeStateKeyWithFallback(rawKey, hasExplicitNamespace, context) {
|
|
14
|
+
try {
|
|
15
|
+
const decoded = decodeStateKey(rawKey);
|
|
16
|
+
return { namespace: decoded.namespace || undefined, key: decoded.key };
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
if (!hasExplicitNamespace) {
|
|
20
|
+
writeStderr(`[WARN] State key '${rawKey}' contains multiple colon delimiters; treating it as a bare key. Use -n/--namespace to specify the namespace explicitly.`, context);
|
|
21
|
+
}
|
|
22
|
+
return { namespace: undefined, key: rawKey };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
6
25
|
/**
|
|
7
26
|
* 注册 state 状态管理命令(get、set、delete、clear、keys、list)。
|
|
8
27
|
*
|
|
@@ -28,7 +47,6 @@ export function registerStateCommands(program, context) {
|
|
|
28
47
|
});
|
|
29
48
|
renderResult(keys, {
|
|
30
49
|
json: options.json,
|
|
31
|
-
envelope: options.envelope,
|
|
32
50
|
humanFormatter: () => renderStateList(keys, `Remote Server ${resolved.serverUrl}${resolved.profileName ? ` (Profile: ${resolved.profileName})` : ""}`, false, effectiveIntent),
|
|
33
51
|
context,
|
|
34
52
|
});
|
|
@@ -39,7 +57,7 @@ export function registerStateCommands(program, context) {
|
|
|
39
57
|
if (options.package) {
|
|
40
58
|
targetRoot = resolvePackageRoot(options.package);
|
|
41
59
|
if (!targetRoot) {
|
|
42
|
-
throw
|
|
60
|
+
throw packageNotFoundError(options.package);
|
|
43
61
|
}
|
|
44
62
|
}
|
|
45
63
|
else {
|
|
@@ -81,7 +99,6 @@ export function registerStateCommands(program, context) {
|
|
|
81
99
|
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
82
100
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
83
101
|
.option("--json", "Output as JSON")
|
|
84
|
-
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
85
102
|
.action(handleListKeys);
|
|
86
103
|
// state keys 别名
|
|
87
104
|
applyTargetOptions(stateCmd
|
|
@@ -95,7 +112,6 @@ export function registerStateCommands(program, context) {
|
|
|
95
112
|
.option("--no-fallback", "Disable fallback")
|
|
96
113
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
97
114
|
.option("--json", "Output as JSON")
|
|
98
|
-
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
99
115
|
.action(handleListKeys);
|
|
100
116
|
// state get <key>
|
|
101
117
|
applyTargetOptions(stateCmd
|
|
@@ -106,7 +122,6 @@ export function registerStateCommands(program, context) {
|
|
|
106
122
|
.option("-n, --namespace <ns>", "Explicit namespace scope for key"))
|
|
107
123
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
108
124
|
.option("--json", "Output as JSON")
|
|
109
|
-
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
110
125
|
.action(async (rawKey, rawOptions, cmd) => {
|
|
111
126
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
112
127
|
if (!rawKey) {
|
|
@@ -115,8 +130,8 @@ export function registerStateCommands(program, context) {
|
|
|
115
130
|
await withTarget(options, context, async (target, resolved) => {
|
|
116
131
|
const actionId = options.action || "";
|
|
117
132
|
if (resolved.type === "remote") {
|
|
118
|
-
const decoded =
|
|
119
|
-
const effectiveNamespace = options.namespace ||
|
|
133
|
+
const decoded = decodeStateKeyWithFallback(rawKey, options.namespace !== undefined, context);
|
|
134
|
+
const effectiveNamespace = options.namespace || decoded.namespace;
|
|
120
135
|
const actualKey = options.namespace ? rawKey : decoded.key;
|
|
121
136
|
const entry = await target.getState(options.package || "", actionId, actualKey, {
|
|
122
137
|
namespace: effectiveNamespace,
|
|
@@ -128,7 +143,6 @@ export function registerStateCommands(program, context) {
|
|
|
128
143
|
const val = entry.value;
|
|
129
144
|
renderResult({ key: rawKey, value: val, namespace: entry.namespace || effectiveNamespace }, {
|
|
130
145
|
json: options.json,
|
|
131
|
-
envelope: options.envelope,
|
|
132
146
|
humanFormatter: () => (typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)),
|
|
133
147
|
context,
|
|
134
148
|
});
|
|
@@ -148,7 +162,6 @@ export function registerStateCommands(program, context) {
|
|
|
148
162
|
const matchedNamespace = entry.namespace;
|
|
149
163
|
renderResult({ key: rawKey, value: val, namespace: matchedNamespace }, {
|
|
150
164
|
json: options.json,
|
|
151
|
-
envelope: options.envelope,
|
|
152
165
|
humanFormatter: () => (typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)),
|
|
153
166
|
context,
|
|
154
167
|
});
|
|
@@ -182,8 +195,8 @@ export function registerStateCommands(program, context) {
|
|
|
182
195
|
await withTarget(options, context, async (target, resolved) => {
|
|
183
196
|
const actionId = options.action || "";
|
|
184
197
|
if (resolved.type === "remote") {
|
|
185
|
-
const decoded =
|
|
186
|
-
const effectiveNamespace = options.namespace ||
|
|
198
|
+
const decoded = decodeStateKeyWithFallback(rawKey, options.namespace !== undefined, context);
|
|
199
|
+
const effectiveNamespace = options.namespace || decoded.namespace;
|
|
187
200
|
const actualKey = options.namespace ? rawKey : decoded.key;
|
|
188
201
|
await target.setState(options.package || "", actionId, actualKey, parsedVal, {
|
|
189
202
|
namespace: effectiveNamespace,
|
|
@@ -198,7 +211,7 @@ export function registerStateCommands(program, context) {
|
|
|
198
211
|
let actualNamespace = options.namespace;
|
|
199
212
|
let finalKey = effectiveKey;
|
|
200
213
|
if (options.namespace === undefined && effectiveKey.includes(":")) {
|
|
201
|
-
const decoded =
|
|
214
|
+
const decoded = decodeStateKeyWithFallback(effectiveKey, false, context);
|
|
202
215
|
actualNamespace = decoded.namespace;
|
|
203
216
|
finalKey = decoded.key;
|
|
204
217
|
}
|
|
@@ -227,8 +240,8 @@ export function registerStateCommands(program, context) {
|
|
|
227
240
|
await withTarget(options, context, async (target, resolved) => {
|
|
228
241
|
const actionId = options.action || "";
|
|
229
242
|
if (resolved.type === "remote") {
|
|
230
|
-
const decoded =
|
|
231
|
-
const effectiveNamespace = options.namespace ||
|
|
243
|
+
const decoded = decodeStateKeyWithFallback(rawKey, options.namespace !== undefined, context);
|
|
244
|
+
const effectiveNamespace = options.namespace || decoded.namespace;
|
|
232
245
|
const actualKey = options.namespace ? rawKey : decoded.key;
|
|
233
246
|
const deleted = await target.deleteState(options.package || "", actionId, actualKey, {
|
|
234
247
|
namespace: effectiveNamespace,
|
package/dist/commands/test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { findProjectRoot } from "@actiondock/core";
|
|
4
|
+
import { writeStderr } from "../renderer.js";
|
|
4
5
|
import { spawnAsync } from "../utils/index.js";
|
|
5
6
|
export function registerTestCommand(program) {
|
|
6
7
|
program
|
|
@@ -32,11 +33,14 @@ export function registerTestCommand(program) {
|
|
|
32
33
|
testArgs.push(pattern);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
// stdio inherit
|
|
36
|
+
// stdio inherit 保持实时输出;进程异常(如命令不存在)时透传错误消息并以退出码 1 失败
|
|
36
37
|
const proc = await spawnAsync(testCmd, testArgs, {
|
|
37
38
|
cwd,
|
|
38
39
|
stdio: "inherit",
|
|
39
|
-
}).catch(() =>
|
|
40
|
+
}).catch((err) => {
|
|
41
|
+
writeStderr(`[ERROR] Failed to run test command '${testCmd}': ${err instanceof Error ? err.message : String(err)}`);
|
|
42
|
+
return { status: 1, signal: null, stdout: "", stderr: "" };
|
|
43
|
+
});
|
|
40
44
|
if (proc.status !== 0) {
|
|
41
45
|
process.exitCode = proc.status ?? 1;
|
|
42
46
|
}
|