@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
@@ -1,1357 +0,0 @@
1
- // src/core/DaggerExecutionProvider.ts - Enhanced Dagger execution provider with hanging prevention
2
- import { connect, Client, Container } from "@dagger.io/dagger";
3
- import {
4
- ExecutionProvider,
5
- type EnactTool,
6
- type ExecutionEnvironment,
7
- type ExecutionResult,
8
- } from "../types.js";
9
- import logger from "../exec/logger.js";
10
- import { parseTimeout } from "../utils/timeout.js";
11
- import fs from "fs/promises";
12
- import path from "path";
13
- import crypto from "crypto";
14
- import { spawn, spawnSync } from "child_process";
15
- import { exit } from "process";
16
-
17
- export interface DaggerExecutionOptions {
18
- baseImage?: string; // Default container image
19
- workdir?: string; // Working directory in container
20
- enableNetwork?: boolean; // Allow network access
21
- enableHostFS?: boolean; // Allow mounting host filesystem
22
- maxMemory?: string; // Memory limit (e.g., "512Mi", "2Gi")
23
- maxCPU?: string; // CPU limit (e.g., "0.5", "2")
24
- cacheVolume?: string; // Cache volume name for persistence
25
- useShell?: boolean; // Use shell wrapper for complex commands
26
- engineTimeout?: number; // Engine connection timeout (ms)
27
- maxRetries?: number; // Max retries for failed operations
28
- enableEngineHealthCheck?: boolean; // Enable periodic engine health checks
29
- }
30
-
31
- interface CommandResult {
32
- stdout: string;
33
- stderr: string;
34
- exitCode: number;
35
- }
36
-
37
- interface EngineHealthStatus {
38
- isHealthy: boolean;
39
- lastCheck: Date;
40
- consecutiveFailures: number;
41
- }
42
-
43
- export class DaggerExecutionProvider extends ExecutionProvider {
44
- private client: Client | null = null;
45
- private options: DaggerExecutionOptions;
46
- private tempDir: string;
47
- private connectionCleanup: (() => void) | null = null;
48
- private engineHealth: EngineHealthStatus;
49
- private abortController: AbortController | null = null;
50
- private activeSessions: Set<string> = new Set();
51
- private isShuttingDown = false;
52
-
53
- constructor(options: DaggerExecutionOptions = {}) {
54
- super();
55
- this.options = {
56
- baseImage: "node:20-slim",
57
- workdir: "/workspace",
58
- enableNetwork: true,
59
- enableHostFS: false,
60
- useShell: true,
61
- engineTimeout: 30000, // 30 second engine timeout
62
- maxRetries: 3, // Max 3 retries
63
- enableEngineHealthCheck: true,
64
- ...options,
65
- };
66
- this.tempDir = "";
67
- this.engineHealth = {
68
- isHealthy: true,
69
- lastCheck: new Date(),
70
- consecutiveFailures: 0,
71
- };
72
-
73
- // Register comprehensive cleanup handlers
74
- this.registerCleanupHandlers();
75
-
76
- // Start periodic health checks if enabled
77
- if (this.options.enableEngineHealthCheck) {
78
- this.startEngineHealthMonitoring();
79
- }
80
- }
81
-
82
- async setup(tool: EnactTool): Promise<boolean> {
83
- try {
84
- // Perform engine health check before setup
85
- if (!(await this.checkEngineHealth())) {
86
- logger.warn("🔧 Engine unhealthy, attempting reset...");
87
- await this.resetEngineContainer();
88
- }
89
-
90
- // Create a temporary directory for this execution
91
- this.tempDir = path.join(
92
- "/tmp",
93
- `enact-${crypto.randomBytes(8).toString("hex")}`,
94
- );
95
- await fs.mkdir(this.tempDir, { recursive: true });
96
-
97
- logger.info(
98
- `🐳 Dagger execution provider initialized for tool: ${tool.name}`,
99
- );
100
- return true;
101
- } catch (error) {
102
- logger.error(`Failed to setup Dagger execution provider: ${error}`);
103
- return false;
104
- }
105
- }
106
-
107
- /**
108
- * Enhanced cleanup with comprehensive engine management and session tracking
109
- */
110
- async cleanup(): Promise<boolean> {
111
- this.isShuttingDown = true;
112
-
113
- try {
114
- // Cancel any active operations
115
- if (this.abortController) {
116
- this.abortController.abort();
117
- }
118
-
119
- // Wait for active sessions to complete (with timeout)
120
- await this.waitForActiveSessions(5000);
121
-
122
- // Clean up temporary directory
123
- if (this.tempDir) {
124
- await fs.rm(this.tempDir, { recursive: true, force: true });
125
- }
126
-
127
- // Enhanced engine cleanup with better detection and error handling
128
- await this.performEngineCleanup();
129
-
130
- // Reset client reference
131
- this.client = null;
132
-
133
- logger.info("🧹 Dagger execution provider cleaned up successfully");
134
- return true;
135
- } catch (error) {
136
- logger.error(`Failed to cleanup Dagger execution provider: ${error}`);
137
- return false;
138
- }
139
- }
140
-
141
- /**
142
- * Enhanced engine cleanup with better container detection
143
- */
144
- private async performEngineCleanup(): Promise<void> {
145
- try {
146
- logger.debug("🔍 Detecting Dagger engine containers...");
147
-
148
- // Get all Dagger engine containers (running and stopped)
149
- const containerListResult = spawnSync(
150
- "docker",
151
- [
152
- "container",
153
- "list",
154
- "--all",
155
- "--filter",
156
- "name=^dagger-engine-*",
157
- "--format",
158
- "{{.Names}}",
159
- ],
160
- {
161
- encoding: "utf8",
162
- timeout: 10000,
163
- },
164
- );
165
-
166
- if (containerListResult.error) {
167
- logger.warn(
168
- "Could not list Docker containers, skipping engine cleanup",
169
- );
170
- return;
171
- }
172
-
173
- const containerNames = containerListResult.stdout
174
- .trim()
175
- .split("\n")
176
- .filter((name) => name.trim())
177
- .map((name) => name.trim());
178
-
179
- if (containerNames.length === 0) {
180
- logger.debug("No Dagger engine containers found");
181
- return;
182
- }
183
-
184
- logger.info(
185
- `🔄 Found ${containerNames.length} Dagger engine container(s), cleaning up...`,
186
- );
187
-
188
- // Force remove all engine containers
189
- for (const containerName of containerNames) {
190
- try {
191
- logger.debug(`Removing container: ${containerName}`);
192
- spawnSync("docker", ["container", "rm", "-f", containerName], {
193
- timeout: 10000,
194
- });
195
- } catch (e) {
196
- logger.debug(`Failed to remove container ${containerName}:`, e);
197
- }
198
- }
199
-
200
- // Optional: Clean up engine images if requested (more aggressive cleanup)
201
- if (process.env.DAGGER_AGGRESSIVE_CLEANUP === "true") {
202
- logger.debug(
203
- "🧹 Performing aggressive cleanup - removing engine images...",
204
- );
205
- spawnSync(
206
- "docker",
207
- [
208
- "rmi",
209
- "--force",
210
- ...spawnSync(
211
- "docker",
212
- [
213
- "images",
214
- "-q",
215
- "--filter",
216
- "reference=registry.dagger.io/engine",
217
- ],
218
- {
219
- encoding: "utf8",
220
- },
221
- )
222
- .stdout.trim()
223
- .split("\n")
224
- .filter(Boolean),
225
- ],
226
- { timeout: 15000 },
227
- );
228
- }
229
-
230
- logger.info("✅ Dagger engine cleanup completed");
231
- } catch (error) {
232
- logger.debug("Engine cleanup failed (this is usually fine):", error);
233
- }
234
- }
235
-
236
- /**
237
- * Check engine health with comprehensive diagnostics
238
- */
239
- private async checkEngineHealth(): Promise<boolean> {
240
- try {
241
- // Check if Docker daemon is accessible
242
- const dockerCheck = spawnSync("docker", ["version"], {
243
- encoding: "utf8",
244
- timeout: 5000,
245
- });
246
-
247
- if (dockerCheck.error || dockerCheck.status !== 0) {
248
- logger.warn("Docker daemon not accessible");
249
- this.engineHealth.consecutiveFailures++;
250
- return false;
251
- }
252
-
253
- // Check for hanging engine containers
254
- const hangingContainers = spawnSync(
255
- "docker",
256
- [
257
- "ps",
258
- "--filter",
259
- "name=dagger-engine",
260
- "--filter",
261
- "status=exited",
262
- "--format",
263
- "{{.Names}}",
264
- ],
265
- {
266
- encoding: "utf8",
267
- timeout: 5000,
268
- },
269
- );
270
-
271
- if (hangingContainers.stdout.trim()) {
272
- logger.warn("Detected stopped Dagger engine containers");
273
- this.engineHealth.consecutiveFailures++;
274
- return false;
275
- }
276
-
277
- // Reset failure count on success
278
- this.engineHealth.consecutiveFailures = 0;
279
- this.engineHealth.isHealthy = true;
280
- this.engineHealth.lastCheck = new Date();
281
- return true;
282
- } catch (error) {
283
- logger.debug("Engine health check failed:", error);
284
- this.engineHealth.consecutiveFailures++;
285
- return false;
286
- }
287
- }
288
-
289
- /**
290
- * Reset engine container when health check fails
291
- */
292
- private async resetEngineContainer(): Promise<void> {
293
- logger.info("🔄 Resetting Dagger engine container...");
294
-
295
- try {
296
- // Stop and remove all engine containers
297
- await this.performEngineCleanup();
298
-
299
- // Wait a moment for cleanup to complete
300
- await new Promise((resolve) => setTimeout(resolve, 2000));
301
-
302
- // Engine will auto-restart on next connection
303
- this.engineHealth.isHealthy = true;
304
- this.engineHealth.consecutiveFailures = 0;
305
-
306
- logger.info("✅ Engine reset completed");
307
- } catch (error) {
308
- logger.error("Failed to reset engine container:", error);
309
- throw error;
310
- }
311
- }
312
-
313
- /**
314
- * Start periodic engine health monitoring
315
- */
316
- private startEngineHealthMonitoring(): void {
317
- // Check engine health every 60 seconds
318
- setInterval(async () => {
319
- if (this.isShuttingDown) return;
320
-
321
- const isHealthy = await this.checkEngineHealth();
322
-
323
- if (!isHealthy && this.engineHealth.consecutiveFailures >= 3) {
324
- logger.warn("🚨 Engine health degraded, triggering reset...");
325
- try {
326
- await this.resetEngineContainer();
327
- } catch (error) {
328
- logger.error("Failed to auto-reset engine:", error);
329
- }
330
- }
331
- }, 60000);
332
- }
333
-
334
- /**
335
- * Wait for active sessions to complete with timeout
336
- */
337
- private async waitForActiveSessions(timeoutMs: number): Promise<void> {
338
- if (this.activeSessions.size === 0) return;
339
-
340
- logger.info(
341
- `⏳ Waiting for ${this.activeSessions.size} active sessions to complete...`,
342
- );
343
-
344
- const startTime = Date.now();
345
- while (this.activeSessions.size > 0 && Date.now() - startTime < timeoutMs) {
346
- await new Promise((resolve) => setTimeout(resolve, 100));
347
- }
348
-
349
- if (this.activeSessions.size > 0) {
350
- logger.warn(
351
- `⚠️ ${this.activeSessions.size} sessions did not complete within timeout`,
352
- );
353
- }
354
- }
355
-
356
- async resolveEnvironmentVariables(
357
- envConfig: Record<string, any>,
358
- namespace?: string,
359
- ): Promise<Record<string, any>> {
360
- const resolved: Record<string, any> = {};
361
-
362
- for (const [key, config] of Object.entries(envConfig)) {
363
- if (typeof config === "object" && config.source) {
364
- switch (config.source) {
365
- case "env":
366
- resolved[key] = process.env[key] || config.default;
367
- break;
368
- case "user":
369
- resolved[key] = config.default;
370
- break;
371
- default:
372
- resolved[key] = config.default;
373
- }
374
- } else {
375
- resolved[key] = config;
376
- }
377
- }
378
-
379
- return resolved;
380
- }
381
-
382
- async execute(
383
- tool: EnactTool,
384
- inputs: Record<string, any>,
385
- environment: ExecutionEnvironment,
386
- ): Promise<ExecutionResult> {
387
- const executionId = crypto.randomBytes(16).toString("hex");
388
- const startTime = new Date().toISOString();
389
-
390
- // Track this session
391
- this.activeSessions.add(executionId);
392
-
393
- try {
394
- logger.info(`🚀 Executing Enact tool "${tool.name}" in Dagger container`);
395
- logger.debug(`Tool command: ${tool.command}`);
396
- logger.debug(`Tool timeout: ${tool.timeout || "default"}`);
397
-
398
- // Retry logic for handling transient failures
399
- let lastError: Error | null = null;
400
- for (let attempt = 1; attempt <= this.options.maxRetries!; attempt++) {
401
- try {
402
- // Check engine health before each attempt
403
- if (!(await this.checkEngineHealth())) {
404
- logger.warn(`Attempt ${attempt}: Engine unhealthy, resetting...`);
405
- await this.resetEngineContainer();
406
- }
407
-
408
- const result = await this.executeCommand(
409
- tool.command,
410
- inputs,
411
- environment,
412
- tool.timeout,
413
- undefined,
414
- tool,
415
- );
416
-
417
- logger.debug(
418
- `Command result: exitCode=${result.exitCode}, stdout length=${result.stdout?.length || 0}, stderr length=${result.stderr?.length || 0}`,
419
- );
420
-
421
- const output = this.parseOutput(result.stdout, tool);
422
-
423
- return {
424
- success: result.exitCode === 0,
425
- output: output,
426
- error:
427
- result.exitCode !== 0
428
- ? {
429
- message: result.stderr || "Command failed",
430
- code: result.exitCode.toString(),
431
- details: {
432
- stdout: result.stdout,
433
- stderr: result.stderr,
434
- exitCode: result.exitCode,
435
- attempt,
436
- },
437
- }
438
- : undefined,
439
- metadata: {
440
- executionId,
441
- toolName: tool.name,
442
- version: tool.version || "1.0.0",
443
- executedAt: startTime,
444
- environment: "dagger",
445
- timeout: tool.timeout,
446
- command: tool.command,
447
- },
448
- };
449
- } catch (error) {
450
- lastError = error instanceof Error ? error : new Error(String(error));
451
- logger.warn(
452
- `Attempt ${attempt}/${this.options.maxRetries} failed: ${lastError.message}`,
453
- );
454
-
455
- if (attempt < this.options.maxRetries!) {
456
- // Wait before retry with exponential backoff
457
- const waitTime = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
458
- logger.debug(`Waiting ${waitTime}ms before retry...`);
459
- await new Promise((resolve) => setTimeout(resolve, waitTime));
460
- }
461
- }
462
-
463
- }
464
-
465
- // All retries failed
466
- throw lastError || new Error("Unknown error during execution");
467
- } catch (error) {
468
- logger.error(`Execution failed for Enact tool ${tool.name}: ${error}`);
469
-
470
- // Enhanced error categorization
471
- const errorType = this.categorizeError(error);
472
-
473
- return {
474
- success: false,
475
- error: {
476
- message: error instanceof Error ? error.message : "Unknown error",
477
- code: errorType,
478
- details: {
479
- error,
480
- engineHealth: this.engineHealth,
481
- activeSessions: this.activeSessions.size,
482
- },
483
- },
484
- metadata: {
485
- executionId,
486
- toolName: tool.name,
487
- version: tool.version || "1.0.0",
488
- executedAt: startTime,
489
- environment: "dagger",
490
- timeout: tool.timeout,
491
- command: tool.command,
492
- },
493
- };
494
- } finally {
495
- // Remove from active sessions
496
- this.activeSessions.delete(executionId);
497
- }
498
- }
499
-
500
- /**
501
- * Categorize errors for better handling
502
- */
503
- private categorizeError(error: unknown): string {
504
- if (!(error instanceof Error)) return "UNKNOWN_ERROR";
505
-
506
- const message = error.message.toLowerCase();
507
-
508
- if (message.includes("timeout") || message.includes("timed out")) {
509
- return "TIMEOUT";
510
- }
511
- if (message.includes("buildkit") || message.includes("failed to respond")) {
512
- return "ENGINE_CONNECTION_ERROR";
513
- }
514
- if (message.includes("docker") || message.includes("container")) {
515
- return "CONTAINER_ERROR";
516
- }
517
- if (message.includes("network") || message.includes("dns")) {
518
- return "NETWORK_ERROR";
519
- }
520
-
521
- return "EXECUTION_ERROR";
522
- }
523
-
524
- async executeCommand(
525
- command: string,
526
- inputs: Record<string, any>,
527
- environment: ExecutionEnvironment,
528
- timeout?: string,
529
- options?: {
530
- verbose?: boolean;
531
- showSpinner?: boolean;
532
- streamOutput?: boolean;
533
- },
534
- tool?: EnactTool,
535
- ): Promise<CommandResult> {
536
- const verbose = options?.verbose ?? false;
537
- const showSpinner = options?.showSpinner ?? false;
538
-
539
- // Create abort controller for this execution
540
- this.abortController = new AbortController();
541
-
542
- // Start spinner if requested
543
- let spinner: any = null;
544
- if (showSpinner) {
545
- try {
546
- const p = require("@clack/prompts");
547
- spinner = p.spinner();
548
- spinner.start("Executing Enact tool in container...");
549
- } catch (e) {
550
- console.log("Executing Enact tool in container...");
551
- }
552
- }
553
-
554
- try {
555
- // Substitute template variables in command (Enact Protocol style)
556
- const substitutedCommand = this.substituteCommandVariables(
557
- command,
558
- inputs,
559
- );
560
-
561
- if (verbose) {
562
- // Determine which container image to use - prefer tool's 'from' field over default baseImage
563
- const containerImage = tool?.from || this.options.baseImage!;
564
-
565
- try {
566
- const pc = require("picocolors");
567
- console.error(
568
- pc.cyan("\n🐳 Executing Enact command in Dagger container:"),
569
- );
570
- console.error(pc.white(substitutedCommand));
571
- console.error(pc.gray(`Container image: ${containerImage}${tool?.from ? ' (from tool.from)' : ' (default baseImage)'}`));
572
- } catch (e) {
573
- console.error("\n🐳 Executing Enact command in Dagger container:");
574
- console.error(substitutedCommand);
575
- console.error(`Container image: ${containerImage}${tool?.from ? ' (from tool.from)' : ' (default baseImage)'}`);
576
- }
577
- }
578
-
579
- // Parse and apply timeout with engine timeout consideration
580
- const timeoutMs = timeout ? parseTimeout(timeout) : 30000;
581
- const effectiveTimeout = Math.max(timeoutMs, this.options.engineTimeout!);
582
- logger.debug(
583
- `Parsed timeout: ${effectiveTimeout}ms (command: ${timeoutMs}ms, engine: ${this.options.engineTimeout}ms)`,
584
- );
585
-
586
- // Execute command with enhanced error handling and timeout management
587
- const result = await Promise.race([
588
- this.executeWithConnect(substitutedCommand, environment, inputs, tool),
589
- this.createTimeoutPromise(effectiveTimeout),
590
- ]);
591
-
592
- if (spinner) {
593
- spinner.stop("✅ Enact tool execution completed");
594
- }
595
-
596
- return result;
597
- } catch (error) {
598
- if (spinner) {
599
- spinner.stop("❌ Enact tool execution failed");
600
- }
601
-
602
- // Enhanced timeout handling
603
- if (
604
- error instanceof Error &&
605
- (error.message === "TIMEOUT" || error.message.includes("timed out"))
606
- ) {
607
- // Mark engine as potentially unhealthy after timeout
608
- this.engineHealth.consecutiveFailures++;
609
- throw new Error(
610
- `Command timed out after ${timeout || "30s"} - consider increasing timeout or checking engine health`,
611
- );
612
- }
613
-
614
- // Handle connection errors specifically
615
- if (
616
- error instanceof Error &&
617
- error.message.includes("buildkit failed to respond")
618
- ) {
619
- this.engineHealth.consecutiveFailures++;
620
- throw new Error(
621
- "Dagger engine connection failed - engine may need reset",
622
- );
623
- }
624
-
625
- throw error;
626
- } finally {
627
- this.abortController = null;
628
-
629
- }
630
- }
631
-
632
- /**
633
- * Execute command using Dagger connect with proper session management
634
- */
635
- private async executeWithConnect(
636
- command: string,
637
- environment: ExecutionEnvironment,
638
- inputs: Record<string, any>,
639
- tool?: EnactTool,
640
- ): Promise<CommandResult> {
641
- return new Promise<CommandResult>((resolve, reject) => {
642
- // Setup abort handling
643
- const abortHandler = () => {
644
- reject(new Error("Execution aborted"));
645
- };
646
-
647
- if (this.abortController) {
648
- this.abortController.signal.addEventListener("abort", abortHandler);
649
- }
650
-
651
- connect(async (client: Client) => {
652
- try {
653
- logger.debug("🔗 Connected to Dagger client");
654
- const container = await this.setupContainer(
655
- client,
656
- environment,
657
- inputs,
658
- tool,
659
- );
660
- logger.debug("📦 Container setup complete");
661
- const commandResult = await this.executeInContainer(
662
- container,
663
- command,
664
- );
665
- logger.debug("⚡ Command execution complete");
666
-
667
- // Remove abort handler on success
668
- if (this.abortController) {
669
- this.abortController.signal.removeEventListener(
670
- "abort",
671
- abortHandler,
672
- );
673
- }
674
-
675
- resolve(commandResult);
676
- } catch (error) {
677
- logger.error("❌ Error in Dagger execution:", error);
678
-
679
- // Remove abort handler on error
680
- if (this.abortController) {
681
- this.abortController.signal.removeEventListener(
682
- "abort",
683
- abortHandler,
684
- );
685
- }
686
-
687
- reject(error);
688
- }
689
- }).catch((error) => {
690
- logger.error("❌ Error in Dagger connect:", error);
691
-
692
- // Remove abort handler on connection error
693
- if (this.abortController) {
694
- this.abortController.signal.removeEventListener(
695
- "abort",
696
- abortHandler,
697
- );
698
- }
699
-
700
- reject(error);
701
- });
702
- });
703
- }
704
-
705
- /**
706
- * Setup directory mounting for the container
707
- */
708
- private async setupDirectoryMount(
709
- client: Client,
710
- container: Container,
711
- mountSpec: string,
712
- ): Promise<Container> {
713
- try {
714
- // Parse mount specification (format: "localPath" or "localPath:containerPath")
715
- let localPath: string;
716
- let containerPath: string;
717
-
718
- // Handle Windows drive letters (e.g., C:\path) vs mount separator (:)
719
- const colonIndex = mountSpec.indexOf(':');
720
-
721
- if (colonIndex > 0) {
722
- // Check if this might be a Windows drive letter (single letter followed by colon)
723
- const potentialDriveLetter = mountSpec.substring(0, colonIndex);
724
- const isWindowsDrive = potentialDriveLetter.length === 1 && /[A-Za-z]/.test(potentialDriveLetter);
725
-
726
- if (isWindowsDrive) {
727
- // Look for the next colon that separates local from container path
728
- const nextColonIndex = mountSpec.indexOf(':', colonIndex + 1);
729
- if (nextColonIndex > 0) {
730
- localPath = mountSpec.substring(0, nextColonIndex);
731
- containerPath = mountSpec.substring(nextColonIndex + 1);
732
- } else {
733
- // No container path specified, use default
734
- localPath = mountSpec;
735
- containerPath = '/workspace/src';
736
- }
737
- } else {
738
- // Regular path:container split
739
- localPath = mountSpec.substring(0, colonIndex);
740
- containerPath = mountSpec.substring(colonIndex + 1);
741
- }
742
- } else if (colonIndex === 0) {
743
- // Starts with colon (e.g., ":/app")
744
- localPath = '';
745
- containerPath = mountSpec.substring(1);
746
- } else {
747
- localPath = mountSpec;
748
- containerPath = '/workspace/src'; // Default container path
749
- }
750
-
751
- // Resolve local path to absolute path
752
- const path = require('path');
753
- const resolvedLocalPath = path.resolve(localPath);
754
-
755
- // Check if local directory exists
756
- const fs = require('fs');
757
- if (!fs.existsSync(resolvedLocalPath)) {
758
- throw new Error(`Mount source directory does not exist: ${resolvedLocalPath}`);
759
- }
760
-
761
- // Create Directory object from local path
762
- const hostDirectory = client.host().directory(resolvedLocalPath);
763
-
764
- // Mount directory in container using withMountedDirectory for better performance
765
- container = container.withMountedDirectory(containerPath, hostDirectory);
766
-
767
- logger.debug(`📂 Mounted ${resolvedLocalPath} -> ${containerPath}`);
768
-
769
- return container;
770
- } catch (error) {
771
- logger.error(`Failed to setup directory mount: ${error}`);
772
- throw error;
773
- }
774
- }
775
-
776
- /**
777
- * Enhanced container setup with better tool detection and installation
778
- */
779
- private async setupContainer(
780
- client: Client,
781
- environment: ExecutionEnvironment,
782
- inputs: Record<string, any>,
783
- tool?: EnactTool,
784
- ): Promise<Container> {
785
- // Determine which container image to use - prefer tool's 'from' field over default baseImage
786
- const containerImage = tool?.from || this.options.baseImage!;
787
-
788
- logger.debug(
789
- `🚀 Setting up container with image: ${containerImage}${tool?.from ? ' (from tool.from)' : ' (default baseImage)'}`,
790
- );
791
-
792
- // Start with base container
793
- let container = client.container().from(containerImage);
794
- logger.debug("📦 Base container created");
795
-
796
- // Set working directory
797
- container = container.withWorkdir(this.options.workdir!);
798
- logger.debug(`📁 Working directory set to: ${this.options.workdir}`);
799
-
800
- // Handle directory mounting if specified
801
- if (environment.mount) {
802
- container = await this.setupDirectoryMount(client, container, environment.mount);
803
- }
804
-
805
- // Add environment variables from Enact tool env config
806
- for (const [key, value] of Object.entries(environment.vars)) {
807
- container = container.withEnvVariable(key, String(value));
808
- }
809
- logger.debug(
810
- `🌍 Added ${Object.keys(environment.vars).length} environment variables`,
811
- );
812
-
813
- // Install common tools needed for Enact commands
814
- if (this.options.enableNetwork) {
815
- container = await this.installCommonTools(container, containerImage);
816
- logger.debug("🔧 Common tools installed");
817
- } else {
818
- logger.debug("🔧 Skipping common tools installation (network disabled)");
819
- }
820
-
821
- // Create input files if needed (Enact Protocol supports file inputs)
822
- container = await this.prepareInputFiles(container, inputs);
823
- logger.debug("📄 Input files prepared");
824
-
825
- // Apply resource limits if specified
826
- if (environment.resources) {
827
- container = this.applyResourceLimits(container, environment.resources);
828
- logger.debug("💾 Resource limits applied");
829
- }
830
-
831
- logger.debug("✅ Container setup complete");
832
- return container;
833
- }
834
-
835
- /**
836
- * Install common tools that Enact commands might need
837
- * Enhanced with better error handling and timeout
838
- */
839
- private async installCommonTools(container: Container, containerImage: string): Promise<Container> {
840
- logger.debug(
841
- `🔧 Installing common tools for container image: ${containerImage}`,
842
- );
843
-
844
- try {
845
- // For node images, most tools are already available, so we can skip installation
846
- if (containerImage.includes("node:")) {
847
- logger.debug(
848
- "📦 Node.js image detected, skipping tool installation (most tools already available)",
849
- );
850
- return container;
851
- }
852
-
853
- // Determine package manager based on base image
854
- const isAlpine = containerImage.includes("alpine");
855
- const isDebian =
856
- containerImage.includes("debian") ||
857
- containerImage.includes("ubuntu");
858
-
859
- if (isAlpine) {
860
- logger.debug("📦 Detected Alpine Linux, installing basic tools");
861
- // Alpine Linux uses apk package manager - with better timeout handling
862
- container = container.withExec([
863
- "sh",
864
- "-c",
865
- 'timeout 60 apk update --no-cache && timeout 60 apk add --no-cache curl wget git || echo "Package installation failed, continuing..."',
866
- ]);
867
- } else if (isDebian) {
868
- logger.debug("📦 Detected Debian/Ubuntu, installing basic tools");
869
- // Debian/Ubuntu uses apt-get - with better timeout and error handling
870
- container = container.withExec([
871
- "sh",
872
- "-c",
873
- 'timeout 60 apt-get update && timeout 60 apt-get install -y curl wget git && rm -rf /var/lib/apt/lists/* || echo "Package installation failed, continuing..."',
874
- ]);
875
- } else {
876
- logger.warn(
877
- `Unknown container image ${containerImage}, skipping tool installation`,
878
- );
879
- }
880
-
881
- logger.debug("✅ Common tools installation complete");
882
- return container;
883
- } catch (error) {
884
- logger.warn(
885
- `⚠️ Tool installation failed, continuing without additional tools: ${error}`,
886
- );
887
- return container;
888
- }
889
- }
890
-
891
- /**
892
- * Execute command in container with enhanced error handling
893
- */
894
- private async executeInContainer(
895
- container: Container,
896
- command: string,
897
- ): Promise<CommandResult> {
898
- logger.debug(`⚡ Executing command in container: ${command}`);
899
-
900
- try {
901
- let execContainer: Container;
902
-
903
- if (this.options.useShell) {
904
- logger.debug("🐚 Using shell wrapper for command execution");
905
- execContainer = container.withExec(["sh", "-c", command]);
906
- } else {
907
- logger.debug("📋 Using direct command execution");
908
- const commandParts = this.parseCommand(command);
909
- execContainer = container.withExec(commandParts);
910
- }
911
-
912
- logger.debug("📤 Getting stdout from container...");
913
- const stdout = await execContainer.stdout();
914
- logger.debug(`📥 Got stdout: ${stdout.length} characters`);
915
-
916
- let stderr = "";
917
- try {
918
- logger.debug("📤 Getting stderr from container...");
919
- stderr = await execContainer.stderr();
920
- logger.debug(`📥 Got stderr: ${stderr.length} characters`);
921
- } catch (e) {
922
- logger.debug(
923
- "📥 stderr not available (this is normal for successful commands)",
924
- );
925
- }
926
-
927
- logger.debug("✅ Command executed successfully");
928
- return {
929
- stdout,
930
- stderr,
931
- exitCode: 0,
932
- };
933
- } catch (error) {
934
- logger.debug(`❌ Command execution failed: ${error}`);
935
- const errorMessage =
936
- error instanceof Error ? error.message : "Command execution failed";
937
- const parsedError = this.parseExecutionError(errorMessage);
938
-
939
- return {
940
- stdout: parsedError.stdout || "",
941
- stderr: parsedError.stderr || errorMessage,
942
- exitCode: parsedError.exitCode || 1,
943
- };
944
- }
945
- }
946
-
947
- /**
948
- * Enhanced execution error parsing
949
- */
950
- private parseExecutionError(errorMessage: string): Partial<CommandResult> {
951
- const result: Partial<CommandResult> = {};
952
-
953
- const exitCodeMatch = errorMessage.match(/exit code:?\s*(\d+)/);
954
- if (exitCodeMatch) {
955
- result.exitCode = parseInt(exitCodeMatch[1], 10);
956
- }
957
-
958
- const stdoutMatch = errorMessage.match(
959
- /(?:stdout|Stdout):\s*\n([\s\S]*?)(?:\n(?:stderr|Stderr):|$)/i,
960
- );
961
- if (stdoutMatch) {
962
- result.stdout = stdoutMatch[1].trim();
963
- }
964
-
965
- const stderrMatch = errorMessage.match(
966
- /(?:stderr|Stderr):\s*\n([\s\S]*)$/i,
967
- );
968
- if (stderrMatch) {
969
- result.stderr = stderrMatch[1].trim();
970
- }
971
-
972
- if (!result.stderr && !result.stdout) {
973
- result.stderr = errorMessage;
974
- }
975
-
976
- if (!result.exitCode) {
977
- result.exitCode = 1;
978
- }
979
-
980
- return result;
981
- }
982
-
983
- /**
984
- * Apply resource limits based on Enact tool specifications
985
- */
986
- private applyResourceLimits(container: Container, resources: any): Container {
987
- if (resources.memory) {
988
- logger.info(
989
- `Resource limit requested: memory=${resources.memory} (not yet supported by Dagger)`,
990
- );
991
- }
992
- if (resources.cpu) {
993
- logger.info(
994
- `Resource limit requested: cpu=${resources.cpu} (not yet supported by Dagger)`,
995
- );
996
- }
997
- return container;
998
- }
999
-
1000
- /**
1001
- * Substitute template variables in Enact commands with enhanced security
1002
- */
1003
- private substituteCommandVariables(
1004
- command: string,
1005
- inputs: Record<string, any>,
1006
- ): string {
1007
- let substitutedCommand = command;
1008
-
1009
- for (const [key, value] of Object.entries(inputs)) {
1010
- const templateVar = `\${${key}}`;
1011
- let substitutionValue: string;
1012
-
1013
- if (typeof value === "string") {
1014
- substitutionValue = this.escapeShellArg(value);
1015
- } else if (typeof value === "object") {
1016
- substitutionValue = this.escapeShellArg(JSON.stringify(value));
1017
- } else {
1018
- substitutionValue = this.escapeShellArg(String(value));
1019
- }
1020
-
1021
- substitutedCommand = substitutedCommand.replace(
1022
- new RegExp(`\\$\\{${key}\\}`, "g"),
1023
- substitutionValue,
1024
- );
1025
- }
1026
-
1027
- return substitutedCommand;
1028
- }
1029
-
1030
- /**
1031
- * Enhanced shell argument escaping
1032
- */
1033
- private escapeShellArg(arg: string): string {
1034
- // For maximum safety, use single quotes and escape any single quotes within
1035
- return "'" + arg.replace(/'/g, "'\"'\"'") + "'";
1036
- }
1037
-
1038
- /**
1039
- * Prepare input files for Enact tools that expect file inputs
1040
- */
1041
- private async prepareInputFiles(
1042
- container: Container,
1043
- inputs: Record<string, any>,
1044
- ): Promise<Container> {
1045
- for (const [key, value] of Object.entries(inputs)) {
1046
- if (typeof value === "string" && this.looksLikeFileContent(key, value)) {
1047
- const fileName = this.getInputFileName(key, value);
1048
- const filePath = `${this.options.workdir}/${fileName}`;
1049
-
1050
- container = container.withNewFile(filePath, { contents: value });
1051
- logger.debug(`📁 Added input file for Enact tool: ${filePath}`);
1052
- }
1053
- }
1054
-
1055
- return container;
1056
- }
1057
-
1058
- /**
1059
- * Heuristics to determine if input should be treated as file content
1060
- */
1061
- private looksLikeFileContent(key: string, value: string): boolean {
1062
- const lowerKey = key.toLowerCase();
1063
- return (
1064
- lowerKey.includes("file") ||
1065
- lowerKey.includes("content") ||
1066
- lowerKey.includes("data") ||
1067
- lowerKey.includes("source") ||
1068
- (lowerKey.includes("input") && value.length > 100) ||
1069
- value.includes("\n") ||
1070
- value.startsWith("data:") ||
1071
- this.hasCommonFileExtensions(value)
1072
- );
1073
- }
1074
-
1075
- /**
1076
- * Check if content looks like common file types
1077
- */
1078
- private hasCommonFileExtensions(value: string): boolean {
1079
- const trimmed = value.trim();
1080
- return (
1081
- (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
1082
- (trimmed.startsWith("<") && trimmed.includes(">")) ||
1083
- trimmed.startsWith("#") ||
1084
- /^---\s*\n/.test(trimmed)
1085
- );
1086
- }
1087
-
1088
- /**
1089
- * Generate appropriate filename for input content
1090
- */
1091
- private getInputFileName(key: string, value: string): string {
1092
- const lowerKey = key.toLowerCase();
1093
- const trimmedValue = value.trim();
1094
-
1095
- if (lowerKey.includes("markdown") || lowerKey.includes("md"))
1096
- return `${key}.md`;
1097
- if (lowerKey.includes("json")) return `${key}.json`;
1098
- if (lowerKey.includes("yaml") || lowerKey.includes("yml"))
1099
- return `${key}.yaml`;
1100
- if (lowerKey.includes("html")) return `${key}.html`;
1101
- if (lowerKey.includes("css")) return `${key}.css`;
1102
- if (lowerKey.includes("js") || lowerKey.includes("javascript"))
1103
- return `${key}.js`;
1104
- if (lowerKey.includes("python") || lowerKey.includes("py"))
1105
- return `${key}.py`;
1106
-
1107
- if (trimmedValue.startsWith("#")) return `${key}.md`;
1108
- if (trimmedValue.startsWith("{") && trimmedValue.endsWith("}"))
1109
- return `${key}.json`;
1110
- if (/^---\s*\n/.test(trimmedValue)) return `${key}.yaml`;
1111
- if (trimmedValue.includes("<html")) return `${key}.html`;
1112
-
1113
- if (trimmedValue.startsWith("data:")) {
1114
- const mimeMatch = trimmedValue.match(/^data:([^;]+)/);
1115
- if (mimeMatch) {
1116
- const ext = this.getExtensionFromMimeType(mimeMatch[1]);
1117
- return `${key}${ext}`;
1118
- }
1119
- }
1120
-
1121
- return `${key}.txt`;
1122
- }
1123
-
1124
- /**
1125
- * Map MIME types to file extensions
1126
- */
1127
- private getExtensionFromMimeType(mimeType: string): string {
1128
- const mimeMap: Record<string, string> = {
1129
- "text/plain": ".txt",
1130
- "text/markdown": ".md",
1131
- "text/html": ".html",
1132
- "text/css": ".css",
1133
- "application/json": ".json",
1134
- "application/javascript": ".js",
1135
- "text/javascript": ".js",
1136
- "application/yaml": ".yaml",
1137
- "text/yaml": ".yaml",
1138
- "text/x-python": ".py",
1139
- "application/x-python-code": ".py",
1140
- "image/png": ".png",
1141
- "image/jpeg": ".jpg",
1142
- "image/gif": ".gif",
1143
- "image/svg+xml": ".svg",
1144
- };
1145
-
1146
- return mimeMap[mimeType] || ".txt";
1147
- }
1148
-
1149
- /**
1150
- * Enhanced command parsing for non-shell execution
1151
- */
1152
- private parseCommand(command: string): string[] {
1153
- const args: string[] = [];
1154
- let current = "";
1155
- let inQuotes = false;
1156
- let quoteChar = "";
1157
-
1158
- for (let i = 0; i < command.length; i++) {
1159
- const char = command[i];
1160
-
1161
- if ((char === '"' || char === "'") && !inQuotes) {
1162
- inQuotes = true;
1163
- quoteChar = char;
1164
- } else if (char === quoteChar && inQuotes) {
1165
- inQuotes = false;
1166
- quoteChar = "";
1167
- } else if (char === " " && !inQuotes) {
1168
- if (current) {
1169
- args.push(current);
1170
- current = "";
1171
- }
1172
- } else {
1173
- current += char;
1174
- }
1175
- }
1176
-
1177
- if (current) {
1178
- args.push(current);
1179
- }
1180
-
1181
- return args;
1182
- }
1183
-
1184
- /**
1185
- * Enhanced timeout promise with abort signal support
1186
- */
1187
- private createTimeoutPromise(timeoutMs: number): Promise<never> {
1188
- return new Promise((_, reject) => {
1189
- const timeoutId = setTimeout(() => {
1190
- reject(new Error("TIMEOUT"));
1191
- }, timeoutMs);
1192
-
1193
- // Clear timeout if aborted
1194
- if (this.abortController) {
1195
- this.abortController.signal.addEventListener("abort", () => {
1196
- clearTimeout(timeoutId);
1197
- reject(new Error("ABORTED"));
1198
- });
1199
- }
1200
- });
1201
- }
1202
-
1203
- /**
1204
- * Parse command output according to Enact tool output schema
1205
- */
1206
- private parseOutput(stdout: string, tool: EnactTool): any {
1207
- if (!stdout.trim()) {
1208
- return null;
1209
- }
1210
-
1211
- if (tool.outputSchema) {
1212
- try {
1213
- const parsed = JSON.parse(stdout);
1214
- // TODO: Validate against outputSchema if validation library is available
1215
- return parsed;
1216
- } catch {
1217
- logger.warn(
1218
- `Tool ${tool.name} has outputSchema but produced non-JSON output`,
1219
- );
1220
- return stdout;
1221
- }
1222
- }
1223
-
1224
- try {
1225
- return JSON.parse(stdout);
1226
- } catch {
1227
- return stdout;
1228
- }
1229
- }
1230
-
1231
- /**
1232
- * Execute command with exec.ts style interface for backwards compatibility
1233
- */
1234
- async executeCommandExecStyle(
1235
- command: string,
1236
- timeout: string,
1237
- verbose: boolean = false,
1238
- envVars: Record<string, string> = {},
1239
- ): Promise<void> {
1240
- const environment: ExecutionEnvironment = {
1241
- vars: envVars,
1242
- resources: { timeout },
1243
- };
1244
-
1245
- const result = await this.executeCommand(
1246
- command,
1247
- {},
1248
- environment,
1249
- timeout,
1250
- {
1251
- verbose,
1252
- showSpinner: true,
1253
- streamOutput: false,
1254
- },
1255
- undefined, // no tool parameter for backwards compatibility
1256
- );
1257
-
1258
- if (result.exitCode !== 0) {
1259
- throw new Error(
1260
- `Command failed with exit code ${result.exitCode}: ${result.stderr}`,
1261
- );
1262
- }
1263
- }
1264
-
1265
- /**
1266
- * Enhanced cleanup handlers with graceful shutdown
1267
- */
1268
- private registerCleanupHandlers(): void {
1269
- const cleanup = () => {
1270
- if (!this.isShuttingDown) {
1271
- this.gracefulShutdown();
1272
- }
1273
- };
1274
-
1275
- // Register multiple signal handlers for comprehensive cleanup
1276
- process.once("SIGTERM", cleanup);
1277
- process.once("SIGINT", cleanup);
1278
- process.once("SIGUSR2", cleanup); // For nodemon
1279
- process.once("exit", cleanup);
1280
-
1281
- // Handle unhandled promise rejections
1282
- process.once("unhandledRejection", (reason, promise) => {
1283
- logger.error("Unhandled Rejection at:", promise, "reason:", reason);
1284
- cleanup();
1285
- });
1286
-
1287
- // Handle uncaught exceptions
1288
- process.once("uncaughtException", (error) => {
1289
- logger.error("Uncaught Exception:", error);
1290
- cleanup();
1291
- });
1292
- }
1293
-
1294
- /**
1295
- * Graceful shutdown with proper async cleanup
1296
- */
1297
- private async gracefulShutdown(): Promise<void> {
1298
- this.isShuttingDown = true;
1299
-
1300
- try {
1301
- logger.info("🔄 Starting graceful shutdown...");
1302
-
1303
- // Cancel any active operations
1304
- if (this.abortController) {
1305
- this.abortController.abort();
1306
- }
1307
-
1308
- // Wait for active sessions with timeout
1309
- await this.waitForActiveSessions(10000);
1310
-
1311
- // Perform comprehensive cleanup
1312
- await this.cleanup();
1313
-
1314
- logger.info("✅ Graceful shutdown completed");
1315
- process.exit(0);
1316
- } catch (error) {
1317
- logger.error("❌ Error during graceful shutdown:", error);
1318
- process.exit(1);
1319
- }
1320
- }
1321
-
1322
-
1323
-
1324
- /**
1325
- * Get current engine status for debugging
1326
- */
1327
- public getEngineStatus(): {
1328
- health: EngineHealthStatus;
1329
- activeSessions: number;
1330
- isShuttingDown: boolean;
1331
- } {
1332
- return {
1333
- health: { ...this.engineHealth },
1334
- activeSessions: this.activeSessions.size,
1335
- isShuttingDown: this.isShuttingDown,
1336
- };
1337
- }
1338
-
1339
- /**
1340
- * Manually trigger engine reset (for debugging/testing)
1341
- */
1342
- public async resetEngine(): Promise<void> {
1343
- logger.info("🔄 Manual engine reset triggered...");
1344
- await this.resetEngineContainer();
1345
- }
1346
-
1347
- /**
1348
- * Check if provider is ready for new executions
1349
- */
1350
- public isReady(): boolean {
1351
- return (
1352
- !this.isShuttingDown &&
1353
- this.engineHealth.isHealthy &&
1354
- this.engineHealth.consecutiveFailures < 3
1355
- );
1356
- }
1357
- }