@enactprotocol/cli 1.2.13 → 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.
- package/README.md +88 -0
- package/package.json +34 -38
- package/src/commands/auth/index.ts +940 -0
- package/src/commands/cache/index.ts +361 -0
- package/src/commands/config/README.md +239 -0
- package/src/commands/config/index.ts +164 -0
- package/src/commands/env/README.md +197 -0
- package/src/commands/env/index.ts +392 -0
- package/src/commands/exec/README.md +110 -0
- package/src/commands/exec/index.ts +195 -0
- package/src/commands/get/index.ts +198 -0
- package/src/commands/index.ts +30 -0
- package/src/commands/inspect/index.ts +264 -0
- package/src/commands/install/README.md +146 -0
- package/src/commands/install/index.ts +682 -0
- package/src/commands/list/README.md +115 -0
- package/src/commands/list/index.ts +138 -0
- package/src/commands/publish/index.ts +350 -0
- package/src/commands/report/index.ts +366 -0
- package/src/commands/run/README.md +124 -0
- package/src/commands/run/index.ts +686 -0
- package/src/commands/search/index.ts +368 -0
- package/src/commands/setup/index.ts +274 -0
- package/src/commands/sign/index.ts +652 -0
- package/src/commands/trust/README.md +214 -0
- package/src/commands/trust/index.ts +453 -0
- package/src/commands/unyank/index.ts +107 -0
- package/src/commands/yank/index.ts +143 -0
- package/src/index.ts +96 -0
- package/src/types.ts +81 -0
- package/src/utils/errors.ts +409 -0
- package/src/utils/exit-codes.ts +159 -0
- package/src/utils/ignore.ts +147 -0
- package/src/utils/index.ts +107 -0
- package/src/utils/output.ts +242 -0
- package/src/utils/spinner.ts +214 -0
- package/tests/commands/auth.test.ts +217 -0
- package/tests/commands/cache.test.ts +286 -0
- package/tests/commands/config.test.ts +277 -0
- package/tests/commands/env.test.ts +293 -0
- package/tests/commands/exec.test.ts +112 -0
- package/tests/commands/get.test.ts +179 -0
- package/tests/commands/inspect.test.ts +201 -0
- package/tests/commands/install-integration.test.ts +343 -0
- package/tests/commands/install.test.ts +288 -0
- package/tests/commands/list.test.ts +160 -0
- package/tests/commands/publish.test.ts +186 -0
- package/tests/commands/report.test.ts +194 -0
- package/tests/commands/run.test.ts +231 -0
- package/tests/commands/search.test.ts +131 -0
- package/tests/commands/sign.test.ts +164 -0
- package/tests/commands/trust.test.ts +236 -0
- package/tests/commands/unyank.test.ts +114 -0
- package/tests/commands/yank.test.ts +154 -0
- package/tests/e2e.test.ts +554 -0
- package/tests/fixtures/calculator/enact.yaml +34 -0
- package/tests/fixtures/echo-tool/enact.md +31 -0
- package/tests/fixtures/env-tool/enact.yaml +19 -0
- package/tests/fixtures/greeter/enact.yaml +18 -0
- package/tests/fixtures/invalid-tool/enact.yaml +4 -0
- package/tests/index.test.ts +8 -0
- package/tests/types.test.ts +84 -0
- package/tests/utils/errors.test.ts +303 -0
- package/tests/utils/exit-codes.test.ts +189 -0
- package/tests/utils/ignore.test.ts +461 -0
- package/tests/utils/output.test.ts +126 -0
- package/tsconfig.json +17 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/index.js +0 -231612
- package/dist/index.js.bak +0 -231611
- package/dist/web/static/app.js +0 -663
- package/dist/web/static/index.html +0 -117
- package/dist/web/static/style.css +0 -291
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enact env command
|
|
3
|
+
*
|
|
4
|
+
* Manage environment variables and secrets.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
deleteEnv,
|
|
9
|
+
deleteSecret,
|
|
10
|
+
getEnv,
|
|
11
|
+
listEnv,
|
|
12
|
+
listSecrets,
|
|
13
|
+
secretExists,
|
|
14
|
+
setEnv,
|
|
15
|
+
setSecret,
|
|
16
|
+
} from "@enactprotocol/secrets";
|
|
17
|
+
import type { Command } from "commander";
|
|
18
|
+
import type { CommandContext, GlobalOptions } from "../../types";
|
|
19
|
+
import {
|
|
20
|
+
type TableColumn,
|
|
21
|
+
dim,
|
|
22
|
+
error,
|
|
23
|
+
formatError,
|
|
24
|
+
header,
|
|
25
|
+
info,
|
|
26
|
+
json,
|
|
27
|
+
keyValue,
|
|
28
|
+
newline,
|
|
29
|
+
password,
|
|
30
|
+
success,
|
|
31
|
+
table,
|
|
32
|
+
} from "../../utils";
|
|
33
|
+
|
|
34
|
+
interface EnvSetOptions extends GlobalOptions {
|
|
35
|
+
secret?: boolean;
|
|
36
|
+
namespace?: string;
|
|
37
|
+
local?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface EnvGetOptions extends GlobalOptions {
|
|
41
|
+
secret?: boolean;
|
|
42
|
+
namespace?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface EnvListOptions extends GlobalOptions {
|
|
46
|
+
secret?: boolean;
|
|
47
|
+
namespace?: string;
|
|
48
|
+
local?: boolean;
|
|
49
|
+
global?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface EnvDeleteOptions extends GlobalOptions {
|
|
53
|
+
secret?: boolean;
|
|
54
|
+
namespace?: string;
|
|
55
|
+
local?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Set environment variable or secret
|
|
60
|
+
*/
|
|
61
|
+
async function envSetHandler(
|
|
62
|
+
key: string,
|
|
63
|
+
value: string | undefined,
|
|
64
|
+
options: EnvSetOptions,
|
|
65
|
+
ctx: CommandContext
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
if (options.secret) {
|
|
68
|
+
// Setting a secret in the keyring
|
|
69
|
+
if (!options.namespace) {
|
|
70
|
+
error("--namespace is required when setting a secret");
|
|
71
|
+
dim("Example: enact env set API_KEY --secret --namespace alice/api");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If no value provided, prompt for it
|
|
76
|
+
let secretValue = value;
|
|
77
|
+
if (!secretValue) {
|
|
78
|
+
if (!ctx.isInteractive) {
|
|
79
|
+
error("Value is required in non-interactive mode");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const prompted = await password(`Enter value for ${key}:`);
|
|
83
|
+
if (!prompted) {
|
|
84
|
+
error("No value provided");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
secretValue = prompted;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await setSecret(options.namespace, key, secretValue);
|
|
91
|
+
|
|
92
|
+
if (options.json) {
|
|
93
|
+
json({ set: true, key, namespace: options.namespace, type: "secret" });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
success(`Secret ${key} set for namespace ${options.namespace}`);
|
|
98
|
+
} else {
|
|
99
|
+
// Setting an environment variable in .env file
|
|
100
|
+
if (!value) {
|
|
101
|
+
error("Value is required for environment variables");
|
|
102
|
+
dim("Example: enact env set API_URL https://api.example.com");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const scope = options.local ? "local" : "global";
|
|
107
|
+
setEnv(key, value, scope, ctx.cwd);
|
|
108
|
+
|
|
109
|
+
if (options.json) {
|
|
110
|
+
json({ set: true, key, value, scope, type: "env" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
success(`Environment variable ${key} set (${scope})`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get environment variable or check secret existence
|
|
120
|
+
*/
|
|
121
|
+
async function envGetHandler(
|
|
122
|
+
key: string,
|
|
123
|
+
options: EnvGetOptions,
|
|
124
|
+
ctx: CommandContext
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
if (options.secret) {
|
|
127
|
+
// Check if secret exists (never show value)
|
|
128
|
+
if (!options.namespace) {
|
|
129
|
+
error("--namespace is required when getting a secret");
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const exists = await secretExists(options.namespace, key);
|
|
134
|
+
|
|
135
|
+
if (options.json) {
|
|
136
|
+
json({ key, namespace: options.namespace, exists, type: "secret" });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (exists) {
|
|
141
|
+
success(`Secret ${key} exists for namespace ${options.namespace}`);
|
|
142
|
+
} else {
|
|
143
|
+
info(`Secret ${key} not found for namespace ${options.namespace}`);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// Get environment variable
|
|
147
|
+
const result = getEnv(key, undefined, ctx.cwd);
|
|
148
|
+
|
|
149
|
+
if (options.json) {
|
|
150
|
+
json(result ? { ...result, type: "env" } : { key, found: false, type: "env" });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (result) {
|
|
155
|
+
keyValue("Key", key);
|
|
156
|
+
keyValue("Value", result.value);
|
|
157
|
+
keyValue("Source", result.source);
|
|
158
|
+
if (result.filePath) {
|
|
159
|
+
keyValue("File", result.filePath);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
info(`Environment variable ${key} not found`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* List environment variables or secrets
|
|
169
|
+
*/
|
|
170
|
+
async function envListHandler(options: EnvListOptions, ctx: CommandContext): Promise<void> {
|
|
171
|
+
if (options.secret) {
|
|
172
|
+
// List secrets for a namespace
|
|
173
|
+
if (!options.namespace) {
|
|
174
|
+
error("--namespace is required when listing secrets");
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const secrets = await listSecrets(options.namespace);
|
|
179
|
+
|
|
180
|
+
if (options.json) {
|
|
181
|
+
json({ namespace: options.namespace, secrets, type: "secrets" });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (secrets.length === 0) {
|
|
186
|
+
info(`No secrets found for namespace ${options.namespace}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
header(`Secrets for ${options.namespace}`);
|
|
191
|
+
newline();
|
|
192
|
+
for (const name of secrets) {
|
|
193
|
+
dim(` • ${name}`);
|
|
194
|
+
}
|
|
195
|
+
newline();
|
|
196
|
+
dim(`Total: ${secrets.length} secret(s)`);
|
|
197
|
+
} else {
|
|
198
|
+
// List environment variables
|
|
199
|
+
let scope: "local" | "global" | "all" = "all";
|
|
200
|
+
if (options.local && !options.global) {
|
|
201
|
+
scope = "local";
|
|
202
|
+
} else if (options.global && !options.local) {
|
|
203
|
+
scope = "global";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const envVars = listEnv(scope, ctx.cwd);
|
|
207
|
+
|
|
208
|
+
if (options.json) {
|
|
209
|
+
json({ scope, variables: envVars, type: "env" });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (envVars.length === 0) {
|
|
214
|
+
info("No environment variables found");
|
|
215
|
+
dim("Set variables with 'enact env set KEY VALUE'");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
header("Environment Variables");
|
|
220
|
+
newline();
|
|
221
|
+
|
|
222
|
+
const columns: TableColumn[] = [
|
|
223
|
+
{ key: "key", header: "Key", width: 25 },
|
|
224
|
+
{ key: "value", header: "Value", width: 40 },
|
|
225
|
+
{ key: "source", header: "Source", width: 10 },
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
// Transform to table-compatible format
|
|
229
|
+
const tableData = envVars.map((v) => ({
|
|
230
|
+
key: v.key,
|
|
231
|
+
value: v.value.length > 40 ? `${v.value.slice(0, 37)}...` : v.value,
|
|
232
|
+
source: v.source,
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
table(tableData, columns);
|
|
236
|
+
newline();
|
|
237
|
+
dim(`Total: ${envVars.length} variable(s)`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Delete environment variable or secret
|
|
243
|
+
*/
|
|
244
|
+
async function envDeleteHandler(
|
|
245
|
+
key: string,
|
|
246
|
+
options: EnvDeleteOptions,
|
|
247
|
+
ctx: CommandContext
|
|
248
|
+
): Promise<void> {
|
|
249
|
+
if (options.secret) {
|
|
250
|
+
// Delete secret from keyring
|
|
251
|
+
if (!options.namespace) {
|
|
252
|
+
error("--namespace is required when deleting a secret");
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const deleted = await deleteSecret(options.namespace, key);
|
|
257
|
+
|
|
258
|
+
if (options.json) {
|
|
259
|
+
json({ deleted, key, namespace: options.namespace, type: "secret" });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (deleted) {
|
|
264
|
+
success(`Secret ${key} deleted from namespace ${options.namespace}`);
|
|
265
|
+
} else {
|
|
266
|
+
info(`Secret ${key} not found for namespace ${options.namespace}`);
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
// Delete environment variable
|
|
270
|
+
const scope = options.local ? "local" : "global";
|
|
271
|
+
const deleted = deleteEnv(key, scope, ctx.cwd);
|
|
272
|
+
|
|
273
|
+
if (options.json) {
|
|
274
|
+
json({ deleted, key, scope, type: "env" });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (deleted) {
|
|
279
|
+
success(`Environment variable ${key} deleted (${scope})`);
|
|
280
|
+
} else {
|
|
281
|
+
info(`Environment variable ${key} not found (${scope})`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Configure the env command
|
|
288
|
+
*/
|
|
289
|
+
export function configureEnvCommand(program: Command): void {
|
|
290
|
+
const env = program.command("env").description("Manage environment variables and secrets");
|
|
291
|
+
|
|
292
|
+
// env set
|
|
293
|
+
env
|
|
294
|
+
.command("set")
|
|
295
|
+
.description("Set an environment variable or secret")
|
|
296
|
+
.argument("<key>", "Variable name")
|
|
297
|
+
.argument("[value]", "Variable value (prompted if secret and not provided)")
|
|
298
|
+
.option("-s, --secret", "Store as secret in OS keyring")
|
|
299
|
+
.option("-n, --namespace <namespace>", "Namespace for secret (required with --secret)")
|
|
300
|
+
.option("-l, --local", "Set in project .enact/.env instead of global")
|
|
301
|
+
.option("--json", "Output as JSON")
|
|
302
|
+
.action(async (key: string, value: string | undefined, options: EnvSetOptions) => {
|
|
303
|
+
const ctx: CommandContext = {
|
|
304
|
+
cwd: process.cwd(),
|
|
305
|
+
options,
|
|
306
|
+
isCI: Boolean(process.env.CI),
|
|
307
|
+
isInteractive: process.stdout.isTTY ?? false,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
await envSetHandler(key, value, options, ctx);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
error(formatError(err));
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// env get
|
|
319
|
+
env
|
|
320
|
+
.command("get")
|
|
321
|
+
.description("Get an environment variable or check if a secret exists")
|
|
322
|
+
.argument("<key>", "Variable name")
|
|
323
|
+
.option("-s, --secret", "Check secret in OS keyring (never shows value)")
|
|
324
|
+
.option("-n, --namespace <namespace>", "Namespace for secret (required with --secret)")
|
|
325
|
+
.option("--json", "Output as JSON")
|
|
326
|
+
.action(async (key: string, options: EnvGetOptions) => {
|
|
327
|
+
const ctx: CommandContext = {
|
|
328
|
+
cwd: process.cwd(),
|
|
329
|
+
options,
|
|
330
|
+
isCI: Boolean(process.env.CI),
|
|
331
|
+
isInteractive: process.stdout.isTTY ?? false,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
await envGetHandler(key, options, ctx);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
error(formatError(err));
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// env list
|
|
343
|
+
env
|
|
344
|
+
.command("list")
|
|
345
|
+
.description("List environment variables or secrets")
|
|
346
|
+
.option("-s, --secret", "List secrets from OS keyring")
|
|
347
|
+
.option("-n, --namespace <namespace>", "Namespace for secrets (required with --secret)")
|
|
348
|
+
.option("-l, --local", "Show only project .enact/.env variables")
|
|
349
|
+
.option("-g, --global", "Show only global ~/.enact/.env variables")
|
|
350
|
+
.option("--json", "Output as JSON")
|
|
351
|
+
.action(async (options: EnvListOptions) => {
|
|
352
|
+
const ctx: CommandContext = {
|
|
353
|
+
cwd: process.cwd(),
|
|
354
|
+
options,
|
|
355
|
+
isCI: Boolean(process.env.CI),
|
|
356
|
+
isInteractive: process.stdout.isTTY ?? false,
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
await envListHandler(options, ctx);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
error(formatError(err));
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// env delete
|
|
368
|
+
env
|
|
369
|
+
.command("delete")
|
|
370
|
+
.alias("rm")
|
|
371
|
+
.description("Delete an environment variable or secret")
|
|
372
|
+
.argument("<key>", "Variable name")
|
|
373
|
+
.option("-s, --secret", "Delete secret from OS keyring")
|
|
374
|
+
.option("-n, --namespace <namespace>", "Namespace for secret (required with --secret)")
|
|
375
|
+
.option("-l, --local", "Delete from project .enact/.env instead of global")
|
|
376
|
+
.option("--json", "Output as JSON")
|
|
377
|
+
.action(async (key: string, options: EnvDeleteOptions) => {
|
|
378
|
+
const ctx: CommandContext = {
|
|
379
|
+
cwd: process.cwd(),
|
|
380
|
+
options,
|
|
381
|
+
isCI: Boolean(process.env.CI),
|
|
382
|
+
isInteractive: process.stdout.isTTY ?? false,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await envDeleteHandler(key, options, ctx);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
error(formatError(err));
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# enact exec
|
|
2
|
+
|
|
3
|
+
Execute an arbitrary command in a tool's container environment.
|
|
4
|
+
|
|
5
|
+
## Synopsis
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
enact exec <tool> "<command>" [options]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Description
|
|
12
|
+
|
|
13
|
+
The `exec` command allows you to run any command inside a tool's container environment, not just the manifest-defined command. This is useful for:
|
|
14
|
+
|
|
15
|
+
- Debugging and inspecting tool containers
|
|
16
|
+
- Running one-off commands in a tool's environment
|
|
17
|
+
- Testing commands before adding them to a manifest
|
|
18
|
+
- Exploring tool dependencies and file structure
|
|
19
|
+
|
|
20
|
+
The container environment includes:
|
|
21
|
+
- The same base image defined in the manifest
|
|
22
|
+
- All environment variables and secrets
|
|
23
|
+
- The tool's source directory mounted
|
|
24
|
+
|
|
25
|
+
## Arguments
|
|
26
|
+
|
|
27
|
+
| Argument | Description |
|
|
28
|
+
|----------|-------------|
|
|
29
|
+
| `<tool>` | Tool to run in. Can be a tool name, path, or `.` for current directory |
|
|
30
|
+
| `<command>` | Command to execute. Quote complex commands with spaces or special characters |
|
|
31
|
+
|
|
32
|
+
## Options
|
|
33
|
+
|
|
34
|
+
| Option | Description |
|
|
35
|
+
|--------|-------------|
|
|
36
|
+
| `-t, --timeout <duration>` | Execution timeout (e.g., `30s`, `5m`, `1h`) |
|
|
37
|
+
| `-v, --verbose` | Show detailed output including stderr and timing |
|
|
38
|
+
| `--json` | Output result as JSON |
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
### Basic usage
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# View the tool's manifest
|
|
46
|
+
enact exec alice/utils/greeter "cat enact.md"
|
|
47
|
+
|
|
48
|
+
# List files in the container
|
|
49
|
+
enact exec alice/utils/greeter "ls -la"
|
|
50
|
+
|
|
51
|
+
# Check installed packages
|
|
52
|
+
enact exec python-tool "pip list"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Debugging
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Interactive shell exploration
|
|
59
|
+
enact exec my-tool "sh -c 'echo $PATH && which python'"
|
|
60
|
+
|
|
61
|
+
# Check environment variables
|
|
62
|
+
enact exec my-tool "env | sort"
|
|
63
|
+
|
|
64
|
+
# Test a command before adding to manifest
|
|
65
|
+
enact exec my-tool "python --version"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### With options
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Long-running command with timeout
|
|
72
|
+
enact exec my-tool "sleep 10 && echo done" --timeout 30s
|
|
73
|
+
|
|
74
|
+
# Verbose output for debugging
|
|
75
|
+
enact exec my-tool "some-command" --verbose
|
|
76
|
+
|
|
77
|
+
# JSON output for scripting
|
|
78
|
+
enact exec my-tool "cat config.json" --json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Differences from `enact run`
|
|
82
|
+
|
|
83
|
+
| Feature | `enact run` | `enact exec` |
|
|
84
|
+
|---------|-------------|--------------|
|
|
85
|
+
| Command | Manifest-defined | User-specified |
|
|
86
|
+
| Input validation | Yes (via inputSchema) | No |
|
|
87
|
+
| Parameter interpolation | Yes (`${param}`) | No |
|
|
88
|
+
| Use case | Production execution | Debugging/exploration |
|
|
89
|
+
|
|
90
|
+
## Security Note
|
|
91
|
+
|
|
92
|
+
The `exec` command runs with the same isolation as `run`:
|
|
93
|
+
- Commands execute inside the container
|
|
94
|
+
- Secrets are injected as environment variables
|
|
95
|
+
- Network access follows manifest settings
|
|
96
|
+
|
|
97
|
+
However, since you can run arbitrary commands, be careful when using `exec` with untrusted tools.
|
|
98
|
+
|
|
99
|
+
## Exit Codes
|
|
100
|
+
|
|
101
|
+
| Code | Description |
|
|
102
|
+
|------|-------------|
|
|
103
|
+
| `0` | Successful execution |
|
|
104
|
+
| `1` | Execution failed or error |
|
|
105
|
+
| `3` | Tool not found |
|
|
106
|
+
|
|
107
|
+
## See Also
|
|
108
|
+
|
|
109
|
+
- [enact run](../run/README.md) - Execute a tool's manifest-defined command
|
|
110
|
+
- [enact install](../install/README.md) - Install tools
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enact exec command
|
|
3
|
+
*
|
|
4
|
+
* Execute an arbitrary command in a tool's container environment.
|
|
5
|
+
* Unlike `run`, this allows running any command, not just the manifest-defined one.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DaggerExecutionProvider, type ExecutionResult } from "@enactprotocol/execution";
|
|
9
|
+
import { resolveSecrets, resolveToolEnv } from "@enactprotocol/secrets";
|
|
10
|
+
import { resolveToolAuto } from "@enactprotocol/shared";
|
|
11
|
+
import type { Command } from "commander";
|
|
12
|
+
import type { CommandContext, GlobalOptions } from "../../types";
|
|
13
|
+
import { dim, error, formatError, json, newline, suggest, symbols, withSpinner } from "../../utils";
|
|
14
|
+
|
|
15
|
+
interface ExecOptions extends GlobalOptions {
|
|
16
|
+
timeout?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Display execution result
|
|
21
|
+
*/
|
|
22
|
+
function displayResult(result: ExecutionResult, options: ExecOptions): void {
|
|
23
|
+
if (options.json) {
|
|
24
|
+
json(result);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (result.success) {
|
|
29
|
+
if (result.output?.stdout) {
|
|
30
|
+
process.stdout.write(result.output.stdout);
|
|
31
|
+
if (!result.output.stdout.endsWith("\n")) {
|
|
32
|
+
newline();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options.verbose && result.output?.stderr) {
|
|
37
|
+
dim(`stderr: ${result.output.stderr}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options.verbose && result.metadata) {
|
|
41
|
+
newline();
|
|
42
|
+
dim(`Duration: ${result.metadata.durationMs}ms`);
|
|
43
|
+
dim(`Exit code: ${result.output?.exitCode ?? 0}`);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
error(`Execution failed: ${result.error?.message ?? "Unknown error"}`);
|
|
47
|
+
|
|
48
|
+
if (result.output?.stderr) {
|
|
49
|
+
newline();
|
|
50
|
+
dim("stderr:");
|
|
51
|
+
dim(result.output.stderr);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse timeout string (e.g., "30s", "5m", "1h")
|
|
58
|
+
*/
|
|
59
|
+
function parseTimeout(timeout: string): number {
|
|
60
|
+
const match = timeout.match(/^(\d+)(s|m|h)?$/);
|
|
61
|
+
if (!match) {
|
|
62
|
+
throw new Error(`Invalid timeout format: ${timeout}. Use format like "30s", "5m", or "1h".`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const value = Number.parseInt(match[1] ?? "0", 10);
|
|
66
|
+
const unit = match[2] || "s";
|
|
67
|
+
|
|
68
|
+
switch (unit) {
|
|
69
|
+
case "h":
|
|
70
|
+
return value * 60 * 60 * 1000;
|
|
71
|
+
case "m":
|
|
72
|
+
return value * 60 * 1000;
|
|
73
|
+
default:
|
|
74
|
+
return value * 1000;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Exec command handler
|
|
80
|
+
*/
|
|
81
|
+
async function execHandler(
|
|
82
|
+
tool: string,
|
|
83
|
+
command: string,
|
|
84
|
+
options: ExecOptions,
|
|
85
|
+
ctx: CommandContext
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
// Resolve the tool
|
|
88
|
+
const resolution = await withSpinner(
|
|
89
|
+
`Resolving tool: ${tool}`,
|
|
90
|
+
async () => resolveToolAuto(tool, { startDir: ctx.cwd }),
|
|
91
|
+
`${symbols.success} Resolved: ${tool}`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (!resolution) {
|
|
95
|
+
error(`Tool not found: ${tool}`);
|
|
96
|
+
suggest(`Try 'enact install ${tool}' first, or check the tool name.`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const manifest = resolution.manifest;
|
|
101
|
+
|
|
102
|
+
// Resolve environment variables (non-secrets)
|
|
103
|
+
const { resolved: envResolved } = resolveToolEnv(manifest.env ?? {}, ctx.cwd);
|
|
104
|
+
const envVars: Record<string, string> = {};
|
|
105
|
+
for (const [key, envRes] of envResolved) {
|
|
106
|
+
envVars[key] = envRes.value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Resolve secrets
|
|
110
|
+
const secretDeclarations = Object.entries(manifest.env ?? {})
|
|
111
|
+
.filter(([_, v]) => v.secret)
|
|
112
|
+
.map(([k]) => k);
|
|
113
|
+
|
|
114
|
+
if (secretDeclarations.length > 0) {
|
|
115
|
+
const namespace = manifest.name.split("/").slice(0, -1).join("/") || manifest.name;
|
|
116
|
+
const secretResults = await resolveSecrets(namespace, secretDeclarations);
|
|
117
|
+
|
|
118
|
+
for (const [key, result] of secretResults) {
|
|
119
|
+
if (result.found && result.value) {
|
|
120
|
+
envVars[key] = result.value;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Execute the custom command
|
|
126
|
+
const providerConfig: { defaultTimeout?: number; verbose?: boolean } = {};
|
|
127
|
+
if (options.timeout) {
|
|
128
|
+
providerConfig.defaultTimeout = parseTimeout(options.timeout);
|
|
129
|
+
}
|
|
130
|
+
if (options.verbose) {
|
|
131
|
+
providerConfig.verbose = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const provider = new DaggerExecutionProvider(providerConfig);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await provider.initialize();
|
|
138
|
+
|
|
139
|
+
// Create a modified manifest with the custom command
|
|
140
|
+
const execManifest = {
|
|
141
|
+
...manifest,
|
|
142
|
+
command,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = await withSpinner(
|
|
146
|
+
`Executing in ${manifest.name}...`,
|
|
147
|
+
async () =>
|
|
148
|
+
provider.execute(execManifest, {
|
|
149
|
+
params: {},
|
|
150
|
+
envOverrides: envVars,
|
|
151
|
+
}),
|
|
152
|
+
options.verbose ? `${symbols.success} Execution complete` : undefined
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
displayResult(result, options);
|
|
156
|
+
|
|
157
|
+
if (!result.success) {
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
// Provider cleanup handled by Dagger
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Configure the exec command
|
|
167
|
+
*/
|
|
168
|
+
export function configureExecCommand(program: Command): void {
|
|
169
|
+
program
|
|
170
|
+
.command("exec")
|
|
171
|
+
.description("Execute an arbitrary command in a tool's container environment")
|
|
172
|
+
.argument("<tool>", "Tool to run in (name, path, or '.' for current directory)")
|
|
173
|
+
.argument("<command>", "Command to execute (quote complex commands)")
|
|
174
|
+
.option("-t, --timeout <duration>", "Execution timeout (e.g., 30s, 5m)")
|
|
175
|
+
.option("-v, --verbose", "Show detailed output")
|
|
176
|
+
.option("--json", "Output result as JSON")
|
|
177
|
+
.action(async (tool: string, command: string, options: ExecOptions) => {
|
|
178
|
+
const ctx: CommandContext = {
|
|
179
|
+
cwd: process.cwd(),
|
|
180
|
+
options,
|
|
181
|
+
isCI: Boolean(process.env.CI),
|
|
182
|
+
isInteractive: process.stdout.isTTY ?? false,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await execHandler(tool, command, options, ctx);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
error(formatError(err));
|
|
189
|
+
if (options.verbose && err instanceof Error && err.stack) {
|
|
190
|
+
dim(err.stack);
|
|
191
|
+
}
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|