@actiondock/cli 2.6.0 → 2.8.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 +34 -22
- package/bin/ad.js +4 -4
- package/dist/commands/action/create.js +9 -25
- package/dist/commands/add.js +9 -17
- package/dist/commands/build.js +3 -18
- package/dist/commands/config/delete.js +8 -7
- package/dist/commands/config/env.js +7 -11
- package/dist/commands/config/get.js +8 -5
- package/dist/commands/config/list.js +16 -22
- package/dist/commands/config/schema.d.ts +7 -1
- package/dist/commands/config/schema.js +44 -67
- package/dist/commands/config/set.js +9 -7
- package/dist/commands/describe.js +15 -26
- package/dist/commands/doctor.js +4 -3
- package/dist/commands/export.js +5 -13
- package/dist/commands/generate.js +4 -3
- package/dist/commands/info.js +3 -1
- package/dist/commands/init.js +3 -3
- package/dist/commands/link.js +4 -4
- package/dist/commands/list.js +19 -10
- package/dist/commands/mcp.js +27 -66
- package/dist/commands/pack.js +3 -12
- package/dist/commands/playbook.js +30 -10
- package/dist/commands/profile.js +10 -19
- package/dist/commands/remove.js +5 -14
- package/dist/commands/run.d.ts +6 -2
- package/dist/commands/run.js +60 -37
- package/dist/commands/runs.js +26 -35
- package/dist/commands/serve.js +66 -44
- package/dist/commands/state-list.d.ts +2 -2
- package/dist/commands/state-list.js +10 -8
- package/dist/commands/state.d.ts +2 -2
- package/dist/commands/state.js +110 -103
- package/dist/commands/test.js +1 -1
- package/dist/commands/validate.js +4 -17
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +78 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.js +60 -8
- package/dist/renderer.d.ts +3 -2
- package/dist/renderer.js +4 -19
- package/dist/services/config-merge.d.ts +6 -5
- package/dist/services/config-merge.js +10 -8
- package/dist/services/inspect.js +3 -1
- package/dist/standalone/index.d.ts +7 -11
- package/dist/standalone/index.js +8 -17
- package/dist/types.d.ts +7 -1
- package/dist/types.js +1 -1
- package/dist/utils/cert.js +1 -1
- package/dist/utils/duration.d.ts +1 -1
- package/dist/utils/duration.js +1 -1
- package/dist/utils/index.d.ts +1 -2
- package/dist/utils/index.js +1 -2
- package/dist/utils/input.d.ts +5 -42
- package/dist/utils/input.js +7 -84
- package/dist/utils/server-launch.d.ts +61 -0
- package/dist/utils/server-launch.js +114 -0
- package/dist/utils/target.d.ts +14 -13
- package/dist/utils/target.js +20 -39
- package/dist/utils/utils.d.ts +101 -1
- package/dist/utils/utils.js +97 -7
- package/package.json +5 -6
- package/dist/utils/bytes.d.ts +0 -1
- package/dist/utils/bytes.js +0 -1
- package/dist/utils/filter.d.ts +0 -1
- package/dist/utils/filter.js +0 -1
package/dist/commands/state.js
CHANGED
|
@@ -1,32 +1,78 @@
|
|
|
1
|
-
import { decodeStateKey,
|
|
1
|
+
import { decodeStateKey, } from "@actiondock/sdk";
|
|
2
|
+
import { findProjectRoot, loadProjectConfig, } from "@actiondock/core";
|
|
3
|
+
import { resolvePackageRoot, } from "@actiondock/core/registry";
|
|
2
4
|
import { ArgumentError, ExecutionError, packageNotFoundError } from "../errors.js";
|
|
3
5
|
import { renderResult, renderStateList, writeStderr, writeStdout } from "../renderer.js";
|
|
4
|
-
import { applyTargetOptions, getEffectiveOptions, getTargetRoot, resolveIntent,
|
|
6
|
+
import { applyTargetOptions, getEffectiveOptions, getTargetRoot, remoteTargetLabel, resolveFallbackStrategy, resolveIntent, resolveTargetFromOptions, withService, } from "../utils/index.js";
|
|
5
7
|
import { renderLinkedPackagesStateList, renderProjectScopedStateList, } from "./state-list.js";
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
9
|
+
* 统一预解码状态键寻址(get、set、delete 共享的单一事实源)。
|
|
8
10
|
*
|
|
9
|
-
*
|
|
11
|
+
* 三命令在 CLI 层即完成 `ns:key` 复合键解耦:
|
|
12
|
+
* - 显式 `-n/--namespace` 指定时命名空间以其为准,键保持原串不拆解;
|
|
13
|
+
* - 未显式指定时按复合键语法预解码出命名空间与裸键;
|
|
14
|
+
* - 多冒号歧义键降级为整串裸键并提示。
|
|
15
|
+
*
|
|
16
|
+
* 歧义键(如 `review:owner/repo:42`)不再直接崩溃:
|
|
10
17
|
* 降级为「整串作为裸 key」并在 stderr 提示可用 -n/--namespace 显式指定,
|
|
11
18
|
* 保持与本地存储层歧义兜底(deleteStateSmart 的纯键回退)一致的容错语义。
|
|
19
|
+
*
|
|
20
|
+
* 本地与远端分支都消费解耦后的 namespace + key,
|
|
21
|
+
* 消除「set 预解码而 get/delete 依赖存储层回退」的语义不对称。
|
|
12
22
|
*/
|
|
13
|
-
function
|
|
23
|
+
function resolveStateAddress(rawKey, options, context) {
|
|
24
|
+
if (options.namespace !== undefined) {
|
|
25
|
+
return { namespace: options.namespace, key: rawKey };
|
|
26
|
+
}
|
|
14
27
|
try {
|
|
15
28
|
const decoded = decodeStateKey(rawKey);
|
|
16
29
|
return { namespace: decoded.namespace || undefined, key: decoded.key };
|
|
17
30
|
}
|
|
18
31
|
catch {
|
|
19
|
-
|
|
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
|
-
}
|
|
32
|
+
writeStderr(`[WARN] State key '${rawKey}' contains multiple colon delimiters; treating it as a bare key. Use -n/--namespace to specify the namespace explicitly.`, context);
|
|
22
33
|
return { namespace: undefined, key: rawKey };
|
|
23
34
|
}
|
|
24
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* 解析状态命令目标作用域(remote 与 local 双分支模板的单一事实源)。
|
|
38
|
+
*
|
|
39
|
+
* 远端分支直接以 -P 参数寻址,消息后缀固定为 "on remote server";
|
|
40
|
+
* 本地分支基于 getTargetRoot 完成包寻址(含从键中剥离 pkg/ 前缀),
|
|
41
|
+
* 以工程配置 id 寻址,消息后缀为 "in/from package '...'"。
|
|
42
|
+
*
|
|
43
|
+
* @param resolved 目标拓扑解析结果
|
|
44
|
+
* @param options 命令选项视图
|
|
45
|
+
* @param rawKey 原始键(本地分支用于剥离 pkg/ 前缀)
|
|
46
|
+
* @returns 作用域解析结果(含包标识、消息后缀、本地根目录与工程配置)
|
|
47
|
+
*/
|
|
48
|
+
function resolveStateScope(resolved, options, rawKey) {
|
|
49
|
+
if (resolved.type === "remote") {
|
|
50
|
+
return {
|
|
51
|
+
packageId: options.package || "",
|
|
52
|
+
scopeSuffix: "on remote server",
|
|
53
|
+
deleteSuffix: "on remote server",
|
|
54
|
+
effectiveKey: rawKey || "",
|
|
55
|
+
projConfig: undefined,
|
|
56
|
+
localRoot: undefined,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// 本地项目模式:getTargetRoot 会从键中剥离 pkg/ 前缀,寻址基于解耦后的裸键
|
|
60
|
+
const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
|
|
61
|
+
const projConfig = loadProjectConfig(root);
|
|
62
|
+
return {
|
|
63
|
+
packageId: projConfig.id,
|
|
64
|
+
scopeSuffix: `in package '${projConfig.id}'`,
|
|
65
|
+
deleteSuffix: `from package '${projConfig.id}'`,
|
|
66
|
+
effectiveKey,
|
|
67
|
+
projConfig,
|
|
68
|
+
localRoot: root,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
25
71
|
/**
|
|
26
72
|
* 注册 state 状态管理命令(get、set、delete、clear、keys、list)。
|
|
27
73
|
*
|
|
28
|
-
* @param program Commander
|
|
29
|
-
* @param context
|
|
74
|
+
* @param program Commander 根程序对象
|
|
75
|
+
* @param context CLI 上下文
|
|
30
76
|
*/
|
|
31
77
|
export function registerStateCommands(program, context) {
|
|
32
78
|
const stateCmd = program
|
|
@@ -36,18 +82,18 @@ export function registerStateCommands(program, context) {
|
|
|
36
82
|
const handleListKeys = async (prefix = "", rawOptions, cmd) => {
|
|
37
83
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
38
84
|
const effectiveIntent = resolveIntent(options.intent, prefix ? [prefix] : []);
|
|
39
|
-
const shouldFallback = options
|
|
40
|
-
await
|
|
85
|
+
const { shouldFallback } = resolveFallbackStrategy(options);
|
|
86
|
+
await withService(options, context, async (service, resolved) => {
|
|
41
87
|
const actionId = options.action || "";
|
|
42
88
|
// 远端服务模式:直接列举远端包作用域键
|
|
43
89
|
if (resolved.type === "remote") {
|
|
44
|
-
const keys = await
|
|
90
|
+
const keys = (await service.management?.state.list(options.package || "", actionId, {
|
|
45
91
|
namespace: options.namespace,
|
|
46
92
|
prefix,
|
|
47
|
-
});
|
|
93
|
+
})) ?? [];
|
|
48
94
|
renderResult(keys, {
|
|
49
95
|
json: options.json,
|
|
50
|
-
humanFormatter: () => renderStateList(keys,
|
|
96
|
+
humanFormatter: () => renderStateList(keys, remoteTargetLabel(resolved), false, effectiveIntent),
|
|
51
97
|
context,
|
|
52
98
|
});
|
|
53
99
|
return;
|
|
@@ -65,7 +111,7 @@ export function registerStateCommands(program, context) {
|
|
|
65
111
|
}
|
|
66
112
|
if (targetRoot) {
|
|
67
113
|
await renderProjectScopedStateList({
|
|
68
|
-
|
|
114
|
+
service,
|
|
69
115
|
targetRoot,
|
|
70
116
|
actionId,
|
|
71
117
|
prefix,
|
|
@@ -77,7 +123,7 @@ export function registerStateCommands(program, context) {
|
|
|
77
123
|
return;
|
|
78
124
|
}
|
|
79
125
|
await renderLinkedPackagesStateList({
|
|
80
|
-
|
|
126
|
+
service,
|
|
81
127
|
actionId,
|
|
82
128
|
prefix,
|
|
83
129
|
options,
|
|
@@ -96,6 +142,7 @@ export function registerStateCommands(program, context) {
|
|
|
96
142
|
.option("-n, --namespace <ns>", "Filter keys under specific namespace (omit to list all namespaces)"))
|
|
97
143
|
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
|
|
98
144
|
.option("--detail", "Include metadata (ttl, expiresAt, size, updatedAt) in JSON output")
|
|
145
|
+
.option("--fallback", "Enable fallback to full list when no items match intent")
|
|
99
146
|
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
100
147
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
101
148
|
.option("--json", "Output as JSON")
|
|
@@ -109,6 +156,7 @@ export function registerStateCommands(program, context) {
|
|
|
109
156
|
.option("-n, --namespace <ns>", "Filter keys under specific namespace"))
|
|
110
157
|
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter")
|
|
111
158
|
.option("--detail", "Include metadata in JSON output")
|
|
159
|
+
.option("--fallback", "Enable fallback to full list when no items match intent")
|
|
112
160
|
.option("--no-fallback", "Disable fallback")
|
|
113
161
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
114
162
|
.option("--json", "Output as JSON")
|
|
@@ -127,45 +175,28 @@ export function registerStateCommands(program, context) {
|
|
|
127
175
|
if (!rawKey) {
|
|
128
176
|
throw new ArgumentError("State key is required");
|
|
129
177
|
}
|
|
130
|
-
|
|
178
|
+
// 惰性作用域缓存:仅本地分支求值一次,避免远端模式触发包寻址副作用
|
|
179
|
+
let scope;
|
|
180
|
+
const resolveScope = (resolved) => (scope ??= resolveStateScope(resolved, options, rawKey));
|
|
181
|
+
await withService(options, context, async (service, resolved) => {
|
|
131
182
|
const actionId = options.action || "";
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const entry = await target.getState(options.package || "", actionId, actualKey, {
|
|
137
|
-
namespace: effectiveNamespace,
|
|
138
|
-
detail: true,
|
|
139
|
-
});
|
|
140
|
-
if (entry === undefined || entry.value === undefined) {
|
|
141
|
-
throw new ExecutionError(`State key '${rawKey}' not found on remote server`);
|
|
142
|
-
}
|
|
143
|
-
const val = entry.value;
|
|
144
|
-
renderResult({ key: rawKey, value: val, namespace: entry.namespace || effectiveNamespace }, {
|
|
145
|
-
json: options.json,
|
|
146
|
-
humanFormatter: () => (typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)),
|
|
147
|
-
context,
|
|
148
|
-
});
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
// 本地项目模式
|
|
152
|
-
const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
|
|
153
|
-
const projConfig = loadProjectConfig(root);
|
|
154
|
-
const entry = await target.getState(projConfig.id, actionId, effectiveKey, {
|
|
155
|
-
namespace: options.namespace,
|
|
183
|
+
const targetScope = resolveScope(resolved);
|
|
184
|
+
const address = resolveStateAddress(targetScope.effectiveKey, options, context);
|
|
185
|
+
const entry = await service.management?.state.get(targetScope.packageId, actionId, address.key, {
|
|
186
|
+
namespace: address.namespace,
|
|
156
187
|
detail: true,
|
|
157
188
|
});
|
|
158
189
|
if (entry === undefined || entry.value === undefined) {
|
|
159
|
-
throw new ExecutionError(`State key '${rawKey}' not found
|
|
190
|
+
throw new ExecutionError(`State key '${rawKey}' not found ${targetScope.scopeSuffix}`);
|
|
160
191
|
}
|
|
161
192
|
const val = entry.value;
|
|
162
|
-
const matchedNamespace = entry.namespace;
|
|
193
|
+
const matchedNamespace = resolved.type === "remote" ? (entry.namespace || address.namespace) : entry.namespace;
|
|
163
194
|
renderResult({ key: rawKey, value: val, namespace: matchedNamespace }, {
|
|
164
195
|
json: options.json,
|
|
165
196
|
humanFormatter: () => (typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)),
|
|
166
197
|
context,
|
|
167
198
|
});
|
|
168
|
-
}, { localRoot: () =>
|
|
199
|
+
}, { localRoot: () => resolveScope(resolveTargetFromOptions(options, context)).localRoot });
|
|
169
200
|
});
|
|
170
201
|
// state set <key> <value>
|
|
171
202
|
applyTargetOptions(stateCmd
|
|
@@ -192,36 +223,21 @@ export function registerStateCommands(program, context) {
|
|
|
192
223
|
if (options.ttl && (isNaN(ttlSec) || ttlSec <= 0)) {
|
|
193
224
|
throw new ArgumentError(`Invalid --ttl value: '${options.ttl}'. Must be a positive integer.`);
|
|
194
225
|
}
|
|
195
|
-
|
|
226
|
+
// 惰性作用域缓存:仅本地分支求值一次,避免远端模式触发包寻址副作用
|
|
227
|
+
let scope;
|
|
228
|
+
const resolveScope = (resolved) => (scope ??= resolveStateScope(resolved, options, rawKey));
|
|
229
|
+
await withService(options, context, async (service, resolved) => {
|
|
196
230
|
const actionId = options.action || "";
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
await target.setState(options.package || "", actionId, actualKey, parsedVal, {
|
|
202
|
-
namespace: effectiveNamespace,
|
|
203
|
-
ttl: ttlSec,
|
|
204
|
-
});
|
|
205
|
-
writeStdout(`[OK] State '${rawKey}' updated on remote server`, context);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
// 本地项目模式
|
|
209
|
-
const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
|
|
210
|
-
const projConfig = loadProjectConfig(root);
|
|
211
|
-
let actualNamespace = options.namespace;
|
|
212
|
-
let finalKey = effectiveKey;
|
|
213
|
-
if (options.namespace === undefined && effectiveKey.includes(":")) {
|
|
214
|
-
const decoded = decodeStateKeyWithFallback(effectiveKey, false, context);
|
|
215
|
-
actualNamespace = decoded.namespace;
|
|
216
|
-
finalKey = decoded.key;
|
|
217
|
-
}
|
|
218
|
-
await target.setState(projConfig.id, actionId, finalKey, parsedVal, {
|
|
219
|
-
namespace: actualNamespace,
|
|
231
|
+
const targetScope = resolveScope(resolved);
|
|
232
|
+
const address = resolveStateAddress(targetScope.effectiveKey, options, context);
|
|
233
|
+
await service.management?.state.set(targetScope.packageId, actionId, address.key, parsedVal, {
|
|
234
|
+
namespace: address.namespace,
|
|
220
235
|
ttl: ttlSec,
|
|
221
236
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
237
|
+
// 远端回显原始键;本地回显解耦后的 ns:key 复合视图
|
|
238
|
+
const displayKey = resolved.type === "remote" ? rawKey : (address.namespace ? `${address.namespace}:${address.key}` : address.key);
|
|
239
|
+
writeStdout(`[OK] State '${displayKey}' updated ${targetScope.scopeSuffix}`, context);
|
|
240
|
+
}, { localRoot: () => resolveScope(resolveTargetFromOptions(options, context)).localRoot });
|
|
225
241
|
});
|
|
226
242
|
// state delete <key>
|
|
227
243
|
applyTargetOptions(stateCmd
|
|
@@ -237,32 +253,21 @@ export function registerStateCommands(program, context) {
|
|
|
237
253
|
if (!rawKey) {
|
|
238
254
|
throw new ArgumentError("State key is required");
|
|
239
255
|
}
|
|
240
|
-
|
|
256
|
+
// 惰性作用域缓存:仅本地分支求值一次,避免远端模式触发包寻址副作用
|
|
257
|
+
let scope;
|
|
258
|
+
const resolveScope = (resolved) => (scope ??= resolveStateScope(resolved, options, rawKey));
|
|
259
|
+
await withService(options, context, async (service, resolved) => {
|
|
241
260
|
const actionId = options.action || "";
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const deleted = await target.deleteState(options.package || "", actionId, actualKey, {
|
|
247
|
-
namespace: effectiveNamespace,
|
|
248
|
-
});
|
|
249
|
-
if (!deleted) {
|
|
250
|
-
throw new ExecutionError(`State key '${rawKey}' not found on remote server`);
|
|
251
|
-
}
|
|
252
|
-
writeStdout(`[OK] State '${rawKey}' deleted on remote server`, context);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
// 本地项目模式
|
|
256
|
-
const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
|
|
257
|
-
const projConfig = loadProjectConfig(root);
|
|
258
|
-
const deleted = await target.deleteState(projConfig.id, actionId, effectiveKey, {
|
|
259
|
-
namespace: options.namespace,
|
|
261
|
+
const targetScope = resolveScope(resolved);
|
|
262
|
+
const address = resolveStateAddress(targetScope.effectiveKey, options, context);
|
|
263
|
+
const deleted = await service.management?.state.delete(targetScope.packageId, actionId, address.key, {
|
|
264
|
+
namespace: address.namespace,
|
|
260
265
|
});
|
|
261
266
|
if (!deleted) {
|
|
262
|
-
throw new ExecutionError(`State key '${rawKey}' not found
|
|
267
|
+
throw new ExecutionError(`State key '${rawKey}' not found ${targetScope.scopeSuffix}`);
|
|
263
268
|
}
|
|
264
|
-
writeStdout(`[OK] State '${rawKey}' deleted
|
|
265
|
-
}, { localRoot: () =>
|
|
269
|
+
writeStdout(`[OK] State '${rawKey}' deleted ${targetScope.deleteSuffix}`, context);
|
|
270
|
+
}, { localRoot: () => resolveScope(resolveTargetFromOptions(options, context)).localRoot });
|
|
266
271
|
});
|
|
267
272
|
// state clear
|
|
268
273
|
applyTargetOptions(stateCmd
|
|
@@ -279,27 +284,29 @@ export function registerStateCommands(program, context) {
|
|
|
279
284
|
if (!options.all && options.namespace === undefined) {
|
|
280
285
|
throw new ArgumentError("Must specify either -n/--namespace <name> to clear specific namespace, or --all to clear entire package state store.");
|
|
281
286
|
}
|
|
282
|
-
|
|
287
|
+
// 惰性作用域缓存:仅本地分支求值一次,避免远端模式触发包寻址副作用
|
|
288
|
+
let scope;
|
|
289
|
+
const resolveScope = (resolved) => (scope ??= resolveStateScope(resolved, options));
|
|
290
|
+
await withService(options, context, async (service, resolved) => {
|
|
283
291
|
const actionId = options.action || "";
|
|
284
292
|
if (resolved.type === "remote") {
|
|
285
|
-
const count = await
|
|
293
|
+
const count = (await service.management?.state.clear(options.package || "", actionId, {
|
|
286
294
|
namespace: options.namespace,
|
|
287
295
|
all: Boolean(options.all),
|
|
288
|
-
});
|
|
296
|
+
})) ?? 0;
|
|
289
297
|
writeStdout(`[OK] Cleared ${count} state entry(s) on remote server`, context);
|
|
290
298
|
return;
|
|
291
299
|
}
|
|
292
300
|
// 本地项目模式
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
const count = await target.clearState(projConfig.id, actionId, {
|
|
301
|
+
const targetScope = resolveScope(resolved);
|
|
302
|
+
const count = (await service.management?.state.clear(targetScope.packageId, actionId, {
|
|
296
303
|
namespace: options.namespace,
|
|
297
304
|
all: Boolean(options.all),
|
|
298
|
-
});
|
|
305
|
+
})) ?? 0;
|
|
299
306
|
const scopeDesc = options.all
|
|
300
307
|
? "all namespaces"
|
|
301
308
|
: `namespace '${options.namespace}'`;
|
|
302
|
-
writeStdout(`[OK] Cleared ${count} state entry(s) in ${scopeDesc} for package '${projConfig.id}'`, context);
|
|
303
|
-
}, { localRoot: () =>
|
|
309
|
+
writeStdout(`[OK] Cleared ${count} state entry(s) in ${scopeDesc} for package '${targetScope.projConfig.id}'`, context);
|
|
310
|
+
}, { localRoot: () => resolveScope(resolveTargetFromOptions(options, context)).localRoot });
|
|
304
311
|
});
|
|
305
312
|
}
|
package/dist/commands/test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { findProjectRoot } from "@actiondock/core";
|
|
3
|
+
import { findProjectRoot, } from "@actiondock/core";
|
|
4
4
|
import { writeStderr } from "../renderer.js";
|
|
5
5
|
import { spawnAsync } from "../utils/index.js";
|
|
6
6
|
export function registerTestCommand(program) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { checkGeneratedTypes,
|
|
2
|
-
import { ArgumentError, ExecutionError
|
|
1
|
+
import { checkGeneratedTypes, GENERATED_TYPES_OUTDATED_CODE, loadActions, } from "@actiondock/core/project";
|
|
2
|
+
import { ArgumentError, ExecutionError } from "../errors.js";
|
|
3
3
|
import { renderActionValidation, renderResult } from "../renderer.js";
|
|
4
|
-
import { getEffectiveOptions } from "../utils/index.js";
|
|
4
|
+
import { getEffectiveOptions, requirePackageRoot } from "../utils/index.js";
|
|
5
5
|
/**
|
|
6
6
|
* 挂载 validate 子命令至指定 Commander 节点。
|
|
7
7
|
*
|
|
@@ -17,20 +17,7 @@ export function attachValidateCommand(parent, context) {
|
|
|
17
17
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
18
18
|
.action(async (id, rawOptions, cmd) => {
|
|
19
19
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
20
|
-
|
|
21
|
-
if (options.package) {
|
|
22
|
-
root = resolvePackageRoot(options.package);
|
|
23
|
-
if (!root) {
|
|
24
|
-
throw packageNotFoundError(options.package);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
root = findProjectRoot();
|
|
29
|
-
}
|
|
30
|
-
if (!root) {
|
|
31
|
-
throw notInProjectError("Please specify -P, --package <id> or cd into a project directory.");
|
|
32
|
-
}
|
|
33
|
-
const config = loadProjectConfig(root);
|
|
20
|
+
const { root, projConfig: config } = requirePackageRoot(options.package, { loadConfig: true });
|
|
34
21
|
const actions = await loadActions(root, config.actionsDir);
|
|
35
22
|
let toValidate = [];
|
|
36
23
|
if (id) {
|
package/dist/errors.d.ts
CHANGED
|
@@ -20,6 +20,20 @@ export declare class ArgumentError extends CliError {
|
|
|
20
20
|
export declare class ExecutionError extends CliError {
|
|
21
21
|
constructor(message: string, details?: unknown, code?: string);
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* 将任意异常统一包裹为 CLI 执行错误(错误码保留辅助)。
|
|
25
|
+
*
|
|
26
|
+
* 包裹规则:
|
|
27
|
+
* - CliError 与 ActionDockError 原样透传(保留原始 code、details 与退出码映射);
|
|
28
|
+
* - 其余异常包裹为 ExecutionError,并保留 cause 链接与数字型 code 字段,
|
|
29
|
+
* 避免 core 抛出的结构化错误码被一律降级为 EXECUTION_FAILURE。
|
|
30
|
+
* - messagePrefix 仅对需要包裹的分支生效(附加命令语境说明);
|
|
31
|
+
* 透传分支保持原始实例不变,不重建消息。
|
|
32
|
+
*
|
|
33
|
+
* @param err 待包裹的原始异常
|
|
34
|
+
* @param messagePrefix 可选的消息前缀(仅包裹分支生效)
|
|
35
|
+
*/
|
|
36
|
+
export declare function wrapAsExecutionError(err: unknown, messagePrefix?: string): Error;
|
|
23
37
|
/**
|
|
24
38
|
* 用户中断信号错误(退出码为 130)。
|
|
25
39
|
*/
|
package/dist/errors.js
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
+
import { ActionDockError, INVALID_ARGUMENT, INVALID_PACKAGE_ID, PACKAGE_NOT_FOUND, ACTION_NOT_FOUND, NOT_FOUND, PATH_TRAVERSAL, INVALID_FLAT_ARGUMENT, INVALID_JSON, INVALID_JSON_LITERAL, INPUT_PATH_CONFLICT, FLAT_INPUT_LIMIT_EXCEEDED, INPUT_LIMIT_EXCEEDED, INPUT_POLICY_VIOLATION, INPUT_CONFLICT, INPUT_FILE_NOT_FOUND, INPUT_FILE_READ_FAILED, } from "@actiondock/core";
|
|
2
|
+
import { InputError } from "@actiondock/core/project";
|
|
1
3
|
import { ExitCode } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* 扁平与解析输入错误码集合。
|
|
6
|
+
*/
|
|
7
|
+
const FLAT_ERROR_CODES = new Set([
|
|
8
|
+
INVALID_FLAT_ARGUMENT,
|
|
9
|
+
INVALID_JSON,
|
|
10
|
+
INVALID_JSON_LITERAL,
|
|
11
|
+
INPUT_PATH_CONFLICT,
|
|
12
|
+
FLAT_INPUT_LIMIT_EXCEEDED,
|
|
13
|
+
INPUT_LIMIT_EXCEEDED,
|
|
14
|
+
INPUT_POLICY_VIOLATION,
|
|
15
|
+
INPUT_CONFLICT,
|
|
16
|
+
INPUT_FILE_NOT_FOUND,
|
|
17
|
+
INPUT_FILE_READ_FAILED,
|
|
18
|
+
]);
|
|
2
19
|
/**
|
|
3
20
|
* CLI 信封错误码常量(仅 CLI 输出协议使用,属包内部码,就地常量化管理)。
|
|
4
21
|
*/
|
|
@@ -39,6 +56,33 @@ export class ExecutionError extends CliError {
|
|
|
39
56
|
this.name = "ExecutionError";
|
|
40
57
|
}
|
|
41
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* 将任意异常统一包裹为 CLI 执行错误(错误码保留辅助)。
|
|
61
|
+
*
|
|
62
|
+
* 包裹规则:
|
|
63
|
+
* - CliError 与 ActionDockError 原样透传(保留原始 code、details 与退出码映射);
|
|
64
|
+
* - 其余异常包裹为 ExecutionError,并保留 cause 链接与数字型 code 字段,
|
|
65
|
+
* 避免 core 抛出的结构化错误码被一律降级为 EXECUTION_FAILURE。
|
|
66
|
+
* - messagePrefix 仅对需要包裹的分支生效(附加命令语境说明);
|
|
67
|
+
* 透传分支保持原始实例不变,不重建消息。
|
|
68
|
+
*
|
|
69
|
+
* @param err 待包裹的原始异常
|
|
70
|
+
* @param messagePrefix 可选的消息前缀(仅包裹分支生效)
|
|
71
|
+
*/
|
|
72
|
+
export function wrapAsExecutionError(err, messagePrefix) {
|
|
73
|
+
if (err instanceof CliError || err instanceof ActionDockError) {
|
|
74
|
+
return err;
|
|
75
|
+
}
|
|
76
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
77
|
+
const message = messagePrefix ? `${messagePrefix}${originalMessage}` : originalMessage;
|
|
78
|
+
const rawCode = err?.code;
|
|
79
|
+
const code = typeof rawCode === "string" && rawCode ? rawCode : "EXECUTION_FAILURE";
|
|
80
|
+
const wrapped = new ExecutionError(message, undefined, code);
|
|
81
|
+
if (err instanceof Error) {
|
|
82
|
+
wrapped.cause = err;
|
|
83
|
+
}
|
|
84
|
+
return wrapped;
|
|
85
|
+
}
|
|
42
86
|
/**
|
|
43
87
|
* 用户中断信号错误(退出码为 130)。
|
|
44
88
|
*/
|
|
@@ -80,10 +124,41 @@ export function formatError(err) {
|
|
|
80
124
|
details: err.details,
|
|
81
125
|
};
|
|
82
126
|
}
|
|
127
|
+
if (err instanceof InputError) {
|
|
128
|
+
return {
|
|
129
|
+
code: err.code,
|
|
130
|
+
message: err.message,
|
|
131
|
+
exitCode: ExitCode.INVALID_ARGUMENT,
|
|
132
|
+
details: err.details,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (err instanceof ActionDockError) {
|
|
136
|
+
const isArgument = err.code === INVALID_ARGUMENT ||
|
|
137
|
+
err.code === INVALID_PACKAGE_ID ||
|
|
138
|
+
err.code === PACKAGE_NOT_FOUND ||
|
|
139
|
+
err.code === ACTION_NOT_FOUND ||
|
|
140
|
+
err.code === NOT_FOUND ||
|
|
141
|
+
err.code === PATH_TRAVERSAL ||
|
|
142
|
+
FLAT_ERROR_CODES.has(err.code);
|
|
143
|
+
return {
|
|
144
|
+
code: err.code,
|
|
145
|
+
message: err.message,
|
|
146
|
+
exitCode: isArgument ? ExitCode.INVALID_ARGUMENT : ExitCode.FAILURE,
|
|
147
|
+
details: err.details,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
83
150
|
// Commander.js 原生错误处理
|
|
84
151
|
if (typeof err === "object" && err !== null && "code" in err && typeof err.code === "string") {
|
|
152
|
+
const code = err.code;
|
|
153
|
+
if (FLAT_ERROR_CODES.has(code)) {
|
|
154
|
+
return {
|
|
155
|
+
code,
|
|
156
|
+
message: err.message || String(err),
|
|
157
|
+
exitCode: ExitCode.INVALID_ARGUMENT,
|
|
158
|
+
details: err.details,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
85
161
|
const commanderErr = err;
|
|
86
|
-
const code = commanderErr.code;
|
|
87
162
|
// 参数类异常
|
|
88
163
|
if (code.startsWith("commander.unknownOption") ||
|
|
89
164
|
code.startsWith("commander.missingArgument") ||
|
|
@@ -108,9 +183,10 @@ export function formatError(err) {
|
|
|
108
183
|
}
|
|
109
184
|
if (err instanceof Error) {
|
|
110
185
|
return {
|
|
111
|
-
code: CLI_CODE_ERROR,
|
|
186
|
+
code: err.code || CLI_CODE_ERROR,
|
|
112
187
|
message: err.message,
|
|
113
188
|
exitCode: ExitCode.FAILURE,
|
|
189
|
+
details: err.details,
|
|
114
190
|
};
|
|
115
191
|
}
|
|
116
192
|
return {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type InvocationControl, type CliContext } from "./types.js";
|
|
1
2
|
export * from "./types.js";
|
|
2
3
|
export * from "./errors.js";
|
|
3
4
|
export * from "./renderer.js";
|
|
@@ -10,6 +11,12 @@ export * from "./commands/index.js";
|
|
|
10
11
|
* ActionDock CLI 主执行入口函数。
|
|
11
12
|
*
|
|
12
13
|
* @param argv 命令行参数数组(默认使用 process.argv)
|
|
14
|
+
* @param control 可选的中断与取消控制契约
|
|
13
15
|
* @returns 进程退出码
|
|
14
16
|
*/
|
|
15
|
-
export declare function main(argv?: string[]): Promise<number>;
|
|
17
|
+
export declare function main(argv?: string[], control?: InvocationControl, contextOptions?: Partial<CliContext>): Promise<number>;
|
|
18
|
+
/**
|
|
19
|
+
* 命令行独立进程适配器。
|
|
20
|
+
* 仅在命令行可执行入口调用,负责全局 SIGINT 监听与退出状态码写入。
|
|
21
|
+
*/
|
|
22
|
+
export declare function runCliProcess(argv?: string[]): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CommanderError } from "commander";
|
|
2
2
|
import { createCliProgram } from "./commands/index.js";
|
|
3
|
-
import { formatError } from "./errors.js";
|
|
3
|
+
import { formatError, SigintError } from "./errors.js";
|
|
4
4
|
import { renderError } from "./renderer.js";
|
|
5
|
+
import { ExitCode } from "./types.js";
|
|
5
6
|
export * from "./types.js";
|
|
6
7
|
export * from "./errors.js";
|
|
7
8
|
export * from "./renderer.js";
|
|
@@ -14,16 +15,37 @@ export * from "./commands/index.js";
|
|
|
14
15
|
* ActionDock CLI 主执行入口函数。
|
|
15
16
|
*
|
|
16
17
|
* @param argv 命令行参数数组(默认使用 process.argv)
|
|
18
|
+
* @param control 可选的中断与取消控制契约
|
|
17
19
|
* @returns 进程退出码
|
|
18
20
|
*/
|
|
19
|
-
export async function main(argv = process.argv) {
|
|
20
|
-
const
|
|
21
|
+
export async function main(argv = process.argv, control, contextOptions) {
|
|
22
|
+
const context = { control, exitCode: 0, ...contextOptions };
|
|
23
|
+
const program = createCliProgram(context);
|
|
21
24
|
const isMachine = argv.includes("--json");
|
|
25
|
+
const normalizedArgv = argv.length >= 2 &&
|
|
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];
|
|
22
35
|
try {
|
|
23
|
-
await program.parseAsync(
|
|
24
|
-
|
|
36
|
+
await program.parseAsync(normalizedArgv);
|
|
37
|
+
if (control?.cancellationSource === "sigint" && control?.signal?.aborted) {
|
|
38
|
+
return ExitCode.SIGINT;
|
|
39
|
+
}
|
|
40
|
+
return (context.exitCode && context.exitCode !== 0)
|
|
41
|
+
? context.exitCode
|
|
42
|
+
: (typeof process.exitCode === "number" ? process.exitCode : 0);
|
|
25
43
|
}
|
|
26
44
|
catch (err) {
|
|
45
|
+
if (control?.cancellationSource === "sigint" &&
|
|
46
|
+
(control?.signal?.aborted || err?.name === "AbortError" || err?.code === "SIGINT_INTERRUPTED" || err instanceof SigintError)) {
|
|
47
|
+
return ExitCode.SIGINT;
|
|
48
|
+
}
|
|
27
49
|
if (err instanceof CommanderError) {
|
|
28
50
|
if (err.exitCode === 0) {
|
|
29
51
|
return 0;
|
|
@@ -32,9 +54,39 @@ export async function main(argv = process.argv) {
|
|
|
32
54
|
// 复用 renderer 的 renderError 统一错误信封,消除手工拼装的双实现
|
|
33
55
|
renderError(err, {
|
|
34
56
|
json: isMachine,
|
|
57
|
+
context,
|
|
35
58
|
});
|
|
36
|
-
|
|
37
|
-
|
|
59
|
+
return formatError(err).exitCode;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 命令行独立进程适配器。
|
|
64
|
+
* 仅在命令行可执行入口调用,负责全局 SIGINT 监听与退出状态码写入。
|
|
65
|
+
*/
|
|
66
|
+
export async function runCliProcess(argv = process.argv) {
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const control = {
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
};
|
|
71
|
+
let sigintCount = 0;
|
|
72
|
+
const sigintHandler = () => {
|
|
73
|
+
sigintCount++;
|
|
74
|
+
if (sigintCount === 1) {
|
|
75
|
+
control.cancellationSource = "sigint";
|
|
76
|
+
controller.abort(new Error("Interrupted by SIGINT"));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
process.exitCode = 130;
|
|
80
|
+
process.exit(130);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
process.on("SIGINT", sigintHandler);
|
|
84
|
+
try {
|
|
85
|
+
const exitCode = await main(argv, control);
|
|
86
|
+
process.exitCode = exitCode;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
38
90
|
}
|
|
39
91
|
}
|
|
40
92
|
import { fileURLToPath } from "node:url";
|
|
@@ -42,7 +94,7 @@ import { resolve } from "node:path";
|
|
|
42
94
|
const isDirectRun = Boolean(process.argv[1]) &&
|
|
43
95
|
resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
44
96
|
if (isDirectRun) {
|
|
45
|
-
|
|
97
|
+
runCliProcess().catch((err) => {
|
|
46
98
|
console.error("Fatal error:", err);
|
|
47
99
|
process.exitCode = 1;
|
|
48
100
|
});
|
package/dist/renderer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RunRecord } from "@actiondock/sdk";
|
|
2
|
-
import type
|
|
2
|
+
import { type RegistryStatusReport } from "@actiondock/core/registry";
|
|
3
3
|
import type { Envelope, ProjectDetailInfo, AggregatedPackage, EnvCheckItem, CliContext } from "./types.js";
|
|
4
4
|
/**
|
|
5
5
|
* 构造标准成功结果信封。
|
|
@@ -112,7 +112,8 @@ export declare function renderActionList(items: Array<{
|
|
|
112
112
|
packageId?: string;
|
|
113
113
|
}>, title?: string, isFallback?: boolean, intent?: string): string;
|
|
114
114
|
/**
|
|
115
|
-
* 格式化渲染 Action
|
|
115
|
+
* 格式化渲染 Action 详情(编码顾问 Encoding Advisor)。
|
|
116
|
+
* 统一复用 core 下沉的 formatActionDetail 实现,与独立分发器保持完全一致。
|
|
116
117
|
*/
|
|
117
118
|
export declare function renderActionDetail(action: {
|
|
118
119
|
id: string;
|