@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,539 @@
1
+ import { existsSync } from "node:fs";
2
+ import {
3
+ fetchRemoteInfo,
4
+ filterWithFallbackInfo,
5
+ findProjectRoot,
6
+ getRegistryStatus,
7
+ listLinkedPackages,
8
+ loadActions,
9
+ loadPlaybooks,
10
+ loadProjectConfig,
11
+ resolvePackageRoot,
12
+ resolveTarget,
13
+ } from "@actiondock/core";
14
+ import { Command } from "commander";
15
+ import { resolveIntent } from "../utils/filter";
16
+
17
+ interface AggregatedPackage {
18
+ id: string;
19
+ name: string;
20
+ version: string;
21
+ description?: string;
22
+ path: string;
23
+ actionsCount: number;
24
+ playbooksCount: number;
25
+ actions: string[];
26
+ playbooks: string[];
27
+ configDeclared: string[];
28
+ }
29
+
30
+ async function getProjectDetailInfo(root: string) {
31
+ const config = loadProjectConfig(root);
32
+ const actions = await loadActions(root, config.actionsDir);
33
+ const playbooks = loadPlaybooks(root, config.playbooksDir);
34
+
35
+ return {
36
+ id: config.id,
37
+ name: config.name,
38
+ version: config.version,
39
+ description: config.description,
40
+ projectRoot: root,
41
+ actionsDir: config.actionsDir || "actions",
42
+ playbooksDir: config.playbooksDir || "playbooks",
43
+ actionsCount: actions.size,
44
+ playbooksCount: playbooks.size,
45
+ actions: Array.from(actions.keys()),
46
+ playbooks: Array.from(playbooks.keys()),
47
+ configDeclared: config.config ? Object.keys(config.config) : [],
48
+ configDef: config.config,
49
+ actionsMap: actions,
50
+ playbooksMap: playbooks,
51
+ };
52
+ }
53
+
54
+ function projectDetailToJson(info: Awaited<ReturnType<typeof getProjectDetailInfo>>) {
55
+ return {
56
+ id: info.id,
57
+ name: info.name,
58
+ version: info.version,
59
+ description: info.description,
60
+ projectRoot: info.projectRoot,
61
+ actionsDir: info.actionsDir,
62
+ playbooksDir: info.playbooksDir,
63
+ actionsCount: info.actionsCount,
64
+ playbooksCount: info.playbooksCount,
65
+ actions: info.actions,
66
+ playbooks: info.playbooks,
67
+ configDeclared: info.configDeclared,
68
+ };
69
+ }
70
+
71
+ function printProjectDetail(info: Awaited<ReturnType<typeof getProjectDetailInfo>>) {
72
+ console.log(`ActionDock Project: ${info.name} (${info.id})`);
73
+ console.log(`Version: ${info.version}`);
74
+ if (info.description) console.log(`Description: ${info.description}`);
75
+ console.log(`Root: ${info.projectRoot}`);
76
+ console.log(`\nActions (${info.actionsCount}):`);
77
+ for (const [id, act] of info.actionsMap.entries()) {
78
+ console.log(` - ${id.padEnd(28)} ${act.description || ""}`);
79
+ }
80
+ console.log(`\nPlaybooks (${info.playbooksCount}):`);
81
+ for (const [id, pb] of info.playbooksMap.entries()) {
82
+ console.log(` - ${id.padEnd(28)} ${pb.description || ""}`);
83
+ }
84
+ if (info.configDeclared.length > 0) {
85
+ console.log(`\nDeclared Config Keys:`);
86
+ for (const k of info.configDeclared) {
87
+ const item = info.configDef?.[k];
88
+ const isSec = item?.secret ? " [secret]" : "";
89
+ const def =
90
+ item?.default !== undefined
91
+ ? ` (default: ${JSON.stringify(item.default)})`
92
+ : "";
93
+ console.log(
94
+ ` - ${k.padEnd(24)} ${item?.description || ""}${def}${isSec}`
95
+ );
96
+ }
97
+ }
98
+ }
99
+
100
+ function printAggregatedPackages(
101
+ packages: AggregatedPackage[],
102
+ options?: { header?: string; showTip?: boolean }
103
+ ) {
104
+ if (options?.header) {
105
+ console.log(options.header);
106
+ } else {
107
+ console.log(`ActionDock Linked Packages (${packages.length}):\n`);
108
+ }
109
+ for (const p of packages) {
110
+ console.log(`* ${p.name} (${p.id}) v${p.version}`);
111
+ console.log(` Path: ${p.path}`);
112
+ if (p.description) {
113
+ console.log(` Desc: ${p.description}`);
114
+ }
115
+ console.log(
116
+ ` Actions (${p.actionsCount}): ${p.actions.join(", ") || "(none)"}`
117
+ );
118
+ console.log(
119
+ ` Playbooks (${p.playbooksCount}): ${p.playbooks.join(", ") || "(none)"}`
120
+ );
121
+ console.log("");
122
+ }
123
+ if (options?.showTip !== false) {
124
+ console.log(
125
+ "Tip: Run 'ac info <package-id>' to view detailed package configuration and schema."
126
+ );
127
+ }
128
+ }
129
+
130
+ function printRegistryTree(status: ReturnType<typeof getRegistryStatus>): void {
131
+ const hasWorkspaces = status.workspaces.length > 0;
132
+ const hasPackages = status.packages.length > 0;
133
+
134
+ if (!hasWorkspaces && !hasPackages) {
135
+ console.log("[INFO] No ActionDock packages or workspaces currently linked.");
136
+ console.log(" Run 'ac link' inside an Action package or workspace to register it.");
137
+ return;
138
+ }
139
+
140
+ console.log("[ActionDock Workspace & Package Tree]\n");
141
+
142
+ if (hasWorkspaces) {
143
+ console.log("Workspaces:");
144
+ for (const ws of status.workspaces) {
145
+ const tag = ws.status === "active" ? "[OK]" : "[STALE]";
146
+ console.log(` ${tag} ${ws.path} (${ws.packagesCount} package${ws.packagesCount === 1 ? "" : "s"})`);
147
+ if (ws.children && ws.children.length > 0) {
148
+ ws.children.forEach((child: any, idx: number) => {
149
+ const isLast = idx === ws.children!.length - 1;
150
+ const prefix = isLast ? " +-- " : " |-- ";
151
+ console.log(`${prefix}${child.id} (v${child.version}) -> ${child.path}`);
152
+ });
153
+ }
154
+ }
155
+ }
156
+
157
+ if (hasPackages) {
158
+ if (hasWorkspaces) console.log("");
159
+ console.log("Standalone Packages:");
160
+ for (const pkg of status.packages) {
161
+ const tag = pkg.status === "active" ? "[OK]" : "[STALE]";
162
+ console.log(` ${tag} ${pkg.id} (v${pkg.version || "unknown"}) -> ${pkg.path}`);
163
+ }
164
+ }
165
+
166
+ console.log(`\n[Summary] Total: ${status.totalPackagesCount} active package(s), ${status.workspaces.length} workspace(s)`);
167
+ if (status.staleCount > 0) {
168
+ console.log(`[WARN] ${status.staleCount} stale entry/entries detected. Run 'ac unlink --prune' to clean up.`);
169
+ }
170
+ }
171
+
172
+ export function registerInfoCommand(program: Command): void {
173
+ program
174
+ .command("info [patterns...]")
175
+ .description(
176
+ "Display information about current project, linked package, or remote target"
177
+ )
178
+ .option(
179
+ "-i, --intent <pattern>",
180
+ "Regex or fuzzy intent filter; falls back to full list when no match"
181
+ )
182
+ .option("-P, --package <id>", "Target package ID or path")
183
+ .option("--tree", "Display packages in hierarchical tree view grouped by workspace")
184
+ .option("-p, --profile <name>", "Query against a specific profile")
185
+ .option("-s, --server <url>", "Remote server URL")
186
+ .option("-t, --token <token>", "Auth token for remote server")
187
+ .option(
188
+ "--no-fallback",
189
+ "Disable fallback to full list when no items match intent"
190
+ )
191
+ .option("--json", "Output information as JSON")
192
+ .action(async (patterns: string[] = [], options: any) => {
193
+ try {
194
+ if (options.tree) {
195
+ const status = getRegistryStatus();
196
+ if (options.json) {
197
+ console.log(JSON.stringify(status, null, 2));
198
+ } else {
199
+ printRegistryTree(status);
200
+ }
201
+ return;
202
+ }
203
+
204
+ const effectiveIntent = resolveIntent(options.intent, patterns);
205
+ const shouldFallback = options.fallback !== false;
206
+
207
+ const target = resolveTarget({
208
+ profile: options.profile,
209
+ server: options.server,
210
+ token: options.token,
211
+ });
212
+
213
+ // 1. Remote target handling
214
+ if (target.type === "remote") {
215
+ const remoteInfo = await fetchRemoteInfo(
216
+ target.serverUrl!,
217
+ target.token
218
+ );
219
+
220
+ if (options.json) {
221
+ if (Array.isArray(remoteInfo.linkedPackages) && effectiveIntent) {
222
+ const filterRes = filterWithFallbackInfo(
223
+ remoteInfo.linkedPackages,
224
+ effectiveIntent,
225
+ [
226
+ (p: any) => p.id,
227
+ (p: any) => p.name,
228
+ (p: any) => p.description,
229
+ (p: any) => p.path,
230
+ (p: any) => p.actions,
231
+ (p: any) => p.playbooks,
232
+ ],
233
+ shouldFallback
234
+ );
235
+ console.log(
236
+ JSON.stringify(
237
+ {
238
+ ...remoteInfo,
239
+ linkedPackages: filterRes.items,
240
+ isFallback: filterRes.isFallback,
241
+ },
242
+ null,
243
+ 2
244
+ )
245
+ );
246
+ } else {
247
+ console.log(JSON.stringify(remoteInfo, null, 2));
248
+ }
249
+ } else {
250
+ console.log(
251
+ `Remote ActionDock Server: ${target.serverUrl}${target.profileName ? ` (Profile: ${target.profileName})` : ""}`
252
+ );
253
+ if (remoteInfo.id) {
254
+ if (effectiveIntent) {
255
+ const match = filterWithFallbackInfo(
256
+ [remoteInfo],
257
+ effectiveIntent,
258
+ [
259
+ (r: any) => r.id,
260
+ (r: any) => r.name,
261
+ (r: any) => r.description,
262
+ (r: any) => r.actions,
263
+ (r: any) => r.playbooks,
264
+ ],
265
+ shouldFallback
266
+ );
267
+ if (match.matchedCount === 0 && !shouldFallback) {
268
+ console.error(
269
+ `Error: Remote project '${remoteInfo.id}' did not match intent '${effectiveIntent}'`
270
+ );
271
+ process.exit(1);
272
+ }
273
+ }
274
+ console.log(
275
+ `Project: ${remoteInfo.name || remoteInfo.id} (${remoteInfo.id})`
276
+ );
277
+ console.log(`Version: ${remoteInfo.version || "unknown"}`);
278
+ if (remoteInfo.description) {
279
+ console.log(`Description: ${remoteInfo.description}`);
280
+ }
281
+ if (Array.isArray(remoteInfo.actions)) {
282
+ console.log(`\nActions (${remoteInfo.actions.length}):`);
283
+ for (const actId of remoteInfo.actions) {
284
+ console.log(` - ${actId}`);
285
+ }
286
+ }
287
+ if (Array.isArray(remoteInfo.playbooks)) {
288
+ console.log(`\nPlaybooks (${remoteInfo.playbooks.length}):`);
289
+ for (const pbId of remoteInfo.playbooks) {
290
+ console.log(` - ${pbId}`);
291
+ }
292
+ }
293
+ } else if (Array.isArray(remoteInfo.linkedPackages)) {
294
+ let list = remoteInfo.linkedPackages;
295
+ let isFallback = false;
296
+ if (effectiveIntent) {
297
+ const filterRes = filterWithFallbackInfo(
298
+ list,
299
+ effectiveIntent,
300
+ [
301
+ (p: any) => p.id,
302
+ (p: any) => p.name,
303
+ (p: any) => p.description,
304
+ (p: any) => p.path,
305
+ (p: any) => p.actions,
306
+ (p: any) => p.playbooks,
307
+ ],
308
+ shouldFallback
309
+ );
310
+ if (filterRes.matchedCount === 0 && !shouldFallback) {
311
+ console.error(
312
+ `Error: No remote packages matched intent '${effectiveIntent}'`
313
+ );
314
+ process.exit(1);
315
+ }
316
+ list = filterRes.items;
317
+ isFallback = filterRes.isFallback;
318
+ }
319
+ console.log(`Version: ${remoteInfo.version || "2.0.0"}`);
320
+ if (isFallback && effectiveIntent) {
321
+ console.log(
322
+ `\n(No remote packages matched intent '${effectiveIntent}', showing all linked packages)`
323
+ );
324
+ }
325
+ console.log(`\nLinked Packages (${list.length}):`);
326
+ for (const pkg of list) {
327
+ console.log(` - ${pkg.id} (${pkg.path})`);
328
+ }
329
+ } else {
330
+ console.log(JSON.stringify(remoteInfo, null, 2));
331
+ }
332
+ }
333
+ return;
334
+ }
335
+
336
+ // 2. Explicit package option (-P, --package)
337
+ if (options.package) {
338
+ const directRoot = resolvePackageRoot(options.package);
339
+ if (directRoot) {
340
+ const detail = await getProjectDetailInfo(directRoot);
341
+ if (options.json) {
342
+ console.log(JSON.stringify(projectDetailToJson(detail), null, 2));
343
+ } else {
344
+ printProjectDetail(detail);
345
+ }
346
+ return;
347
+ }
348
+ console.error(
349
+ `Error: Package '${options.package}' not found in linked packages or path`
350
+ );
351
+ process.exit(1);
352
+ }
353
+
354
+ // 3. Collect local candidate packages (current project + linked packages)
355
+ const currentRoot = findProjectRoot();
356
+ const linkedList = listLinkedPackages();
357
+ const aggregated: AggregatedPackage[] = [];
358
+ const seenPaths = new Set<string>();
359
+
360
+ if (currentRoot) {
361
+ try {
362
+ const config = loadProjectConfig(currentRoot);
363
+ const actions = await loadActions(currentRoot, config.actionsDir);
364
+ const playbooks = loadPlaybooks(currentRoot, config.playbooksDir);
365
+ aggregated.push({
366
+ id: config.id,
367
+ name: config.name,
368
+ version: config.version,
369
+ description: config.description,
370
+ path: currentRoot,
371
+ actionsCount: actions.size,
372
+ playbooksCount: playbooks.size,
373
+ actions: Array.from(actions.keys()),
374
+ playbooks: Array.from(playbooks.keys()),
375
+ configDeclared: config.config ? Object.keys(config.config) : [],
376
+ });
377
+ seenPaths.add(currentRoot);
378
+ } catch {
379
+ // Ignore broken project root
380
+ }
381
+ }
382
+
383
+ for (const pkg of linkedList) {
384
+ if (!existsSync(pkg.path)) continue;
385
+ if (seenPaths.has(pkg.path)) continue;
386
+ try {
387
+ const config = loadProjectConfig(pkg.path);
388
+ const actions = await loadActions(pkg.path, config.actionsDir);
389
+ const playbooks = loadPlaybooks(pkg.path, config.playbooksDir);
390
+
391
+ aggregated.push({
392
+ id: config.id,
393
+ name: config.name,
394
+ version: config.version,
395
+ description: config.description,
396
+ path: pkg.path,
397
+ actionsCount: actions.size,
398
+ playbooksCount: playbooks.size,
399
+ actions: Array.from(actions.keys()),
400
+ playbooks: Array.from(playbooks.keys()),
401
+ configDeclared: config.config ? Object.keys(config.config) : [],
402
+ });
403
+ seenPaths.add(pkg.path);
404
+ } catch {
405
+ // Ignore broken linked package
406
+ }
407
+ }
408
+
409
+ // 4. Case: No pattern / intent specified
410
+ if (!effectiveIntent) {
411
+ if (currentRoot) {
412
+ const detail = await getProjectDetailInfo(currentRoot);
413
+ if (options.json) {
414
+ console.log(JSON.stringify(projectDetailToJson(detail), null, 2));
415
+ } else {
416
+ printProjectDetail(detail);
417
+ }
418
+ return;
419
+ }
420
+
421
+ if (aggregated.length === 0) {
422
+ console.log(
423
+ "No ActionDock project in current directory, and no packages linked."
424
+ );
425
+ console.log(
426
+ "Run 'ac link' inside an Action package to register it."
427
+ );
428
+ return;
429
+ }
430
+
431
+ if (options.json) {
432
+ console.log(
433
+ JSON.stringify({ linkedPackages: aggregated }, null, 2)
434
+ );
435
+ } else {
436
+ printAggregatedPackages(aggregated);
437
+ }
438
+ return;
439
+ }
440
+
441
+ // 5. Case: Pattern / intent specified
442
+ // 5.1 Check if single positional argument directly matches an exact path or package ID/slug
443
+ if (patterns.length === 1 && !options.intent) {
444
+ const directRoot = resolvePackageRoot(patterns[0]);
445
+ if (directRoot) {
446
+ const detail = await getProjectDetailInfo(directRoot);
447
+ if (options.json) {
448
+ console.log(JSON.stringify(projectDetailToJson(detail), null, 2));
449
+ } else {
450
+ printProjectDetail(detail);
451
+ }
452
+ return;
453
+ }
454
+ }
455
+
456
+ if (aggregated.length === 0) {
457
+ console.error(
458
+ `Error: No ActionDock project or linked packages available to match '${effectiveIntent}'`
459
+ );
460
+ process.exit(1);
461
+ }
462
+
463
+ // 5.2 Intent filtering across candidate packages
464
+ const filterRes = filterWithFallbackInfo(
465
+ aggregated,
466
+ effectiveIntent,
467
+ [
468
+ (p) => p.id,
469
+ (p) => p.name,
470
+ (p) => p.description,
471
+ (p) => p.path,
472
+ (p) => p.actions,
473
+ (p) => p.playbooks,
474
+ ],
475
+ shouldFallback
476
+ );
477
+
478
+ // Subcase A: 0 items matched
479
+ if (filterRes.matchedCount === 0) {
480
+ if (!shouldFallback) {
481
+ if (options.json) {
482
+ console.log(JSON.stringify({ linkedPackages: [] }, null, 2));
483
+ } else {
484
+ console.error(
485
+ `Error: No packages matched intent '${effectiveIntent}'`
486
+ );
487
+ }
488
+ process.exit(1);
489
+ }
490
+
491
+ // Fallback enabled: display all packages with a notice
492
+ if (options.json) {
493
+ console.log(
494
+ JSON.stringify(
495
+ { linkedPackages: filterRes.items, isFallback: true },
496
+ null,
497
+ 2
498
+ )
499
+ );
500
+ } else {
501
+ console.log(
502
+ `(No linked packages matched intent '${effectiveIntent}', showing all packages)\n`
503
+ );
504
+ printAggregatedPackages(filterRes.items);
505
+ }
506
+ return;
507
+ }
508
+
509
+ // Subcase B: Exactly 1 item matched -> Display its full project detail
510
+ if (filterRes.matchedCount === 1 && !filterRes.isFallback) {
511
+ const matchedPkg = filterRes.items[0];
512
+ const detail = await getProjectDetailInfo(matchedPkg.path);
513
+ if (options.json) {
514
+ console.log(JSON.stringify(projectDetailToJson(detail), null, 2));
515
+ } else {
516
+ printProjectDetail(detail);
517
+ }
518
+ return;
519
+ }
520
+
521
+ // Subcase C: Multiple items matched -> Display filtered summary list
522
+ if (options.json) {
523
+ console.log(
524
+ JSON.stringify({ linkedPackages: filterRes.items }, null, 2)
525
+ );
526
+ } else {
527
+ printAggregatedPackages(filterRes.items, {
528
+ header: `ActionDock Linked Packages (${filterRes.matchedCount} matches for '${effectiveIntent}'):\n`,
529
+ showTip: true,
530
+ });
531
+ }
532
+ } catch (err: any) {
533
+ console.error(`Error: ${err.message}`);
534
+ process.exit(1);
535
+ }
536
+ });
537
+ }
538
+
539
+
@@ -0,0 +1,24 @@
1
+ import { initProject } from "@actiondock/core";
2
+ import { Command } from "commander";
3
+
4
+ export function registerInitCommand(program: Command): void {
5
+ program
6
+ .command("init [directory]")
7
+ .description("Initialize a new ActionDock project")
8
+ .option("-i, --id <id>", "Project identifier")
9
+ .option("-n, --name <name>", "Project display name")
10
+ .option("-d, --desc <description>", "Project description")
11
+ .action((dir = ".", options) => {
12
+ try {
13
+ initProject(dir, {
14
+ id: options.id,
15
+ name: options.name,
16
+ description: options.desc,
17
+ });
18
+ console.log(`Successfully initialized ActionDock project in ${dir}`);
19
+ } catch (err: any) {
20
+ console.error(`Error: ${err.message}`);
21
+ process.exit(1);
22
+ }
23
+ });
24
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ linkPackage,
3
+ pruneRegistry,
4
+ unlinkPackage,
5
+ } from "@actiondock/core";
6
+ import { Command } from "commander";
7
+
8
+ export function registerLinkCommands(program: Command): void {
9
+ // ac link [path]
10
+ program
11
+ .command("link [path]")
12
+ .option("-r, --recursive", "Recursively discover and link packages in subdirectories")
13
+ .description("Link local Action package(s) or workspace directory into global registry for instant cross-directory execution")
14
+ .action(async (targetPath, options) => {
15
+ try {
16
+ const result = linkPackage(targetPath || process.cwd(), undefined, {
17
+ recursive: options.recursive,
18
+ });
19
+
20
+ if (result.isWorkspace) {
21
+ console.log(`[OK] Linked workspace '${result.path}' (${result.entries.length} package${result.entries.length > 1 ? "s" : ""}):`);
22
+ for (const e of result.entries) {
23
+ console.log(` - ${e.id} (v${e.version}) -> ${e.path}`);
24
+ }
25
+ console.log("[INFO] Sub-packages added to this workspace will be automatically discovered.");
26
+ } else {
27
+ console.log(`[OK] Linked package '${result.id}' (v${result.version}) from ${result.path}`);
28
+ }
29
+ } catch (err: any) {
30
+ console.error(`Error: ${err.message}`);
31
+ process.exit(1);
32
+ }
33
+ });
34
+
35
+ // ac unlink [id|path]
36
+ program
37
+ .command("unlink [identifier]")
38
+ .option("-p, --prune", "Automatically scan and prune any stale/missing paths from registry")
39
+ .description("Unlink a package, workspace, or prune stale entries from global developer registry")
40
+ .action((identifier, options) => {
41
+ try {
42
+ if (options.prune || identifier === "--prune") {
43
+ const result = pruneRegistry();
44
+ const totalPruned = result.prunedPackages.length + result.prunedWorkspaces.length;
45
+ if (totalPruned === 0) {
46
+ console.log("[OK] No stale registry entries found.");
47
+ } else {
48
+ console.log(`[OK] Pruned ${result.prunedWorkspaces.length} workspace(s) and ${result.prunedPackages.length} package(s) from registry.`);
49
+ for (const ws of result.prunedWorkspaces) {
50
+ console.log(` - [workspace] ${ws.path}`);
51
+ }
52
+ for (const pkg of result.prunedPackages) {
53
+ console.log(` - [package] ${pkg.id} (${pkg.path})`);
54
+ }
55
+ }
56
+ return;
57
+ }
58
+
59
+ const removed = unlinkPackage(identifier || process.cwd());
60
+ if (removed) {
61
+ if (removed.type === "workspace") {
62
+ console.log(`[OK] Unlinked workspace '${removed.path}' (${removed.packagesCount || 0} package${(removed.packagesCount || 0) > 1 ? "s" : ""} unlinked)`);
63
+ } else {
64
+ console.log(`[OK] Unlinked package '${removed.id}' (${removed.path})`);
65
+ }
66
+ } else {
67
+ console.log(`Package or workspace '${identifier || process.cwd()}' was not linked in registry`);
68
+ }
69
+ } catch (err: any) {
70
+ console.error(`Error: ${err.message}`);
71
+ process.exit(1);
72
+ }
73
+ });
74
+ }