@actiondock/cli 2.0.11 → 2.2.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.
Files changed (123) hide show
  1. package/README.md +41 -21
  2. package/bin/ad.js +29 -31
  3. package/dist/commands/action/create.d.ts +10 -0
  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 +3 -0
  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 +3 -0
  26. package/dist/commands/doctor.js +82 -0
  27. package/dist/commands/export.d.ts +3 -0
  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 +32 -0
  32. package/dist/commands/index.js +111 -0
  33. package/dist/commands/info.d.ts +9 -0
  34. package/dist/commands/info.js +210 -0
  35. package/dist/commands/init.d.ts +2 -0
  36. package/dist/commands/init.js +24 -0
  37. package/dist/commands/link.d.ts +2 -0
  38. package/dist/commands/link.js +71 -0
  39. package/dist/commands/list.d.ts +9 -0
  40. package/dist/commands/list.js +77 -0
  41. package/dist/commands/mcp.d.ts +9 -0
  42. package/dist/commands/mcp.js +172 -0
  43. package/dist/commands/new.d.ts +9 -0
  44. package/dist/commands/new.js +33 -0
  45. package/dist/commands/pack.d.ts +3 -0
  46. package/dist/commands/pack.js +63 -0
  47. package/dist/commands/playbook.d.ts +10 -0
  48. package/dist/commands/playbook.js +433 -0
  49. package/dist/commands/profile.d.ts +2 -0
  50. package/dist/commands/profile.js +253 -0
  51. package/dist/commands/remove.d.ts +7 -0
  52. package/dist/commands/remove.js +118 -0
  53. package/dist/commands/run.d.ts +13 -0
  54. package/dist/commands/run.js +146 -0
  55. package/dist/commands/runs.d.ts +9 -0
  56. package/dist/commands/runs.js +245 -0
  57. package/dist/commands/serve.d.ts +9 -0
  58. package/dist/commands/serve.js +129 -0
  59. package/dist/commands/state-list.d.ts +24 -0
  60. package/dist/commands/state-list.js +82 -0
  61. package/dist/commands/state.d.ts +9 -0
  62. package/dist/commands/state.js +310 -0
  63. package/dist/commands/test.d.ts +2 -0
  64. package/dist/commands/test.js +44 -0
  65. package/dist/commands/validate.d.ts +9 -0
  66. package/dist/commands/validate.js +106 -0
  67. package/dist/errors.d.ts +43 -0
  68. package/dist/errors.js +103 -0
  69. package/dist/index.d.ts +15 -0
  70. package/dist/index.js +47 -65969
  71. package/dist/prompt.d.ts +53 -0
  72. package/dist/prompt.js +192 -0
  73. package/dist/renderer.d.ts +197 -0
  74. package/dist/renderer.js +502 -0
  75. package/dist/services/index.d.ts +1 -0
  76. package/dist/services/index.js +1 -0
  77. package/dist/services/inspect.d.ts +23 -0
  78. package/dist/services/inspect.js +96 -0
  79. package/dist/standalone/index.d.ts +16 -0
  80. package/dist/standalone/index.js +22 -0
  81. package/dist/types.d.ts +132 -0
  82. package/dist/types.js +9 -0
  83. package/dist/utils/bytes.d.ts +1 -0
  84. package/dist/utils/bytes.js +1 -0
  85. package/dist/utils/duration.js +1 -0
  86. package/dist/utils/filter.d.ts +1 -0
  87. package/dist/utils/filter.js +1 -0
  88. package/dist/utils/index.d.ts +6 -0
  89. package/dist/utils/index.js +6 -0
  90. package/dist/utils/spawn.d.ts +27 -0
  91. package/dist/utils/spawn.js +35 -0
  92. package/dist/utils/target.d.ts +45 -0
  93. package/dist/utils/target.js +78 -0
  94. package/dist/utils/utils.d.ts +38 -0
  95. package/dist/utils/utils.js +111 -0
  96. package/package.json +20 -17
  97. package/src/commands/action/create.ts +0 -129
  98. package/src/commands/action/index.ts +0 -27
  99. package/src/commands/action/list.ts +0 -247
  100. package/src/commands/action/run.ts +0 -197
  101. package/src/commands/action/show.ts +0 -128
  102. package/src/commands/action/sync.ts +0 -76
  103. package/src/commands/action/validate.ts +0 -102
  104. package/src/commands/action.ts +0 -1
  105. package/src/commands/build.ts +0 -56
  106. package/src/commands/config.ts +0 -583
  107. package/src/commands/doctor.ts +0 -84
  108. package/src/commands/export.ts +0 -196
  109. package/src/commands/index.ts +0 -75
  110. package/src/commands/info.ts +0 -537
  111. package/src/commands/init.ts +0 -24
  112. package/src/commands/link.ts +0 -73
  113. package/src/commands/mcp.ts +0 -195
  114. package/src/commands/playbook.ts +0 -566
  115. package/src/commands/profile.ts +0 -296
  116. package/src/commands/runs.ts +0 -452
  117. package/src/commands/serve.ts +0 -132
  118. package/src/commands/state.ts +0 -590
  119. package/src/commands/test.ts +0 -48
  120. package/src/index.ts +0 -58
  121. package/src/utils/bytes.ts +0 -2
  122. package/src/utils/filter.ts +0 -2
  123. /package/{src/utils/duration.ts → dist/utils/duration.d.ts} +0 -0
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import { Command } from "commander";
2
+ import type { CliContext } from "../types.js";
3
+ /**
4
+ * 注册 ad remove 依赖移除命令。
5
+ * 遵循规范:移除依赖并更新锁文件,保留该包的历史存储数据,检查反向引用防止误删。
6
+ */
7
+ export declare function registerRemoveCommand(program: Command, context?: CliContext): void;
@@ -0,0 +1,118 @@
1
+ import { resolve } from "node:path";
2
+ import { beginTransaction, findProjectRoot, getInstallCommand, loadLockfile, loadManifest, saveLockfile, saveManifest, } from "@actiondock/core";
3
+ import { ArgumentError, ExecutionError } from "../errors.js";
4
+ import { renderResult } from "../renderer.js";
5
+ import { getEffectiveOptions, spawnAsync } from "../utils/index.js";
6
+ /**
7
+ * 注册 ad remove 依赖移除命令。
8
+ * 遵循规范:移除依赖并更新锁文件,保留该包的历史存储数据,检查反向引用防止误删。
9
+ */
10
+ export function registerRemoveCommand(program, context) {
11
+ program
12
+ .command("remove <package>")
13
+ .description("Remove an Action package dependency and update actiondock.lock.json")
14
+ .option("-P, --package <path>", "Target project directory path")
15
+ .option("--json", "Output as JSON")
16
+ .option("--envelope", "Wrap JSON output in standard envelope")
17
+ .action(async (packageIdentifier, rawOptions, cmd) => {
18
+ const options = getEffectiveOptions(rawOptions, cmd);
19
+ const root = options.package ? resolve(options.package) : findProjectRoot();
20
+ if (!root) {
21
+ throw new ArgumentError("Not in an ActionDock project (actiondock.json not found).\nPlease specify -P, --package <path> or cd into a project directory.");
22
+ }
23
+ const manifest = loadManifest(root);
24
+ if (!manifest) {
25
+ throw new ArgumentError(`actiondock.json not found in ${root}`);
26
+ }
27
+ // 寻址逻辑包标识
28
+ let targetPackageId = null;
29
+ let npmPackageName = packageIdentifier;
30
+ if (manifest.dependencies?.[packageIdentifier]) {
31
+ targetPackageId = packageIdentifier;
32
+ npmPackageName = manifest.dependencies[packageIdentifier];
33
+ }
34
+ else if (manifest.dependencies) {
35
+ for (const [id, spec] of Object.entries(manifest.dependencies)) {
36
+ if (spec === packageIdentifier || id === packageIdentifier) {
37
+ targetPackageId = id;
38
+ npmPackageName = spec;
39
+ break;
40
+ }
41
+ }
42
+ }
43
+ if (!targetPackageId) {
44
+ throw new ArgumentError(`Package '${packageIdentifier}' is not a declared dependency in actiondock.json`);
45
+ }
46
+ // 检查当前工程中 Action 的反向 uses 依赖声明
47
+ if (manifest.actions) {
48
+ for (const [actionId, actionEntry] of Object.entries(manifest.actions)) {
49
+ if (actionEntry.uses && Array.isArray(actionEntry.uses)) {
50
+ const hasRef = actionEntry.uses.some((u) => u === targetPackageId ||
51
+ u === `${targetPackageId}/*` ||
52
+ u.startsWith(`${targetPackageId}/`));
53
+ if (hasRef) {
54
+ throw new ExecutionError(`Cannot remove package '${targetPackageId}': action '${actionId}' declares dependency on it in 'uses'. Please remove the 'uses' declaration first.`, undefined, "DEPENDENCY_CONFLICT");
55
+ }
56
+ }
57
+ }
58
+ }
59
+ // 检查当前工程中 Playbook 的反向动作引用
60
+ if (manifest.playbooks) {
61
+ for (const [pbId, pbEntry] of Object.entries(manifest.playbooks)) {
62
+ if (pbEntry.actions && Array.isArray(pbEntry.actions)) {
63
+ const hasRef = pbEntry.actions.some((a) => a === targetPackageId ||
64
+ a === `${targetPackageId}/*` ||
65
+ a.startsWith(`${targetPackageId}/`));
66
+ if (hasRef) {
67
+ throw new ExecutionError(`Cannot remove package '${targetPackageId}': playbook '${pbId}' references it in 'actions'. Please update the playbook first.`, undefined, "DEPENDENCY_CONFLICT");
68
+ }
69
+ }
70
+ }
71
+ }
72
+ // 获取排他锁与事务快照保护
73
+ const tx = await beginTransaction(root, `ad remove ${packageIdentifier}`);
74
+ try {
75
+ // 从 actiondock.json 中移除依赖声明
76
+ if (manifest.dependencies) {
77
+ delete manifest.dependencies[targetPackageId];
78
+ if (Object.keys(manifest.dependencies).length === 0) {
79
+ delete manifest.dependencies;
80
+ }
81
+ saveManifest(root, manifest);
82
+ }
83
+ // 从 actiondock.lock.json 中移除锁定记录
84
+ const lockfile = loadLockfile(root);
85
+ if (lockfile && lockfile.packages[targetPackageId]) {
86
+ delete lockfile.packages[targetPackageId];
87
+ saveLockfile(root, lockfile);
88
+ }
89
+ // 调用包管理器执行卸载
90
+ const installCmd = getInstallCommand(root);
91
+ const pm = installCmd[0];
92
+ const args = pm === "bun" ? ["remove", npmPackageName] : ["uninstall", npmPackageName, "--ignore-scripts"];
93
+ const uninstallProc = await spawnAsync(pm, args, {
94
+ cwd: root,
95
+ stdio: "pipe",
96
+ });
97
+ // 卸载失败不阻断事务提交(与原同步实现一致:忽略退出码)
98
+ void uninstallProc;
99
+ // 提交事务
100
+ await tx.commit();
101
+ const resultPayload = {
102
+ packageId: targetPackageId,
103
+ npmPackage: npmPackageName,
104
+ retainedNamespace: targetPackageId,
105
+ };
106
+ renderResult(resultPayload, {
107
+ json: options.json,
108
+ envelope: options.envelope,
109
+ humanFormatter: () => `Successfully removed '${resultPayload.packageId}'.\n[INFO] Retained configuration and state namespace for package '${resultPayload.packageId}'.`,
110
+ context,
111
+ });
112
+ }
113
+ catch (err) {
114
+ await tx.rollback({ frozenInstall: false });
115
+ throw new ExecutionError(err.message, err, err.code || "REMOVE_DEPENDENCY_FAILED");
116
+ }
117
+ });
118
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from "commander";
2
+ import type { CliContext } from "../types.js";
3
+ /**
4
+ * 统一执行 Action 核心逻辑(使用 ActionDockTarget 门面)。
5
+ */
6
+ export declare function executeAction(id: string, options: any, context?: CliContext): Promise<void>;
7
+ /**
8
+ * 注册顶层统一 run 命令:执行指定 Action。
9
+ *
10
+ * @param program Commander 根程序对象
11
+ * @param context 命令行上下文
12
+ */
13
+ export declare function registerRunCommand(program: Command, context?: CliContext): void;
@@ -0,0 +1,146 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parseDuration, resolveTarget } from "@actiondock/core";
3
+ import { ArgumentError, ExecutionError, SigintError } from "../errors.js";
4
+ import { writeStdout } from "../renderer.js";
5
+ import { getEffectiveOptions, resolveLocalPackageRoot, withTarget } from "../utils/index.js";
6
+ /**
7
+ * 统一执行 Action 核心逻辑(使用 ActionDockTarget 门面)。
8
+ */
9
+ export async function executeAction(id, options, context) {
10
+ if (!id) {
11
+ throw new ArgumentError("Action ID is required for run");
12
+ }
13
+ let input = {};
14
+ if (options.input) {
15
+ try {
16
+ input = JSON.parse(options.input);
17
+ }
18
+ catch (err) {
19
+ throw new ArgumentError(`Error parsing --input JSON: ${err.message}`);
20
+ }
21
+ }
22
+ else if (options.inputFile) {
23
+ try {
24
+ input = JSON.parse(readFileSync(options.inputFile, "utf-8"));
25
+ }
26
+ catch (err) {
27
+ throw new ArgumentError(`Error reading --input-file: ${err.message}`);
28
+ }
29
+ }
30
+ let timeoutMs;
31
+ if (options.timeout) {
32
+ try {
33
+ timeoutMs = parseDuration(options.timeout);
34
+ }
35
+ catch (err) {
36
+ throw new ArgumentError(`Invalid timeout format: ${err.message}`);
37
+ }
38
+ }
39
+ const configOverrides = {};
40
+ if (options.config) {
41
+ const list = Array.isArray(options.config) ? options.config : [options.config];
42
+ for (const item of list) {
43
+ const [k, ...v] = item.split("=");
44
+ if (k)
45
+ configOverrides[k] = v.join("=");
46
+ }
47
+ }
48
+ const controller = new AbortController();
49
+ let receivedSigint = false;
50
+ const sigintHandler = () => {
51
+ receivedSigint = true;
52
+ controller.abort(new Error("Interrupted by SIGINT"));
53
+ };
54
+ process.once("SIGINT", sigintHandler);
55
+ try {
56
+ // 异步执行仅支持远端目标,本地模式直接拒绝
57
+ const resolvedTopology = resolveTarget({
58
+ profile: options.profile,
59
+ server: options.server,
60
+ token: options.token,
61
+ }, context?.customHome);
62
+ if (resolvedTopology.type === "local" && options.async) {
63
+ throw new ExecutionError("Async execution requires a long-running ActionDock server.\nUse --profile, --server, or start 'ad serve'.");
64
+ }
65
+ // 目标拓扑解析(仅 local 分支需要包寻址)
66
+ const targetPackageRoot = resolveLocalPackageRoot(options.package);
67
+ if (options.package && !targetPackageRoot) {
68
+ throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
69
+ }
70
+ let targetRef = id;
71
+ if (options.package && !id.includes("/") && !id.includes(":")) {
72
+ targetRef = `${options.package}/${id}`;
73
+ }
74
+ // 通过 Target 统一执行
75
+ await withTarget(options, context, async (target) => {
76
+ if (options.async) {
77
+ const ticket = await target.startAction(targetRef, input, {
78
+ signal: controller.signal,
79
+ timeoutMs,
80
+ config: configOverrides,
81
+ requestId: options.requestId,
82
+ });
83
+ const asyncOutput = {
84
+ ok: ticket.status !== "failed",
85
+ runId: ticket.runId,
86
+ status: ticket.status,
87
+ };
88
+ writeStdout(JSON.stringify(asyncOutput, null, 2), context);
89
+ if (ticket.status === "failed") {
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+ }
94
+ else {
95
+ const result = await target.runAction(targetRef, input, {
96
+ signal: controller.signal,
97
+ timeoutMs,
98
+ config: configOverrides,
99
+ requestId: options.requestId,
100
+ });
101
+ writeStdout(JSON.stringify(result, null, 2), context);
102
+ if (!result.ok) {
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ }
107
+ }, { localRoot: targetPackageRoot || undefined, scanLinkedPackages: true });
108
+ }
109
+ catch (err) {
110
+ if (receivedSigint || err?.name === "AbortError" || err?.message?.includes("SIGINT")) {
111
+ throw new SigintError();
112
+ }
113
+ throw err;
114
+ }
115
+ finally {
116
+ process.removeListener("SIGINT", sigintHandler);
117
+ }
118
+ }
119
+ /**
120
+ * 注册顶层统一 run 命令:执行指定 Action。
121
+ *
122
+ * @param program Commander 根程序对象
123
+ * @param context 命令行上下文
124
+ */
125
+ export function registerRunCommand(program, context) {
126
+ program
127
+ .command("run <id>")
128
+ .description("Execute an action (from current project, linked packages, or remote profile)")
129
+ .option("-P, --package <id>", "Target package ID or path")
130
+ .option("-i, --input <json>", "Input as JSON string")
131
+ .option("-f, --input-file <path>", "Input from JSON file")
132
+ .option("-c, --config <key=value...>", "Temporary config override (repeatable)")
133
+ .option("-p, --profile <name>", "Execute against a specific profile")
134
+ .option("-s, --server <url>", "Remote server URL")
135
+ .option("-t, --token <token>", "Auth token for remote server")
136
+ .option("--timeout <duration>", "Execution timeout (e.g. 30s, 5m, 500ms)")
137
+ .option("--request-id <id>", "Idempotency request ID for deduplication")
138
+ .option("--async", "Execute asynchronously in background (requires remote server or profile)")
139
+ .option("--data-dir <path>", "Custom database directory")
140
+ .option("--json", "Output as JSON")
141
+ .option("--envelope", "Wrap JSON output in standard envelope")
142
+ .action(async (id, rawOptions, cmd) => {
143
+ const options = getEffectiveOptions(rawOptions, cmd);
144
+ await executeAction(id, options, context);
145
+ });
146
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from "commander";
2
+ import type { CliContext } from "../types.js";
3
+ /**
4
+ * 注册 runs 动作执行历史管理命令(list、show、clear、cancel)。
5
+ *
6
+ * @param program Commander 实例
7
+ * @param context 命令行上下文
8
+ */
9
+ export declare function registerRunsCommands(program: Command, context?: CliContext): void;