@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,314 @@
1
+ /**
2
+ * Command interpolation and parsing
3
+ *
4
+ * Handles ${parameter} substitution in command templates with proper escaping.
5
+ */
6
+
7
+ import type { CommandToken, InterpolationOptions, ParsedCommand } from "./types";
8
+
9
+ /**
10
+ * Pattern to match ${parameter} in command strings
11
+ */
12
+ const PARAM_PATTERN = /\$\{([^}]+)\}/g;
13
+
14
+ /**
15
+ * Parse a command template into tokens
16
+ *
17
+ * @param command - Command template with ${parameter} placeholders
18
+ * @returns Parsed command with tokens and parameter list
19
+ */
20
+ export function parseCommand(command: string): ParsedCommand {
21
+ const tokens: CommandToken[] = [];
22
+ const parameters: string[] = [];
23
+
24
+ let lastIndex = 0;
25
+ let match: RegExpExecArray | null = null;
26
+
27
+ // Reset regex state
28
+ PARAM_PATTERN.lastIndex = 0;
29
+
30
+ match = PARAM_PATTERN.exec(command);
31
+ while (match !== null) {
32
+ // Add literal text before this match
33
+ if (match.index > lastIndex) {
34
+ tokens.push({
35
+ type: "literal",
36
+ value: command.slice(lastIndex, match.index),
37
+ });
38
+ }
39
+
40
+ // Add the parameter token
41
+ const paramName = match[1];
42
+ if (paramName) {
43
+ tokens.push({
44
+ type: "parameter",
45
+ name: paramName,
46
+ });
47
+
48
+ if (!parameters.includes(paramName)) {
49
+ parameters.push(paramName);
50
+ }
51
+ }
52
+
53
+ lastIndex = match.index + match[0].length;
54
+ match = PARAM_PATTERN.exec(command);
55
+ }
56
+
57
+ // Add any remaining literal text
58
+ if (lastIndex < command.length) {
59
+ tokens.push({
60
+ type: "literal",
61
+ value: command.slice(lastIndex),
62
+ });
63
+ }
64
+
65
+ return {
66
+ original: command,
67
+ tokens,
68
+ parameters,
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Shell-escape a value for safe inclusion in a command
74
+ *
75
+ * Uses single quotes and handles embedded single quotes.
76
+ * Example: "it's a test" becomes "'it'\"'\"'s a test'"
77
+ *
78
+ * @param value - Value to escape
79
+ * @returns Shell-safe escaped string
80
+ */
81
+ export function shellEscape(value: string): string {
82
+ // If the value is empty, return empty quoted string
83
+ if (value === "") {
84
+ return "''";
85
+ }
86
+
87
+ // If value contains no special characters, return as-is
88
+ if (/^[a-zA-Z0-9._\-/]+$/.test(value)) {
89
+ return value;
90
+ }
91
+
92
+ // Use single quotes, escaping any embedded single quotes
93
+ // The technique: end quote, add escaped quote, start new quote
94
+ // 'it'"'"'s' means: 'it' + "'" + 's'
95
+ return `'${value.replace(/'/g, "'\"'\"'")}'`;
96
+ }
97
+
98
+ /**
99
+ * Convert a value to string for command interpolation
100
+ *
101
+ * Handles different types:
102
+ * - string: as-is
103
+ * - number: toString()
104
+ * - boolean: "true" or "false"
105
+ * - object/array: JSON.stringify
106
+ * - null/undefined: empty string
107
+ *
108
+ * @param value - Value to convert
109
+ * @param jsonifyObjects - Whether to JSON-stringify objects
110
+ * @returns String representation
111
+ */
112
+ export function valueToString(value: unknown, jsonifyObjects = true): string {
113
+ if (value === null || value === undefined) {
114
+ return "";
115
+ }
116
+
117
+ if (typeof value === "string") {
118
+ return value;
119
+ }
120
+
121
+ if (typeof value === "number" || typeof value === "boolean") {
122
+ return String(value);
123
+ }
124
+
125
+ if (jsonifyObjects && (typeof value === "object" || Array.isArray(value))) {
126
+ return JSON.stringify(value);
127
+ }
128
+
129
+ return String(value);
130
+ }
131
+
132
+ /**
133
+ * Interpolate a command template with parameter values
134
+ *
135
+ * @param command - Command template or parsed command
136
+ * @param params - Parameter values
137
+ * @param options - Interpolation options
138
+ * @returns Interpolated command string
139
+ * @throws Error if required parameter is missing and onMissing is "error"
140
+ */
141
+ export function interpolateCommand(
142
+ command: string | ParsedCommand,
143
+ params: Record<string, unknown>,
144
+ options: InterpolationOptions = {}
145
+ ): string {
146
+ const { escape: shouldEscape = true, jsonifyObjects = true, onMissing = "error" } = options;
147
+
148
+ const parsed = typeof command === "string" ? parseCommand(command) : command;
149
+
150
+ const parts: string[] = [];
151
+
152
+ for (const token of parsed.tokens) {
153
+ if (token.type === "literal") {
154
+ parts.push(token.value);
155
+ } else {
156
+ const paramName = token.name;
157
+ const value = params[paramName];
158
+
159
+ if (value === undefined) {
160
+ switch (onMissing) {
161
+ case "error":
162
+ throw new Error(`Missing required parameter: ${paramName}`);
163
+ case "empty":
164
+ parts.push("");
165
+ break;
166
+ case "keep":
167
+ parts.push(`\${${paramName}}`);
168
+ break;
169
+ }
170
+ } else {
171
+ const stringValue = valueToString(value, jsonifyObjects);
172
+ parts.push(shouldEscape ? shellEscape(stringValue) : stringValue);
173
+ }
174
+ }
175
+ }
176
+
177
+ return parts.join("");
178
+ }
179
+
180
+ /**
181
+ * Parse a command string respecting quotes
182
+ *
183
+ * Splits a command into arguments, respecting single and double quotes.
184
+ *
185
+ * @param command - Command string to parse
186
+ * @returns Array of command arguments
187
+ */
188
+ export function parseCommandArgs(command: string): string[] {
189
+ const args: string[] = [];
190
+ let current = "";
191
+ let inSingleQuote = false;
192
+ let inDoubleQuote = false;
193
+ let escapeNext = false;
194
+
195
+ for (let i = 0; i < command.length; i++) {
196
+ const char = command[i] as string;
197
+
198
+ if (escapeNext) {
199
+ current += char;
200
+ escapeNext = false;
201
+ continue;
202
+ }
203
+
204
+ if (char === "\\") {
205
+ escapeNext = true;
206
+ continue;
207
+ }
208
+
209
+ if (char === "'" && !inDoubleQuote) {
210
+ inSingleQuote = !inSingleQuote;
211
+ continue;
212
+ }
213
+
214
+ if (char === '"' && !inSingleQuote) {
215
+ inDoubleQuote = !inDoubleQuote;
216
+ continue;
217
+ }
218
+
219
+ if (char === " " && !inSingleQuote && !inDoubleQuote) {
220
+ if (current.length > 0) {
221
+ args.push(current);
222
+ current = "";
223
+ }
224
+ continue;
225
+ }
226
+
227
+ current += char;
228
+ }
229
+
230
+ // Add the last argument
231
+ if (current.length > 0) {
232
+ args.push(current);
233
+ }
234
+
235
+ return args;
236
+ }
237
+
238
+ /**
239
+ * Wrap a command with sh -c for execution
240
+ *
241
+ * Useful when the command contains shell features like pipes, redirects, etc.
242
+ *
243
+ * @param command - Command to wrap
244
+ * @returns Arguments for sh -c execution
245
+ */
246
+ export function wrapWithShell(command: string): string[] {
247
+ return ["sh", "-c", command];
248
+ }
249
+
250
+ /**
251
+ * Check if a command needs shell wrapping
252
+ *
253
+ * Returns true if the command contains shell special characters.
254
+ *
255
+ * @param command - Command to check
256
+ * @returns Whether the command needs sh -c wrapping
257
+ */
258
+ export function needsShellWrap(command: string): boolean {
259
+ // Check for shell operators and features
260
+ return /[|&;<>()$`\\"\n*?[\]#~=%]/.test(command);
261
+ }
262
+
263
+ /**
264
+ * Prepare a command for execution
265
+ *
266
+ * Parses the command and determines if it needs shell wrapping.
267
+ *
268
+ * @param command - Command template
269
+ * @param params - Parameter values for interpolation
270
+ * @param options - Interpolation options
271
+ * @returns Command ready for execution [program, ...args]
272
+ */
273
+ export function prepareCommand(
274
+ command: string,
275
+ params: Record<string, unknown>,
276
+ options: InterpolationOptions = {}
277
+ ): string[] {
278
+ // Interpolate parameters
279
+ const interpolated = interpolateCommand(command, params, options);
280
+
281
+ // Check if we need shell wrapping
282
+ if (needsShellWrap(interpolated)) {
283
+ return wrapWithShell(interpolated);
284
+ }
285
+
286
+ // Parse into arguments
287
+ return parseCommandArgs(interpolated);
288
+ }
289
+
290
+ /**
291
+ * Validate that all required parameters are provided
292
+ *
293
+ * @param command - Parsed command
294
+ * @param params - Provided parameters
295
+ * @returns Array of missing parameter names
296
+ */
297
+ export function getMissingParams(
298
+ command: string | ParsedCommand,
299
+ params: Record<string, unknown>
300
+ ): string[] {
301
+ const parsed = typeof command === "string" ? parseCommand(command) : command;
302
+
303
+ return parsed.parameters.filter((param) => params[param] === undefined);
304
+ }
305
+
306
+ /**
307
+ * Get all parameters in a command template
308
+ *
309
+ * @param command - Command template
310
+ * @returns Array of parameter names
311
+ */
312
+ export function getCommandParams(command: string): string[] {
313
+ return parseCommand(command).parameters;
314
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Execution Engine Module
3
+ *
4
+ * Provides containerized tool execution using the Dagger SDK.
5
+ * This is the main entry point for Phase 3 of Enact CLI 2.0.
6
+ */
7
+
8
+ // Types
9
+ export type {
10
+ // Input/Output types
11
+ ExecutionInput,
12
+ FileInput,
13
+ ExecutionOutput,
14
+ ExecutionResult,
15
+ ExecutionMetadata,
16
+ ExecutionError,
17
+ ExecutionErrorCode,
18
+ // Options
19
+ ExecutionOptions,
20
+ RetryConfig,
21
+ // Runtime types
22
+ ContainerRuntime,
23
+ RuntimeDetection,
24
+ RuntimeStatus,
25
+ // Engine health
26
+ EngineHealth,
27
+ EngineState,
28
+ // Provider interface
29
+ ExecutionProvider,
30
+ // Command types
31
+ ParsedCommand,
32
+ CommandToken,
33
+ InterpolationOptions,
34
+ // Validation types
35
+ InputValidationResult,
36
+ InputValidationError,
37
+ // Dry run
38
+ DryRunResult,
39
+ } from "./types.js";
40
+
41
+ // Constants
42
+ export { DEFAULT_RETRY_CONFIG } from "./types.js";
43
+
44
+ // Runtime detection
45
+ export {
46
+ detectRuntime,
47
+ clearRuntimeCache,
48
+ isRuntimeAvailable,
49
+ getAvailableRuntimes,
50
+ RuntimeStatusTracker,
51
+ createRuntimeTracker,
52
+ } from "./runtime.js";
53
+
54
+ // Command interpolation
55
+ export {
56
+ parseCommand,
57
+ interpolateCommand,
58
+ shellEscape,
59
+ parseCommandArgs,
60
+ prepareCommand,
61
+ getMissingParams,
62
+ } from "./command.js";
63
+
64
+ // Input validation
65
+ export {
66
+ validateInputs,
67
+ applyDefaults,
68
+ getRequiredParams,
69
+ getParamInfo,
70
+ } from "./validation.js";
71
+
72
+ // NOTE: Dagger provider moved to @enactprotocol/execution package
73
+ // This keeps @enactprotocol/shared browser-safe (no Dagger SDK dependency)
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Container runtime detection
3
+ *
4
+ * Auto-detects available container runtimes (docker, podman, nerdctl)
5
+ * and provides runtime status monitoring.
6
+ */
7
+
8
+ import { spawnSync } from "node:child_process";
9
+ import type { ContainerRuntime, RuntimeDetection, RuntimeStatus } from "./types";
10
+
11
+ /**
12
+ * Order of preference for container runtime detection
13
+ */
14
+ const RUNTIME_PREFERENCE: ContainerRuntime[] = ["docker", "podman", "nerdctl"];
15
+
16
+ /**
17
+ * Runtime-specific version commands
18
+ */
19
+ const VERSION_COMMANDS: Record<ContainerRuntime, string[]> = {
20
+ docker: ["docker", "--version"],
21
+ podman: ["podman", "--version"],
22
+ nerdctl: ["nerdctl", "--version"],
23
+ };
24
+
25
+ /**
26
+ * Cached detection result
27
+ */
28
+ let cachedDetection: RuntimeDetection | null = null;
29
+ let cachedDetectionTime = 0;
30
+ const CACHE_TTL_MS = 60000; // 1 minute
31
+
32
+ /**
33
+ * Check if a command is available in PATH
34
+ */
35
+ function commandExists(command: string): boolean {
36
+ try {
37
+ const result = spawnSync("which", [command], {
38
+ encoding: "utf-8",
39
+ timeout: 5000,
40
+ });
41
+ return result.status === 0 && result.stdout.trim().length > 0;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Get the version of a container runtime
49
+ */
50
+ function getRuntimeVersion(runtime: ContainerRuntime): string | undefined {
51
+ try {
52
+ const versionCmd = VERSION_COMMANDS[runtime];
53
+ const cmd = versionCmd[0];
54
+ if (!cmd) return undefined;
55
+ const args = versionCmd.slice(1);
56
+ const result = spawnSync(cmd, args, {
57
+ encoding: "utf-8",
58
+ timeout: 5000,
59
+ });
60
+
61
+ if (result.status === 0) {
62
+ // Parse version from output
63
+ // Docker: "Docker version 24.0.6, build ed223bc"
64
+ // Podman: "podman version 4.5.1"
65
+ // nerdctl: "nerdctl version 1.5.0"
66
+ const match = result.stdout.match(/(\d+\.\d+\.\d+)/);
67
+ return match?.[1];
68
+ }
69
+ } catch {
70
+ // Ignore errors
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ /**
76
+ * Get the path to a runtime binary
77
+ */
78
+ function getRuntimePath(runtime: ContainerRuntime): string | undefined {
79
+ try {
80
+ const result = spawnSync("which", [runtime], {
81
+ encoding: "utf-8",
82
+ timeout: 5000,
83
+ });
84
+ if (result.status === 0) {
85
+ return result.stdout.trim();
86
+ }
87
+ } catch {
88
+ // Ignore errors
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ /**
94
+ * Detect available container runtime
95
+ *
96
+ * Checks for docker, podman, and nerdctl in order of preference.
97
+ * Results are cached for 1 minute.
98
+ *
99
+ * @returns Detection result with runtime info or error
100
+ */
101
+ export function detectRuntime(): RuntimeDetection {
102
+ // Return cached result if still valid
103
+ const now = Date.now();
104
+ if (cachedDetection && now - cachedDetectionTime < CACHE_TTL_MS) {
105
+ return cachedDetection;
106
+ }
107
+
108
+ for (const runtime of RUNTIME_PREFERENCE) {
109
+ if (commandExists(runtime)) {
110
+ const version = getRuntimeVersion(runtime);
111
+ const path = getRuntimePath(runtime);
112
+
113
+ const result: RuntimeDetection = {
114
+ found: true,
115
+ runtime,
116
+ };
117
+ if (path) result.path = path;
118
+ if (version) result.version = version;
119
+
120
+ cachedDetection = result;
121
+ cachedDetectionTime = now;
122
+ return result;
123
+ }
124
+ }
125
+
126
+ const notFoundResult: RuntimeDetection = {
127
+ found: false,
128
+ error: getInstallInstructions(),
129
+ };
130
+ cachedDetection = notFoundResult;
131
+ cachedDetectionTime = now;
132
+ return notFoundResult;
133
+ }
134
+
135
+ /**
136
+ * Get helpful installation instructions when no runtime is found
137
+ */
138
+ function getInstallInstructions(): string {
139
+ const platform = process.platform;
140
+
141
+ if (platform === "darwin") {
142
+ return (
143
+ "No container runtime found. Install Docker Desktop:\n" +
144
+ " brew install --cask docker\n" +
145
+ "Or install Podman:\n" +
146
+ " brew install podman"
147
+ );
148
+ }
149
+
150
+ if (platform === "linux") {
151
+ return (
152
+ "No container runtime found. Install Docker:\n" +
153
+ " curl -fsSL https://get.docker.com | sh\n" +
154
+ "Or install Podman:\n" +
155
+ " sudo apt install podman # Debian/Ubuntu\n" +
156
+ " sudo dnf install podman # Fedora/RHEL"
157
+ );
158
+ }
159
+
160
+ if (platform === "win32") {
161
+ return (
162
+ "No container runtime found. Install Docker Desktop:\n" +
163
+ " winget install Docker.DockerDesktop\n" +
164
+ "Or download from: https://www.docker.com/products/docker-desktop"
165
+ );
166
+ }
167
+
168
+ return "No container runtime found. Please install Docker, Podman, or nerdctl.";
169
+ }
170
+
171
+ /**
172
+ * Clear the cached detection result
173
+ * Useful after installing a runtime
174
+ */
175
+ export function clearRuntimeCache(): void {
176
+ cachedDetection = null;
177
+ cachedDetectionTime = 0;
178
+ }
179
+
180
+ /**
181
+ * Force detection of a specific runtime
182
+ *
183
+ * @param runtime - The runtime to check
184
+ * @returns Whether the runtime is available
185
+ */
186
+ export function isRuntimeAvailable(runtime: ContainerRuntime): boolean {
187
+ return commandExists(runtime);
188
+ }
189
+
190
+ /**
191
+ * Get all available runtimes
192
+ *
193
+ * @returns Array of available runtimes
194
+ */
195
+ export function getAvailableRuntimes(): ContainerRuntime[] {
196
+ return RUNTIME_PREFERENCE.filter((runtime) => commandExists(runtime));
197
+ }
198
+
199
+ /**
200
+ * Runtime status tracker for health monitoring
201
+ */
202
+ export class RuntimeStatusTracker {
203
+ private status: RuntimeStatus;
204
+ private healthCheckInterval: ReturnType<typeof setInterval> | null = null;
205
+
206
+ constructor(runtime: ContainerRuntime) {
207
+ this.status = {
208
+ available: true,
209
+ runtime,
210
+ engineHealthy: true,
211
+ lastHealthCheck: new Date(),
212
+ failureCount: 0,
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Record a successful operation
218
+ */
219
+ recordSuccess(): void {
220
+ this.status.failureCount = 0;
221
+ this.status.engineHealthy = true;
222
+ this.status.lastHealthCheck = new Date();
223
+ }
224
+
225
+ /**
226
+ * Record a failed operation
227
+ *
228
+ * @returns Whether the engine should be reset (3+ consecutive failures)
229
+ */
230
+ recordFailure(): boolean {
231
+ this.status.failureCount++;
232
+ this.status.lastHealthCheck = new Date();
233
+
234
+ if (this.status.failureCount >= 3) {
235
+ this.status.engineHealthy = false;
236
+ return true; // Engine needs reset
237
+ }
238
+
239
+ return false;
240
+ }
241
+
242
+ /**
243
+ * Get current status
244
+ */
245
+ getStatus(): RuntimeStatus {
246
+ return { ...this.status };
247
+ }
248
+
249
+ /**
250
+ * Check if engine needs reset
251
+ */
252
+ needsReset(): boolean {
253
+ return this.status.failureCount >= 3;
254
+ }
255
+
256
+ /**
257
+ * Reset failure count after engine restart
258
+ */
259
+ resetFailureCount(): void {
260
+ this.status.failureCount = 0;
261
+ this.status.engineHealthy = true;
262
+ }
263
+
264
+ /**
265
+ * Start periodic health checks
266
+ *
267
+ * @param intervalMs - Check interval in milliseconds (default: 60000)
268
+ * @param onUnhealthy - Callback when engine becomes unhealthy
269
+ */
270
+ startHealthChecks(intervalMs = 60000, onUnhealthy?: (status: RuntimeStatus) => void): void {
271
+ if (this.healthCheckInterval) {
272
+ clearInterval(this.healthCheckInterval);
273
+ }
274
+
275
+ this.healthCheckInterval = setInterval(() => {
276
+ const detection = detectRuntime();
277
+ this.status.available = detection.found;
278
+ this.status.lastHealthCheck = new Date();
279
+
280
+ if (!detection.found || !this.status.engineHealthy) {
281
+ onUnhealthy?.(this.status);
282
+ }
283
+ }, intervalMs);
284
+ }
285
+
286
+ /**
287
+ * Stop periodic health checks
288
+ */
289
+ stopHealthChecks(): void {
290
+ if (this.healthCheckInterval) {
291
+ clearInterval(this.healthCheckInterval);
292
+ this.healthCheckInterval = null;
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Create a runtime status tracker for the detected runtime
299
+ *
300
+ * @returns Status tracker or null if no runtime found
301
+ */
302
+ export function createRuntimeTracker(): RuntimeStatusTracker | null {
303
+ const detection = detectRuntime();
304
+ if (!detection.found || !detection.runtime) {
305
+ return null;
306
+ }
307
+ return new RuntimeStatusTracker(detection.runtime);
308
+ }