@enactprotocol/cli 2.0.10 → 2.0.13

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.
@@ -0,0 +1,47 @@
1
+ # AGENTS.md
2
+
3
+ This project uses Enact tools — containerized, cryptographically-signed executables.
4
+
5
+ ## Running Tools
6
+ ```bash
7
+ enact run <tool-name> --args '{"key": "value"}' # Run installed tool
8
+ enact run ./path/to/tool --args '{}' # Run local tool
9
+ ```
10
+
11
+ ## Finding & Installing Tools
12
+ ```bash
13
+ enact search "pdf extraction" # Search registry
14
+ enact get author/category/tool # View tool info
15
+ enact install author/category/tool # Add to project (.enact/tools.json)
16
+ enact install author/category/tool --global # Add globally
17
+ enact list # List project tools
18
+ ```
19
+
20
+ ## Tool Output
21
+ Tools output JSON to stdout. Parse with jq or your language's JSON parser:
22
+ ```bash
23
+ enact run tool --args '{}' | jq '.result'
24
+ ```
25
+
26
+ ## Creating Local Tools
27
+ Create `tools/<name>/enact.md` with:
28
+ ```yaml
29
+ ---
30
+ name: my-tool
31
+ description: What it does
32
+ command: echo "Hello ${name}"
33
+ inputSchema:
34
+ type: object
35
+ properties:
36
+ name: { type: string }
37
+ ---
38
+ # My Tool
39
+ Documentation here.
40
+ ```
41
+ Run with: `enact run ./tools/<name> --args '{"name": "World"}'`
42
+
43
+ ## Environment & Secrets
44
+ ```bash
45
+ enact env set API_KEY --secret --namespace author/tool # Set secret
46
+ enact env list # List env vars
47
+ ```
@@ -0,0 +1,65 @@
1
+ # CLAUDE.md
2
+
3
+ This project uses Enact tools — containerized, signed executables you can run via CLI.
4
+
5
+ ## Quick Reference
6
+ ```bash
7
+ enact run <tool> --args '{"key": "value"}' # Run a tool
8
+ enact search "keyword" # Find tools
9
+ enact install author/tool # Install tool
10
+ enact list # List installed tools
11
+ ```
12
+
13
+ ## Running Tools
14
+ Tools take JSON input and return JSON output:
15
+ ```bash
16
+ # Run and capture output
17
+ result=$(enact run author/utils/formatter --args '{"code": "const x=1"}')
18
+
19
+ # Parse with jq
20
+ enact run tool --args '{}' | jq '.data'
21
+ ```
22
+
23
+ ## Creating Tools
24
+ Create `enact.md` in a directory:
25
+ ```yaml
26
+ ---
27
+ name: namespace/category/tool
28
+ description: Clear description for search
29
+ version: 1.0.0
30
+ from: python:3.12-slim # Docker image
31
+ build: pip install requests # Install dependencies (cached)
32
+ command: python /work/main.py ${input}
33
+ inputSchema:
34
+ type: object
35
+ properties:
36
+ input: { type: string, description: "The input to process" }
37
+ required: [input]
38
+ ---
39
+ # Tool Name
40
+ Usage documentation here.
41
+ ```
42
+
43
+ Key points:
44
+ - `${param}` is auto-quoted — never add manual quotes
45
+ - `from:` pin image versions (not `:latest`)
46
+ - `build:` steps are cached by Dagger
47
+ - Output JSON to stdout, errors to stderr
48
+ - Non-zero exit = failure
49
+
50
+ ## Tool Development Workflow
51
+ ```bash
52
+ enact run ./ --args '{"input": "test"}' # Test locally
53
+ enact run ./ --args '{}' --dry-run # Preview command
54
+ enact sign ./ && enact publish ./ # Publish
55
+ ```
56
+
57
+ ## Secrets
58
+ Declare in enact.md, set via CLI:
59
+ ```yaml
60
+ env:
61
+ API_KEY: # Declared but not set
62
+ ```
63
+ ```bash
64
+ enact env set API_KEY --secret --namespace author/tool
65
+ ```
@@ -0,0 +1,56 @@
1
+ # AGENTS.md
2
+
3
+ Enact tool: containerized, signed executable. Manifest: `enact.md` (YAML frontmatter + Markdown docs).
4
+
5
+ ## Commands
6
+ ```bash
7
+ enact run ./ --args '{"name": "Test"}' # Run locally
8
+ enact run ./ --args '{}' --dry-run # Preview execution
9
+ enact sign ./ && enact publish ./ # Sign and publish
10
+ ```
11
+
12
+ ## enact.md Structure
13
+ ```yaml
14
+ ---
15
+ name: {{TOOL_NAME}} # org/category/tool format
16
+ description: What it does
17
+ version: 1.0.0 # semver
18
+ from: python:3.12-slim # pin versions, not :latest
19
+ build: pip install requests # cached by Dagger
20
+ command: python /work/main.py ${input}
21
+ timeout: 30s
22
+ inputSchema:
23
+ type: object
24
+ properties:
25
+ input: { type: string }
26
+ required: [input]
27
+ env:
28
+ API_KEY: # declare secrets (set via: enact env set API_KEY --secret)
29
+ ---
30
+ # Tool Name
31
+ Documentation here (usage examples, etc.)
32
+ ```
33
+
34
+ ## Parameter Substitution
35
+ - `${param}` — auto-quoted (handles spaces, JSON, special chars)
36
+ - `${param:raw}` — unquoted (use carefully)
37
+ - **Never manually quote**: `"${param}"` causes double-quoting
38
+
39
+ ## Output
40
+ Always output valid JSON when `outputSchema` is defined:
41
+ ```python
42
+ import json, sys
43
+ print(json.dumps({"result": data})) # stdout = tool output
44
+ sys.exit(1) # non-zero = error
45
+ ```
46
+
47
+ ## File Access
48
+ Tool runs in container with `/work` as working directory. Source files copied there.
49
+
50
+ ## Adding Dependencies
51
+ - Python: `build: pip install package1 package2`
52
+ - Node: `build: ["npm install", "npm run build"]`
53
+ - System: `build: apt-get update && apt-get install -y libfoo`
54
+ - Compiled: `build: rustc /work/main.rs -o /work/tool`
55
+
56
+ Build steps are cached — first run slow, subsequent runs instant.
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: {{TOOL_NAME}}
3
+ description: A simple tool that echoes a greeting
4
+ version: 0.1.0
5
+ enact: "2.0"
6
+
7
+ from: alpine:latest
8
+
9
+ inputSchema:
10
+ type: object
11
+ properties:
12
+ name:
13
+ type: string
14
+ description: Name to greet
15
+ default: World
16
+ required: []
17
+
18
+ command: |
19
+ echo "Hello, ${name}!"
20
+ ---
21
+
22
+ # {{TOOL_NAME}}
23
+
24
+ A simple greeting tool created with `enact init`.
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ enact run ./ --args '{"name": "Alice"}'
30
+ ```
31
+
32
+ ## Customization
33
+
34
+ Edit this file to create your own tool:
35
+
36
+ 1. Update the `name` and `description` in the frontmatter
37
+ 2. Modify the `inputSchema` to define your tool's inputs
38
+ 3. Change the `command` to run your desired shell commands
39
+ 4. Update this documentation section
40
+
41
+ ## Learn More
42
+
43
+ - [Enact Documentation](https://enact.dev/docs)
44
+ - [Tool Manifest Reference](https://enact.dev/docs/manifest)
package/src/index.ts CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  } from "./commands";
33
33
  import { error, formatError } from "./utils";
34
34
 
35
- export const version = "2.0.10";
35
+ export const version = "2.0.13";
36
36
 
37
37
  // Export types for external use
38
38
  export type { GlobalOptions, CommandContext } from "./types";
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Tests for the init command
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
6
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { Command } from "commander";
9
+ import { configureInitCommand } from "../../src/commands/init";
10
+
11
+ describe("init command", () => {
12
+ describe("command configuration", () => {
13
+ test("configures init command on program", () => {
14
+ const program = new Command();
15
+ configureInitCommand(program);
16
+
17
+ const initCmd = program.commands.find((cmd) => cmd.name() === "init");
18
+ expect(initCmd).toBeDefined();
19
+ });
20
+
21
+ test("has correct description", () => {
22
+ const program = new Command();
23
+ configureInitCommand(program);
24
+
25
+ const initCmd = program.commands.find((cmd) => cmd.name() === "init");
26
+ expect(initCmd?.description()).toBe("Initialize Enact in the current directory");
27
+ });
28
+
29
+ test("has --name option", () => {
30
+ const program = new Command();
31
+ configureInitCommand(program);
32
+
33
+ const initCmd = program.commands.find((cmd) => cmd.name() === "init");
34
+ const opts = initCmd?.options ?? [];
35
+ const nameOpt = opts.find((o) => o.long === "--name");
36
+ expect(nameOpt).toBeDefined();
37
+ expect(nameOpt?.short).toBe("-n");
38
+ });
39
+
40
+ test("has --force option", () => {
41
+ const program = new Command();
42
+ configureInitCommand(program);
43
+
44
+ const initCmd = program.commands.find((cmd) => cmd.name() === "init");
45
+ const opts = initCmd?.options ?? [];
46
+ const forceOpt = opts.find((o) => o.long === "--force");
47
+ expect(forceOpt).toBeDefined();
48
+ expect(forceOpt?.short).toBe("-f");
49
+ });
50
+
51
+ test("has --tool option", () => {
52
+ const program = new Command();
53
+ configureInitCommand(program);
54
+
55
+ const initCmd = program.commands.find((cmd) => cmd.name() === "init");
56
+ const opts = initCmd?.options ?? [];
57
+ const toolOpt = opts.find((o) => o.long === "--tool");
58
+ expect(toolOpt).toBeDefined();
59
+ });
60
+
61
+ test("has --agent option", () => {
62
+ const program = new Command();
63
+ configureInitCommand(program);
64
+
65
+ const initCmd = program.commands.find((cmd) => cmd.name() === "init");
66
+ const opts = initCmd?.options ?? [];
67
+ const agentOpt = opts.find((o) => o.long === "--agent");
68
+ expect(agentOpt).toBeDefined();
69
+ });
70
+
71
+ test("has --claude option", () => {
72
+ const program = new Command();
73
+ configureInitCommand(program);
74
+
75
+ const initCmd = program.commands.find((cmd) => cmd.name() === "init");
76
+ const opts = initCmd?.options ?? [];
77
+ const claudeOpt = opts.find((o) => o.long === "--claude");
78
+ expect(claudeOpt).toBeDefined();
79
+ });
80
+
81
+ test("has --verbose option", () => {
82
+ const program = new Command();
83
+ configureInitCommand(program);
84
+
85
+ const initCmd = program.commands.find((cmd) => cmd.name() === "init");
86
+ const opts = initCmd?.options ?? [];
87
+ const verboseOpt = opts.find((o) => o.long === "--verbose");
88
+ expect(verboseOpt).toBeDefined();
89
+ });
90
+ });
91
+
92
+ describe("file generation", () => {
93
+ const testDir = join(import.meta.dir, ".test-init-temp");
94
+
95
+ beforeEach(() => {
96
+ // Clean up before each test
97
+ if (existsSync(testDir)) {
98
+ rmSync(testDir, { recursive: true });
99
+ }
100
+ mkdirSync(testDir, { recursive: true });
101
+ });
102
+
103
+ afterEach(() => {
104
+ // Clean up after each test
105
+ if (existsSync(testDir)) {
106
+ rmSync(testDir, { recursive: true });
107
+ }
108
+ });
109
+
110
+ test("default mode creates enact.md", async () => {
111
+ const program = new Command();
112
+ program.exitOverride(); // Prevent process.exit
113
+ configureInitCommand(program);
114
+
115
+ // Change to test directory and run init
116
+ const originalCwd = process.cwd();
117
+ process.chdir(testDir);
118
+
119
+ try {
120
+ await program.parseAsync(["node", "test", "init", "--name", "test/my-tool"]);
121
+ } catch {
122
+ // Command may throw due to exitOverride
123
+ } finally {
124
+ process.chdir(originalCwd);
125
+ }
126
+
127
+ const manifestPath = join(testDir, "enact.md");
128
+ expect(existsSync(manifestPath)).toBe(true);
129
+
130
+ const content = readFileSync(manifestPath, "utf-8");
131
+ expect(content).toContain("name: test/my-tool");
132
+ expect(content).toContain("description:");
133
+ expect(content).toContain("command:");
134
+ });
135
+
136
+ test("default mode creates AGENTS.md for tool development", async () => {
137
+ const program = new Command();
138
+ program.exitOverride();
139
+ configureInitCommand(program);
140
+
141
+ const originalCwd = process.cwd();
142
+ process.chdir(testDir);
143
+
144
+ try {
145
+ await program.parseAsync(["node", "test", "init", "--name", "test/my-tool"]);
146
+ } catch {
147
+ // Command may throw due to exitOverride
148
+ } finally {
149
+ process.chdir(originalCwd);
150
+ }
151
+
152
+ const agentsPath = join(testDir, "AGENTS.md");
153
+ expect(existsSync(agentsPath)).toBe(true);
154
+
155
+ const content = readFileSync(agentsPath, "utf-8");
156
+ expect(content).toContain("enact run");
157
+ expect(content).toContain("enact.md");
158
+ expect(content).toContain("Parameter Substitution");
159
+ });
160
+
161
+ test("--agent mode creates AGENTS.md for tool consumers", async () => {
162
+ const program = new Command();
163
+ program.exitOverride();
164
+ configureInitCommand(program);
165
+
166
+ const originalCwd = process.cwd();
167
+ process.chdir(testDir);
168
+
169
+ try {
170
+ await program.parseAsync(["node", "test", "init", "--agent"]);
171
+ } catch {
172
+ // Command may throw due to exitOverride
173
+ } finally {
174
+ process.chdir(originalCwd);
175
+ }
176
+
177
+ const agentsPath = join(testDir, "AGENTS.md");
178
+ expect(existsSync(agentsPath)).toBe(true);
179
+
180
+ const content = readFileSync(agentsPath, "utf-8");
181
+ expect(content).toContain("enact search");
182
+ expect(content).toContain("enact install");
183
+ expect(content).toContain("Finding & Installing Tools");
184
+
185
+ // Should NOT create enact.md in agent mode
186
+ const manifestPath = join(testDir, "enact.md");
187
+ expect(existsSync(manifestPath)).toBe(false);
188
+ });
189
+
190
+ test("--claude mode creates CLAUDE.md", async () => {
191
+ const program = new Command();
192
+ program.exitOverride();
193
+ configureInitCommand(program);
194
+
195
+ const originalCwd = process.cwd();
196
+ process.chdir(testDir);
197
+
198
+ try {
199
+ await program.parseAsync(["node", "test", "init", "--claude"]);
200
+ } catch {
201
+ // Command may throw due to exitOverride
202
+ } finally {
203
+ process.chdir(originalCwd);
204
+ }
205
+
206
+ const claudePath = join(testDir, "CLAUDE.md");
207
+ expect(existsSync(claudePath)).toBe(true);
208
+
209
+ const content = readFileSync(claudePath, "utf-8");
210
+ expect(content).toContain("enact run");
211
+ expect(content).toContain("enact search");
212
+
213
+ // Should NOT create enact.md or AGENTS.md in claude mode
214
+ expect(existsSync(join(testDir, "enact.md"))).toBe(false);
215
+ expect(existsSync(join(testDir, "AGENTS.md"))).toBe(false);
216
+ });
217
+
218
+ test("enact.md contains valid YAML frontmatter", async () => {
219
+ const program = new Command();
220
+ program.exitOverride();
221
+ configureInitCommand(program);
222
+
223
+ const originalCwd = process.cwd();
224
+ process.chdir(testDir);
225
+
226
+ try {
227
+ await program.parseAsync(["node", "test", "init", "--name", "myorg/utils/greeter"]);
228
+ } catch {
229
+ // Command may throw due to exitOverride
230
+ } finally {
231
+ process.chdir(originalCwd);
232
+ }
233
+
234
+ const content = readFileSync(join(testDir, "enact.md"), "utf-8");
235
+
236
+ // Check frontmatter structure
237
+ expect(content.startsWith("---")).toBe(true);
238
+ expect(content).toContain("name: myorg/utils/greeter");
239
+ expect(content).toContain("version:");
240
+ expect(content).toContain("from:");
241
+ expect(content).toContain("inputSchema:");
242
+ expect(content).toContain("command:");
243
+
244
+ // Check it has closing frontmatter and markdown body
245
+ const parts = content.split("---");
246
+ expect(parts.length).toBeGreaterThanOrEqual(3); // empty, frontmatter, body
247
+ });
248
+
249
+ test("AGENTS.md for tools contains development instructions", async () => {
250
+ const program = new Command();
251
+ program.exitOverride();
252
+ configureInitCommand(program);
253
+
254
+ const originalCwd = process.cwd();
255
+ process.chdir(testDir);
256
+
257
+ try {
258
+ await program.parseAsync(["node", "test", "init", "--name", "test/tool"]);
259
+ } catch {
260
+ // Command may throw due to exitOverride
261
+ } finally {
262
+ process.chdir(originalCwd);
263
+ }
264
+
265
+ const content = readFileSync(join(testDir, "AGENTS.md"), "utf-8");
266
+
267
+ // Should contain tool development-specific content
268
+ expect(content).toContain("enact sign");
269
+ expect(content).toContain("enact publish");
270
+ expect(content).toContain("${param}");
271
+ expect(content).toContain("build:");
272
+ expect(content).toContain("/work");
273
+ });
274
+
275
+ test("AGENTS.md for agent projects contains usage instructions", async () => {
276
+ const program = new Command();
277
+ program.exitOverride();
278
+ configureInitCommand(program);
279
+
280
+ const originalCwd = process.cwd();
281
+ process.chdir(testDir);
282
+
283
+ try {
284
+ await program.parseAsync(["node", "test", "init", "--agent"]);
285
+ } catch {
286
+ // Command may throw due to exitOverride
287
+ } finally {
288
+ process.chdir(originalCwd);
289
+ }
290
+
291
+ const content = readFileSync(join(testDir, "AGENTS.md"), "utf-8");
292
+
293
+ // Should contain consumer-focused content
294
+ expect(content).toContain("enact search");
295
+ expect(content).toContain("enact install");
296
+ expect(content).toContain("enact list");
297
+ expect(content).toContain(".enact/tools.json");
298
+ });
299
+ });
300
+
301
+ describe("option conflicts", () => {
302
+ test("--tool is the default when no mode specified", () => {
303
+ const program = new Command();
304
+ configureInitCommand(program);
305
+
306
+ const initCmd = program.commands.find((cmd) => cmd.name() === "init");
307
+ const opts = initCmd?.options ?? [];
308
+ const toolOpt = opts.find((o) => o.long === "--tool");
309
+
310
+ // Description should indicate it's the default
311
+ expect(toolOpt?.description).toContain("default");
312
+ });
313
+ });
314
+
315
+ describe("template content quality", () => {
316
+ test("tool template has all required manifest fields", async () => {
317
+ const program = new Command();
318
+ program.exitOverride();
319
+ configureInitCommand(program);
320
+
321
+ const testDir = join(import.meta.dir, ".test-init-quality");
322
+ if (existsSync(testDir)) {
323
+ rmSync(testDir, { recursive: true });
324
+ }
325
+ mkdirSync(testDir, { recursive: true });
326
+
327
+ const originalCwd = process.cwd();
328
+ process.chdir(testDir);
329
+
330
+ try {
331
+ await program.parseAsync(["node", "test", "init", "--name", "test/tool"]);
332
+ } catch {
333
+ // Command may throw due to exitOverride
334
+ } finally {
335
+ process.chdir(originalCwd);
336
+ }
337
+
338
+ const content = readFileSync(join(testDir, "enact.md"), "utf-8");
339
+
340
+ // Required fields per spec
341
+ expect(content).toContain("name:");
342
+ expect(content).toContain("description:");
343
+
344
+ // Recommended fields
345
+ expect(content).toContain("version:");
346
+ expect(content).toContain("enact:");
347
+ expect(content).toContain("from:");
348
+ expect(content).toContain("inputSchema:");
349
+ expect(content).toContain("command:");
350
+
351
+ // Clean up
352
+ rmSync(testDir, { recursive: true });
353
+ });
354
+
355
+ test("AGENTS.md does not contain unnecessary verbosity", async () => {
356
+ const program = new Command();
357
+ program.exitOverride();
358
+ configureInitCommand(program);
359
+
360
+ const testDir = join(import.meta.dir, ".test-init-verbosity");
361
+ if (existsSync(testDir)) {
362
+ rmSync(testDir, { recursive: true });
363
+ }
364
+ mkdirSync(testDir, { recursive: true });
365
+
366
+ const originalCwd = process.cwd();
367
+ process.chdir(testDir);
368
+
369
+ try {
370
+ await program.parseAsync(["node", "test", "init", "--name", "test/tool"]);
371
+ } catch {
372
+ // Command may throw due to exitOverride
373
+ } finally {
374
+ process.chdir(originalCwd);
375
+ }
376
+
377
+ const content = readFileSync(join(testDir, "AGENTS.md"), "utf-8");
378
+
379
+ // Should be concise - check line count is reasonable
380
+ const lines = content.split("\n").length;
381
+ expect(lines).toBeLessThan(100); // Should be concise
382
+
383
+ // Clean up
384
+ rmSync(testDir, { recursive: true });
385
+ });
386
+ });
387
+ });