@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.
- package/README.md +44 -0
- package/package.json +16 -58
- package/src/config.ts +476 -0
- package/src/constants.ts +36 -0
- package/src/execution/command.ts +314 -0
- package/src/execution/index.ts +73 -0
- package/src/execution/runtime.ts +308 -0
- package/src/execution/types.ts +379 -0
- package/src/execution/validation.ts +508 -0
- package/src/index.ts +237 -30
- package/src/manifest/index.ts +36 -0
- package/src/manifest/loader.ts +187 -0
- package/src/manifest/parser.ts +173 -0
- package/src/manifest/validator.ts +309 -0
- package/src/paths.ts +108 -0
- package/src/registry.ts +219 -0
- package/src/resolver.ts +345 -0
- package/src/types/index.ts +30 -0
- package/src/types/manifest.ts +255 -0
- package/src/types.ts +5 -188
- package/src/utils/fs.ts +281 -0
- package/src/utils/logger.ts +270 -59
- package/src/utils/version.ts +304 -36
- package/tests/config.test.ts +515 -0
- package/tests/execution/command.test.ts +317 -0
- package/tests/execution/validation.test.ts +384 -0
- package/tests/fixtures/invalid-tool.yaml +4 -0
- package/tests/fixtures/valid-tool.md +62 -0
- package/tests/fixtures/valid-tool.yaml +40 -0
- package/tests/index.test.ts +8 -0
- package/tests/manifest/loader.test.ts +291 -0
- package/tests/manifest/parser.test.ts +345 -0
- package/tests/manifest/validator.test.ts +394 -0
- package/tests/manifest-types.test.ts +358 -0
- package/tests/paths.test.ts +153 -0
- package/tests/registry.test.ts +231 -0
- package/tests/resolver.test.ts +272 -0
- package/tests/utils/fs.test.ts +388 -0
- package/tests/utils/logger.test.ts +480 -0
- package/tests/utils/version.test.ts +390 -0
- package/tsconfig.json +12 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/LocalToolResolver.d.ts +0 -84
- package/dist/LocalToolResolver.js +0 -353
- package/dist/api/enact-api.d.ts +0 -130
- package/dist/api/enact-api.js +0 -428
- package/dist/api/index.d.ts +0 -2
- package/dist/api/index.js +0 -2
- package/dist/api/types.d.ts +0 -103
- package/dist/api/types.js +0 -1
- package/dist/constants.d.ts +0 -7
- package/dist/constants.js +0 -10
- package/dist/core/DaggerExecutionProvider.d.ts +0 -169
- package/dist/core/DaggerExecutionProvider.js +0 -1029
- package/dist/core/DirectExecutionProvider.d.ts +0 -23
- package/dist/core/DirectExecutionProvider.js +0 -406
- package/dist/core/EnactCore.d.ts +0 -162
- package/dist/core/EnactCore.js +0 -597
- package/dist/core/NativeExecutionProvider.d.ts +0 -9
- package/dist/core/NativeExecutionProvider.js +0 -16
- package/dist/core/index.d.ts +0 -3
- package/dist/core/index.js +0 -3
- package/dist/exec/index.d.ts +0 -3
- package/dist/exec/index.js +0 -3
- package/dist/exec/logger.d.ts +0 -11
- package/dist/exec/logger.js +0 -57
- package/dist/exec/validate.d.ts +0 -5
- package/dist/exec/validate.js +0 -167
- package/dist/index.d.ts +0 -21
- package/dist/index.js +0 -25
- package/dist/lib/enact-direct.d.ts +0 -150
- package/dist/lib/enact-direct.js +0 -159
- package/dist/lib/index.d.ts +0 -1
- package/dist/lib/index.js +0 -1
- package/dist/security/index.d.ts +0 -3
- package/dist/security/index.js +0 -3
- package/dist/security/security.d.ts +0 -23
- package/dist/security/security.js +0 -137
- package/dist/security/sign.d.ts +0 -103
- package/dist/security/sign.js +0 -666
- package/dist/security/verification-enforcer.d.ts +0 -53
- package/dist/security/verification-enforcer.js +0 -204
- package/dist/services/McpCoreService.d.ts +0 -98
- package/dist/services/McpCoreService.js +0 -124
- package/dist/services/index.d.ts +0 -1
- package/dist/services/index.js +0 -1
- package/dist/types.d.ts +0 -132
- package/dist/types.js +0 -3
- package/dist/utils/config.d.ts +0 -111
- package/dist/utils/config.js +0 -342
- package/dist/utils/env-loader.d.ts +0 -54
- package/dist/utils/env-loader.js +0 -270
- package/dist/utils/help.d.ts +0 -36
- package/dist/utils/help.js +0 -248
- package/dist/utils/index.d.ts +0 -7
- package/dist/utils/index.js +0 -7
- package/dist/utils/logger.d.ts +0 -35
- package/dist/utils/logger.js +0 -75
- package/dist/utils/silent-monitor.d.ts +0 -67
- package/dist/utils/silent-monitor.js +0 -242
- package/dist/utils/timeout.d.ts +0 -5
- package/dist/utils/timeout.js +0 -23
- package/dist/utils/version.d.ts +0 -4
- package/dist/utils/version.js +0 -35
- package/dist/web/env-manager-server.d.ts +0 -29
- package/dist/web/env-manager-server.js +0 -367
- package/dist/web/index.d.ts +0 -1
- package/dist/web/index.js +0 -1
- package/src/LocalToolResolver.ts +0 -424
- package/src/api/enact-api.ts +0 -604
- package/src/api/index.ts +0 -2
- package/src/api/types.ts +0 -114
- package/src/core/DaggerExecutionProvider.ts +0 -1357
- package/src/core/DirectExecutionProvider.ts +0 -484
- package/src/core/EnactCore.ts +0 -847
- package/src/core/index.ts +0 -3
- package/src/exec/index.ts +0 -3
- package/src/exec/logger.ts +0 -63
- package/src/exec/validate.ts +0 -238
- package/src/lib/enact-direct.ts +0 -254
- package/src/lib/index.ts +0 -1
- package/src/services/McpCoreService.ts +0 -201
- package/src/services/index.ts +0 -1
- package/src/utils/config.ts +0 -438
- package/src/utils/env-loader.ts +0 -370
- package/src/utils/help.ts +0 -257
- package/src/utils/index.ts +0 -7
- package/src/utils/silent-monitor.ts +0 -328
- package/src/utils/timeout.ts +0 -26
- package/src/web/env-manager-server.ts +0 -465
- package/src/web/index.ts +0 -1
- package/src/web/static/app.js +0 -663
- package/src/web/static/index.html +0 -117
- 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
|
+
}
|