@enactprotocol/cli 2.1.24 → 2.1.29
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/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +4 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/init/templates/claude.d.ts +1 -1
- package/dist/commands/init/templates/claude.d.ts.map +1 -1
- package/dist/commands/init/templates/claude.js +268 -28
- package/dist/commands/init/templates/claude.js.map +1 -1
- package/dist/commands/init/templates/tool-agents.d.ts +1 -1
- package/dist/commands/init/templates/tool-agents.d.ts.map +1 -1
- package/dist/commands/init/templates/tool-agents.js +90 -15
- package/dist/commands/init/templates/tool-agents.js.map +1 -1
- package/dist/commands/install/index.d.ts.map +1 -1
- package/dist/commands/install/index.js +9 -1
- package/dist/commands/install/index.js.map +1 -1
- package/dist/commands/learn/index.d.ts.map +1 -1
- package/dist/commands/learn/index.js +4 -11
- package/dist/commands/learn/index.js.map +1 -1
- package/dist/commands/mcp/index.d.ts.map +1 -1
- package/dist/commands/mcp/index.js +204 -53
- package/dist/commands/mcp/index.js.map +1 -1
- package/dist/commands/run/index.d.ts.map +1 -1
- package/dist/commands/run/index.js +380 -39
- package/dist/commands/run/index.js.map +1 -1
- package/dist/commands/validate/index.d.ts +11 -0
- package/dist/commands/validate/index.d.ts.map +1 -0
- package/dist/commands/validate/index.js +299 -0
- package/dist/commands/validate/index.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/errors.d.ts +8 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +13 -2
- package/dist/utils/errors.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/index.ts +5 -0
- package/src/commands/init/templates/claude.ts +268 -28
- package/src/commands/init/templates/tool-agents.ts +90 -15
- package/src/commands/install/index.ts +11 -0
- package/src/commands/learn/index.ts +6 -11
- package/src/commands/mcp/index.ts +768 -0
- package/src/commands/run/README.md +68 -1
- package/src/commands/run/index.ts +475 -35
- package/src/commands/validate/index.ts +344 -0
- package/src/index.ts +8 -1
- package/src/types.ts +2 -0
- package/src/utils/errors.ts +26 -6
- package/tests/commands/init.test.ts +2 -2
- package/tests/commands/run.test.ts +260 -0
- package/tests/commands/validate.test.ts +81 -0
- package/tests/utils/errors.test.ts +36 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -9,8 +9,20 @@
|
|
|
9
9
|
* 3. Run from resolved location (never copies to installed tools)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
cpSync,
|
|
14
|
+
existsSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
mkdtempSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
renameSync,
|
|
19
|
+
rmSync,
|
|
20
|
+
statSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
14
26
|
import * as clack from "@clack/prompts";
|
|
15
27
|
import {
|
|
16
28
|
type AttestationListResponse,
|
|
@@ -35,12 +47,14 @@ import {
|
|
|
35
47
|
prepareCommand,
|
|
36
48
|
toolNameToPath,
|
|
37
49
|
tryResolveTool,
|
|
50
|
+
tryResolveToolDetailed,
|
|
38
51
|
validateInputs,
|
|
39
52
|
} from "@enactprotocol/shared";
|
|
40
53
|
import type { Command } from "commander";
|
|
41
54
|
import type { CommandContext, GlobalOptions } from "../../types";
|
|
42
55
|
import {
|
|
43
56
|
EXIT_EXECUTION_ERROR,
|
|
57
|
+
ManifestError,
|
|
44
58
|
ToolNotFoundError,
|
|
45
59
|
TrustError,
|
|
46
60
|
ValidationError,
|
|
@@ -66,7 +80,11 @@ interface RunOptions extends GlobalOptions {
|
|
|
66
80
|
timeout?: string;
|
|
67
81
|
noCache?: boolean;
|
|
68
82
|
local?: boolean;
|
|
83
|
+
remote?: boolean;
|
|
69
84
|
verbose?: boolean;
|
|
85
|
+
output?: string;
|
|
86
|
+
apply?: boolean;
|
|
87
|
+
debug?: boolean;
|
|
70
88
|
}
|
|
71
89
|
|
|
72
90
|
/**
|
|
@@ -144,6 +162,144 @@ function parseInputArgs(
|
|
|
144
162
|
return inputs;
|
|
145
163
|
}
|
|
146
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Input path configuration (file or directory)
|
|
167
|
+
*/
|
|
168
|
+
interface InputPathConfig {
|
|
169
|
+
/** Absolute path on host */
|
|
170
|
+
path: string;
|
|
171
|
+
/** Whether it's a file or directory */
|
|
172
|
+
type: "file" | "directory";
|
|
173
|
+
/** Named input (for multi-input support, e.g., "left" from --input left=./path) */
|
|
174
|
+
name?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Parse --input flags to separate key=value parameters from directory/file paths
|
|
179
|
+
*
|
|
180
|
+
* The --input flag is overloaded to handle both:
|
|
181
|
+
* 1. Key=value parameters: --input name=Alice --input count=5
|
|
182
|
+
* 2. Directory/file paths: --input ./data --input left=./old
|
|
183
|
+
*
|
|
184
|
+
* Detection logic:
|
|
185
|
+
* - If value contains '=' and doesn't start with './' or '/' → key=value param
|
|
186
|
+
* - If value is a path (starts with ./, ../, /) → input path
|
|
187
|
+
* - If value exists as a path on disk → input path
|
|
188
|
+
* - Named input: name=./path where path exists → named input
|
|
189
|
+
*/
|
|
190
|
+
function parseInputPaths(inputs: string[] | undefined): {
|
|
191
|
+
params: Record<string, unknown>;
|
|
192
|
+
inputPaths: InputPathConfig[];
|
|
193
|
+
} {
|
|
194
|
+
if (!inputs) return { params: {}, inputPaths: [] };
|
|
195
|
+
|
|
196
|
+
const params: Record<string, unknown> = {};
|
|
197
|
+
const inputPaths: InputPathConfig[] = [];
|
|
198
|
+
|
|
199
|
+
for (const input of inputs) {
|
|
200
|
+
const eqIndex = input.indexOf("=");
|
|
201
|
+
|
|
202
|
+
// Check if it's a path (no = or starts with path chars)
|
|
203
|
+
const looksLikePath =
|
|
204
|
+
input.startsWith("./") ||
|
|
205
|
+
input.startsWith("../") ||
|
|
206
|
+
input.startsWith("/") ||
|
|
207
|
+
(eqIndex === -1 && existsSync(input));
|
|
208
|
+
|
|
209
|
+
if (looksLikePath) {
|
|
210
|
+
// Simple path: --input ./data
|
|
211
|
+
const absolutePath = resolve(input);
|
|
212
|
+
if (!existsSync(absolutePath)) {
|
|
213
|
+
throw new Error(`Input path does not exist: ${input}`);
|
|
214
|
+
}
|
|
215
|
+
const stat = statSync(absolutePath);
|
|
216
|
+
inputPaths.push({
|
|
217
|
+
path: absolutePath,
|
|
218
|
+
type: stat.isDirectory() ? "directory" : "file",
|
|
219
|
+
});
|
|
220
|
+
} else if (eqIndex > 0) {
|
|
221
|
+
const key = input.slice(0, eqIndex);
|
|
222
|
+
const value = input.slice(eqIndex + 1);
|
|
223
|
+
|
|
224
|
+
// Check if value is a path (named input like left=./old)
|
|
225
|
+
const valueLooksLikePath =
|
|
226
|
+
value.startsWith("./") ||
|
|
227
|
+
value.startsWith("../") ||
|
|
228
|
+
value.startsWith("/") ||
|
|
229
|
+
existsSync(value);
|
|
230
|
+
|
|
231
|
+
if (valueLooksLikePath && existsSync(value)) {
|
|
232
|
+
// Named input path: --input left=./old
|
|
233
|
+
const absolutePath = resolve(value);
|
|
234
|
+
const stat = statSync(absolutePath);
|
|
235
|
+
inputPaths.push({
|
|
236
|
+
path: absolutePath,
|
|
237
|
+
type: stat.isDirectory() ? "directory" : "file",
|
|
238
|
+
name: key,
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
// Key=value parameter: --input name=Alice
|
|
242
|
+
try {
|
|
243
|
+
params[key] = JSON.parse(value);
|
|
244
|
+
} catch {
|
|
245
|
+
params[key] = value;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
// No = sign and doesn't exist as path - treat as error
|
|
250
|
+
throw new Error(`Invalid input: "${input}". Expected key=value or a valid path.`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { params, inputPaths };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Atomically replace directory contents with new contents
|
|
259
|
+
*
|
|
260
|
+
* Process:
|
|
261
|
+
* 1. Create backup of original directory
|
|
262
|
+
* 2. Copy new contents to original location
|
|
263
|
+
* 3. Remove backup on success, or restore on failure
|
|
264
|
+
*
|
|
265
|
+
* @param targetDir - Directory to replace contents of
|
|
266
|
+
* @param sourceDir - Directory containing new contents
|
|
267
|
+
*/
|
|
268
|
+
function atomicReplace(targetDir: string, sourceDir: string): void {
|
|
269
|
+
const backupDir = `${targetDir}.backup-${Date.now()}`;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
// Step 1: Backup original
|
|
273
|
+
if (existsSync(targetDir)) {
|
|
274
|
+
renameSync(targetDir, backupDir);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Step 2: Move new contents to target
|
|
278
|
+
// We copy instead of rename because source might be on different filesystem (temp)
|
|
279
|
+
mkdirSync(targetDir, { recursive: true });
|
|
280
|
+
const entries = readdirSync(sourceDir, { withFileTypes: true });
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
const srcPath = join(sourceDir, entry.name);
|
|
283
|
+
const destPath = join(targetDir, entry.name);
|
|
284
|
+
cpSync(srcPath, destPath, { recursive: true });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Step 3: Remove backup on success
|
|
288
|
+
if (existsSync(backupDir)) {
|
|
289
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
290
|
+
}
|
|
291
|
+
} catch (err) {
|
|
292
|
+
// Restore backup on failure
|
|
293
|
+
if (existsSync(backupDir)) {
|
|
294
|
+
if (existsSync(targetDir)) {
|
|
295
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
296
|
+
}
|
|
297
|
+
renameSync(backupDir, targetDir);
|
|
298
|
+
}
|
|
299
|
+
throw err;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
147
303
|
/**
|
|
148
304
|
* Extract a bundle to the cache directory
|
|
149
305
|
*/
|
|
@@ -418,7 +574,10 @@ function displayDryRun(
|
|
|
418
574
|
manifest: ToolManifest,
|
|
419
575
|
inputs: Record<string, unknown>,
|
|
420
576
|
command: string[],
|
|
421
|
-
env: Record<string, string
|
|
577
|
+
env: Record<string, string>,
|
|
578
|
+
inputPaths: InputPathConfig[],
|
|
579
|
+
outputPath: string | undefined,
|
|
580
|
+
apply?: boolean
|
|
422
581
|
): void {
|
|
423
582
|
newline();
|
|
424
583
|
info(colors.bold("Dry Run Preview"));
|
|
@@ -430,7 +589,7 @@ function displayDryRun(
|
|
|
430
589
|
newline();
|
|
431
590
|
|
|
432
591
|
if (Object.keys(inputs).length > 0) {
|
|
433
|
-
info("
|
|
592
|
+
info("Parameters:");
|
|
434
593
|
for (const [key, value] of Object.entries(inputs)) {
|
|
435
594
|
dim(` ${key}: ${JSON.stringify(value)}`);
|
|
436
595
|
}
|
|
@@ -445,11 +604,111 @@ function displayDryRun(
|
|
|
445
604
|
newline();
|
|
446
605
|
}
|
|
447
606
|
|
|
607
|
+
if (inputPaths.length > 0) {
|
|
608
|
+
info("Input:");
|
|
609
|
+
for (const input of inputPaths) {
|
|
610
|
+
const target = input.name
|
|
611
|
+
? `/inputs/${input.name}`
|
|
612
|
+
: input.type === "file"
|
|
613
|
+
? `/input/${basename(input.path)}`
|
|
614
|
+
: "/input";
|
|
615
|
+
dim(` ${input.path} → ${target} (${input.type})`);
|
|
616
|
+
}
|
|
617
|
+
newline();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (outputPath) {
|
|
621
|
+
info("Output:");
|
|
622
|
+
dim(` /output → ${outputPath}`);
|
|
623
|
+
if (apply) {
|
|
624
|
+
dim(` ${colors.warning("(--apply)")} Changes will be atomically applied to ${outputPath}`);
|
|
625
|
+
}
|
|
626
|
+
newline();
|
|
627
|
+
}
|
|
628
|
+
|
|
448
629
|
info("Command:");
|
|
449
630
|
dim(` ${command.join(" ")}`);
|
|
450
631
|
newline();
|
|
451
632
|
}
|
|
452
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Display debug information about parameter resolution
|
|
636
|
+
*/
|
|
637
|
+
function displayDebugInfo(
|
|
638
|
+
manifest: ToolManifest,
|
|
639
|
+
rawInputs: Record<string, unknown>,
|
|
640
|
+
inputsWithDefaults: Record<string, unknown>,
|
|
641
|
+
finalInputs: Record<string, unknown>,
|
|
642
|
+
env: Record<string, string>,
|
|
643
|
+
command: string[]
|
|
644
|
+
): void {
|
|
645
|
+
newline();
|
|
646
|
+
info(colors.bold("Debug: Parameter Resolution"));
|
|
647
|
+
newline();
|
|
648
|
+
|
|
649
|
+
// Show schema information
|
|
650
|
+
if (manifest.inputSchema?.properties) {
|
|
651
|
+
info("Schema Properties:");
|
|
652
|
+
const required = new Set(manifest.inputSchema.required || []);
|
|
653
|
+
for (const [name, prop] of Object.entries(manifest.inputSchema.properties)) {
|
|
654
|
+
const propSchema = prop as { type?: string; default?: unknown; description?: string };
|
|
655
|
+
const isRequired = required.has(name);
|
|
656
|
+
const hasDefault = propSchema.default !== undefined;
|
|
657
|
+
const status = isRequired ? colors.error("required") : colors.dim("optional");
|
|
658
|
+
dim(
|
|
659
|
+
` ${name}: ${propSchema.type || "any"} [${status}]${hasDefault ? ` (default: ${JSON.stringify(propSchema.default)})` : ""}`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
newline();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Show raw inputs (what was provided)
|
|
666
|
+
info("Raw Inputs (provided by user):");
|
|
667
|
+
if (Object.keys(rawInputs).length === 0) {
|
|
668
|
+
dim(" (none)");
|
|
669
|
+
} else {
|
|
670
|
+
for (const [key, value] of Object.entries(rawInputs)) {
|
|
671
|
+
dim(` ${key}: ${JSON.stringify(value)}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
newline();
|
|
675
|
+
|
|
676
|
+
// Show inputs after defaults applied
|
|
677
|
+
info("After Defaults Applied:");
|
|
678
|
+
for (const [key, value] of Object.entries(inputsWithDefaults)) {
|
|
679
|
+
const wasDefault = rawInputs[key] === undefined;
|
|
680
|
+
dim(` ${key}: ${JSON.stringify(value)}${wasDefault ? colors.dim(" (default)") : ""}`);
|
|
681
|
+
}
|
|
682
|
+
newline();
|
|
683
|
+
|
|
684
|
+
// Show final inputs (after coercion)
|
|
685
|
+
info("Final Inputs (after validation/coercion):");
|
|
686
|
+
for (const [key, value] of Object.entries(finalInputs)) {
|
|
687
|
+
dim(` ${key}: ${JSON.stringify(value)}`);
|
|
688
|
+
}
|
|
689
|
+
newline();
|
|
690
|
+
|
|
691
|
+
// Show environment variables
|
|
692
|
+
if (Object.keys(env).length > 0) {
|
|
693
|
+
info("Environment Variables:");
|
|
694
|
+
for (const [key, value] of Object.entries(env)) {
|
|
695
|
+
// Mask potentially sensitive values
|
|
696
|
+
const isSensitive =
|
|
697
|
+
key.toLowerCase().includes("secret") ||
|
|
698
|
+
key.toLowerCase().includes("key") ||
|
|
699
|
+
key.toLowerCase().includes("token") ||
|
|
700
|
+
key.toLowerCase().includes("password");
|
|
701
|
+
dim(` ${key}=${isSensitive ? "***" : value}`);
|
|
702
|
+
}
|
|
703
|
+
newline();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Show final command
|
|
707
|
+
info("Final Command:");
|
|
708
|
+
dim(` ${command.join(" ")}`);
|
|
709
|
+
newline();
|
|
710
|
+
}
|
|
711
|
+
|
|
453
712
|
/**
|
|
454
713
|
* Display execution result
|
|
455
714
|
*/
|
|
@@ -481,15 +740,30 @@ function displayResult(result: ExecutionResult, options: RunOptions): void {
|
|
|
481
740
|
} else {
|
|
482
741
|
error(`Execution failed: ${result.error?.message ?? "Unknown error"}`);
|
|
483
742
|
|
|
484
|
-
if (
|
|
743
|
+
// Show stdout if present (useful for debugging - command may have printed before failing)
|
|
744
|
+
if (result.output?.stdout?.trim()) {
|
|
485
745
|
newline();
|
|
486
|
-
|
|
746
|
+
info("stdout:");
|
|
747
|
+
console.log(result.output.stdout);
|
|
487
748
|
}
|
|
488
749
|
|
|
489
|
-
|
|
750
|
+
// Show stderr (the actual error output)
|
|
751
|
+
if (result.output?.stderr?.trim()) {
|
|
490
752
|
newline();
|
|
491
|
-
|
|
492
|
-
|
|
753
|
+
error("stderr:");
|
|
754
|
+
console.log(result.output.stderr);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Show additional error details if present (and different from stderr)
|
|
758
|
+
if (result.error?.details) {
|
|
759
|
+
const detailsStr = JSON.stringify(result.error.details, null, 2);
|
|
760
|
+
// Only show if it adds new information (not just duplicating stderr)
|
|
761
|
+
const stderrInDetails = result.error.details.stderr;
|
|
762
|
+
if (!stderrInDetails || stderrInDetails !== result.output?.stderr) {
|
|
763
|
+
newline();
|
|
764
|
+
dim("Additional details:");
|
|
765
|
+
dim(detailsStr);
|
|
766
|
+
}
|
|
493
767
|
}
|
|
494
768
|
}
|
|
495
769
|
}
|
|
@@ -499,25 +773,48 @@ function displayResult(result: ExecutionResult, options: RunOptions): void {
|
|
|
499
773
|
*/
|
|
500
774
|
async function runHandler(tool: string, options: RunOptions, ctx: CommandContext): Promise<void> {
|
|
501
775
|
let resolution: ToolResolution | null = null;
|
|
776
|
+
let resolveResult: ReturnType<typeof tryResolveToolDetailed> | null = null;
|
|
502
777
|
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
778
|
+
// Check if --remote flag is valid (requires namespace/name format)
|
|
779
|
+
const isRegistryFormat = tool.includes("/") && !tool.startsWith("/") && !tool.startsWith(".");
|
|
780
|
+
if (options.remote && !isRegistryFormat) {
|
|
781
|
+
throw new ValidationError(
|
|
782
|
+
`--remote requires a registry tool name (e.g., user/tool), got: ${tool}`
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Skip local resolution if --remote is set
|
|
787
|
+
if (!options.remote) {
|
|
788
|
+
// First, try to resolve locally (project → user → cache)
|
|
789
|
+
if (!options.verbose) {
|
|
790
|
+
resolveResult = tryResolveToolDetailed(tool, { startDir: ctx.cwd });
|
|
791
|
+
resolution = resolveResult.resolution;
|
|
512
792
|
} else {
|
|
513
|
-
spinner.
|
|
793
|
+
const spinner = clack.spinner();
|
|
794
|
+
spinner.start(`Resolving tool: ${tool}`);
|
|
795
|
+
resolveResult = tryResolveToolDetailed(tool, { startDir: ctx.cwd });
|
|
796
|
+
resolution = resolveResult.resolution;
|
|
797
|
+
if (resolution) {
|
|
798
|
+
spinner.stop(`${symbols.success} Resolved: ${tool}`);
|
|
799
|
+
} else {
|
|
800
|
+
spinner.stop(`${symbols.info} Checking registry...`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// If manifest was found but had errors, throw a descriptive error immediately
|
|
805
|
+
if (!resolution && resolveResult?.manifestFound && resolveResult?.error) {
|
|
806
|
+
const errorMessage = resolveResult.error.message;
|
|
807
|
+
const manifestPath = resolveResult.manifestPath;
|
|
808
|
+
throw new ManifestError(
|
|
809
|
+
`Invalid manifest${manifestPath ? ` at ${manifestPath}` : ""}: ${errorMessage}`
|
|
810
|
+
);
|
|
514
811
|
}
|
|
515
812
|
}
|
|
516
813
|
|
|
517
814
|
// If not found locally and --local flag not set, try fetching from registry
|
|
518
815
|
if (!resolution && !options.local) {
|
|
519
816
|
// Check if this looks like a tool name (namespace/name format)
|
|
520
|
-
if (
|
|
817
|
+
if (isRegistryFormat) {
|
|
521
818
|
resolution = !options.verbose
|
|
522
819
|
? await fetchAndCacheTool(tool, options, ctx)
|
|
523
820
|
: await withSpinner(
|
|
@@ -530,15 +827,30 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
530
827
|
|
|
531
828
|
if (!resolution) {
|
|
532
829
|
if (options.local) {
|
|
533
|
-
throw new ToolNotFoundError(
|
|
830
|
+
throw new ToolNotFoundError(tool, {
|
|
831
|
+
localOnly: true,
|
|
832
|
+
...(resolveResult?.searchedLocations && {
|
|
833
|
+
searchedLocations: resolveResult.searchedLocations,
|
|
834
|
+
}),
|
|
835
|
+
});
|
|
534
836
|
}
|
|
535
|
-
throw new ToolNotFoundError(tool
|
|
837
|
+
throw new ToolNotFoundError(tool, {
|
|
838
|
+
...(resolveResult?.searchedLocations && {
|
|
839
|
+
searchedLocations: resolveResult.searchedLocations,
|
|
840
|
+
}),
|
|
841
|
+
});
|
|
536
842
|
}
|
|
537
843
|
|
|
538
844
|
const manifest = resolution.manifest;
|
|
539
845
|
|
|
540
|
-
// Parse inputs
|
|
541
|
-
const
|
|
846
|
+
// Parse --input flags to separate key=value params from path inputs
|
|
847
|
+
const { params: pathParams, inputPaths } = parseInputPaths(options.input);
|
|
848
|
+
|
|
849
|
+
// Parse other input sources (--args, --input-file)
|
|
850
|
+
const otherInputs = parseInputArgs(options.args, options.inputFile, undefined);
|
|
851
|
+
|
|
852
|
+
// Merge inputs: path params override other inputs
|
|
853
|
+
const inputs = { ...otherInputs, ...pathParams };
|
|
542
854
|
|
|
543
855
|
// Apply defaults from schema
|
|
544
856
|
const inputsWithDefaults = manifest.inputSchema
|
|
@@ -555,6 +867,44 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
555
867
|
// Use coerced values from validation (or inputs with defaults)
|
|
556
868
|
const finalInputs = validation.coercedValues ?? inputsWithDefaults;
|
|
557
869
|
|
|
870
|
+
// Validate output path if provided
|
|
871
|
+
if (options.output) {
|
|
872
|
+
const outputDir = dirname(resolve(options.output));
|
|
873
|
+
if (!existsSync(outputDir)) {
|
|
874
|
+
mkdirSync(outputDir, { recursive: true });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Validate --apply flag requirements
|
|
879
|
+
// --apply requires exactly one input directory and output path
|
|
880
|
+
let applyInputPath: string | undefined;
|
|
881
|
+
if (options.apply) {
|
|
882
|
+
// Must have exactly one directory input
|
|
883
|
+
const dirInputs = inputPaths.filter((p) => p.type === "directory" && !p.name);
|
|
884
|
+
if (dirInputs.length !== 1) {
|
|
885
|
+
throw new ValidationError(
|
|
886
|
+
"--apply requires exactly one unnamed directory input (e.g., --input ./src)"
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
applyInputPath = dirInputs[0]?.path;
|
|
890
|
+
|
|
891
|
+
// Must have output path
|
|
892
|
+
if (!options.output) {
|
|
893
|
+
throw new ValidationError("--apply requires --output to be specified");
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Output should point to same location as input for in-place apply
|
|
897
|
+
const resolvedOutput = resolve(options.output);
|
|
898
|
+
if (applyInputPath && resolvedOutput !== applyInputPath) {
|
|
899
|
+
// Warn but allow - user might want to apply to a different location
|
|
900
|
+
if (options.verbose) {
|
|
901
|
+
dim(
|
|
902
|
+
`Note: --apply with different input/output paths will copy results to ${resolvedOutput}`
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
558
908
|
// Check if this is an instruction-based tool (no command)
|
|
559
909
|
if (!manifest.command) {
|
|
560
910
|
// For instruction tools, just display the markdown body
|
|
@@ -630,9 +980,44 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
630
980
|
}
|
|
631
981
|
}
|
|
632
982
|
|
|
983
|
+
// Build mount configuration
|
|
984
|
+
// Tool source directory is mounted to /workspace
|
|
985
|
+
const mountDirs: Record<string, string> = {
|
|
986
|
+
[resolution.sourceDir]: "/workspace",
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
// Add input paths to mount configuration
|
|
990
|
+
for (const input of inputPaths) {
|
|
991
|
+
if (input.name) {
|
|
992
|
+
// Named input: --input left=./old → /inputs/left
|
|
993
|
+
mountDirs[input.path] = `/inputs/${input.name}`;
|
|
994
|
+
} else if (input.type === "file") {
|
|
995
|
+
// Single file: mount parent dir and we'll use withFile in provider
|
|
996
|
+
// For now, mount as /input/<filename>
|
|
997
|
+
// Note: Dagger's withFile is better but requires provider changes
|
|
998
|
+
mountDirs[input.path] = `/input/${basename(input.path)}`;
|
|
999
|
+
} else {
|
|
1000
|
+
// Single directory: --input ./data → /input
|
|
1001
|
+
mountDirs[input.path] = "/input";
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Debug mode - show detailed parameter resolution info
|
|
1006
|
+
if (options.debug) {
|
|
1007
|
+
displayDebugInfo(manifest, inputs, inputsWithDefaults, finalInputs, envVars, command);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
633
1010
|
// Dry run mode
|
|
634
1011
|
if (options.dryRun) {
|
|
635
|
-
displayDryRun(
|
|
1012
|
+
displayDryRun(
|
|
1013
|
+
manifest,
|
|
1014
|
+
finalInputs,
|
|
1015
|
+
command,
|
|
1016
|
+
envVars,
|
|
1017
|
+
inputPaths,
|
|
1018
|
+
options.output,
|
|
1019
|
+
options.apply
|
|
1020
|
+
);
|
|
636
1021
|
return;
|
|
637
1022
|
}
|
|
638
1023
|
|
|
@@ -647,23 +1032,41 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
647
1032
|
|
|
648
1033
|
const provider = new DaggerExecutionProvider(providerConfig);
|
|
649
1034
|
|
|
1035
|
+
// For --apply, we export to a temp directory first, then atomically replace
|
|
1036
|
+
let tempOutputDir: string | undefined;
|
|
1037
|
+
if (options.apply && options.output) {
|
|
1038
|
+
tempOutputDir = mkdtempSync(join(tmpdir(), "enact-apply-"));
|
|
1039
|
+
}
|
|
1040
|
+
|
|
650
1041
|
try {
|
|
651
1042
|
await provider.initialize();
|
|
652
1043
|
|
|
653
|
-
const executeTask = () =>
|
|
654
|
-
|
|
1044
|
+
const executeTask = () => {
|
|
1045
|
+
const execOptions: {
|
|
1046
|
+
mountDirs: Record<string, string>;
|
|
1047
|
+
inputPaths: typeof inputPaths;
|
|
1048
|
+
outputPath?: string;
|
|
1049
|
+
} = {
|
|
1050
|
+
mountDirs,
|
|
1051
|
+
inputPaths,
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
// When using --apply, export to temp dir first
|
|
1055
|
+
if (tempOutputDir) {
|
|
1056
|
+
execOptions.outputPath = tempOutputDir;
|
|
1057
|
+
} else if (options.output) {
|
|
1058
|
+
execOptions.outputPath = resolve(options.output);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return provider.execute(
|
|
655
1062
|
manifest,
|
|
656
1063
|
{
|
|
657
1064
|
params: finalInputs,
|
|
658
1065
|
envOverrides: envVars,
|
|
659
1066
|
},
|
|
660
|
-
|
|
661
|
-
// Mount the tool directory to /work in the container
|
|
662
|
-
mountDirs: {
|
|
663
|
-
[resolution.sourceDir]: "/work",
|
|
664
|
-
},
|
|
665
|
-
}
|
|
1067
|
+
execOptions
|
|
666
1068
|
);
|
|
1069
|
+
};
|
|
667
1070
|
|
|
668
1071
|
// Build a descriptive message - container may need to be pulled
|
|
669
1072
|
const containerImage = manifest.from ?? "node:18-alpine";
|
|
@@ -678,8 +1081,35 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
678
1081
|
if (!result.success) {
|
|
679
1082
|
process.exit(EXIT_EXECUTION_ERROR);
|
|
680
1083
|
}
|
|
1084
|
+
|
|
1085
|
+
// Apply atomically if --apply was used and execution succeeded
|
|
1086
|
+
if (options.apply && tempOutputDir && options.output) {
|
|
1087
|
+
const targetPath = resolve(options.output);
|
|
1088
|
+
|
|
1089
|
+
if (options.verbose) {
|
|
1090
|
+
info(`Applying changes atomically to ${targetPath}...`);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
atomicReplace(targetPath, tempOutputDir);
|
|
1095
|
+
if (options.verbose) {
|
|
1096
|
+
success(`Changes applied to ${targetPath}`);
|
|
1097
|
+
}
|
|
1098
|
+
} catch (applyErr) {
|
|
1099
|
+
error(`Failed to apply changes: ${formatError(applyErr)}`);
|
|
1100
|
+
dim("Original directory preserved. Changes available in temp directory.");
|
|
1101
|
+
throw applyErr;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
681
1104
|
} finally {
|
|
682
|
-
//
|
|
1105
|
+
// Clean up temp directory if it exists
|
|
1106
|
+
if (tempOutputDir && existsSync(tempOutputDir)) {
|
|
1107
|
+
try {
|
|
1108
|
+
rmSync(tempOutputDir, { recursive: true, force: true });
|
|
1109
|
+
} catch {
|
|
1110
|
+
// Ignore cleanup errors
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
683
1113
|
}
|
|
684
1114
|
}
|
|
685
1115
|
|
|
@@ -715,11 +1145,21 @@ export function configureRunCommand(program: Command): void {
|
|
|
715
1145
|
.argument("<tool>", "Tool to run (name, path, or '.' for current directory)")
|
|
716
1146
|
.option("-a, --args <json>", "Input arguments as JSON string (recommended)")
|
|
717
1147
|
.option("-f, --input-file <path>", "Load input arguments from JSON file")
|
|
718
|
-
.option(
|
|
1148
|
+
.option(
|
|
1149
|
+
"-i, --input <value...>",
|
|
1150
|
+
"Input: key=value params, ./path for data, or name=./path for named inputs"
|
|
1151
|
+
)
|
|
1152
|
+
.option("-o, --output <path>", "Export /output directory to this path after execution")
|
|
1153
|
+
.option(
|
|
1154
|
+
"--apply",
|
|
1155
|
+
"Apply output back to input directory atomically (use with --input and --output pointing to same path)"
|
|
1156
|
+
)
|
|
719
1157
|
.option("-t, --timeout <duration>", "Execution timeout (e.g., 30s, 5m)")
|
|
720
1158
|
.option("--no-cache", "Disable container caching")
|
|
721
1159
|
.option("--local", "Only resolve from local sources")
|
|
1160
|
+
.option("-r, --remote", "Skip local resolution and fetch from registry")
|
|
722
1161
|
.option("--dry-run", "Show what would be executed without running")
|
|
1162
|
+
.option("--debug", "Show detailed parameter and environment variable resolution")
|
|
723
1163
|
.option("-v, --verbose", "Show progress spinners and detailed output")
|
|
724
1164
|
.option("--json", "Output result as JSON")
|
|
725
1165
|
.action(async (tool: string, options: RunOptions) => {
|