@enactprotocol/cli 2.3.5 → 2.3.8

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 (72) hide show
  1. package/dist/commands/index.d.ts +1 -0
  2. package/dist/commands/index.d.ts.map +1 -1
  3. package/dist/commands/index.js +2 -0
  4. package/dist/commands/index.js.map +1 -1
  5. package/dist/commands/init/index.d.ts.map +1 -1
  6. package/dist/commands/init/index.js +94 -46
  7. package/dist/commands/init/index.js.map +1 -1
  8. package/dist/commands/init/templates/agent-agents.d.ts +1 -1
  9. package/dist/commands/init/templates/agent-agents.d.ts.map +1 -1
  10. package/dist/commands/init/templates/agent-agents.js +1 -1
  11. package/dist/commands/init/templates/default-skill-package.d.ts +5 -0
  12. package/dist/commands/init/templates/default-skill-package.d.ts.map +1 -0
  13. package/dist/commands/init/templates/default-skill-package.js +13 -0
  14. package/dist/commands/init/templates/default-skill-package.js.map +1 -0
  15. package/dist/commands/init/templates/default-skill-script.d.ts +5 -0
  16. package/dist/commands/init/templates/default-skill-script.d.ts.map +1 -0
  17. package/dist/commands/init/templates/default-skill-script.js +11 -0
  18. package/dist/commands/init/templates/default-skill-script.js.map +1 -0
  19. package/dist/commands/init/templates/index.d.ts +2 -0
  20. package/dist/commands/init/templates/index.d.ts.map +1 -1
  21. package/dist/commands/init/templates/index.js +2 -0
  22. package/dist/commands/init/templates/index.js.map +1 -1
  23. package/dist/commands/install/index.d.ts +2 -2
  24. package/dist/commands/install/index.js +18 -18
  25. package/dist/commands/install/index.js.map +1 -1
  26. package/dist/commands/list/index.d.ts +2 -2
  27. package/dist/commands/list/index.js +3 -3
  28. package/dist/commands/list/index.js.map +1 -1
  29. package/dist/commands/org/index.d.ts +16 -0
  30. package/dist/commands/org/index.d.ts.map +1 -0
  31. package/dist/commands/org/index.js +203 -0
  32. package/dist/commands/org/index.js.map +1 -0
  33. package/dist/commands/publish/index.d.ts.map +1 -1
  34. package/dist/commands/publish/index.js +5 -1
  35. package/dist/commands/publish/index.js.map +1 -1
  36. package/dist/commands/search/index.js +2 -2
  37. package/dist/commands/search/index.js.map +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/utils/errors.js +2 -2
  43. package/dist/utils/errors.js.map +1 -1
  44. package/package.json +6 -6
  45. package/src/commands/env/README.md +1 -1
  46. package/src/commands/index.ts +3 -0
  47. package/src/commands/init/index.ts +103 -46
  48. package/src/commands/init/templates/agent-agents.ts +1 -1
  49. package/src/commands/init/templates/default-skill-package.ts +12 -0
  50. package/src/commands/init/templates/default-skill-script.ts +10 -0
  51. package/src/commands/init/templates/index.ts +2 -0
  52. package/src/commands/install/README.md +2 -2
  53. package/src/commands/install/index.ts +18 -18
  54. package/src/commands/list/index.ts +3 -3
  55. package/src/commands/org/index.ts +263 -0
  56. package/src/commands/publish/index.ts +4 -1
  57. package/src/commands/run/README.md +1 -1
  58. package/src/commands/search/index.ts +2 -2
  59. package/src/index.ts +5 -1
  60. package/src/utils/errors.ts +2 -2
  61. package/tests/commands/cache.test.ts +2 -2
  62. package/tests/commands/init.test.ts +66 -55
  63. package/tests/commands/install-integration.test.ts +11 -11
  64. package/tests/commands/publish.test.ts +5 -3
  65. package/tests/commands/sign.test.ts +1 -1
  66. package/tests/e2e.test.ts +13 -11
  67. package/tests/utils/ignore.test.ts +2 -1
  68. package/tsconfig.tsbuildinfo +1 -1
  69. /package/tests/fixtures/calculator/{skill.yaml → skill.package.yaml} +0 -0
  70. /package/tests/fixtures/env-tool/{skill.yaml → skill.package.yaml} +0 -0
  71. /package/tests/fixtures/greeter/{skill.yaml → skill.package.yaml} +0 -0
  72. /package/tests/fixtures/invalid-tool/{skill.yaml → skill.package.yaml} +0 -0
@@ -0,0 +1,263 @@
1
+ /**
2
+ * enact org command
3
+ *
4
+ * Manage organizations for @org scoped tool namespaces.
5
+ *
6
+ * Subcommands:
7
+ * - create: Create a new organization
8
+ * - info: Show organization details
9
+ * - list: List your organizations (TODO: requires user orgs endpoint)
10
+ * - add-member: Add a member to an organization
11
+ * - remove-member: Remove a member from an organization
12
+ * - set-role: Change a member's role
13
+ */
14
+
15
+ import {
16
+ addOrgMember,
17
+ createApiClient,
18
+ createOrg,
19
+ getOrg,
20
+ listOrgMembers,
21
+ removeOrgMember,
22
+ updateOrgMemberRole,
23
+ } from "@enactprotocol/api";
24
+ import { getSecret } from "@enactprotocol/secrets";
25
+ import { loadConfig } from "@enactprotocol/shared";
26
+ import type { OrgRole } from "@enactprotocol/shared";
27
+ import type { Command } from "commander";
28
+ import type { GlobalOptions } from "../../types";
29
+ import {
30
+ type TableColumn,
31
+ dim,
32
+ error,
33
+ formatError,
34
+ header,
35
+ json,
36
+ keyValue,
37
+ newline,
38
+ success,
39
+ table,
40
+ } from "../../utils";
41
+
42
+ const AUTH_NAMESPACE = "enact:auth";
43
+ const ACCESS_TOKEN_KEY = "access_token";
44
+
45
+ const VALID_ROLES = ["owner", "admin", "member"] as const;
46
+
47
+ async function getClient() {
48
+ const config = loadConfig();
49
+ const token = await getSecret(AUTH_NAMESPACE, ACCESS_TOKEN_KEY);
50
+ return createApiClient({
51
+ baseUrl: config.registry?.url,
52
+ authToken: token ?? undefined,
53
+ });
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Handlers
58
+ // ---------------------------------------------------------------------------
59
+
60
+ async function createHandler(
61
+ name: string,
62
+ options: { displayName?: string; description?: string } & GlobalOptions
63
+ ) {
64
+ try {
65
+ const client = await getClient();
66
+ const org = await createOrg(client, {
67
+ name,
68
+ displayName: options.displayName,
69
+ description: options.description,
70
+ });
71
+
72
+ if (options.json) {
73
+ json(org);
74
+ return;
75
+ }
76
+
77
+ success(`Organization "@${org.name}" created`);
78
+ newline();
79
+ keyValue("Name", `@${org.name}`);
80
+ if (org.display_name) keyValue("Display Name", org.display_name);
81
+ if (org.description) keyValue("Description", org.description);
82
+ newline();
83
+ dim("You are the owner. Publish tools with:");
84
+ dim(` enact publish (with name: "@${org.name}/your-tool" in SKILL.md)`);
85
+ } catch (err) {
86
+ error(formatError(err));
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ async function infoHandler(name: string, options: GlobalOptions) {
92
+ try {
93
+ // Strip @ prefix if provided
94
+ const orgName = name.startsWith("@") ? name.substring(1) : name;
95
+ const client = await getClient();
96
+
97
+ const org = await getOrg(client, orgName);
98
+ const members = await listOrgMembers(client, orgName);
99
+
100
+ if (options.json) {
101
+ json({ ...org, members });
102
+ return;
103
+ }
104
+
105
+ header(`@${org.name}`);
106
+ if (org.display_name) keyValue("Display Name", org.display_name);
107
+ if (org.description) keyValue("Description", org.description);
108
+ keyValue("Tools", String(org.tool_count));
109
+ keyValue("Members", String(org.member_count));
110
+ keyValue("Created", org.created_at);
111
+
112
+ newline();
113
+ header("Members");
114
+ const columns: TableColumn[] = [
115
+ { header: "Username", key: "username", width: 20 },
116
+ { header: "Role", key: "role", width: 10 },
117
+ { header: "Added", key: "added_at", width: 20 },
118
+ ];
119
+ table(
120
+ members.map((m) => ({
121
+ username: m.username,
122
+ role: m.role,
123
+ added_at: m.added_at,
124
+ })),
125
+ columns
126
+ );
127
+ } catch (err) {
128
+ error(formatError(err));
129
+ process.exit(1);
130
+ }
131
+ }
132
+
133
+ async function addMemberHandler(
134
+ orgName: string,
135
+ username: string,
136
+ options: { role?: string } & GlobalOptions
137
+ ) {
138
+ try {
139
+ const org = orgName.startsWith("@") ? orgName.substring(1) : orgName;
140
+ const role = (options.role ?? "member") as OrgRole;
141
+
142
+ if (!VALID_ROLES.includes(role as (typeof VALID_ROLES)[number])) {
143
+ error(`Invalid role: ${role}. Must be one of: ${VALID_ROLES.join(", ")}`);
144
+ process.exit(1);
145
+ }
146
+
147
+ const client = await getClient();
148
+ await addOrgMember(client, org, { username, role });
149
+
150
+ if (options.json) {
151
+ json({ username, role, org });
152
+ return;
153
+ }
154
+
155
+ success(`Added "${username}" to @${org} as ${role}`);
156
+ } catch (err) {
157
+ error(formatError(err));
158
+ process.exit(1);
159
+ }
160
+ }
161
+
162
+ async function removeMemberHandler(orgName: string, username: string, options: GlobalOptions) {
163
+ try {
164
+ const org = orgName.startsWith("@") ? orgName.substring(1) : orgName;
165
+ const client = await getClient();
166
+ await removeOrgMember(client, org, username);
167
+
168
+ if (options.json) {
169
+ json({ removed: username, org });
170
+ return;
171
+ }
172
+
173
+ success(`Removed "${username}" from @${org}`);
174
+ } catch (err) {
175
+ error(formatError(err));
176
+ process.exit(1);
177
+ }
178
+ }
179
+
180
+ async function setRoleHandler(
181
+ orgName: string,
182
+ username: string,
183
+ role: string,
184
+ options: GlobalOptions
185
+ ) {
186
+ try {
187
+ const org = orgName.startsWith("@") ? orgName.substring(1) : orgName;
188
+
189
+ if (!VALID_ROLES.includes(role as (typeof VALID_ROLES)[number])) {
190
+ error(`Invalid role: ${role}. Must be one of: ${VALID_ROLES.join(", ")}`);
191
+ process.exit(1);
192
+ }
193
+
194
+ const client = await getClient();
195
+ await updateOrgMemberRole(client, org, username, role as OrgRole);
196
+
197
+ if (options.json) {
198
+ json({ username, role, org });
199
+ return;
200
+ }
201
+
202
+ success(`Set "${username}" role to ${role} in @${org}`);
203
+ } catch (err) {
204
+ error(formatError(err));
205
+ process.exit(1);
206
+ }
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Command Configuration
211
+ // ---------------------------------------------------------------------------
212
+
213
+ export function configureOrgCommand(program: Command): void {
214
+ const org = program.command("org").description("Manage organizations (@org scoped namespaces)");
215
+
216
+ // enact org create <name>
217
+ org
218
+ .command("create <name>")
219
+ .description("Create a new organization")
220
+ .option("--display-name <name>", "Display name for the organization")
221
+ .option("--description <desc>", "Organization description")
222
+ .option("--json", "Output as JSON")
223
+ .action(async (name: string, options) => {
224
+ await createHandler(name, options);
225
+ });
226
+
227
+ // enact org info <name>
228
+ org
229
+ .command("info <name>")
230
+ .description("Show organization details and members")
231
+ .option("--json", "Output as JSON")
232
+ .action(async (name: string, options) => {
233
+ await infoHandler(name, options);
234
+ });
235
+
236
+ // enact org add-member <org> <username>
237
+ org
238
+ .command("add-member <org> <username>")
239
+ .description("Add a member to an organization")
240
+ .option("--role <role>", "Member role (owner, admin, member)", "member")
241
+ .option("--json", "Output as JSON")
242
+ .action(async (orgName: string, username: string, options) => {
243
+ await addMemberHandler(orgName, username, options);
244
+ });
245
+
246
+ // enact org remove-member <org> <username>
247
+ org
248
+ .command("remove-member <org> <username>")
249
+ .description("Remove a member from an organization")
250
+ .option("--json", "Output as JSON")
251
+ .action(async (orgName: string, username: string, options) => {
252
+ await removeMemberHandler(orgName, username, options);
253
+ });
254
+
255
+ // enact org set-role <org> <username> <role>
256
+ org
257
+ .command("set-role <org> <username> <role>")
258
+ .description("Change a member's role (owner, admin, member)")
259
+ .option("--json", "Output as JSON")
260
+ .action(async (orgName: string, username: string, role: string, options) => {
261
+ await setRoleHandler(orgName, username, role, options);
262
+ });
263
+ }
@@ -320,7 +320,10 @@ async function publishHandler(
320
320
  const currentUsername = await getCurrentUsername();
321
321
  if (currentUsername) {
322
322
  const toolNamespace = extractNamespace(toolName);
323
- if (toolNamespace !== currentUsername) {
323
+ if (toolNamespace.startsWith("@")) {
324
+ // Org-scoped tool — server will enforce org membership
325
+ dim(`Publishing to organization namespace "${toolNamespace}"...`);
326
+ } else if (toolNamespace !== currentUsername) {
324
327
  error(
325
328
  `Namespace mismatch: Tool namespace "${toolNamespace}" does not match your username "${currentUsername}".`
326
329
  );
@@ -10,7 +10,7 @@ enact run <tool> [options]
10
10
 
11
11
  ## Description
12
12
 
13
- The `run` command executes a tool using the command defined in its manifest (`skill.yaml` or `SKILL.md`). The tool runs in an isolated container environment with:
13
+ The `run` command executes a tool using the command defined in its manifest (`skill.package.yml` or `SKILL.md`). The tool runs in an isolated container environment with:
14
14
 
15
15
  - Input validation against the tool's JSON Schema
16
16
  - Automatic secret resolution from the OS keyring
@@ -379,8 +379,8 @@ export function configureSearchCommand(program: Command): void {
379
379
  program
380
380
  .command("search <query>")
381
381
  .description("Search the Enact registry for tools")
382
- .option("--local", "Search project tools (.enact/tools/) instead of registry")
383
- .option("-g, --global", "Search global tools (~/.enact/tools/) instead of registry")
382
+ .option("--local", "Search project skills (agents/skills/) instead of registry")
383
+ .option("-g, --global", "Search global skills (~/.agents/skills/) instead of registry")
384
384
  .option("-t, --tags <tags>", "Filter by tags (comma-separated, registry only)")
385
385
  .option("-l, --limit <number>", "Maximum results to return (default: 20, registry only)")
386
386
  .option("-o, --offset <number>", "Pagination offset (default: 0, registry only)")
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  configureLearnCommand,
23
23
  configureListCommand,
24
24
  configureMcpCommand,
25
+ configureOrgCommand,
25
26
  configurePublishCommand,
26
27
  configureReportCommand,
27
28
  configureRunCommand,
@@ -37,7 +38,7 @@ import {
37
38
  } from "./commands";
38
39
  import { error, formatError } from "./utils";
39
40
 
40
- export const version = "2.3.5";
41
+ export const version = "2.3.8";
41
42
 
42
43
  // Export types for external use
43
44
  export type { GlobalOptions, CommandContext } from "./types";
@@ -93,6 +94,9 @@ async function main() {
93
94
  // Self-hosted registry
94
95
  configureServeCommand(program);
95
96
 
97
+ // Organizations
98
+ configureOrgCommand(program);
99
+
96
100
  // Global error handler - handle Commander's help/version exits gracefully
97
101
  program.exitOverride((err) => {
98
102
  // Commander throws errors for help, version, and other "exit" scenarios
@@ -84,7 +84,7 @@ export class ManifestError extends CliError {
84
84
  super(
85
85
  fullMessage,
86
86
  EXIT_MANIFEST_ERROR,
87
- "Ensure the directory contains a valid skill.yaml or SKILL.md file."
87
+ "Ensure the directory contains a valid skill.package.yaml, skill.package.yml, or SKILL.md file."
88
88
  );
89
89
  this.name = "ManifestError";
90
90
  }
@@ -377,7 +377,7 @@ export const ErrorMessages = {
377
377
  message: `No manifest found in ${dir}`,
378
378
  suggestions: [
379
379
  `Create a manifest: ${colors.command("enact init")}`,
380
- "Ensure the directory contains skill.yaml or SKILL.md",
380
+ "Ensure the directory contains skill.package.yaml, skill.package.yml, or SKILL.md",
381
381
  ],
382
382
  }),
383
383
 
@@ -271,14 +271,14 @@ describe("cache command", () => {
271
271
  }
272
272
 
273
273
  const info: CacheInfo = {
274
- path: "/Users/test/.agent/skills",
274
+ path: "/Users/test/.agents/skills",
275
275
  totalSize: 10_485_760, // 10 MB
276
276
  toolCount: 25,
277
277
  oldestEntry: "2024-01-01T00:00:00Z",
278
278
  newestEntry: "2024-01-20T12:00:00Z",
279
279
  };
280
280
 
281
- expect(info.path).toContain(".agent");
281
+ expect(info.path).toContain(".agents");
282
282
  expect(info.totalSize).toBeGreaterThan(0);
283
283
  expect(info.toolCount).toBeGreaterThanOrEqual(0);
284
284
  });
@@ -107,7 +107,7 @@ describe("init command", () => {
107
107
  }
108
108
  });
109
109
 
110
- test("default mode creates AGENTS.md for tool consumers", async () => {
110
+ test("default mode creates skill.package.yaml", async () => {
111
111
  const program = new Command();
112
112
  program.exitOverride();
113
113
  configureInitCommand(program);
@@ -116,27 +116,23 @@ describe("init command", () => {
116
116
  process.chdir(testDir);
117
117
 
118
118
  try {
119
- await program.parseAsync(["node", "test", "init"]);
119
+ await program.parseAsync(["node", "test", "init", "--name", "test/my-tool"]);
120
120
  } catch {
121
121
  // Command may throw due to exitOverride
122
122
  } finally {
123
123
  process.chdir(originalCwd);
124
124
  }
125
125
 
126
- const agentsPath = join(testDir, "AGENTS.md");
127
- expect(existsSync(agentsPath)).toBe(true);
126
+ const packagePath = join(testDir, "skill.package.yaml");
127
+ expect(existsSync(packagePath)).toBe(true);
128
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);
129
+ const content = readFileSync(packagePath, "utf-8");
130
+ expect(content).toContain("name: test/my-tool");
131
+ expect(content).toContain("enact:");
132
+ expect(content).toContain("scripts:");
137
133
  });
138
134
 
139
- test("default mode creates .enact/tools.json", async () => {
135
+ test("default mode creates hello.py", async () => {
140
136
  const program = new Command();
141
137
  program.exitOverride();
142
138
  configureInitCommand(program);
@@ -145,18 +141,19 @@ describe("init command", () => {
145
141
  process.chdir(testDir);
146
142
 
147
143
  try {
148
- await program.parseAsync(["node", "test", "init"]);
144
+ await program.parseAsync(["node", "test", "init", "--name", "test/my-tool"]);
149
145
  } catch {
150
146
  // Command may throw due to exitOverride
151
147
  } finally {
152
148
  process.chdir(originalCwd);
153
149
  }
154
150
 
155
- const toolsJsonPath = join(testDir, ".enact", "tools.json");
156
- expect(existsSync(toolsJsonPath)).toBe(true);
151
+ const scriptPath = join(testDir, "hello.py");
152
+ expect(existsSync(scriptPath)).toBe(true);
157
153
 
158
- const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
159
- expect(content).toEqual({ tools: {} });
154
+ const content = readFileSync(scriptPath, "utf-8");
155
+ expect(content).toContain("json.dumps");
156
+ expect(content).toContain("Hello");
160
157
  });
161
158
 
162
159
  test("--tool mode creates SKILL.md", async () => {
@@ -267,7 +264,7 @@ describe("init command", () => {
267
264
  expect(existsSync(join(testDir, "AGENTS.md"))).toBe(false);
268
265
  });
269
266
 
270
- test("--agent mode creates .enact/tools.json", async () => {
267
+ test("--agent mode creates agents/skills.json", async () => {
271
268
  const program = new Command();
272
269
  program.exitOverride();
273
270
  configureInitCommand(program);
@@ -283,14 +280,14 @@ describe("init command", () => {
283
280
  process.chdir(originalCwd);
284
281
  }
285
282
 
286
- const toolsJsonPath = join(testDir, ".enact", "tools.json");
287
- expect(existsSync(toolsJsonPath)).toBe(true);
283
+ const skillsJsonPath = join(testDir, "agents", "skills.json");
284
+ expect(existsSync(skillsJsonPath)).toBe(true);
288
285
 
289
- const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
286
+ const content = JSON.parse(readFileSync(skillsJsonPath, "utf-8"));
290
287
  expect(content).toEqual({ tools: {} });
291
288
  });
292
289
 
293
- test("--claude mode creates .enact/tools.json", async () => {
290
+ test("--claude mode creates agents/skills.json", async () => {
294
291
  const program = new Command();
295
292
  program.exitOverride();
296
293
  configureInitCommand(program);
@@ -306,20 +303,20 @@ describe("init command", () => {
306
303
  process.chdir(originalCwd);
307
304
  }
308
305
 
309
- const toolsJsonPath = join(testDir, ".enact", "tools.json");
310
- expect(existsSync(toolsJsonPath)).toBe(true);
306
+ const skillsJsonPath = join(testDir, "agents", "skills.json");
307
+ expect(existsSync(skillsJsonPath)).toBe(true);
311
308
 
312
- const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
309
+ const content = JSON.parse(readFileSync(skillsJsonPath, "utf-8"));
313
310
  expect(content).toEqual({ tools: {} });
314
311
  });
315
312
 
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");
313
+ test("--agent mode with --force overwrites existing agents/skills.json", async () => {
314
+ // Create existing agents/skills.json with some content
315
+ const agentsDir = join(testDir, "agents");
316
+ mkdirSync(agentsDir, { recursive: true });
317
+ const skillsJsonPath = join(agentsDir, "skills.json");
321
318
  const existingContent = { tools: { "some/tool": "1.0.0" } };
322
- writeFileSync(toolsJsonPath, JSON.stringify(existingContent));
319
+ writeFileSync(skillsJsonPath, JSON.stringify(existingContent));
323
320
 
324
321
  const program = new Command();
325
322
  program.exitOverride();
@@ -336,17 +333,17 @@ describe("init command", () => {
336
333
  process.chdir(originalCwd);
337
334
  }
338
335
 
339
- const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
336
+ const content = JSON.parse(readFileSync(skillsJsonPath, "utf-8"));
340
337
  expect(content).toEqual({ tools: {} });
341
338
  });
342
339
 
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");
340
+ test("--agent mode preserves existing agents/skills.json without --force", async () => {
341
+ // Create existing agents/skills.json with some content
342
+ const agentsDir = join(testDir, "agents");
343
+ mkdirSync(agentsDir, { recursive: true });
344
+ const skillsJsonPath = join(agentsDir, "skills.json");
348
345
  const existingContent = { tools: { "some/tool": "1.0.0" } };
349
- writeFileSync(toolsJsonPath, JSON.stringify(existingContent));
346
+ writeFileSync(skillsJsonPath, JSON.stringify(existingContent));
350
347
 
351
348
  // Also create AGENTS.md so the command doesn't fail early
352
349
  writeFileSync(join(testDir, "AGENTS.md"), "existing");
@@ -359,10 +356,6 @@ describe("init command", () => {
359
356
  process.chdir(testDir);
360
357
 
361
358
  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
359
  await program.parseAsync(["node", "test", "init", "--agent"]);
367
360
  } catch {
368
361
  // Command may throw due to exitOverride or warning about existing file
@@ -370,12 +363,12 @@ describe("init command", () => {
370
363
  process.chdir(originalCwd);
371
364
  }
372
365
 
373
- // tools.json should be preserved since AGENTS.md existed and no --force was used
374
- const content = JSON.parse(readFileSync(toolsJsonPath, "utf-8"));
366
+ // skills.json should be preserved since AGENTS.md existed and no --force was used
367
+ const content = JSON.parse(readFileSync(skillsJsonPath, "utf-8"));
375
368
  expect(content).toEqual(existingContent);
376
369
  });
377
370
 
378
- test("--tool mode does NOT create .enact/tools.json", async () => {
371
+ test("--tool mode does NOT create agents/skills.json", async () => {
379
372
  const program = new Command();
380
373
  program.exitOverride();
381
374
  configureInitCommand(program);
@@ -391,8 +384,8 @@ describe("init command", () => {
391
384
  process.chdir(originalCwd);
392
385
  }
393
386
 
394
- const toolsJsonPath = join(testDir, ".enact", "tools.json");
395
- expect(existsSync(toolsJsonPath)).toBe(false);
387
+ const skillsJsonPath = join(testDir, "agents", "skills.json");
388
+ expect(existsSync(skillsJsonPath)).toBe(false);
396
389
  });
397
390
 
398
391
  test("SKILL.md contains valid YAML frontmatter", async () => {
@@ -481,21 +474,39 @@ describe("init command", () => {
481
474
  expect(content).toContain("enact search");
482
475
  expect(content).toContain("enact install");
483
476
  expect(content).toContain("enact list");
484
- expect(content).toContain(".enact/tools.json");
477
+ expect(content).toContain("agents/skills.json");
485
478
  });
486
479
  });
487
480
 
488
481
  describe("option conflicts", () => {
489
- test("--agent is the default when no mode specified", () => {
482
+ test("default mode creates a skill when no mode specified", async () => {
490
483
  const program = new Command();
484
+ program.exitOverride();
491
485
  configureInitCommand(program);
492
486
 
493
- const initCmd = program.commands.find((cmd) => cmd.name() === "init");
494
- const opts = initCmd?.options ?? [];
495
- const agentOpt = opts.find((o) => o.long === "--agent");
487
+ const testDir = join(import.meta.dir, ".test-init-default");
488
+ if (existsSync(testDir)) {
489
+ rmSync(testDir, { recursive: true });
490
+ }
491
+ mkdirSync(testDir, { recursive: true });
492
+
493
+ const originalCwd = process.cwd();
494
+ process.chdir(testDir);
495
+
496
+ try {
497
+ await program.parseAsync(["node", "test", "init", "--name", "test/tool"]);
498
+ } catch {
499
+ // Command may throw due to exitOverride
500
+ } finally {
501
+ process.chdir(originalCwd);
502
+ }
503
+
504
+ // Default should create skill files, not AGENTS.md
505
+ expect(existsSync(join(testDir, "skill.package.yaml"))).toBe(true);
506
+ expect(existsSync(join(testDir, "hello.py"))).toBe(true);
507
+ expect(existsSync(join(testDir, "AGENTS.md"))).toBe(false);
496
508
 
497
- // Description should indicate it's the default
498
- expect(agentOpt?.description).toContain("default");
509
+ rmSync(testDir, { recursive: true });
499
510
  });
500
511
  });
501
512
 
@@ -46,7 +46,7 @@ describe("install integration", () => {
46
46
  mkdirSync(join(TEST_GLOBAL_HOME, ".enact", "cache"), { recursive: true });
47
47
 
48
48
  // Create sample tool source
49
- writeFileSync(join(TEST_TOOL_SRC, "skill.yaml"), SAMPLE_MANIFEST);
49
+ writeFileSync(join(TEST_TOOL_SRC, "skill.package.yml"), SAMPLE_MANIFEST);
50
50
  });
51
51
 
52
52
  beforeEach(() => {
@@ -92,7 +92,7 @@ describe("install integration", () => {
92
92
 
93
93
  // Verify installation
94
94
  expect(existsSync(destPath)).toBe(true);
95
- expect(existsSync(join(destPath, "skill.yaml"))).toBe(true);
95
+ expect(existsSync(join(destPath, "skill.package.yml"))).toBe(true);
96
96
 
97
97
  // Verify manifest can be loaded from destination
98
98
  const installedManifest = loadManifestFromDir(destPath);
@@ -118,7 +118,7 @@ describe("install integration", () => {
118
118
  // Create v2 source
119
119
  const v2Source = join(TEST_BASE, "source-tool-v2");
120
120
  mkdirSync(v2Source, { recursive: true });
121
- writeFileSync(join(v2Source, "skill.yaml"), SAMPLE_MANIFEST_V2);
121
+ writeFileSync(join(v2Source, "skill.package.yml"), SAMPLE_MANIFEST_V2);
122
122
 
123
123
  // Simulate force overwrite
124
124
  rmSync(destPath, { recursive: true, force: true });
@@ -167,9 +167,9 @@ describe("install integration", () => {
167
167
  test("global install extracts to skills path", async () => {
168
168
  const { getToolCachePath } = await import("@enactprotocol/shared");
169
169
 
170
- // Verify skill path structure (~/.agent/skills/{name}/, no version subdir)
170
+ // Verify skill path structure (~/.agents/skills/{name}/, no version subdir)
171
171
  const skillPath = getToolCachePath("test/sample-tool", "1.0.0");
172
- expect(skillPath).toContain(".agent");
172
+ expect(skillPath).toContain(".agents");
173
173
  expect(skillPath).toContain("skills");
174
174
  expect(skillPath).toContain("test/sample-tool");
175
175
  });
@@ -240,7 +240,7 @@ describe("install integration", () => {
240
240
  const cachePath = join(TEST_GLOBAL_HOME, ".enact", "cache", "test", "cached-tool", "v1.0.0");
241
241
  mkdirSync(cachePath, { recursive: true });
242
242
  writeFileSync(
243
- join(cachePath, "skill.yaml"),
243
+ join(cachePath, "skill.package.yml"),
244
244
  `
245
245
  name: test/cached-tool
246
246
  version: 1.0.0
@@ -251,7 +251,7 @@ command: echo "cached"
251
251
 
252
252
  // The resolver should be able to find this tool once it's registered
253
253
  // Note: Full resolver testing is in resolver.test.ts
254
- expect(existsSync(join(cachePath, "skill.yaml"))).toBe(true);
254
+ expect(existsSync(join(cachePath, "skill.package.yml"))).toBe(true);
255
255
  });
256
256
  });
257
257
  });
@@ -260,7 +260,7 @@ describe("tools.json edge cases", () => {
260
260
  const EDGE_TEST_DIR = join(TEST_BASE, "edge-cases");
261
261
 
262
262
  beforeAll(() => {
263
- mkdirSync(join(EDGE_TEST_DIR, ".enact"), { recursive: true });
263
+ mkdirSync(join(EDGE_TEST_DIR, "agents"), { recursive: true });
264
264
  });
265
265
 
266
266
  afterAll(() => {
@@ -270,10 +270,10 @@ describe("tools.json edge cases", () => {
270
270
  });
271
271
 
272
272
  beforeEach(() => {
273
- // Ensure .enact directory exists
274
- mkdirSync(join(EDGE_TEST_DIR, ".enact"), { recursive: true });
273
+ // Ensure agents directory exists
274
+ mkdirSync(join(EDGE_TEST_DIR, "agents"), { recursive: true });
275
275
 
276
- const jsonPath = join(EDGE_TEST_DIR, ".enact", "tools.json");
276
+ const jsonPath = join(EDGE_TEST_DIR, "agents", "skills.json");
277
277
  if (existsSync(jsonPath)) {
278
278
  rmSync(jsonPath);
279
279
  }