@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.
@@ -1,6 +1,15 @@
1
- import { exportSkill, findProjectRoot, resolvePackageRoot } from "@actiondock/core";
1
+ import { existsSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import { exportCompositeSkill, exportSkill, exportSkillBatch } from "@actiondock/builder";
4
+ import { discoverProjects, findProjectRoot, listLinkedPackages, resolvePackageRoot } from "@actiondock/core";
5
+ import { ExecutionError } from "@actiondock/runtime-cli";
2
6
  import { Command } from "commander";
3
7
 
8
+ function parseListOption(val: string, prev: string[] = []): string[] {
9
+ const parts = val.split(",").map((s) => s.trim()).filter(Boolean);
10
+ return [...prev, ...parts];
11
+ }
12
+
4
13
  export function registerExportCommand(program: Command): void {
5
14
  const exportCmd = program
6
15
  .command("export")
@@ -9,7 +18,18 @@ export function registerExportCommand(program: Command): void {
9
18
  exportCmd
10
19
  .command("skill")
11
20
  .description("Export Skill directory for AI Agents (default: source skill; use -s/--standalone for pre-built standalone binary)")
12
- .option("-P, --package <id>", "Target package ID or path")
21
+ .option(
22
+ "-P, --package <id...>",
23
+ "Target package ID(s) or path(s) (can be specified multiple times or comma-separated)",
24
+ parseListOption,
25
+ []
26
+ )
27
+ .option("--workspace", "Export all packages discovered in current workspace")
28
+ .option("--all", "Export all linked packages from global registry")
29
+ .option(
30
+ "--bundle [name]",
31
+ "Export multiple packages as a unified composite Skill bundle (defaults to workspace directory name if omitted)"
32
+ )
13
33
  .option("-s, --standalone", "Export pre-compiled standalone binary skill (for environments without ActionDock runtime)")
14
34
  .option("-t, --target <target>", "Target compilation platform for standalone mode (e.g. host, linux-x64, darwin-arm64, windows-x64)")
15
35
  .option("-o, --out <path>", "Output skill directory")
@@ -20,25 +40,126 @@ export function registerExportCommand(program: Command): void {
20
40
  .option("--bytecode", "Compile JavaScript to bytecode in standalone mode (default: true)", true)
21
41
  .option("--no-bytecode", "Disable bytecode compilation in standalone mode")
22
42
  .option("-z, --archive", "Create a .zip archive of the exported skill")
43
+ .option("--skill-md <path>", "Use specified existing SKILL.md file instead of auto-generating")
23
44
  .action(async (options) => {
24
- const root = options.package
25
- ? resolvePackageRoot(options.package)
26
- : findProjectRoot();
45
+ const isStandalone = Boolean(options.standalone);
46
+
47
+ if (options.bundle && isStandalone) {
48
+ throw new ExecutionError(
49
+ "Composite Skill export (--bundle) currently only supports source mode. Please remove --standalone to export as a composite workspace Skill."
50
+ );
51
+ }
52
+
53
+ const sourceCount =
54
+ (options.all ? 1 : 0) +
55
+ (options.workspace ? 1 : 0) +
56
+ (options.package && options.package.length > 0 ? 1 : 0);
57
+ if (sourceCount > 1) {
58
+ throw new ExecutionError(
59
+ "Options -P/--package, --workspace, and --all are mutually exclusive. Please specify only one package selection mode."
60
+ );
61
+ }
62
+
63
+ let roots: string[] = [];
27
64
 
28
- if (!root) {
29
- if (options.package) {
30
- console.error(`Error: Package '${options.package}' not found in linked packages or path`);
65
+ if (options.all) {
66
+ const linked = listLinkedPackages();
67
+ roots = linked.map((p) => p.path).filter((p) => existsSync(p));
68
+ if (roots.length === 0) {
69
+ throw new ExecutionError("No linked packages found in global registry.");
70
+ }
71
+ } else if (options.workspace) {
72
+ const cwd = process.cwd();
73
+ const discovered = discoverProjects(cwd);
74
+ roots = discovered.length > 0 ? discovered : (findProjectRoot(cwd) ? [findProjectRoot(cwd)!] : []);
75
+ if (roots.length === 0) {
76
+ throw new ExecutionError(
77
+ `No ActionDock packages found in workspace '${cwd}'.\nMake sure child directories contain actiondock.json.`
78
+ );
79
+ }
80
+ } else if (options.package && options.package.length > 0) {
81
+ for (const pkgIdOrPath of options.package) {
82
+ const root = resolvePackageRoot(pkgIdOrPath);
83
+ if (!root) {
84
+ throw new ExecutionError(`Package '${pkgIdOrPath}' not found in linked packages or path`);
85
+ }
86
+ if (!roots.includes(root)) {
87
+ roots.push(root);
88
+ }
89
+ }
90
+ } else {
91
+ const root = findProjectRoot();
92
+ if (root) {
93
+ roots.push(root);
31
94
  } else {
32
- console.error(
33
- "Error: Not in an ActionDock project (actiondock.json not found).\nPlease specify -P, --package <id> or cd into a project directory."
95
+ throw new ExecutionError(
96
+ "Not in an ActionDock project (actiondock.json not found).\nPlease specify -P, --package <id>, --workspace, --all, or cd into a project directory."
34
97
  );
35
98
  }
36
- process.exit(1);
37
99
  }
38
100
 
39
- const isStandalone = Boolean(options.standalone);
101
+ if (roots.length > 1 && (options.actions || options.playbook)) {
102
+ throw new ExecutionError(
103
+ "Filtering options (--actions, --playbook) can only be used when exporting a single package."
104
+ );
105
+ }
40
106
 
41
107
  try {
108
+ if (options.bundle !== undefined) {
109
+ const bundleName =
110
+ typeof options.bundle === "string" && options.bundle.trim()
111
+ ? options.bundle.trim()
112
+ : basename(process.cwd());
113
+ console.log(`Exporting composite Skill bundle '${bundleName}' (${roots.length} package${roots.length > 1 ? "s" : ""})...`);
114
+ const result = await exportCompositeSkill({
115
+ bundleName,
116
+ projectRoots: roots,
117
+ outDir: options.out,
118
+ archive: options.archive,
119
+ workspaceRoot: options.workspace ? process.cwd() : undefined,
120
+ skillMdPath: options.skillMd,
121
+ });
122
+
123
+ console.log(`[OK] Successfully exported Composite Skill: ${result.bundleName}`);
124
+ console.log(` Packages: ${result.packagesCount}`);
125
+ console.log(` Actions: ${result.actionsCount}`);
126
+ console.log(` Playbooks: ${result.playbooksCount}`);
127
+ console.log(` Skill Dir: ${result.skillDir}`);
128
+ if (result.usedExistingSkillMd) {
129
+ console.log(` SKILL.md: Reused existing file from ${result.usedExistingSkillMd}`);
130
+ }
131
+ if (result.archivePath) {
132
+ console.log(` Archive: ${result.archivePath}`);
133
+ }
134
+ return;
135
+ }
136
+
137
+ if (roots.length > 1) {
138
+ console.log(`Batch exporting ${roots.length} Skill packages...`);
139
+ const batchRes = await exportSkillBatch({
140
+ projectRoots: roots,
141
+ mode: isStandalone ? "standalone" : "source",
142
+ standalone: isStandalone,
143
+ target: options.target,
144
+ outDir: options.out,
145
+ archive: options.archive,
146
+ playbooks: options.playbook,
147
+ actions: options.actions,
148
+ minify: options.minify,
149
+ bytecode: options.bytecode,
150
+ skillMdPath: options.skillMd,
151
+ });
152
+
153
+ console.log(`[OK] Successfully batch exported ${batchRes.results.length} Skill packages to: ${batchRes.outDir}`);
154
+ for (const res of batchRes.results) {
155
+ console.log(` - ${res.packageId} (v${res.version}): ${res.actionsCount} actions, ${res.playbooksCount} playbooks -> ${res.skillDir}`);
156
+ }
157
+ console.log(` Total Actions: ${batchRes.totalActions}`);
158
+ console.log(` Total Playbooks: ${batchRes.totalPlaybooks}`);
159
+ return;
160
+ }
161
+
162
+ const root = roots[0];
42
163
  console.log(`Exporting ${isStandalone ? "standalone binary" : "source"} Skill artifact...`);
43
164
  const result = await exportSkill({
44
165
  projectRoot: root,
@@ -51,6 +172,7 @@ export function registerExportCommand(program: Command): void {
51
172
  actions: options.actions,
52
173
  minify: options.minify,
53
174
  bytecode: options.bytecode,
175
+ skillMdPath: options.skillMd,
54
176
  });
55
177
 
56
178
  console.log(`[OK] Successfully exported ${result.mode === "standalone" ? "Standalone" : "Source"} Skill: ${result.packageId} (v${result.version})`);
@@ -58,13 +180,17 @@ export function registerExportCommand(program: Command): void {
58
180
  console.log(` Actions: ${result.actionsCount}`);
59
181
  console.log(` Playbooks: ${result.playbooksCount}`);
60
182
  console.log(` Skill Dir: ${result.skillDir}`);
183
+ if (result.usedExistingSkillMd) {
184
+ console.log(` SKILL.md: Reused existing file from ${result.usedExistingSkillMd}`);
185
+ }
61
186
  if (result.archivePath) {
62
187
  console.log(` Archive: ${result.archivePath}`);
63
188
  }
64
189
  } catch (err: any) {
65
- console.error(`Export failed: ${err.message}`);
66
- process.exit(1);
190
+ throw new ExecutionError(`Export failed: ${err.message}`);
67
191
  }
68
192
  });
69
193
  }
70
194
 
195
+
196
+
@@ -15,13 +15,43 @@ import { registerServeCommand } from "./serve";
15
15
  import { registerStateCommands } from "./state";
16
16
  import { registerTestCommand } from "./test";
17
17
 
18
+ function applyCommonOptions(cmd: Command): void {
19
+ const hasOpt = (flagName: string) =>
20
+ cmd.options.some((o) => o.name() === flagName || o.long === `--${flagName}`);
21
+
22
+ if (!hasOpt("json")) {
23
+ cmd.option("--json", "Output as JSON");
24
+ }
25
+ if (!hasOpt("envelope")) {
26
+ cmd.option("--envelope", "Wrap JSON output in standard envelope");
27
+ }
28
+ if (!hasOpt("dataDir") && !hasOpt("data-dir")) {
29
+ cmd.option("--data-dir <path>", "Custom database storage directory");
30
+ }
31
+
32
+ for (const sub of cmd.commands) {
33
+ applyCommonOptions(sub);
34
+ }
35
+ }
36
+
18
37
  export function createCliProgram(): Command {
19
38
  const program = new Command();
20
39
 
21
40
  program
22
41
  .name("ad")
23
42
  .description("ActionDock (ad) 2.0 - Toolchain for building and shipping standalone AI Agent Actions & Skills")
24
- .version("2.0.9");
43
+ .version("2.0.11-beta.0", "-v, --version");
44
+
45
+ program.option("-V", "output the version number");
46
+ program.on("option:V", () => {
47
+ (program as any)._outputConfiguration.writeOut(`${program.version()}\n`);
48
+ (program as any)._exit(0, "commander.version", program.version()!);
49
+ });
50
+
51
+ // 禁用 Commander 内部直接 process.exit,统一由顶层调度器管控退出码
52
+ program.exitOverride((err) => {
53
+ throw err;
54
+ });
25
55
 
26
56
  registerInitCommand(program);
27
57
  registerInfoCommand(program);
@@ -39,5 +69,7 @@ export function createCliProgram(): Command {
39
69
  registerServeCommand(program);
40
70
  registerMcpCommands(program);
41
71
 
72
+ applyCommonOptions(program);
73
+
42
74
  return program;
43
75
  }
@@ -5,13 +5,14 @@ import {
5
5
  findProjectRoot,
6
6
  getRegistryStatus,
7
7
  listLinkedPackages,
8
- loadActions,
8
+ loadManifest,
9
9
  loadPlaybooks,
10
10
  loadProjectConfig,
11
11
  resolvePackageRoot,
12
12
  resolveTarget,
13
13
  } from "@actiondock/core";
14
14
  import { Command } from "commander";
15
+ import { ArgumentError, CliError, ExecutionError, getEffectiveOptions, renderResult } from "@actiondock/runtime-cli";
15
16
  import { resolveIntent } from "../utils/filter";
16
17
 
17
18
  interface AggregatedPackage {
@@ -29,7 +30,16 @@ interface AggregatedPackage {
29
30
 
30
31
  async function getProjectDetailInfo(root: string) {
31
32
  const config = loadProjectConfig(root);
32
- const actions = await loadActions(root, config.actionsDir);
33
+ const manifest = loadManifest(root);
34
+ const actionsMap = new Map<string, { id: string; description?: string }>();
35
+ if (manifest?.actions) {
36
+ for (const [id, item] of Object.entries(manifest.actions)) {
37
+ actionsMap.set(id, {
38
+ id,
39
+ description: item.description,
40
+ });
41
+ }
42
+ }
33
43
  const playbooks = loadPlaybooks(root, config.playbooksDir);
34
44
 
35
45
  return {
@@ -40,13 +50,13 @@ async function getProjectDetailInfo(root: string) {
40
50
  projectRoot: root,
41
51
  actionsDir: config.actionsDir || "actions",
42
52
  playbooksDir: config.playbooksDir || "playbooks",
43
- actionsCount: actions.size,
53
+ actionsCount: actionsMap.size,
44
54
  playbooksCount: playbooks.size,
45
- actions: Array.from(actions.keys()),
55
+ actions: Array.from(actionsMap.keys()),
46
56
  playbooks: Array.from(playbooks.keys()),
47
57
  configDeclared: config.config ? Object.keys(config.config) : [],
48
58
  configDef: config.config,
49
- actionsMap: actions,
59
+ actionsMap,
50
60
  playbooksMap: playbooks,
51
61
  };
52
62
  }
@@ -184,15 +194,21 @@ export function registerInfoCommand(program: Command): void {
184
194
  .option("-p, --profile <name>", "Query against a specific profile")
185
195
  .option("-s, --server <url>", "Remote server URL")
186
196
  .option("-t, --token <token>", "Auth token for remote server")
197
+ .option("--fallback", "Enable fallback to full list when no items match intent")
187
198
  .option(
188
199
  "--no-fallback",
189
200
  "Disable fallback to full list when no items match intent"
190
201
  )
191
202
  .option("--json", "Output information as JSON")
192
- .action(async (patterns: string[] = [], options: any) => {
203
+ .option("--envelope", "Wrap JSON output in standard envelope")
204
+ .option("--data-dir <path>", "Custom database storage directory")
205
+ .action(async (patterns: string[] = [], rawOptions: any, cmd: any) => {
193
206
  try {
207
+ const options = getEffectiveOptions(rawOptions, cmd);
194
208
  const effectiveIntent = resolveIntent(options.intent, patterns);
195
- const shouldFallback = options.fallback !== false;
209
+ const isMachine = Boolean(options.json || options.envelope);
210
+ const fallbackExplicit = options.fallback === true || (Array.isArray(process.argv) && process.argv.includes("--fallback"));
211
+ const shouldFallback = isMachine ? fallbackExplicit : options.fallback !== false;
196
212
 
197
213
  const target = resolveTarget({
198
214
  profile: options.profile,
@@ -212,8 +228,8 @@ export function registerInfoCommand(program: Command): void {
212
228
  }
213
229
  );
214
230
 
215
- if (options.json) {
216
- console.log(JSON.stringify(remoteInfo, null, 2));
231
+ if (isMachine) {
232
+ renderResult(remoteInfo, { json: options.json, envelope: options.envelope });
217
233
  return;
218
234
  }
219
235
 
@@ -304,8 +320,8 @@ export function registerInfoCommand(program: Command): void {
304
320
  // Local Tree
305
321
  if (options.tree) {
306
322
  const status = getRegistryStatus();
307
- if (options.json) {
308
- console.log(JSON.stringify(status, null, 2));
323
+ if (isMachine) {
324
+ renderResult(status, { json: options.json, envelope: options.envelope });
309
325
  } else {
310
326
  printRegistryTree(status);
311
327
  }
@@ -317,17 +333,16 @@ export function registerInfoCommand(program: Command): void {
317
333
  const directRoot = resolvePackageRoot(options.package);
318
334
  if (directRoot) {
319
335
  const detail = await getProjectDetailInfo(directRoot);
320
- if (options.json) {
321
- console.log(JSON.stringify(projectDetailToJson(detail), null, 2));
336
+ if (isMachine) {
337
+ renderResult(projectDetailToJson(detail), { json: options.json, envelope: options.envelope });
322
338
  } else {
323
339
  printProjectDetail(detail);
324
340
  }
325
341
  return;
326
342
  }
327
- console.error(
328
- `Error: Package '${options.package}' not found in linked packages or path`
343
+ throw new ArgumentError(
344
+ `Package '${options.package}' not found in linked packages or path`
329
345
  );
330
- process.exit(1);
331
346
  }
332
347
 
333
348
  // 3. Collect local candidate packages (current project + linked packages)
@@ -339,7 +354,8 @@ export function registerInfoCommand(program: Command): void {
339
354
  if (currentRoot) {
340
355
  try {
341
356
  const config = loadProjectConfig(currentRoot);
342
- const actions = await loadActions(currentRoot, config.actionsDir);
357
+ const manifest = loadManifest(currentRoot);
358
+ const manifestActionIds = manifest?.actions ? Object.keys(manifest.actions) : [];
343
359
  const playbooks = loadPlaybooks(currentRoot, config.playbooksDir);
344
360
  aggregated.push({
345
361
  id: config.id,
@@ -347,9 +363,9 @@ export function registerInfoCommand(program: Command): void {
347
363
  version: config.version,
348
364
  description: config.description,
349
365
  path: currentRoot,
350
- actionsCount: actions.size,
366
+ actionsCount: manifestActionIds.length,
351
367
  playbooksCount: playbooks.size,
352
- actions: Array.from(actions.keys()),
368
+ actions: manifestActionIds,
353
369
  playbooks: Array.from(playbooks.keys()),
354
370
  configDeclared: config.config ? Object.keys(config.config) : [],
355
371
  });
@@ -364,7 +380,8 @@ export function registerInfoCommand(program: Command): void {
364
380
  if (seenPaths.has(pkg.path)) continue;
365
381
  try {
366
382
  const config = loadProjectConfig(pkg.path);
367
- const actions = await loadActions(pkg.path, config.actionsDir);
383
+ const manifest = loadManifest(pkg.path);
384
+ const manifestActionIds = manifest?.actions ? Object.keys(manifest.actions) : [];
368
385
  const playbooks = loadPlaybooks(pkg.path, config.playbooksDir);
369
386
 
370
387
  aggregated.push({
@@ -373,9 +390,9 @@ export function registerInfoCommand(program: Command): void {
373
390
  version: config.version,
374
391
  description: config.description,
375
392
  path: pkg.path,
376
- actionsCount: actions.size,
393
+ actionsCount: manifestActionIds.length,
377
394
  playbooksCount: playbooks.size,
378
- actions: Array.from(actions.keys()),
395
+ actions: manifestActionIds,
379
396
  playbooks: Array.from(playbooks.keys()),
380
397
  configDeclared: config.config ? Object.keys(config.config) : [],
381
398
  });
@@ -389,8 +406,8 @@ export function registerInfoCommand(program: Command): void {
389
406
  if (!effectiveIntent) {
390
407
  if (currentRoot) {
391
408
  const detail = await getProjectDetailInfo(currentRoot);
392
- if (options.json) {
393
- console.log(JSON.stringify(projectDetailToJson(detail), null, 2));
409
+ if (isMachine) {
410
+ renderResult(projectDetailToJson(detail), { json: options.json, envelope: options.envelope });
394
411
  } else {
395
412
  printProjectDetail(detail);
396
413
  }
@@ -398,19 +415,21 @@ export function registerInfoCommand(program: Command): void {
398
415
  }
399
416
 
400
417
  if (aggregated.length === 0) {
418
+ if (isMachine) {
419
+ renderResult({ linkedPackages: [] }, { json: options.json, envelope: options.envelope });
420
+ return;
421
+ }
401
422
  console.log(
402
423
  "No ActionDock project in current directory, and no packages linked."
403
424
  );
404
425
  console.log(
405
- "Run 'ad link' inside an Action package to register it."
426
+ "Run 'ad link' inside an Action package or specify a target package."
406
427
  );
407
428
  return;
408
429
  }
409
430
 
410
- if (options.json) {
411
- console.log(
412
- JSON.stringify({ linkedPackages: aggregated }, null, 2)
413
- );
431
+ if (isMachine) {
432
+ renderResult({ linkedPackages: aggregated }, { json: options.json, envelope: options.envelope });
414
433
  } else {
415
434
  printAggregatedPackages(aggregated);
416
435
  }
@@ -423,8 +442,8 @@ export function registerInfoCommand(program: Command): void {
423
442
  const directRoot = resolvePackageRoot(patterns[0]);
424
443
  if (directRoot) {
425
444
  const detail = await getProjectDetailInfo(directRoot);
426
- if (options.json) {
427
- console.log(JSON.stringify(projectDetailToJson(detail), null, 2));
445
+ if (isMachine) {
446
+ renderResult(projectDetailToJson(detail), { json: options.json, envelope: options.envelope });
428
447
  } else {
429
448
  printProjectDetail(detail);
430
449
  }
@@ -433,10 +452,13 @@ export function registerInfoCommand(program: Command): void {
433
452
  }
434
453
 
435
454
  if (aggregated.length === 0) {
436
- console.error(
437
- `Error: No ActionDock project or linked packages available to match '${effectiveIntent}'`
455
+ if (isMachine) {
456
+ renderResult({ linkedPackages: [], matchedCount: 0, isFallback: false }, { json: options.json, envelope: options.envelope });
457
+ return;
458
+ }
459
+ throw new ExecutionError(
460
+ `No ActionDock project or linked packages available to match '${effectiveIntent}'`
438
461
  );
439
- process.exit(1);
440
462
  }
441
463
 
442
464
  // 5.2 Intent filtering across candidate packages
@@ -456,61 +478,58 @@ export function registerInfoCommand(program: Command): void {
456
478
 
457
479
  // Subcase A: 0 items matched
458
480
  if (filterRes.matchedCount === 0) {
459
- if (!shouldFallback) {
460
- if (options.json) {
461
- console.log(JSON.stringify({ linkedPackages: [] }, null, 2));
462
- } else {
463
- console.error(
464
- `Error: No packages matched intent '${effectiveIntent}'`
465
- );
481
+ if (!filterRes.isFallback) {
482
+ if (isMachine) {
483
+ renderResult({ linkedPackages: [], matchedCount: 0, isFallback: false }, { json: options.json, envelope: options.envelope });
484
+ return;
466
485
  }
467
- process.exit(1);
486
+ throw new ExecutionError(
487
+ `No packages matched intent '${effectiveIntent}'`
488
+ );
468
489
  }
469
490
 
470
491
  // Fallback enabled: display all packages with a notice
471
- if (options.json) {
472
- console.log(
473
- JSON.stringify(
474
- { linkedPackages: filterRes.items, isFallback: true },
475
- null,
476
- 2
477
- )
492
+ if (isMachine) {
493
+ renderResult(
494
+ { linkedPackages: filterRes.items, isFallback: true, matchedCount: 0 },
495
+ { json: options.json, envelope: options.envelope }
478
496
  );
479
- } else {
480
- console.log(
481
- `(No linked packages matched intent '${effectiveIntent}', showing all packages)\n`
482
- );
483
- printAggregatedPackages(filterRes.items);
497
+ return;
484
498
  }
499
+ console.log(
500
+ `(No linked packages matched intent '${effectiveIntent}', showing all packages)\n`
501
+ );
502
+ printAggregatedPackages(filterRes.items);
485
503
  return;
486
504
  }
487
505
 
488
- // Subcase B: Exactly 1 item matched -> Display its full project detail
489
- if (filterRes.matchedCount === 1 && !filterRes.isFallback) {
506
+ // Subcase B: Matched items in machine mode
507
+ if (isMachine) {
508
+ renderResult(
509
+ { linkedPackages: filterRes.items, matchedCount: filterRes.matchedCount, isFallback: false },
510
+ { json: options.json, envelope: options.envelope }
511
+ );
512
+ return;
513
+ }
514
+
515
+ // Subcase C: Exactly 1 item matched in human terminal mode -> Display full project detail
516
+ if (filterRes.matchedCount === 1) {
490
517
  const matchedPkg = filterRes.items[0];
491
518
  const detail = await getProjectDetailInfo(matchedPkg.path);
492
- if (options.json) {
493
- console.log(JSON.stringify(projectDetailToJson(detail), null, 2));
494
- } else {
495
- printProjectDetail(detail);
496
- }
519
+ printProjectDetail(detail);
497
520
  return;
498
521
  }
499
522
 
500
- // Subcase C: Multiple items matched -> Display filtered summary list
501
- if (options.json) {
502
- console.log(
503
- JSON.stringify({ linkedPackages: filterRes.items }, null, 2)
504
- );
505
- } else {
506
- printAggregatedPackages(filterRes.items, {
507
- header: `ActionDock Linked Packages (${filterRes.matchedCount} matches for '${effectiveIntent}'):\n`,
508
- showTip: true,
509
- });
510
- }
523
+ // Subcase D: Multiple items matched in human terminal mode -> Display filtered summary list
524
+ printAggregatedPackages(filterRes.items, {
525
+ header: `ActionDock Linked Packages (${filterRes.matchedCount} matches for '${effectiveIntent}'):\n`,
526
+ showTip: true,
527
+ });
511
528
  } catch (err: any) {
512
- console.error(`Error: ${err.message}`);
513
- process.exit(1);
529
+ if (err instanceof CliError) {
530
+ throw err;
531
+ }
532
+ throw new ExecutionError(err.message);
514
533
  }
515
534
  });
516
535
  }
@@ -1,4 +1,5 @@
1
1
  import { initProject } from "@actiondock/core";
2
+ import { ExecutionError } from "@actiondock/runtime-cli";
2
3
  import { Command } from "commander";
3
4
 
4
5
  export function registerInitCommand(program: Command): void {
@@ -17,8 +18,7 @@ export function registerInitCommand(program: Command): void {
17
18
  });
18
19
  console.log(`Successfully initialized ActionDock project in ${dir}`);
19
20
  } catch (err: any) {
20
- console.error(`Error: ${err.message}`);
21
- process.exit(1);
21
+ throw new ExecutionError(err.message);
22
22
  }
23
23
  });
24
24
  }
@@ -3,6 +3,7 @@ import {
3
3
  pruneRegistry,
4
4
  unlinkPackage,
5
5
  } from "@actiondock/core";
6
+ import { ExecutionError } from "@actiondock/runtime-cli";
6
7
  import { Command } from "commander";
7
8
 
8
9
  export function registerLinkCommands(program: Command): void {
@@ -27,8 +28,7 @@ export function registerLinkCommands(program: Command): void {
27
28
  console.log(`[OK] Linked package '${result.id}' (v${result.version}) from ${result.path}`);
28
29
  }
29
30
  } catch (err: any) {
30
- console.error(`Error: ${err.message}`);
31
- process.exit(1);
31
+ throw new ExecutionError(err.message);
32
32
  }
33
33
  });
34
34
 
@@ -67,8 +67,7 @@ export function registerLinkCommands(program: Command): void {
67
67
  console.log(`Package or workspace '${identifier || process.cwd()}' was not linked in registry`);
68
68
  }
69
69
  } catch (err: any) {
70
- console.error(`Error: ${err.message}`);
71
- process.exit(1);
70
+ throw new ExecutionError(err.message);
72
71
  }
73
72
  });
74
73
  }