@actiondock/cli 2.0.9 → 2.0.11-beta.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 +17 -0
- package/dist/index.js +46701 -35780
- package/package.json +7 -7
- package/src/commands/action/create.ts +129 -0
- package/src/commands/action/index.ts +27 -0
- package/src/commands/action/list.ts +247 -0
- package/src/commands/action/run.ts +197 -0
- package/src/commands/action/show.ts +128 -0
- package/src/commands/action/sync.ts +76 -0
- package/src/commands/action/validate.ts +102 -0
- package/src/commands/action.ts +1 -633
- package/src/commands/build.ts +19 -17
- package/src/commands/config.ts +168 -69
- package/src/commands/doctor.ts +44 -42
- package/src/commands/export.ts +140 -14
- package/src/commands/index.ts +33 -1
- package/src/commands/info.ts +93 -74
- package/src/commands/init.ts +2 -2
- package/src/commands/link.ts +3 -4
- package/src/commands/mcp.ts +6 -10
- package/src/commands/playbook.ts +289 -161
- package/src/commands/profile.ts +125 -92
- package/src/commands/runs.ts +92 -49
- package/src/commands/serve.ts +3 -4
- package/src/commands/state.ts +145 -64
- package/src/commands/test.ts +1 -1
- package/src/index.ts +39 -3
- package/src/utils/bytes.ts +1 -35
- package/src/utils/filter.ts +2 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actiondock/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11-beta.0",
|
|
4
4
|
"description": "ActionDock CLI facade for developing, testing, building, and exporting standalone Agent Actions and Skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"build": "bun build ./src/index.ts --outdir ./dist --target bun"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@actiondock/builder": "
|
|
32
|
-
"@actiondock/core": "
|
|
33
|
-
"@actiondock/mcp": "
|
|
34
|
-
"@actiondock/runtime-cli": "
|
|
35
|
-
"@actiondock/runtime-node": "
|
|
36
|
-
"@actiondock/sdk": "
|
|
31
|
+
"@actiondock/builder": "2.0.11-beta.0",
|
|
32
|
+
"@actiondock/core": "2.0.11-beta.0",
|
|
33
|
+
"@actiondock/mcp": "2.0.11-beta.0",
|
|
34
|
+
"@actiondock/runtime-cli": "2.0.11-beta.0",
|
|
35
|
+
"@actiondock/runtime-node": "2.0.11-beta.0",
|
|
36
|
+
"@actiondock/sdk": "2.0.11-beta.0",
|
|
37
37
|
"commander": "^13.1.0",
|
|
38
38
|
"tsx": "^4.19.3"
|
|
39
39
|
},
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
findProjectRoot,
|
|
5
|
+
getPackageSlug,
|
|
6
|
+
loadManifest,
|
|
7
|
+
loadProjectConfig,
|
|
8
|
+
saveManifest,
|
|
9
|
+
} from "@actiondock/core";
|
|
10
|
+
import { ExecutionError } from "@actiondock/runtime-cli";
|
|
11
|
+
import type { Command } from "commander";
|
|
12
|
+
|
|
13
|
+
export function registerActionCreateCommand(actionCmd: Command): void {
|
|
14
|
+
actionCmd
|
|
15
|
+
.command("create <id>")
|
|
16
|
+
.alias("new")
|
|
17
|
+
.description("Scaffold a new Action definition file")
|
|
18
|
+
.option("-d, --desc <description>", "Action description")
|
|
19
|
+
.option("-f, --file <filePath>", "Target file path relative to actions dir")
|
|
20
|
+
.action(async (id, options) => {
|
|
21
|
+
const root = findProjectRoot();
|
|
22
|
+
if (!root) {
|
|
23
|
+
throw new ExecutionError("Not in an ActionDock project (actiondock.json not found)");
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const config = loadProjectConfig(root);
|
|
27
|
+
const actionsDir = resolve(root, config.actionsDir || "actions");
|
|
28
|
+
if (!existsSync(actionsDir)) {
|
|
29
|
+
mkdirSync(actionsDir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cleanName = getPackageSlug(id);
|
|
33
|
+
const targetRelFile = options.file || `${cleanName}.ts`;
|
|
34
|
+
const targetFullFile = resolve(actionsDir, targetRelFile);
|
|
35
|
+
|
|
36
|
+
if (existsSync(targetFullFile)) {
|
|
37
|
+
throw new ExecutionError(`Target action file already exists at ${targetFullFile}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const desc = options.desc || `Action ${id}`;
|
|
41
|
+
const template = `import { defineAction } from "@actiondock/sdk";
|
|
42
|
+
|
|
43
|
+
export interface Input {
|
|
44
|
+
exampleParam?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Output {
|
|
48
|
+
success: boolean;
|
|
49
|
+
result?: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default defineAction<Input, Output>({
|
|
53
|
+
id: "${id}",
|
|
54
|
+
description: "${desc}",
|
|
55
|
+
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
exampleParam: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "Example parameter description",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: [],
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
outputSchema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
success: { type: "boolean" },
|
|
71
|
+
result: {},
|
|
72
|
+
},
|
|
73
|
+
required: ["success"],
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async run(input, ctx) {
|
|
77
|
+
ctx.log.info("Running ${id}", input);
|
|
78
|
+
|
|
79
|
+
// Access config: ctx.config.get("MY_CONFIG")
|
|
80
|
+
// Access state: await ctx.state.get("my_key") / await ctx.state.set("my_key", val)
|
|
81
|
+
// Call action: await ctx.actions.invoke(otherAction, input)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
result: input.exampleParam || "done",
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
writeFileSync(targetFullFile, template, "utf-8");
|
|
92
|
+
|
|
93
|
+
const manifest = loadManifest(root) || { schemaVersion: 1, actions: {}, assets: [] };
|
|
94
|
+
manifest.actions = manifest.actions || {};
|
|
95
|
+
manifest.actions[id] = {
|
|
96
|
+
entry: join(config.actionsDir || "actions", targetRelFile),
|
|
97
|
+
description: desc,
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
exampleParam: {
|
|
102
|
+
type: "string",
|
|
103
|
+
description: "Example parameter description",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
required: [],
|
|
107
|
+
},
|
|
108
|
+
outputSchema: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
success: { type: "boolean" },
|
|
112
|
+
result: {},
|
|
113
|
+
},
|
|
114
|
+
required: ["success"],
|
|
115
|
+
},
|
|
116
|
+
uses: [],
|
|
117
|
+
tags: [],
|
|
118
|
+
};
|
|
119
|
+
saveManifest(root, manifest);
|
|
120
|
+
|
|
121
|
+
console.log(`[OK] Created Action '${id}' at ${targetFullFile}`);
|
|
122
|
+
console.log(`\nTo run this action:`);
|
|
123
|
+
console.log(` ad action run ${id} --input '{"exampleParam": "hello"}'`);
|
|
124
|
+
} catch (err: any) {
|
|
125
|
+
if (err instanceof ExecutionError) throw err;
|
|
126
|
+
throw new ExecutionError(err.message);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { registerActionCreateCommand } from "./create";
|
|
3
|
+
import { registerActionListCommand } from "./list";
|
|
4
|
+
import { executeAction, registerActionRunCommand } from "./run";
|
|
5
|
+
import { registerActionShowCommand } from "./show";
|
|
6
|
+
import { registerActionSyncCommand } from "./sync";
|
|
7
|
+
import { registerActionValidateCommand } from "./validate";
|
|
8
|
+
|
|
9
|
+
export { executeAction };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 注册 Action 相关的 CLI 命令集合(包括 list, describe, run, create, sync, validate 以及顶层 run 别名)。
|
|
13
|
+
*
|
|
14
|
+
* @param program Commander 根程序对象
|
|
15
|
+
*/
|
|
16
|
+
export function registerActionCommands(program: Command): void {
|
|
17
|
+
const actionCmd = program
|
|
18
|
+
.command("action")
|
|
19
|
+
.description("Manage and execute Actions");
|
|
20
|
+
|
|
21
|
+
registerActionListCommand(actionCmd);
|
|
22
|
+
registerActionCreateCommand(actionCmd);
|
|
23
|
+
registerActionSyncCommand(actionCmd);
|
|
24
|
+
registerActionShowCommand(actionCmd);
|
|
25
|
+
registerActionValidateCommand(actionCmd);
|
|
26
|
+
registerActionRunCommand(actionCmd, program);
|
|
27
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
fetchRemoteActions,
|
|
4
|
+
filterWithFallbackInfo,
|
|
5
|
+
findProjectRoot,
|
|
6
|
+
listLinkedPackages,
|
|
7
|
+
loadManifest,
|
|
8
|
+
loadProjectConfig,
|
|
9
|
+
resolvePackageRoot,
|
|
10
|
+
resolveTarget,
|
|
11
|
+
} from "@actiondock/core";
|
|
12
|
+
import { ArgumentError, ExecutionError, getEffectiveOptions, renderResult } from "@actiondock/runtime-cli";
|
|
13
|
+
import type { Command } from "commander";
|
|
14
|
+
import { resolveIntent } from "../../utils/filter";
|
|
15
|
+
|
|
16
|
+
export function registerActionListCommand(actionCmd: Command): void {
|
|
17
|
+
actionCmd
|
|
18
|
+
.command("list [patterns...]")
|
|
19
|
+
.description("List actions in current project, linked packages, or remote profile")
|
|
20
|
+
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
|
|
21
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
22
|
+
.option("-p, --profile <name>", "Execute or query against a specific profile")
|
|
23
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
24
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
25
|
+
.option("--fallback", "Enable fallback to full list when no items match intent")
|
|
26
|
+
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
27
|
+
.option("--json", "Output as JSON")
|
|
28
|
+
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
29
|
+
.action(async (patterns, rawOptions, cmd) => {
|
|
30
|
+
try {
|
|
31
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
32
|
+
const effectiveIntent = resolveIntent(options.intent, patterns);
|
|
33
|
+
const isMachine = Boolean(options.json || options.envelope);
|
|
34
|
+
const fallbackExplicit = options.fallback === true || (Array.isArray(process.argv) && process.argv.includes("--fallback"));
|
|
35
|
+
const shouldFallback = isMachine ? fallbackExplicit : options.fallback !== false;
|
|
36
|
+
|
|
37
|
+
const target = resolveTarget({
|
|
38
|
+
profile: options.profile,
|
|
39
|
+
server: options.server,
|
|
40
|
+
token: options.token,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (target.type === "remote") {
|
|
44
|
+
let list = await fetchRemoteActions(
|
|
45
|
+
target.serverUrl!,
|
|
46
|
+
target.token,
|
|
47
|
+
effectiveIntent
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
let isFallback = false;
|
|
51
|
+
if (list.length === 0 && effectiveIntent && shouldFallback) {
|
|
52
|
+
list = await fetchRemoteActions(target.serverUrl!, target.token);
|
|
53
|
+
isFallback = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isFallback && isMachine) {
|
|
57
|
+
renderResult(
|
|
58
|
+
{ items: list, isFallback: true, matchedCount: 0 },
|
|
59
|
+
{ json: options.json, envelope: options.envelope }
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
renderResult(list, {
|
|
65
|
+
json: options.json,
|
|
66
|
+
envelope: options.envelope,
|
|
67
|
+
humanFormatter: () => {
|
|
68
|
+
const lines = [
|
|
69
|
+
`Actions on remote server ${target.serverUrl}${target.profileName ? ` (Profile: ${target.profileName})` : ""}:\n`,
|
|
70
|
+
];
|
|
71
|
+
if (isFallback && effectiveIntent) {
|
|
72
|
+
lines.push(`(No remote actions matched intent '${effectiveIntent}', showing all actions)\n`);
|
|
73
|
+
}
|
|
74
|
+
for (const a of list) {
|
|
75
|
+
lines.push(` ${a.id.padEnd(28)} ${a.description}`);
|
|
76
|
+
}
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let targetRoot: string | null = null;
|
|
84
|
+
if (options.package) {
|
|
85
|
+
targetRoot = resolvePackageRoot(options.package);
|
|
86
|
+
if (!targetRoot) {
|
|
87
|
+
throw new ArgumentError(
|
|
88
|
+
`Package '${options.package}' not found in linked packages or path`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
targetRoot = findProjectRoot();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (targetRoot) {
|
|
96
|
+
const config = loadProjectConfig(targetRoot);
|
|
97
|
+
const manifest = loadManifest(targetRoot);
|
|
98
|
+
const rawList = manifest?.actions
|
|
99
|
+
? Object.entries(manifest.actions).map(([actId, a]) => ({
|
|
100
|
+
id: actId,
|
|
101
|
+
description: a.description || "",
|
|
102
|
+
packageId: config.id,
|
|
103
|
+
}))
|
|
104
|
+
: [];
|
|
105
|
+
|
|
106
|
+
const filterRes = filterWithFallbackInfo(
|
|
107
|
+
rawList,
|
|
108
|
+
effectiveIntent,
|
|
109
|
+
[(a) => a.id, (a) => a.description, (a) => a.packageId],
|
|
110
|
+
shouldFallback
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (filterRes.isFallback && isMachine) {
|
|
114
|
+
renderResult(
|
|
115
|
+
{ items: filterRes.items, isFallback: true, matchedCount: 0 },
|
|
116
|
+
{ json: options.json, envelope: options.envelope }
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
renderResult(filterRes.items, {
|
|
122
|
+
json: options.json,
|
|
123
|
+
envelope: options.envelope,
|
|
124
|
+
humanFormatter: () => {
|
|
125
|
+
const lines = [`Actions in ${config.id} (${targetRoot}):\n`];
|
|
126
|
+
if (filterRes.isFallback && effectiveIntent) {
|
|
127
|
+
lines.push(`(No actions matched intent '${effectiveIntent}', showing all actions)\n`);
|
|
128
|
+
}
|
|
129
|
+
for (const a of filterRes.items) {
|
|
130
|
+
lines.push(` ${a.id.padEnd(28)} ${a.description}`);
|
|
131
|
+
}
|
|
132
|
+
return lines.join("\n");
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// List actions across all linked packages
|
|
139
|
+
const linkedList = listLinkedPackages();
|
|
140
|
+
if (linkedList.length === 0) {
|
|
141
|
+
renderResult([], {
|
|
142
|
+
json: options.json,
|
|
143
|
+
envelope: options.envelope,
|
|
144
|
+
humanFormatter: () =>
|
|
145
|
+
"No ActionDock project in current directory, and no packages linked.\nRun 'ad link' inside an Action package to register it.",
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const aggregated: Array<{
|
|
151
|
+
packageId: string;
|
|
152
|
+
packageName: string;
|
|
153
|
+
path: string;
|
|
154
|
+
actions: Array<{ id: string; description: string }>;
|
|
155
|
+
}> = [];
|
|
156
|
+
|
|
157
|
+
for (const pkg of linkedList) {
|
|
158
|
+
if (!existsSync(pkg.path)) continue;
|
|
159
|
+
try {
|
|
160
|
+
const config = loadProjectConfig(pkg.path);
|
|
161
|
+
const manifest = loadManifest(pkg.path);
|
|
162
|
+
const pkgActions = manifest?.actions
|
|
163
|
+
? Object.entries(manifest.actions).map(([actId, a]) => ({
|
|
164
|
+
id: actId,
|
|
165
|
+
description: a.description || "",
|
|
166
|
+
}))
|
|
167
|
+
: [];
|
|
168
|
+
|
|
169
|
+
aggregated.push({
|
|
170
|
+
packageId: pkg.id,
|
|
171
|
+
packageName: pkg.name,
|
|
172
|
+
path: pkg.path,
|
|
173
|
+
actions: pkgActions,
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
// Ignore broken linked package
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let filteredPackages: typeof aggregated = [];
|
|
181
|
+
let isFallback = false;
|
|
182
|
+
if (!effectiveIntent) {
|
|
183
|
+
filteredPackages = aggregated;
|
|
184
|
+
} else {
|
|
185
|
+
for (const pkg of aggregated) {
|
|
186
|
+
const pkgMatches = filterWithFallbackInfo(
|
|
187
|
+
[pkg],
|
|
188
|
+
effectiveIntent,
|
|
189
|
+
[(p) => p.packageId, (p) => p.packageName, (p) => p.path],
|
|
190
|
+
false
|
|
191
|
+
).matchedCount > 0;
|
|
192
|
+
|
|
193
|
+
if (pkgMatches) {
|
|
194
|
+
filteredPackages.push(pkg);
|
|
195
|
+
} else {
|
|
196
|
+
const matchedActions = filterWithFallbackInfo(
|
|
197
|
+
pkg.actions,
|
|
198
|
+
effectiveIntent,
|
|
199
|
+
[(a) => a.id, (a) => a.description],
|
|
200
|
+
false
|
|
201
|
+
).items;
|
|
202
|
+
|
|
203
|
+
if (matchedActions.length > 0) {
|
|
204
|
+
filteredPackages.push({
|
|
205
|
+
...pkg,
|
|
206
|
+
actions: matchedActions,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (filteredPackages.length === 0 && shouldFallback) {
|
|
213
|
+
filteredPackages = aggregated;
|
|
214
|
+
isFallback = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (isFallback && isMachine) {
|
|
219
|
+
renderResult(
|
|
220
|
+
{ packages: filteredPackages, isFallback: true, matchedCount: 0 },
|
|
221
|
+
{ json: options.json, envelope: options.envelope }
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
renderResult(filteredPackages, {
|
|
227
|
+
json: options.json,
|
|
228
|
+
envelope: options.envelope,
|
|
229
|
+
humanFormatter: () => {
|
|
230
|
+
const lines = ["Linked Action Packages:\n"];
|
|
231
|
+
for (const pkg of filteredPackages) {
|
|
232
|
+
lines.push(`* Package: ${pkg.packageId} (${pkg.path})`);
|
|
233
|
+
for (const a of pkg.actions) {
|
|
234
|
+
lines.push(` - ${a.id.padEnd(26)} ${a.description}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return lines.join("\n");
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
} catch (err: any) {
|
|
241
|
+
if (err instanceof ArgumentError || err instanceof ExecutionError) {
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
throw new ExecutionError(err.message);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
ActionRunner,
|
|
4
|
+
createStorage,
|
|
5
|
+
executeRemoteAction,
|
|
6
|
+
loadActions,
|
|
7
|
+
loadProjectConfig,
|
|
8
|
+
resolveActionProject,
|
|
9
|
+
resolvePackageRoot,
|
|
10
|
+
resolveTarget,
|
|
11
|
+
} from "@actiondock/core";
|
|
12
|
+
import { ArgumentError, ExecutionError, getEffectiveOptions } from "@actiondock/runtime-cli";
|
|
13
|
+
import type { Command } from "commander";
|
|
14
|
+
import { parseDuration } from "../../utils/duration";
|
|
15
|
+
|
|
16
|
+
export async function executeAction(id: string, options: any): Promise<void> {
|
|
17
|
+
let input: unknown = {};
|
|
18
|
+
if (options.input) {
|
|
19
|
+
try {
|
|
20
|
+
input = JSON.parse(options.input);
|
|
21
|
+
} catch (err: any) {
|
|
22
|
+
throw new ArgumentError(`Error parsing --input JSON: ${err.message}`);
|
|
23
|
+
}
|
|
24
|
+
} else if (options.inputFile) {
|
|
25
|
+
try {
|
|
26
|
+
input = JSON.parse(readFileSync(options.inputFile, "utf-8"));
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
throw new ArgumentError(`Error reading --input-file: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let timeoutMs: number | undefined;
|
|
33
|
+
if (options.timeout) {
|
|
34
|
+
try {
|
|
35
|
+
timeoutMs = parseDuration(options.timeout);
|
|
36
|
+
} catch (err: any) {
|
|
37
|
+
throw new ArgumentError(err.message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const configOverrides: Record<string, unknown> = {};
|
|
42
|
+
if (options.config) {
|
|
43
|
+
const list = Array.isArray(options.config) ? options.config : [options.config];
|
|
44
|
+
for (const item of list) {
|
|
45
|
+
const [k, ...v] = item.split("=");
|
|
46
|
+
if (k) configOverrides[k] = v.join("=");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check target (remote profile vs local)
|
|
51
|
+
let target;
|
|
52
|
+
try {
|
|
53
|
+
target = resolveTarget({
|
|
54
|
+
profile: options.profile,
|
|
55
|
+
server: options.server,
|
|
56
|
+
token: options.token,
|
|
57
|
+
});
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
throw new ExecutionError(err.message);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const sigintHandler = () => {
|
|
64
|
+
controller.abort(new Error("Interrupted by SIGINT"));
|
|
65
|
+
};
|
|
66
|
+
process.once("SIGINT", sigintHandler);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
if (target.type === "remote") {
|
|
70
|
+
try {
|
|
71
|
+
const result = await executeRemoteAction(target.serverUrl!, id, input, {
|
|
72
|
+
configOverrides,
|
|
73
|
+
token: target.token,
|
|
74
|
+
timeoutMs,
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
async: Boolean(options.async),
|
|
77
|
+
});
|
|
78
|
+
console.log(JSON.stringify(result, null, 2));
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
} catch (err: any) {
|
|
84
|
+
throw new ExecutionError(err.message);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Local execution
|
|
89
|
+
if (options.async) {
|
|
90
|
+
throw new ExecutionError(
|
|
91
|
+
"Async execution requires a long-running ActionDock server.\nUse --profile, --server, or start `ad serve`."
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let actionTarget = id;
|
|
96
|
+
if (options.package && !id.includes("/") && !id.includes(":")) {
|
|
97
|
+
const pkgRoot = resolvePackageRoot(options.package);
|
|
98
|
+
if (!pkgRoot) {
|
|
99
|
+
throw new ArgumentError(
|
|
100
|
+
`Package '${options.package}' not found in linked packages or path`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
actionTarget = `${options.package}/${id}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let resolvedProjectRoot: string;
|
|
107
|
+
let resolvedActionId: string;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const resolved = await resolveActionProject(actionTarget);
|
|
111
|
+
resolvedProjectRoot = resolved.projectRoot;
|
|
112
|
+
resolvedActionId = resolved.actionId;
|
|
113
|
+
} catch (err: any) {
|
|
114
|
+
if (err.message?.includes("not found") || err.message?.includes("no longer exists")) {
|
|
115
|
+
throw new ArgumentError(err.message);
|
|
116
|
+
}
|
|
117
|
+
throw new ExecutionError(err.message);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const config = loadProjectConfig(resolvedProjectRoot);
|
|
122
|
+
const actions = await loadActions(resolvedProjectRoot, config.actionsDir);
|
|
123
|
+
const storage = createStorage(config.id, {
|
|
124
|
+
projectRoot: resolvedProjectRoot,
|
|
125
|
+
dataDir: options.dataDir,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const runner = new ActionRunner({
|
|
129
|
+
packageId: config.id,
|
|
130
|
+
storage,
|
|
131
|
+
projectConfig: config,
|
|
132
|
+
configOverrides,
|
|
133
|
+
actions,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = await runner.execute(resolvedActionId, input, {
|
|
137
|
+
signal: controller.signal,
|
|
138
|
+
timeoutMs,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
console.log(JSON.stringify(result, null, 2));
|
|
142
|
+
storage.close();
|
|
143
|
+
|
|
144
|
+
if (!result.ok) {
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
}
|
|
147
|
+
} catch (err: any) {
|
|
148
|
+
if (err instanceof ArgumentError) throw err;
|
|
149
|
+
throw new ExecutionError(err.message);
|
|
150
|
+
}
|
|
151
|
+
} finally {
|
|
152
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function registerActionRunCommand(actionCmd: Command, program: Command): void {
|
|
157
|
+
actionCmd
|
|
158
|
+
.command("run <id>")
|
|
159
|
+
.description("Execute an action (from current project, linked packages, or remote profile)")
|
|
160
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
161
|
+
.option("-i, --input <json>", "Input as JSON string")
|
|
162
|
+
.option("-f, --input-file <path>", "Input from JSON file")
|
|
163
|
+
.option("-c, --config <key=value...>", "Temporary config override (repeatable or comma-separated)")
|
|
164
|
+
.option("-p, --profile <name>", "Execute against a specific profile")
|
|
165
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
166
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
167
|
+
.option("--timeout <duration>", "Execution timeout (e.g. 30s, 5m, 500ms)")
|
|
168
|
+
.option("--async", "Execute asynchronously in background (requires remote server or profile)")
|
|
169
|
+
.option("--data-dir <path>", "Custom database directory")
|
|
170
|
+
.option("--json", "Output as JSON")
|
|
171
|
+
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
172
|
+
.action(async (id, rawOptions, cmd) => {
|
|
173
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
174
|
+
await executeAction(id, options);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Root level alias: ad run <id>
|
|
178
|
+
program
|
|
179
|
+
.command("run <id>")
|
|
180
|
+
.description("Alias for 'ad action run <id>'")
|
|
181
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
182
|
+
.option("-i, --input <json>", "Input as JSON string")
|
|
183
|
+
.option("-f, --input-file <path>", "Input from JSON file")
|
|
184
|
+
.option("-c, --config <key=value...>", "Temporary config override")
|
|
185
|
+
.option("-p, --profile <name>", "Execute against a specific profile")
|
|
186
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
187
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
188
|
+
.option("--timeout <duration>", "Execution timeout (e.g. 30s, 5m, 500ms)")
|
|
189
|
+
.option("--async", "Execute asynchronously in background (requires remote server or profile)")
|
|
190
|
+
.option("--data-dir <path>", "Custom database directory")
|
|
191
|
+
.option("--json", "Output as JSON")
|
|
192
|
+
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
193
|
+
.action(async (id, rawOptions, cmd) => {
|
|
194
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
195
|
+
await executeAction(id, options);
|
|
196
|
+
});
|
|
197
|
+
}
|