@actiondock/cli 2.0.12 → 2.2.1

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.
Files changed (100) hide show
  1. package/README.md +41 -34
  2. package/bin/ad.js +28 -41
  3. package/dist/commands/action/create.d.ts +9 -1
  4. package/dist/commands/action/create.js +120 -0
  5. package/dist/commands/add.d.ts +7 -0
  6. package/dist/commands/add.js +144 -0
  7. package/dist/commands/build.d.ts +2 -1
  8. package/dist/commands/build.js +83 -0
  9. package/dist/commands/config/delete.d.ts +9 -0
  10. package/dist/commands/config/delete.js +52 -0
  11. package/dist/commands/config/env.d.ts +9 -0
  12. package/dist/commands/config/env.js +85 -0
  13. package/dist/commands/config/get.d.ts +9 -0
  14. package/dist/commands/config/get.js +120 -0
  15. package/dist/commands/config/index.d.ts +9 -0
  16. package/dist/commands/config/index.js +23 -0
  17. package/dist/commands/config/list.d.ts +9 -0
  18. package/dist/commands/config/list.js +137 -0
  19. package/dist/commands/config/schema.d.ts +9 -0
  20. package/dist/commands/config/schema.js +121 -0
  21. package/dist/commands/config/set.d.ts +9 -0
  22. package/dist/commands/config/set.js +85 -0
  23. package/dist/commands/describe.d.ts +9 -0
  24. package/dist/commands/describe.js +68 -0
  25. package/dist/commands/doctor.d.ts +2 -1
  26. package/dist/commands/doctor.js +82 -0
  27. package/dist/commands/export.d.ts +2 -1
  28. package/dist/commands/export.js +212 -0
  29. package/dist/commands/generate.d.ts +9 -0
  30. package/dist/commands/generate.js +44 -0
  31. package/dist/commands/index.d.ts +31 -1
  32. package/dist/commands/index.js +111 -0
  33. package/dist/commands/info.d.ts +9 -2
  34. package/dist/commands/info.js +210 -0
  35. package/dist/commands/init.js +24 -0
  36. package/dist/commands/link.js +71 -0
  37. package/dist/commands/list.d.ts +9 -0
  38. package/dist/commands/list.js +77 -0
  39. package/dist/commands/mcp.d.ts +9 -2
  40. package/dist/commands/mcp.js +173 -0
  41. package/dist/commands/new.d.ts +9 -0
  42. package/dist/commands/new.js +33 -0
  43. package/dist/commands/pack.d.ts +3 -0
  44. package/dist/commands/pack.js +63 -0
  45. package/dist/commands/playbook.d.ts +7 -3
  46. package/dist/commands/playbook.js +433 -0
  47. package/dist/commands/profile.js +253 -0
  48. package/dist/commands/remove.d.ts +7 -0
  49. package/dist/commands/remove.js +118 -0
  50. package/dist/commands/run.d.ts +13 -0
  51. package/dist/commands/run.js +146 -0
  52. package/dist/commands/runs.d.ts +9 -2
  53. package/dist/commands/runs.js +245 -0
  54. package/dist/commands/serve.d.ts +9 -2
  55. package/dist/commands/serve.js +135 -0
  56. package/dist/commands/state-list.d.ts +24 -0
  57. package/dist/commands/state-list.js +82 -0
  58. package/dist/commands/state.d.ts +9 -2
  59. package/dist/commands/state.js +310 -0
  60. package/dist/commands/test.js +44 -0
  61. package/dist/commands/validate.d.ts +9 -0
  62. package/dist/commands/validate.js +106 -0
  63. package/dist/errors.d.ts +43 -0
  64. package/dist/errors.js +103 -0
  65. package/dist/index.d.ts +8 -1
  66. package/dist/index.js +41 -1000
  67. package/dist/prompt.d.ts +53 -0
  68. package/dist/prompt.js +192 -0
  69. package/dist/renderer.d.ts +197 -0
  70. package/dist/renderer.js +502 -0
  71. package/dist/services/index.d.ts +1 -0
  72. package/dist/services/index.js +1 -0
  73. package/dist/services/inspect.d.ts +23 -0
  74. package/dist/services/inspect.js +96 -0
  75. package/dist/standalone/index.d.ts +16 -0
  76. package/dist/standalone/index.js +22 -0
  77. package/dist/types.d.ts +132 -0
  78. package/dist/types.js +9 -0
  79. package/dist/utils/bytes.d.ts +1 -1
  80. package/dist/utils/bytes.js +1 -0
  81. package/dist/utils/duration.js +1 -0
  82. package/dist/utils/filter.d.ts +1 -1
  83. package/dist/utils/filter.js +1 -0
  84. package/dist/utils/index.d.ts +6 -0
  85. package/dist/utils/index.js +6 -0
  86. package/dist/utils/spawn.d.ts +27 -0
  87. package/dist/utils/spawn.js +35 -0
  88. package/dist/utils/target.d.ts +45 -0
  89. package/dist/utils/target.js +78 -0
  90. package/dist/utils/utils.d.ts +38 -0
  91. package/dist/utils/utils.js +111 -0
  92. package/package.json +10 -12
  93. package/dist/commands/action/index.d.ts +0 -16
  94. package/dist/commands/action/list.d.ts +0 -5
  95. package/dist/commands/action/run.d.ts +0 -4
  96. package/dist/commands/action/show.d.ts +0 -5
  97. package/dist/commands/action/sync.d.ts +0 -2
  98. package/dist/commands/action/validate.d.ts +0 -5
  99. package/dist/commands/action.d.ts +0 -1
  100. package/dist/commands/config.d.ts +0 -2
@@ -0,0 +1,433 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
3
+ import { assertPathWithinRoot, fetchRemotePlaybooks, fetchRemotePlaybookShow, filterWithFallbackInfo, findProjectRoot, listLinkedPackages, loadActions, loadManifest, loadPlaybooks, loadProjectConfig, resolveActionProject, resolvePackageRoot, resolvePlaybookProject, resolveTarget, saveManifest, } from "@actiondock/core";
4
+ import { ArgumentError, CliError, ExecutionError } from "../errors.js";
5
+ import { renderPlaybookDetail, renderPlaybookList, renderResult, writeStdout, } from "../renderer.js";
6
+ import { getEffectiveOptions, resolveIntent } from "../utils/index.js";
7
+ /**
8
+ * 注册 Playbook 命令集合(list, show, validate, create)。
9
+ *
10
+ * @param program Commander 根程序对象
11
+ * @param context 命令行上下文
12
+ */
13
+ export function registerPlaybookCommands(program, context) {
14
+ const pbCmd = program
15
+ .command("playbook")
16
+ .description("Manage task Playbooks (Task SOPs for AI Agents)");
17
+ // playbook list
18
+ pbCmd
19
+ .command("list [patterns...]")
20
+ .description("List playbooks in current project or linked packages")
21
+ .option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
22
+ .option("-P, --package <id>", "Target package ID or path")
23
+ .option("-p, --profile <name>", "Query against a specific profile")
24
+ .option("-s, --server <url>", "Remote server URL")
25
+ .option("-t, --token <token>", "Auth token for remote server")
26
+ .option("--no-fallback", "Disable fallback to full list when no items match intent")
27
+ .option("--json", "Output as JSON")
28
+ .option("--envelope", "Wrap JSON output in standard envelope")
29
+ .action(async (patterns = [], rawOptions, cmd) => {
30
+ const options = getEffectiveOptions(rawOptions, cmd);
31
+ const effectiveIntent = resolveIntent(options.intent, patterns);
32
+ const shouldFallback = options.fallback !== false;
33
+ const isMachine = Boolean(options.json || options.envelope);
34
+ // 1. 远端服务分支
35
+ const target = resolveTarget({
36
+ profile: options.profile,
37
+ server: options.server,
38
+ token: options.token,
39
+ }, context?.customHome);
40
+ if (target.type === "remote") {
41
+ const remotePbs = await fetchRemotePlaybooks(target.serverUrl, target.token, {
42
+ intent: effectiveIntent,
43
+ package: options.package,
44
+ });
45
+ renderResult(remotePbs, {
46
+ json: options.json,
47
+ envelope: options.envelope,
48
+ humanFormatter: () => renderPlaybookList(remotePbs, `Playbooks on remote server ${target.serverUrl}${target.profileName ? ` (Profile: ${target.profileName})` : ""}`, false, effectiveIntent),
49
+ context,
50
+ });
51
+ return;
52
+ }
53
+ // 2. 本地工程模式
54
+ let targetRoot = null;
55
+ if (options.package) {
56
+ targetRoot = resolvePackageRoot(options.package);
57
+ if (!targetRoot) {
58
+ throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
59
+ }
60
+ }
61
+ else {
62
+ targetRoot = findProjectRoot();
63
+ }
64
+ if (targetRoot) {
65
+ const config = loadProjectConfig(targetRoot);
66
+ const playbooks = loadPlaybooks(targetRoot, config.playbooksDir);
67
+ const rawList = Array.from(playbooks.values()).map((pb) => ({
68
+ id: pb.id,
69
+ description: pb.description,
70
+ packageId: config.id,
71
+ }));
72
+ const filterRes = filterWithFallbackInfo(rawList, effectiveIntent, [(p) => p.id, (p) => p.description], shouldFallback);
73
+ if (filterRes.isFallback && isMachine) {
74
+ renderResult({ items: filterRes.items, isFallback: true, matchedCount: 0 }, { json: options.json, envelope: options.envelope, context });
75
+ return;
76
+ }
77
+ renderResult(filterRes.items, {
78
+ json: options.json,
79
+ envelope: options.envelope,
80
+ humanFormatter: () => renderPlaybookList(filterRes.items, `Playbooks in ${config.id} (${targetRoot})`, filterRes.isFallback, effectiveIntent),
81
+ context,
82
+ });
83
+ return;
84
+ }
85
+ // 3. 扫描所有已链接的外部包
86
+ const linkedList = listLinkedPackages(context?.customHome);
87
+ if (linkedList.length === 0) {
88
+ renderResult([], {
89
+ json: options.json,
90
+ envelope: options.envelope,
91
+ humanFormatter: () => "No ActionDock project in current directory, and no packages linked.",
92
+ context,
93
+ });
94
+ return;
95
+ }
96
+ const aggregated = [];
97
+ for (const pkg of linkedList) {
98
+ if (!existsSync(pkg.path))
99
+ continue;
100
+ try {
101
+ const config = loadProjectConfig(pkg.path);
102
+ const pbs = loadPlaybooks(pkg.path, config.playbooksDir);
103
+ const list = Array.from(pbs.values()).map((p) => ({
104
+ id: p.id,
105
+ description: p.description,
106
+ }));
107
+ aggregated.push({
108
+ packageId: pkg.id,
109
+ packageName: pkg.name,
110
+ path: pkg.path,
111
+ playbooks: list,
112
+ });
113
+ }
114
+ catch {
115
+ // 单个链接包清单损坏或 Playbook 加载失败时跳过,继续聚合其余包
116
+ }
117
+ }
118
+ let filteredPackages = [];
119
+ let isFallback = false;
120
+ if (!effectiveIntent) {
121
+ filteredPackages = aggregated;
122
+ }
123
+ else {
124
+ for (const pkg of aggregated) {
125
+ const pkgMatches = filterWithFallbackInfo([pkg], effectiveIntent, [(p) => p.packageId, (p) => p.packageName, (p) => p.path], false).matchedCount > 0;
126
+ if (pkgMatches) {
127
+ filteredPackages.push(pkg);
128
+ }
129
+ else {
130
+ const matchedPbs = filterWithFallbackInfo(pkg.playbooks, effectiveIntent, [(p) => p.id, (p) => p.description], false).items;
131
+ if (matchedPbs.length > 0) {
132
+ filteredPackages.push({
133
+ ...pkg,
134
+ playbooks: matchedPbs,
135
+ });
136
+ }
137
+ }
138
+ }
139
+ if (filteredPackages.length === 0 && shouldFallback) {
140
+ filteredPackages = aggregated;
141
+ isFallback = true;
142
+ }
143
+ }
144
+ if (isFallback && isMachine) {
145
+ renderResult({ packages: filteredPackages, isFallback: true, matchedCount: 0 }, { json: options.json, envelope: options.envelope, context });
146
+ return;
147
+ }
148
+ renderResult(filteredPackages, {
149
+ json: options.json,
150
+ envelope: options.envelope,
151
+ humanFormatter: () => {
152
+ const lines = ["Linked Playbooks:\n"];
153
+ for (const pkg of filteredPackages) {
154
+ lines.push(`- Package: ${pkg.packageId} (${pkg.path})`);
155
+ for (const pb of pkg.playbooks) {
156
+ lines.push(` - ${pb.id.padEnd(26)} ${pb.description || ""}`);
157
+ }
158
+ }
159
+ return lines.join("\n");
160
+ },
161
+ context,
162
+ });
163
+ });
164
+ // playbook show <id>
165
+ pbCmd
166
+ .command("show <id>")
167
+ .description("Show playbook content and metadata")
168
+ .option("-P, --package <id>", "Target package ID or path")
169
+ .option("-p, --profile <name>", "Query against a specific profile")
170
+ .option("-s, --server <url>", "Remote server URL")
171
+ .option("-t, --token <token>", "Auth token for remote server")
172
+ .option("--json", "Output as JSON")
173
+ .option("--envelope", "Wrap JSON output in standard envelope")
174
+ .action(async (id, rawOptions, cmd) => {
175
+ const options = getEffectiveOptions(rawOptions, cmd);
176
+ if (!id) {
177
+ throw new ArgumentError("Playbook ID is required for show");
178
+ }
179
+ // 1. 远端服务分支
180
+ const target = resolveTarget({
181
+ profile: options.profile,
182
+ server: options.server,
183
+ token: options.token,
184
+ }, context?.customHome);
185
+ if (target.type === "remote") {
186
+ const detail = await fetchRemotePlaybookShow(target.serverUrl, id, target.token);
187
+ renderResult(detail, {
188
+ json: options.json,
189
+ envelope: options.envelope,
190
+ humanFormatter: () => renderPlaybookDetail(detail),
191
+ context,
192
+ });
193
+ return;
194
+ }
195
+ // 2. 本地项目模式
196
+ let showTarget = id;
197
+ if (options.package && !id.includes("/")) {
198
+ const pkgRoot = resolvePackageRoot(options.package);
199
+ if (!pkgRoot) {
200
+ throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
201
+ }
202
+ showTarget = `${options.package}/${id}`;
203
+ }
204
+ let resolved;
205
+ try {
206
+ resolved = resolvePlaybookProject(showTarget);
207
+ }
208
+ catch (err) {
209
+ throw new ArgumentError(err.message);
210
+ }
211
+ const pb = resolved.playbook;
212
+ const detail = {
213
+ id: pb.id,
214
+ packageId: resolved.packageId,
215
+ description: pb.description,
216
+ actions: pb.actions,
217
+ filePath: pb.filePath,
218
+ content: pb.content,
219
+ };
220
+ renderResult(detail, {
221
+ json: options.json,
222
+ envelope: options.envelope,
223
+ humanFormatter: () => renderPlaybookDetail(detail),
224
+ context,
225
+ });
226
+ });
227
+ // playbook validate
228
+ pbCmd
229
+ .command("validate [id]")
230
+ .description("Validate playbook format and action references (in current project or linked packages)")
231
+ .option("-P, --package <id>", "Target package ID or path")
232
+ .option("--json", "Output as JSON")
233
+ .option("--envelope", "Wrap JSON output in standard envelope")
234
+ .action(async (id, rawOptions, cmd) => {
235
+ const options = getEffectiveOptions(rawOptions, cmd);
236
+ let root = null;
237
+ if (options.package) {
238
+ root = resolvePackageRoot(options.package);
239
+ if (!root) {
240
+ throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
241
+ }
242
+ }
243
+ else {
244
+ root = findProjectRoot();
245
+ }
246
+ const targets = [];
247
+ if (root) {
248
+ const config = loadProjectConfig(root);
249
+ const playbooks = loadPlaybooks(root, config.playbooksDir);
250
+ if (id) {
251
+ const pb = playbooks.get(id);
252
+ if (!pb) {
253
+ throw new ArgumentError(`Playbook '${id}' not found in package '${config.id}'`);
254
+ }
255
+ targets.push({ root, packageId: config.id, playbooks: [pb] });
256
+ }
257
+ else {
258
+ targets.push({ root, packageId: config.id, playbooks: Array.from(playbooks.values()) });
259
+ }
260
+ }
261
+ else if (id) {
262
+ let resolved;
263
+ try {
264
+ resolved = resolvePlaybookProject(id);
265
+ }
266
+ catch (err) {
267
+ throw new ArgumentError(err.message);
268
+ }
269
+ targets.push({
270
+ root: resolved.projectRoot,
271
+ packageId: resolved.packageId,
272
+ playbooks: [resolved.playbook],
273
+ });
274
+ }
275
+ else {
276
+ const linkedList = listLinkedPackages(context?.customHome);
277
+ if (linkedList.length === 0) {
278
+ throw new ArgumentError("Not in an ActionDock project, and no packages linked.");
279
+ }
280
+ for (const pkg of linkedList) {
281
+ if (!existsSync(pkg.path))
282
+ continue;
283
+ try {
284
+ const config = loadProjectConfig(pkg.path);
285
+ const playbooks = loadPlaybooks(pkg.path, config.playbooksDir);
286
+ targets.push({
287
+ root: pkg.path,
288
+ packageId: pkg.id,
289
+ playbooks: Array.from(playbooks.values()),
290
+ });
291
+ }
292
+ catch {
293
+ // 单个链接包清单损坏或 Playbook 加载失败时跳过,不阻断整体校验
294
+ }
295
+ }
296
+ }
297
+ const results = [];
298
+ for (const target of targets) {
299
+ const config = loadProjectConfig(target.root);
300
+ const manifest = loadManifest(target.root);
301
+ const actions = await loadActions(target.root, config.actionsDir);
302
+ for (const pb of target.playbooks) {
303
+ const warnings = [];
304
+ const errors = [];
305
+ if (!pb.id) {
306
+ errors.push("Missing id in playbook");
307
+ }
308
+ if (!pb.description) {
309
+ warnings.push("Missing description in actiondock.json");
310
+ }
311
+ if (pb.actions && Array.isArray(pb.actions)) {
312
+ for (const actRef of pb.actions) {
313
+ if (actRef.includes("/")) {
314
+ try {
315
+ await resolveActionProject(actRef);
316
+ }
317
+ catch (e) {
318
+ errors.push(`Referenced cross-package action '${actRef}' not resolvable: ${e.message}`);
319
+ }
320
+ }
321
+ else {
322
+ const foundInActions = actions.has(actRef);
323
+ const foundInManifest = Boolean(manifest?.actions?.[actRef]);
324
+ if (!foundInActions && !foundInManifest) {
325
+ errors.push(`Referenced action '${actRef}' not found in project actions`);
326
+ }
327
+ }
328
+ }
329
+ }
330
+ results.push({
331
+ id: pb.id,
332
+ packageId: target.packageId,
333
+ valid: errors.length === 0,
334
+ warnings,
335
+ errors,
336
+ });
337
+ }
338
+ }
339
+ const allValid = results.every((r) => r.valid);
340
+ renderResult({ valid: allValid, results }, {
341
+ json: options.json,
342
+ envelope: options.envelope,
343
+ humanFormatter: () => {
344
+ const lines = [];
345
+ for (const r of results) {
346
+ const tag = r.valid ? "[OK]" : "[FAIL]";
347
+ lines.push(`${tag} ${r.packageId}/${r.id}: ${r.valid ? "Valid" : "Invalid"}`);
348
+ for (const e of r.errors) {
349
+ lines.push(` Error: ${e}`);
350
+ }
351
+ for (const w of r.warnings) {
352
+ lines.push(` Warning: ${w}`);
353
+ }
354
+ }
355
+ return lines.join("\n");
356
+ },
357
+ context,
358
+ });
359
+ if (!allValid) {
360
+ process.exitCode = 1;
361
+ return;
362
+ }
363
+ });
364
+ // playbook create / new
365
+ pbCmd
366
+ .command("create <id>")
367
+ .alias("new")
368
+ .description("Scaffold a new Playbook markdown file")
369
+ .option("-d, --desc <description>", "Playbook description")
370
+ .option("-a, --actions <actions...>", "Referenced action IDs")
371
+ .option("-f, --file <filePath>", "Target file path relative to playbooks dir")
372
+ .action((id, options) => {
373
+ handlePlaybookCreate(id, options, context);
374
+ });
375
+ }
376
+ export function handlePlaybookCreate(id, options, context) {
377
+ const root = findProjectRoot();
378
+ if (!root) {
379
+ throw new ExecutionError("Not in an ActionDock project (actiondock.json not found)");
380
+ }
381
+ try {
382
+ const config = loadProjectConfig(root);
383
+ const pbDir = resolve(root, config.playbooksDir || "playbooks");
384
+ if (!existsSync(pbDir)) {
385
+ mkdirSync(pbDir, { recursive: true });
386
+ }
387
+ if (options.file && isAbsolute(options.file)) {
388
+ throw new ExecutionError(`--file option must be a relative path, received: ${options.file}`);
389
+ }
390
+ const cleanName = id.replace(/[^a-zA-Z0-9-_]/g, "-");
391
+ const targetRelFile = options.file || `${cleanName}.md`;
392
+ const targetFullFile = resolve(pbDir, targetRelFile);
393
+ assertPathWithinRoot(pbDir, targetFullFile, "playbook file");
394
+ if (existsSync(targetFullFile)) {
395
+ throw new ExecutionError(`File '${targetFullFile}' already exists`);
396
+ }
397
+ mkdirSync(dirname(targetFullFile), { recursive: true });
398
+ const desc = options.desc || `SOP guide for ${id}`;
399
+ const actionsList = Array.isArray(options.actions) ? options.actions : [];
400
+ const template = `# ${id.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} SOP
401
+
402
+ This playbook provides task execution guidance for AI Agents.
403
+
404
+ ## Instructions
405
+
406
+ - Inspect available actions with \`<binary> list --json\`.
407
+ - Follow the required steps to complete the task.
408
+ `;
409
+ writeFileSync(targetFullFile, template, "utf-8");
410
+ const manifest = loadManifest(root) || {
411
+ $schema: "https://actiondock.dev/schema/v2/actiondock.json",
412
+ id: config.id,
413
+ name: config.name,
414
+ version: config.version,
415
+ playbooks: {},
416
+ };
417
+ manifest.playbooks = manifest.playbooks || {};
418
+ const relEntry = relative(root, targetFullFile).replace(/\\/g, "/");
419
+ manifest.playbooks[id] = {
420
+ entry: relEntry,
421
+ description: desc,
422
+ actions: actionsList,
423
+ };
424
+ saveManifest(root, manifest);
425
+ writeStdout(`[OK] Created Playbook '${id}' at ${targetFullFile}`, context);
426
+ }
427
+ catch (err) {
428
+ if (err instanceof CliError) {
429
+ throw err;
430
+ }
431
+ throw new ExecutionError(err.message);
432
+ }
433
+ }
@@ -0,0 +1,253 @@
1
+ import { addProfile, checkRemoteHealth, filterWithFallbackInfo, getProfile, listProfiles, loadProfiles, maskSecretValue, removeProfile, resolveProfileToken, resolveTarget, toSnakeUpperCase, useProfile, } from "@actiondock/core";
2
+ import { ArgumentError, CliError, ExecutionError, } from "../errors.js";
3
+ import { renderResult, writeStdout } from "../renderer.js";
4
+ import { getEffectiveOptions, resolveFallbackStrategy, resolveIntent } from "../utils/index.js";
5
+ export function registerProfileCommands(program) {
6
+ const profileCmd = program
7
+ .command("profile")
8
+ .description("Manage multi-cloud and remote execution profiles");
9
+ // ad profile list
10
+ profileCmd
11
+ .command("list [patterns...]")
12
+ .description("List all configured profiles")
13
+ .option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
14
+ .option("--reveal, --show-secrets", "Reveal plain text values for tokens")
15
+ .option("--fallback", "Enable fallback to full list when no items match intent")
16
+ .option("--no-fallback", "Disable fallback to full list when no items match intent")
17
+ .option("--json", "Output as JSON")
18
+ .option("--envelope", "Wrap JSON output in standard envelope")
19
+ .action((patterns, rawOptions, cmd) => {
20
+ try {
21
+ const options = getEffectiveOptions(rawOptions, cmd);
22
+ const effectiveIntent = resolveIntent(options.intent, patterns);
23
+ const { isMachine, shouldFallback } = resolveFallbackStrategy(options);
24
+ const reveal = Boolean(options.reveal || options.showSecrets);
25
+ const list = listProfiles();
26
+ const enriched = list.map((p) => {
27
+ const resolved = resolveProfileToken(p.name, p.entry);
28
+ return {
29
+ name: p.name,
30
+ isCurrent: p.isCurrent,
31
+ serverUrl: p.entry.serverUrl,
32
+ description: p.entry.description || "",
33
+ tokenEnv: p.entry.tokenEnv,
34
+ tokenConfigured: resolved.source !== "none",
35
+ tokenSource: resolved.source,
36
+ token: reveal ? resolved.token : (resolved.token ? maskSecretValue(resolved.token) : undefined),
37
+ };
38
+ });
39
+ const filterRes = filterWithFallbackInfo(enriched, effectiveIntent, [(p) => p.name, (p) => p.serverUrl, (p) => p.description, (p) => p.tokenSource], shouldFallback);
40
+ if (filterRes.isFallback && isMachine) {
41
+ renderResult({ items: filterRes.items, isFallback: true, matchedCount: 0 }, { json: options.json, envelope: options.envelope });
42
+ return;
43
+ }
44
+ renderResult(filterRes.items, {
45
+ json: options.json,
46
+ envelope: options.envelope,
47
+ humanFormatter: () => {
48
+ const lines = ["ActionDock Execution Profiles:\n"];
49
+ if (filterRes.isFallback && effectiveIntent) {
50
+ lines.push(`(No profiles matched intent '${effectiveIntent}', showing all profiles)\n`);
51
+ }
52
+ for (const item of filterRes.items) {
53
+ const currentMarker = item.isCurrent ? "* " : " ";
54
+ let tokenInfo = "";
55
+ if (item.tokenSource === "tokenEnv") {
56
+ tokenInfo = ` [token: env(${item.tokenEnv})]`;
57
+ }
58
+ else if (item.tokenSource === "profileEnv") {
59
+ tokenInfo = ` [token: env(ACTIONDOCK_${toSnakeUpperCase(item.name)}_TOKEN)]`;
60
+ }
61
+ else if (item.tokenSource === "profile") {
62
+ tokenInfo = ` [token: stored in profile]`;
63
+ }
64
+ else if (item.tokenSource === "globalEnv") {
65
+ tokenInfo = ` [token: env(ACTIONDOCK_TOKEN)]`;
66
+ }
67
+ if (reveal && item.token) {
68
+ tokenInfo += ` = ${item.token}`;
69
+ }
70
+ const desc = item.description ? ` - ${item.description}` : "";
71
+ lines.push(`${currentMarker}${item.name.padEnd(20)} ${item.serverUrl}${tokenInfo}${desc}`);
72
+ }
73
+ lines.push("\nUse 'ad profile use <name>' to switch or 'ad run <action> --profile <name>' to execute on a specific target.");
74
+ return lines.join("\n");
75
+ },
76
+ });
77
+ }
78
+ catch (err) {
79
+ if (err instanceof ArgumentError || err instanceof CliError)
80
+ throw err;
81
+ throw new ExecutionError(err.message);
82
+ }
83
+ });
84
+ // ad profile add <name>
85
+ profileCmd
86
+ .command("add <name>")
87
+ .description("Add or update a remote execution profile")
88
+ .requiredOption("-s, --server <url>", "Remote ActionDock server URL (e.g. http://1.2.3.4:5177)")
89
+ .option("-t, --token <token>", "Authentication token for the remote server (deprecated; prefer --token-env)")
90
+ .option("--token-env <env>", "Environment variable name containing the authentication token")
91
+ .option("-d, --desc <description>", "Description of this profile/machine")
92
+ .action((name, options) => {
93
+ try {
94
+ if (options.token) {
95
+ console.warn("Warning: storing tokens directly in profiles.json is deprecated. Use --token-env or standard environment variables (e.g. ACTIONDOCK_<PROFILE>_TOKEN) instead.");
96
+ }
97
+ addProfile(name, {
98
+ serverUrl: options.server,
99
+ token: options.token,
100
+ tokenEnv: options.tokenEnv,
101
+ description: options.desc,
102
+ });
103
+ writeStdout(`[OK] Profile '${name}' configured for server: ${options.server}`);
104
+ }
105
+ catch (err) {
106
+ throw new ExecutionError(err.message);
107
+ }
108
+ });
109
+ // ad profile use <name>
110
+ profileCmd
111
+ .command("use <name>")
112
+ .description("Switch active default profile")
113
+ .action((name) => {
114
+ try {
115
+ useProfile(name);
116
+ writeStdout(`[OK] Active profile switched to '${name}'`);
117
+ }
118
+ catch (err) {
119
+ throw new ExecutionError(err.message);
120
+ }
121
+ });
122
+ // ad profile show [name]
123
+ profileCmd
124
+ .command("show [name]")
125
+ .description("Display details of a profile (defaults to active profile)")
126
+ .option("--reveal, --show-secrets", "Reveal plain text values for tokens")
127
+ .option("--json", "Output as JSON")
128
+ .option("--envelope", "Wrap JSON output in standard envelope")
129
+ .action((name, rawOptions, cmd) => {
130
+ try {
131
+ const options = getEffectiveOptions(rawOptions, cmd);
132
+ const config = loadProfiles();
133
+ const targetName = name || config.currentProfile || "local";
134
+ const entry = getProfile(targetName);
135
+ const reveal = Boolean(options.reveal || options.showSecrets);
136
+ if (!entry && targetName !== "local") {
137
+ throw new ArgumentError(`Profile '${targetName}' not found.`);
138
+ }
139
+ const resolved = resolveProfileToken(targetName, entry);
140
+ const displayToken = reveal
141
+ ? resolved.token
142
+ : (resolved.token ? maskSecretValue(resolved.token) : undefined);
143
+ const data = {
144
+ name: targetName,
145
+ isCurrent: config.currentProfile === targetName,
146
+ serverUrl: entry?.serverUrl || "local",
147
+ tokenConfigured: resolved.source !== "none",
148
+ tokenSource: resolved.source,
149
+ tokenEnv: entry?.tokenEnv,
150
+ token: displayToken,
151
+ description: entry?.description || "",
152
+ };
153
+ renderResult(data, {
154
+ json: options.json,
155
+ envelope: options.envelope,
156
+ humanFormatter: () => {
157
+ const lines = [];
158
+ lines.push(`Profile: ${data.name}${data.isCurrent ? " (Active)" : ""}`);
159
+ lines.push(`Server URL: ${data.serverUrl}`);
160
+ let sourceDetail = "None";
161
+ if (data.tokenSource === "tokenEnv") {
162
+ sourceDetail = `Environment Variable ($${data.tokenEnv})`;
163
+ }
164
+ else if (data.tokenSource === "profileEnv") {
165
+ sourceDetail = `Profile Environment Variable ($ACTIONDOCK_${toSnakeUpperCase(data.name)}_TOKEN)`;
166
+ }
167
+ else if (data.tokenSource === "profile") {
168
+ sourceDetail = "Stored in profiles.json (Deprecated)";
169
+ }
170
+ else if (data.tokenSource === "globalEnv") {
171
+ sourceDetail = "Global Environment Variable ($ACTIONDOCK_TOKEN)";
172
+ }
173
+ lines.push(`Auth Source: ${sourceDetail}`);
174
+ if (data.tokenConfigured) {
175
+ lines.push(`Token Value: ${data.token}`);
176
+ }
177
+ if (data.description) {
178
+ lines.push(`Description: ${data.description}`);
179
+ }
180
+ return lines.join("\n");
181
+ },
182
+ });
183
+ }
184
+ catch (err) {
185
+ if (err instanceof ArgumentError || err instanceof CliError) {
186
+ throw err;
187
+ }
188
+ throw new ExecutionError(err.message);
189
+ }
190
+ });
191
+ // ad profile rm / remove <name>
192
+ profileCmd
193
+ .command("rm <name>")
194
+ .alias("remove")
195
+ .description("Remove a profile")
196
+ .action((name) => {
197
+ try {
198
+ if (name === "local") {
199
+ throw new ExecutionError("Cannot remove built-in 'local' profile");
200
+ }
201
+ const removed = removeProfile(name);
202
+ if (!removed) {
203
+ throw new ExecutionError(`Profile '${name}' not found`);
204
+ }
205
+ writeStdout(`[OK] Profile '${name}' removed`);
206
+ }
207
+ catch (err) {
208
+ if (err instanceof CliError) {
209
+ throw err;
210
+ }
211
+ throw new ExecutionError(err.message);
212
+ }
213
+ });
214
+ // ad profile test [name]
215
+ profileCmd
216
+ .command("test [name]")
217
+ .description("Test connection latency and health of a profile")
218
+ .option("--json", "Output as JSON")
219
+ .option("--envelope", "Wrap JSON output in standard envelope")
220
+ .action(async (name, rawOptions, cmd) => {
221
+ try {
222
+ const options = getEffectiveOptions(rawOptions, cmd);
223
+ const target = resolveTarget({ profile: name });
224
+ if (target.type === "local") {
225
+ renderResult({ ok: true, type: "local", message: "Local execution" }, {
226
+ json: options.json,
227
+ envelope: options.envelope,
228
+ humanFormatter: () => `Target profile '${target.profileName || "local"}' is local (runs in local runtime).`,
229
+ });
230
+ return;
231
+ }
232
+ const health = await checkRemoteHealth(target.serverUrl, target.token);
233
+ renderResult(health, {
234
+ json: options.json,
235
+ envelope: options.envelope,
236
+ humanFormatter: () => {
237
+ if (health.ok) {
238
+ return `[OK] Connected to ${target.serverUrl} (${health.latencyMs}ms) - Version: ${health.version}, Status: ${health.status}`;
239
+ }
240
+ return `[FAIL] Connection to ${target.serverUrl} failed (${health.latencyMs}ms): ${health.error}`;
241
+ },
242
+ });
243
+ if (!health.ok) {
244
+ process.exitCode = 1;
245
+ }
246
+ }
247
+ catch (err) {
248
+ if (err instanceof ArgumentError || err instanceof CliError)
249
+ throw err;
250
+ throw new ExecutionError(err.message);
251
+ }
252
+ });
253
+ }