@actiondock/cli 2.10.0 → 2.12.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 +10 -5
- package/bin/ad.js +225 -5
- package/dist/commands/action/create.d.ts +1 -1
- package/dist/commands/action/create.js +7 -14
- package/dist/commands/add.js +1 -1
- package/dist/commands/config/delete.js +3 -1
- package/dist/commands/config/env.js +2 -1
- package/dist/commands/config/get.js +3 -1
- package/dist/commands/config/list.js +6 -5
- package/dist/commands/config/set.js +3 -1
- package/dist/commands/describe.js +2 -3
- package/dist/commands/export.js +34 -16
- package/dist/commands/index.d.ts +37 -24
- package/dist/commands/index.js +326 -48
- package/dist/commands/mcp.js +0 -1
- package/dist/commands/playbook.js +15 -35
- package/dist/commands/profile.js +2 -1
- package/dist/commands/remove.js +1 -1
- package/dist/commands/run.d.ts +7 -2
- package/dist/commands/run.js +47 -59
- package/dist/commands/runs.d.ts +6 -1
- package/dist/commands/runs.js +274 -11
- package/dist/commands/serve.js +5 -2
- package/dist/commands/state.js +84 -15
- package/dist/index.d.ts +10 -2
- package/dist/index.js +15 -12
- package/dist/prompt.d.ts +3 -0
- package/dist/prompt.js +6 -14
- package/dist/renderer.d.ts +7 -137
- package/dist/renderer.js +6 -438
- package/dist/renderers/action.d.ts +78 -0
- package/dist/renderers/action.js +187 -0
- package/dist/renderers/config.d.ts +31 -0
- package/dist/renderers/config.js +93 -0
- package/dist/renderers/playbook.d.ts +19 -0
- package/dist/renderers/playbook.js +42 -0
- package/dist/renderers/project.d.ts +45 -0
- package/dist/renderers/project.js +196 -0
- package/dist/renderers/runs.d.ts +15 -0
- package/dist/renderers/runs.js +47 -0
- package/dist/renderers/state.d.ts +4 -0
- package/dist/renderers/state.js +19 -0
- package/dist/services/config-merge.js +1 -2
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/run-watch.d.ts +156 -0
- package/dist/services/run-watch.js +395 -0
- package/dist/utils/cert.js +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/input.d.ts +2 -1
- package/dist/utils/input.js +2 -1
- package/dist/utils/target.d.ts +6 -2
- package/dist/utils/target.js +4 -4
- package/dist/utils/utils.js +3 -11
- package/dist/utils/warning.d.ts +4 -0
- package/dist/utils/warning.js +4 -0
- package/package.json +5 -5
- package/dist/commands/state-list.d.ts +0 -24
- package/dist/commands/state-list.js +0 -81
- package/dist/standalone/index.d.ts +0 -12
- package/dist/standalone/index.js +0 -13
- package/dist/utils/duration.d.ts +0 -1
- package/dist/utils/duration.js +0 -1
package/dist/commands/run.js
CHANGED
|
@@ -1,51 +1,34 @@
|
|
|
1
1
|
import { validateActionInputValue, mapInputValidationFailure, } from "@actiondock/core/project";
|
|
2
2
|
import { resolveTarget, } from "@actiondock/core/profile";
|
|
3
|
-
import { resolveExecutionHint, } from "@actiondock/core";
|
|
3
|
+
import { ACTION_NOT_FOUND, NOT_FOUND, resolveExecutionHint, } from "@actiondock/core";
|
|
4
4
|
import { ArgumentError, ExecutionError, SigintError, packageNotFoundError } from "../errors.js";
|
|
5
|
-
import { writeStderr, writeStdout } from "../renderer.js";
|
|
5
|
+
import { extractTextFieldPayload, resolveActionDefaultTextField, writeStderr, writeStdout, } from "../renderer.js";
|
|
6
6
|
import { applyTargetOptions, getEffectiveOptions, resolveActionInput, resolveLocalPackageRoot, resolveTimeoutMs, withService, } from "../utils/index.js";
|
|
7
7
|
/**
|
|
8
8
|
* 以原始纯文本形式渲染 Action 执行终态结果(默认纯文本模式)。
|
|
9
9
|
*
|
|
10
10
|
* 设计契约:
|
|
11
|
-
*
|
|
11
|
+
* - 通用纯文本透传:标量直接输出,结构化对象按标准规范呈现,杜绝私有业务字段嗅探。
|
|
12
|
+
* - 显式或声明式正文输出:指定 textField 时仅输出该自有字符串字段至 stdout,剩余字段以 JSON 对象形式输出至 stderr。
|
|
13
|
+
* - 标准输出仅承载业务有效载荷,保持原生排版与真实换行,供用户调阅或下游管道消费。
|
|
12
14
|
*/
|
|
13
|
-
export function renderRawExecutionResult(targetRef, result, context) {
|
|
15
|
+
export function renderRawExecutionResult(targetRef, result, context, options) {
|
|
14
16
|
if (result.ok) {
|
|
17
|
+
if (options?.textField !== undefined) {
|
|
18
|
+
const { text, metadata } = extractTextFieldPayload(result.data, options.textField);
|
|
19
|
+
writeStdout(text, context);
|
|
20
|
+
if (metadata) {
|
|
21
|
+
writeStderr(JSON.stringify(metadata, null, 2), context);
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
15
25
|
const data = result.data;
|
|
16
26
|
let rawText;
|
|
17
|
-
let metaInfo;
|
|
18
27
|
if (typeof data === "string") {
|
|
19
28
|
rawText = data;
|
|
20
29
|
}
|
|
21
30
|
else if (data !== null && typeof data === "object") {
|
|
22
|
-
|
|
23
|
-
rawText =
|
|
24
|
-
typeof data.content === "object" && data.content !== null
|
|
25
|
-
? JSON.stringify(data.content, null, 2)
|
|
26
|
-
: String(data.content);
|
|
27
|
-
const { content, ...rest } = data;
|
|
28
|
-
if (Object.keys(rest).length > 0) {
|
|
29
|
-
metaInfo = rest;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
else if ("text" in data && typeof data.text === "string") {
|
|
33
|
-
rawText = data.text;
|
|
34
|
-
const { text, ...rest } = data;
|
|
35
|
-
if (Object.keys(rest).length > 0) {
|
|
36
|
-
metaInfo = rest;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
else if ("message" in data && typeof data.message === "string") {
|
|
40
|
-
rawText = data.message;
|
|
41
|
-
const { message, ...rest } = data;
|
|
42
|
-
if (Object.keys(rest).length > 0) {
|
|
43
|
-
metaInfo = rest;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
rawText = JSON.stringify(data, null, 2);
|
|
48
|
-
}
|
|
31
|
+
rawText = JSON.stringify(data, null, 2);
|
|
49
32
|
}
|
|
50
33
|
else if (data !== undefined) {
|
|
51
34
|
rawText = String(data);
|
|
@@ -53,32 +36,6 @@ export function renderRawExecutionResult(targetRef, result, context) {
|
|
|
53
36
|
else {
|
|
54
37
|
rawText = "";
|
|
55
38
|
}
|
|
56
|
-
if (metaInfo) {
|
|
57
|
-
const parts = [];
|
|
58
|
-
const title = metaInfo.path ? String(metaInfo.path) : targetRef;
|
|
59
|
-
parts.push(title);
|
|
60
|
-
if (metaInfo.startLine !== undefined && metaInfo.endLine !== undefined) {
|
|
61
|
-
parts.push(`lines ${metaInfo.startLine}-${metaInfo.endLine}`);
|
|
62
|
-
}
|
|
63
|
-
else if (metaInfo.line !== undefined) {
|
|
64
|
-
parts.push(`line ${metaInfo.line}`);
|
|
65
|
-
}
|
|
66
|
-
if (metaInfo.hasMore !== undefined) {
|
|
67
|
-
parts.push(`hasMore: ${metaInfo.hasMore}`);
|
|
68
|
-
}
|
|
69
|
-
if (metaInfo.truncated) {
|
|
70
|
-
parts.push("truncated: true");
|
|
71
|
-
}
|
|
72
|
-
const handled = new Set(["path", "startLine", "endLine", "line", "hasMore", "truncated"]);
|
|
73
|
-
for (const [k, v] of Object.entries(metaInfo)) {
|
|
74
|
-
if (!handled.has(k) && v !== undefined) {
|
|
75
|
-
parts.push(`${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (parts.length > 0) {
|
|
79
|
-
writeStderr(`[${parts.join(" | ")}]`, context);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
39
|
writeStdout(rawText, context);
|
|
83
40
|
}
|
|
84
41
|
else {
|
|
@@ -110,6 +67,17 @@ export async function executeAction(id, options, context, flatArgs, control) {
|
|
|
110
67
|
if (!id) {
|
|
111
68
|
throw new ArgumentError("Action ID is required for run");
|
|
112
69
|
}
|
|
70
|
+
if (options.textField !== undefined) {
|
|
71
|
+
if (typeof options.textField !== "string" || options.textField.trim() === "") {
|
|
72
|
+
throw new ArgumentError("The --text-field option requires a non-empty field name.");
|
|
73
|
+
}
|
|
74
|
+
if (options.json) {
|
|
75
|
+
throw new ArgumentError("Cannot specify both --text-field and --json.");
|
|
76
|
+
}
|
|
77
|
+
if (options.async) {
|
|
78
|
+
throw new ArgumentError("Cannot specify both --text-field and --async.");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
113
81
|
const effectiveControl = control ?? context?.control;
|
|
114
82
|
const effectiveSignal = effectiveControl?.signal;
|
|
115
83
|
const effectiveFlatArgs = flatArgs ?? options.flatArgs;
|
|
@@ -183,6 +151,21 @@ export async function executeAction(id, options, context, flatArgs, control) {
|
|
|
183
151
|
}
|
|
184
152
|
}
|
|
185
153
|
else {
|
|
154
|
+
let effectiveTextField = options.textField;
|
|
155
|
+
if (!isMachine && effectiveTextField === undefined) {
|
|
156
|
+
try {
|
|
157
|
+
const spec = await service.discovery.describeAction(targetRef);
|
|
158
|
+
effectiveTextField = resolveActionDefaultTextField(spec.annotations);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
if (err?.code === ACTION_NOT_FOUND || err?.code === NOT_FOUND) {
|
|
162
|
+
// 若动作未找到,继续交由 execution.run 生成统一的执行与提示结果
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
throw err;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
186
169
|
const result = await service.execution.run(targetRef, input, {
|
|
187
170
|
signal: effectiveSignal,
|
|
188
171
|
timeoutMs,
|
|
@@ -204,7 +187,7 @@ export async function executeAction(id, options, context, flatArgs, control) {
|
|
|
204
187
|
}
|
|
205
188
|
}
|
|
206
189
|
else {
|
|
207
|
-
renderRawExecutionResult(targetRef, result, context);
|
|
190
|
+
renderRawExecutionResult(targetRef, result, context, { textField: effectiveTextField });
|
|
208
191
|
}
|
|
209
192
|
if (!result.ok) {
|
|
210
193
|
if (context) {
|
|
@@ -255,6 +238,10 @@ Flat Input Syntax & Examples:
|
|
|
255
238
|
# Nested object properties
|
|
256
239
|
ad run <id> -- user.name="alice" user.age:=30
|
|
257
240
|
|
|
241
|
+
Text Field Output:
|
|
242
|
+
# Extract specific text field to stdout (remaining fields as JSON to stderr)
|
|
243
|
+
ad run <id> --text-field content -- path="README.md"
|
|
244
|
+
|
|
258
245
|
Complex or multiline inputs:
|
|
259
246
|
# Pass via JSON file with --input-file to avoid shell escaping issues
|
|
260
247
|
ad run <id> --input-file input.json`;
|
|
@@ -281,6 +268,7 @@ export function attachRunCommand(parent, context) {
|
|
|
281
268
|
.option("--async", "Execute asynchronously in background (requires remote server or profile)")
|
|
282
269
|
.option("--data-dir <path>", "Custom database directory")
|
|
283
270
|
.option("--json", "Output as JSON")
|
|
271
|
+
.option("--text-field <field>", "Extract and output a specific top-level string field as raw text")
|
|
284
272
|
.addHelpText("after", RUN_HELP_AFTER_TEXT)
|
|
285
273
|
.action(async (id, params, rawOptions, cmd) => {
|
|
286
274
|
let flatArgs;
|
package/dist/commands/runs.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import type { CliContext } from "../types.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* 归一化 requestId 选项值为去重后的非空原样字符串数组。
|
|
5
|
+
* 将 requestId 作为不作解释的原样字符串,不拆分逗号、不剔除首尾空白,只做非空有效字符串检查与去重。
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeRequestIds(raw: unknown): string[];
|
|
8
|
+
/**
|
|
9
|
+
* 注册 runs 动作执行历史管理命令(list、show、watch、clear、cancel)。
|
|
5
10
|
*
|
|
6
11
|
* @param program Commander 实例
|
|
7
12
|
* @param context 命令行上下文
|
package/dist/commands/runs.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadProjectConfig, parseDuration, } from "@actiondock/core";
|
|
2
2
|
import { filterWithFallbackInfo, } from "@actiondock/core/project";
|
|
3
|
-
import { listLinkedPackages,
|
|
4
|
-
import { ArgumentError, ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE,
|
|
5
|
-
import { renderResult, renderRunDetail, renderRunsList } from "../renderer.js";
|
|
6
|
-
import {
|
|
3
|
+
import { listLinkedPackages, } from "@actiondock/core/registry";
|
|
4
|
+
import { ArgumentError, ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE, } from "../errors.js";
|
|
5
|
+
import { renderResult, renderRunDetail, renderRunsList, writeStdout } from "../renderer.js";
|
|
6
|
+
import { pollRunsUntilTerminal, renderWatchSummary, resolveRunsByRequestIds, } from "../services/run-watch.js";
|
|
7
|
+
import { applyTargetOptions, getEffectiveOptions, remoteTargetSuffix, requirePackageRoot, resolveFallbackStrategy, resolveIntent, resolveLocalPackageRoot, withService, } from "../utils/index.js";
|
|
7
8
|
/**
|
|
8
9
|
* 解析本地目标包根目录与工程配置(仅在 local 分支调用)。
|
|
9
10
|
* 显式指定包且寻址失败时抛出参数错误;工程清单损坏时降级为链接包视图。
|
|
10
11
|
*/
|
|
11
12
|
function resolveLocalRunScope(packageOption) {
|
|
12
13
|
if (packageOption) {
|
|
13
|
-
const root =
|
|
14
|
-
if (!root) {
|
|
15
|
-
throw packageNotFoundError(packageOption);
|
|
16
|
-
}
|
|
14
|
+
const { root } = requirePackageRoot(packageOption);
|
|
17
15
|
try {
|
|
18
16
|
return { targetPackageRoot: root, projConfig: loadProjectConfig(root) };
|
|
19
17
|
}
|
|
@@ -22,7 +20,7 @@ function resolveLocalRunScope(packageOption) {
|
|
|
22
20
|
return { targetPackageRoot: root, projConfig: null };
|
|
23
21
|
}
|
|
24
22
|
}
|
|
25
|
-
const root =
|
|
23
|
+
const root = resolveLocalPackageRoot();
|
|
26
24
|
if (!root) {
|
|
27
25
|
return { targetPackageRoot: undefined, projConfig: null };
|
|
28
26
|
}
|
|
@@ -35,7 +33,51 @@ function resolveLocalRunScope(packageOption) {
|
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
/**
|
|
38
|
-
*
|
|
36
|
+
* 解析 watch 命令的正时长选项值(毫秒)。
|
|
37
|
+
* 格式非法或非正值时抛参数错误(退出码 2)。
|
|
38
|
+
*/
|
|
39
|
+
function parsePositiveDuration(raw, flagName) {
|
|
40
|
+
if (raw === undefined || raw === null || String(raw).trim() === "") {
|
|
41
|
+
throw new ArgumentError(`Invalid ${flagName} argument: value is required`);
|
|
42
|
+
}
|
|
43
|
+
let ms;
|
|
44
|
+
try {
|
|
45
|
+
ms = parseDuration(String(raw));
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
throw new ArgumentError(`Invalid ${flagName} argument: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
if (ms === undefined || ms <= 0) {
|
|
51
|
+
throw new ArgumentError(`Invalid ${flagName} argument: must be a positive duration (e.g. 500ms, 2s)`);
|
|
52
|
+
}
|
|
53
|
+
return ms;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Commander 可重复选项收集函数:同一选项多次出现时累积为数组。
|
|
57
|
+
*/
|
|
58
|
+
function collectRepeatableOption(value, previous) {
|
|
59
|
+
return previous.concat([value]);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 归一化 requestId 选项值为去重后的非空原样字符串数组。
|
|
63
|
+
* 将 requestId 作为不作解释的原样字符串,不拆分逗号、不剔除首尾空白,只做非空有效字符串检查与去重。
|
|
64
|
+
*/
|
|
65
|
+
export function normalizeRequestIds(raw) {
|
|
66
|
+
const items = Array.isArray(raw) ? raw : raw !== undefined && raw !== null ? [raw] : [];
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
const result = [];
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
if (typeof item === "string" && item.length > 0) {
|
|
71
|
+
if (!seen.has(item)) {
|
|
72
|
+
seen.add(item);
|
|
73
|
+
result.push(item);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 注册 runs 动作执行历史管理命令(list、show、watch、clear、cancel)。
|
|
39
81
|
*
|
|
40
82
|
* @param program Commander 实例
|
|
41
83
|
* @param context 命令行上下文
|
|
@@ -51,6 +93,7 @@ export function registerRunsCommands(program, context) {
|
|
|
51
93
|
.option("-P, --package <id>", "Target package ID or path")
|
|
52
94
|
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
|
|
53
95
|
.option("-a, --action <actionId>", "Filter by action ID")
|
|
96
|
+
.option("--request-id <id>", "Filter by idempotency request ID (repeatable)", collectRepeatableOption, [])
|
|
54
97
|
.option("-n, --limit <count>", "Maximum number of records to return", "20"))
|
|
55
98
|
.option("--fallback", "Enable fallback to full list when no items match intent")
|
|
56
99
|
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
@@ -61,6 +104,7 @@ export function registerRunsCommands(program, context) {
|
|
|
61
104
|
const effectiveIntent = resolveIntent(options.intent, patterns);
|
|
62
105
|
const { shouldFallback } = resolveFallbackStrategy(options);
|
|
63
106
|
const limit = Number.parseInt(options.limit, 10) || 20;
|
|
107
|
+
const requestIds = normalizeRequestIds(options.requestId);
|
|
64
108
|
const scope = resolveLocalRunScope(options.package);
|
|
65
109
|
// 通过 Service 门面统一访问
|
|
66
110
|
await withService(options, context, async (service, resolved) => {
|
|
@@ -71,6 +115,7 @@ export function registerRunsCommands(program, context) {
|
|
|
71
115
|
const records = await service.runs.list({
|
|
72
116
|
packageId: options.package,
|
|
73
117
|
actionId: options.action,
|
|
118
|
+
requestIds: requestIds.length > 0 ? requestIds : undefined,
|
|
74
119
|
limit: queryLimit,
|
|
75
120
|
});
|
|
76
121
|
// 若处于本地无项目环境且无任何软链接包,则直接输出友好提示
|
|
@@ -140,6 +185,224 @@ export function registerRunsCommands(program, context) {
|
|
|
140
185
|
});
|
|
141
186
|
}, { localRoot: scope.targetPackageRoot, scanLinkedPackages: true });
|
|
142
187
|
});
|
|
188
|
+
// runs watch [ids...] / --request-id <id>
|
|
189
|
+
applyTargetOptions(runsCmd
|
|
190
|
+
.command("watch [ids...]")
|
|
191
|
+
.description("Block until the given runs reach a terminal state, then aggregate results")
|
|
192
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
193
|
+
.option("--request-id <id>", "Wait by idempotency request ID(s) instead of run IDs (repeatable)", collectRepeatableOption, []))
|
|
194
|
+
.option("--resolve-timeout <duration>", "Budget for resolving --request-id to run IDs (e.g. 30s)", "30s")
|
|
195
|
+
.option("--timeout <duration>", "Overall wait limit (e.g. 30s, 5m, 500ms); exits with current statuses on expiry")
|
|
196
|
+
.option("--interval <duration>", "Polling interval (e.g. 500ms, 30s)", "2s")
|
|
197
|
+
.option("-q, --quiet", "Suppress periodic waiting progress lines")
|
|
198
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
199
|
+
.option("--json", "Output as JSON")
|
|
200
|
+
.action(async (ids, rawOptions, cmd) => {
|
|
201
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
202
|
+
const watchIds = (ids || []).filter((id) => id && id.trim());
|
|
203
|
+
const requestIds = normalizeRequestIds(options.requestId);
|
|
204
|
+
if (watchIds.length === 0 && requestIds.length === 0) {
|
|
205
|
+
throw new ArgumentError("At least one run ID or --request-id is required for watch");
|
|
206
|
+
}
|
|
207
|
+
const intervalMs = parsePositiveDuration(options.interval, "--interval");
|
|
208
|
+
const timeoutMs = options.timeout
|
|
209
|
+
? parsePositiveDuration(options.timeout, "--timeout")
|
|
210
|
+
: undefined;
|
|
211
|
+
const resolveTimeoutMs = parsePositiveDuration(options.resolveTimeout, "--resolve-timeout");
|
|
212
|
+
// 从命令入口开始计算统一绝对截止时间
|
|
213
|
+
const commandStartedAt = Date.now();
|
|
214
|
+
const overallDeadline = timeoutMs !== undefined ? commandStartedAt + timeoutMs : undefined;
|
|
215
|
+
const effectiveControl = context?.control;
|
|
216
|
+
const externalSignal = effectiveControl?.signal;
|
|
217
|
+
// 绑定统一超时与外部取消信号至读取取消控制器
|
|
218
|
+
const timeoutController = overallDeadline !== undefined ? new AbortController() : undefined;
|
|
219
|
+
let timeoutTimer;
|
|
220
|
+
if (overallDeadline !== undefined) {
|
|
221
|
+
const remainingDelay = Math.max(0, overallDeadline - commandStartedAt);
|
|
222
|
+
timeoutTimer = setTimeout(() => {
|
|
223
|
+
timeoutController?.abort();
|
|
224
|
+
}, remainingDelay);
|
|
225
|
+
if (timeoutTimer.unref)
|
|
226
|
+
timeoutTimer.unref();
|
|
227
|
+
}
|
|
228
|
+
const readSignal = externalSignal && timeoutController
|
|
229
|
+
? AbortSignal.any([externalSignal, timeoutController.signal])
|
|
230
|
+
: externalSignal ?? timeoutController?.signal;
|
|
231
|
+
try {
|
|
232
|
+
// 单一环境服务生命周期:一次 withService 包住标识解析和整个等待过程
|
|
233
|
+
await withService(options, context, async (service, resolved) => {
|
|
234
|
+
const scope = resolved.type === "local"
|
|
235
|
+
? resolveLocalRunScope(options.package)
|
|
236
|
+
: undefined;
|
|
237
|
+
const normalizedPackageId = options.package
|
|
238
|
+
? (resolved.type === "remote" ? options.package : scope?.projConfig?.id || options.package)
|
|
239
|
+
: undefined;
|
|
240
|
+
const whereDescription = resolved.type === "remote"
|
|
241
|
+
? `on remote server ${resolved.serverUrl || ""}`
|
|
242
|
+
: options.package
|
|
243
|
+
? `in package '${normalizedPackageId}'`
|
|
244
|
+
: "in current project or any linked packages";
|
|
245
|
+
// 1. 解析 requestId
|
|
246
|
+
const resolvedMap = new Map();
|
|
247
|
+
if (requestIds.length > 0) {
|
|
248
|
+
const current = Date.now();
|
|
249
|
+
const resolveDeadline = overallDeadline !== undefined
|
|
250
|
+
? Math.min(overallDeadline, commandStartedAt + resolveTimeoutMs)
|
|
251
|
+
: commandStartedAt + resolveTimeoutMs;
|
|
252
|
+
const remainingResolveBudgetMs = Math.max(0, resolveDeadline - current);
|
|
253
|
+
const resolveTimeoutController = new AbortController();
|
|
254
|
+
let resolveTimer;
|
|
255
|
+
if (remainingResolveBudgetMs < Infinity) {
|
|
256
|
+
resolveTimer = setTimeout(() => {
|
|
257
|
+
resolveTimeoutController.abort();
|
|
258
|
+
}, remainingResolveBudgetMs);
|
|
259
|
+
if (resolveTimer.unref)
|
|
260
|
+
resolveTimer.unref();
|
|
261
|
+
}
|
|
262
|
+
const resolveSignals = [resolveTimeoutController.signal];
|
|
263
|
+
if (externalSignal)
|
|
264
|
+
resolveSignals.push(externalSignal);
|
|
265
|
+
if (timeoutController)
|
|
266
|
+
resolveSignals.push(timeoutController.signal);
|
|
267
|
+
const resolveSignal = AbortSignal.any(resolveSignals);
|
|
268
|
+
let hits = [];
|
|
269
|
+
let unresolved = [];
|
|
270
|
+
try {
|
|
271
|
+
const res = await resolveRunsByRequestIds(service, requestIds, {
|
|
272
|
+
packageId: normalizedPackageId,
|
|
273
|
+
intervalMs,
|
|
274
|
+
resolveTimeoutMs: remainingResolveBudgetMs,
|
|
275
|
+
deadline: resolveDeadline,
|
|
276
|
+
signal: resolveSignal,
|
|
277
|
+
});
|
|
278
|
+
hits = res.resolved;
|
|
279
|
+
unresolved = res.unresolved;
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
if (resolveTimer)
|
|
283
|
+
clearTimeout(resolveTimer);
|
|
284
|
+
}
|
|
285
|
+
// 查询返回后再次校验截止时间
|
|
286
|
+
if (unresolved.length > 0 || Date.now() >= resolveDeadline) {
|
|
287
|
+
const missingList = unresolved.length > 0 ? unresolved : requestIds;
|
|
288
|
+
throw new ExecutionError(`Request ID(s) not found ${whereDescription} within ${options.resolveTimeout}: ${missingList.join(", ")}`);
|
|
289
|
+
}
|
|
290
|
+
for (const hit of hits) {
|
|
291
|
+
resolvedMap.set(hit.requestId, hit);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// 2. 保持用户输入顺序与 runId 去重
|
|
295
|
+
const sources = [];
|
|
296
|
+
const seenRunIds = new Set();
|
|
297
|
+
// 先按用户输入的位置参数添加
|
|
298
|
+
for (const id of watchIds) {
|
|
299
|
+
if (seenRunIds.has(id))
|
|
300
|
+
continue;
|
|
301
|
+
seenRunIds.add(id);
|
|
302
|
+
sources.push({ runId: id, source: resolved.type });
|
|
303
|
+
}
|
|
304
|
+
// 再按用户输入的 --request-id 顺序添加或补齐
|
|
305
|
+
for (const reqId of requestIds) {
|
|
306
|
+
const hit = resolvedMap.get(reqId);
|
|
307
|
+
if (!hit)
|
|
308
|
+
continue;
|
|
309
|
+
const existing = sources.find((s) => s.runId === hit.runId);
|
|
310
|
+
if (existing) {
|
|
311
|
+
if (!existing.requestId)
|
|
312
|
+
existing.requestId = hit.requestId;
|
|
313
|
+
if (!existing.initialRecord)
|
|
314
|
+
existing.initialRecord = hit.record;
|
|
315
|
+
}
|
|
316
|
+
else if (!seenRunIds.has(hit.runId)) {
|
|
317
|
+
seenRunIds.add(hit.runId);
|
|
318
|
+
sources.push({
|
|
319
|
+
runId: hit.runId,
|
|
320
|
+
source: resolved.type,
|
|
321
|
+
requestId: hit.requestId,
|
|
322
|
+
initialRecord: hit.record,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// 3. 校验位置参数 runId 是否存在与归属校验
|
|
327
|
+
const notFound = [];
|
|
328
|
+
for (const source of sources) {
|
|
329
|
+
if (!source.initialRecord) {
|
|
330
|
+
const record = await service.runs.get(source.runId, { signal: readSignal });
|
|
331
|
+
if (record) {
|
|
332
|
+
if (normalizedPackageId && record.packageId !== normalizedPackageId) {
|
|
333
|
+
notFound.push(source.runId);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
source.initialRecord = record;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
notFound.push(source.runId);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else if (normalizedPackageId && source.initialRecord.packageId !== normalizedPackageId) {
|
|
344
|
+
notFound.push(source.runId);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (notFound.length > 0) {
|
|
348
|
+
throw new ExecutionError(`Run record(s) not found ${whereDescription}: ${notFound.join(", ")}`);
|
|
349
|
+
}
|
|
350
|
+
if (sources.length === 0) {
|
|
351
|
+
throw new ArgumentError("At least one valid run is required for watch");
|
|
352
|
+
}
|
|
353
|
+
// 4. 等待结果
|
|
354
|
+
const remainingOverallMs = overallDeadline !== undefined
|
|
355
|
+
? Math.max(0, overallDeadline - Date.now())
|
|
356
|
+
: undefined;
|
|
357
|
+
const aggregation = await pollRunsUntilTerminal(sources, {
|
|
358
|
+
service,
|
|
359
|
+
intervalMs,
|
|
360
|
+
timeoutMs: remainingOverallMs,
|
|
361
|
+
deadline: overallDeadline,
|
|
362
|
+
signal: externalSignal,
|
|
363
|
+
timeoutSignal: timeoutController?.signal,
|
|
364
|
+
quiet: options.quiet || options.json,
|
|
365
|
+
context,
|
|
366
|
+
});
|
|
367
|
+
// 5. 输出结果(携带 reason、timedOut、interrupted 与逐运行结果)
|
|
368
|
+
if (options.json) {
|
|
369
|
+
writeStdout(JSON.stringify({
|
|
370
|
+
ok: aggregation.ok,
|
|
371
|
+
timedOut: aggregation.timedOut,
|
|
372
|
+
interrupted: aggregation.interrupted,
|
|
373
|
+
reason: aggregation.reason,
|
|
374
|
+
runs: aggregation.runs.map((r) => ({
|
|
375
|
+
runId: r.runId,
|
|
376
|
+
source: r.source,
|
|
377
|
+
...(r.requestId !== undefined ? { requestId: r.requestId } : {}),
|
|
378
|
+
status: r.status,
|
|
379
|
+
terminal: r.terminal,
|
|
380
|
+
...(r.data !== undefined ? { data: r.data } : {}),
|
|
381
|
+
...(r.error !== undefined ? { error: r.error } : {}),
|
|
382
|
+
})),
|
|
383
|
+
}, null, 2), context);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
writeStdout(renderWatchSummary(aggregation, resolved.type === "remote" ? remoteTargetSuffix(resolved) : undefined), context);
|
|
387
|
+
}
|
|
388
|
+
if (!aggregation.ok) {
|
|
389
|
+
if (context) {
|
|
390
|
+
context.exitCode = 1;
|
|
391
|
+
}
|
|
392
|
+
if (!context?.control) {
|
|
393
|
+
process.exitCode = 1;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}, {
|
|
397
|
+
localRoot: () => resolveLocalRunScope(options.package).targetPackageRoot,
|
|
398
|
+
scanLinkedPackages: !options.package,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
if (timeoutTimer)
|
|
403
|
+
clearTimeout(timeoutTimer);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
143
406
|
// runs cancel
|
|
144
407
|
applyTargetOptions(runsCmd
|
|
145
408
|
.command("cancel <id>")
|
package/dist/commands/serve.js
CHANGED
|
@@ -213,7 +213,11 @@ export function registerServeCommand(program, context) {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
-
catch {
|
|
216
|
+
catch (err) {
|
|
217
|
+
// 防御与透明:解析动作白名单引用失败时输出警告,不中断服务启动但确保配置异常可观测
|
|
218
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
219
|
+
writeStderr(`Warning: Invalid action reference '${actRef}' in action allowlist: ${reason}`, context);
|
|
220
|
+
}
|
|
217
221
|
}
|
|
218
222
|
}
|
|
219
223
|
}
|
|
@@ -222,7 +226,6 @@ export function registerServeCommand(program, context) {
|
|
|
222
226
|
const platform = createNodePlatform({
|
|
223
227
|
customHome: context?.customHome,
|
|
224
228
|
dataDir: options.dataDir || context?.dataDir,
|
|
225
|
-
rootDir: projectRoot || undefined,
|
|
226
229
|
});
|
|
227
230
|
const service = await createActionDock({
|
|
228
231
|
type: "local",
|
package/dist/commands/state.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { decodeStateKey, } from "@actiondock/sdk";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { loadProjectConfig, } from "@actiondock/core";
|
|
3
|
+
import { filterWithFallbackInfo, } from "@actiondock/core/project";
|
|
4
|
+
import { listLinkedPackages, } from "@actiondock/core/registry";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { ArgumentError, ExecutionError, NO_PROJECT_NO_LINKED_MESSAGE } from "../errors.js";
|
|
5
7
|
import { renderResult, renderStateList, writeStderr, writeStdout } from "../renderer.js";
|
|
6
|
-
import { applyTargetOptions, getEffectiveOptions, getTargetRoot, remoteTargetLabel, resolveFallbackStrategy, resolveIntent, resolveTargetFromOptions, withService, } from "../utils/index.js";
|
|
7
|
-
import { renderLinkedPackagesStateList, renderProjectScopedStateList, } from "./state-list.js";
|
|
8
|
+
import { applyTargetOptions, getEffectiveOptions, getTargetRoot, remoteTargetLabel, requirePackageRoot, resolveFallbackStrategy, resolveIntent, resolveLocalPackageRoot, resolveTargetFromOptions, withService, } from "../utils/index.js";
|
|
8
9
|
/**
|
|
9
10
|
* 统一预解码状态键寻址(get、set、delete 共享的单一事实源)。
|
|
10
11
|
*
|
|
@@ -68,6 +69,81 @@ function resolveStateScope(resolved, options, rawKey) {
|
|
|
68
69
|
localRoot: root,
|
|
69
70
|
};
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* 渲染单个项目作用域的状态键列表(含 --detail JSON 明细分支)。
|
|
74
|
+
*/
|
|
75
|
+
async function renderProjectScopedStateList(args) {
|
|
76
|
+
const { service, targetRoot, actionId, prefix, options, effectiveIntent, shouldFallback, context } = args;
|
|
77
|
+
const projConfig = loadProjectConfig(targetRoot);
|
|
78
|
+
const allKeys = (await service.management?.state.list(projConfig.id, actionId, {
|
|
79
|
+
namespace: options.namespace !== undefined ? options.namespace : null,
|
|
80
|
+
prefix,
|
|
81
|
+
})) ?? [];
|
|
82
|
+
if (options.detail && options.json) {
|
|
83
|
+
const entries = [];
|
|
84
|
+
for (const k of allKeys) {
|
|
85
|
+
const entry = await service.management?.state.get(projConfig.id, actionId, k, {
|
|
86
|
+
namespace: options.namespace,
|
|
87
|
+
detail: true,
|
|
88
|
+
});
|
|
89
|
+
if (entry) {
|
|
90
|
+
entries.push(entry);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const filterRes = filterWithFallbackInfo(entries, effectiveIntent, [(e) => e.fullKey, (e) => e.key, (e) => e.namespace], shouldFallback);
|
|
94
|
+
renderResult(filterRes.items, {
|
|
95
|
+
json: true,
|
|
96
|
+
context,
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const filterRes = filterWithFallbackInfo(allKeys, effectiveIntent, [(k) => k], shouldFallback);
|
|
101
|
+
renderResult(filterRes.items, {
|
|
102
|
+
json: options.json,
|
|
103
|
+
humanFormatter: () => renderStateList(filterRes.items, `${projConfig.name} (${projConfig.id})`, filterRes.isFallback, effectiveIntent),
|
|
104
|
+
context,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 渲染全部链接包聚合的状态键列表(跳过磁盘失效与清单损坏的链接项)。
|
|
109
|
+
*/
|
|
110
|
+
async function renderLinkedPackagesStateList(args) {
|
|
111
|
+
const { service, actionId, prefix, options, effectiveIntent, shouldFallback, context } = args;
|
|
112
|
+
// 扫描所有链接的包状态
|
|
113
|
+
const linked = listLinkedPackages();
|
|
114
|
+
if (linked.length === 0) {
|
|
115
|
+
renderResult([], {
|
|
116
|
+
json: options.json,
|
|
117
|
+
humanFormatter: () => NO_PROJECT_NO_LINKED_MESSAGE,
|
|
118
|
+
context,
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const aggregatedKeys = [];
|
|
123
|
+
for (const pkg of linked) {
|
|
124
|
+
if (!existsSync(pkg.path))
|
|
125
|
+
continue;
|
|
126
|
+
try {
|
|
127
|
+
const config = loadProjectConfig(pkg.path);
|
|
128
|
+
const keys = (await service.management?.state.list(config.id, actionId, {
|
|
129
|
+
namespace: options.namespace !== undefined ? options.namespace : null,
|
|
130
|
+
prefix,
|
|
131
|
+
})) ?? [];
|
|
132
|
+
for (const k of keys) {
|
|
133
|
+
aggregatedKeys.push(`${config.id}/${k}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// 单个链接包清单损坏或状态读取失败时跳过,继续聚合其余包
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const filterRes = filterWithFallbackInfo(aggregatedKeys, effectiveIntent, [(k) => k], shouldFallback);
|
|
141
|
+
renderResult(filterRes.items, {
|
|
142
|
+
json: options.json,
|
|
143
|
+
humanFormatter: () => renderStateList(filterRes.items, "Linked Packages State Store", filterRes.isFallback, effectiveIntent),
|
|
144
|
+
context,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
71
147
|
/**
|
|
72
148
|
* 注册 state 状态管理命令(get、set、delete、clear、keys、list)。
|
|
73
149
|
*
|
|
@@ -99,16 +175,9 @@ export function registerStateCommands(program, context) {
|
|
|
99
175
|
return;
|
|
100
176
|
}
|
|
101
177
|
// 本地工程与链接包模式:按目标根目录决策输出范围
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (!targetRoot) {
|
|
106
|
-
throw packageNotFoundError(options.package);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
targetRoot = findProjectRoot();
|
|
111
|
-
}
|
|
178
|
+
const targetRoot = options.package
|
|
179
|
+
? requirePackageRoot(options.package).root
|
|
180
|
+
: resolveLocalPackageRoot();
|
|
112
181
|
if (targetRoot) {
|
|
113
182
|
await renderProjectScopedStateList({
|
|
114
183
|
service,
|