@actiondock/cli 2.0.11 → 2.2.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 +41 -21
- package/bin/ad.js +29 -31
- package/dist/commands/action/create.d.ts +10 -0
- package/dist/commands/action/create.js +120 -0
- package/dist/commands/add.d.ts +7 -0
- package/dist/commands/add.js +144 -0
- package/dist/commands/build.d.ts +3 -0
- package/dist/commands/build.js +83 -0
- package/dist/commands/config/delete.d.ts +9 -0
- package/dist/commands/config/delete.js +52 -0
- package/dist/commands/config/env.d.ts +9 -0
- package/dist/commands/config/env.js +85 -0
- package/dist/commands/config/get.d.ts +9 -0
- package/dist/commands/config/get.js +120 -0
- package/dist/commands/config/index.d.ts +9 -0
- package/dist/commands/config/index.js +23 -0
- package/dist/commands/config/list.d.ts +9 -0
- package/dist/commands/config/list.js +137 -0
- package/dist/commands/config/schema.d.ts +9 -0
- package/dist/commands/config/schema.js +121 -0
- package/dist/commands/config/set.d.ts +9 -0
- package/dist/commands/config/set.js +85 -0
- package/dist/commands/describe.d.ts +9 -0
- package/dist/commands/describe.js +68 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +82 -0
- package/dist/commands/export.d.ts +3 -0
- package/dist/commands/export.js +212 -0
- package/dist/commands/generate.d.ts +9 -0
- package/dist/commands/generate.js +44 -0
- package/dist/commands/index.d.ts +32 -0
- package/dist/commands/index.js +111 -0
- package/dist/commands/info.d.ts +9 -0
- package/dist/commands/info.js +210 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +24 -0
- package/dist/commands/link.d.ts +2 -0
- package/dist/commands/link.js +71 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.js +77 -0
- package/dist/commands/mcp.d.ts +9 -0
- package/dist/commands/mcp.js +172 -0
- package/dist/commands/new.d.ts +9 -0
- package/dist/commands/new.js +33 -0
- package/dist/commands/pack.d.ts +3 -0
- package/dist/commands/pack.js +63 -0
- package/dist/commands/playbook.d.ts +10 -0
- package/dist/commands/playbook.js +433 -0
- package/dist/commands/profile.d.ts +2 -0
- package/dist/commands/profile.js +253 -0
- package/dist/commands/remove.d.ts +7 -0
- package/dist/commands/remove.js +118 -0
- package/dist/commands/run.d.ts +13 -0
- package/dist/commands/run.js +146 -0
- package/dist/commands/runs.d.ts +9 -0
- package/dist/commands/runs.js +245 -0
- package/dist/commands/serve.d.ts +9 -0
- package/dist/commands/serve.js +129 -0
- package/dist/commands/state-list.d.ts +24 -0
- package/dist/commands/state-list.js +82 -0
- package/dist/commands/state.d.ts +9 -0
- package/dist/commands/state.js +310 -0
- package/dist/commands/test.d.ts +2 -0
- package/dist/commands/test.js +44 -0
- package/dist/commands/validate.d.ts +9 -0
- package/dist/commands/validate.js +106 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.js +103 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +47 -65969
- package/dist/prompt.d.ts +53 -0
- package/dist/prompt.js +192 -0
- package/dist/renderer.d.ts +197 -0
- package/dist/renderer.js +502 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/inspect.d.ts +23 -0
- package/dist/services/inspect.js +96 -0
- package/dist/standalone/index.d.ts +16 -0
- package/dist/standalone/index.js +22 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +9 -0
- package/dist/utils/bytes.d.ts +1 -0
- package/dist/utils/bytes.js +1 -0
- package/dist/utils/duration.js +1 -0
- package/dist/utils/filter.d.ts +1 -0
- package/dist/utils/filter.js +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/spawn.d.ts +27 -0
- package/dist/utils/spawn.js +35 -0
- package/dist/utils/target.d.ts +45 -0
- package/dist/utils/target.js +78 -0
- package/dist/utils/utils.d.ts +38 -0
- package/dist/utils/utils.js +111 -0
- package/package.json +20 -17
- package/src/commands/action/create.ts +0 -129
- package/src/commands/action/index.ts +0 -27
- package/src/commands/action/list.ts +0 -247
- package/src/commands/action/run.ts +0 -197
- package/src/commands/action/show.ts +0 -128
- package/src/commands/action/sync.ts +0 -76
- package/src/commands/action/validate.ts +0 -102
- package/src/commands/action.ts +0 -1
- package/src/commands/build.ts +0 -56
- package/src/commands/config.ts +0 -583
- package/src/commands/doctor.ts +0 -84
- package/src/commands/export.ts +0 -196
- package/src/commands/index.ts +0 -75
- package/src/commands/info.ts +0 -537
- package/src/commands/init.ts +0 -24
- package/src/commands/link.ts +0 -73
- package/src/commands/mcp.ts +0 -195
- package/src/commands/playbook.ts +0 -566
- package/src/commands/profile.ts +0 -296
- package/src/commands/runs.ts +0 -452
- package/src/commands/serve.ts +0 -132
- package/src/commands/state.ts +0 -590
- package/src/commands/test.ts +0 -48
- package/src/index.ts +0 -58
- package/src/utils/bytes.ts +0 -2
- package/src/utils/filter.ts +0 -2
- /package/{src/utils/duration.ts → dist/utils/duration.d.ts} +0 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { decodeStateKey, findProjectRoot, loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
|
|
2
|
+
import { ArgumentError, ExecutionError } from "../errors.js";
|
|
3
|
+
import { renderResult, renderStateList, writeStdout } from "../renderer.js";
|
|
4
|
+
import { getEffectiveOptions, getTargetRoot, resolveIntent, withTarget } from "../utils/index.js";
|
|
5
|
+
import { renderLinkedPackagesStateList, renderProjectScopedStateList, } from "./state-list.js";
|
|
6
|
+
/**
|
|
7
|
+
* 注册 state 状态管理命令(get、set、delete、clear、keys、list)。
|
|
8
|
+
*
|
|
9
|
+
* @param program Commander 实例
|
|
10
|
+
* @param context 命令行上下文
|
|
11
|
+
*/
|
|
12
|
+
export function registerStateCommands(program, context) {
|
|
13
|
+
const stateCmd = program
|
|
14
|
+
.command("state")
|
|
15
|
+
.description("Inspect and manage Shared State store");
|
|
16
|
+
// state list / keys 共享实现(远端、本地项目、链接包聚合三分支)
|
|
17
|
+
const handleListKeys = async (prefix = "", rawOptions, cmd) => {
|
|
18
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
19
|
+
const effectiveIntent = resolveIntent(options.intent, prefix ? [prefix] : []);
|
|
20
|
+
const shouldFallback = options.fallback !== false;
|
|
21
|
+
await withTarget(options, context, async (target, resolved) => {
|
|
22
|
+
const actionId = options.action || "";
|
|
23
|
+
// 远端服务模式:直接列举远端包作用域键
|
|
24
|
+
if (resolved.type === "remote") {
|
|
25
|
+
const keys = await target.listStateKeys(options.package || "", actionId, {
|
|
26
|
+
namespace: options.namespace,
|
|
27
|
+
prefix,
|
|
28
|
+
});
|
|
29
|
+
renderResult(keys, {
|
|
30
|
+
json: options.json,
|
|
31
|
+
envelope: options.envelope,
|
|
32
|
+
humanFormatter: () => renderStateList(keys, `Remote Server ${resolved.serverUrl}${resolved.profileName ? ` (Profile: ${resolved.profileName})` : ""}`, false, effectiveIntent),
|
|
33
|
+
context,
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// 本地工程与链接包模式:按目标根目录决策输出范围
|
|
38
|
+
let targetRoot = null;
|
|
39
|
+
if (options.package) {
|
|
40
|
+
targetRoot = resolvePackageRoot(options.package);
|
|
41
|
+
if (!targetRoot) {
|
|
42
|
+
throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
targetRoot = findProjectRoot();
|
|
47
|
+
}
|
|
48
|
+
if (targetRoot) {
|
|
49
|
+
await renderProjectScopedStateList({
|
|
50
|
+
target,
|
|
51
|
+
targetRoot,
|
|
52
|
+
actionId,
|
|
53
|
+
prefix,
|
|
54
|
+
options,
|
|
55
|
+
effectiveIntent,
|
|
56
|
+
shouldFallback,
|
|
57
|
+
context,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await renderLinkedPackagesStateList({
|
|
62
|
+
target,
|
|
63
|
+
actionId,
|
|
64
|
+
prefix,
|
|
65
|
+
options,
|
|
66
|
+
effectiveIntent,
|
|
67
|
+
shouldFallback,
|
|
68
|
+
context,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
// state list
|
|
73
|
+
stateCmd
|
|
74
|
+
.command("list [prefix]")
|
|
75
|
+
.description("List state keys in current project or linked packages")
|
|
76
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
77
|
+
.option("-a, --action <id>", "Action identifier scope")
|
|
78
|
+
.option("-n, --namespace <ns>", "Filter keys under specific namespace (omit to list all namespaces)")
|
|
79
|
+
.option("-p, --profile <name>", "Query state on a remote target")
|
|
80
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
81
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
82
|
+
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
|
|
83
|
+
.option("--detail", "Include metadata (ttl, expiresAt, size, updatedAt) in JSON output")
|
|
84
|
+
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
85
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
86
|
+
.option("--json", "Output as JSON")
|
|
87
|
+
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
88
|
+
.action(handleListKeys);
|
|
89
|
+
// state keys 别名
|
|
90
|
+
stateCmd
|
|
91
|
+
.command("keys [prefix]")
|
|
92
|
+
.description("Alias for 'ad state list [prefix]'")
|
|
93
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
94
|
+
.option("-a, --action <id>", "Action identifier scope")
|
|
95
|
+
.option("-n, --namespace <ns>", "Filter keys under specific namespace")
|
|
96
|
+
.option("-p, --profile <name>", "Query state on a remote target")
|
|
97
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
98
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
99
|
+
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter")
|
|
100
|
+
.option("--detail", "Include metadata in JSON output")
|
|
101
|
+
.option("--no-fallback", "Disable fallback")
|
|
102
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
103
|
+
.option("--json", "Output as JSON")
|
|
104
|
+
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
105
|
+
.action(handleListKeys);
|
|
106
|
+
// state get <key>
|
|
107
|
+
stateCmd
|
|
108
|
+
.command("get <key>")
|
|
109
|
+
.description("Get state value by key (supports composite 'ns:key' and detail mode)")
|
|
110
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
111
|
+
.option("-a, --action <id>", "Action identifier scope")
|
|
112
|
+
.option("-n, --namespace <ns>", "Explicit namespace scope for key")
|
|
113
|
+
.option("-p, --profile <name>", "Query state on a remote target")
|
|
114
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
115
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
116
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
117
|
+
.option("--json", "Output as JSON")
|
|
118
|
+
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
119
|
+
.action(async (rawKey, rawOptions, cmd) => {
|
|
120
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
121
|
+
if (!rawKey) {
|
|
122
|
+
throw new ArgumentError("State key is required");
|
|
123
|
+
}
|
|
124
|
+
await withTarget(options, context, async (target, resolved) => {
|
|
125
|
+
const actionId = options.action || "";
|
|
126
|
+
if (resolved.type === "remote") {
|
|
127
|
+
const decoded = decodeStateKey(rawKey);
|
|
128
|
+
const effectiveNamespace = options.namespace || (decoded.namespace || undefined);
|
|
129
|
+
const actualKey = options.namespace ? rawKey : decoded.key;
|
|
130
|
+
const entry = await target.getState(options.package || "", actionId, actualKey, {
|
|
131
|
+
namespace: effectiveNamespace,
|
|
132
|
+
detail: true,
|
|
133
|
+
});
|
|
134
|
+
if (entry === undefined || entry.value === undefined) {
|
|
135
|
+
throw new ExecutionError(`State key '${rawKey}' not found on remote server`);
|
|
136
|
+
}
|
|
137
|
+
const val = entry.value;
|
|
138
|
+
renderResult({ key: rawKey, value: val, namespace: entry.namespace || effectiveNamespace }, {
|
|
139
|
+
json: options.json,
|
|
140
|
+
envelope: options.envelope,
|
|
141
|
+
humanFormatter: () => (typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)),
|
|
142
|
+
context,
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// 本地项目模式
|
|
147
|
+
const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
|
|
148
|
+
const projConfig = loadProjectConfig(root);
|
|
149
|
+
const entry = await target.getState(projConfig.id, actionId, effectiveKey, {
|
|
150
|
+
namespace: options.namespace,
|
|
151
|
+
detail: true,
|
|
152
|
+
});
|
|
153
|
+
if (entry === undefined || entry.value === undefined) {
|
|
154
|
+
throw new ExecutionError(`State key '${rawKey}' not found in package '${projConfig.id}'`);
|
|
155
|
+
}
|
|
156
|
+
const val = entry.value;
|
|
157
|
+
const matchedNamespace = entry.namespace;
|
|
158
|
+
renderResult({ key: rawKey, value: val, namespace: matchedNamespace }, {
|
|
159
|
+
json: options.json,
|
|
160
|
+
envelope: options.envelope,
|
|
161
|
+
humanFormatter: () => (typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)),
|
|
162
|
+
context,
|
|
163
|
+
});
|
|
164
|
+
}, { localRoot: () => getTargetRoot(options.package, rawKey).root });
|
|
165
|
+
});
|
|
166
|
+
// state set <key> <value>
|
|
167
|
+
stateCmd
|
|
168
|
+
.command("set <key> <value>")
|
|
169
|
+
.description("Set state key-value (supports JSON value, composite 'ns:key', and --ttl)")
|
|
170
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
171
|
+
.option("-a, --action <id>", "Action identifier scope")
|
|
172
|
+
.option("-n, --namespace <ns>", "Explicit namespace scope for key")
|
|
173
|
+
.option("--ttl <seconds>", "Time-To-Live expiration in seconds")
|
|
174
|
+
.option("-p, --profile <name>", "Set state on a remote target")
|
|
175
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
176
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
177
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
178
|
+
.action(async (rawKey, rawVal, rawOptions, cmd) => {
|
|
179
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
180
|
+
if (!rawKey || rawVal === undefined) {
|
|
181
|
+
throw new ArgumentError("Both key and value are required for state set");
|
|
182
|
+
}
|
|
183
|
+
let parsedVal = rawVal;
|
|
184
|
+
try {
|
|
185
|
+
parsedVal = JSON.parse(rawVal);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
parsedVal = rawVal;
|
|
189
|
+
}
|
|
190
|
+
const ttlSec = options.ttl ? parseInt(options.ttl, 10) : undefined;
|
|
191
|
+
if (options.ttl && (isNaN(ttlSec) || ttlSec <= 0)) {
|
|
192
|
+
throw new ArgumentError(`Invalid --ttl value: '${options.ttl}'. Must be a positive integer.`);
|
|
193
|
+
}
|
|
194
|
+
await withTarget(options, context, async (target, resolved) => {
|
|
195
|
+
const actionId = options.action || "";
|
|
196
|
+
if (resolved.type === "remote") {
|
|
197
|
+
const decoded = decodeStateKey(rawKey);
|
|
198
|
+
const effectiveNamespace = options.namespace || (decoded.namespace || undefined);
|
|
199
|
+
const actualKey = options.namespace ? rawKey : decoded.key;
|
|
200
|
+
await target.setState(options.package || "", actionId, actualKey, parsedVal, {
|
|
201
|
+
namespace: effectiveNamespace,
|
|
202
|
+
ttl: ttlSec,
|
|
203
|
+
});
|
|
204
|
+
writeStdout(`[OK] State '${rawKey}' updated on remote server`, context);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// 本地项目模式
|
|
208
|
+
const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
|
|
209
|
+
const projConfig = loadProjectConfig(root);
|
|
210
|
+
let actualNamespace = options.namespace;
|
|
211
|
+
let finalKey = effectiveKey;
|
|
212
|
+
if (options.namespace === undefined && effectiveKey.includes(":")) {
|
|
213
|
+
const decoded = decodeStateKey(effectiveKey);
|
|
214
|
+
actualNamespace = decoded.namespace;
|
|
215
|
+
finalKey = decoded.key;
|
|
216
|
+
}
|
|
217
|
+
await target.setState(projConfig.id, actionId, finalKey, parsedVal, {
|
|
218
|
+
namespace: actualNamespace,
|
|
219
|
+
ttl: ttlSec,
|
|
220
|
+
});
|
|
221
|
+
const displayKey = actualNamespace ? `${actualNamespace}:${finalKey}` : finalKey;
|
|
222
|
+
writeStdout(`[OK] State '${displayKey}' updated in package '${projConfig.id}'`, context);
|
|
223
|
+
}, { localRoot: () => getTargetRoot(options.package, rawKey).root });
|
|
224
|
+
});
|
|
225
|
+
// state delete <key>
|
|
226
|
+
stateCmd
|
|
227
|
+
.command("delete <key>")
|
|
228
|
+
.alias("rm")
|
|
229
|
+
.description("Delete state key entry")
|
|
230
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
231
|
+
.option("-a, --action <id>", "Action identifier scope")
|
|
232
|
+
.option("-n, --namespace <ns>", "Explicit namespace scope for key")
|
|
233
|
+
.option("-p, --profile <name>", "Delete state on a remote target")
|
|
234
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
235
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
236
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
237
|
+
.action(async (rawKey, rawOptions, cmd) => {
|
|
238
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
239
|
+
if (!rawKey) {
|
|
240
|
+
throw new ArgumentError("State key is required");
|
|
241
|
+
}
|
|
242
|
+
await withTarget(options, context, async (target, resolved) => {
|
|
243
|
+
const actionId = options.action || "";
|
|
244
|
+
if (resolved.type === "remote") {
|
|
245
|
+
const decoded = decodeStateKey(rawKey);
|
|
246
|
+
const effectiveNamespace = options.namespace || (decoded.namespace || undefined);
|
|
247
|
+
const actualKey = options.namespace ? rawKey : decoded.key;
|
|
248
|
+
const deleted = await target.deleteState(options.package || "", actionId, actualKey, {
|
|
249
|
+
namespace: effectiveNamespace,
|
|
250
|
+
});
|
|
251
|
+
if (!deleted) {
|
|
252
|
+
throw new ExecutionError(`State key '${rawKey}' not found on remote server`);
|
|
253
|
+
}
|
|
254
|
+
writeStdout(`[OK] State '${rawKey}' deleted on remote server`, context);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// 本地项目模式
|
|
258
|
+
const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
|
|
259
|
+
const projConfig = loadProjectConfig(root);
|
|
260
|
+
const deleted = await target.deleteState(projConfig.id, actionId, effectiveKey, {
|
|
261
|
+
namespace: options.namespace,
|
|
262
|
+
});
|
|
263
|
+
if (!deleted) {
|
|
264
|
+
throw new ExecutionError(`State key '${rawKey}' not found in package '${projConfig.id}'`);
|
|
265
|
+
}
|
|
266
|
+
writeStdout(`[OK] State '${rawKey}' deleted from package '${projConfig.id}'`, context);
|
|
267
|
+
}, { localRoot: () => getTargetRoot(options.package, rawKey).root });
|
|
268
|
+
});
|
|
269
|
+
// state clear
|
|
270
|
+
stateCmd
|
|
271
|
+
.command("clear")
|
|
272
|
+
.alias("clean")
|
|
273
|
+
.description("Clear state entries (supports namespace cleanup or global wipe via --all)")
|
|
274
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
275
|
+
.option("--action <id>", "Action identifier scope")
|
|
276
|
+
.option("-n, --namespace <ns>", "Target namespace to clear (required unless --all is specified)")
|
|
277
|
+
.option("-a, --all", "Dangerously clear all state namespaces for the package")
|
|
278
|
+
.option("-p, --profile <name>", "Clear state on a remote target")
|
|
279
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
280
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
281
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
282
|
+
.action(async (rawOptions, cmd) => {
|
|
283
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
284
|
+
if (!options.all && options.namespace === undefined) {
|
|
285
|
+
throw new ArgumentError("Must specify either -n/--namespace <name> to clear specific namespace, or --all to clear entire package state store.");
|
|
286
|
+
}
|
|
287
|
+
await withTarget(options, context, async (target, resolved) => {
|
|
288
|
+
const actionId = options.action || "";
|
|
289
|
+
if (resolved.type === "remote") {
|
|
290
|
+
const count = await target.clearState(options.package || "", actionId, {
|
|
291
|
+
namespace: options.namespace,
|
|
292
|
+
all: Boolean(options.all),
|
|
293
|
+
});
|
|
294
|
+
writeStdout(`[OK] Cleared ${count} state entry(s) on remote server`, context);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// 本地项目模式
|
|
298
|
+
const { root } = getTargetRoot(options.package);
|
|
299
|
+
const projConfig = loadProjectConfig(root);
|
|
300
|
+
const count = await target.clearState(projConfig.id, actionId, {
|
|
301
|
+
namespace: options.namespace,
|
|
302
|
+
all: Boolean(options.all),
|
|
303
|
+
});
|
|
304
|
+
const scopeDesc = options.all
|
|
305
|
+
? "all namespaces"
|
|
306
|
+
: `namespace '${options.namespace}'`;
|
|
307
|
+
writeStdout(`[OK] Cleared ${count} state entry(s) in ${scopeDesc} for package '${projConfig.id}'`, context);
|
|
308
|
+
}, { localRoot: () => getTargetRoot(options.package).root });
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { findProjectRoot } from "@actiondock/core";
|
|
4
|
+
import { spawnAsync } from "../utils/index.js";
|
|
5
|
+
export function registerTestCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command("test [pattern]")
|
|
8
|
+
.description("Run project tests using configured test runner (node:test or bun test)")
|
|
9
|
+
.action(async (pattern) => {
|
|
10
|
+
const root = findProjectRoot();
|
|
11
|
+
const cwd = root || process.cwd();
|
|
12
|
+
const pkgJsonPath = join(cwd, "package.json");
|
|
13
|
+
let testCmd = "npm";
|
|
14
|
+
let testArgs = ["test"];
|
|
15
|
+
if (existsSync(pkgJsonPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
18
|
+
if (pkg.scripts?.test?.includes("bun test") && typeof globalThis.Bun !== "undefined") {
|
|
19
|
+
testCmd = "bun";
|
|
20
|
+
testArgs = ["test"];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// package.json 解析失败时回退 npm test
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (pattern) {
|
|
28
|
+
if (testCmd === "npm") {
|
|
29
|
+
testArgs.push("--", pattern);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
testArgs.push(pattern);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// stdio inherit 保持实时输出;进程异常(如命令不存在)时回退退出码 1
|
|
36
|
+
const proc = await spawnAsync(testCmd, testArgs, {
|
|
37
|
+
cwd,
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
}).catch(() => ({ status: 1, signal: null, stdout: "", stderr: "" }));
|
|
40
|
+
if (proc.status !== 0) {
|
|
41
|
+
process.exitCode = proc.status ?? 1;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { CliContext } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* 注册顶层统一 validate 命令:校验 Action 的 Schema 与方法定义完整性。
|
|
5
|
+
*
|
|
6
|
+
* @param program Commander 根程序对象
|
|
7
|
+
* @param context 命令行上下文
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerValidateCommand(program: Command, context?: CliContext): void;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { checkGeneratedTypes, findProjectRoot, GENERATED_TYPES_OUTDATED_CODE, loadActions, loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
|
|
2
|
+
import { SelectionPlanner } from "@actiondock/builder";
|
|
3
|
+
import { ArgumentError, ExecutionError } from "../errors.js";
|
|
4
|
+
import { renderActionValidation, renderResult } from "../renderer.js";
|
|
5
|
+
import { getEffectiveOptions } from "../utils/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* 注册顶层统一 validate 命令:校验 Action 的 Schema 与方法定义完整性。
|
|
8
|
+
*
|
|
9
|
+
* @param program Commander 根程序对象
|
|
10
|
+
* @param context 命令行上下文
|
|
11
|
+
*/
|
|
12
|
+
export function registerValidateCommand(program, context) {
|
|
13
|
+
program
|
|
14
|
+
.command("validate [id]")
|
|
15
|
+
.description("Validate action schemas and definitions")
|
|
16
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
17
|
+
.option("--json", "Output as JSON")
|
|
18
|
+
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
19
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
20
|
+
.action(async (id, rawOptions, cmd) => {
|
|
21
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
22
|
+
let root = null;
|
|
23
|
+
if (options.package) {
|
|
24
|
+
root = resolvePackageRoot(options.package);
|
|
25
|
+
if (!root) {
|
|
26
|
+
throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
root = findProjectRoot();
|
|
31
|
+
}
|
|
32
|
+
if (!root) {
|
|
33
|
+
throw new ArgumentError("Not in an ActionDock project (actiondock.json not found).\nPlease specify -P, --package <id> or cd into a project directory.");
|
|
34
|
+
}
|
|
35
|
+
const config = loadProjectConfig(root);
|
|
36
|
+
const actions = await loadActions(root, config.actionsDir);
|
|
37
|
+
let toValidate = [];
|
|
38
|
+
if (id) {
|
|
39
|
+
const act = actions.get(id);
|
|
40
|
+
if (!act) {
|
|
41
|
+
throw new ArgumentError(`Action '${id}' not found in project '${config.id}'`);
|
|
42
|
+
}
|
|
43
|
+
toValidate = [{ id, act, spec: config.actions?.[id] }];
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
toValidate = Array.from(actions.entries()).map(([actId, act]) => ({
|
|
47
|
+
id: actId,
|
|
48
|
+
act,
|
|
49
|
+
spec: config.actions?.[actId],
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
const results = [];
|
|
53
|
+
for (const item of toValidate) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
if (!item.id)
|
|
56
|
+
errors.push("Missing id property");
|
|
57
|
+
if (!item.act?.run || typeof item.act.run !== "function")
|
|
58
|
+
errors.push("Missing run method");
|
|
59
|
+
const inSchema = item.spec?.inputSchema ?? item.act.inputSchema;
|
|
60
|
+
if (inSchema && typeof inSchema !== "object") {
|
|
61
|
+
errors.push("Invalid inputSchema object");
|
|
62
|
+
}
|
|
63
|
+
const outSchema = item.spec?.outputSchema ?? item.act.outputSchema;
|
|
64
|
+
if (outSchema && typeof outSchema !== "object") {
|
|
65
|
+
errors.push("Invalid outputSchema object");
|
|
66
|
+
}
|
|
67
|
+
results.push({
|
|
68
|
+
id: item.id,
|
|
69
|
+
valid: errors.length === 0,
|
|
70
|
+
errors,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const allValid = results.every((r) => r.valid);
|
|
74
|
+
const validationPayload = { valid: allValid, results };
|
|
75
|
+
renderResult(validationPayload, {
|
|
76
|
+
json: options.json,
|
|
77
|
+
envelope: options.envelope,
|
|
78
|
+
humanFormatter: () => renderActionValidation(results),
|
|
79
|
+
context,
|
|
80
|
+
});
|
|
81
|
+
if (!allValid) {
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// 校验 Action 本地相对依赖完整性
|
|
86
|
+
try {
|
|
87
|
+
SelectionPlanner.plan({ projectRoot: root, actions: id ? [id] : undefined });
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err?.code === "UNMET_LOCAL_DEPENDENCY" ||
|
|
91
|
+
err?.code === "EXTERNAL_LOCAL_DEPENDENCY" ||
|
|
92
|
+
err?.code === "FILE_NOT_FOUND") {
|
|
93
|
+
throw new ExecutionError(`Action dependency integrity validation failed:\n${err.message}`, undefined, err.code);
|
|
94
|
+
}
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
// 只读校验已生成的类型文件摘要是否过期
|
|
98
|
+
const typeCheck = checkGeneratedTypes(root);
|
|
99
|
+
if (typeCheck.exists && typeCheck.outdated) {
|
|
100
|
+
throw new ExecutionError(`Generated types in ${typeCheck.filePath} are outdated. Expected manifest digest '${typeCheck.expectedDigest}', but got '${typeCheck.actualDigest || "none"}'. Run 'ad generate types' to update.`, {
|
|
101
|
+
expectedDigest: typeCheck.expectedDigest,
|
|
102
|
+
actualDigest: typeCheck.actualDigest,
|
|
103
|
+
}, GENERATED_TYPES_OUTDATED_CODE);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ExitCodeValue } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 命令行标准错误基类。
|
|
4
|
+
*/
|
|
5
|
+
export declare class CliError extends Error {
|
|
6
|
+
readonly exitCode: ExitCodeValue;
|
|
7
|
+
readonly code: string;
|
|
8
|
+
readonly details?: unknown;
|
|
9
|
+
constructor(message: string, exitCode?: ExitCodeValue, code?: string, details?: unknown);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 命令行参数与选项校验错误(退出码为 2)。
|
|
13
|
+
*/
|
|
14
|
+
export declare class ArgumentError extends CliError {
|
|
15
|
+
constructor(message: string, details?: unknown);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 业务逻辑或框架执行失败错误(退出码为 1)。
|
|
19
|
+
*/
|
|
20
|
+
export declare class ExecutionError extends CliError {
|
|
21
|
+
constructor(message: string, details?: unknown, code?: string);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 用户中断信号错误(退出码为 130)。
|
|
25
|
+
*/
|
|
26
|
+
export declare class SigintError extends CliError {
|
|
27
|
+
constructor(message?: string);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 标准化错误信息解析结果。
|
|
31
|
+
*/
|
|
32
|
+
export interface FormattedError {
|
|
33
|
+
code: string;
|
|
34
|
+
message: string;
|
|
35
|
+
exitCode: ExitCodeValue;
|
|
36
|
+
details?: unknown;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 解析并格式化任意异常为结构化错误对象。
|
|
40
|
+
*
|
|
41
|
+
* @param err 待解析的异常对象
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatError(err: unknown): FormattedError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ExitCode } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* CLI 信封错误码常量(仅 CLI 输出协议使用,属包内部码,就地常量化管理)。
|
|
4
|
+
*/
|
|
5
|
+
const CLI_CODE_SUCCESS = "SUCCESS";
|
|
6
|
+
const CLI_CODE_INVALID_ARGUMENT = "INVALID_ARGUMENT";
|
|
7
|
+
const CLI_CODE_ERROR = "ERROR";
|
|
8
|
+
const CLI_CODE_UNKNOWN_ERROR = "UNKNOWN_ERROR";
|
|
9
|
+
/**
|
|
10
|
+
* 命令行标准错误基类。
|
|
11
|
+
*/
|
|
12
|
+
export class CliError extends Error {
|
|
13
|
+
exitCode;
|
|
14
|
+
code;
|
|
15
|
+
details;
|
|
16
|
+
constructor(message, exitCode = ExitCode.FAILURE, code = "CLI_ERROR", details) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CliError";
|
|
19
|
+
this.exitCode = exitCode;
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.details = details;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 命令行参数与选项校验错误(退出码为 2)。
|
|
26
|
+
*/
|
|
27
|
+
export class ArgumentError extends CliError {
|
|
28
|
+
constructor(message, details) {
|
|
29
|
+
super(message, ExitCode.INVALID_ARGUMENT, "INVALID_ARGUMENT", details);
|
|
30
|
+
this.name = "ArgumentError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 业务逻辑或框架执行失败错误(退出码为 1)。
|
|
35
|
+
*/
|
|
36
|
+
export class ExecutionError extends CliError {
|
|
37
|
+
constructor(message, details, code = "EXECUTION_FAILURE") {
|
|
38
|
+
super(message, ExitCode.FAILURE, code, details);
|
|
39
|
+
this.name = "ExecutionError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 用户中断信号错误(退出码为 130)。
|
|
44
|
+
*/
|
|
45
|
+
export class SigintError extends CliError {
|
|
46
|
+
constructor(message = "Interrupted by SIGINT") {
|
|
47
|
+
super(message, ExitCode.SIGINT, "SIGINT_INTERRUPTED");
|
|
48
|
+
this.name = "SigintError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 解析并格式化任意异常为结构化错误对象。
|
|
53
|
+
*
|
|
54
|
+
* @param err 待解析的异常对象
|
|
55
|
+
*/
|
|
56
|
+
export function formatError(err) {
|
|
57
|
+
if (err instanceof CliError) {
|
|
58
|
+
return {
|
|
59
|
+
code: err.code,
|
|
60
|
+
message: err.message,
|
|
61
|
+
exitCode: err.exitCode,
|
|
62
|
+
details: err.details,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Commander.js 原生错误处理
|
|
66
|
+
if (typeof err === "object" && err !== null && "code" in err && typeof err.code === "string") {
|
|
67
|
+
const commanderErr = err;
|
|
68
|
+
const code = commanderErr.code;
|
|
69
|
+
// 参数类异常
|
|
70
|
+
if (code.startsWith("commander.unknownOption") ||
|
|
71
|
+
code.startsWith("commander.missingArgument") ||
|
|
72
|
+
code.startsWith("commander.missingMandatoryOptionValue") ||
|
|
73
|
+
code.startsWith("commander.optionMissingArgument") ||
|
|
74
|
+
code.startsWith("commander.invalidArgument") ||
|
|
75
|
+
code.startsWith("commander.excessArguments")) {
|
|
76
|
+
return {
|
|
77
|
+
code: CLI_CODE_INVALID_ARGUMENT,
|
|
78
|
+
message: commanderErr.message,
|
|
79
|
+
exitCode: ExitCode.INVALID_ARGUMENT,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// 正常退出(如 --help 或 --version)
|
|
83
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
84
|
+
return {
|
|
85
|
+
code: CLI_CODE_SUCCESS,
|
|
86
|
+
message: commanderErr.message,
|
|
87
|
+
exitCode: ExitCode.SUCCESS,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (err instanceof Error) {
|
|
92
|
+
return {
|
|
93
|
+
code: CLI_CODE_ERROR,
|
|
94
|
+
message: err.message,
|
|
95
|
+
exitCode: ExitCode.FAILURE,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
code: CLI_CODE_UNKNOWN_ERROR,
|
|
100
|
+
message: String(err),
|
|
101
|
+
exitCode: ExitCode.FAILURE,
|
|
102
|
+
};
|
|
103
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./errors.js";
|
|
3
|
+
export * from "./renderer.js";
|
|
4
|
+
export * from "./prompt.js";
|
|
5
|
+
export * from "./standalone/index.js";
|
|
6
|
+
export * from "./utils/index.js";
|
|
7
|
+
export * from "./services/index.js";
|
|
8
|
+
export * from "./commands/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* ActionDock CLI 主执行入口函数。
|
|
11
|
+
*
|
|
12
|
+
* @param argv 命令行参数数组(默认使用 process.argv)
|
|
13
|
+
* @returns 进程退出码
|
|
14
|
+
*/
|
|
15
|
+
export declare function main(argv?: string[]): Promise<number>;
|