@enactprotocol/shared 1.2.13 → 2.0.0

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 (134) hide show
  1. package/README.md +44 -0
  2. package/package.json +16 -58
  3. package/src/config.ts +476 -0
  4. package/src/constants.ts +36 -0
  5. package/src/execution/command.ts +314 -0
  6. package/src/execution/index.ts +73 -0
  7. package/src/execution/runtime.ts +308 -0
  8. package/src/execution/types.ts +379 -0
  9. package/src/execution/validation.ts +508 -0
  10. package/src/index.ts +237 -30
  11. package/src/manifest/index.ts +36 -0
  12. package/src/manifest/loader.ts +187 -0
  13. package/src/manifest/parser.ts +173 -0
  14. package/src/manifest/validator.ts +309 -0
  15. package/src/paths.ts +108 -0
  16. package/src/registry.ts +219 -0
  17. package/src/resolver.ts +345 -0
  18. package/src/types/index.ts +30 -0
  19. package/src/types/manifest.ts +255 -0
  20. package/src/types.ts +5 -188
  21. package/src/utils/fs.ts +281 -0
  22. package/src/utils/logger.ts +270 -59
  23. package/src/utils/version.ts +304 -36
  24. package/tests/config.test.ts +515 -0
  25. package/tests/execution/command.test.ts +317 -0
  26. package/tests/execution/validation.test.ts +384 -0
  27. package/tests/fixtures/invalid-tool.yaml +4 -0
  28. package/tests/fixtures/valid-tool.md +62 -0
  29. package/tests/fixtures/valid-tool.yaml +40 -0
  30. package/tests/index.test.ts +8 -0
  31. package/tests/manifest/loader.test.ts +291 -0
  32. package/tests/manifest/parser.test.ts +345 -0
  33. package/tests/manifest/validator.test.ts +394 -0
  34. package/tests/manifest-types.test.ts +358 -0
  35. package/tests/paths.test.ts +153 -0
  36. package/tests/registry.test.ts +231 -0
  37. package/tests/resolver.test.ts +272 -0
  38. package/tests/utils/fs.test.ts +388 -0
  39. package/tests/utils/logger.test.ts +480 -0
  40. package/tests/utils/version.test.ts +390 -0
  41. package/tsconfig.json +12 -0
  42. package/tsconfig.tsbuildinfo +1 -0
  43. package/dist/LocalToolResolver.d.ts +0 -84
  44. package/dist/LocalToolResolver.js +0 -353
  45. package/dist/api/enact-api.d.ts +0 -130
  46. package/dist/api/enact-api.js +0 -428
  47. package/dist/api/index.d.ts +0 -2
  48. package/dist/api/index.js +0 -2
  49. package/dist/api/types.d.ts +0 -103
  50. package/dist/api/types.js +0 -1
  51. package/dist/constants.d.ts +0 -7
  52. package/dist/constants.js +0 -10
  53. package/dist/core/DaggerExecutionProvider.d.ts +0 -169
  54. package/dist/core/DaggerExecutionProvider.js +0 -1029
  55. package/dist/core/DirectExecutionProvider.d.ts +0 -23
  56. package/dist/core/DirectExecutionProvider.js +0 -406
  57. package/dist/core/EnactCore.d.ts +0 -162
  58. package/dist/core/EnactCore.js +0 -597
  59. package/dist/core/NativeExecutionProvider.d.ts +0 -9
  60. package/dist/core/NativeExecutionProvider.js +0 -16
  61. package/dist/core/index.d.ts +0 -3
  62. package/dist/core/index.js +0 -3
  63. package/dist/exec/index.d.ts +0 -3
  64. package/dist/exec/index.js +0 -3
  65. package/dist/exec/logger.d.ts +0 -11
  66. package/dist/exec/logger.js +0 -57
  67. package/dist/exec/validate.d.ts +0 -5
  68. package/dist/exec/validate.js +0 -167
  69. package/dist/index.d.ts +0 -21
  70. package/dist/index.js +0 -25
  71. package/dist/lib/enact-direct.d.ts +0 -150
  72. package/dist/lib/enact-direct.js +0 -159
  73. package/dist/lib/index.d.ts +0 -1
  74. package/dist/lib/index.js +0 -1
  75. package/dist/security/index.d.ts +0 -3
  76. package/dist/security/index.js +0 -3
  77. package/dist/security/security.d.ts +0 -23
  78. package/dist/security/security.js +0 -137
  79. package/dist/security/sign.d.ts +0 -103
  80. package/dist/security/sign.js +0 -666
  81. package/dist/security/verification-enforcer.d.ts +0 -53
  82. package/dist/security/verification-enforcer.js +0 -204
  83. package/dist/services/McpCoreService.d.ts +0 -98
  84. package/dist/services/McpCoreService.js +0 -124
  85. package/dist/services/index.d.ts +0 -1
  86. package/dist/services/index.js +0 -1
  87. package/dist/types.d.ts +0 -132
  88. package/dist/types.js +0 -3
  89. package/dist/utils/config.d.ts +0 -111
  90. package/dist/utils/config.js +0 -342
  91. package/dist/utils/env-loader.d.ts +0 -54
  92. package/dist/utils/env-loader.js +0 -270
  93. package/dist/utils/help.d.ts +0 -36
  94. package/dist/utils/help.js +0 -248
  95. package/dist/utils/index.d.ts +0 -7
  96. package/dist/utils/index.js +0 -7
  97. package/dist/utils/logger.d.ts +0 -35
  98. package/dist/utils/logger.js +0 -75
  99. package/dist/utils/silent-monitor.d.ts +0 -67
  100. package/dist/utils/silent-monitor.js +0 -242
  101. package/dist/utils/timeout.d.ts +0 -5
  102. package/dist/utils/timeout.js +0 -23
  103. package/dist/utils/version.d.ts +0 -4
  104. package/dist/utils/version.js +0 -35
  105. package/dist/web/env-manager-server.d.ts +0 -29
  106. package/dist/web/env-manager-server.js +0 -367
  107. package/dist/web/index.d.ts +0 -1
  108. package/dist/web/index.js +0 -1
  109. package/src/LocalToolResolver.ts +0 -424
  110. package/src/api/enact-api.ts +0 -604
  111. package/src/api/index.ts +0 -2
  112. package/src/api/types.ts +0 -114
  113. package/src/core/DaggerExecutionProvider.ts +0 -1357
  114. package/src/core/DirectExecutionProvider.ts +0 -484
  115. package/src/core/EnactCore.ts +0 -847
  116. package/src/core/index.ts +0 -3
  117. package/src/exec/index.ts +0 -3
  118. package/src/exec/logger.ts +0 -63
  119. package/src/exec/validate.ts +0 -238
  120. package/src/lib/enact-direct.ts +0 -254
  121. package/src/lib/index.ts +0 -1
  122. package/src/services/McpCoreService.ts +0 -201
  123. package/src/services/index.ts +0 -1
  124. package/src/utils/config.ts +0 -438
  125. package/src/utils/env-loader.ts +0 -370
  126. package/src/utils/help.ts +0 -257
  127. package/src/utils/index.ts +0 -7
  128. package/src/utils/silent-monitor.ts +0 -328
  129. package/src/utils/timeout.ts +0 -26
  130. package/src/web/env-manager-server.ts +0 -465
  131. package/src/web/index.ts +0 -1
  132. package/src/web/static/app.js +0 -663
  133. package/src/web/static/index.html +0 -117
  134. package/src/web/static/style.css +0 -291
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Tests for the local tool registry (tools.json management)
3
+ */
4
+
5
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import {
9
+ addToolToRegistry,
10
+ getInstalledVersion,
11
+ getToolCachePath,
12
+ getToolsJsonPath,
13
+ isToolInstalled,
14
+ listInstalledTools,
15
+ loadToolsRegistry,
16
+ removeToolFromRegistry,
17
+ saveToolsRegistry,
18
+ } from "../src/registry";
19
+
20
+ const TEST_DIR = join(import.meta.dir, "temp-registry-test");
21
+ const PROJECT_DIR = join(TEST_DIR, "project");
22
+ const PROJECT_ENACT_DIR = join(PROJECT_DIR, ".enact");
23
+
24
+ describe("registry", () => {
25
+ beforeAll(() => {
26
+ // Create test directory structure
27
+ mkdirSync(PROJECT_ENACT_DIR, { recursive: true });
28
+ });
29
+
30
+ afterAll(() => {
31
+ // Clean up test directories
32
+ if (existsSync(TEST_DIR)) {
33
+ rmSync(TEST_DIR, { recursive: true, force: true });
34
+ }
35
+ });
36
+
37
+ describe("getToolsJsonPath", () => {
38
+ test("returns path for global scope", () => {
39
+ const path = getToolsJsonPath("global");
40
+ expect(path).not.toBeNull();
41
+ expect(path).toContain(".enact");
42
+ expect(path).toEndWith("tools.json");
43
+ });
44
+
45
+ test("returns path for project scope when .enact exists", () => {
46
+ const path = getToolsJsonPath("project", PROJECT_DIR);
47
+ expect(path).not.toBeNull();
48
+ expect(path).toContain(PROJECT_ENACT_DIR);
49
+ expect(path).toEndWith("tools.json");
50
+ });
51
+
52
+ test("returns null for project scope when no .enact", () => {
53
+ const path = getToolsJsonPath("project", "/tmp/nonexistent-test-path");
54
+ expect(path).toBeNull();
55
+ });
56
+ });
57
+
58
+ describe("loadToolsRegistry", () => {
59
+ test("returns empty registry when file does not exist", () => {
60
+ // Ensure tools.json doesn't exist from previous tests
61
+ const toolsJsonPath = join(PROJECT_ENACT_DIR, "tools.json");
62
+ if (existsSync(toolsJsonPath)) {
63
+ rmSync(toolsJsonPath);
64
+ }
65
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
66
+ expect(registry.tools).toEqual({});
67
+ });
68
+
69
+ test("loads existing registry", () => {
70
+ // Create a test registry file
71
+ const registryPath = join(PROJECT_ENACT_DIR, "tools.json");
72
+ writeFileSync(
73
+ registryPath,
74
+ JSON.stringify({
75
+ tools: {
76
+ "test/tool": "1.0.0",
77
+ "other/tool": "2.0.0",
78
+ },
79
+ })
80
+ );
81
+
82
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
83
+ expect(registry.tools["test/tool"]).toBe("1.0.0");
84
+ expect(registry.tools["other/tool"]).toBe("2.0.0");
85
+
86
+ // Clean up
87
+ rmSync(registryPath);
88
+ });
89
+
90
+ test("returns empty registry on parse error", () => {
91
+ const registryPath = join(PROJECT_ENACT_DIR, "tools.json");
92
+ writeFileSync(registryPath, "invalid json");
93
+
94
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
95
+ expect(registry.tools).toEqual({});
96
+
97
+ // Clean up
98
+ rmSync(registryPath);
99
+ });
100
+ });
101
+
102
+ describe("saveToolsRegistry", () => {
103
+ test("saves registry to file", () => {
104
+ const registry = {
105
+ tools: {
106
+ "test/save": "1.0.0",
107
+ },
108
+ };
109
+
110
+ saveToolsRegistry(registry, "project", PROJECT_DIR);
111
+
112
+ const registryPath = join(PROJECT_ENACT_DIR, "tools.json");
113
+ expect(existsSync(registryPath)).toBe(true);
114
+
115
+ const loaded = loadToolsRegistry("project", PROJECT_DIR);
116
+ expect(loaded.tools["test/save"]).toBe("1.0.0");
117
+
118
+ // Clean up
119
+ rmSync(registryPath);
120
+ });
121
+ });
122
+
123
+ describe("addToolToRegistry", () => {
124
+ test("adds tool to registry", () => {
125
+ addToolToRegistry("test/add", "1.0.0", "project", PROJECT_DIR);
126
+
127
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
128
+ expect(registry.tools["test/add"]).toBe("1.0.0");
129
+
130
+ // Clean up
131
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
132
+ });
133
+
134
+ test("updates existing tool version", () => {
135
+ addToolToRegistry("test/update", "1.0.0", "project", PROJECT_DIR);
136
+ addToolToRegistry("test/update", "2.0.0", "project", PROJECT_DIR);
137
+
138
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
139
+ expect(registry.tools["test/update"]).toBe("2.0.0");
140
+
141
+ // Clean up
142
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
143
+ });
144
+ });
145
+
146
+ describe("removeToolFromRegistry", () => {
147
+ test("removes tool from registry", () => {
148
+ addToolToRegistry("test/remove", "1.0.0", "project", PROJECT_DIR);
149
+ const removed = removeToolFromRegistry("test/remove", "project", PROJECT_DIR);
150
+
151
+ expect(removed).toBe(true);
152
+
153
+ const registry = loadToolsRegistry("project", PROJECT_DIR);
154
+ expect(registry.tools["test/remove"]).toBeUndefined();
155
+
156
+ // Clean up
157
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
158
+ });
159
+
160
+ test("returns false for non-existent tool", () => {
161
+ const removed = removeToolFromRegistry("nonexistent/tool", "project", PROJECT_DIR);
162
+ expect(removed).toBe(false);
163
+ });
164
+ });
165
+
166
+ describe("isToolInstalled", () => {
167
+ test("returns true for installed tool", () => {
168
+ addToolToRegistry("test/installed", "1.0.0", "project", PROJECT_DIR);
169
+ expect(isToolInstalled("test/installed", "project", PROJECT_DIR)).toBe(true);
170
+
171
+ // Clean up
172
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
173
+ });
174
+
175
+ test("returns false for non-installed tool", () => {
176
+ expect(isToolInstalled("not/installed", "project", PROJECT_DIR)).toBe(false);
177
+ });
178
+ });
179
+
180
+ describe("getInstalledVersion", () => {
181
+ test("returns version for installed tool", () => {
182
+ addToolToRegistry("test/version", "3.0.0", "project", PROJECT_DIR);
183
+ const version = getInstalledVersion("test/version", "project", PROJECT_DIR);
184
+ expect(version).toBe("3.0.0");
185
+
186
+ // Clean up
187
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
188
+ });
189
+
190
+ test("returns null for non-installed tool", () => {
191
+ const version = getInstalledVersion("not/installed", "project", PROJECT_DIR);
192
+ expect(version).toBeNull();
193
+ });
194
+ });
195
+
196
+ describe("getToolCachePath", () => {
197
+ test("returns cache path with version", () => {
198
+ const path = getToolCachePath("org/tool", "1.0.0");
199
+ expect(path).toContain(".enact");
200
+ expect(path).toContain("cache");
201
+ expect(path).toContain("org/tool");
202
+ expect(path).toContain("v1.0.0");
203
+ });
204
+
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);
209
+ });
210
+ });
211
+
212
+ describe("listInstalledTools", () => {
213
+ test("returns list of installed tools", () => {
214
+ addToolToRegistry("test/list1", "1.0.0", "project", PROJECT_DIR);
215
+ addToolToRegistry("test/list2", "2.0.0", "project", PROJECT_DIR);
216
+
217
+ const tools = listInstalledTools("project", PROJECT_DIR);
218
+ expect(tools.length).toBe(2);
219
+ expect(tools.find((t) => t.name === "test/list1")).toBeTruthy();
220
+ expect(tools.find((t) => t.name === "test/list2")).toBeTruthy();
221
+
222
+ // Clean up
223
+ rmSync(join(PROJECT_ENACT_DIR, "tools.json"));
224
+ });
225
+
226
+ test("returns empty list when no tools installed", () => {
227
+ const tools = listInstalledTools("project", PROJECT_DIR);
228
+ expect(tools.length).toBe(0);
229
+ });
230
+ });
231
+ });
@@ -0,0 +1,272 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ ToolResolveError,
6
+ getToolPath,
7
+ getToolSearchPaths,
8
+ normalizeToolName,
9
+ resolveTool,
10
+ resolveToolAuto,
11
+ resolveToolFromPath,
12
+ toolNameToPath,
13
+ tryResolveTool,
14
+ } from "../src/resolver";
15
+
16
+ const TEST_DIR = join(import.meta.dir, "temp-resolver-test");
17
+ const PROJECT_DIR = join(TEST_DIR, "project");
18
+ const PROJECT_ENACT_DIR = join(PROJECT_DIR, ".enact");
19
+
20
+ describe("tool resolver", () => {
21
+ beforeAll(() => {
22
+ // Create test directories
23
+ mkdirSync(join(PROJECT_ENACT_DIR, "tools", "test", "project-tool"), { recursive: true });
24
+
25
+ // Create a project-level tool
26
+ writeFileSync(
27
+ join(PROJECT_ENACT_DIR, "tools", "test", "project-tool", "enact.yaml"),
28
+ `
29
+ name: test/project-tool
30
+ description: A project-level test tool
31
+ version: "1.0.0"
32
+ `,
33
+ "utf-8"
34
+ );
35
+
36
+ // Create a direct tool directory for path-based resolution
37
+ mkdirSync(join(TEST_DIR, "direct-tool"), { recursive: true });
38
+ writeFileSync(
39
+ join(TEST_DIR, "direct-tool", "enact.yaml"),
40
+ `
41
+ name: test/direct-tool
42
+ description: A directly referenced tool
43
+ version: "2.0.0"
44
+ `,
45
+ "utf-8"
46
+ );
47
+
48
+ // Create a tool with enact.md
49
+ mkdirSync(join(TEST_DIR, "md-tool"), { recursive: true });
50
+ writeFileSync(
51
+ join(TEST_DIR, "md-tool", "enact.md"),
52
+ `---
53
+ name: test/md-tool
54
+ description: A markdown tool
55
+ version: "3.0.0"
56
+ ---
57
+
58
+ # MD Tool
59
+
60
+ Documentation here.
61
+ `,
62
+ "utf-8"
63
+ );
64
+ });
65
+
66
+ afterAll(() => {
67
+ // Cleanup
68
+ if (existsSync(TEST_DIR)) {
69
+ rmSync(TEST_DIR, { recursive: true, force: true });
70
+ }
71
+ });
72
+
73
+ describe("utility functions", () => {
74
+ describe("normalizeToolName", () => {
75
+ test("lowercases name", () => {
76
+ expect(normalizeToolName("Acme/Utils/Greeter")).toBe("acme/utils/greeter");
77
+ });
78
+
79
+ test("converts backslashes to forward slashes", () => {
80
+ expect(normalizeToolName("acme\\utils\\greeter")).toBe("acme/utils/greeter");
81
+ });
82
+
83
+ test("trims whitespace", () => {
84
+ expect(normalizeToolName(" acme/tool ")).toBe("acme/tool");
85
+ });
86
+ });
87
+
88
+ describe("toolNameToPath", () => {
89
+ test("returns path-like string", () => {
90
+ expect(toolNameToPath("acme/utils/greeter")).toBe("acme/utils/greeter");
91
+ });
92
+
93
+ test("normalizes backslashes", () => {
94
+ expect(toolNameToPath("acme\\utils")).toBe("acme/utils");
95
+ });
96
+ });
97
+
98
+ describe("getToolPath", () => {
99
+ test("joins tools dir and tool name", () => {
100
+ const result = getToolPath("/home/user/.enact/tools", "acme/greeter");
101
+ expect(result).toContain("acme");
102
+ expect(result).toContain("greeter");
103
+ });
104
+ });
105
+
106
+ describe("getToolSearchPaths", () => {
107
+ test("returns array of paths", () => {
108
+ const paths = getToolSearchPaths("test/tool", { startDir: PROJECT_DIR });
109
+ expect(Array.isArray(paths)).toBe(true);
110
+ expect(paths.length).toBeGreaterThan(0);
111
+ });
112
+
113
+ test("respects skipProject option", () => {
114
+ const withProject = getToolSearchPaths("test/tool", { startDir: PROJECT_DIR });
115
+ const withoutProject = getToolSearchPaths("test/tool", {
116
+ startDir: PROJECT_DIR,
117
+ skipProject: true,
118
+ });
119
+ expect(withoutProject.length).toBeLessThan(withProject.length);
120
+ });
121
+
122
+ test("respects skipUser option", () => {
123
+ // skipUser only affects global tools that are registered in tools.json
124
+ // Since no tools are installed globally, both should return the same paths
125
+ // This tests that skipUser doesn't add extra paths when no global tools exist
126
+ const withUser = getToolSearchPaths("test/tool", { skipCache: true });
127
+ const withoutUser = getToolSearchPaths("test/tool", { skipUser: true, skipCache: true });
128
+ expect(withoutUser.length).toBeLessThanOrEqual(withUser.length);
129
+ });
130
+ });
131
+ });
132
+
133
+ describe("resolveToolFromPath", () => {
134
+ test("resolves tool from directory", () => {
135
+ const toolDir = join(TEST_DIR, "direct-tool");
136
+ const result = resolveToolFromPath(toolDir);
137
+
138
+ expect(result.manifest.name).toBe("test/direct-tool");
139
+ expect(result.location).toBe("file");
140
+ expect(result.sourceDir).toBe(toolDir);
141
+ });
142
+
143
+ test("resolves tool from manifest file directly", () => {
144
+ const manifestPath = join(TEST_DIR, "direct-tool", "enact.yaml");
145
+ const result = resolveToolFromPath(manifestPath);
146
+
147
+ expect(result.manifest.name).toBe("test/direct-tool");
148
+ expect(result.manifestPath).toBe(manifestPath);
149
+ });
150
+
151
+ test("resolves markdown tool", () => {
152
+ const toolDir = join(TEST_DIR, "md-tool");
153
+ const result = resolveToolFromPath(toolDir);
154
+
155
+ expect(result.manifest.name).toBe("test/md-tool");
156
+ });
157
+
158
+ test("throws ToolResolveError for non-existent path", () => {
159
+ expect(() => resolveToolFromPath("/non/existent/path")).toThrow(ToolResolveError);
160
+ });
161
+
162
+ test("throws ToolResolveError for directory without manifest", () => {
163
+ const emptyDir = join(TEST_DIR, "empty-dir");
164
+ mkdirSync(emptyDir, { recursive: true });
165
+
166
+ expect(() => resolveToolFromPath(emptyDir)).toThrow(ToolResolveError);
167
+ });
168
+ });
169
+
170
+ describe("resolveTool", () => {
171
+ test("resolves tool from project", () => {
172
+ const result = resolveTool("test/project-tool", { startDir: PROJECT_DIR });
173
+
174
+ expect(result.manifest.name).toBe("test/project-tool");
175
+ expect(result.location).toBe("project");
176
+ });
177
+
178
+ test("throws ToolResolveError for non-existent tool", () => {
179
+ expect(() =>
180
+ resolveTool("non-existent/tool", { startDir: PROJECT_DIR, skipUser: true, skipCache: true })
181
+ ).toThrow(ToolResolveError);
182
+ });
183
+
184
+ test("error includes searched locations", () => {
185
+ try {
186
+ resolveTool("non-existent/tool", { startDir: PROJECT_DIR });
187
+ expect.unreachable("Should have thrown");
188
+ } catch (error) {
189
+ expect(error).toBeInstanceOf(ToolResolveError);
190
+ expect((error as ToolResolveError).searchedLocations).toBeDefined();
191
+ expect((error as ToolResolveError).searchedLocations?.length).toBeGreaterThan(0);
192
+ }
193
+ });
194
+
195
+ test("normalizes tool name", () => {
196
+ const result = resolveTool("TEST/PROJECT-TOOL", { startDir: PROJECT_DIR });
197
+ expect(result.manifest.name).toBe("test/project-tool");
198
+ });
199
+ });
200
+
201
+ describe("tryResolveTool", () => {
202
+ test("returns result on success", () => {
203
+ const result = tryResolveTool("test/project-tool", { startDir: PROJECT_DIR });
204
+ expect(result).not.toBeNull();
205
+ expect(result?.manifest.name).toBe("test/project-tool");
206
+ });
207
+
208
+ test("returns null on failure", () => {
209
+ const result = tryResolveTool("non-existent/tool", {
210
+ startDir: PROJECT_DIR,
211
+ skipUser: true,
212
+ skipCache: true,
213
+ });
214
+ expect(result).toBeNull();
215
+ });
216
+
217
+ test("handles path input", () => {
218
+ const toolDir = join(TEST_DIR, "direct-tool");
219
+ const result = tryResolveTool(toolDir);
220
+ expect(result).not.toBeNull();
221
+ expect(result?.manifest.name).toBe("test/direct-tool");
222
+ });
223
+
224
+ test("handles relative path with ./", () => {
225
+ // This would need the cwd to be set appropriately
226
+ // For now, just verify it doesn't crash
227
+ const result = tryResolveTool("./non-existent");
228
+ expect(result).toBeNull();
229
+ });
230
+ });
231
+
232
+ describe("resolveToolAuto", () => {
233
+ test("resolves path starting with /", () => {
234
+ const toolDir = join(TEST_DIR, "direct-tool");
235
+ const result = resolveToolAuto(toolDir);
236
+ expect(result.manifest.name).toBe("test/direct-tool");
237
+ });
238
+
239
+ test("resolves path starting with ./", () => {
240
+ // Use an absolute path that exists
241
+ const toolDir = join(TEST_DIR, "direct-tool");
242
+ const result = resolveToolAuto(toolDir);
243
+ expect(result.manifest.name).toBe("test/direct-tool");
244
+ });
245
+
246
+ test("resolves tool name", () => {
247
+ const result = resolveToolAuto("test/project-tool", { startDir: PROJECT_DIR });
248
+ expect(result.manifest.name).toBe("test/project-tool");
249
+ });
250
+
251
+ test("throws for non-existent", () => {
252
+ expect(() =>
253
+ resolveToolAuto("completely/non-existent", {
254
+ startDir: PROJECT_DIR,
255
+ skipUser: true,
256
+ skipCache: true,
257
+ })
258
+ ).toThrow(ToolResolveError);
259
+ });
260
+ });
261
+
262
+ describe("ToolResolveError", () => {
263
+ test("has correct properties", () => {
264
+ const error = new ToolResolveError("Test error", "test/tool", ["/path/1", "/path/2"]);
265
+
266
+ expect(error.name).toBe("ToolResolveError");
267
+ expect(error.message).toBe("Test error");
268
+ expect(error.toolPath).toBe("test/tool");
269
+ expect(error.searchedLocations).toEqual(["/path/1", "/path/2"]);
270
+ });
271
+ });
272
+ });