@actiondock/cli 2.0.8 → 2.0.10

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.
@@ -13,6 +13,13 @@ import {
13
13
  useProfile,
14
14
  } from "@actiondock/core";
15
15
  import { Command } from "commander";
16
+ import {
17
+ ArgumentError,
18
+ CliError,
19
+ ExecutionError,
20
+ getEffectiveOptions,
21
+ renderResult,
22
+ } from "@actiondock/runtime-cli";
16
23
  import { resolveIntent } from "../utils/filter";
17
24
 
18
25
  export function registerProfileCommands(program: Command): void {
@@ -26,12 +33,17 @@ export function registerProfileCommands(program: Command): void {
26
33
  .description("List all configured profiles")
27
34
  .option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
28
35
  .option("--reveal, --show-secrets", "Reveal plain text values for tokens")
36
+ .option("--fallback", "Enable fallback to full list when no items match intent")
29
37
  .option("--no-fallback", "Disable fallback to full list when no items match intent")
30
38
  .option("--json", "Output as JSON")
31
- .action((patterns, options) => {
39
+ .option("--envelope", "Wrap JSON output in standard envelope")
40
+ .action((patterns, rawOptions, cmd) => {
32
41
  try {
42
+ const options = getEffectiveOptions(rawOptions, cmd);
33
43
  const effectiveIntent = resolveIntent(options.intent, patterns);
34
- const shouldFallback = options.fallback !== false;
44
+ const isMachine = Boolean(options.json || options.envelope);
45
+ const fallbackExplicit = options.fallback === true || (Array.isArray(process.argv) && process.argv.includes("--fallback"));
46
+ const shouldFallback = isMachine ? fallbackExplicit : options.fallback !== false;
35
47
  const reveal = Boolean(options.reveal || options.showSecrets);
36
48
 
37
49
  const list = listProfiles();
@@ -56,42 +68,53 @@ export function registerProfileCommands(program: Command): void {
56
68
  shouldFallback
57
69
  );
58
70
 
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
- }
71
+ if (filterRes.isFallback && isMachine) {
72
+ renderResult(
73
+ { items: filterRes.items, isFallback: true, matchedCount: 0 },
74
+ { json: options.json, envelope: options.envelope }
75
+ );
76
+ return;
77
+ }
78
78
 
79
- if (reveal && item.token) {
80
- tokenInfo += ` = ${item.token}`;
79
+ renderResult(filterRes.items, {
80
+ json: options.json,
81
+ envelope: options.envelope,
82
+ humanFormatter: () => {
83
+ const lines = ["ActionDock Execution Profiles:\n"];
84
+ if (filterRes.isFallback && effectiveIntent) {
85
+ lines.push(`(No profiles matched intent '${effectiveIntent}', showing all profiles)\n`);
81
86
  }
87
+ for (const item of filterRes.items) {
88
+ const currentMarker = item.isCurrent ? "* " : " ";
89
+ let tokenInfo = "";
90
+ if (item.tokenSource === "tokenEnv") {
91
+ tokenInfo = ` [token: env(${item.tokenEnv})]`;
92
+ } else if (item.tokenSource === "profileEnv") {
93
+ tokenInfo = ` [token: env(ACTIONDOCK_${toSnakeUpperCase(item.name)}_TOKEN)]`;
94
+ } else if (item.tokenSource === "profile") {
95
+ tokenInfo = ` [token: stored in profile]`;
96
+ } else if (item.tokenSource === "globalEnv") {
97
+ tokenInfo = ` [token: env(ACTIONDOCK_TOKEN)]`;
98
+ }
99
+
100
+ if (reveal && item.token) {
101
+ tokenInfo += ` = ${item.token}`;
102
+ }
82
103
 
83
- const desc = item.description ? ` - ${item.description}` : "";
84
- console.log(
85
- `${currentMarker}${item.name.padEnd(20)} ${item.serverUrl}${tokenInfo}${desc}`
104
+ const desc = item.description ? ` - ${item.description}` : "";
105
+ lines.push(
106
+ `${currentMarker}${item.name.padEnd(20)} ${item.serverUrl}${tokenInfo}${desc}`
107
+ );
108
+ }
109
+ lines.push(
110
+ "\nUse 'ad profile use <name>' to switch or 'ad run <action> --profile <name>' to execute on a specific target."
86
111
  );
87
- }
88
- console.log(
89
- "\nUse 'ad profile use <name>' to switch or 'ad run <action> --profile <name>' to execute on a specific target."
90
- );
91
- }
112
+ return lines.join("\n");
113
+ },
114
+ });
92
115
  } catch (err: any) {
93
- console.error(`Error: ${err.message}`);
94
- process.exit(1);
116
+ if (err instanceof ArgumentError || err instanceof CliError) throw err;
117
+ throw new ExecutionError(err.message);
95
118
  }
96
119
  });
97
120
 
@@ -118,8 +141,7 @@ export function registerProfileCommands(program: Command): void {
118
141
  });
119
142
  console.log(`[OK] Profile '${name}' configured for server: ${options.server}`);
120
143
  } catch (err: any) {
121
- console.error(`Error: ${err.message}`);
122
- process.exit(1);
144
+ throw new ExecutionError(err.message);
123
145
  }
124
146
  });
125
147
 
@@ -132,8 +154,7 @@ export function registerProfileCommands(program: Command): void {
132
154
  useProfile(name);
133
155
  console.log(`[OK] Active profile switched to '${name}'`);
134
156
  } catch (err: any) {
135
- console.error(`Error: ${err.message}`);
136
- process.exit(1);
157
+ throw new ExecutionError(err.message);
137
158
  }
138
159
  });
139
160
 
@@ -143,16 +164,17 @@ export function registerProfileCommands(program: Command): void {
143
164
  .description("Display details of a profile (defaults to active profile)")
144
165
  .option("--reveal, --show-secrets", "Reveal plain text values for tokens")
145
166
  .option("--json", "Output as JSON")
146
- .action((name, options) => {
167
+ .option("--envelope", "Wrap JSON output in standard envelope")
168
+ .action((name, rawOptions, cmd) => {
147
169
  try {
170
+ const options = getEffectiveOptions(rawOptions, cmd);
148
171
  const config = loadProfiles();
149
172
  const targetName = name || config.currentProfile || "local";
150
173
  const entry = getProfile(targetName);
151
174
  const reveal = Boolean(options.reveal || options.showSecrets);
152
175
 
153
176
  if (!entry && targetName !== "local") {
154
- console.error(`Error: Profile '${targetName}' not found.`);
155
- process.exit(1);
177
+ throw new ArgumentError(`Profile '${targetName}' not found.`);
156
178
  }
157
179
 
158
180
  const resolved = resolveProfileToken(targetName, entry);
@@ -171,32 +193,38 @@ export function registerProfileCommands(program: Command): void {
171
193
  description: entry?.description || "",
172
194
  };
173
195
 
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
- }
196
+ renderResult(data, {
197
+ json: options.json,
198
+ envelope: options.envelope,
199
+ humanFormatter: () => {
200
+ const lines: string[] = [];
201
+ lines.push(`Profile: ${data.name}${data.isCurrent ? " (Active)" : ""}`);
202
+ lines.push(`Server URL: ${data.serverUrl}`);
203
+ let sourceDetail = "None";
204
+ if (data.tokenSource === "tokenEnv") {
205
+ sourceDetail = `Environment Variable ($${data.tokenEnv})`;
206
+ } else if (data.tokenSource === "profileEnv") {
207
+ sourceDetail = `Profile Environment Variable ($ACTIONDOCK_${toSnakeUpperCase(data.name)}_TOKEN)`;
208
+ } else if (data.tokenSource === "profile") {
209
+ sourceDetail = "Stored in profiles.json (Deprecated)";
210
+ } else if (data.tokenSource === "globalEnv") {
211
+ sourceDetail = "Global Environment Variable ($ACTIONDOCK_TOKEN)";
212
+ }
213
+ lines.push(`Auth Source: ${sourceDetail}`);
214
+ if (data.tokenConfigured) {
215
+ lines.push(`Token Value: ${data.token}`);
216
+ }
217
+ if (data.description) {
218
+ lines.push(`Description: ${data.description}`);
219
+ }
220
+ return lines.join("\n");
221
+ },
222
+ });
197
223
  } catch (err: any) {
198
- console.error(`Error: ${err.message}`);
199
- process.exit(1);
224
+ if (err instanceof ArgumentError || err instanceof CliError) {
225
+ throw err;
226
+ }
227
+ throw new ExecutionError(err.message);
200
228
  }
201
229
  });
202
230
 
@@ -208,18 +236,18 @@ export function registerProfileCommands(program: Command): void {
208
236
  .action((name) => {
209
237
  try {
210
238
  if (name === "local") {
211
- console.error("Error: Cannot remove built-in 'local' profile");
212
- process.exit(1);
239
+ throw new ExecutionError("Cannot remove built-in 'local' profile");
213
240
  }
214
241
  const removed = removeProfile(name);
215
242
  if (!removed) {
216
- console.error(`Error: Profile '${name}' not found`);
217
- process.exit(1);
243
+ throw new ExecutionError(`Profile '${name}' not found`);
218
244
  }
219
245
  console.log(`[OK] Profile '${name}' removed`);
220
246
  } catch (err: any) {
221
- console.error(`Error: ${err.message}`);
222
- process.exit(1);
247
+ if (err instanceof CliError) {
248
+ throw err;
249
+ }
250
+ throw new ExecutionError(err.message);
223
251
  }
224
252
  });
225
253
 
@@ -228,36 +256,41 @@ export function registerProfileCommands(program: Command): void {
228
256
  .command("test [name]")
229
257
  .description("Test connection latency and health of a profile")
230
258
  .option("--json", "Output as JSON")
231
- .action(async (name, options) => {
259
+ .option("--envelope", "Wrap JSON output in standard envelope")
260
+ .action(async (name, rawOptions, cmd) => {
232
261
  try {
262
+ const options = getEffectiveOptions(rawOptions, cmd);
233
263
  const target = resolveTarget({ profile: name });
234
264
  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 runtime).`);
239
- }
265
+ renderResult(
266
+ { ok: true, type: "local", message: "Local execution" },
267
+ {
268
+ json: options.json,
269
+ envelope: options.envelope,
270
+ humanFormatter: () =>
271
+ `Target profile '${target.profileName || "local"}' is local (runs in local runtime).`,
272
+ }
273
+ );
240
274
  return;
241
275
  }
242
276
 
243
277
  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
- }
278
+ renderResult(health, {
279
+ json: options.json,
280
+ envelope: options.envelope,
281
+ humanFormatter: () => {
282
+ if (health.ok) {
283
+ return `[OK] Connected to ${target.serverUrl} (${health.latencyMs}ms) - Version: ${health.version}, Status: ${health.status}`;
284
+ }
285
+ return `[FAIL] Connection to ${target.serverUrl} failed (${health.latencyMs}ms): ${health.error}`;
286
+ },
287
+ });
288
+ if (!health.ok) {
289
+ process.exitCode = 1;
257
290
  }
258
291
  } catch (err: any) {
259
- console.error(`Error: ${err.message}`);
260
- process.exit(1);
292
+ if (err instanceof ArgumentError || err instanceof CliError) throw err;
293
+ throw new ExecutionError(err.message);
261
294
  }
262
295
  });
263
296
  }
@@ -13,6 +13,7 @@ import {
13
13
  resolveTarget,
14
14
  } from "@actiondock/core";
15
15
  import { Command } from "commander";
16
+ import { ArgumentError, CliError, ExecutionError, getEffectiveOptions, renderResult } from "@actiondock/runtime-cli";
16
17
  import { resolveIntent } from "../utils/filter";
17
18
 
18
19
  function printRunRecord(run: any): void {
@@ -55,12 +56,16 @@ export function registerRunsCommands(program: Command): void {
55
56
  .option("-s, --server <url>", "Remote server URL")
56
57
  .option("-t, --token <token>", "Auth token for remote server")
57
58
  .option("--no-fallback", "Disable fallback to full list when no items match intent")
59
+ .option("--data-dir <path>", "Custom database storage directory")
58
60
  .option("--json", "Output as JSON")
59
- .action(async (patterns, options) => {
61
+ .option("--envelope", "Wrap JSON output in standard envelope")
62
+ .action(async (patterns, rawOptions, cmd) => {
60
63
  try {
64
+ const options = getEffectiveOptions(rawOptions, cmd);
61
65
  const effectiveIntent = resolveIntent(options.intent, patterns);
62
66
  const shouldFallback = options.fallback !== false;
63
67
  const limit = Number.parseInt(options.limit, 10) || 20;
68
+ const isMachine = Boolean(options.json || options.envelope);
64
69
 
65
70
  const target = resolveTarget({
66
71
  profile: options.profile,
@@ -75,8 +80,8 @@ export function registerRunsCommands(program: Command): void {
75
80
  intent: effectiveIntent,
76
81
  limit,
77
82
  });
78
- if (options.json) {
79
- console.log(JSON.stringify(res.items, null, 2));
83
+ if (isMachine) {
84
+ renderResult(res.items, { json: options.json, envelope: options.envelope });
80
85
  return;
81
86
  }
82
87
  console.log(
@@ -100,13 +105,15 @@ export function registerRunsCommands(program: Command): void {
100
105
  : findProjectRoot();
101
106
 
102
107
  if (options.package && !targetRoot) {
103
- console.error(`Error: Package '${options.package}' not found in linked packages or path`);
104
- process.exit(1);
108
+ throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
105
109
  }
106
110
 
107
111
  if (targetRoot) {
108
112
  const projConfig = loadProjectConfig(targetRoot);
109
- const storage = createStorage(projConfig.id, { projectRoot: targetRoot });
113
+ const storage = createStorage(projConfig.id, {
114
+ projectRoot: targetRoot,
115
+ dataDir: options.dataDir,
116
+ });
110
117
  const records = storage.listRuns({
111
118
  actionId: options.action,
112
119
  limit,
@@ -120,8 +127,8 @@ export function registerRunsCommands(program: Command): void {
120
127
  shouldFallback
121
128
  );
122
129
 
123
- if (options.json) {
124
- console.log(JSON.stringify(filterRes.items, null, 2));
130
+ if (isMachine) {
131
+ renderResult(filterRes.items, { json: options.json, envelope: options.envelope });
125
132
  } else {
126
133
  console.log(`Execution Runs in ${projConfig.id} (${filterRes.items.length}):\n`);
127
134
  if (filterRes.isFallback && effectiveIntent) {
@@ -144,6 +151,10 @@ export function registerRunsCommands(program: Command): void {
144
151
  // Outside project: List recent runs across all linked packages
145
152
  const linkedList = listLinkedPackages();
146
153
  if (linkedList.length === 0) {
154
+ if (isMachine) {
155
+ renderResult([], { json: options.json, envelope: options.envelope });
156
+ return;
157
+ }
147
158
  console.log("No ActionDock project in current directory, and no packages linked.");
148
159
  console.log("Run 'ad link' inside an Action package to register it.");
149
160
  return;
@@ -154,7 +165,10 @@ export function registerRunsCommands(program: Command): void {
154
165
  if (!existsSync(pkg.path)) continue;
155
166
  try {
156
167
  const projConfig = loadProjectConfig(pkg.path);
157
- const storage = createStorage(projConfig.id, { projectRoot: pkg.path });
168
+ const storage = createStorage(projConfig.id, {
169
+ projectRoot: pkg.path,
170
+ dataDir: options.dataDir,
171
+ });
158
172
  const records = storage.listRuns({
159
173
  actionId: options.action,
160
174
  limit,
@@ -179,8 +193,8 @@ export function registerRunsCommands(program: Command): void {
179
193
  shouldFallback
180
194
  );
181
195
 
182
- if (options.json) {
183
- console.log(JSON.stringify(filterRes.items, null, 2));
196
+ if (isMachine) {
197
+ renderResult(filterRes.items, { json: options.json, envelope: options.envelope });
184
198
  } else {
185
199
  console.log(`Execution Runs across Linked Packages (${filterRes.items.length}):\n`);
186
200
  if (filterRes.isFallback && effectiveIntent) {
@@ -198,8 +212,10 @@ export function registerRunsCommands(program: Command): void {
198
212
  }
199
213
  }
200
214
  } catch (err: any) {
201
- console.error(`Error: ${err.message}`);
202
- process.exit(1);
215
+ if (err instanceof CliError) {
216
+ throw err;
217
+ }
218
+ throw new ExecutionError(err.message);
203
219
  }
204
220
  });
205
221
 
@@ -211,9 +227,13 @@ export function registerRunsCommands(program: Command): void {
211
227
  .option("-p, --profile <name>", "Query run against a specific profile")
212
228
  .option("-s, --server <url>", "Remote server URL")
213
229
  .option("-t, --token <token>", "Auth token for remote server")
230
+ .option("--data-dir <path>", "Custom database storage directory")
214
231
  .option("--json", "Output as JSON")
215
- .action(async (id, options) => {
232
+ .option("--envelope", "Wrap JSON output in standard envelope")
233
+ .action(async (id, rawOptions, cmd) => {
216
234
  let target;
235
+ const options = getEffectiveOptions(rawOptions, cmd);
236
+ const isMachine = Boolean(options.json || options.envelope);
217
237
  try {
218
238
  target = resolveTarget({
219
239
  profile: options.profile,
@@ -221,22 +241,20 @@ export function registerRunsCommands(program: Command): void {
221
241
  token: options.token,
222
242
  });
223
243
  } catch (err: any) {
224
- console.error(`Error: ${err.message}`);
225
- process.exit(1);
244
+ throw new ExecutionError(err.message);
226
245
  }
227
246
 
228
247
  if (target.type === "remote") {
229
248
  try {
230
249
  const run = await fetchRemoteRun(target.serverUrl!, id, target.token);
231
- if (options.json) {
232
- console.log(JSON.stringify(run, null, 2));
250
+ if (isMachine) {
251
+ renderResult(run, { json: options.json, envelope: options.envelope });
233
252
  } else {
234
253
  printRunRecord(run);
235
254
  }
236
255
  return;
237
256
  } catch (err: any) {
238
- console.error(`Error: ${err.message}`);
239
- process.exit(1);
257
+ throw new ExecutionError(err.message);
240
258
  }
241
259
  }
242
260
 
@@ -247,11 +265,13 @@ export function registerRunsCommands(program: Command): void {
247
265
  if (options.package) {
248
266
  const targetRoot = resolvePackageRoot(options.package);
249
267
  if (!targetRoot) {
250
- console.error(`Error: Package '${options.package}' not found in linked packages or path`);
251
- process.exit(1);
268
+ throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
252
269
  }
253
270
  const projConfig = loadProjectConfig(targetRoot);
254
- const storage = createStorage(projConfig.id, { projectRoot: targetRoot });
271
+ const storage = createStorage(projConfig.id, {
272
+ projectRoot: targetRoot,
273
+ dataDir: options.dataDir,
274
+ });
255
275
  foundRun = storage.getRun(id);
256
276
  storage.close();
257
277
  if (foundRun && !foundRun.packageId) {
@@ -263,7 +283,10 @@ export function registerRunsCommands(program: Command): void {
263
283
  if (currentRoot) {
264
284
  try {
265
285
  const projConfig = loadProjectConfig(currentRoot);
266
- const storage = createStorage(projConfig.id, { projectRoot: currentRoot });
286
+ const storage = createStorage(projConfig.id, {
287
+ projectRoot: currentRoot,
288
+ dataDir: options.dataDir,
289
+ });
267
290
  foundRun = storage.getRun(id);
268
291
  storage.close();
269
292
  if (foundRun && !foundRun.packageId) {
@@ -279,7 +302,10 @@ export function registerRunsCommands(program: Command): void {
279
302
  if (!existsSync(pkg.path)) continue;
280
303
  try {
281
304
  const projConfig = loadProjectConfig(pkg.path);
282
- const storage = createStorage(projConfig.id, { projectRoot: pkg.path });
305
+ const storage = createStorage(projConfig.id, {
306
+ projectRoot: pkg.path,
307
+ dataDir: options.dataDir,
308
+ });
283
309
  const r = storage.getRun(id);
284
310
  storage.close();
285
311
  if (r) {
@@ -292,18 +318,19 @@ export function registerRunsCommands(program: Command): void {
292
318
  }
293
319
 
294
320
  if (!foundRun) {
295
- console.error(`Error: Run record '${id}' not found in current project or any linked packages`);
296
- process.exit(1);
321
+ throw new ExecutionError(`Run record '${id}' not found in current project or any linked packages`);
297
322
  }
298
323
 
299
- if (options.json) {
300
- console.log(JSON.stringify(foundRun, null, 2));
324
+ if (isMachine) {
325
+ renderResult(foundRun, { json: options.json, envelope: options.envelope });
301
326
  } else {
302
327
  printRunRecord(foundRun);
303
328
  }
304
329
  } catch (err: any) {
305
- console.error(`Error: ${err.message}`);
306
- process.exit(1);
330
+ if (err instanceof CliError) {
331
+ throw err;
332
+ }
333
+ throw new ExecutionError(err.message);
307
334
  }
308
335
  });
309
336
 
@@ -316,8 +343,11 @@ export function registerRunsCommands(program: Command): void {
316
343
  .option("-t, --token <token>", "Auth token for remote server")
317
344
  .option("-r, --reason <reason>", "Reason for cancellation")
318
345
  .option("--json", "Output as JSON")
319
- .action(async (id, options) => {
346
+ .option("--envelope", "Wrap JSON output in standard envelope")
347
+ .action(async (id, rawOptions, cmd) => {
320
348
  let target;
349
+ const options = getEffectiveOptions(rawOptions, cmd);
350
+ const isMachine = Boolean(options.json || options.envelope);
321
351
  try {
322
352
  target = resolveTarget({
323
353
  profile: options.profile,
@@ -325,15 +355,13 @@ export function registerRunsCommands(program: Command): void {
325
355
  token: options.token,
326
356
  });
327
357
  } catch (err: any) {
328
- console.error(`Error: ${err.message}`);
329
- process.exit(1);
358
+ throw new ExecutionError(err.message);
330
359
  }
331
360
 
332
361
  if (target.type === "local") {
333
- console.error(
334
- "Error: 'ad runs cancel' is only supported for remote execution targets. Use --profile <name> or --server <url>."
362
+ throw new ArgumentError(
363
+ "'ad runs cancel' is only supported for remote execution targets. Use --profile <name> or --server <url>."
335
364
  );
336
- process.exit(1);
337
365
  }
338
366
 
339
367
  try {
@@ -343,14 +371,13 @@ export function registerRunsCommands(program: Command): void {
343
371
  target.token,
344
372
  options.reason
345
373
  );
346
- if (options.json) {
347
- console.log(JSON.stringify(result, null, 2));
374
+ if (isMachine) {
375
+ renderResult(result, { json: options.json, envelope: options.envelope });
348
376
  } else {
349
377
  console.log(`Run '${id}' cancellation requested (Status: ${result.status}).`);
350
378
  }
351
379
  } catch (err: any) {
352
- console.error(`Error: ${err.message}`);
353
- process.exit(1);
380
+ throw new ExecutionError(err.message);
354
381
  }
355
382
  });
356
383
 
@@ -363,9 +390,13 @@ export function registerRunsCommands(program: Command): void {
363
390
  .option("-p, --profile <name>", "Target profile")
364
391
  .option("-s, --server <url>", "Remote server URL")
365
392
  .option("-t, --token <token>", "Auth token for remote server")
393
+ .option("--data-dir <path>", "Custom database storage directory")
366
394
  .option("--json", "Output as JSON")
367
- .action(async (options) => {
395
+ .option("--envelope", "Wrap JSON output in standard envelope")
396
+ .action(async (rawOptions, cmd) => {
368
397
  try {
398
+ const options = getEffectiveOptions(rawOptions, cmd);
399
+ const isMachine = Boolean(options.json || options.envelope);
369
400
  const target = resolveTarget({
370
401
  profile: options.profile,
371
402
  server: options.server,
@@ -377,8 +408,8 @@ export function registerRunsCommands(program: Command): void {
377
408
  packageId: options.package,
378
409
  actionId: options.action,
379
410
  });
380
- if (options.json) {
381
- console.log(JSON.stringify(res, null, 2));
411
+ if (isMachine) {
412
+ renderResult(res, { json: options.json, envelope: options.envelope });
382
413
  } else {
383
414
  console.log(`Cleared ${res.clearedCount} execution run(s) on remote server.`);
384
415
  }
@@ -389,20 +420,32 @@ export function registerRunsCommands(program: Command): void {
389
420
  ? resolvePackageRoot(options.package)
390
421
  : findProjectRoot();
391
422
 
423
+ if (options.package && !targetRoot) {
424
+ throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
425
+ }
426
+
392
427
  if (targetRoot) {
393
428
  const projConfig = loadProjectConfig(targetRoot);
394
- const storage = createStorage(projConfig.id, { projectRoot: targetRoot });
429
+ const storage = createStorage(projConfig.id, {
430
+ projectRoot: targetRoot,
431
+ dataDir: options.dataDir,
432
+ });
395
433
  const count = storage.clearRuns({ actionId: options.action });
396
434
  storage.close();
397
- if (options.json) {
398
- console.log(JSON.stringify({ ok: true, clearedCount: count }, null, 2));
435
+ if (isMachine) {
436
+ renderResult({ ok: true, clearedCount: count, packageId: projConfig.id }, { json: options.json, envelope: options.envelope });
399
437
  } else {
400
438
  console.log(`Cleared ${count} execution run(s) in package '${projConfig.id}'.`);
401
439
  }
440
+ return;
402
441
  }
442
+
443
+ throw new ArgumentError(
444
+ "Not in an ActionDock project. Please specify -P, --package <id> or cd into a project directory."
445
+ );
403
446
  } catch (err: any) {
404
- console.error(`Error: ${err.message}`);
405
- process.exit(1);
447
+ if (err instanceof CliError) throw err;
448
+ throw new ExecutionError(err.message);
406
449
  }
407
450
  });
408
451
  }