@enactprotocol/shared 1.2.13 → 2.0.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 (207) hide show
  1. package/README.md +44 -0
  2. package/dist/config.d.ts +164 -0
  3. package/dist/config.d.ts.map +1 -0
  4. package/dist/config.js +386 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/constants.d.ts +15 -5
  7. package/dist/constants.d.ts.map +1 -0
  8. package/dist/constants.js +24 -8
  9. package/dist/constants.js.map +1 -0
  10. package/dist/execution/command.d.ts +102 -0
  11. package/dist/execution/command.d.ts.map +1 -0
  12. package/dist/execution/command.js +262 -0
  13. package/dist/execution/command.js.map +1 -0
  14. package/dist/execution/index.d.ts +12 -0
  15. package/dist/execution/index.d.ts.map +1 -0
  16. package/dist/execution/index.js +17 -0
  17. package/dist/execution/index.js.map +1 -0
  18. package/dist/execution/runtime.d.ts +82 -0
  19. package/dist/execution/runtime.d.ts.map +1 -0
  20. package/dist/execution/runtime.js +273 -0
  21. package/dist/execution/runtime.js.map +1 -0
  22. package/dist/execution/types.d.ts +306 -0
  23. package/dist/execution/types.d.ts.map +1 -0
  24. package/dist/execution/types.js +14 -0
  25. package/dist/execution/types.js.map +1 -0
  26. package/dist/execution/validation.d.ts +43 -0
  27. package/dist/execution/validation.d.ts.map +1 -0
  28. package/dist/execution/validation.js +430 -0
  29. package/dist/execution/validation.js.map +1 -0
  30. package/dist/index.d.ts +21 -21
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +49 -25
  33. package/dist/index.js.map +1 -0
  34. package/dist/manifest/index.d.ts +7 -0
  35. package/dist/manifest/index.d.ts.map +1 -0
  36. package/dist/manifest/index.js +10 -0
  37. package/dist/manifest/index.js.map +1 -0
  38. package/dist/manifest/loader.d.ts +76 -0
  39. package/dist/manifest/loader.d.ts.map +1 -0
  40. package/dist/manifest/loader.js +146 -0
  41. package/dist/manifest/loader.js.map +1 -0
  42. package/dist/manifest/parser.d.ts +64 -0
  43. package/dist/manifest/parser.d.ts.map +1 -0
  44. package/dist/manifest/parser.js +135 -0
  45. package/dist/manifest/parser.js.map +1 -0
  46. package/dist/manifest/validator.d.ts +95 -0
  47. package/dist/manifest/validator.d.ts.map +1 -0
  48. package/dist/manifest/validator.js +258 -0
  49. package/dist/manifest/validator.js.map +1 -0
  50. package/dist/paths.d.ts +57 -0
  51. package/dist/paths.d.ts.map +1 -0
  52. package/dist/paths.js +93 -0
  53. package/dist/paths.js.map +1 -0
  54. package/dist/registry.d.ts +73 -0
  55. package/dist/registry.d.ts.map +1 -0
  56. package/dist/registry.js +147 -0
  57. package/dist/registry.js.map +1 -0
  58. package/dist/resolver.d.ts +89 -0
  59. package/dist/resolver.d.ts.map +1 -0
  60. package/dist/resolver.js +282 -0
  61. package/dist/resolver.js.map +1 -0
  62. package/dist/types/index.d.ts +6 -0
  63. package/dist/types/index.d.ts.map +1 -0
  64. package/dist/types/index.js +5 -0
  65. package/dist/types/index.js.map +1 -0
  66. package/dist/types/manifest.d.ts +201 -0
  67. package/dist/types/manifest.d.ts.map +1 -0
  68. package/dist/types/manifest.js +13 -0
  69. package/dist/types/manifest.js.map +1 -0
  70. package/dist/types.d.ts +5 -132
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +5 -3
  73. package/dist/types.js.map +1 -0
  74. package/dist/utils/fs.d.ts +105 -0
  75. package/dist/utils/fs.d.ts.map +1 -0
  76. package/dist/utils/fs.js +233 -0
  77. package/dist/utils/fs.js.map +1 -0
  78. package/dist/utils/logger.d.ts +102 -25
  79. package/dist/utils/logger.d.ts.map +1 -0
  80. package/dist/utils/logger.js +214 -57
  81. package/dist/utils/logger.js.map +1 -0
  82. package/dist/utils/version.d.ts +60 -2
  83. package/dist/utils/version.d.ts.map +1 -0
  84. package/dist/utils/version.js +255 -31
  85. package/dist/utils/version.js.map +1 -0
  86. package/package.json +16 -58
  87. package/src/config.ts +510 -0
  88. package/src/constants.ts +36 -0
  89. package/src/execution/command.ts +314 -0
  90. package/src/execution/index.ts +73 -0
  91. package/src/execution/runtime.ts +308 -0
  92. package/src/execution/types.ts +379 -0
  93. package/src/execution/validation.ts +508 -0
  94. package/src/index.ts +238 -30
  95. package/src/manifest/index.ts +36 -0
  96. package/src/manifest/loader.ts +187 -0
  97. package/src/manifest/parser.ts +173 -0
  98. package/src/manifest/validator.ts +309 -0
  99. package/src/paths.ts +108 -0
  100. package/src/registry.ts +219 -0
  101. package/src/resolver.ts +345 -0
  102. package/src/types/index.ts +30 -0
  103. package/src/types/manifest.ts +255 -0
  104. package/src/types.ts +5 -188
  105. package/src/utils/fs.ts +281 -0
  106. package/src/utils/logger.ts +270 -59
  107. package/src/utils/version.ts +304 -36
  108. package/tests/config.test.ts +515 -0
  109. package/tests/execution/command.test.ts +317 -0
  110. package/tests/execution/validation.test.ts +384 -0
  111. package/tests/fixtures/invalid-tool.yaml +4 -0
  112. package/tests/fixtures/valid-tool.md +62 -0
  113. package/tests/fixtures/valid-tool.yaml +40 -0
  114. package/tests/index.test.ts +8 -0
  115. package/tests/manifest/loader.test.ts +291 -0
  116. package/tests/manifest/parser.test.ts +345 -0
  117. package/tests/manifest/validator.test.ts +394 -0
  118. package/tests/manifest-types.test.ts +358 -0
  119. package/tests/paths.test.ts +153 -0
  120. package/tests/registry.test.ts +231 -0
  121. package/tests/resolver.test.ts +272 -0
  122. package/tests/utils/fs.test.ts +388 -0
  123. package/tests/utils/logger.test.ts +480 -0
  124. package/tests/utils/version.test.ts +390 -0
  125. package/tsconfig.json +12 -0
  126. package/dist/LocalToolResolver.d.ts +0 -84
  127. package/dist/LocalToolResolver.js +0 -353
  128. package/dist/api/enact-api.d.ts +0 -130
  129. package/dist/api/enact-api.js +0 -428
  130. package/dist/api/index.d.ts +0 -2
  131. package/dist/api/index.js +0 -2
  132. package/dist/api/types.d.ts +0 -103
  133. package/dist/api/types.js +0 -1
  134. package/dist/core/DaggerExecutionProvider.d.ts +0 -169
  135. package/dist/core/DaggerExecutionProvider.js +0 -1029
  136. package/dist/core/DirectExecutionProvider.d.ts +0 -23
  137. package/dist/core/DirectExecutionProvider.js +0 -406
  138. package/dist/core/EnactCore.d.ts +0 -162
  139. package/dist/core/EnactCore.js +0 -597
  140. package/dist/core/NativeExecutionProvider.d.ts +0 -9
  141. package/dist/core/NativeExecutionProvider.js +0 -16
  142. package/dist/core/index.d.ts +0 -3
  143. package/dist/core/index.js +0 -3
  144. package/dist/exec/index.d.ts +0 -3
  145. package/dist/exec/index.js +0 -3
  146. package/dist/exec/logger.d.ts +0 -11
  147. package/dist/exec/logger.js +0 -57
  148. package/dist/exec/validate.d.ts +0 -5
  149. package/dist/exec/validate.js +0 -167
  150. package/dist/lib/enact-direct.d.ts +0 -150
  151. package/dist/lib/enact-direct.js +0 -159
  152. package/dist/lib/index.d.ts +0 -1
  153. package/dist/lib/index.js +0 -1
  154. package/dist/security/index.d.ts +0 -3
  155. package/dist/security/index.js +0 -3
  156. package/dist/security/security.d.ts +0 -23
  157. package/dist/security/security.js +0 -137
  158. package/dist/security/sign.d.ts +0 -103
  159. package/dist/security/sign.js +0 -666
  160. package/dist/security/verification-enforcer.d.ts +0 -53
  161. package/dist/security/verification-enforcer.js +0 -204
  162. package/dist/services/McpCoreService.d.ts +0 -98
  163. package/dist/services/McpCoreService.js +0 -124
  164. package/dist/services/index.d.ts +0 -1
  165. package/dist/services/index.js +0 -1
  166. package/dist/utils/config.d.ts +0 -111
  167. package/dist/utils/config.js +0 -342
  168. package/dist/utils/env-loader.d.ts +0 -54
  169. package/dist/utils/env-loader.js +0 -270
  170. package/dist/utils/help.d.ts +0 -36
  171. package/dist/utils/help.js +0 -248
  172. package/dist/utils/index.d.ts +0 -7
  173. package/dist/utils/index.js +0 -7
  174. package/dist/utils/silent-monitor.d.ts +0 -67
  175. package/dist/utils/silent-monitor.js +0 -242
  176. package/dist/utils/timeout.d.ts +0 -5
  177. package/dist/utils/timeout.js +0 -23
  178. package/dist/web/env-manager-server.d.ts +0 -29
  179. package/dist/web/env-manager-server.js +0 -367
  180. package/dist/web/index.d.ts +0 -1
  181. package/dist/web/index.js +0 -1
  182. package/src/LocalToolResolver.ts +0 -424
  183. package/src/api/enact-api.ts +0 -604
  184. package/src/api/index.ts +0 -2
  185. package/src/api/types.ts +0 -114
  186. package/src/core/DaggerExecutionProvider.ts +0 -1357
  187. package/src/core/DirectExecutionProvider.ts +0 -484
  188. package/src/core/EnactCore.ts +0 -847
  189. package/src/core/index.ts +0 -3
  190. package/src/exec/index.ts +0 -3
  191. package/src/exec/logger.ts +0 -63
  192. package/src/exec/validate.ts +0 -238
  193. package/src/lib/enact-direct.ts +0 -254
  194. package/src/lib/index.ts +0 -1
  195. package/src/services/McpCoreService.ts +0 -201
  196. package/src/services/index.ts +0 -1
  197. package/src/utils/config.ts +0 -438
  198. package/src/utils/env-loader.ts +0 -370
  199. package/src/utils/help.ts +0 -257
  200. package/src/utils/index.ts +0 -7
  201. package/src/utils/silent-monitor.ts +0 -328
  202. package/src/utils/timeout.ts +0 -26
  203. package/src/web/env-manager-server.ts +0 -465
  204. package/src/web/index.ts +0 -1
  205. package/src/web/static/app.js +0 -663
  206. package/src/web/static/index.html +0 -117
  207. package/src/web/static/style.css +0 -291
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Manifest validator using Zod
3
+ *
4
+ * Validates that parsed manifests conform to the Enact specification
5
+ */
6
+
7
+ import { z } from "zod/v4";
8
+ import type {
9
+ ToolManifest,
10
+ ValidationError,
11
+ ValidationResult,
12
+ ValidationWarning,
13
+ } from "../types/manifest";
14
+
15
+ // ==================== Zod Schemas ====================
16
+
17
+ /**
18
+ * Environment variable schema
19
+ */
20
+ const EnvVariableSchema = z.object({
21
+ description: z.string().min(1, "Description is required"),
22
+ secret: z.boolean().optional(),
23
+ default: z.string().optional(),
24
+ });
25
+
26
+ /**
27
+ * Author schema
28
+ */
29
+ const AuthorSchema = z.object({
30
+ name: z.string().min(1, "Author name is required"),
31
+ email: z.string().email().optional(),
32
+ url: z.string().url().optional(),
33
+ });
34
+
35
+ /**
36
+ * Tool annotations schema
37
+ */
38
+ const ToolAnnotationsSchema = z.object({
39
+ title: z.string().optional(),
40
+ readOnlyHint: z.boolean().optional(),
41
+ destructiveHint: z.boolean().optional(),
42
+ idempotentHint: z.boolean().optional(),
43
+ openWorldHint: z.boolean().optional(),
44
+ });
45
+
46
+ /**
47
+ * Resource requirements schema
48
+ */
49
+ const ResourceRequirementsSchema = z.object({
50
+ memory: z.string().optional(),
51
+ gpu: z.string().optional(),
52
+ disk: z.string().optional(),
53
+ });
54
+
55
+ /**
56
+ * Tool example schema
57
+ */
58
+ const ToolExampleSchema = z.object({
59
+ input: z.record(z.string(), z.unknown()).optional(),
60
+ output: z.unknown().optional(),
61
+ description: z.string().optional(),
62
+ });
63
+
64
+ /**
65
+ * JSON Schema validation (basic structure check)
66
+ * We don't fully validate JSON Schema here, just ensure it's an object
67
+ */
68
+ const JsonSchemaSchema = z
69
+ .object({
70
+ type: z.string().optional(),
71
+ properties: z.record(z.string(), z.unknown()).optional(),
72
+ required: z.array(z.string()).optional(),
73
+ items: z.unknown().optional(),
74
+ enum: z.array(z.unknown()).optional(),
75
+ description: z.string().optional(),
76
+ })
77
+ .passthrough(); // Allow additional JSON Schema fields
78
+
79
+ /**
80
+ * Semantic version regex
81
+ */
82
+ const SEMVER_REGEX =
83
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
84
+
85
+ /**
86
+ * Tool name regex - hierarchical path format
87
+ */
88
+ const TOOL_NAME_REGEX = /^[a-z0-9_-]+(?:\/[a-z0-9_-]+)+$/;
89
+
90
+ /**
91
+ * Go duration regex (used for timeout)
92
+ */
93
+ const GO_DURATION_REGEX = /^(\d+)(ns|us|µs|ms|s|m|h)$/;
94
+
95
+ /**
96
+ * Complete tool manifest schema
97
+ */
98
+ const ToolManifestSchema = z
99
+ .object({
100
+ // Required fields
101
+ name: z
102
+ .string()
103
+ .min(1, "Tool name is required")
104
+ .regex(
105
+ TOOL_NAME_REGEX,
106
+ "Tool name must be hierarchical path format (e.g., 'org/tool' or 'org/category/tool')"
107
+ ),
108
+
109
+ description: z
110
+ .string()
111
+ .min(1, "Description is required")
112
+ .max(500, "Description should be 500 characters or less"),
113
+
114
+ // Recommended fields
115
+ enact: z.string().optional(),
116
+ version: z
117
+ .string()
118
+ .regex(SEMVER_REGEX, "Version must be valid semver (e.g., '1.0.0')")
119
+ .optional(),
120
+ from: z.string().optional(),
121
+ command: z.string().optional(),
122
+ timeout: z
123
+ .string()
124
+ .regex(GO_DURATION_REGEX, "Timeout must be Go duration format (e.g., '30s', '5m', '1h')")
125
+ .optional(),
126
+ license: z.string().optional(),
127
+ tags: z.array(z.string()).optional(),
128
+
129
+ // Schema fields
130
+ inputSchema: JsonSchemaSchema.optional(),
131
+ outputSchema: JsonSchemaSchema.optional(),
132
+
133
+ // Environment variables
134
+ env: z.record(z.string(), EnvVariableSchema).optional(),
135
+
136
+ // Behavior & Resources
137
+ annotations: ToolAnnotationsSchema.optional(),
138
+ resources: ResourceRequirementsSchema.optional(),
139
+
140
+ // Documentation
141
+ doc: z.string().optional(),
142
+ authors: z.array(AuthorSchema).optional(),
143
+
144
+ // Testing
145
+ examples: z.array(ToolExampleSchema).optional(),
146
+ })
147
+ .passthrough(); // Allow x-* custom fields
148
+
149
+ // ==================== Validation Functions ====================
150
+
151
+ /**
152
+ * Convert Zod error to our ValidationError format
153
+ */
154
+ function zodErrorToValidationErrors(zodError: z.ZodError): ValidationError[] {
155
+ return zodError.issues.map((issue) => ({
156
+ path: issue.path.join("."),
157
+ message: issue.message,
158
+ code: issue.code,
159
+ }));
160
+ }
161
+
162
+ /**
163
+ * Generate warnings for recommended but missing fields
164
+ */
165
+ function generateWarnings(manifest: ToolManifest): ValidationWarning[] {
166
+ const warnings: ValidationWarning[] = [];
167
+
168
+ // Check for recommended fields
169
+ if (!manifest.enact) {
170
+ warnings.push({
171
+ path: "enact",
172
+ message: "Protocol version (enact) is recommended for compatibility",
173
+ code: "MISSING_RECOMMENDED",
174
+ });
175
+ }
176
+
177
+ if (!manifest.version) {
178
+ warnings.push({
179
+ path: "version",
180
+ message: "Tool version is recommended for versioning and updates",
181
+ code: "MISSING_RECOMMENDED",
182
+ });
183
+ }
184
+
185
+ if (!manifest.from && manifest.command) {
186
+ warnings.push({
187
+ path: "from",
188
+ message: "Container image (from) is recommended for reproducibility",
189
+ code: "MISSING_RECOMMENDED",
190
+ });
191
+ }
192
+
193
+ if (!manifest.license) {
194
+ warnings.push({
195
+ path: "license",
196
+ message: "License is recommended for published tools",
197
+ code: "MISSING_RECOMMENDED",
198
+ });
199
+ }
200
+
201
+ if (!manifest.inputSchema && manifest.command) {
202
+ warnings.push({
203
+ path: "inputSchema",
204
+ message: "Input schema is recommended for tools with parameters",
205
+ code: "MISSING_RECOMMENDED",
206
+ });
207
+ }
208
+
209
+ if (!manifest.outputSchema) {
210
+ warnings.push({
211
+ path: "outputSchema",
212
+ message: "Output schema is recommended for structured output validation",
213
+ code: "MISSING_RECOMMENDED",
214
+ });
215
+ }
216
+
217
+ // Check for potential issues
218
+ if (manifest.env) {
219
+ for (const [key, value] of Object.entries(manifest.env)) {
220
+ if (value.secret && value.default) {
221
+ warnings.push({
222
+ path: `env.${key}`,
223
+ message: "Secret variables should not have default values",
224
+ code: "SECRET_WITH_DEFAULT",
225
+ });
226
+ }
227
+ }
228
+ }
229
+
230
+ // Check for timeout on command tools
231
+ if (manifest.command && !manifest.timeout) {
232
+ warnings.push({
233
+ path: "timeout",
234
+ message: "Timeout is recommended for command-based tools",
235
+ code: "MISSING_RECOMMENDED",
236
+ });
237
+ }
238
+
239
+ return warnings;
240
+ }
241
+
242
+ /**
243
+ * Validate a tool manifest
244
+ *
245
+ * @param manifest - The manifest to validate (parsed but unvalidated)
246
+ * @returns ValidationResult with valid flag, errors, and warnings
247
+ */
248
+ export function validateManifest(manifest: unknown): ValidationResult {
249
+ const result = ToolManifestSchema.safeParse(manifest);
250
+
251
+ if (!result.success) {
252
+ return {
253
+ valid: false,
254
+ errors: zodErrorToValidationErrors(result.error),
255
+ warnings: [],
256
+ };
257
+ }
258
+
259
+ // Generate warnings for missing recommended fields
260
+ const warnings = generateWarnings(result.data as ToolManifest);
261
+
262
+ return {
263
+ valid: true,
264
+ warnings: warnings.length > 0 ? warnings : undefined,
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Validate and return the typed manifest
270
+ * Throws if validation fails
271
+ *
272
+ * @param manifest - The manifest to validate
273
+ * @returns The validated ToolManifest
274
+ * @throws Error if validation fails
275
+ */
276
+ export function validateManifestStrict(manifest: unknown): ToolManifest {
277
+ const result = validateManifest(manifest);
278
+
279
+ if (!result.valid) {
280
+ const errorMessages = result.errors?.map((e) => `${e.path}: ${e.message}`).join(", ");
281
+ throw new Error(`Manifest validation failed: ${errorMessages}`);
282
+ }
283
+
284
+ return manifest as ToolManifest;
285
+ }
286
+
287
+ /**
288
+ * Check if a string is a valid tool name
289
+ */
290
+ export function isValidToolName(name: string): boolean {
291
+ return TOOL_NAME_REGEX.test(name);
292
+ }
293
+
294
+ /**
295
+ * Check if a string is a valid semver version
296
+ */
297
+ export function isValidVersion(version: string): boolean {
298
+ return SEMVER_REGEX.test(version);
299
+ }
300
+
301
+ /**
302
+ * Check if a string is a valid Go duration
303
+ */
304
+ export function isValidTimeout(timeout: string): boolean {
305
+ return GO_DURATION_REGEX.test(timeout);
306
+ }
307
+
308
+ // Export the schema for external use
309
+ export { ToolManifestSchema };
package/src/paths.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Path resolution utilities for Enact directories and tool locations
3
+ */
4
+
5
+ import { existsSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join, resolve } from "node:path";
8
+
9
+ /**
10
+ * Scope for tool directories
11
+ */
12
+ export type ToolScope = "user" | "project";
13
+
14
+ /**
15
+ * Get the Enact home directory (~/.enact/)
16
+ * @returns Absolute path to ~/.enact/
17
+ */
18
+ export function getEnactHome(): string {
19
+ return join(homedir(), ".enact");
20
+ }
21
+
22
+ /**
23
+ * Get the project-level .enact directory
24
+ * Searches up from current working directory to find .enact/
25
+ * NOTE: Does NOT return ~/.enact/ - that's the global home, not a project dir
26
+ * @param startDir - Directory to start searching from (defaults to cwd)
27
+ * @returns Absolute path to .enact/ or null if not found
28
+ */
29
+ export function getProjectEnactDir(startDir?: string): string | null {
30
+ let currentDir = resolve(startDir ?? process.cwd());
31
+ const root = resolve("/");
32
+ const enactHome = getEnactHome();
33
+
34
+ // Walk up directory tree looking for .enact/
35
+ while (currentDir !== root) {
36
+ const enactDir = join(currentDir, ".enact");
37
+ // Skip ~/.enact/ - that's the global home, not a project directory
38
+ if (existsSync(enactDir) && enactDir !== enactHome) {
39
+ return enactDir;
40
+ }
41
+ const parentDir = resolve(currentDir, "..");
42
+ if (parentDir === currentDir) {
43
+ break; // Reached root
44
+ }
45
+ currentDir = parentDir;
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * Get the tools directory for specified scope
53
+ *
54
+ * NOTE: For global scope ("user"), this is DEPRECATED.
55
+ * Global tools are now tracked in ~/.enact/tools.json and stored in cache.
56
+ * Use getToolsJsonPath("global") and getToolCachePath() from ./registry instead.
57
+ *
58
+ * For project scope, this returns .enact/tools/ where project tools are copied.
59
+ *
60
+ * @param scope - 'user' for ~/.enact/tools/ (deprecated) or 'project' for .enact/tools/
61
+ * @param startDir - For project scope, directory to start searching from
62
+ * @returns Absolute path to tools directory or null if project scope and not found
63
+ * @deprecated Use registry.ts functions for global tools
64
+ */
65
+ export function getToolsDir(scope: ToolScope, startDir?: string): string | null {
66
+ if (scope === "user") {
67
+ // DEPRECATED: Global tools now use tools.json + cache
68
+ // This path is kept for backward compatibility during migration
69
+ return join(getEnactHome(), "tools");
70
+ }
71
+
72
+ const projectDir = getProjectEnactDir(startDir);
73
+ return projectDir ? join(projectDir, "tools") : null;
74
+ }
75
+
76
+ /**
77
+ * Get the cache directory (~/.enact/cache/)
78
+ * @returns Absolute path to ~/.enact/cache/
79
+ */
80
+ export function getCacheDir(): string {
81
+ return join(getEnactHome(), "cache");
82
+ }
83
+
84
+ /**
85
+ * Get the configuration file path (~/.enact/config.yaml)
86
+ * @returns Absolute path to ~/.enact/config.yaml
87
+ */
88
+ export function getConfigPath(): string {
89
+ return join(getEnactHome(), "config.yaml");
90
+ }
91
+
92
+ /**
93
+ * Get the global .env file path (~/.enact/.env)
94
+ * @returns Absolute path to ~/.enact/.env
95
+ */
96
+ export function getGlobalEnvPath(): string {
97
+ return join(getEnactHome(), ".env");
98
+ }
99
+
100
+ /**
101
+ * Get the project .env file path (.enact/.env)
102
+ * @param startDir - Directory to start searching from
103
+ * @returns Absolute path to .enact/.env or null if not found
104
+ */
105
+ export function getProjectEnvPath(startDir?: string): string | null {
106
+ const projectDir = getProjectEnactDir(startDir);
107
+ return projectDir ? join(projectDir, ".env") : null;
108
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Local tool registry management
3
+ *
4
+ * Manages tools.json files for tracking installed tools:
5
+ * - Global: ~/.enact/tools.json (installed with -g)
6
+ * - Project: .enact/tools.json (project dependencies)
7
+ *
8
+ * Tools are stored in cache and referenced by version in tools.json.
9
+ * This eliminates the need for a separate ~/.enact/tools/ directory.
10
+ */
11
+
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ import { getCacheDir, getEnactHome, getProjectEnactDir } from "./paths";
15
+
16
+ /**
17
+ * Structure of tools.json file
18
+ */
19
+ export interface ToolsRegistry {
20
+ /** Map of tool name to installed version */
21
+ tools: Record<string, string>;
22
+ }
23
+
24
+ /**
25
+ * Scope for tool registry
26
+ */
27
+ export type RegistryScope = "global" | "project";
28
+
29
+ /**
30
+ * Information about an installed tool
31
+ */
32
+ export interface InstalledToolInfo {
33
+ name: string;
34
+ version: string;
35
+ scope: RegistryScope;
36
+ cachePath: string;
37
+ }
38
+
39
+ /**
40
+ * Get the path to tools.json for the specified scope
41
+ */
42
+ export function getToolsJsonPath(scope: RegistryScope, startDir?: string): string | null {
43
+ if (scope === "global") {
44
+ return join(getEnactHome(), "tools.json");
45
+ }
46
+
47
+ const projectDir = getProjectEnactDir(startDir);
48
+ return projectDir ? join(projectDir, "tools.json") : null;
49
+ }
50
+
51
+ /**
52
+ * Load tools.json from the specified scope
53
+ * Returns empty registry if file doesn't exist
54
+ */
55
+ export function loadToolsRegistry(scope: RegistryScope, startDir?: string): ToolsRegistry {
56
+ const registryPath = getToolsJsonPath(scope, startDir);
57
+
58
+ if (!registryPath || !existsSync(registryPath)) {
59
+ return { tools: {} };
60
+ }
61
+
62
+ try {
63
+ const content = readFileSync(registryPath, "utf-8");
64
+ const parsed = JSON.parse(content);
65
+ return {
66
+ tools: parsed.tools ?? {},
67
+ };
68
+ } catch {
69
+ // Return empty registry on parse error
70
+ return { tools: {} };
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Save tools.json to the specified scope
76
+ */
77
+ export function saveToolsRegistry(
78
+ registry: ToolsRegistry,
79
+ scope: RegistryScope,
80
+ startDir?: string
81
+ ): void {
82
+ let registryPath = getToolsJsonPath(scope, startDir);
83
+
84
+ // For project scope, create .enact/ directory if it doesn't exist
85
+ if (!registryPath && scope === "project") {
86
+ const projectRoot = startDir ?? process.cwd();
87
+ const enactDir = join(projectRoot, ".enact");
88
+ mkdirSync(enactDir, { recursive: true });
89
+ registryPath = join(enactDir, "tools.json");
90
+ }
91
+
92
+ if (!registryPath) {
93
+ throw new Error("Cannot save project registry: unable to determine registry path");
94
+ }
95
+
96
+ // Ensure directory exists
97
+ const dir = dirname(registryPath);
98
+ if (!existsSync(dir)) {
99
+ mkdirSync(dir, { recursive: true });
100
+ }
101
+
102
+ const content = JSON.stringify(registry, null, 2);
103
+ writeFileSync(registryPath, content, "utf-8");
104
+ }
105
+
106
+ /**
107
+ * Add a tool to the registry
108
+ */
109
+ export function addToolToRegistry(
110
+ toolName: string,
111
+ version: string,
112
+ scope: RegistryScope,
113
+ startDir?: string
114
+ ): void {
115
+ const registry = loadToolsRegistry(scope, startDir);
116
+ registry.tools[toolName] = version;
117
+ saveToolsRegistry(registry, scope, startDir);
118
+ }
119
+
120
+ /**
121
+ * Remove a tool from the registry
122
+ */
123
+ export function removeToolFromRegistry(
124
+ toolName: string,
125
+ scope: RegistryScope,
126
+ startDir?: string
127
+ ): boolean {
128
+ const registry = loadToolsRegistry(scope, startDir);
129
+
130
+ if (!(toolName in registry.tools)) {
131
+ return false;
132
+ }
133
+
134
+ delete registry.tools[toolName];
135
+ saveToolsRegistry(registry, scope, startDir);
136
+ return true;
137
+ }
138
+
139
+ /**
140
+ * Check if a tool is installed in the registry
141
+ */
142
+ export function isToolInstalled(
143
+ toolName: string,
144
+ scope: RegistryScope,
145
+ startDir?: string
146
+ ): boolean {
147
+ const registry = loadToolsRegistry(scope, startDir);
148
+ return toolName in registry.tools;
149
+ }
150
+
151
+ /**
152
+ * Get the installed version of a tool
153
+ * Returns null if not installed
154
+ */
155
+ export function getInstalledVersion(
156
+ toolName: string,
157
+ scope: RegistryScope,
158
+ startDir?: string
159
+ ): string | null {
160
+ const registry = loadToolsRegistry(scope, startDir);
161
+ return registry.tools[toolName] ?? null;
162
+ }
163
+
164
+ /**
165
+ * Get the cache path for an installed tool
166
+ */
167
+ export function getToolCachePath(toolName: string, version: string): string {
168
+ const cacheDir = getCacheDir();
169
+ const normalizedVersion = version.startsWith("v") ? version.slice(1) : version;
170
+ return join(cacheDir, toolName, `v${normalizedVersion}`);
171
+ }
172
+
173
+ /**
174
+ * List all installed tools in a registry
175
+ */
176
+ export function listInstalledTools(scope: RegistryScope, startDir?: string): InstalledToolInfo[] {
177
+ const registry = loadToolsRegistry(scope, startDir);
178
+ const tools: InstalledToolInfo[] = [];
179
+
180
+ for (const [name, version] of Object.entries(registry.tools)) {
181
+ tools.push({
182
+ name,
183
+ version,
184
+ scope,
185
+ cachePath: getToolCachePath(name, version),
186
+ });
187
+ }
188
+
189
+ return tools;
190
+ }
191
+
192
+ /**
193
+ * Get tool info if installed (checks cache path exists)
194
+ */
195
+ export function getInstalledToolInfo(
196
+ toolName: string,
197
+ scope: RegistryScope,
198
+ startDir?: string
199
+ ): InstalledToolInfo | null {
200
+ const version = getInstalledVersion(toolName, scope, startDir);
201
+
202
+ if (!version) {
203
+ return null;
204
+ }
205
+
206
+ const cachePath = getToolCachePath(toolName, version);
207
+
208
+ // Verify cache exists
209
+ if (!existsSync(cachePath)) {
210
+ return null;
211
+ }
212
+
213
+ return {
214
+ name: toolName,
215
+ version,
216
+ scope,
217
+ cachePath,
218
+ };
219
+ }