@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.
- package/README.md +41 -21
- package/bin/ad.js +29 -31
- package/dist/commands/action/create.d.ts +10 -0
- package/dist/commands/action/create.js +120 -0
- package/dist/commands/add.d.ts +7 -0
- package/dist/commands/add.js +144 -0
- package/dist/commands/build.d.ts +3 -0
- package/dist/commands/build.js +83 -0
- package/dist/commands/config/delete.d.ts +9 -0
- package/dist/commands/config/delete.js +52 -0
- package/dist/commands/config/env.d.ts +9 -0
- package/dist/commands/config/env.js +85 -0
- package/dist/commands/config/get.d.ts +9 -0
- package/dist/commands/config/get.js +120 -0
- package/dist/commands/config/index.d.ts +9 -0
- package/dist/commands/config/index.js +23 -0
- package/dist/commands/config/list.d.ts +9 -0
- package/dist/commands/config/list.js +137 -0
- package/dist/commands/config/schema.d.ts +9 -0
- package/dist/commands/config/schema.js +121 -0
- package/dist/commands/config/set.d.ts +9 -0
- package/dist/commands/config/set.js +85 -0
- package/dist/commands/describe.d.ts +9 -0
- package/dist/commands/describe.js +68 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +82 -0
- package/dist/commands/export.d.ts +3 -0
- package/dist/commands/export.js +212 -0
- package/dist/commands/generate.d.ts +9 -0
- package/dist/commands/generate.js +44 -0
- package/dist/commands/index.d.ts +32 -0
- package/dist/commands/index.js +111 -0
- package/dist/commands/info.d.ts +9 -0
- package/dist/commands/info.js +210 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +24 -0
- package/dist/commands/link.d.ts +2 -0
- package/dist/commands/link.js +71 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.js +77 -0
- package/dist/commands/mcp.d.ts +9 -0
- package/dist/commands/mcp.js +172 -0
- package/dist/commands/new.d.ts +9 -0
- package/dist/commands/new.js +33 -0
- package/dist/commands/pack.d.ts +3 -0
- package/dist/commands/pack.js +63 -0
- package/dist/commands/playbook.d.ts +10 -0
- package/dist/commands/playbook.js +433 -0
- package/dist/commands/profile.d.ts +2 -0
- package/dist/commands/profile.js +253 -0
- package/dist/commands/remove.d.ts +7 -0
- package/dist/commands/remove.js +118 -0
- package/dist/commands/run.d.ts +13 -0
- package/dist/commands/run.js +146 -0
- package/dist/commands/runs.d.ts +9 -0
- package/dist/commands/runs.js +245 -0
- package/dist/commands/serve.d.ts +9 -0
- package/dist/commands/serve.js +129 -0
- package/dist/commands/state-list.d.ts +24 -0
- package/dist/commands/state-list.js +82 -0
- package/dist/commands/state.d.ts +9 -0
- package/dist/commands/state.js +310 -0
- package/dist/commands/test.d.ts +2 -0
- package/dist/commands/test.js +44 -0
- package/dist/commands/validate.d.ts +9 -0
- package/dist/commands/validate.js +106 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.js +103 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +47 -65969
- package/dist/prompt.d.ts +53 -0
- package/dist/prompt.js +192 -0
- package/dist/renderer.d.ts +197 -0
- package/dist/renderer.js +502 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/inspect.d.ts +23 -0
- package/dist/services/inspect.js +96 -0
- package/dist/standalone/index.d.ts +16 -0
- package/dist/standalone/index.js +22 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +9 -0
- package/dist/utils/bytes.d.ts +1 -0
- package/dist/utils/bytes.js +1 -0
- package/dist/utils/duration.js +1 -0
- package/dist/utils/filter.d.ts +1 -0
- package/dist/utils/filter.js +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/spawn.d.ts +27 -0
- package/dist/utils/spawn.js +35 -0
- package/dist/utils/target.d.ts +45 -0
- package/dist/utils/target.js +78 -0
- package/dist/utils/utils.d.ts +38 -0
- package/dist/utils/utils.js +111 -0
- package/package.json +20 -17
- package/src/commands/action/create.ts +0 -129
- package/src/commands/action/index.ts +0 -27
- package/src/commands/action/list.ts +0 -247
- package/src/commands/action/run.ts +0 -197
- package/src/commands/action/show.ts +0 -128
- package/src/commands/action/sync.ts +0 -76
- package/src/commands/action/validate.ts +0 -102
- package/src/commands/action.ts +0 -1
- package/src/commands/build.ts +0 -56
- package/src/commands/config.ts +0 -583
- package/src/commands/doctor.ts +0 -84
- package/src/commands/export.ts +0 -196
- package/src/commands/index.ts +0 -75
- package/src/commands/info.ts +0 -537
- package/src/commands/init.ts +0 -24
- package/src/commands/link.ts +0 -73
- package/src/commands/mcp.ts +0 -195
- package/src/commands/playbook.ts +0 -566
- package/src/commands/profile.ts +0 -296
- package/src/commands/runs.ts +0 -452
- package/src/commands/serve.ts +0 -132
- package/src/commands/state.ts +0 -590
- package/src/commands/test.ts +0 -48
- package/src/index.ts +0 -58
- package/src/utils/bytes.ts +0 -2
- package/src/utils/filter.ts +0 -2
- /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,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;
|