@actiondock/cli 2.10.0 → 2.11.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 +2 -2
- package/bin/ad.js +225 -5
- package/dist/commands/action/create.d.ts +1 -1
- package/dist/commands/action/create.js +6 -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 +2 -1
- package/dist/commands/run.js +4 -56
- 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 +51 -0
- package/dist/renderers/action.js +112 -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/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,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { type InvocationControl, type CliContext } from "./types.js";
|
|
1
|
+
import { type InvocationControl, type CliContext, type StandaloneOptions } from "./types.js";
|
|
2
2
|
export * from "./types.js";
|
|
3
3
|
export * from "./errors.js";
|
|
4
4
|
export * from "./renderer.js";
|
|
5
5
|
export * from "./prompt.js";
|
|
6
|
-
export * from "./standalone/index.js";
|
|
7
6
|
export * from "./utils/index.js";
|
|
8
7
|
export * from "./services/index.js";
|
|
9
8
|
export * from "./commands/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* 运行独立二进制参数解析分发器并返回退出状态码(委托 Core 统一实现)。
|
|
11
|
+
*/
|
|
12
|
+
export declare function runStandaloneCli(argv: string[] | undefined, options: StandaloneOptions, control?: InvocationControl): Promise<number>;
|
|
13
|
+
/**
|
|
14
|
+
* 独立二进制进程入口适配器(事实源位于 core 包,此处 re-export 维持既有导入路径兼容)。
|
|
15
|
+
* 仅在命令行可执行入口调用,负责全局 SIGINT 监听与退出状态码写入。
|
|
16
|
+
*/
|
|
17
|
+
export { runStandaloneProcess } from "@actiondock/core/server";
|
|
10
18
|
/**
|
|
11
19
|
* ActionDock CLI 主执行入口函数。
|
|
12
20
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CommanderError } from "commander";
|
|
2
|
-
import {
|
|
2
|
+
import { StandaloneDispatcher } from "@actiondock/core/server";
|
|
3
|
+
import { createCliProgram, isNodeArgv } from "./commands/index.js";
|
|
3
4
|
import { formatError, SigintError } from "./errors.js";
|
|
4
5
|
import { renderError } from "./renderer.js";
|
|
5
6
|
import { ExitCode } from "./types.js";
|
|
@@ -7,10 +8,21 @@ export * from "./types.js";
|
|
|
7
8
|
export * from "./errors.js";
|
|
8
9
|
export * from "./renderer.js";
|
|
9
10
|
export * from "./prompt.js";
|
|
10
|
-
export * from "./standalone/index.js";
|
|
11
11
|
export * from "./utils/index.js";
|
|
12
12
|
export * from "./services/index.js";
|
|
13
13
|
export * from "./commands/index.js";
|
|
14
|
+
/**
|
|
15
|
+
* 运行独立二进制参数解析分发器并返回退出状态码(委托 Core 统一实现)。
|
|
16
|
+
*/
|
|
17
|
+
export async function runStandaloneCli(argv = process.argv.slice(2), options, control) {
|
|
18
|
+
const dispatcher = new StandaloneDispatcher(options);
|
|
19
|
+
return dispatcher.dispatch(argv, control);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 独立二进制进程入口适配器(事实源位于 core 包,此处 re-export 维持既有导入路径兼容)。
|
|
23
|
+
* 仅在命令行可执行入口调用,负责全局 SIGINT 监听与退出状态码写入。
|
|
24
|
+
*/
|
|
25
|
+
export { runStandaloneProcess } from "@actiondock/core/server";
|
|
14
26
|
/**
|
|
15
27
|
* ActionDock CLI 主执行入口函数。
|
|
16
28
|
*
|
|
@@ -22,16 +34,7 @@ export async function main(argv = process.argv, control, contextOptions) {
|
|
|
22
34
|
const context = { control, exitCode: 0, ...contextOptions };
|
|
23
35
|
const program = createCliProgram(context);
|
|
24
36
|
const isMachine = argv.includes("--json");
|
|
25
|
-
const normalizedArgv = argv
|
|
26
|
-
(argv[0].endsWith("node") ||
|
|
27
|
-
argv[0].endsWith("bun") ||
|
|
28
|
-
argv[0].endsWith("tsx") ||
|
|
29
|
-
argv[1].includes("/") ||
|
|
30
|
-
argv[1].includes("\\") ||
|
|
31
|
-
argv[1].endsWith(".js") ||
|
|
32
|
-
argv[1].endsWith(".ts"))
|
|
33
|
-
? argv
|
|
34
|
-
: ["node", "ad", ...argv];
|
|
37
|
+
const normalizedArgv = isNodeArgv(argv) ? argv : ["node", "ad", ...argv];
|
|
35
38
|
try {
|
|
36
39
|
await program.parseAsync(normalizedArgv);
|
|
37
40
|
if (control?.cancellationSource === "sigint" && control?.signal?.aborted) {
|
package/dist/prompt.d.ts
CHANGED
|
@@ -34,6 +34,9 @@ export declare function promptText(promptText: string, options?: {
|
|
|
34
34
|
export declare function readStreamLine(stream: NodeJS.ReadableStream): Promise<string>;
|
|
35
35
|
/**
|
|
36
36
|
* 从流中完整读取所有数据,并移除末尾的换行符。
|
|
37
|
+
*
|
|
38
|
+
* 委托 readStdinBounded 统一收集:保留宽松 UTF-8 解码与 BOM,
|
|
39
|
+
* 补齐默认 10MB 上限保护,流错误通过 INPUT_FILE_READ_FAILED 透传。
|
|
37
40
|
*/
|
|
38
41
|
export declare function readEntireStdin(stream?: NodeJS.ReadableStream): Promise<string>;
|
|
39
42
|
/**
|
package/dist/prompt.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
|
+
import { readStdinBounded } from "@actiondock/core/project";
|
|
2
3
|
import { ArgumentError, SigintError } from "./errors.js";
|
|
3
4
|
/**
|
|
4
5
|
* 在终端中安全交互式读取用户输入的密码或敏感信息。
|
|
@@ -126,22 +127,13 @@ export async function readStreamLine(stream) {
|
|
|
126
127
|
}
|
|
127
128
|
/**
|
|
128
129
|
* 从流中完整读取所有数据,并移除末尾的换行符。
|
|
130
|
+
*
|
|
131
|
+
* 委托 readStdinBounded 统一收集:保留宽松 UTF-8 解码与 BOM,
|
|
132
|
+
* 补齐默认 10MB 上限保护,流错误通过 INPUT_FILE_READ_FAILED 透传。
|
|
129
133
|
*/
|
|
130
134
|
export async function readEntireStdin(stream = process.stdin) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
stream.on("data", (chunk) => {
|
|
134
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
135
|
-
});
|
|
136
|
-
stream.once("end", () => {
|
|
137
|
-
const content = Buffer.concat(chunks).toString("utf-8");
|
|
138
|
-
resolve(content.replace(/[\r\n]+$/, ""));
|
|
139
|
-
});
|
|
140
|
-
stream.once("error", reject);
|
|
141
|
-
if ("resume" in stream && typeof stream.resume === "function") {
|
|
142
|
-
stream.resume();
|
|
143
|
-
}
|
|
144
|
-
});
|
|
135
|
+
const content = await readStdinBounded(stream, { strictUtf8: false });
|
|
136
|
+
return content.replace(/[\r\n]+$/, "");
|
|
145
137
|
}
|
|
146
138
|
/**
|
|
147
139
|
* 解析并获取配置项的具体输入值。
|