@enactprotocol/cli 2.1.5 → 2.1.7

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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
6
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { Command } from "commander";
9
9
  import { configureInitCommand } from "../../src/commands/init";
@@ -107,7 +107,59 @@ describe("init command", () => {
107
107
  }
108
108
  });
109
109
 
110
- test("default mode creates enact.md", async () => {
110
+ test("default mode creates AGENTS.md for tool consumers", async () => {
111
+ const program = new Command();
112
+ program.exitOverride();
113
+ configureInitCommand(program);
114
+
115
+ const originalCwd = process.cwd();
116
+ process.chdir(testDir);
117
+
118
+ try {
119
+ await program.parseAsync(["node", "test", "init"]);
120
+ } catch {
121
+ // Command may throw due to exitOverride
122
+ } finally {
123
+ process.chdir(originalCwd);
124
+ }
125
+
126
+ const agentsPath = join(testDir, "AGENTS.md");
127
+ expect(existsSync(agentsPath)).toBe(true);
128
+
129
+ const content = readFileSync(agentsPath, "utf-8");
130
+ expect(content).toContain("enact search");
131
+ expect(content).toContain("enact install");
132
+ expect(content).toContain("Finding & Installing Tools");
133
+
134
+ // Should NOT create enact.md in default mode
135
+ const manifestPath = join(testDir, "enact.md");
136
+ expect(existsSync(manifestPath)).toBe(false);
137
+ });
138
+
139
+ test("default mode creates .enact/tools.json", async () => {
140
+ const program = new Command();
141
+ program.exitOverride();
142
+ configureInitCommand(program);
143
+
144
+ const originalCwd = process.cwd();
145
+ process.chdir(testDir);
146
+
147
+ try {
148
+ await program.parseAsync(["node", "test", "init"]);
149
+ } catch {
150
+ // Command may throw due to exitOverride
151
+ } finally {
152
+ process.chdir(originalCwd);
153
+ }
154
+
155
+ const toolsJsonPath = join(testDir, ".enact", "tools.json");
156
+ expect(existsSync(toolsJsonPath)).toBe(true);
157
+
158
+ const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
159
+ expect(content).toEqual({ tools: {} });
160
+ });
161
+
162
+ test("--tool mode creates enact.md", async () => {
111
163
  const program = new Command();
112
164
  program.exitOverride(); // Prevent process.exit
113
165
  configureInitCommand(program);
@@ -117,7 +169,7 @@ describe("init command", () => {
117
169
  process.chdir(testDir);
118
170
 
119
171
  try {
120
- await program.parseAsync(["node", "test", "init", "--name", "test/my-tool"]);
172
+ await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/my-tool"]);
121
173
  } catch {
122
174
  // Command may throw due to exitOverride
123
175
  } finally {
@@ -133,7 +185,7 @@ describe("init command", () => {
133
185
  expect(content).toContain("command:");
134
186
  });
135
187
 
136
- test("default mode creates AGENTS.md for tool development", async () => {
188
+ test("--tool mode creates AGENTS.md for tool development", async () => {
137
189
  const program = new Command();
138
190
  program.exitOverride();
139
191
  configureInitCommand(program);
@@ -142,7 +194,7 @@ describe("init command", () => {
142
194
  process.chdir(testDir);
143
195
 
144
196
  try {
145
- await program.parseAsync(["node", "test", "init", "--name", "test/my-tool"]);
197
+ await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/my-tool"]);
146
198
  } catch {
147
199
  // Command may throw due to exitOverride
148
200
  } finally {
@@ -215,6 +267,134 @@ describe("init command", () => {
215
267
  expect(existsSync(join(testDir, "AGENTS.md"))).toBe(false);
216
268
  });
217
269
 
270
+ test("--agent mode creates .enact/tools.json", async () => {
271
+ const program = new Command();
272
+ program.exitOverride();
273
+ configureInitCommand(program);
274
+
275
+ const originalCwd = process.cwd();
276
+ process.chdir(testDir);
277
+
278
+ try {
279
+ await program.parseAsync(["node", "test", "init", "--agent"]);
280
+ } catch {
281
+ // Command may throw due to exitOverride
282
+ } finally {
283
+ process.chdir(originalCwd);
284
+ }
285
+
286
+ const toolsJsonPath = join(testDir, ".enact", "tools.json");
287
+ expect(existsSync(toolsJsonPath)).toBe(true);
288
+
289
+ const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
290
+ expect(content).toEqual({ tools: {} });
291
+ });
292
+
293
+ test("--claude mode creates .enact/tools.json", async () => {
294
+ const program = new Command();
295
+ program.exitOverride();
296
+ configureInitCommand(program);
297
+
298
+ const originalCwd = process.cwd();
299
+ process.chdir(testDir);
300
+
301
+ try {
302
+ await program.parseAsync(["node", "test", "init", "--claude"]);
303
+ } catch {
304
+ // Command may throw due to exitOverride
305
+ } finally {
306
+ process.chdir(originalCwd);
307
+ }
308
+
309
+ const toolsJsonPath = join(testDir, ".enact", "tools.json");
310
+ expect(existsSync(toolsJsonPath)).toBe(true);
311
+
312
+ const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
313
+ expect(content).toEqual({ tools: {} });
314
+ });
315
+
316
+ test("--agent mode with --force overwrites existing .enact/tools.json", async () => {
317
+ // Create existing .enact/tools.json with some content
318
+ const enactDir = join(testDir, ".enact");
319
+ mkdirSync(enactDir, { recursive: true });
320
+ const toolsJsonPath = join(enactDir, "tools.json");
321
+ const existingContent = { tools: { "some/tool": "1.0.0" } };
322
+ writeFileSync(toolsJsonPath, JSON.stringify(existingContent));
323
+
324
+ const program = new Command();
325
+ program.exitOverride();
326
+ configureInitCommand(program);
327
+
328
+ const originalCwd = process.cwd();
329
+ process.chdir(testDir);
330
+
331
+ try {
332
+ await program.parseAsync(["node", "test", "init", "--agent", "--force"]);
333
+ } catch {
334
+ // Command may throw due to exitOverride
335
+ } finally {
336
+ process.chdir(originalCwd);
337
+ }
338
+
339
+ const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
340
+ expect(content).toEqual({ tools: {} });
341
+ });
342
+
343
+ test("--agent mode preserves existing .enact/tools.json without --force", async () => {
344
+ // Create existing .enact/tools.json with some content
345
+ const enactDir = join(testDir, ".enact");
346
+ mkdirSync(enactDir, { recursive: true });
347
+ const toolsJsonPath = join(enactDir, "tools.json");
348
+ const existingContent = { tools: { "some/tool": "1.0.0" } };
349
+ writeFileSync(toolsJsonPath, JSON.stringify(existingContent));
350
+
351
+ // Also create AGENTS.md so the command doesn't fail early
352
+ writeFileSync(join(testDir, "AGENTS.md"), "existing");
353
+
354
+ const program = new Command();
355
+ program.exitOverride();
356
+ configureInitCommand(program);
357
+
358
+ const originalCwd = process.cwd();
359
+ process.chdir(testDir);
360
+
361
+ try {
362
+ // Without --force, AGENTS.md check will fail and return early
363
+ // So we need to test with --force on AGENTS.md but not tools.json
364
+ // Actually the --force flag applies to both, so let's just verify
365
+ // tools.json is preserved when it exists and no --force
366
+ await program.parseAsync(["node", "test", "init", "--agent"]);
367
+ } catch {
368
+ // Command may throw due to exitOverride or warning about existing file
369
+ } finally {
370
+ process.chdir(originalCwd);
371
+ }
372
+
373
+ // tools.json should be preserved since AGENTS.md existed and no --force was used
374
+ const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
375
+ expect(content).toEqual(existingContent);
376
+ });
377
+
378
+ test("--tool mode does NOT create .enact/tools.json", async () => {
379
+ const program = new Command();
380
+ program.exitOverride();
381
+ configureInitCommand(program);
382
+
383
+ const originalCwd = process.cwd();
384
+ process.chdir(testDir);
385
+
386
+ try {
387
+ await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/my-tool"]);
388
+ } catch {
389
+ // Command may throw due to exitOverride
390
+ } finally {
391
+ process.chdir(originalCwd);
392
+ }
393
+
394
+ const toolsJsonPath = join(testDir, ".enact", "tools.json");
395
+ expect(existsSync(toolsJsonPath)).toBe(false);
396
+ });
397
+
218
398
  test("enact.md contains valid YAML frontmatter", async () => {
219
399
  const program = new Command();
220
400
  program.exitOverride();
@@ -224,7 +404,14 @@ describe("init command", () => {
224
404
  process.chdir(testDir);
225
405
 
226
406
  try {
227
- await program.parseAsync(["node", "test", "init", "--name", "myorg/utils/greeter"]);
407
+ await program.parseAsync([
408
+ "node",
409
+ "test",
410
+ "init",
411
+ "--tool",
412
+ "--name",
413
+ "myorg/utils/greeter",
414
+ ]);
228
415
  } catch {
229
416
  // Command may throw due to exitOverride
230
417
  } finally {
@@ -255,7 +442,7 @@ describe("init command", () => {
255
442
  process.chdir(testDir);
256
443
 
257
444
  try {
258
- await program.parseAsync(["node", "test", "init", "--name", "test/tool"]);
445
+ await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/tool"]);
259
446
  } catch {
260
447
  // Command may throw due to exitOverride
261
448
  } finally {
@@ -299,16 +486,16 @@ describe("init command", () => {
299
486
  });
300
487
 
301
488
  describe("option conflicts", () => {
302
- test("--tool is the default when no mode specified", () => {
489
+ test("--agent is the default when no mode specified", () => {
303
490
  const program = new Command();
304
491
  configureInitCommand(program);
305
492
 
306
493
  const initCmd = program.commands.find((cmd) => cmd.name() === "init");
307
494
  const opts = initCmd?.options ?? [];
308
- const toolOpt = opts.find((o) => o.long === "--tool");
495
+ const agentOpt = opts.find((o) => o.long === "--agent");
309
496
 
310
497
  // Description should indicate it's the default
311
- expect(toolOpt?.description).toContain("default");
498
+ expect(agentOpt?.description).toContain("default");
312
499
  });
313
500
  });
314
501
 
@@ -328,7 +515,7 @@ describe("init command", () => {
328
515
  process.chdir(testDir);
329
516
 
330
517
  try {
331
- await program.parseAsync(["node", "test", "init", "--name", "test/tool"]);
518
+ await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/tool"]);
332
519
  } catch {
333
520
  // Command may throw due to exitOverride
334
521
  } finally {
@@ -367,7 +554,7 @@ describe("init command", () => {
367
554
  process.chdir(testDir);
368
555
 
369
556
  try {
370
- await program.parseAsync(["node", "test", "init", "--name", "test/tool"]);
557
+ await program.parseAsync(["node", "test", "init", "--tool", "--name", "test/tool"]);
371
558
  } catch {
372
559
  // Command may throw due to exitOverride
373
560
  } finally {
@@ -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
  });