@enactprotocol/cli 2.1.6 → 2.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/commands/auth/index.js +1 -1
  2. package/dist/commands/auth/index.js.map +1 -1
  3. package/dist/commands/index.d.ts +1 -1
  4. package/dist/commands/index.d.ts.map +1 -1
  5. package/dist/commands/index.js +1 -1
  6. package/dist/commands/index.js.map +1 -1
  7. package/dist/commands/info/index.d.ts +11 -0
  8. package/dist/commands/info/index.d.ts.map +1 -0
  9. package/dist/commands/info/index.js +232 -0
  10. package/dist/commands/info/index.js.map +1 -0
  11. package/dist/commands/init/index.d.ts.map +1 -1
  12. package/dist/commands/init/index.js +92 -61
  13. package/dist/commands/init/index.js.map +1 -1
  14. package/dist/commands/learn/index.d.ts +4 -0
  15. package/dist/commands/learn/index.d.ts.map +1 -1
  16. package/dist/commands/learn/index.js +159 -5
  17. package/dist/commands/learn/index.js.map +1 -1
  18. package/dist/commands/mcp/index.d.ts +20 -0
  19. package/dist/commands/mcp/index.d.ts.map +1 -0
  20. package/dist/commands/mcp/index.js +460 -0
  21. package/dist/commands/mcp/index.js.map +1 -0
  22. package/dist/commands/publish/index.d.ts.map +1 -1
  23. package/dist/commands/publish/index.js +14 -7
  24. package/dist/commands/publish/index.js.map +1 -1
  25. package/dist/commands/sign/index.d.ts +2 -1
  26. package/dist/commands/sign/index.d.ts.map +1 -1
  27. package/dist/commands/sign/index.js +75 -17
  28. package/dist/commands/sign/index.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +4 -4
  32. package/dist/index.js.map +1 -1
  33. package/package.json +5 -5
  34. package/src/commands/auth/index.ts +1 -1
  35. package/src/commands/index.ts +1 -1
  36. package/src/commands/{get → info}/index.ts +103 -16
  37. package/src/commands/init/index.ts +104 -65
  38. package/src/commands/learn/index.ts +228 -5
  39. package/src/commands/publish/index.ts +14 -7
  40. package/src/commands/sign/index.ts +93 -18
  41. package/src/index.ts +3 -3
  42. package/tests/commands/{get.test.ts → info.test.ts} +35 -33
  43. package/tests/commands/init.test.ts +204 -17
  44. package/tests/commands/learn.test.ts +2 -2
  45. package/tests/e2e.test.ts +1 -1
  46. package/tsconfig.tsbuildinfo +1 -1
  47. /package/tests/fixtures/echo-tool/{enact.md → SKILL.md} +0 -0
@@ -7,12 +7,18 @@
7
7
  *
8
8
  * Supports both local paths and remote tool references:
9
9
  * - Local: enact sign ./my-tool
10
- * - Remote: enact sign author/tool@1.0.0
10
+ * - Remote: enact sign author/tool (prompts for version)
11
+ * - Remote: enact sign author/tool@1.0.0 (specific version)
11
12
  */
12
13
 
13
14
  import { readFileSync, writeFileSync } from "node:fs";
14
15
  import { dirname, join, resolve } from "node:path";
15
- import { createApiClient, getToolVersion, submitAttestationToRegistry } from "@enactprotocol/api";
16
+ import {
17
+ createApiClient,
18
+ getToolInfo,
19
+ getToolVersion,
20
+ submitAttestationToRegistry,
21
+ } from "@enactprotocol/api";
16
22
  import { getSecret } from "@enactprotocol/secrets";
17
23
  import {
18
24
  addTrustedAuditor,
@@ -42,6 +48,7 @@ import {
42
48
  json,
43
49
  keyValue,
44
50
  newline,
51
+ select,
45
52
  success,
46
53
  symbols,
47
54
  warning,
@@ -63,26 +70,37 @@ interface SignOptions extends GlobalOptions {
63
70
  const DEFAULT_BUNDLE_FILENAME = ".sigstore-bundle.json";
64
71
 
65
72
  /**
66
- * Parse a remote tool reference like "author/tool@1.0.0"
67
- * Returns null if not a valid remote reference
73
+ * Parse a remote tool reference like "author/tool@1.0.0" or "author/tool"
74
+ * Version is optional - if not provided, will prompt user to select
75
+ * Returns null if not a valid remote reference (i.e., looks like a local path)
68
76
  */
69
- function parseRemoteToolRef(ref: string): { name: string; version: string } | null {
77
+ function parseRemoteToolRef(ref: string): { name: string; version: string | undefined } | null {
70
78
  // Remote refs look like: author/tool@version or org/author/tool@version
71
- // They don't start with . or / and contain @ for version
79
+ // They don't start with . or / and must contain at least one /
72
80
  if (ref.startsWith(".") || ref.startsWith("/") || ref.startsWith("~")) {
73
81
  return null;
74
82
  }
75
83
 
84
+ // Must have at least one / in the name (author/tool format)
85
+ if (!ref.includes("/")) {
86
+ return null;
87
+ }
88
+
76
89
  const atIndex = ref.lastIndexOf("@");
77
- if (atIndex === -1 || atIndex === 0) {
90
+ if (atIndex === -1) {
91
+ // No version specified - that's OK, we'll prompt for it
92
+ return { name: ref, version: undefined };
93
+ }
94
+
95
+ if (atIndex === 0) {
78
96
  return null;
79
97
  }
80
98
 
81
99
  const name = ref.substring(0, atIndex);
82
100
  const version = ref.substring(atIndex + 1);
83
101
 
84
- // Must have at least one / in the name (author/tool)
85
- if (!name.includes("/") || !version) {
102
+ // Version after @ must not be empty
103
+ if (!version) {
86
104
  return null;
87
105
  }
88
106
 
@@ -276,9 +294,9 @@ function displayResult(
276
294
  * Sign a remote tool from the registry
277
295
  */
278
296
  async function signRemoteTool(
279
- toolRef: { name: string; version: string },
297
+ toolRef: { name: string; version: string | undefined },
280
298
  options: SignOptions,
281
- _ctx: CommandContext
299
+ ctx: CommandContext
282
300
  ): Promise<void> {
283
301
  const config = loadConfig();
284
302
  const registryUrl =
@@ -308,14 +326,71 @@ async function signRemoteTool(
308
326
  newline();
309
327
  }
310
328
 
329
+ // Resolve version - prompt if not provided
330
+ let targetVersion = toolRef.version;
331
+
332
+ if (!targetVersion) {
333
+ // Fetch tool info to get available versions
334
+ info(`Fetching versions for ${toolRef.name}...`);
335
+
336
+ let toolMetadata: Awaited<ReturnType<typeof getToolInfo>>;
337
+ try {
338
+ toolMetadata = await getToolInfo(client, toolRef.name);
339
+ } catch (err) {
340
+ error(`Tool not found: ${toolRef.name}`);
341
+ if (err instanceof Error) {
342
+ dim(` ${err.message}`);
343
+ }
344
+ process.exit(1);
345
+ }
346
+
347
+ if (toolMetadata.versions.length === 0) {
348
+ error(`No published versions found for ${toolRef.name}`);
349
+ process.exit(1);
350
+ }
351
+
352
+ // Filter out yanked versions for selection (unless there are no non-yanked versions)
353
+ const availableVersions = toolMetadata.versions.filter((v) => !v.yanked);
354
+ const versionsToShow = availableVersions.length > 0 ? availableVersions : toolMetadata.versions;
355
+
356
+ if (ctx.isInteractive) {
357
+ // Prompt user to select a version
358
+ newline();
359
+ const selectedVersion = await select(
360
+ "Select a version to sign:",
361
+ versionsToShow.map((v) => {
362
+ const option: { value: string; label: string; hint?: string } = {
363
+ value: v.version,
364
+ label: v.version + (v.version === toolMetadata.latestVersion ? " (latest)" : ""),
365
+ };
366
+ if (v.yanked) {
367
+ option.hint = "yanked";
368
+ }
369
+ return option;
370
+ })
371
+ );
372
+
373
+ if (!selectedVersion) {
374
+ info("Signing cancelled");
375
+ return;
376
+ }
377
+
378
+ targetVersion = selectedVersion;
379
+ } else {
380
+ // Non-interactive: use latest version
381
+ targetVersion = toolMetadata.latestVersion;
382
+ info(`Using latest version: ${targetVersion}`);
383
+ }
384
+ }
385
+
311
386
  // Fetch tool info from registry
312
- info(`Fetching ${toolRef.name}@${toolRef.version} from registry...`);
387
+ info(`Fetching ${toolRef.name}@${targetVersion} from registry...`);
313
388
 
314
389
  let toolInfo: Awaited<ReturnType<typeof getToolVersion>>;
315
390
  try {
316
- toolInfo = await getToolVersion(client, toolRef.name, toolRef.version);
391
+ toolInfo = await getToolVersion(client, toolRef.name, targetVersion);
317
392
  } catch (err) {
318
- error(`Tool not found: ${toolRef.name}@${toolRef.version}`);
393
+ error(`Tool not found: ${toolRef.name}@${targetVersion}`);
319
394
  if (err instanceof Error) {
320
395
  dim(` ${err.message}`);
321
396
  }
@@ -357,7 +432,7 @@ async function signRemoteTool(
357
432
  }
358
433
 
359
434
  // Confirm signing
360
- if (_ctx.isInteractive) {
435
+ if (ctx.isInteractive) {
361
436
  newline();
362
437
  const shouldSign = await confirm(
363
438
  `Sign ${toolInfo.name}@${toolInfo.version} with your identity?`,
@@ -466,10 +541,10 @@ async function signRemoteTool(
466
541
  }
467
542
 
468
543
  // Prompt to add to trust list - extract issuer from bundle for correct identity format
469
- if (_ctx.isInteractive && !options.json) {
544
+ if (ctx.isInteractive && !options.json) {
470
545
  const certificate = extractCertificateFromBundle(result.bundle);
471
546
  const issuer = certificate?.identity?.issuer;
472
- await promptAddToTrustList(attestationResult.auditor, _ctx.isInteractive, issuer);
547
+ await promptAddToTrustList(attestationResult.auditor, ctx.isInteractive, issuer);
473
548
  }
474
549
 
475
550
  if (options.json) {
@@ -719,7 +794,7 @@ export function configureSignCommand(program: Command): void {
719
794
  .description("Cryptographically sign a tool and submit attestation to registry")
720
795
  .argument(
721
796
  "<path>",
722
- "Path to tool directory, manifest file, or remote tool (author/tool@version)"
797
+ "Path to tool directory, manifest file, or remote tool (author/tool or author/tool@version)"
723
798
  )
724
799
  .option("-i, --identity <email>", "Sign with specific identity (uses OAuth)")
725
800
  .option("-o, --output <path>", "Output path for signature bundle (local only)")
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  configureConfigCommand,
16
16
  configureEnvCommand,
17
17
  configureExecCommand,
18
- configureGetCommand,
18
+ configureInfoCommand,
19
19
  configureInitCommand,
20
20
  configureInspectCommand,
21
21
  configureInstallCommand,
@@ -33,7 +33,7 @@ import {
33
33
  } from "./commands";
34
34
  import { error, formatError } from "./utils";
35
35
 
36
- export const version = "2.1.6";
36
+ export const version = "2.1.10";
37
37
 
38
38
  // Export types for external use
39
39
  export type { GlobalOptions, CommandContext } from "./types";
@@ -63,7 +63,7 @@ async function main() {
63
63
 
64
64
  // Registry commands (Phase 8)
65
65
  configureSearchCommand(program);
66
- configureGetCommand(program);
66
+ configureInfoCommand(program);
67
67
  configureLearnCommand(program);
68
68
  configurePublishCommand(program);
69
69
  configureAuthCommand(program);
@@ -1,64 +1,66 @@
1
1
  /**
2
- * Tests for the get command
2
+ * Tests for the info command
3
3
  */
4
4
 
5
5
  import { describe, expect, test } from "bun:test";
6
6
  import type { ToolVersionInfo } from "@enactprotocol/api";
7
7
  import { Command } from "commander";
8
- import { configureGetCommand } from "../../src/commands/get";
8
+ import { configureInfoCommand } from "../../src/commands/info";
9
9
 
10
- describe("get command", () => {
10
+ describe("info command", () => {
11
11
  describe("command configuration", () => {
12
- test("configures get command on program", () => {
12
+ test("configures info command on program", () => {
13
13
  const program = new Command();
14
- configureGetCommand(program);
14
+ configureInfoCommand(program);
15
15
 
16
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
17
- expect(getCmd).toBeDefined();
16
+ const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
17
+ expect(infoCmd).toBeDefined();
18
18
  });
19
19
 
20
20
  test("has correct description", () => {
21
21
  const program = new Command();
22
- configureGetCommand(program);
22
+ configureInfoCommand(program);
23
23
 
24
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
25
- expect(getCmd?.description()).toBe("Show detailed information about a tool");
24
+ const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
25
+ expect(infoCmd?.description()).toBe(
26
+ "Show detailed information about a tool (local path or registry)"
27
+ );
26
28
  });
27
29
 
28
- test("has info as alias", () => {
30
+ test("has get as alias", () => {
29
31
  const program = new Command();
30
- configureGetCommand(program);
32
+ configureInfoCommand(program);
31
33
 
32
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
33
- expect(getCmd?.aliases()).toContain("info");
34
+ const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
35
+ expect(infoCmd?.aliases()).toContain("get");
34
36
  });
35
37
 
36
38
  test("accepts tool argument", () => {
37
39
  const program = new Command();
38
- configureGetCommand(program);
40
+ configureInfoCommand(program);
39
41
 
40
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
41
- const args = getCmd?.registeredArguments ?? [];
42
+ const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
43
+ const args = infoCmd?.registeredArguments ?? [];
42
44
  expect(args.length).toBeGreaterThan(0);
43
45
  expect(args[0]?.name()).toBe("tool");
44
46
  });
45
47
 
46
48
  test("has --ver option for specifying version", () => {
47
49
  const program = new Command();
48
- configureGetCommand(program);
50
+ configureInfoCommand(program);
49
51
 
50
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
51
- const opts = getCmd?.options ?? [];
52
+ const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
53
+ const opts = infoCmd?.options ?? [];
52
54
  const verOpt = opts.find((o) => o.long === "--ver");
53
55
  expect(verOpt).toBeDefined();
54
56
  });
55
57
 
56
58
  test("has -v short option for verbose (not version)", () => {
57
59
  const program = new Command();
58
- configureGetCommand(program);
60
+ configureInfoCommand(program);
59
61
 
60
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
61
- const opts = getCmd?.options ?? [];
62
+ const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
63
+ const opts = infoCmd?.options ?? [];
62
64
  // -v is for verbose, not version (--ver is for version)
63
65
  const verboseOpt = opts.find((o) => o.short === "-v");
64
66
  expect(verboseOpt).toBeDefined();
@@ -67,20 +69,20 @@ describe("get command", () => {
67
69
 
68
70
  test("has --json option", () => {
69
71
  const program = new Command();
70
- configureGetCommand(program);
72
+ configureInfoCommand(program);
71
73
 
72
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
73
- const opts = getCmd?.options ?? [];
74
+ const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
75
+ const opts = infoCmd?.options ?? [];
74
76
  const jsonOpt = opts.find((o) => o.long === "--json");
75
77
  expect(jsonOpt).toBeDefined();
76
78
  });
77
79
 
78
80
  test("has --verbose option", () => {
79
81
  const program = new Command();
80
- configureGetCommand(program);
82
+ configureInfoCommand(program);
81
83
 
82
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
83
- const opts = getCmd?.options ?? [];
84
+ const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
85
+ const opts = infoCmd?.options ?? [];
84
86
  const verboseOpt = opts.find((o) => o.long === "--verbose");
85
87
  expect(verboseOpt).toBeDefined();
86
88
  });
@@ -249,12 +251,12 @@ Documentation here.`;
249
251
  expect(enactMdContent).toContain("Documentation here.");
250
252
  });
251
253
 
252
- test("verbose option is available on get command", () => {
254
+ test("verbose option is available on info command", () => {
253
255
  const program = new Command();
254
- configureGetCommand(program);
256
+ configureInfoCommand(program);
255
257
 
256
- const getCmd = program.commands.find((cmd) => cmd.name() === "get");
257
- const opts = getCmd?.options ?? [];
258
+ const infoCmd = program.commands.find((cmd) => cmd.name() === "info");
259
+ const opts = infoCmd?.options ?? [];
258
260
 
259
261
  // Check both short and long form exist
260
262
  const verboseOpt = opts.find((o) => o.long === "--verbose");
@@ -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 SKILL.md", async () => {
111
163
  const program = new Command();
112
164
  program.exitOverride(); // Prevent process.exit
113
165
  configureInitCommand(program);
@@ -117,14 +169,14 @@ 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 {
124
176
  process.chdir(originalCwd);
125
177
  }
126
178
 
127
- const manifestPath = join(testDir, "enact.md");
179
+ const manifestPath = join(testDir, "SKILL.md");
128
180
  expect(existsSync(manifestPath)).toBe(true);
129
181
 
130
182
  const content = readFileSync(manifestPath, "utf-8");
@@ -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 {
@@ -154,7 +206,7 @@ describe("init command", () => {
154
206
 
155
207
  const content = readFileSync(agentsPath, "utf-8");
156
208
  expect(content).toContain("enact run");
157
- expect(content).toContain("enact.md");
209
+ expect(content).toContain("SKILL.md");
158
210
  expect(content).toContain("Parameter Substitution");
159
211
  });
160
212
 
@@ -215,7 +267,7 @@ describe("init command", () => {
215
267
  expect(existsSync(join(testDir, "AGENTS.md"))).toBe(false);
216
268
  });
217
269
 
218
- test("enact.md contains valid YAML frontmatter", async () => {
270
+ test("--agent mode creates .enact/tools.json", async () => {
219
271
  const program = new Command();
220
272
  program.exitOverride();
221
273
  configureInitCommand(program);
@@ -224,14 +276,149 @@ describe("init command", () => {
224
276
  process.chdir(testDir);
225
277
 
226
278
  try {
227
- await program.parseAsync(["node", "test", "init", "--name", "myorg/utils/greeter"]);
279
+ await program.parseAsync(["node", "test", "init", "--agent"]);
228
280
  } catch {
229
281
  // Command may throw due to exitOverride
230
282
  } finally {
231
283
  process.chdir(originalCwd);
232
284
  }
233
285
 
234
- const content = readFileSync(join(testDir, "enact.md"), "utf-8");
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
+
398
+ test("SKILL.md contains valid YAML frontmatter", async () => {
399
+ const program = new Command();
400
+ program.exitOverride();
401
+ configureInitCommand(program);
402
+
403
+ const originalCwd = process.cwd();
404
+ process.chdir(testDir);
405
+
406
+ try {
407
+ await program.parseAsync([
408
+ "node",
409
+ "test",
410
+ "init",
411
+ "--tool",
412
+ "--name",
413
+ "myorg/utils/greeter",
414
+ ]);
415
+ } catch {
416
+ // Command may throw due to exitOverride
417
+ } finally {
418
+ process.chdir(originalCwd);
419
+ }
420
+
421
+ const content = readFileSync(join(testDir, "SKILL.md"), "utf-8");
235
422
 
236
423
  // Check frontmatter structure
237
424
  expect(content.startsWith("---")).toBe(true);
@@ -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,14 +515,14 @@ 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 {
335
522
  process.chdir(originalCwd);
336
523
  }
337
524
 
338
- const content = readFileSync(join(testDir, "enact.md"), "utf-8");
525
+ const content = readFileSync(join(testDir, "SKILL.md"), "utf-8");
339
526
 
340
527
  // Required fields per spec
341
528
  expect(content).toContain("name:");
@@ -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 {