@actiondock/cli 2.0.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 +85 -0
- package/bin/ac.js +4 -0
- package/dist/index.js +44620 -0
- package/package.json +51 -0
- package/src/commands/action.ts +602 -0
- package/src/commands/build.ts +54 -0
- package/src/commands/config.ts +398 -0
- package/src/commands/doctor.ts +64 -0
- package/src/commands/export.ts +70 -0
- package/src/commands/index.ts +43 -0
- package/src/commands/info.ts +539 -0
- package/src/commands/init.ts +24 -0
- package/src/commands/link.ts +74 -0
- package/src/commands/mcp.ts +199 -0
- package/src/commands/playbook.ts +359 -0
- package/src/commands/profile.ts +263 -0
- package/src/commands/runs.ts +319 -0
- package/src/commands/serve.ts +101 -0
- package/src/commands/state.ts +396 -0
- package/src/commands/test.ts +26 -0
- package/src/index.ts +20 -0
- package/src/utils/bytes.ts +36 -0
- package/src/utils/duration.ts +1 -0
- package/src/utils/filter.ts +24 -0
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@actiondock/cli",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "ActionDock CLI facade for developing, testing, building, and exporting standalone Agent Actions and Skills",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ac": "./bin/ac.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.ts",
|
|
10
|
+
"module": "./src/index.ts",
|
|
11
|
+
"types": "./src/index.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"bun": ">=1.2.0"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"registry": "https://registry.npmjs.org/"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "bun test",
|
|
27
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@actiondock/core": "^2.0.0",
|
|
31
|
+
"@actiondock/mcp": "^2.0.0",
|
|
32
|
+
"@actiondock/sdk": "^2.0.0",
|
|
33
|
+
"commander": "^13.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/bun": "latest",
|
|
37
|
+
"typescript": "^5.7.0"
|
|
38
|
+
},
|
|
39
|
+
"keywords": ["actiondock", "ac", "agent", "cli", "skill", "builder", "mcp"],
|
|
40
|
+
"author": "team4u",
|
|
41
|
+
"license": "Apache-2.0",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/team4u/actiondock.git",
|
|
45
|
+
"directory": "packages/cli"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/team4u/actiondock#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/team4u/actiondock/issues"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
ActionRunner,
|
|
5
|
+
createStorage,
|
|
6
|
+
executeRemoteAction,
|
|
7
|
+
fetchRemoteActions,
|
|
8
|
+
fetchRemoteActionShow,
|
|
9
|
+
filterWithFallbackInfo,
|
|
10
|
+
findProjectRoot,
|
|
11
|
+
getPackageSlug,
|
|
12
|
+
listLinkedPackages,
|
|
13
|
+
loadActions,
|
|
14
|
+
loadProjectConfig,
|
|
15
|
+
resolveActionProject,
|
|
16
|
+
resolveTarget,
|
|
17
|
+
validateSchema,
|
|
18
|
+
} from "@actiondock/core";
|
|
19
|
+
import { Command } from "commander";
|
|
20
|
+
import { parseDuration } from "../utils/duration";
|
|
21
|
+
import { resolveIntent } from "../utils/filter";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 注册 Action 相关的 CLI 命令集合(包括 action list, action describe, action run, action new, action test 以及顶层 run 别名)。
|
|
25
|
+
*
|
|
26
|
+
* @param program Commander 根程序对象
|
|
27
|
+
*/
|
|
28
|
+
export function registerActionCommands(program: Command): void {
|
|
29
|
+
const actionCmd = program
|
|
30
|
+
.command("action")
|
|
31
|
+
.description("Manage and execute Actions");
|
|
32
|
+
|
|
33
|
+
// action list
|
|
34
|
+
actionCmd
|
|
35
|
+
.command("list [patterns...]")
|
|
36
|
+
.description("List actions in current project, linked packages, or remote profile")
|
|
37
|
+
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
|
|
38
|
+
.option("-p, --profile <name>", "Execute or query against a specific profile")
|
|
39
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
40
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
41
|
+
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
42
|
+
.option("--json", "Output as JSON")
|
|
43
|
+
.action(async (patterns, options) => {
|
|
44
|
+
try {
|
|
45
|
+
const effectiveIntent = resolveIntent(options.intent, patterns);
|
|
46
|
+
const shouldFallback = options.fallback !== false;
|
|
47
|
+
|
|
48
|
+
const target = resolveTarget({
|
|
49
|
+
profile: options.profile,
|
|
50
|
+
server: options.server,
|
|
51
|
+
token: options.token,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (target.type === "remote") {
|
|
55
|
+
let list = await fetchRemoteActions(
|
|
56
|
+
target.serverUrl!,
|
|
57
|
+
target.token,
|
|
58
|
+
effectiveIntent
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
let isFallback = false;
|
|
62
|
+
if (list.length === 0 && effectiveIntent && shouldFallback) {
|
|
63
|
+
list = await fetchRemoteActions(target.serverUrl!, target.token);
|
|
64
|
+
isFallback = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (options.json) {
|
|
68
|
+
console.log(JSON.stringify(list, null, 2));
|
|
69
|
+
} else {
|
|
70
|
+
console.log(
|
|
71
|
+
`Actions on remote server ${target.serverUrl}${target.profileName ? ` (Profile: ${target.profileName})` : ""}:\n`
|
|
72
|
+
);
|
|
73
|
+
if (isFallback && effectiveIntent) {
|
|
74
|
+
console.log(`(No remote actions matched intent '${effectiveIntent}', showing all actions)\n`);
|
|
75
|
+
}
|
|
76
|
+
for (const a of list) {
|
|
77
|
+
console.log(` ${a.id.padEnd(28)} ${a.description}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const root = findProjectRoot();
|
|
84
|
+
if (root) {
|
|
85
|
+
const config = loadProjectConfig(root);
|
|
86
|
+
const actions = await loadActions(root, config.actionsDir);
|
|
87
|
+
const rawList = Array.from(actions.values()).map((a) => ({
|
|
88
|
+
id: a.id,
|
|
89
|
+
description: a.description || "",
|
|
90
|
+
packageId: config.id,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const filterRes = filterWithFallbackInfo(
|
|
94
|
+
rawList,
|
|
95
|
+
effectiveIntent,
|
|
96
|
+
[(a) => a.id, (a) => a.description, (a) => a.packageId],
|
|
97
|
+
shouldFallback
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (options.json) {
|
|
101
|
+
console.log(JSON.stringify(filterRes.items, null, 2));
|
|
102
|
+
} else {
|
|
103
|
+
console.log(`Actions in ${config.id} (${root}):\n`);
|
|
104
|
+
if (filterRes.isFallback && effectiveIntent) {
|
|
105
|
+
console.log(`(No actions matched intent '${effectiveIntent}', showing all actions)\n`);
|
|
106
|
+
}
|
|
107
|
+
for (const a of filterRes.items) {
|
|
108
|
+
console.log(` ${a.id.padEnd(28)} ${a.description}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// List actions across all linked packages
|
|
113
|
+
const linkedList = listLinkedPackages();
|
|
114
|
+
if (linkedList.length === 0) {
|
|
115
|
+
console.log("No ActionDock project in current directory, and no packages linked.");
|
|
116
|
+
console.log("Run 'ac link' inside an Action package to register it.");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const aggregated: Array<{
|
|
121
|
+
packageId: string;
|
|
122
|
+
packageName: string;
|
|
123
|
+
path: string;
|
|
124
|
+
actions: Array<{ id: string; description: string }>;
|
|
125
|
+
}> = [];
|
|
126
|
+
|
|
127
|
+
for (const pkg of linkedList) {
|
|
128
|
+
if (!existsSync(pkg.path)) continue;
|
|
129
|
+
try {
|
|
130
|
+
const config = loadProjectConfig(pkg.path);
|
|
131
|
+
const actions = await loadActions(pkg.path, config.actionsDir);
|
|
132
|
+
const pkgActions = Array.from(actions.values()).map((a) => ({
|
|
133
|
+
id: a.id,
|
|
134
|
+
description: a.description || "",
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
aggregated.push({
|
|
138
|
+
packageId: pkg.id,
|
|
139
|
+
packageName: pkg.name,
|
|
140
|
+
path: pkg.path,
|
|
141
|
+
actions: pkgActions,
|
|
142
|
+
});
|
|
143
|
+
} catch {
|
|
144
|
+
// Ignore broken linked package
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let filteredPackages: typeof aggregated = [];
|
|
149
|
+
if (!effectiveIntent) {
|
|
150
|
+
filteredPackages = aggregated;
|
|
151
|
+
} else {
|
|
152
|
+
for (const pkg of aggregated) {
|
|
153
|
+
const pkgMatches = filterWithFallbackInfo(
|
|
154
|
+
[pkg],
|
|
155
|
+
effectiveIntent,
|
|
156
|
+
[(p) => p.packageId, (p) => p.packageName, (p) => p.path],
|
|
157
|
+
false
|
|
158
|
+
).matchedCount > 0;
|
|
159
|
+
|
|
160
|
+
if (pkgMatches) {
|
|
161
|
+
filteredPackages.push(pkg);
|
|
162
|
+
} else {
|
|
163
|
+
const matchedActions = filterWithFallbackInfo(
|
|
164
|
+
pkg.actions,
|
|
165
|
+
effectiveIntent,
|
|
166
|
+
[(a) => a.id, (a) => a.description],
|
|
167
|
+
false
|
|
168
|
+
).items;
|
|
169
|
+
|
|
170
|
+
if (matchedActions.length > 0) {
|
|
171
|
+
filteredPackages.push({
|
|
172
|
+
...pkg,
|
|
173
|
+
actions: matchedActions,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (filteredPackages.length === 0 && shouldFallback) {
|
|
180
|
+
filteredPackages = aggregated;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (options.json) {
|
|
185
|
+
console.log(JSON.stringify(filteredPackages, null, 2));
|
|
186
|
+
} else {
|
|
187
|
+
console.log("Linked Action Packages:\n");
|
|
188
|
+
for (const pkg of filteredPackages) {
|
|
189
|
+
console.log(`* Package: ${pkg.packageId} (${pkg.path})`);
|
|
190
|
+
for (const a of pkg.actions) {
|
|
191
|
+
console.log(` - ${a.id.padEnd(26)} ${a.description}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch (err: any) {
|
|
197
|
+
console.error(`Error: ${err.message}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
// action create / new
|
|
204
|
+
actionCmd
|
|
205
|
+
.command("create <id>")
|
|
206
|
+
.alias("new")
|
|
207
|
+
.description("Scaffold a new Action definition file")
|
|
208
|
+
.option("-d, --desc <description>", "Action description")
|
|
209
|
+
.option("-f, --file <filePath>", "Target file path relative to actions dir")
|
|
210
|
+
.action(async (id, options) => {
|
|
211
|
+
const root = findProjectRoot();
|
|
212
|
+
if (!root) {
|
|
213
|
+
console.error("Error: Not in an ActionDock project (actiondock.json not found)");
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const config = loadProjectConfig(root);
|
|
218
|
+
const actionsDir = resolve(root, config.actionsDir || "actions");
|
|
219
|
+
if (!existsSync(actionsDir)) {
|
|
220
|
+
mkdirSync(actionsDir, { recursive: true });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const cleanName = getPackageSlug(id);
|
|
224
|
+
const targetRelFile = options.file || `${cleanName}.ts`;
|
|
225
|
+
const targetFullFile = resolve(actionsDir, targetRelFile);
|
|
226
|
+
|
|
227
|
+
if (existsSync(targetFullFile)) {
|
|
228
|
+
console.error(`Error: Target action file already exists at ${targetFullFile}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const desc = options.desc || `Action ${id}`;
|
|
233
|
+
const template = `import { defineAction } from "@actiondock/sdk";
|
|
234
|
+
|
|
235
|
+
export interface Input {
|
|
236
|
+
exampleParam?: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface Output {
|
|
240
|
+
success: boolean;
|
|
241
|
+
result?: unknown;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export default defineAction<Input, Output>({
|
|
245
|
+
id: "${id}",
|
|
246
|
+
description: "${desc}",
|
|
247
|
+
|
|
248
|
+
inputSchema: {
|
|
249
|
+
type: "object",
|
|
250
|
+
properties: {
|
|
251
|
+
exampleParam: {
|
|
252
|
+
type: "string",
|
|
253
|
+
description: "Example parameter description",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
required: [],
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
outputSchema: {
|
|
260
|
+
type: "object",
|
|
261
|
+
properties: {
|
|
262
|
+
success: { type: "boolean" },
|
|
263
|
+
result: {},
|
|
264
|
+
},
|
|
265
|
+
required: ["success"],
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
async run(input, ctx) {
|
|
269
|
+
ctx.log.info("Running ${id}", input);
|
|
270
|
+
|
|
271
|
+
// Access config: ctx.config.get("MY_CONFIG")
|
|
272
|
+
// Access state: await ctx.state.get("my_key") / await ctx.state.set("my_key", val)
|
|
273
|
+
// Call action: await ctx.actions.invoke(otherAction, input)
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
success: true,
|
|
277
|
+
result: input.exampleParam || "done",
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
`;
|
|
282
|
+
|
|
283
|
+
writeFileSync(targetFullFile, template, "utf-8");
|
|
284
|
+
console.log(`[OK] Created Action '${id}' at ${targetFullFile}`);
|
|
285
|
+
console.log(`\nTo run this action:`);
|
|
286
|
+
console.log(` ac action run ${id} --input '{"exampleParam": "hello"}'`);
|
|
287
|
+
} catch (err: any) {
|
|
288
|
+
console.error(`Error: ${err.message}`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// action show / describe
|
|
294
|
+
actionCmd
|
|
295
|
+
.command("show <id>")
|
|
296
|
+
.alias("describe")
|
|
297
|
+
.description("Show action definition, schema, and description")
|
|
298
|
+
.option("-p, --profile <name>", "Execute or query against a specific profile")
|
|
299
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
300
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
301
|
+
.option("--json", "Output as JSON")
|
|
302
|
+
.action(async (id, options) => {
|
|
303
|
+
try {
|
|
304
|
+
const target = resolveTarget({
|
|
305
|
+
profile: options.profile,
|
|
306
|
+
server: options.server,
|
|
307
|
+
token: options.token,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (target.type === "remote") {
|
|
311
|
+
const detail = await fetchRemoteActionShow(
|
|
312
|
+
target.serverUrl!,
|
|
313
|
+
id,
|
|
314
|
+
target.token
|
|
315
|
+
);
|
|
316
|
+
if (options.json) {
|
|
317
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
318
|
+
} else {
|
|
319
|
+
console.log(`Action: ${detail.id}`);
|
|
320
|
+
if (detail.packageId) console.log(`Package: ${detail.packageId}`);
|
|
321
|
+
if (detail.description) console.log(`Description: ${detail.description}`);
|
|
322
|
+
if (detail.inputSchema) {
|
|
323
|
+
console.log("\nInput Schema:");
|
|
324
|
+
console.log(JSON.stringify(detail.inputSchema, null, 2));
|
|
325
|
+
}
|
|
326
|
+
if (detail.outputSchema) {
|
|
327
|
+
console.log("\nOutput Schema:");
|
|
328
|
+
console.log(JSON.stringify(detail.outputSchema, null, 2));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const resolved = await resolveActionProject(id);
|
|
335
|
+
const config = loadProjectConfig(resolved.projectRoot);
|
|
336
|
+
const actions = await loadActions(resolved.projectRoot, config.actionsDir);
|
|
337
|
+
const action = actions.get(resolved.actionId);
|
|
338
|
+
if (!action) {
|
|
339
|
+
console.error(`Error: Action '${resolved.actionId}' not found in package '${resolved.packageId}'`);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (options.json) {
|
|
344
|
+
console.log(
|
|
345
|
+
JSON.stringify(
|
|
346
|
+
{
|
|
347
|
+
id: action.id,
|
|
348
|
+
packageId: resolved.packageId,
|
|
349
|
+
description: action.description,
|
|
350
|
+
inputSchema: action.inputSchema,
|
|
351
|
+
outputSchema: action.outputSchema,
|
|
352
|
+
},
|
|
353
|
+
null,
|
|
354
|
+
2
|
|
355
|
+
)
|
|
356
|
+
);
|
|
357
|
+
} else {
|
|
358
|
+
console.log(`Action: ${action.id}`);
|
|
359
|
+
console.log(`Package: ${resolved.packageId} (${resolved.projectRoot})`);
|
|
360
|
+
if (action.description) console.log(`Description: ${action.description}`);
|
|
361
|
+
if (action.inputSchema) {
|
|
362
|
+
console.log("\nInput Schema:");
|
|
363
|
+
console.log(JSON.stringify(action.inputSchema, null, 2));
|
|
364
|
+
}
|
|
365
|
+
if (action.outputSchema) {
|
|
366
|
+
console.log("\nOutput Schema:");
|
|
367
|
+
console.log(JSON.stringify(action.outputSchema, null, 2));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} catch (err: any) {
|
|
371
|
+
console.error(`Error: ${err.message}`);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// action validate
|
|
377
|
+
actionCmd
|
|
378
|
+
.command("validate [id]")
|
|
379
|
+
.description("Validate action schemas and definitions")
|
|
380
|
+
.option("--json", "Output as JSON")
|
|
381
|
+
.action(async (id, options) => {
|
|
382
|
+
const root = findProjectRoot();
|
|
383
|
+
if (!root) {
|
|
384
|
+
console.error("Error: Not in an ActionDock project");
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
const config = loadProjectConfig(root);
|
|
389
|
+
const actions = await loadActions(root, config.actionsDir);
|
|
390
|
+
|
|
391
|
+
const toValidate = id ? [actions.get(id)].filter(Boolean) : Array.from(actions.values());
|
|
392
|
+
if (id && toValidate.length === 0) {
|
|
393
|
+
console.error(`Error: Action '${id}' not found in project`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const results: Array<{ id: string; valid: boolean; errors: string[] }> = [];
|
|
398
|
+
|
|
399
|
+
for (const act of toValidate as any[]) {
|
|
400
|
+
const errors: string[] = [];
|
|
401
|
+
if (!act.id) errors.push("Missing id property");
|
|
402
|
+
if (!act.run || typeof act.run !== "function") errors.push("Missing run method");
|
|
403
|
+
if (act.inputSchema) {
|
|
404
|
+
const vRes = validateSchema(act.inputSchema, {});
|
|
405
|
+
// Check if schema itself is valid
|
|
406
|
+
if (typeof act.inputSchema !== "object") {
|
|
407
|
+
errors.push("Invalid inputSchema object");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (act.outputSchema) {
|
|
411
|
+
if (typeof act.outputSchema !== "object") {
|
|
412
|
+
errors.push("Invalid outputSchema object");
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
results.push({
|
|
416
|
+
id: act.id,
|
|
417
|
+
valid: errors.length === 0,
|
|
418
|
+
errors,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const allValid = results.every((r) => r.valid);
|
|
423
|
+
if (options.json) {
|
|
424
|
+
console.log(JSON.stringify({ valid: allValid, results }, null, 2));
|
|
425
|
+
} else {
|
|
426
|
+
for (const r of results) {
|
|
427
|
+
if (r.valid) {
|
|
428
|
+
console.log(`[OK] ${r.id}: Valid`);
|
|
429
|
+
} else {
|
|
430
|
+
console.log(`[FAIL] ${r.id}: ${r.errors.join(", ")}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (!allValid) process.exit(1);
|
|
435
|
+
} catch (err: any) {
|
|
436
|
+
console.error(`Error: ${err.message}`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// action run
|
|
442
|
+
actionCmd
|
|
443
|
+
.command("run <id>")
|
|
444
|
+
.description("Execute an action (from current project, linked packages, or remote profile)")
|
|
445
|
+
.option("-i, --input <json>", "Input as JSON string")
|
|
446
|
+
.option("-f, --input-file <path>", "Input from JSON file")
|
|
447
|
+
.option("-c, --config <key=value...>", "Temporary config override (repeatable or comma-separated)")
|
|
448
|
+
.option("-p, --profile <name>", "Execute against a specific profile")
|
|
449
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
450
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
451
|
+
.option("--timeout <duration>", "Execution timeout (e.g. 30s, 5m, 500ms)")
|
|
452
|
+
.option("--async", "Execute asynchronously in background (requires remote server or profile)")
|
|
453
|
+
.action(async (id, options) => {
|
|
454
|
+
await executeAction(id, options);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Root level alias: ac run <id>
|
|
458
|
+
program
|
|
459
|
+
.command("run <id>")
|
|
460
|
+
.description("Alias for 'ac action run <id>'")
|
|
461
|
+
.option("-i, --input <json>", "Input as JSON string")
|
|
462
|
+
.option("-f, --input-file <path>", "Input from JSON file")
|
|
463
|
+
.option("-c, --config <key=value...>", "Temporary config override")
|
|
464
|
+
.option("-p, --profile <name>", "Execute against a specific profile")
|
|
465
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
466
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
467
|
+
.option("--timeout <duration>", "Execution timeout (e.g. 30s, 5m, 500ms)")
|
|
468
|
+
.option("--async", "Execute asynchronously in background (requires remote server or profile)")
|
|
469
|
+
.action(async (id, options) => {
|
|
470
|
+
await executeAction(id, options);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function executeAction(id: string, options: any): Promise<void> {
|
|
475
|
+
let input: unknown = {};
|
|
476
|
+
if (options.input) {
|
|
477
|
+
try {
|
|
478
|
+
input = JSON.parse(options.input);
|
|
479
|
+
} catch (err: any) {
|
|
480
|
+
console.error(`Error parsing --input JSON: ${err.message}`);
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
} else if (options.inputFile) {
|
|
484
|
+
try {
|
|
485
|
+
input = JSON.parse(readFileSync(options.inputFile, "utf-8"));
|
|
486
|
+
} catch (err: any) {
|
|
487
|
+
console.error(`Error reading --input-file: ${err.message}`);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let timeoutMs: number | undefined;
|
|
493
|
+
if (options.timeout) {
|
|
494
|
+
try {
|
|
495
|
+
timeoutMs = parseDuration(options.timeout);
|
|
496
|
+
} catch (err: any) {
|
|
497
|
+
console.error(`Error: ${err.message}`);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const configOverrides: Record<string, unknown> = {};
|
|
503
|
+
if (options.config) {
|
|
504
|
+
const list = Array.isArray(options.config) ? options.config : [options.config];
|
|
505
|
+
for (const item of list) {
|
|
506
|
+
const [k, ...v] = item.split("=");
|
|
507
|
+
if (k) configOverrides[k] = v.join("=");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Check target (remote profile vs local)
|
|
512
|
+
let target;
|
|
513
|
+
try {
|
|
514
|
+
target = resolveTarget({
|
|
515
|
+
profile: options.profile,
|
|
516
|
+
server: options.server,
|
|
517
|
+
token: options.token,
|
|
518
|
+
});
|
|
519
|
+
} catch (err: any) {
|
|
520
|
+
console.error(`Error: ${err.message}`);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const controller = new AbortController();
|
|
525
|
+
const sigintHandler = () => {
|
|
526
|
+
controller.abort(new Error("Interrupted by SIGINT"));
|
|
527
|
+
};
|
|
528
|
+
process.once("SIGINT", sigintHandler);
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
if (target.type === "remote") {
|
|
532
|
+
try {
|
|
533
|
+
const result = await executeRemoteAction(target.serverUrl!, id, input, {
|
|
534
|
+
configOverrides,
|
|
535
|
+
token: target.token,
|
|
536
|
+
timeoutMs,
|
|
537
|
+
signal: controller.signal,
|
|
538
|
+
async: Boolean(options.async),
|
|
539
|
+
});
|
|
540
|
+
console.log(JSON.stringify(result, null, 2));
|
|
541
|
+
if (!result.ok) {
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
} catch (err: any) {
|
|
546
|
+
console.error(`Error: ${err.message}`);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Local execution
|
|
552
|
+
if (options.async) {
|
|
553
|
+
console.error(
|
|
554
|
+
"Error: Async execution requires a long-running ActionDock server.\nUse --profile, --server, or start `ac serve`."
|
|
555
|
+
);
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
let resolvedProjectRoot: string;
|
|
560
|
+
let resolvedActionId: string;
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
const resolved = await resolveActionProject(id);
|
|
564
|
+
resolvedProjectRoot = resolved.projectRoot;
|
|
565
|
+
resolvedActionId = resolved.actionId;
|
|
566
|
+
} catch (err: any) {
|
|
567
|
+
console.error(`Error: ${err.message}`);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
const config = loadProjectConfig(resolvedProjectRoot);
|
|
573
|
+
const actions = await loadActions(resolvedProjectRoot, config.actionsDir);
|
|
574
|
+
const storage = createStorage(config.id, { projectRoot: resolvedProjectRoot });
|
|
575
|
+
|
|
576
|
+
const runner = new ActionRunner({
|
|
577
|
+
packageId: config.id,
|
|
578
|
+
storage,
|
|
579
|
+
projectConfig: config,
|
|
580
|
+
configOverrides,
|
|
581
|
+
actions,
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const result = await runner.execute(resolvedActionId, input, {
|
|
585
|
+
signal: controller.signal,
|
|
586
|
+
timeoutMs,
|
|
587
|
+
});
|
|
588
|
+
console.log(JSON.stringify(result, null, 2));
|
|
589
|
+
storage.close();
|
|
590
|
+
|
|
591
|
+
if (!result.ok) {
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
} catch (err: any) {
|
|
595
|
+
console.error(`Error: ${err.message}`);
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
} finally {
|
|
599
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { buildProject, findProjectRoot, resolvePackageRoot } from "@actiondock/core";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
export function registerBuildCommand(program: Command): void {
|
|
5
|
+
program
|
|
6
|
+
.command("build")
|
|
7
|
+
.description("Build project actions into a single standalone executable")
|
|
8
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
9
|
+
.option("-t, --target <target>", "Target compilation platform (e.g. bun, linux-x64, darwin-arm64, windows-x64)")
|
|
10
|
+
.option("-o, --out <path>", "Output executable path")
|
|
11
|
+
.option("-a, --actions <actions...>", "Only build specific action(s) into the standalone binary")
|
|
12
|
+
.option("-m, --minify", "Minify bundled JavaScript (default: true)", true)
|
|
13
|
+
.option("--no-minify", "Disable JavaScript minification")
|
|
14
|
+
.option("--bytecode", "Compile JavaScript to bytecode for faster startup (default: true)", true)
|
|
15
|
+
.option("--no-bytecode", "Disable bytecode compilation")
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
const root = options.package
|
|
18
|
+
? resolvePackageRoot(options.package)
|
|
19
|
+
: findProjectRoot();
|
|
20
|
+
|
|
21
|
+
if (!root) {
|
|
22
|
+
if (options.package) {
|
|
23
|
+
console.error(`Error: Package '${options.package}' not found in linked packages or path`);
|
|
24
|
+
} else {
|
|
25
|
+
console.error(
|
|
26
|
+
"Error: Not in an ActionDock project (actiondock.json not found).\nPlease specify -P, --package <id> or cd into a project directory."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
console.log("Building standalone executable...");
|
|
34
|
+
const result = await buildProject({
|
|
35
|
+
projectRoot: root,
|
|
36
|
+
target: options.target,
|
|
37
|
+
outfile: options.out,
|
|
38
|
+
minify: options.minify,
|
|
39
|
+
bytecode: options.bytecode,
|
|
40
|
+
actions: options.actions,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log(`[OK] Successfully compiled ${result.packageId} (v${result.version})`);
|
|
44
|
+
console.log(` Target: ${result.target}`);
|
|
45
|
+
console.log(` Actions: ${result.actions.join(", ")}`);
|
|
46
|
+
console.log(` Executable: ${result.executablePath}`);
|
|
47
|
+
console.log(` Metadata: ${result.metadataPath}`);
|
|
48
|
+
} catch (err: any) {
|
|
49
|
+
console.error(`Build failed: ${err.message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|