@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.
- package/README.md +44 -3
- package/dist/commands/action/create.js +2 -2
- 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 +2 -4
- 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 +23 -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 +8 -0
- package/dist/commands/run.js +118 -33
- package/dist/commands/runs.js +10 -25
- package/dist/commands/serve.js +31 -4
- 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 +20 -2
- package/dist/index.js +1 -2
- package/dist/renderer.d.ts +0 -2
- package/dist/renderer.js +3 -15
- 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/types.d.ts +0 -2
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/input.d.ts +46 -0
- package/dist/utils/input.js +89 -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.js
CHANGED
|
@@ -1,32 +1,106 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { parseDuration, resolveTarget } from "@actiondock/core";
|
|
3
|
-
import { ArgumentError, ExecutionError, SigintError } from "../errors.js";
|
|
4
|
-
import { writeStdout } from "../renderer.js";
|
|
5
|
-
import { applyTargetOptions, getEffectiveOptions, resolveLocalPackageRoot, withTarget, } from "../utils/index.js";
|
|
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
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
input = JSON.parse(readFileSync(options.inputFile, "utf-8"));
|
|
48
|
+
else if (data !== undefined) {
|
|
49
|
+
rawText = String(data);
|
|
25
50
|
}
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
}
|
|
28
79
|
}
|
|
80
|
+
writeStdout(rawText, context);
|
|
29
81
|
}
|
|
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);
|
|
88
|
+
}
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 统一执行 Action 核心逻辑(使用 ActionDockTarget 门面)。
|
|
94
|
+
*/
|
|
95
|
+
export async function executeAction(id, options, context) {
|
|
96
|
+
if (!id) {
|
|
97
|
+
throw new ArgumentError("Action ID is required for run");
|
|
98
|
+
}
|
|
99
|
+
const input = await resolveActionInput({
|
|
100
|
+
input: options.input,
|
|
101
|
+
inputFile: options.inputFile,
|
|
102
|
+
stdin: context?.stdin,
|
|
103
|
+
});
|
|
30
104
|
let timeoutMs;
|
|
31
105
|
if (options.timeout) {
|
|
32
106
|
try {
|
|
@@ -66,7 +140,7 @@ export async function executeAction(id, options, context) {
|
|
|
66
140
|
// 目标拓扑解析(仅 local 分支需要包寻址)
|
|
67
141
|
const targetPackageRoot = resolveLocalPackageRoot(options.package);
|
|
68
142
|
if (options.package && !targetPackageRoot) {
|
|
69
|
-
throw
|
|
143
|
+
throw packageNotFoundError(options.package);
|
|
70
144
|
}
|
|
71
145
|
let targetRef = id;
|
|
72
146
|
if (options.package && !id.includes("/") && !id.includes(":")) {
|
|
@@ -74,6 +148,7 @@ export async function executeAction(id, options, context) {
|
|
|
74
148
|
}
|
|
75
149
|
// 通过 Target 统一执行
|
|
76
150
|
await withTarget(options, context, async (target) => {
|
|
151
|
+
const isMachine = Boolean(options.json);
|
|
77
152
|
if (options.async) {
|
|
78
153
|
const ticket = await target.startAction(targetRef, input, {
|
|
79
154
|
signal: controller.signal,
|
|
@@ -81,12 +156,18 @@ export async function executeAction(id, options, context) {
|
|
|
81
156
|
config: configOverrides,
|
|
82
157
|
requestId: options.requestId,
|
|
83
158
|
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
159
|
+
if (isMachine) {
|
|
160
|
+
const asyncOutput = {
|
|
161
|
+
ok: ticket.status !== "failed",
|
|
162
|
+
runId: ticket.runId,
|
|
163
|
+
status: ticket.status,
|
|
164
|
+
};
|
|
165
|
+
writeStdout(JSON.stringify(asyncOutput, null, 2), context);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
writeStderr(`[${ticket.status}] runId: ${ticket.runId}`, context);
|
|
169
|
+
writeStdout(ticket.runId, context);
|
|
170
|
+
}
|
|
90
171
|
if (ticket.status === "failed") {
|
|
91
172
|
process.exitCode = 1;
|
|
92
173
|
return;
|
|
@@ -99,13 +180,18 @@ export async function executeAction(id, options, context) {
|
|
|
99
180
|
config: configOverrides,
|
|
100
181
|
requestId: options.requestId,
|
|
101
182
|
});
|
|
102
|
-
|
|
183
|
+
if (isMachine) {
|
|
184
|
+
writeStdout(JSON.stringify(result, null, 2), context);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
renderRawExecutionResult(targetRef, result, context);
|
|
188
|
+
}
|
|
103
189
|
if (!result.ok) {
|
|
104
190
|
process.exitCode = 1;
|
|
105
191
|
return;
|
|
106
192
|
}
|
|
107
193
|
}
|
|
108
|
-
}, { localRoot: targetPackageRoot || undefined, scanLinkedPackages: true });
|
|
194
|
+
}, { localRoot: targetPackageRoot || undefined, scanLinkedPackages: true, ownDataDir: true });
|
|
109
195
|
}
|
|
110
196
|
catch (err) {
|
|
111
197
|
if (receivedSigint || err?.name === "AbortError" || err?.message?.includes("SIGINT")) {
|
|
@@ -128,8 +214,8 @@ export function attachRunCommand(parent, context) {
|
|
|
128
214
|
.command("run <id>")
|
|
129
215
|
.description("Execute an action (from current project, linked packages, or remote profile)")
|
|
130
216
|
.option("-P, --package <id>", "Target package ID or path")
|
|
131
|
-
.option("-i, --input <json>", "
|
|
132
|
-
.option("-f, --input-file <path>", "
|
|
217
|
+
.option("-i, --input <json>", "Action input as inline JSON")
|
|
218
|
+
.option("-f, --input-file <path>", "Action input from JSON file, or '-' for stdin")
|
|
133
219
|
.option("-c, --config <key=value...>", "Temporary config override (repeatable)");
|
|
134
220
|
return applyTargetOptions(cmd)
|
|
135
221
|
.option("--timeout <duration>", "Execution timeout (e.g. 30s, 5m, 500ms)")
|
|
@@ -137,7 +223,6 @@ export function attachRunCommand(parent, context) {
|
|
|
137
223
|
.option("--async", "Execute asynchronously in background (requires remote server or profile)")
|
|
138
224
|
.option("--data-dir <path>", "Custom database directory")
|
|
139
225
|
.option("--json", "Output as JSON")
|
|
140
|
-
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
141
226
|
.action(async (id, rawOptions, cmd) => {
|
|
142
227
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
143
228
|
await executeAction(id, options, context);
|
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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { findProjectRoot, formatHostForUrl, loadProjectConfig, startActionDockServer, } from "@actiondock/core";
|
|
3
|
+
import { createActionDockTarget, findProjectRoot, formatHostForUrl, loadProjectConfig, startActionDockServer, } from "@actiondock/core";
|
|
4
|
+
import { createNodePlatform } from "@actiondock/runtime-node";
|
|
4
5
|
import { ArgumentError, ExecutionError } from "../errors.js";
|
|
5
6
|
import { writeStderr, writeStdout } from "../renderer.js";
|
|
6
7
|
import { ensureSelfSignedCertificate, getEffectiveOptions, parseByteSize } from "../utils/index.js";
|
|
@@ -30,9 +31,11 @@ export function registerServeCommand(program, context) {
|
|
|
30
31
|
.option("--tls-ca <path>", "Path to TLS CA certificate file (or set ACTIONDOCK_TLS_CA)")
|
|
31
32
|
.option("--tls-passphrase <passphrase>", "Passphrase for TLS private key (or set ACTIONDOCK_TLS_PASSPHRASE)")
|
|
32
33
|
.option("-d, --dir <path>", "Project root directory (default: current working directory)")
|
|
34
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
33
35
|
.action(async (rawOptions, cmd) => {
|
|
34
36
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
35
|
-
const
|
|
37
|
+
const parsedPort = parseInt(options.port, 10);
|
|
38
|
+
const port = Number.isNaN(parsedPort) ? 5177 : parsedPort;
|
|
36
39
|
const host = options.host || "127.0.0.1";
|
|
37
40
|
const token = options.token || (typeof process !== "undefined" ? process.env?.ACTIONDOCK_TOKEN : undefined);
|
|
38
41
|
const allowInsecureNoAuth = Boolean(options.allowInsecureNoAuth);
|
|
@@ -101,6 +104,19 @@ export function registerServeCommand(program, context) {
|
|
|
101
104
|
// 降级处理
|
|
102
105
|
}
|
|
103
106
|
}
|
|
107
|
+
const platform = createNodePlatform({
|
|
108
|
+
customHome: context?.customHome,
|
|
109
|
+
dataDir: options.dataDir || context?.dataDir,
|
|
110
|
+
rootDir: projectRoot || undefined,
|
|
111
|
+
});
|
|
112
|
+
const target = await createActionDockTarget({
|
|
113
|
+
type: "local",
|
|
114
|
+
projectRoot: projectRoot || undefined,
|
|
115
|
+
customHome: context?.customHome,
|
|
116
|
+
dataDir: options.dataDir || context?.dataDir,
|
|
117
|
+
platform,
|
|
118
|
+
scanLinkedPackages: !projectRoot,
|
|
119
|
+
});
|
|
104
120
|
let mcpHandler;
|
|
105
121
|
const enableMcp = options.mcp !== false;
|
|
106
122
|
if (enableMcp) {
|
|
@@ -112,7 +128,7 @@ export function registerServeCommand(program, context) {
|
|
|
112
128
|
]);
|
|
113
129
|
const handler = createMcpHandler(() => {
|
|
114
130
|
return createActionDockMcpServer({
|
|
115
|
-
|
|
131
|
+
target,
|
|
116
132
|
});
|
|
117
133
|
}, {
|
|
118
134
|
onerror: (err) => {
|
|
@@ -124,6 +140,7 @@ export function registerServeCommand(program, context) {
|
|
|
124
140
|
};
|
|
125
141
|
}
|
|
126
142
|
catch (err) {
|
|
143
|
+
await target.close().catch(() => { });
|
|
127
144
|
throw new ExecutionError(`Failed to initialize MCP endpoint: ${err?.message || String(err)}`, err);
|
|
128
145
|
}
|
|
129
146
|
}
|
|
@@ -142,6 +159,7 @@ export function registerServeCommand(program, context) {
|
|
|
142
159
|
mcpHandler,
|
|
143
160
|
tls,
|
|
144
161
|
projectRoot: projectRoot || undefined,
|
|
162
|
+
target,
|
|
145
163
|
});
|
|
146
164
|
const displayHost = formatHostForUrl(host);
|
|
147
165
|
const actualEndpointHost = formatHostForUrl(host === "0.0.0.0" ? "127.0.0.1" : host);
|
|
@@ -174,12 +192,21 @@ export function registerServeCommand(program, context) {
|
|
|
174
192
|
writeStdout(`Press Ctrl+C to terminate.\n`, context);
|
|
175
193
|
const stopSignalHandler = () => {
|
|
176
194
|
writeStdout("\nStopping ActionDock server...", context);
|
|
177
|
-
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
|
+
});
|
|
178
204
|
};
|
|
179
205
|
process.once("SIGINT", stopSignalHandler);
|
|
180
206
|
process.once("SIGTERM", stopSignalHandler);
|
|
181
207
|
}
|
|
182
208
|
catch (err) {
|
|
209
|
+
await target.close().catch(() => { });
|
|
183
210
|
throw new ExecutionError(`Failed to start ActionDock server: ${err.message}`, err);
|
|
184
211
|
}
|
|
185
212
|
});
|
|
@@ -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
|
}
|