@actiondock/cli 2.0.9 → 2.0.11-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@ import { resolve } from "node:path";
2
2
  import { findProjectRoot, loadProjectConfig } from "@actiondock/core";
3
3
  import { startMcpHttpServer, startMcpStdio } from "@actiondock/mcp";
4
4
  import { Command } from "commander";
5
+ import { ExecutionError } from "@actiondock/runtime-cli";
5
6
  import { parseByteSize } from "../utils/bytes";
6
7
  import { parseDuration } from "../utils/duration";
7
8
 
@@ -35,8 +36,7 @@ export function registerMcpCommands(program: Command): void {
35
36
  try {
36
37
  timeoutMs = parseDuration(options.timeout);
37
38
  } catch (err: any) {
38
- process.stderr.write(`Error: ${err.message}\n`);
39
- process.exit(1);
39
+ throw new ExecutionError(err.message);
40
40
  }
41
41
  }
42
42
 
@@ -56,8 +56,7 @@ export function registerMcpCommands(program: Command): void {
56
56
  timeoutMs,
57
57
  });
58
58
  } catch (err: any) {
59
- process.stderr.write(`Failed to start MCP STDIO server: ${err.message}\n`);
60
- process.exit(1);
59
+ throw new ExecutionError(`Failed to start MCP STDIO server: ${err.message}`);
61
60
  }
62
61
  });
63
62
 
@@ -106,8 +105,7 @@ export function registerMcpCommands(program: Command): void {
106
105
  try {
107
106
  maxBodyBytes = parseByteSize(options.maxBody);
108
107
  } catch (err: any) {
109
- console.error(`Error: ${err.message}`);
110
- process.exit(1);
108
+ throw new ExecutionError(err.message);
111
109
  }
112
110
  }
113
111
 
@@ -116,8 +114,7 @@ export function registerMcpCommands(program: Command): void {
116
114
  try {
117
115
  timeoutMs = parseDuration(options.timeout);
118
116
  } catch (err: any) {
119
- console.error(`Error: ${err.message}`);
120
- process.exit(1);
117
+ throw new ExecutionError(err.message);
121
118
  }
122
119
  }
123
120
 
@@ -192,8 +189,7 @@ export function registerMcpCommands(program: Command): void {
192
189
  process.exit(0);
193
190
  });
194
191
  } catch (err: any) {
195
- console.error(`Failed to start ActionDock MCP HTTP server: ${err.message}`);
196
- process.exit(1);
192
+ throw new ExecutionError(`Failed to start ActionDock MCP HTTP server: ${err.message}`);
197
193
  }
198
194
  });
199
195
  }
@@ -11,11 +11,19 @@ import {
11
11
  loadPlaybooks,
12
12
  loadProjectConfig,
13
13
  resolveActionProject,
14
+ resolvePackageRoot,
14
15
  resolvePlaybookProject,
15
16
  resolveTarget,
16
17
  } from "@actiondock/core";
17
18
  import type { PlaybookDefinition } from "@actiondock/core";
18
19
  import { Command } from "commander";
20
+ import {
21
+ ArgumentError,
22
+ CliError,
23
+ ExecutionError,
24
+ getEffectiveOptions,
25
+ renderResult,
26
+ } from "@actiondock/runtime-cli";
19
27
  import { resolveIntent } from "../utils/filter";
20
28
 
21
29
  export function registerPlaybookCommands(program: Command): void {
@@ -32,12 +40,17 @@ export function registerPlaybookCommands(program: Command): void {
32
40
  .option("-p, --profile <name>", "Query against a specific profile")
33
41
  .option("-s, --server <url>", "Remote server URL")
34
42
  .option("-t, --token <token>", "Auth token for remote server")
43
+ .option("--fallback", "Enable fallback to full list when no items match intent")
35
44
  .option("--no-fallback", "Disable fallback to full list when no items match intent")
36
45
  .option("--json", "Output as JSON")
37
- .action(async (patterns, options) => {
46
+ .option("--envelope", "Wrap JSON output in standard envelope")
47
+ .action(async (patterns, rawOptions, cmd) => {
38
48
  try {
49
+ const options = getEffectiveOptions(rawOptions, cmd);
39
50
  const effectiveIntent = resolveIntent(options.intent, patterns);
40
- const shouldFallback = options.fallback !== false;
51
+ const isMachine = Boolean(options.json || options.envelope);
52
+ const fallbackExplicit = options.fallback === true || (Array.isArray(process.argv) && process.argv.includes("--fallback"));
53
+ const shouldFallback = isMachine ? fallbackExplicit : options.fallback !== false;
41
54
 
42
55
  const target = resolveTarget({
43
56
  profile: options.profile,
@@ -46,31 +59,65 @@ export function registerPlaybookCommands(program: Command): void {
46
59
  });
47
60
 
48
61
  if (target.type === "remote") {
49
- const remotePbs = await fetchRemotePlaybooks(target.serverUrl!, target.token, {
62
+ let remotePbs = await fetchRemotePlaybooks(target.serverUrl!, target.token, {
50
63
  intent: effectiveIntent,
51
64
  package: options.package,
52
65
  });
53
- if (options.json) {
54
- console.log(JSON.stringify(remotePbs, null, 2));
55
- return;
66
+
67
+ let isFallback = false;
68
+ if (remotePbs.length === 0 && effectiveIntent && shouldFallback) {
69
+ remotePbs = await fetchRemotePlaybooks(target.serverUrl!, target.token, {
70
+ package: options.package,
71
+ });
72
+ isFallback = true;
56
73
  }
57
- console.log(
58
- `Playbooks on remote server ${target.serverUrl}${target.profileName ? ` (Profile: ${target.profileName})` : ""}:\n`
59
- );
60
- if (remotePbs.length === 0) {
61
- console.log(" (No playbooks found)");
62
- } else {
63
- for (const p of remotePbs) {
64
- console.log(` ${p.id.padEnd(24)} ${p.description} (Package: ${p.packageId})`);
65
- }
74
+
75
+ if (isFallback && isMachine) {
76
+ renderResult(
77
+ { items: remotePbs, isFallback: true, matchedCount: 0 },
78
+ { json: options.json, envelope: options.envelope }
79
+ );
80
+ return;
66
81
  }
82
+
83
+ renderResult(remotePbs, {
84
+ json: options.json,
85
+ envelope: options.envelope,
86
+ humanFormatter: () => {
87
+ const lines = [
88
+ `Playbooks on remote server ${target.serverUrl}${target.profileName ? ` (Profile: ${target.profileName})` : ""}:\n`,
89
+ ];
90
+ if (isFallback && effectiveIntent) {
91
+ lines.push(`(No remote playbooks matched intent '${effectiveIntent}', showing all playbooks)\n`);
92
+ }
93
+ if (remotePbs.length === 0) {
94
+ lines.push(" (No playbooks found)");
95
+ } else {
96
+ for (const p of remotePbs) {
97
+ lines.push(` ${p.id.padEnd(24)} ${p.description} (Package: ${p.packageId})`);
98
+ }
99
+ }
100
+ return lines.join("\n");
101
+ },
102
+ });
67
103
  return;
68
104
  }
69
105
 
70
- const root = findProjectRoot();
71
- if (root) {
72
- const config = loadProjectConfig(root);
73
- const playbooks = loadPlaybooks(root, config.playbooksDir);
106
+ let targetRoot: string | null = null;
107
+ if (options.package) {
108
+ targetRoot = resolvePackageRoot(options.package);
109
+ if (!targetRoot) {
110
+ throw new ArgumentError(
111
+ `Package '${options.package}' not found in linked packages or path`
112
+ );
113
+ }
114
+ } else {
115
+ targetRoot = findProjectRoot();
116
+ }
117
+
118
+ if (targetRoot) {
119
+ const config = loadProjectConfig(targetRoot);
120
+ const playbooks = loadPlaybooks(targetRoot, config.playbooksDir);
74
121
  const rawList = Array.from(playbooks.values()).map((p) => ({
75
122
  id: p.id,
76
123
  description: p.description || "",
@@ -86,116 +133,144 @@ export function registerPlaybookCommands(program: Command): void {
86
133
  shouldFallback
87
134
  );
88
135
 
89
- if (options.json) {
90
- console.log(JSON.stringify(filterRes.items, null, 2));
91
- } else {
92
- console.log(`Playbooks in ${config.id} (${root}):\n`);
93
- if (filterRes.isFallback && effectiveIntent) {
94
- console.log(`(No playbooks matched intent '${effectiveIntent}', showing all playbooks)\n`);
95
- }
96
- for (const p of filterRes.items) {
97
- console.log(` ${p.id.padEnd(24)} ${p.description}`);
98
- }
99
- }
100
- } else {
101
- // List playbooks across all linked packages
102
- const linkedList = listLinkedPackages();
103
- if (linkedList.length === 0) {
104
- console.log("No ActionDock project in current directory, and no packages linked.");
105
- console.log("Run 'ad link' inside an Action package to register it.");
136
+ if (filterRes.isFallback && isMachine) {
137
+ renderResult(
138
+ { items: filterRes.items, isFallback: true, matchedCount: 0 },
139
+ { json: options.json, envelope: options.envelope }
140
+ );
106
141
  return;
107
142
  }
108
143
 
109
- const aggregated: Array<{
110
- packageId: string;
111
- packageName: string;
112
- path: string;
113
- playbooks: Array<{
114
- id: string;
115
- description: string;
116
- actions: string[];
117
- file: string;
118
- }>;
119
- }> = [];
144
+ renderResult(filterRes.items, {
145
+ json: options.json,
146
+ envelope: options.envelope,
147
+ humanFormatter: () => {
148
+ const lines = [`Playbooks in ${config.id} (${targetRoot}):\n`];
149
+ if (filterRes.isFallback && effectiveIntent) {
150
+ lines.push(`(No playbooks matched intent '${effectiveIntent}', showing all playbooks)\n`);
151
+ }
152
+ for (const p of filterRes.items) {
153
+ lines.push(` ${p.id.padEnd(24)} ${p.description}`);
154
+ }
155
+ return lines.join("\n");
156
+ },
157
+ });
158
+ return;
159
+ }
160
+ // List playbooks across all linked packages
161
+ const linkedList = listLinkedPackages();
162
+ if (linkedList.length === 0) {
163
+ renderResult([], {
164
+ json: options.json,
165
+ envelope: options.envelope,
166
+ humanFormatter: () =>
167
+ "No ActionDock project in current directory, and no packages linked.\nRun 'ad link' inside an Action package to register it.",
168
+ });
169
+ return;
170
+ }
120
171
 
121
- for (const pkg of linkedList) {
122
- if (!existsSync(pkg.path)) continue;
123
- try {
124
- const config = loadProjectConfig(pkg.path);
125
- const playbooks = loadPlaybooks(pkg.path, config.playbooksDir);
126
- const pkgPlaybooks = Array.from(playbooks.values()).map((p) => ({
127
- id: p.id,
128
- description: p.description || "",
129
- actions: p.actions || [],
130
- file: p.filePath,
131
- }));
132
-
133
- aggregated.push({
134
- packageId: pkg.id,
135
- packageName: pkg.name,
136
- path: pkg.path,
137
- playbooks: pkgPlaybooks,
138
- });
139
- } catch {
140
- // Ignore broken linked package
141
- }
172
+ const aggregated: Array<{
173
+ packageId: string;
174
+ packageName: string;
175
+ path: string;
176
+ playbooks: Array<{
177
+ id: string;
178
+ description: string;
179
+ actions: string[];
180
+ file: string;
181
+ }>;
182
+ }> = [];
183
+
184
+ for (const pkg of linkedList) {
185
+ if (!existsSync(pkg.path)) continue;
186
+ try {
187
+ const config = loadProjectConfig(pkg.path);
188
+ const playbooks = loadPlaybooks(pkg.path, config.playbooksDir);
189
+ const pkgPlaybooks = Array.from(playbooks.values()).map((p) => ({
190
+ id: p.id,
191
+ description: p.description || "",
192
+ actions: p.actions || [],
193
+ file: p.filePath,
194
+ }));
195
+
196
+ aggregated.push({
197
+ packageId: pkg.id,
198
+ packageName: pkg.name,
199
+ path: pkg.path,
200
+ playbooks: pkgPlaybooks,
201
+ });
202
+ } catch {
203
+ // Ignore broken linked package
142
204
  }
205
+ }
143
206
 
144
- let filteredPackages: typeof aggregated = [];
145
- if (!effectiveIntent) {
146
- filteredPackages = aggregated;
147
- } else {
148
- for (const pkg of aggregated) {
149
- const pkgMatches = filterWithFallbackInfo(
150
- [pkg],
207
+ let filteredPackages: typeof aggregated = [];
208
+ let isFallback = false;
209
+ if (!effectiveIntent) {
210
+ filteredPackages = aggregated;
211
+ } else {
212
+ for (const pkg of aggregated) {
213
+ const pkgMatches = filterWithFallbackInfo(
214
+ [pkg],
215
+ effectiveIntent,
216
+ [(p) => p.packageId, (p) => p.packageName, (p) => p.path],
217
+ false
218
+ ).matchedCount > 0;
219
+
220
+ if (pkgMatches) {
221
+ filteredPackages.push(pkg);
222
+ } else {
223
+ const matchedPlaybooks = filterWithFallbackInfo(
224
+ pkg.playbooks,
151
225
  effectiveIntent,
152
- [(p) => p.packageId, (p) => p.packageName, (p) => p.path],
226
+ [(p) => p.id, (p) => p.description, (p) => p.actions, (p) => p.file],
153
227
  false
154
- ).matchedCount > 0;
228
+ ).items;
155
229
 
156
- if (pkgMatches) {
157
- filteredPackages.push(pkg);
158
- } else {
159
- const matchedPlaybooks = filterWithFallbackInfo(
160
- pkg.playbooks,
161
- effectiveIntent,
162
- [(p) => p.id, (p) => p.description, (p) => p.actions, (p) => p.file],
163
- false
164
- ).items;
165
-
166
- if (matchedPlaybooks.length > 0) {
167
- filteredPackages.push({
168
- ...pkg,
169
- playbooks: matchedPlaybooks,
170
- });
171
- }
230
+ if (matchedPlaybooks.length > 0) {
231
+ filteredPackages.push({
232
+ ...pkg,
233
+ playbooks: matchedPlaybooks,
234
+ });
172
235
  }
173
236
  }
237
+ }
174
238
 
175
- if (filteredPackages.length === 0 && shouldFallback) {
176
- filteredPackages = aggregated;
177
- }
239
+ if (filteredPackages.length === 0 && shouldFallback) {
240
+ filteredPackages = aggregated;
241
+ isFallback = true;
178
242
  }
243
+ }
179
244
 
180
- if (options.json) {
181
- console.log(JSON.stringify(filteredPackages, null, 2));
182
- } else {
183
- console.log("Playbooks in Linked Packages:\n");
245
+ if (isFallback && isMachine) {
246
+ renderResult(
247
+ { packages: filteredPackages, isFallback: true, matchedCount: 0 },
248
+ { json: options.json, envelope: options.envelope }
249
+ );
250
+ return;
251
+ }
252
+
253
+ renderResult(filteredPackages, {
254
+ json: options.json,
255
+ envelope: options.envelope,
256
+ humanFormatter: () => {
257
+ const lines = ["Playbooks in Linked Packages:\n"];
184
258
  for (const pkg of filteredPackages) {
185
- console.log(`* Package: ${pkg.packageId} (${pkg.path})`);
259
+ lines.push(`* Package: ${pkg.packageId} (${pkg.path})`);
186
260
  if (pkg.playbooks.length === 0) {
187
- console.log(" (No playbooks)");
261
+ lines.push(" (No playbooks)");
188
262
  } else {
189
263
  for (const p of pkg.playbooks) {
190
- console.log(` - ${p.id.padEnd(26)} ${p.description}`);
264
+ lines.push(` - ${p.id.padEnd(26)} ${p.description}`);
191
265
  }
192
266
  }
193
267
  }
194
- }
195
- }
268
+ return lines.join("\n");
269
+ },
270
+ });
196
271
  } catch (err: any) {
197
- console.error(`Error: ${err.message}`);
198
- process.exit(1);
272
+ if (err instanceof ArgumentError || err instanceof CliError) throw err;
273
+ throw new ExecutionError(err.message);
199
274
  }
200
275
  });
201
276
 
@@ -210,8 +285,7 @@ export function registerPlaybookCommands(program: Command): void {
210
285
  .action((id, options) => {
211
286
  const root = findProjectRoot();
212
287
  if (!root) {
213
- console.error("Error: Not in an ActionDock project (actiondock.json not found)");
214
- process.exit(1);
288
+ throw new ExecutionError("Not in an ActionDock project (actiondock.json not found)");
215
289
  }
216
290
  try {
217
291
  const config = loadProjectConfig(root);
@@ -225,8 +299,7 @@ export function registerPlaybookCommands(program: Command): void {
225
299
  const targetFullFile = resolve(pbDir, targetRelFile);
226
300
 
227
301
  if (existsSync(targetFullFile)) {
228
- console.error(`Error: File '${targetFullFile}' already exists`);
229
- process.exit(1);
302
+ throw new ExecutionError(`File '${targetFullFile}' already exists`);
230
303
  }
231
304
 
232
305
  mkdirSync(dirname(targetFullFile), { recursive: true });
@@ -256,8 +329,10 @@ This playbook provides task execution guidance for AI Agents.
256
329
  writeFileSync(targetFullFile, template, "utf-8");
257
330
  console.log(`[OK] Created Playbook '${id}' at ${targetFullFile}`);
258
331
  } catch (err: any) {
259
- console.error(`Error: ${err.message}`);
260
- process.exit(1);
332
+ if (err instanceof CliError) {
333
+ throw err;
334
+ }
335
+ throw new ExecutionError(err.message);
261
336
  }
262
337
  });
263
338
 
@@ -265,12 +340,19 @@ This playbook provides task execution guidance for AI Agents.
265
340
  pbCmd
266
341
  .command("show <id>")
267
342
  .description("Show playbook content and metadata (from current project or linked packages)")
343
+ .option("-P, --package <id>", "Target package ID or path")
268
344
  .option("-p, --profile <name>", "Query against a specific profile")
269
345
  .option("-s, --server <url>", "Remote server URL")
270
346
  .option("-t, --token <token>", "Auth token for remote server")
271
347
  .option("--json", "Output as JSON")
272
- .action(async (id, options) => {
348
+ .option("--envelope", "Wrap JSON output in standard envelope")
349
+ .action(async (id, rawOptions, cmd) => {
273
350
  try {
351
+ const options = getEffectiveOptions(rawOptions, cmd);
352
+ if (!id) {
353
+ throw new ArgumentError("Playbook ID is required for show");
354
+ }
355
+
274
356
  const target = resolveTarget({
275
357
  profile: options.profile,
276
358
  server: options.server,
@@ -279,39 +361,64 @@ This playbook provides task execution guidance for AI Agents.
279
361
 
280
362
  if (target.type === "remote") {
281
363
  const pb = await fetchRemotePlaybookShow(target.serverUrl!, id, target.token);
282
- if (options.json) {
283
- console.log(JSON.stringify(pb, null, 2));
284
- return;
285
- }
286
- console.log(`Playbook: ${pb.id} (Package: ${pb.packageId})`);
287
- if (pb.description) console.log(`Description: ${pb.description}`);
288
- if (pb.actions && pb.actions.length > 0) {
289
- console.log(`Actions: ${pb.actions.join(", ")}`);
290
- }
291
- if (pb.filePath) console.log(`File: ${pb.filePath}\n`);
292
- console.log("--- Content ---");
293
- console.log(pb.content);
364
+ renderResult(pb, {
365
+ json: options.json,
366
+ envelope: options.envelope,
367
+ humanFormatter: () => {
368
+ const lines = [`Playbook: ${pb.id} (Package: ${pb.packageId})`];
369
+ if (pb.description) lines.push(`Description: ${pb.description}`);
370
+ if (pb.actions && pb.actions.length > 0) {
371
+ lines.push(`Actions: ${pb.actions.join(", ")}`);
372
+ }
373
+ if (pb.filePath) lines.push(`File: ${pb.filePath}\n`);
374
+ lines.push("--- Content ---", pb.content);
375
+ return lines.join("\n");
376
+ },
377
+ });
294
378
  return;
295
379
  }
296
380
 
297
- const resolved = resolvePlaybookProject(id);
298
- const pb = resolved.playbook;
381
+ let showTarget = id;
382
+ if (options.package && !id.includes("/") && !id.includes(":")) {
383
+ const pkgRoot = resolvePackageRoot(options.package);
384
+ if (!pkgRoot) {
385
+ throw new ArgumentError(
386
+ `Package '${options.package}' not found in linked packages or path`
387
+ );
388
+ }
389
+ showTarget = `${options.package}/${id}`;
390
+ }
299
391
 
300
- if (options.json) {
301
- console.log(JSON.stringify({ ...pb, packageId: resolved.packageId }, null, 2));
302
- } else {
303
- console.log(`Playbook: ${pb.id} (Package: ${resolved.packageId})`);
304
- if (pb.description) console.log(`Description: ${pb.description}`);
305
- if (pb.actions && pb.actions.length > 0) {
306
- console.log(`Actions: ${pb.actions.join(", ")}`);
392
+ let resolved;
393
+ try {
394
+ resolved = resolvePlaybookProject(showTarget);
395
+ } catch (err: any) {
396
+ if (err.message?.includes("not found") || err.message?.includes("no longer exists")) {
397
+ throw new ArgumentError(err.message);
307
398
  }
308
- console.log(`File: ${pb.filePath}\n`);
309
- console.log("--- Content ---");
310
- console.log(pb.content);
399
+ throw new ExecutionError(err.message);
311
400
  }
401
+
402
+ const pb = resolved.playbook;
403
+ const payload = { ...pb, packageId: resolved.packageId };
404
+
405
+ renderResult(payload, {
406
+ json: options.json,
407
+ envelope: options.envelope,
408
+ humanFormatter: () => {
409
+ const lines = [`Playbook: ${pb.id} (Package: ${resolved.packageId})`];
410
+ if (pb.description) lines.push(`Description: ${pb.description}`);
411
+ if (pb.actions && pb.actions.length > 0) {
412
+ lines.push(`Actions: ${pb.actions.join(", ")}`);
413
+ }
414
+ if (pb.filePath) lines.push(`File: ${pb.filePath}\n`);
415
+ lines.push("--- Content ---", pb.content);
416
+ return lines.join("\n");
417
+ },
418
+ });
312
419
  } catch (err: any) {
313
- console.error(`Error: ${err.message}`);
314
- process.exit(1);
420
+ if (err instanceof ArgumentError || err instanceof CliError) throw err;
421
+ throw new ExecutionError(err.message);
315
422
  }
316
423
  });
317
424
 
@@ -319,10 +426,24 @@ This playbook provides task execution guidance for AI Agents.
319
426
  pbCmd
320
427
  .command("validate [id]")
321
428
  .description("Validate playbook format and action references (in current project or linked packages)")
429
+ .option("-P, --package <id>", "Target package ID or path")
322
430
  .option("--json", "Output as JSON")
323
- .action(async (id, options) => {
431
+ .option("--envelope", "Wrap JSON output in standard envelope")
432
+ .action(async (id, rawOptions, cmd) => {
324
433
  try {
325
- const root = findProjectRoot();
434
+ const options = getEffectiveOptions(rawOptions, cmd);
435
+ let root: string | null = null;
436
+ if (options.package) {
437
+ root = resolvePackageRoot(options.package);
438
+ if (!root) {
439
+ throw new ArgumentError(
440
+ `Package '${options.package}' not found in linked packages or path`
441
+ );
442
+ }
443
+ } else {
444
+ root = findProjectRoot();
445
+ }
446
+
326
447
  const targets: Array<{ root: string; packageId: string; playbooks: PlaybookDefinition[] }> = [];
327
448
 
328
449
  if (root) {
@@ -331,8 +452,7 @@ This playbook provides task execution guidance for AI Agents.
331
452
  if (id) {
332
453
  const pb = playbooks.get(id);
333
454
  if (!pb) {
334
- console.error(`Error: Playbook '${id}' not found in current project`);
335
- process.exit(1);
455
+ throw new ArgumentError(`Playbook '${id}' not found in package '${config.id}'`);
336
456
  }
337
457
  targets.push({ root, packageId: config.id, playbooks: [pb] });
338
458
  } else {
@@ -349,8 +469,7 @@ This playbook provides task execution guidance for AI Agents.
349
469
  // Outside project: validate all linked packages
350
470
  const linkedList = listLinkedPackages();
351
471
  if (linkedList.length === 0) {
352
- console.error("Error: Not in an ActionDock project, and no packages linked.");
353
- process.exit(1);
472
+ throw new ArgumentError("Not in an ActionDock project, and no packages linked.");
354
473
  }
355
474
  for (const pkg of linkedList) {
356
475
  if (!existsSync(pkg.path)) continue;
@@ -415,23 +534,32 @@ This playbook provides task execution guidance for AI Agents.
415
534
  }
416
535
 
417
536
  const allValid = results.every((r) => r.valid);
418
- if (options.json) {
419
- console.log(JSON.stringify({ valid: allValid, results }, null, 2));
420
- } else {
421
- for (const r of results) {
422
- const prefix = targets.length > 1 || !root ? `[${r.packageId}] ` : "";
423
- if (r.valid) {
424
- const warn = r.warnings.length > 0 ? ` (Warnings: ${r.warnings.join("; ")})` : "";
425
- console.log(`[OK] ${prefix}${r.id}: Valid${warn}`);
426
- } else {
427
- console.log(`[FAIL] ${prefix}${r.id}: ${r.errors.join("; ")}`);
428
- }
537
+ renderResult(
538
+ { valid: allValid, results },
539
+ {
540
+ json: options.json,
541
+ envelope: options.envelope,
542
+ humanFormatter: () => {
543
+ const lines: string[] = [];
544
+ for (const r of results) {
545
+ const prefix = targets.length > 1 || !root ? `[${r.packageId}] ` : "";
546
+ if (r.valid) {
547
+ const warn = r.warnings.length > 0 ? ` (Warnings: ${r.warnings.join("; ")})` : "";
548
+ lines.push(`[OK] ${prefix}${r.id}: Valid${warn}`);
549
+ } else {
550
+ lines.push(`[FAIL] ${prefix}${r.id}: ${r.errors.join("; ")}`);
551
+ }
552
+ }
553
+ return lines.join("\n");
554
+ },
429
555
  }
430
- }
431
- if (!allValid) process.exit(1);
556
+ );
557
+ if (!allValid) process.exitCode = 1;
432
558
  } catch (err: any) {
433
- console.error(`Error: ${err.message}`);
434
- process.exit(1);
559
+ if (err instanceof ArgumentError || err instanceof CliError) {
560
+ throw err;
561
+ }
562
+ throw new ExecutionError(err.message);
435
563
  }
436
564
  });
437
565
  }