@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.
@@ -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
+ }