@enactprotocol/cli 2.1.5 → 2.1.6
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 +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +18 -4
- package/dist/commands/init/index.js.map +1 -1
- package/dist/commands/learn/index.d.ts +12 -0
- package/dist/commands/learn/index.d.ts.map +1 -0
- package/dist/commands/learn/index.js +90 -0
- package/dist/commands/learn/index.js.map +1 -0
- package/dist/commands/run/index.d.ts.map +1 -1
- package/dist/commands/run/index.js +36 -6
- package/dist/commands/run/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/index.ts +1 -0
- package/src/commands/init/index.ts +18 -4
- package/src/commands/learn/index.ts +107 -0
- package/src/commands/run/index.ts +39 -5
- package/src/index.ts +4 -2
- package/tests/commands/learn.test.ts +231 -0
- package/tests/commands/run.test.ts +70 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* enact learn command
|
|
3
|
+
*
|
|
4
|
+
* Display the documentation (enact.md) for a tool.
|
|
5
|
+
* Fetches and displays the raw manifest content for easy reading.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createApiClient, getToolInfo, getToolVersion } from "@enactprotocol/api";
|
|
9
|
+
import { loadConfig } from "@enactprotocol/shared";
|
|
10
|
+
import type { Command } from "commander";
|
|
11
|
+
import type { CommandContext, GlobalOptions } from "../../types";
|
|
12
|
+
import { dim, error, formatError, header, json, newline } from "../../utils";
|
|
13
|
+
|
|
14
|
+
interface LearnOptions extends GlobalOptions {
|
|
15
|
+
ver?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Learn command handler
|
|
20
|
+
*/
|
|
21
|
+
async function learnHandler(
|
|
22
|
+
toolName: string,
|
|
23
|
+
options: LearnOptions,
|
|
24
|
+
_ctx: CommandContext
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
const registryUrl =
|
|
28
|
+
process.env.ENACT_REGISTRY_URL ??
|
|
29
|
+
config.registry?.url ??
|
|
30
|
+
"https://siikwkfgsmouioodghho.supabase.co/functions/v1";
|
|
31
|
+
const authToken = config.registry?.authToken;
|
|
32
|
+
const client = createApiClient({
|
|
33
|
+
baseUrl: registryUrl,
|
|
34
|
+
authToken: authToken,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Get the version to fetch - either specified or latest
|
|
39
|
+
let version = options.ver;
|
|
40
|
+
if (!version) {
|
|
41
|
+
const toolInfo = await getToolInfo(client, toolName);
|
|
42
|
+
version = toolInfo.latestVersion;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Get the version info which includes rawManifest
|
|
46
|
+
const versionInfo = await getToolVersion(client, toolName, version);
|
|
47
|
+
|
|
48
|
+
if (options.json) {
|
|
49
|
+
json({
|
|
50
|
+
name: toolName,
|
|
51
|
+
version: versionInfo.version,
|
|
52
|
+
documentation: versionInfo.rawManifest ?? null,
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!versionInfo.rawManifest) {
|
|
58
|
+
error(`No documentation found for ${toolName}@${version}`);
|
|
59
|
+
dim("This tool may not have an enact.md file.");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Display the documentation
|
|
64
|
+
header(`${toolName}@${version}`);
|
|
65
|
+
newline();
|
|
66
|
+
console.log(versionInfo.rawManifest);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof Error) {
|
|
69
|
+
if (err.message.includes("not_found") || err.message.includes("404")) {
|
|
70
|
+
error(`Tool not found: ${toolName}`);
|
|
71
|
+
dim("Check the tool name or search with: enact search <query>");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (err.message.includes("fetch")) {
|
|
75
|
+
error("Unable to connect to registry. Check your internet connection.");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Configure the learn command
|
|
85
|
+
*/
|
|
86
|
+
export function configureLearnCommand(program: Command): void {
|
|
87
|
+
program
|
|
88
|
+
.command("learn <tool>")
|
|
89
|
+
.description("Display documentation (enact.md) for a tool")
|
|
90
|
+
.option("--ver <version>", "Show documentation for a specific version")
|
|
91
|
+
.option("--json", "Output as JSON")
|
|
92
|
+
.action(async (toolName: string, options: LearnOptions) => {
|
|
93
|
+
const ctx: CommandContext = {
|
|
94
|
+
cwd: process.cwd(),
|
|
95
|
+
options,
|
|
96
|
+
isCI: Boolean(process.env.CI),
|
|
97
|
+
isInteractive: process.stdout.isTTY ?? false,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await learnHandler(toolName, options, ctx);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
error(formatError(err));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
|
|
62
62
|
interface RunOptions extends GlobalOptions {
|
|
63
63
|
args?: string;
|
|
64
|
+
inputFile?: string;
|
|
64
65
|
input?: string[];
|
|
65
66
|
timeout?: string;
|
|
66
67
|
noCache?: boolean;
|
|
@@ -70,14 +71,46 @@ interface RunOptions extends GlobalOptions {
|
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
73
|
* Parse input arguments from various formats
|
|
74
|
+
*
|
|
75
|
+
* Priority order (later sources override earlier):
|
|
76
|
+
* 1. --input-file (JSON file)
|
|
77
|
+
* 2. --args (inline JSON)
|
|
78
|
+
* 3. --input (key=value pairs)
|
|
79
|
+
*
|
|
80
|
+
* Recommended for agents: Use --args or --input-file with JSON
|
|
73
81
|
*/
|
|
74
82
|
function parseInputArgs(
|
|
75
83
|
argsJson: string | undefined,
|
|
84
|
+
inputFile: string | undefined,
|
|
76
85
|
inputFlags: string[] | undefined
|
|
77
86
|
): Record<string, unknown> {
|
|
78
87
|
const inputs: Record<string, unknown> = {};
|
|
79
88
|
|
|
80
|
-
// Parse --
|
|
89
|
+
// Parse --input-file JSON file (loaded first, can be overridden)
|
|
90
|
+
if (inputFile) {
|
|
91
|
+
try {
|
|
92
|
+
const { readFileSync, existsSync } = require("node:fs");
|
|
93
|
+
const { resolve } = require("node:path");
|
|
94
|
+
const filePath = resolve(inputFile);
|
|
95
|
+
|
|
96
|
+
if (!existsSync(filePath)) {
|
|
97
|
+
throw new Error(`Input file not found: ${inputFile}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const content = readFileSync(filePath, "utf-8");
|
|
101
|
+
const parsed = JSON.parse(content);
|
|
102
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
103
|
+
Object.assign(inputs, parsed);
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err instanceof Error && err.message.startsWith("Input file not found")) {
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`Invalid JSON in input file: ${formatError(err)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Parse --args JSON (overrides file)
|
|
81
114
|
if (argsJson) {
|
|
82
115
|
try {
|
|
83
116
|
const parsed = JSON.parse(argsJson);
|
|
@@ -89,7 +122,7 @@ function parseInputArgs(
|
|
|
89
122
|
}
|
|
90
123
|
}
|
|
91
124
|
|
|
92
|
-
// Parse --input key=value pairs
|
|
125
|
+
// Parse --input key=value pairs (overrides both)
|
|
93
126
|
if (inputFlags) {
|
|
94
127
|
for (const input of inputFlags) {
|
|
95
128
|
const eqIndex = input.indexOf("=");
|
|
@@ -501,7 +534,7 @@ async function runHandler(tool: string, options: RunOptions, ctx: CommandContext
|
|
|
501
534
|
const manifest = resolution.manifest;
|
|
502
535
|
|
|
503
536
|
// Parse inputs
|
|
504
|
-
const inputs = parseInputArgs(options.args, options.input);
|
|
537
|
+
const inputs = parseInputArgs(options.args, options.inputFile, options.input);
|
|
505
538
|
|
|
506
539
|
// Apply defaults from schema
|
|
507
540
|
const inputsWithDefaults = manifest.inputSchema
|
|
@@ -669,8 +702,9 @@ export function configureRunCommand(program: Command): void {
|
|
|
669
702
|
.command("run")
|
|
670
703
|
.description("Execute a tool with its manifest-defined command")
|
|
671
704
|
.argument("<tool>", "Tool to run (name, path, or '.' for current directory)")
|
|
672
|
-
.option("-a, --args <json>", "Input arguments as JSON")
|
|
673
|
-
.option("-
|
|
705
|
+
.option("-a, --args <json>", "Input arguments as JSON string (recommended)")
|
|
706
|
+
.option("-f, --input-file <path>", "Load input arguments from JSON file")
|
|
707
|
+
.option("-i, --input <key=value...>", "Input arguments as key=value pairs (simple values only)")
|
|
674
708
|
.option("-t, --timeout <duration>", "Execution timeout (e.g., 30s, 5m)")
|
|
675
709
|
.option("--no-cache", "Disable container caching")
|
|
676
710
|
.option("--local", "Only resolve from local sources")
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
configureInitCommand,
|
|
20
20
|
configureInspectCommand,
|
|
21
21
|
configureInstallCommand,
|
|
22
|
+
configureLearnCommand,
|
|
22
23
|
configureListCommand,
|
|
23
24
|
configurePublishCommand,
|
|
24
25
|
configureReportCommand,
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
} from "./commands";
|
|
33
34
|
import { error, formatError } from "./utils";
|
|
34
35
|
|
|
35
|
-
export const version = "2.1.
|
|
36
|
+
export const version = "2.1.6";
|
|
36
37
|
|
|
37
38
|
// Export types for external use
|
|
38
39
|
export type { GlobalOptions, CommandContext } from "./types";
|
|
@@ -47,7 +48,7 @@ async function main() {
|
|
|
47
48
|
program
|
|
48
49
|
.name("enact")
|
|
49
50
|
.description("Enact - Verified, portable protocol for AI-executable tools")
|
|
50
|
-
.version(version);
|
|
51
|
+
.version(version, "-v, --version", "output the version number");
|
|
51
52
|
|
|
52
53
|
// Configure all commands
|
|
53
54
|
configureSetupCommand(program);
|
|
@@ -63,6 +64,7 @@ async function main() {
|
|
|
63
64
|
// Registry commands (Phase 8)
|
|
64
65
|
configureSearchCommand(program);
|
|
65
66
|
configureGetCommand(program);
|
|
67
|
+
configureLearnCommand(program);
|
|
66
68
|
configurePublishCommand(program);
|
|
67
69
|
configureAuthCommand(program);
|
|
68
70
|
configureCacheCommand(program);
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the learn command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import type { ToolVersionInfo } from "@enactprotocol/api";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { configureLearnCommand } from "../../src/commands/learn";
|
|
9
|
+
|
|
10
|
+
describe("learn command", () => {
|
|
11
|
+
describe("command configuration", () => {
|
|
12
|
+
test("configures learn command on program", () => {
|
|
13
|
+
const program = new Command();
|
|
14
|
+
configureLearnCommand(program);
|
|
15
|
+
|
|
16
|
+
const learnCmd = program.commands.find((cmd) => cmd.name() === "learn");
|
|
17
|
+
expect(learnCmd).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("has correct description", () => {
|
|
21
|
+
const program = new Command();
|
|
22
|
+
configureLearnCommand(program);
|
|
23
|
+
|
|
24
|
+
const learnCmd = program.commands.find((cmd) => cmd.name() === "learn");
|
|
25
|
+
expect(learnCmd?.description()).toBe("Display documentation (enact.md) for a tool");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("accepts tool argument", () => {
|
|
29
|
+
const program = new Command();
|
|
30
|
+
configureLearnCommand(program);
|
|
31
|
+
|
|
32
|
+
const learnCmd = program.commands.find((cmd) => cmd.name() === "learn");
|
|
33
|
+
const args = learnCmd?.registeredArguments ?? [];
|
|
34
|
+
expect(args.length).toBeGreaterThan(0);
|
|
35
|
+
expect(args[0]?.name()).toBe("tool");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("has --ver option for specifying version", () => {
|
|
39
|
+
const program = new Command();
|
|
40
|
+
configureLearnCommand(program);
|
|
41
|
+
|
|
42
|
+
const learnCmd = program.commands.find((cmd) => cmd.name() === "learn");
|
|
43
|
+
const opts = learnCmd?.options ?? [];
|
|
44
|
+
const verOpt = opts.find((o) => o.long === "--ver");
|
|
45
|
+
expect(verOpt).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("has --json option", () => {
|
|
49
|
+
const program = new Command();
|
|
50
|
+
configureLearnCommand(program);
|
|
51
|
+
|
|
52
|
+
const learnCmd = program.commands.find((cmd) => cmd.name() === "learn");
|
|
53
|
+
const opts = learnCmd?.options ?? [];
|
|
54
|
+
const jsonOpt = opts.find((o) => o.long === "--json");
|
|
55
|
+
expect(jsonOpt).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("does not have --verbose option (always shows full docs)", () => {
|
|
59
|
+
const program = new Command();
|
|
60
|
+
configureLearnCommand(program);
|
|
61
|
+
|
|
62
|
+
const learnCmd = program.commands.find((cmd) => cmd.name() === "learn");
|
|
63
|
+
const opts = learnCmd?.options ?? [];
|
|
64
|
+
const verboseOpt = opts.find((o) => o.long === "--verbose");
|
|
65
|
+
expect(verboseOpt).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("tool name parsing", () => {
|
|
70
|
+
test("parses simple tool name", () => {
|
|
71
|
+
const toolName = "my-tool";
|
|
72
|
+
expect(toolName).not.toContain("@");
|
|
73
|
+
expect(toolName).not.toContain("/");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("parses namespaced tool name", () => {
|
|
77
|
+
const toolName = "enact/context7/docs";
|
|
78
|
+
const parts = toolName.split("/");
|
|
79
|
+
expect(parts[0]).toBe("enact");
|
|
80
|
+
expect(parts[1]).toBe("context7");
|
|
81
|
+
expect(parts[2]).toBe("docs");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("parses scoped tool name", () => {
|
|
85
|
+
const toolName = "@org/my-tool";
|
|
86
|
+
expect(toolName.startsWith("@")).toBe(true);
|
|
87
|
+
const parts = toolName.slice(1).split("/");
|
|
88
|
+
expect(parts[0]).toBe("org");
|
|
89
|
+
expect(parts[1]).toBe("my-tool");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("documentation content", () => {
|
|
94
|
+
test("ToolVersionInfo type includes rawManifest field for enact.md content", () => {
|
|
95
|
+
const mockVersion: ToolVersionInfo = {
|
|
96
|
+
name: "test/tool",
|
|
97
|
+
version: "1.0.0",
|
|
98
|
+
description: "Test tool",
|
|
99
|
+
license: "MIT",
|
|
100
|
+
yanked: false,
|
|
101
|
+
manifest: { enact: "2.0.0" },
|
|
102
|
+
rawManifest: "---\nenact: 2.0.0\n---\n# Test Tool\n\nThis is a test tool.",
|
|
103
|
+
bundle: {
|
|
104
|
+
hash: "sha256:abc123",
|
|
105
|
+
size: 1024,
|
|
106
|
+
downloadUrl: "https://example.com/bundle.tar.gz",
|
|
107
|
+
},
|
|
108
|
+
attestations: [],
|
|
109
|
+
publishedBy: { username: "testuser" },
|
|
110
|
+
publishedAt: new Date(),
|
|
111
|
+
downloads: 100,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
expect(mockVersion.rawManifest).toBeDefined();
|
|
115
|
+
expect(mockVersion.rawManifest).toContain("# Test Tool");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("ToolVersionInfo allows undefined rawManifest", () => {
|
|
119
|
+
const mockVersion: ToolVersionInfo = {
|
|
120
|
+
name: "test/tool",
|
|
121
|
+
version: "1.0.0",
|
|
122
|
+
description: "Test tool",
|
|
123
|
+
license: "MIT",
|
|
124
|
+
yanked: false,
|
|
125
|
+
manifest: { enact: "2.0.0" },
|
|
126
|
+
// rawManifest is optional - not provided
|
|
127
|
+
bundle: {
|
|
128
|
+
hash: "sha256:abc123",
|
|
129
|
+
size: 1024,
|
|
130
|
+
downloadUrl: "https://example.com/bundle.tar.gz",
|
|
131
|
+
},
|
|
132
|
+
attestations: [],
|
|
133
|
+
publishedBy: { username: "testuser" },
|
|
134
|
+
publishedAt: new Date(),
|
|
135
|
+
downloads: 100,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
expect(mockVersion.rawManifest).toBeUndefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("enact.md content should contain frontmatter and markdown", () => {
|
|
142
|
+
const enactMdContent = `---
|
|
143
|
+
enact: 2.0.0
|
|
144
|
+
name: test/tool
|
|
145
|
+
version: 1.0.0
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
# Test Tool
|
|
149
|
+
|
|
150
|
+
Documentation here.`;
|
|
151
|
+
|
|
152
|
+
// Verify frontmatter is present
|
|
153
|
+
expect(enactMdContent).toContain("---");
|
|
154
|
+
expect(enactMdContent).toContain("enact: 2.0.0");
|
|
155
|
+
|
|
156
|
+
// Verify markdown content
|
|
157
|
+
expect(enactMdContent).toContain("# Test Tool");
|
|
158
|
+
expect(enactMdContent).toContain("Documentation here.");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("documentation includes parameter descriptions", () => {
|
|
162
|
+
const enactMdContent = `---
|
|
163
|
+
enact: 2.0.0
|
|
164
|
+
name: enact/context7/docs
|
|
165
|
+
version: 1.0.0
|
|
166
|
+
inputSchema:
|
|
167
|
+
type: object
|
|
168
|
+
properties:
|
|
169
|
+
library_name:
|
|
170
|
+
type: string
|
|
171
|
+
description: The name of the library to fetch documentation for
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
# Context7 Documentation Fetcher
|
|
175
|
+
|
|
176
|
+
Fetches up-to-date documentation for any library.
|
|
177
|
+
|
|
178
|
+
## Parameters
|
|
179
|
+
|
|
180
|
+
- **library_name** (required): The name of the library to fetch documentation for
|
|
181
|
+
`;
|
|
182
|
+
|
|
183
|
+
expect(enactMdContent).toContain("library_name");
|
|
184
|
+
expect(enactMdContent).toContain("Parameters");
|
|
185
|
+
expect(enactMdContent).toContain("required");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("documentation includes usage examples", () => {
|
|
189
|
+
const enactMdContent = `---
|
|
190
|
+
enact: 2.0.0
|
|
191
|
+
name: test/tool
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
# Test Tool
|
|
195
|
+
|
|
196
|
+
## Usage
|
|
197
|
+
|
|
198
|
+
\`\`\`bash
|
|
199
|
+
enact run test/tool --input '{"query": "hello"}'
|
|
200
|
+
\`\`\`
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
expect(enactMdContent).toContain("## Usage");
|
|
204
|
+
expect(enactMdContent).toContain("enact run");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("JSON output format", () => {
|
|
209
|
+
test("JSON output includes name, version, and documentation", () => {
|
|
210
|
+
const jsonOutput = {
|
|
211
|
+
name: "enact/context7/docs",
|
|
212
|
+
version: "1.0.1",
|
|
213
|
+
documentation: "---\nenact: 2.0.0\n---\n# Context7 Docs\n\nFetches documentation.",
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
expect(jsonOutput.name).toBe("enact/context7/docs");
|
|
217
|
+
expect(jsonOutput.version).toBe("1.0.1");
|
|
218
|
+
expect(jsonOutput.documentation).toContain("# Context7 Docs");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("JSON output handles missing documentation", () => {
|
|
222
|
+
const jsonOutput = {
|
|
223
|
+
name: "test/tool",
|
|
224
|
+
version: "1.0.0",
|
|
225
|
+
documentation: null,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
expect(jsonOutput.documentation).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
6
|
-
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import { configureRunCommand } from "../../src/commands/run";
|
|
@@ -71,6 +71,16 @@ describe("run command", () => {
|
|
|
71
71
|
expect(inputOpt).toBeDefined();
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
test("has --input-file option for JSON file input", () => {
|
|
75
|
+
const program = new Command();
|
|
76
|
+
configureRunCommand(program);
|
|
77
|
+
|
|
78
|
+
const runCmd = program.commands.find((cmd) => cmd.name() === "run");
|
|
79
|
+
const opts = runCmd?.options ?? [];
|
|
80
|
+
const inputFileOpt = opts.find((o) => o.long === "--input-file");
|
|
81
|
+
expect(inputFileOpt).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
74
84
|
test("has --timeout option", () => {
|
|
75
85
|
const program = new Command();
|
|
76
86
|
configureRunCommand(program);
|
|
@@ -228,4 +238,63 @@ describe("run command", () => {
|
|
|
228
238
|
expect(parseTimeout("30")).toBe(30000);
|
|
229
239
|
});
|
|
230
240
|
});
|
|
241
|
+
|
|
242
|
+
describe("input file handling", () => {
|
|
243
|
+
test("JSON input file can be parsed", () => {
|
|
244
|
+
// Create a test JSON file
|
|
245
|
+
const inputFilePath = join(FIXTURES_DIR, "test-inputs.json");
|
|
246
|
+
const inputData = { name: "Alice", count: 5, nested: { key: "value" } };
|
|
247
|
+
writeFileSync(inputFilePath, JSON.stringify(inputData));
|
|
248
|
+
|
|
249
|
+
// Verify the file can be read and parsed
|
|
250
|
+
const content = require("node:fs").readFileSync(inputFilePath, "utf-8");
|
|
251
|
+
const parsed = JSON.parse(content);
|
|
252
|
+
|
|
253
|
+
expect(parsed.name).toBe("Alice");
|
|
254
|
+
expect(parsed.count).toBe(5);
|
|
255
|
+
expect(parsed.nested.key).toBe("value");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("JSON input file with optional params can omit them", () => {
|
|
259
|
+
// This is the recommended pattern for optional params
|
|
260
|
+
const inputFilePath = join(FIXTURES_DIR, "optional-inputs.json");
|
|
261
|
+
// Only required param provided, optional params omitted
|
|
262
|
+
const inputData = { name: "Alice" };
|
|
263
|
+
writeFileSync(inputFilePath, JSON.stringify(inputData));
|
|
264
|
+
|
|
265
|
+
const content = require("node:fs").readFileSync(inputFilePath, "utf-8");
|
|
266
|
+
const parsed = JSON.parse(content);
|
|
267
|
+
|
|
268
|
+
expect(parsed.name).toBe("Alice");
|
|
269
|
+
expect(parsed.greeting).toBeUndefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("JSON input file with explicit empty values", () => {
|
|
273
|
+
// User can explicitly set empty values for optional params
|
|
274
|
+
const inputFilePath = join(FIXTURES_DIR, "explicit-empty.json");
|
|
275
|
+
const inputData = { name: "Alice", prefix: "", suffix: null };
|
|
276
|
+
writeFileSync(inputFilePath, JSON.stringify(inputData));
|
|
277
|
+
|
|
278
|
+
const content = require("node:fs").readFileSync(inputFilePath, "utf-8");
|
|
279
|
+
const parsed = JSON.parse(content);
|
|
280
|
+
|
|
281
|
+
expect(parsed.name).toBe("Alice");
|
|
282
|
+
expect(parsed.prefix).toBe("");
|
|
283
|
+
expect(parsed.suffix).toBeNull();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("input priority: --input overrides --args overrides --input-file", () => {
|
|
287
|
+
// Simulate the merge logic from parseInputArgs
|
|
288
|
+
const fromFile = { a: "file", b: "file", c: "file" };
|
|
289
|
+
const fromArgs = { b: "args", c: "args" };
|
|
290
|
+
const fromInput = { c: "input" };
|
|
291
|
+
|
|
292
|
+
// Merge in order: file -> args -> input
|
|
293
|
+
const merged = { ...fromFile, ...fromArgs, ...fromInput };
|
|
294
|
+
|
|
295
|
+
expect(merged.a).toBe("file"); // Only from file
|
|
296
|
+
expect(merged.b).toBe("args"); // Overridden by args
|
|
297
|
+
expect(merged.c).toBe("input"); // Overridden by input
|
|
298
|
+
});
|
|
299
|
+
});
|
|
231
300
|
});
|