@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
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addProfile,
|
|
3
|
+
checkRemoteHealth,
|
|
4
|
+
filterWithFallbackInfo,
|
|
5
|
+
getProfile,
|
|
6
|
+
listProfiles,
|
|
7
|
+
loadProfiles,
|
|
8
|
+
maskSecretValue,
|
|
9
|
+
removeProfile,
|
|
10
|
+
resolveProfileToken,
|
|
11
|
+
resolveTarget,
|
|
12
|
+
toSnakeUpperCase,
|
|
13
|
+
useProfile,
|
|
14
|
+
} from "@actiondock/core";
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
import { resolveIntent } from "../utils/filter";
|
|
17
|
+
|
|
18
|
+
export function registerProfileCommands(program: Command): void {
|
|
19
|
+
const profileCmd = program
|
|
20
|
+
.command("profile")
|
|
21
|
+
.description("Manage multi-cloud and remote execution profiles");
|
|
22
|
+
|
|
23
|
+
// ac profile list
|
|
24
|
+
profileCmd
|
|
25
|
+
.command("list [patterns...]")
|
|
26
|
+
.description("List all configured profiles")
|
|
27
|
+
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
|
|
28
|
+
.option("--reveal, --show-secrets", "Reveal plain text values for tokens")
|
|
29
|
+
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
30
|
+
.option("--json", "Output as JSON")
|
|
31
|
+
.action((patterns, options) => {
|
|
32
|
+
try {
|
|
33
|
+
const effectiveIntent = resolveIntent(options.intent, patterns);
|
|
34
|
+
const shouldFallback = options.fallback !== false;
|
|
35
|
+
const reveal = Boolean(options.reveal || options.showSecrets);
|
|
36
|
+
|
|
37
|
+
const list = listProfiles();
|
|
38
|
+
const enriched = list.map((p) => {
|
|
39
|
+
const resolved = resolveProfileToken(p.name, p.entry);
|
|
40
|
+
return {
|
|
41
|
+
name: p.name,
|
|
42
|
+
isCurrent: p.isCurrent,
|
|
43
|
+
serverUrl: p.entry.serverUrl,
|
|
44
|
+
description: p.entry.description || "",
|
|
45
|
+
tokenEnv: p.entry.tokenEnv,
|
|
46
|
+
tokenConfigured: resolved.source !== "none",
|
|
47
|
+
tokenSource: resolved.source,
|
|
48
|
+
token: reveal ? resolved.token : (resolved.token ? maskSecretValue(resolved.token) : undefined),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const filterRes = filterWithFallbackInfo(
|
|
53
|
+
enriched,
|
|
54
|
+
effectiveIntent,
|
|
55
|
+
[(p) => p.name, (p) => p.serverUrl, (p) => p.description, (p) => p.tokenSource],
|
|
56
|
+
shouldFallback
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (options.json) {
|
|
60
|
+
console.log(JSON.stringify(filterRes.items, null, 2));
|
|
61
|
+
} else {
|
|
62
|
+
console.log("ActionDock Execution Profiles:\n");
|
|
63
|
+
if (filterRes.isFallback && effectiveIntent) {
|
|
64
|
+
console.log(`(No profiles matched intent '${effectiveIntent}', showing all profiles)\n`);
|
|
65
|
+
}
|
|
66
|
+
for (const item of filterRes.items) {
|
|
67
|
+
const currentMarker = item.isCurrent ? "* " : " ";
|
|
68
|
+
let tokenInfo = "";
|
|
69
|
+
if (item.tokenSource === "tokenEnv") {
|
|
70
|
+
tokenInfo = ` [token: env(${item.tokenEnv})]`;
|
|
71
|
+
} else if (item.tokenSource === "profileEnv") {
|
|
72
|
+
tokenInfo = ` [token: env(ACTIONDOCK_${toSnakeUpperCase(item.name)}_TOKEN)]`;
|
|
73
|
+
} else if (item.tokenSource === "profile") {
|
|
74
|
+
tokenInfo = ` [token: stored in profile]`;
|
|
75
|
+
} else if (item.tokenSource === "globalEnv") {
|
|
76
|
+
tokenInfo = ` [token: env(ACTIONDOCK_TOKEN)]`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (reveal && item.token) {
|
|
80
|
+
tokenInfo += ` = ${item.token}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const desc = item.description ? ` - ${item.description}` : "";
|
|
84
|
+
console.log(
|
|
85
|
+
`${currentMarker}${item.name.padEnd(20)} ${item.serverUrl}${tokenInfo}${desc}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
console.log(
|
|
89
|
+
"\nUse 'ac profile use <name>' to switch or 'ac run <action> --profile <name>' to execute on a specific target."
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
} catch (err: any) {
|
|
93
|
+
console.error(`Error: ${err.message}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ac profile add <name>
|
|
99
|
+
profileCmd
|
|
100
|
+
.command("add <name>")
|
|
101
|
+
.description("Add or update a remote execution profile")
|
|
102
|
+
.requiredOption("-s, --server <url>", "Remote ActionDock server URL (e.g. http://1.2.3.4:5177)")
|
|
103
|
+
.option("-t, --token <token>", "Authentication token for the remote server (deprecated; prefer --token-env)")
|
|
104
|
+
.option("--token-env <env>", "Environment variable name containing the authentication token")
|
|
105
|
+
.option("-d, --desc <description>", "Description of this profile/machine")
|
|
106
|
+
.action((name, options) => {
|
|
107
|
+
try {
|
|
108
|
+
if (options.token) {
|
|
109
|
+
console.warn(
|
|
110
|
+
"Warning: storing tokens directly in profiles.json is deprecated. Use --token-env or standard environment variables (e.g. ACTIONDOCK_<PROFILE>_TOKEN) instead."
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
addProfile(name, {
|
|
114
|
+
serverUrl: options.server,
|
|
115
|
+
token: options.token,
|
|
116
|
+
tokenEnv: options.tokenEnv,
|
|
117
|
+
description: options.desc,
|
|
118
|
+
});
|
|
119
|
+
console.log(`[OK] Profile '${name}' configured for server: ${options.server}`);
|
|
120
|
+
} catch (err: any) {
|
|
121
|
+
console.error(`Error: ${err.message}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ac profile use <name>
|
|
127
|
+
profileCmd
|
|
128
|
+
.command("use <name>")
|
|
129
|
+
.description("Switch active default profile")
|
|
130
|
+
.action((name) => {
|
|
131
|
+
try {
|
|
132
|
+
useProfile(name);
|
|
133
|
+
console.log(`[OK] Active profile switched to '${name}'`);
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
console.error(`Error: ${err.message}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ac profile show [name]
|
|
141
|
+
profileCmd
|
|
142
|
+
.command("show [name]")
|
|
143
|
+
.description("Display details of a profile (defaults to active profile)")
|
|
144
|
+
.option("--reveal, --show-secrets", "Reveal plain text values for tokens")
|
|
145
|
+
.option("--json", "Output as JSON")
|
|
146
|
+
.action((name, options) => {
|
|
147
|
+
try {
|
|
148
|
+
const config = loadProfiles();
|
|
149
|
+
const targetName = name || config.currentProfile || "local";
|
|
150
|
+
const entry = getProfile(targetName);
|
|
151
|
+
const reveal = Boolean(options.reveal || options.showSecrets);
|
|
152
|
+
|
|
153
|
+
if (!entry && targetName !== "local") {
|
|
154
|
+
console.error(`Error: Profile '${targetName}' not found.`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const resolved = resolveProfileToken(targetName, entry);
|
|
159
|
+
const displayToken = reveal
|
|
160
|
+
? resolved.token
|
|
161
|
+
: (resolved.token ? maskSecretValue(resolved.token) : undefined);
|
|
162
|
+
|
|
163
|
+
const data = {
|
|
164
|
+
name: targetName,
|
|
165
|
+
isCurrent: config.currentProfile === targetName,
|
|
166
|
+
serverUrl: entry?.serverUrl || "local",
|
|
167
|
+
tokenConfigured: resolved.source !== "none",
|
|
168
|
+
tokenSource: resolved.source,
|
|
169
|
+
tokenEnv: entry?.tokenEnv,
|
|
170
|
+
token: displayToken,
|
|
171
|
+
description: entry?.description || "",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (options.json) {
|
|
175
|
+
console.log(JSON.stringify(data, null, 2));
|
|
176
|
+
} else {
|
|
177
|
+
console.log(`Profile: ${data.name}${data.isCurrent ? " (Active)" : ""}`);
|
|
178
|
+
console.log(`Server URL: ${data.serverUrl}`);
|
|
179
|
+
let sourceDetail = "None";
|
|
180
|
+
if (data.tokenSource === "tokenEnv") {
|
|
181
|
+
sourceDetail = `Environment Variable ($${data.tokenEnv})`;
|
|
182
|
+
} else if (data.tokenSource === "profileEnv") {
|
|
183
|
+
sourceDetail = `Profile Environment Variable ($ACTIONDOCK_${toSnakeUpperCase(data.name)}_TOKEN)`;
|
|
184
|
+
} else if (data.tokenSource === "profile") {
|
|
185
|
+
sourceDetail = "Stored in profiles.json (Deprecated)";
|
|
186
|
+
} else if (data.tokenSource === "globalEnv") {
|
|
187
|
+
sourceDetail = "Global Environment Variable ($ACTIONDOCK_TOKEN)";
|
|
188
|
+
}
|
|
189
|
+
console.log(`Auth Source: ${sourceDetail}`);
|
|
190
|
+
if (data.tokenConfigured) {
|
|
191
|
+
console.log(`Token Value: ${data.token}`);
|
|
192
|
+
}
|
|
193
|
+
if (data.description) {
|
|
194
|
+
console.log(`Description: ${data.description}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch (err: any) {
|
|
198
|
+
console.error(`Error: ${err.message}`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ac profile rm / remove <name>
|
|
204
|
+
profileCmd
|
|
205
|
+
.command("rm <name>")
|
|
206
|
+
.alias("remove")
|
|
207
|
+
.description("Remove a profile")
|
|
208
|
+
.action((name) => {
|
|
209
|
+
try {
|
|
210
|
+
if (name === "local") {
|
|
211
|
+
console.error("Error: Cannot remove built-in 'local' profile");
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
const removed = removeProfile(name);
|
|
215
|
+
if (!removed) {
|
|
216
|
+
console.error(`Error: Profile '${name}' not found`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
console.log(`[OK] Profile '${name}' removed`);
|
|
220
|
+
} catch (err: any) {
|
|
221
|
+
console.error(`Error: ${err.message}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ac profile test [name]
|
|
227
|
+
profileCmd
|
|
228
|
+
.command("test [name]")
|
|
229
|
+
.description("Test connection latency and health of a profile")
|
|
230
|
+
.option("--json", "Output as JSON")
|
|
231
|
+
.action(async (name, options) => {
|
|
232
|
+
try {
|
|
233
|
+
const target = resolveTarget({ profile: name });
|
|
234
|
+
if (target.type === "local") {
|
|
235
|
+
if (options.json) {
|
|
236
|
+
console.log(JSON.stringify({ ok: true, type: "local", message: "Local execution" }, null, 2));
|
|
237
|
+
} else {
|
|
238
|
+
console.log(`Target profile '${target.profileName || "local"}' is local (runs in local Bun runtime).`);
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const health = await checkRemoteHealth(target.serverUrl!, target.token);
|
|
244
|
+
if (options.json) {
|
|
245
|
+
console.log(JSON.stringify(health, null, 2));
|
|
246
|
+
} else {
|
|
247
|
+
if (health.ok) {
|
|
248
|
+
console.log(
|
|
249
|
+
`[OK] Connected to ${target.serverUrl} (${health.latencyMs}ms) - Version: ${health.version}, Status: ${health.status}`
|
|
250
|
+
);
|
|
251
|
+
} else {
|
|
252
|
+
console.error(
|
|
253
|
+
`[FAIL] Connection to ${target.serverUrl} failed (${health.latencyMs}ms): ${health.error}`
|
|
254
|
+
);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch (err: any) {
|
|
259
|
+
console.error(`Error: ${err.message}`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
cancelRemoteRun,
|
|
4
|
+
createStorage,
|
|
5
|
+
fetchRemoteRun,
|
|
6
|
+
filterWithFallbackInfo,
|
|
7
|
+
findProjectRoot,
|
|
8
|
+
listLinkedPackages,
|
|
9
|
+
loadProjectConfig,
|
|
10
|
+
resolvePackageRoot,
|
|
11
|
+
resolveTarget,
|
|
12
|
+
} from "@actiondock/core";
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
import { resolveIntent } from "../utils/filter";
|
|
15
|
+
|
|
16
|
+
function printRunRecord(run: any): void {
|
|
17
|
+
console.log(`Run: ${run.id}`);
|
|
18
|
+
console.log(`Action: ${run.actionId}`);
|
|
19
|
+
if (run.packageId) console.log(`Package: ${run.packageId}`);
|
|
20
|
+
console.log(`Status: ${run.status}`);
|
|
21
|
+
if (run.parentRunId) console.log(`Parent Run: ${run.parentRunId}`);
|
|
22
|
+
console.log(`Started: ${run.startedAt}`);
|
|
23
|
+
if (run.finishedAt) console.log(`Finished: ${run.finishedAt}`);
|
|
24
|
+
|
|
25
|
+
console.log("\nInput:");
|
|
26
|
+
console.log(JSON.stringify(run.input, null, 2));
|
|
27
|
+
|
|
28
|
+
if (run.output !== undefined) {
|
|
29
|
+
console.log("\nOutput:");
|
|
30
|
+
console.log(JSON.stringify(run.output, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (run.error) {
|
|
34
|
+
console.log("\nError:");
|
|
35
|
+
console.log(JSON.stringify(run.error, null, 2));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function registerRunsCommands(program: Command): void {
|
|
40
|
+
const runsCmd = program
|
|
41
|
+
.command("runs")
|
|
42
|
+
.description("Inspect action execution history");
|
|
43
|
+
|
|
44
|
+
// runs list
|
|
45
|
+
runsCmd
|
|
46
|
+
.command("list [patterns...]")
|
|
47
|
+
.description("List recent execution records (in current project or linked packages)")
|
|
48
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
49
|
+
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
|
|
50
|
+
.option("-a, --action <actionId>", "Filter by action ID")
|
|
51
|
+
.option("-n, --limit <count>", "Maximum number of records to return", "20")
|
|
52
|
+
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
53
|
+
.option("--json", "Output as JSON")
|
|
54
|
+
.action((patterns, options) => {
|
|
55
|
+
try {
|
|
56
|
+
const effectiveIntent = resolveIntent(options.intent, patterns);
|
|
57
|
+
const shouldFallback = options.fallback !== false;
|
|
58
|
+
const limit = Number.parseInt(options.limit, 10) || 20;
|
|
59
|
+
|
|
60
|
+
const targetRoot = options.package
|
|
61
|
+
? resolvePackageRoot(options.package)
|
|
62
|
+
: findProjectRoot();
|
|
63
|
+
|
|
64
|
+
if (options.package && !targetRoot) {
|
|
65
|
+
console.error(`Error: Package '${options.package}' not found in linked packages or path`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (targetRoot) {
|
|
70
|
+
const projConfig = loadProjectConfig(targetRoot);
|
|
71
|
+
const storage = createStorage(projConfig.id, { projectRoot: targetRoot });
|
|
72
|
+
const records = storage.listRuns({
|
|
73
|
+
actionId: options.action,
|
|
74
|
+
limit,
|
|
75
|
+
});
|
|
76
|
+
storage.close();
|
|
77
|
+
|
|
78
|
+
const filterRes = filterWithFallbackInfo(
|
|
79
|
+
records,
|
|
80
|
+
effectiveIntent,
|
|
81
|
+
[(r) => r.id, (r) => r.actionId, (r) => r.status, (r) => r.error?.message],
|
|
82
|
+
shouldFallback
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (options.json) {
|
|
86
|
+
console.log(JSON.stringify(filterRes.items, null, 2));
|
|
87
|
+
} else {
|
|
88
|
+
console.log(`Execution Runs in ${projConfig.id} (${filterRes.items.length}):\n`);
|
|
89
|
+
if (filterRes.isFallback && effectiveIntent) {
|
|
90
|
+
console.log(`(No runs matched intent '${effectiveIntent}', showing all runs)\n`);
|
|
91
|
+
}
|
|
92
|
+
console.log(
|
|
93
|
+
` ${"RUN ID".padEnd(38)} ${"ACTION".padEnd(24)} ${"STATUS".padEnd(10)} ${"STARTED"}`
|
|
94
|
+
);
|
|
95
|
+
console.log(" " + "-".repeat(90));
|
|
96
|
+
for (const r of filterRes.items) {
|
|
97
|
+
const time = r.startedAt.replace("T", " ").slice(0, 19);
|
|
98
|
+
console.log(
|
|
99
|
+
` ${r.id.padEnd(38)} ${r.actionId.padEnd(24)} ${r.status.padEnd(10)} ${time}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Outside project: List recent runs across all linked packages
|
|
107
|
+
const linkedList = listLinkedPackages();
|
|
108
|
+
if (linkedList.length === 0) {
|
|
109
|
+
console.log("No ActionDock project in current directory, and no packages linked.");
|
|
110
|
+
console.log("Run 'ac link' inside an Action package to register it.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let allRecords: any[] = [];
|
|
115
|
+
for (const pkg of linkedList) {
|
|
116
|
+
if (!existsSync(pkg.path)) continue;
|
|
117
|
+
try {
|
|
118
|
+
const projConfig = loadProjectConfig(pkg.path);
|
|
119
|
+
const storage = createStorage(projConfig.id, { projectRoot: pkg.path });
|
|
120
|
+
const records = storage.listRuns({
|
|
121
|
+
actionId: options.action,
|
|
122
|
+
limit,
|
|
123
|
+
});
|
|
124
|
+
storage.close();
|
|
125
|
+
for (const r of records) {
|
|
126
|
+
allRecords.push({ ...r, packageId: projConfig.id });
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// Ignore broken linked package
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Sort by startedAt desc
|
|
134
|
+
allRecords.sort((a, b) => (b.startedAt > a.startedAt ? 1 : b.startedAt < a.startedAt ? -1 : 0));
|
|
135
|
+
allRecords = allRecords.slice(0, limit);
|
|
136
|
+
|
|
137
|
+
const filterRes = filterWithFallbackInfo(
|
|
138
|
+
allRecords,
|
|
139
|
+
effectiveIntent,
|
|
140
|
+
[(r) => r.id, (r) => r.actionId, (r) => r.packageId, (r) => r.status, (r) => r.error?.message],
|
|
141
|
+
shouldFallback
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (options.json) {
|
|
145
|
+
console.log(JSON.stringify(filterRes.items, null, 2));
|
|
146
|
+
} else {
|
|
147
|
+
console.log(`Execution Runs across Linked Packages (${filterRes.items.length}):\n`);
|
|
148
|
+
if (filterRes.isFallback && effectiveIntent) {
|
|
149
|
+
console.log(`(No runs matched intent '${effectiveIntent}', showing all runs)\n`);
|
|
150
|
+
}
|
|
151
|
+
console.log(
|
|
152
|
+
` ${"RUN ID".padEnd(38)} ${"PACKAGE".padEnd(20)} ${"ACTION".padEnd(22)} ${"STATUS".padEnd(10)} ${"STARTED"}`
|
|
153
|
+
);
|
|
154
|
+
console.log(" " + "-".repeat(110));
|
|
155
|
+
for (const r of filterRes.items) {
|
|
156
|
+
const time = r.startedAt.replace("T", " ").slice(0, 19);
|
|
157
|
+
console.log(
|
|
158
|
+
` ${r.id.padEnd(38)} ${(r.packageId || "").padEnd(20)} ${r.actionId.padEnd(22)} ${r.status.padEnd(10)} ${time}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (err: any) {
|
|
163
|
+
console.error(`Error: ${err.message}`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// runs show
|
|
169
|
+
runsCmd
|
|
170
|
+
.command("show <id>")
|
|
171
|
+
.description("Show details of a specific execution run (searches current project or linked packages)")
|
|
172
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
173
|
+
.option("-p, --profile <name>", "Query run against a specific profile")
|
|
174
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
175
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
176
|
+
.option("--json", "Output as JSON")
|
|
177
|
+
.action(async (id, options) => {
|
|
178
|
+
let target;
|
|
179
|
+
try {
|
|
180
|
+
target = resolveTarget({
|
|
181
|
+
profile: options.profile,
|
|
182
|
+
server: options.server,
|
|
183
|
+
token: options.token,
|
|
184
|
+
});
|
|
185
|
+
} catch (err: any) {
|
|
186
|
+
console.error(`Error: ${err.message}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (target.type === "remote") {
|
|
191
|
+
try {
|
|
192
|
+
const run = await fetchRemoteRun(target.serverUrl!, id, target.token);
|
|
193
|
+
if (options.json) {
|
|
194
|
+
console.log(JSON.stringify(run, null, 2));
|
|
195
|
+
} else {
|
|
196
|
+
printRunRecord(run);
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
} catch (err: any) {
|
|
200
|
+
console.error(`Error: ${err.message}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Local show
|
|
206
|
+
try {
|
|
207
|
+
let foundRun: any = null;
|
|
208
|
+
|
|
209
|
+
if (options.package) {
|
|
210
|
+
const targetRoot = resolvePackageRoot(options.package);
|
|
211
|
+
if (!targetRoot) {
|
|
212
|
+
console.error(`Error: Package '${options.package}' not found in linked packages or path`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
const projConfig = loadProjectConfig(targetRoot);
|
|
216
|
+
const storage = createStorage(projConfig.id, { projectRoot: targetRoot });
|
|
217
|
+
foundRun = storage.getRun(id);
|
|
218
|
+
storage.close();
|
|
219
|
+
if (foundRun && !foundRun.packageId) {
|
|
220
|
+
foundRun.packageId = projConfig.id;
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
// 1. Try current project root if exists
|
|
224
|
+
const currentRoot = findProjectRoot();
|
|
225
|
+
if (currentRoot) {
|
|
226
|
+
try {
|
|
227
|
+
const projConfig = loadProjectConfig(currentRoot);
|
|
228
|
+
const storage = createStorage(projConfig.id, { projectRoot: currentRoot });
|
|
229
|
+
foundRun = storage.getRun(id);
|
|
230
|
+
storage.close();
|
|
231
|
+
if (foundRun && !foundRun.packageId) {
|
|
232
|
+
foundRun.packageId = projConfig.id;
|
|
233
|
+
}
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 2. If not found in current project, search across all linked packages
|
|
238
|
+
if (!foundRun) {
|
|
239
|
+
const linkedList = listLinkedPackages();
|
|
240
|
+
for (const pkg of linkedList) {
|
|
241
|
+
if (!existsSync(pkg.path)) continue;
|
|
242
|
+
try {
|
|
243
|
+
const projConfig = loadProjectConfig(pkg.path);
|
|
244
|
+
const storage = createStorage(projConfig.id, { projectRoot: pkg.path });
|
|
245
|
+
const r = storage.getRun(id);
|
|
246
|
+
storage.close();
|
|
247
|
+
if (r) {
|
|
248
|
+
foundRun = { ...r, packageId: projConfig.id };
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!foundRun) {
|
|
257
|
+
console.error(`Error: Run record '${id}' not found in current project or any linked packages`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (options.json) {
|
|
262
|
+
console.log(JSON.stringify(foundRun, null, 2));
|
|
263
|
+
} else {
|
|
264
|
+
printRunRecord(foundRun);
|
|
265
|
+
}
|
|
266
|
+
} catch (err: any) {
|
|
267
|
+
console.error(`Error: ${err.message}`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// runs cancel
|
|
273
|
+
runsCmd
|
|
274
|
+
.command("cancel <id>")
|
|
275
|
+
.description("Cancel a running action execution on a remote server")
|
|
276
|
+
.option("-p, --profile <name>", "Execute cancel against a specific profile")
|
|
277
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
278
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
279
|
+
.option("-r, --reason <reason>", "Reason for cancellation")
|
|
280
|
+
.option("--json", "Output as JSON")
|
|
281
|
+
.action(async (id, options) => {
|
|
282
|
+
let target;
|
|
283
|
+
try {
|
|
284
|
+
target = resolveTarget({
|
|
285
|
+
profile: options.profile,
|
|
286
|
+
server: options.server,
|
|
287
|
+
token: options.token,
|
|
288
|
+
});
|
|
289
|
+
} catch (err: any) {
|
|
290
|
+
console.error(`Error: ${err.message}`);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (target.type === "local") {
|
|
295
|
+
console.error(
|
|
296
|
+
"Error: 'ac runs cancel' is only supported for remote execution targets. Use --profile <name> or --server <url>."
|
|
297
|
+
);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const result = await cancelRemoteRun(
|
|
303
|
+
target.serverUrl!,
|
|
304
|
+
id,
|
|
305
|
+
target.token,
|
|
306
|
+
options.reason
|
|
307
|
+
);
|
|
308
|
+
if (options.json) {
|
|
309
|
+
console.log(JSON.stringify(result, null, 2));
|
|
310
|
+
} else {
|
|
311
|
+
console.log(`Run '${id}' cancellation requested (Status: ${result.status}).`);
|
|
312
|
+
}
|
|
313
|
+
} catch (err: any) {
|
|
314
|
+
console.error(`Error: ${err.message}`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { findProjectRoot, loadProjectConfig, startActionDockServer } from "@actiondock/core";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { parseByteSize } from "../utils/bytes";
|
|
5
|
+
|
|
6
|
+
export function registerServeCommand(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command("serve")
|
|
9
|
+
.description("Start the ActionDock lightweight HTTP Runner server for remote execution")
|
|
10
|
+
.option("-p, --port <port>", "Port to listen on (default: 5177)", "5177")
|
|
11
|
+
.option("-H, --host <host>", "Host address to bind to (default: 127.0.0.1)", "127.0.0.1")
|
|
12
|
+
.option("-t, --token <token>", "Authentication token for securing the endpoint (or set ACTIONDOCK_TOKEN)")
|
|
13
|
+
.option("--allow-insecure-no-auth", "Allow non-loopback host binding without authentication token (INSECURE)")
|
|
14
|
+
.option(
|
|
15
|
+
"--cors-origin <origin>",
|
|
16
|
+
"Allowed CORS origin (can be specified multiple times)",
|
|
17
|
+
(val: string, prev: string[] = []) => [...prev, val],
|
|
18
|
+
[]
|
|
19
|
+
)
|
|
20
|
+
.option("--max-body <size>", "Maximum allowed JSON request body size (e.g. 1mb, 500kb)", "1mb")
|
|
21
|
+
.option("--expose-debug-info", "Expose project root path in health and info responses")
|
|
22
|
+
.option("-d, --dir <path>", "Project root directory (default: current working directory)")
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
const port = parseInt(options.port, 10) || 5177;
|
|
25
|
+
const host = options.host || "127.0.0.1";
|
|
26
|
+
const token = options.token || process.env.ACTIONDOCK_TOKEN;
|
|
27
|
+
const allowInsecureNoAuth = Boolean(options.allowInsecureNoAuth);
|
|
28
|
+
const corsOrigins = options.corsOrigin && options.corsOrigin.length > 0 ? options.corsOrigin : undefined;
|
|
29
|
+
const exposeDebugInfo = Boolean(options.exposeDebugInfo);
|
|
30
|
+
|
|
31
|
+
let maxBodyBytes: number | undefined;
|
|
32
|
+
if (options.maxBody) {
|
|
33
|
+
try {
|
|
34
|
+
maxBodyBytes = parseByteSize(options.maxBody);
|
|
35
|
+
} catch (err: any) {
|
|
36
|
+
console.error(`Error: ${err.message}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const projectRoot = options.dir
|
|
42
|
+
? resolve(options.dir)
|
|
43
|
+
: findProjectRoot(process.cwd());
|
|
44
|
+
|
|
45
|
+
let projectName = "Global Registry Mode";
|
|
46
|
+
let packageId = "none";
|
|
47
|
+
|
|
48
|
+
if (projectRoot) {
|
|
49
|
+
try {
|
|
50
|
+
const config = loadProjectConfig(projectRoot);
|
|
51
|
+
projectName = `${config.name} (${config.id})`;
|
|
52
|
+
packageId = config.id;
|
|
53
|
+
} catch {
|
|
54
|
+
// fallback
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const server = startActionDockServer({
|
|
60
|
+
port,
|
|
61
|
+
host,
|
|
62
|
+
token,
|
|
63
|
+
allowInsecureNoAuth,
|
|
64
|
+
corsOrigins,
|
|
65
|
+
maxBodyBytes,
|
|
66
|
+
exposeDebugInfo,
|
|
67
|
+
projectRoot: projectRoot || undefined,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(`\n======================================================`);
|
|
71
|
+
console.log(` ActionDock 2.0 HTTP Runner Server`);
|
|
72
|
+
console.log(`======================================================`);
|
|
73
|
+
console.log(` * Listening on: http://${host}:${server.port}`);
|
|
74
|
+
console.log(` * Project: ${projectName}`);
|
|
75
|
+
if (projectRoot && exposeDebugInfo) {
|
|
76
|
+
console.log(` * Root Path: ${projectRoot}`);
|
|
77
|
+
}
|
|
78
|
+
console.log(` * Authentication: ${token ? "Bearer Token / Query Token Enabled" : "Disabled (Public/Local)"}`);
|
|
79
|
+
console.log(` * CORS Origins: ${corsOrigins ? corsOrigins.join(", ") : "Disabled (Default)"}`);
|
|
80
|
+
console.log(` * Max Body Size: ${options.maxBody || "1mb"}`);
|
|
81
|
+
console.log(` * Health Endpoint: http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${server.port}/api/v1/health`);
|
|
82
|
+
console.log(`======================================================\n`);
|
|
83
|
+
console.log(`Server is ready to accept remote 'ac run' requests.`);
|
|
84
|
+
console.log(`Press Ctrl+C to terminate.\n`);
|
|
85
|
+
|
|
86
|
+
// Keep process running
|
|
87
|
+
process.on("SIGINT", () => {
|
|
88
|
+
console.log("\nStopping ActionDock server...");
|
|
89
|
+
server.stop();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
});
|
|
92
|
+
process.on("SIGTERM", () => {
|
|
93
|
+
server.stop();
|
|
94
|
+
process.exit(0);
|
|
95
|
+
});
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
console.error(`Failed to start ActionDock server: ${err.message}`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|