@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.
Files changed (73) hide show
  1. package/README.md +88 -0
  2. package/package.json +34 -38
  3. package/src/commands/auth/index.ts +940 -0
  4. package/src/commands/cache/index.ts +361 -0
  5. package/src/commands/config/README.md +239 -0
  6. package/src/commands/config/index.ts +164 -0
  7. package/src/commands/env/README.md +197 -0
  8. package/src/commands/env/index.ts +392 -0
  9. package/src/commands/exec/README.md +110 -0
  10. package/src/commands/exec/index.ts +195 -0
  11. package/src/commands/get/index.ts +198 -0
  12. package/src/commands/index.ts +30 -0
  13. package/src/commands/inspect/index.ts +264 -0
  14. package/src/commands/install/README.md +146 -0
  15. package/src/commands/install/index.ts +682 -0
  16. package/src/commands/list/README.md +115 -0
  17. package/src/commands/list/index.ts +138 -0
  18. package/src/commands/publish/index.ts +350 -0
  19. package/src/commands/report/index.ts +366 -0
  20. package/src/commands/run/README.md +124 -0
  21. package/src/commands/run/index.ts +686 -0
  22. package/src/commands/search/index.ts +368 -0
  23. package/src/commands/setup/index.ts +274 -0
  24. package/src/commands/sign/index.ts +652 -0
  25. package/src/commands/trust/README.md +214 -0
  26. package/src/commands/trust/index.ts +453 -0
  27. package/src/commands/unyank/index.ts +107 -0
  28. package/src/commands/yank/index.ts +143 -0
  29. package/src/index.ts +96 -0
  30. package/src/types.ts +81 -0
  31. package/src/utils/errors.ts +409 -0
  32. package/src/utils/exit-codes.ts +159 -0
  33. package/src/utils/ignore.ts +147 -0
  34. package/src/utils/index.ts +107 -0
  35. package/src/utils/output.ts +242 -0
  36. package/src/utils/spinner.ts +214 -0
  37. package/tests/commands/auth.test.ts +217 -0
  38. package/tests/commands/cache.test.ts +286 -0
  39. package/tests/commands/config.test.ts +277 -0
  40. package/tests/commands/env.test.ts +293 -0
  41. package/tests/commands/exec.test.ts +112 -0
  42. package/tests/commands/get.test.ts +179 -0
  43. package/tests/commands/inspect.test.ts +201 -0
  44. package/tests/commands/install-integration.test.ts +343 -0
  45. package/tests/commands/install.test.ts +288 -0
  46. package/tests/commands/list.test.ts +160 -0
  47. package/tests/commands/publish.test.ts +186 -0
  48. package/tests/commands/report.test.ts +194 -0
  49. package/tests/commands/run.test.ts +231 -0
  50. package/tests/commands/search.test.ts +131 -0
  51. package/tests/commands/sign.test.ts +164 -0
  52. package/tests/commands/trust.test.ts +236 -0
  53. package/tests/commands/unyank.test.ts +114 -0
  54. package/tests/commands/yank.test.ts +154 -0
  55. package/tests/e2e.test.ts +554 -0
  56. package/tests/fixtures/calculator/enact.yaml +34 -0
  57. package/tests/fixtures/echo-tool/enact.md +31 -0
  58. package/tests/fixtures/env-tool/enact.yaml +19 -0
  59. package/tests/fixtures/greeter/enact.yaml +18 -0
  60. package/tests/fixtures/invalid-tool/enact.yaml +4 -0
  61. package/tests/index.test.ts +8 -0
  62. package/tests/types.test.ts +84 -0
  63. package/tests/utils/errors.test.ts +303 -0
  64. package/tests/utils/exit-codes.test.ts +189 -0
  65. package/tests/utils/ignore.test.ts +461 -0
  66. package/tests/utils/output.test.ts +126 -0
  67. package/tsconfig.json +17 -0
  68. package/tsconfig.tsbuildinfo +1 -0
  69. package/dist/index.js +0 -231612
  70. package/dist/index.js.bak +0 -231611
  71. package/dist/web/static/app.js +0 -663
  72. package/dist/web/static/index.html +0 -117
  73. 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
+ }