@enactprotocol/shared 2.2.2 → 2.3.1

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 (111) hide show
  1. package/README.md +1 -18
  2. package/dist/config.d.ts +12 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +32 -6
  5. package/dist/config.js.map +1 -1
  6. package/dist/execution/action-command.d.ts +131 -0
  7. package/dist/execution/action-command.d.ts.map +1 -0
  8. package/dist/execution/action-command.js +300 -0
  9. package/dist/execution/action-command.js.map +1 -0
  10. package/dist/execution/command.d.ts +8 -8
  11. package/dist/execution/command.js +6 -6
  12. package/dist/execution/index.d.ts +1 -0
  13. package/dist/execution/index.d.ts.map +1 -1
  14. package/dist/execution/index.js +2 -0
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/types.d.ts +5 -2
  17. package/dist/execution/types.d.ts.map +1 -1
  18. package/dist/execution/types.js.map +1 -1
  19. package/dist/index.d.ts +9 -7
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +14 -5
  22. package/dist/index.js.map +1 -1
  23. package/dist/manifest/actions-loader.d.ts +29 -0
  24. package/dist/manifest/actions-loader.d.ts.map +1 -0
  25. package/dist/manifest/actions-loader.js +34 -0
  26. package/dist/manifest/actions-loader.js.map +1 -0
  27. package/dist/manifest/actions-parser.d.ts +69 -0
  28. package/dist/manifest/actions-parser.d.ts.map +1 -0
  29. package/dist/manifest/actions-parser.js +265 -0
  30. package/dist/manifest/actions-parser.js.map +1 -0
  31. package/dist/manifest/index.d.ts +2 -0
  32. package/dist/manifest/index.d.ts.map +1 -1
  33. package/dist/manifest/index.js +4 -0
  34. package/dist/manifest/index.js.map +1 -1
  35. package/dist/manifest/loader.d.ts +7 -2
  36. package/dist/manifest/loader.d.ts.map +1 -1
  37. package/dist/manifest/loader.js +71 -4
  38. package/dist/manifest/loader.js.map +1 -1
  39. package/dist/manifest/parser.d.ts +1 -0
  40. package/dist/manifest/parser.d.ts.map +1 -1
  41. package/dist/manifest/parser.js +1 -0
  42. package/dist/manifest/parser.js.map +1 -1
  43. package/dist/manifest/scripts.d.ts +19 -0
  44. package/dist/manifest/scripts.d.ts.map +1 -0
  45. package/dist/manifest/scripts.js +102 -0
  46. package/dist/manifest/scripts.js.map +1 -0
  47. package/dist/manifest/validator.d.ts +1 -8
  48. package/dist/manifest/validator.d.ts.map +1 -1
  49. package/dist/manifest/validator.js +14 -13
  50. package/dist/manifest/validator.js.map +1 -1
  51. package/dist/mcp-registry.js +5 -5
  52. package/dist/mcp-registry.js.map +1 -1
  53. package/dist/paths.d.ts +9 -2
  54. package/dist/paths.d.ts.map +1 -1
  55. package/dist/paths.js +12 -3
  56. package/dist/paths.js.map +1 -1
  57. package/dist/registry.d.ts +47 -2
  58. package/dist/registry.d.ts.map +1 -1
  59. package/dist/registry.js +100 -7
  60. package/dist/registry.js.map +1 -1
  61. package/dist/resolver.d.ts +55 -4
  62. package/dist/resolver.d.ts.map +1 -1
  63. package/dist/resolver.js +144 -77
  64. package/dist/resolver.js.map +1 -1
  65. package/dist/types/actions.d.ts +194 -0
  66. package/dist/types/actions.d.ts.map +1 -0
  67. package/dist/types/actions.js +32 -0
  68. package/dist/types/actions.js.map +1 -0
  69. package/dist/types/index.d.ts +3 -1
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/dist/types/index.js +1 -0
  72. package/dist/types/index.js.map +1 -1
  73. package/dist/types/manifest.d.ts +50 -5
  74. package/dist/types/manifest.d.ts.map +1 -1
  75. package/dist/types/manifest.js +10 -2
  76. package/dist/types/manifest.js.map +1 -1
  77. package/package.json +2 -2
  78. package/src/config.ts +48 -6
  79. package/src/execution/action-command.ts +417 -0
  80. package/src/execution/command.ts +8 -8
  81. package/src/execution/index.ts +17 -0
  82. package/src/execution/types.ts +13 -2
  83. package/src/index.ts +43 -0
  84. package/src/manifest/actions-loader.ts +49 -0
  85. package/src/manifest/index.ts +12 -0
  86. package/src/manifest/loader.ts +77 -4
  87. package/src/manifest/parser.ts +1 -0
  88. package/src/manifest/scripts.ts +116 -0
  89. package/src/manifest/validator.ts +15 -14
  90. package/src/mcp-registry.ts +5 -5
  91. package/src/paths.ts +13 -3
  92. package/src/registry.ts +136 -7
  93. package/src/resolver.ts +185 -79
  94. package/src/types/actions.ts +223 -0
  95. package/src/types/index.ts +11 -0
  96. package/src/types/manifest.ts +67 -6
  97. package/tests/action-command.test.ts +249 -0
  98. package/tests/config-normalization.test.ts +279 -0
  99. package/tests/config.test.ts +4 -1
  100. package/tests/effective-input-schema.test.ts +86 -0
  101. package/tests/fixtures/valid-tool.md +5 -12
  102. package/tests/fixtures/valid-tool.yaml +3 -10
  103. package/tests/hooks.test.ts +177 -0
  104. package/tests/manifest/loader.test.ts +34 -20
  105. package/tests/manifest/parser.test.ts +11 -15
  106. package/tests/manifest/validator.test.ts +7 -17
  107. package/tests/manifest-types.test.ts +9 -11
  108. package/tests/paths.test.ts +11 -4
  109. package/tests/registry.test.ts +204 -8
  110. package/tests/resolver.test.ts +90 -6
  111. package/tsconfig.tsbuildinfo +1 -1
@@ -34,7 +34,7 @@ describe("manifest loader", () => {
34
34
  const filePath = join(FIXTURES_DIR, "valid-tool.yaml");
35
35
  const result = loadManifest(filePath);
36
36
 
37
- expect(result.manifest.name).toBe("acme/utils/greeter");
37
+ expect(result.manifest.name).toBe("acme/greeter");
38
38
  expect(result.manifest.description).toBe("Greets users by name");
39
39
  expect(result.manifest.version).toBe("1.0.0");
40
40
  expect(result.format).toBe("yaml");
@@ -46,7 +46,7 @@ describe("manifest loader", () => {
46
46
  const filePath = join(FIXTURES_DIR, "valid-tool.md");
47
47
  const result = loadManifest(filePath);
48
48
 
49
- expect(result.manifest.name).toBe("acme/docs/analyzer");
49
+ expect(result.manifest.name).toBe("acme/analyzer");
50
50
  expect(result.manifest.description).toBe("Analyzes documentation for completeness");
51
51
  expect(result.format).toBe("md");
52
52
  expect(result.body).toBeDefined();
@@ -104,11 +104,11 @@ description: A minimal tool
104
104
  });
105
105
 
106
106
  describe("loadManifestFromDir", () => {
107
- test("loads manifest from directory with enact.yaml", () => {
107
+ test("loads manifest from directory with skill.yaml", () => {
108
108
  const testDir = join(TEST_DIR, "yaml-dir");
109
109
  mkdirSync(testDir, { recursive: true });
110
110
  writeFileSync(
111
- join(testDir, "enact.yaml"),
111
+ join(testDir, "skill.yaml"),
112
112
  `
113
113
  name: test/yaml
114
114
  description: Test YAML manifest
@@ -120,6 +120,22 @@ description: Test YAML manifest
120
120
  expect(result.manifest.name).toBe("test/yaml");
121
121
  });
122
122
 
123
+ test("loads manifest from directory with legacy enact.yaml", () => {
124
+ const testDir = join(TEST_DIR, "legacy-yaml-dir");
125
+ mkdirSync(testDir, { recursive: true });
126
+ writeFileSync(
127
+ join(testDir, "enact.yaml"),
128
+ `
129
+ name: test/legacy-yaml
130
+ description: Test legacy YAML manifest
131
+ `,
132
+ "utf-8"
133
+ );
134
+
135
+ const result = loadManifestFromDir(testDir);
136
+ expect(result.manifest.name).toBe("test/legacy-yaml");
137
+ });
138
+
123
139
  test("loads manifest from directory with enact.md", () => {
124
140
  const testDir = join(TEST_DIR, "md-dir");
125
141
  mkdirSync(testDir, { recursive: true });
@@ -140,31 +156,29 @@ description: Test Markdown manifest
140
156
  expect(result.format).toBe("md");
141
157
  });
142
158
 
143
- test("prefers enact.md over enact.yaml", () => {
159
+ test("prefers skill.yaml over legacy enact.yaml", () => {
144
160
  const testDir = join(TEST_DIR, "both-dir");
145
161
  mkdirSync(testDir, { recursive: true });
146
162
 
147
- // Create both files
148
163
  writeFileSync(
149
- join(testDir, "enact.md"),
150
- `---
151
- name: test/md-preferred
152
- description: Markdown version
153
- ---
164
+ join(testDir, "skill.yaml"),
165
+ `
166
+ name: test/skill-preferred
167
+ description: Skill version
154
168
  `,
155
169
  "utf-8"
156
170
  );
157
171
  writeFileSync(
158
172
  join(testDir, "enact.yaml"),
159
173
  `
160
- name: test/yaml-version
161
- description: YAML version
174
+ name: test/enact-version
175
+ description: Legacy version
162
176
  `,
163
177
  "utf-8"
164
178
  );
165
179
 
166
180
  const result = loadManifestFromDir(testDir);
167
- expect(result.manifest.name).toBe("test/md-preferred");
181
+ expect(result.manifest.name).toBe("test/skill-preferred");
168
182
  });
169
183
 
170
184
  test("throws for directory without manifest", () => {
@@ -177,13 +191,13 @@ description: YAML version
177
191
  });
178
192
 
179
193
  describe("findManifestFile", () => {
180
- test("finds enact.yaml", () => {
194
+ test("finds skill.yaml", () => {
181
195
  const testDir = join(TEST_DIR, "find-yaml");
182
196
  mkdirSync(testDir, { recursive: true });
183
- writeFileSync(join(testDir, "enact.yaml"), "name: test/find", "utf-8");
197
+ writeFileSync(join(testDir, "skill.yaml"), "name: test/find", "utf-8");
184
198
 
185
199
  const result = findManifestFile(testDir);
186
- expect(result).toBe(join(testDir, "enact.yaml"));
200
+ expect(result).toBe(join(testDir, "skill.yaml"));
187
201
  });
188
202
 
189
203
  test("finds enact.md", () => {
@@ -208,7 +222,7 @@ description: YAML version
208
222
  test("returns true when manifest exists", () => {
209
223
  const testDir = join(TEST_DIR, "has-manifest");
210
224
  mkdirSync(testDir, { recursive: true });
211
- writeFileSync(join(testDir, "enact.yaml"), "name: test/has", "utf-8");
225
+ writeFileSync(join(testDir, "skill.yaml"), "name: test/has", "utf-8");
212
226
 
213
227
  expect(hasManifest(testDir)).toBe(true);
214
228
  });
@@ -227,7 +241,7 @@ description: YAML version
227
241
  const result = tryLoadManifest(filePath);
228
242
 
229
243
  expect(result).not.toBeNull();
230
- expect(result?.manifest.name).toBe("acme/utils/greeter");
244
+ expect(result?.manifest.name).toBe("acme/greeter");
231
245
  });
232
246
 
233
247
  test("returns null on failure", () => {
@@ -250,7 +264,7 @@ description: YAML version
250
264
  const testDir = join(TEST_DIR, "try-success");
251
265
  mkdirSync(testDir, { recursive: true });
252
266
  writeFileSync(
253
- join(testDir, "enact.yaml"),
267
+ join(testDir, "skill.yaml"),
254
268
  `
255
269
  name: test/try
256
270
  description: Test try loading
@@ -25,15 +25,15 @@ version: "1.0.0"
25
25
  test("parses nested YAML", () => {
26
26
  const yaml = `
27
27
  name: org/tool
28
- inputSchema:
28
+ outputSchema:
29
29
  type: object
30
30
  properties:
31
31
  name:
32
32
  type: string
33
33
  `;
34
34
  const result = parseYaml(yaml);
35
- expect(result.inputSchema).toBeDefined();
36
- const schema = result.inputSchema as Record<string, unknown>;
35
+ expect(result.outputSchema).toBeDefined();
36
+ const schema = result.outputSchema as Record<string, unknown>;
37
37
  expect(schema.type).toBe("object");
38
38
  });
39
39
 
@@ -169,27 +169,21 @@ description: A test tool
169
169
  test("parses complete YAML manifest", () => {
170
170
  const yaml = `
171
171
  enact: "2.0.0"
172
- name: acme/utils/greeter
172
+ name: acme/greeter
173
173
  description: Greets users
174
174
  version: "1.0.0"
175
175
  from: node:18-alpine
176
- command: "echo 'Hello \${name}'"
177
176
  timeout: 30s
178
177
  license: MIT
179
178
  tags:
180
179
  - greeting
181
180
  - utility
182
- inputSchema:
183
- type: object
184
- properties:
185
- name:
186
- type: string
187
- required:
188
- - name
181
+ scripts:
182
+ greet: "echo 'Hello {{name}}'"
189
183
  `;
190
184
  const result = parseManifest(yaml, "yaml");
191
185
  expect(result.manifest.enact).toBe("2.0.0");
192
- expect(result.manifest.name).toBe("acme/utils/greeter");
186
+ expect(result.manifest.name).toBe("acme/greeter");
193
187
  expect(result.manifest.from).toBe("node:18-alpine");
194
188
  expect(result.manifest.tags).toContain("greeting");
195
189
  });
@@ -266,12 +260,14 @@ Body here.
266
260
 
267
261
  describe("detectFormat", () => {
268
262
  test("detects .yaml extension", () => {
263
+ expect(detectFormat("skill.yaml")).toBe("yaml");
269
264
  expect(detectFormat("enact.yaml")).toBe("yaml");
270
265
  expect(detectFormat("tool.yaml")).toBe("yaml");
271
- expect(detectFormat("/path/to/enact.yaml")).toBe("yaml");
266
+ expect(detectFormat("/path/to/skill.yaml")).toBe("yaml");
272
267
  });
273
268
 
274
269
  test("detects .yml extension", () => {
270
+ expect(detectFormat("skill.yml")).toBe("yaml");
275
271
  expect(detectFormat("enact.yml")).toBe("yaml");
276
272
  expect(detectFormat("/path/to/tool.yml")).toBe("yaml");
277
273
  });
@@ -302,7 +298,7 @@ Body here.
302
298
  name: org/tool
303
299
  description: Test
304
300
  `;
305
- const result = parseManifestAuto(yaml, "enact.yaml");
301
+ const result = parseManifestAuto(yaml, "skill.yaml");
306
302
  expect(result.manifest.name).toBe("org/tool");
307
303
  expect(result.format).toBe("yaml");
308
304
  });
@@ -24,7 +24,7 @@ describe("manifest validator", () => {
24
24
  test("validates complete manifest", () => {
25
25
  const manifest = {
26
26
  enact: "2.0.0",
27
- name: "acme/utils/greeter",
27
+ name: "acme/greeter",
28
28
  description: "Greets users by name",
29
29
  version: "1.0.0",
30
30
  from: "node:18-alpine",
@@ -32,12 +32,8 @@ describe("manifest validator", () => {
32
32
  timeout: "30s",
33
33
  license: "MIT",
34
34
  tags: ["greeting", "utility"],
35
- inputSchema: {
36
- type: "object",
37
- properties: {
38
- name: { type: "string" },
39
- },
40
- required: ["name"],
35
+ scripts: {
36
+ greet: "echo 'Hello {{name}}'",
41
37
  },
42
38
  outputSchema: {
43
39
  type: "object",
@@ -222,13 +218,7 @@ describe("manifest validator", () => {
222
218
  });
223
219
 
224
220
  test("accepts valid tool name formats", () => {
225
- const names = [
226
- "org/tool",
227
- "acme/utils/greeter",
228
- "my-org/category/sub-category/tool-name",
229
- "a/b",
230
- "org_name/tool_name",
231
- ];
221
+ const names = ["org/tool", "acme/greeter", "@my-org/tool-name", "a/b", "org_name/tool_name"];
232
222
 
233
223
  for (const name of names) {
234
224
  const manifest = { name, description: "Test" };
@@ -405,7 +395,7 @@ describe("manifest validator", () => {
405
395
  describe("isValidToolName", () => {
406
396
  test("returns true for valid names", () => {
407
397
  expect(isValidToolName("org/tool")).toBe(true);
408
- expect(isValidToolName("acme/utils/greeter")).toBe(true);
398
+ expect(isValidToolName("@acme/greeter")).toBe(true);
409
399
  expect(isValidToolName("my-org/my-tool")).toBe(true);
410
400
  expect(isValidToolName("org_name/tool_name")).toBe(true);
411
401
  });
@@ -425,9 +415,9 @@ describe("manifest validator", () => {
425
415
  expect(isValidLocalToolName("simple")).toBe(true);
426
416
  });
427
417
 
428
- test("returns true for hierarchical names", () => {
418
+ test("returns true for scoped names", () => {
429
419
  expect(isValidLocalToolName("org/tool")).toBe(true);
430
- expect(isValidLocalToolName("acme/utils/greeter")).toBe(true);
420
+ expect(isValidLocalToolName("@acme/greeter")).toBe(true);
431
421
  });
432
422
 
433
423
  test("returns false for invalid names", () => {
@@ -29,7 +29,7 @@ describe("manifest types", () => {
29
29
 
30
30
  test("accepts full manifest with all fields", () => {
31
31
  const manifest: ToolManifest = {
32
- name: "acme/utils/greeter",
32
+ name: "acme/greeter",
33
33
  description: "Greets users by name",
34
34
  enact: "2.0.0",
35
35
  version: "1.0.0",
@@ -38,12 +38,8 @@ describe("manifest types", () => {
38
38
  timeout: "30s",
39
39
  license: "MIT",
40
40
  tags: ["greeting", "utility"],
41
- inputSchema: {
42
- type: "object",
43
- properties: {
44
- name: { type: "string" },
45
- },
46
- required: ["name"],
41
+ scripts: {
42
+ greet: "echo 'Hello {{name}}'",
47
43
  },
48
44
  outputSchema: {
49
45
  type: "object",
@@ -90,7 +86,7 @@ describe("manifest types", () => {
90
86
  "x-custom-field": "custom value",
91
87
  };
92
88
 
93
- expect(manifest.name).toBe("acme/utils/greeter");
89
+ expect(manifest.name).toBe("acme/greeter");
94
90
  expect(manifest.enact).toBe("2.0.0");
95
91
  expect(manifest.env?.API_KEY?.secret).toBe(true);
96
92
  expect(manifest.annotations?.readOnlyHint).toBe(true);
@@ -314,7 +310,7 @@ describe("manifest types", () => {
314
310
  },
315
311
  sourceDir: "/home/user/.enact/tools/org/tool",
316
312
  location: "user",
317
- manifestPath: "/home/user/.enact/tools/org/tool/enact.yaml",
313
+ manifestPath: "/home/user/.enact/tools/org/tool/skill.yaml",
318
314
  version: "1.0.0",
319
315
  };
320
316
 
@@ -336,7 +332,7 @@ describe("manifest types", () => {
336
332
  manifest: { name: "test", description: "test" },
337
333
  sourceDir: "/test",
338
334
  location: loc,
339
- manifestPath: "/test/enact.yaml",
335
+ manifestPath: "/test/skill.yaml",
340
336
  };
341
337
  expect(resolution.location).toBe(loc);
342
338
  }
@@ -346,10 +342,12 @@ describe("manifest types", () => {
346
342
  describe("constants", () => {
347
343
  test("MANIFEST_FILES contains expected files", () => {
348
344
  expect(MANIFEST_FILES).toContain("SKILL.md");
345
+ expect(MANIFEST_FILES).toContain("skill.yaml");
346
+ expect(MANIFEST_FILES).toContain("skill.yml");
349
347
  expect(MANIFEST_FILES).toContain("enact.md");
350
348
  expect(MANIFEST_FILES).toContain("enact.yaml");
351
349
  expect(MANIFEST_FILES).toContain("enact.yml");
352
- expect(MANIFEST_FILES.length).toBe(4);
350
+ expect(MANIFEST_FILES.length).toBe(6);
353
351
  });
354
352
 
355
353
  test("PACKAGE_MANIFEST_FILE is correct", () => {
@@ -9,6 +9,7 @@ import {
9
9
  getGlobalEnvPath,
10
10
  getProjectEnactDir,
11
11
  getProjectEnvPath,
12
+ getSkillsDir,
12
13
  getToolsDir,
13
14
  } from "../src/paths";
14
15
 
@@ -106,14 +107,20 @@ describe("path utilities", () => {
106
107
  });
107
108
  });
108
109
 
109
- describe("getCacheDir", () => {
110
- test("returns ~/.enact/cache/ path", () => {
111
- const result = getCacheDir();
112
- const expected = join(homedir(), ".enact", "cache");
110
+ describe("getSkillsDir", () => {
111
+ test("returns ~/.agent/skills/ path", () => {
112
+ const result = getSkillsDir();
113
+ const expected = join(homedir(), ".agent", "skills");
113
114
  expect(result).toBe(expected);
114
115
  });
115
116
  });
116
117
 
118
+ describe("getCacheDir (deprecated)", () => {
119
+ test("returns same path as getSkillsDir", () => {
120
+ expect(getCacheDir()).toBe(getSkillsDir());
121
+ });
122
+ });
123
+
117
124
  describe("getConfigPath", () => {
118
125
  test("returns ~/.enact/config.yaml path", () => {
119
126
  const result = getConfigPath();
@@ -6,14 +6,19 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6
6
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import {
9
+ addAlias,
9
10
  addToolToRegistry,
11
+ getAliasesForTool,
10
12
  getInstalledVersion,
11
13
  getToolCachePath,
12
14
  getToolsJsonPath,
13
15
  isToolInstalled,
14
16
  listInstalledTools,
15
17
  loadToolsRegistry,
18
+ removeAlias,
19
+ removeAliasesForTool,
16
20
  removeToolFromRegistry,
21
+ resolveAlias,
17
22
  saveToolsRegistry,
18
23
  } from "../src/registry";
19
24
 
@@ -194,18 +199,19 @@ describe("registry", () => {
194
199
  });
195
200
 
196
201
  describe("getToolCachePath", () => {
197
- test("returns cache path with version", () => {
202
+ test("returns skill path under ~/.agent/skills/", () => {
198
203
  const path = getToolCachePath("org/tool", "1.0.0");
199
- expect(path).toContain(".enact");
200
- expect(path).toContain("cache");
204
+ expect(path).toContain(".agent");
205
+ expect(path).toContain("skills");
201
206
  expect(path).toContain("org/tool");
202
- expect(path).toContain("v1.0.0");
207
+ // No version subdirectory in new layout
208
+ expect(path).not.toContain("v1.0.0");
203
209
  });
204
210
 
205
- test("normalizes version prefix", () => {
206
- const pathWithV = getToolCachePath("org/tool", "v1.0.0");
207
- const pathWithoutV = getToolCachePath("org/tool", "1.0.0");
208
- expect(pathWithV).toBe(pathWithoutV);
211
+ test("returns same path regardless of version", () => {
212
+ const path1 = getToolCachePath("org/tool", "1.0.0");
213
+ const path2 = getToolCachePath("org/tool", "2.0.0");
214
+ expect(path1).toBe(path2);
209
215
  });
210
216
  });
211
217
 
@@ -228,4 +234,194 @@ describe("registry", () => {
228
234
  expect(tools.length).toBe(0);
229
235
  });
230
236
  });
237
+
238
+ describe("addAlias", () => {
239
+ test("adds alias to registry", () => {
240
+ addToolToRegistry("test/aliased-tool", "1.0.0", "project", PROJECT_DIR);
241
+ addAlias("mytool", "test/aliased-tool", "project", PROJECT_DIR);
242
+
243
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
244
+ expect(registry.aliases?.mytool).toBe("test/aliased-tool");
245
+
246
+ // Clean up
247
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
248
+ });
249
+
250
+ test("throws error when alias already exists for different tool", () => {
251
+ addToolToRegistry("test/tool1", "1.0.0", "project", PROJECT_DIR);
252
+ addToolToRegistry("test/tool2", "1.0.0", "project", PROJECT_DIR);
253
+ addAlias("shared", "test/tool1", "project", PROJECT_DIR);
254
+
255
+ expect(() => {
256
+ addAlias("shared", "test/tool2", "project", PROJECT_DIR);
257
+ }).toThrow('Alias "shared" already exists for tool "test/tool1"');
258
+
259
+ // Clean up
260
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
261
+ });
262
+
263
+ test("allows adding same alias for same tool (idempotent)", () => {
264
+ addToolToRegistry("test/same-tool", "1.0.0", "project", PROJECT_DIR);
265
+ addAlias("same", "test/same-tool", "project", PROJECT_DIR);
266
+
267
+ // Should not throw
268
+ expect(() => {
269
+ addAlias("same", "test/same-tool", "project", PROJECT_DIR);
270
+ }).not.toThrow();
271
+
272
+ // Clean up
273
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
274
+ });
275
+ });
276
+
277
+ describe("removeAlias", () => {
278
+ test("removes existing alias", () => {
279
+ addToolToRegistry("test/removable", "1.0.0", "project", PROJECT_DIR);
280
+ addAlias("removeme", "test/removable", "project", PROJECT_DIR);
281
+
282
+ const removed = removeAlias("removeme", "project", PROJECT_DIR);
283
+ expect(removed).toBe(true);
284
+
285
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
286
+ expect(registry.aliases?.removeme).toBeUndefined();
287
+
288
+ // Clean up
289
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
290
+ });
291
+
292
+ test("returns false for non-existent alias", () => {
293
+ const removed = removeAlias("nonexistent", "project", PROJECT_DIR);
294
+ expect(removed).toBe(false);
295
+ });
296
+ });
297
+
298
+ describe("resolveAlias", () => {
299
+ test("resolves existing alias to tool name", () => {
300
+ addToolToRegistry("org/full-name", "1.0.0", "project", PROJECT_DIR);
301
+ addAlias("short", "org/full-name", "project", PROJECT_DIR);
302
+
303
+ const resolved = resolveAlias("short", "project", PROJECT_DIR);
304
+ expect(resolved).toBe("org/full-name");
305
+
306
+ // Clean up
307
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
308
+ });
309
+
310
+ test("returns null for non-existent alias", () => {
311
+ const resolved = resolveAlias("unknown", "project", PROJECT_DIR);
312
+ expect(resolved).toBeNull();
313
+ });
314
+ });
315
+
316
+ describe("getAliasesForTool", () => {
317
+ test("returns all aliases for a tool", () => {
318
+ addToolToRegistry("test/multi-alias", "1.0.0", "project", PROJECT_DIR);
319
+ addAlias("alias1", "test/multi-alias", "project", PROJECT_DIR);
320
+ addAlias("alias2", "test/multi-alias", "project", PROJECT_DIR);
321
+
322
+ const aliases = getAliasesForTool("test/multi-alias", "project", PROJECT_DIR);
323
+ expect(aliases).toContain("alias1");
324
+ expect(aliases).toContain("alias2");
325
+ expect(aliases.length).toBe(2);
326
+
327
+ // Clean up
328
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
329
+ });
330
+
331
+ test("returns empty array for tool without aliases", () => {
332
+ addToolToRegistry("test/no-alias", "1.0.0", "project", PROJECT_DIR);
333
+
334
+ const aliases = getAliasesForTool("test/no-alias", "project", PROJECT_DIR);
335
+ expect(aliases).toEqual([]);
336
+
337
+ // Clean up
338
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
339
+ });
340
+ });
341
+
342
+ describe("removeAliasesForTool", () => {
343
+ test("removes all aliases for a tool", () => {
344
+ addToolToRegistry("test/cleanup", "1.0.0", "project", PROJECT_DIR);
345
+ addAlias("cleanup1", "test/cleanup", "project", PROJECT_DIR);
346
+ addAlias("cleanup2", "test/cleanup", "project", PROJECT_DIR);
347
+
348
+ const removed = removeAliasesForTool("test/cleanup", "project", PROJECT_DIR);
349
+ expect(removed).toBe(2);
350
+
351
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
352
+ expect(registry.aliases?.cleanup1).toBeUndefined();
353
+ expect(registry.aliases?.cleanup2).toBeUndefined();
354
+
355
+ // Clean up
356
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
357
+ });
358
+
359
+ test("returns 0 for tool without aliases", () => {
360
+ addToolToRegistry("test/no-aliases-to-remove", "1.0.0", "project", PROJECT_DIR);
361
+
362
+ const removed = removeAliasesForTool("test/no-aliases-to-remove", "project", PROJECT_DIR);
363
+ expect(removed).toBe(0);
364
+
365
+ // Clean up
366
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
367
+ });
368
+
369
+ test("does not remove aliases for other tools", () => {
370
+ addToolToRegistry("test/keep", "1.0.0", "project", PROJECT_DIR);
371
+ addToolToRegistry("test/remove", "1.0.0", "project", PROJECT_DIR);
372
+ addAlias("keepme", "test/keep", "project", PROJECT_DIR);
373
+ addAlias("removeme", "test/remove", "project", PROJECT_DIR);
374
+
375
+ removeAliasesForTool("test/remove", "project", PROJECT_DIR);
376
+
377
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
378
+ expect(registry.aliases?.keepme).toBe("test/keep");
379
+ expect(registry.aliases?.removeme).toBeUndefined();
380
+
381
+ // Clean up
382
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
383
+ });
384
+ });
385
+
386
+ describe("loadToolsRegistry with aliases", () => {
387
+ test("loads existing registry with aliases", () => {
388
+ const registryPath = join(PROJECT_ENACT_DIR, "tools.json");
389
+ writeFileSync(
390
+ registryPath,
391
+ JSON.stringify({
392
+ tools: {
393
+ "test/tool": "1.0.0",
394
+ },
395
+ aliases: {
396
+ t: "test/tool",
397
+ },
398
+ })
399
+ );
400
+
401
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
402
+ expect(registry.tools["test/tool"]).toBe("1.0.0");
403
+ expect(registry.aliases?.t).toBe("test/tool");
404
+
405
+ // Clean up
406
+ rmSync(registryPath);
407
+ });
408
+
409
+ test("returns empty aliases when not present in file", () => {
410
+ const registryPath = join(PROJECT_ENACT_DIR, "tools.json");
411
+ writeFileSync(
412
+ registryPath,
413
+ JSON.stringify({
414
+ tools: {
415
+ "test/tool": "1.0.0",
416
+ },
417
+ })
418
+ );
419
+
420
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
421
+ expect(registry.aliases).toEqual({});
422
+
423
+ // Clean up
424
+ rmSync(registryPath);
425
+ });
426
+ });
231
427
  });