@172ai/containers-mcp-server 1.7.0 → 1.7.2

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.
@@ -4,6 +4,7 @@ exports.buildService = exports.BuildService = void 0;
4
4
  const auth_1 = require("../auth");
5
5
  const errorHandler_1 = require("../utils/errorHandler");
6
6
  const enhancedErrorHandler_1 = require("../utils/enhancedErrorHandler");
7
+ const streamingService_1 = require("./streamingService");
7
8
  /**
8
9
  * Generate a unique request ID
9
10
  */
@@ -13,8 +14,9 @@ function generateRequestId() {
13
14
  class BuildService {
14
15
  constructor() {
15
16
  this.mcpServer = null;
16
- // Phase 3: Multi-build stream management
17
- this.activeMonitors = new Map();
17
+ // Real-time streaming management (replaces polling)
18
+ this.activeStreams = new Map();
19
+ this.progressUpdates = new Map();
18
20
  this.streamAnalytics = {
19
21
  totalBuilds: 0,
20
22
  activeBuilds: 0,
@@ -25,8 +27,6 @@ class BuildService {
25
27
  errorsEncountered: 0,
26
28
  lastActivity: new Date().toISOString()
27
29
  };
28
- this.maxConcurrentBuilds = 10;
29
- this.globalMonitoringTimeout = 900000; // 15 minutes
30
30
  this.httpClient = auth_1.authManager.getHttpClient();
31
31
  }
32
32
  /**
@@ -36,292 +36,79 @@ class BuildService {
36
36
  this.mcpServer = server;
37
37
  }
38
38
  /**
39
- * Phase 3: Get stream analytics and monitoring information
39
+ * Get stream analytics and monitoring information
40
40
  */
41
41
  getStreamAnalytics() {
42
42
  return {
43
43
  ...this.streamAnalytics,
44
- activeMonitors: Array.from(this.activeMonitors.values())
44
+ activeStreams: Array.from(this.activeStreams.values())
45
45
  };
46
46
  }
47
47
  /**
48
- * Phase 3: Clean up completed or failed monitors
48
+ * Get progress update for a specific token
49
49
  */
50
- cleanupInactiveMonitors() {
51
- const now = Date.now();
52
- for (const [token, monitor] of this.activeMonitors.entries()) {
53
- if (monitor.status !== 'active' ||
54
- (now - monitor.startTime) > this.globalMonitoringTimeout) {
55
- if (monitor.timeoutHandle) {
56
- clearTimeout(monitor.timeoutHandle);
57
- }
58
- this.activeMonitors.delete(token);
59
- this.streamAnalytics.activeBuilds = Math.max(0, this.streamAnalytics.activeBuilds - 1);
60
- }
50
+ getProgressUpdate(progressToken) {
51
+ const update = this.progressUpdates.get(progressToken);
52
+ if (!update)
53
+ return null;
54
+ // Check if expired (clean up stale data)
55
+ if (update.expiresAt && Date.now() > update.expiresAt) {
56
+ this.progressUpdates.delete(progressToken);
57
+ return null;
61
58
  }
59
+ return update;
62
60
  }
63
61
  /**
64
- * Phase 3: Cancel a specific build monitor
62
+ * Get all progress updates for debugging
65
63
  */
66
- async cancelBuildMonitoring(progressToken, reason = 'User requested cancellation') {
67
- const monitor = this.activeMonitors.get(progressToken);
68
- if (!monitor) {
69
- return false;
70
- }
71
- monitor.status = 'cancelled';
72
- await this.sendProgressNotification(progressToken, monitor.lastProgress, 100, `Build monitoring cancelled: ${reason}`);
73
- if (monitor.timeoutHandle) {
74
- clearTimeout(monitor.timeoutHandle);
64
+ getAllProgressUpdates() {
65
+ const updates = {};
66
+ for (const [token, data] of this.progressUpdates.entries()) {
67
+ updates[token] = data;
75
68
  }
76
- this.activeMonitors.delete(progressToken);
77
- this.streamAnalytics.activeBuilds = Math.max(0, this.streamAnalytics.activeBuilds - 1);
78
- console.log(`[MCP] Cancelled monitoring for build ${monitor.buildId} with token ${progressToken}`);
79
- return true;
69
+ return updates;
80
70
  }
81
71
  /**
82
- * Send MCP progress notification with Phase 3 enhancements
72
+ * Cancel build monitoring for a specific token
83
73
  */
84
- async sendProgressNotification(progressToken, progress, total = 100, message, metadata) {
85
- if (!this.mcpServer || !progressToken) {
86
- return;
74
+ async cancelBuildMonitoring(progressToken, reason = 'User requested cancellation') {
75
+ const stream = this.activeStreams.get(progressToken);
76
+ if (!stream) {
77
+ return false;
87
78
  }
79
+ console.log(`[Build] Cancelling monitoring for build ${stream.buildId} with token ${progressToken}`);
88
80
  try {
89
- // Phase 3: Enhanced notification with metadata
90
- const notificationParams = {
91
- progressToken,
92
- progress,
93
- total,
94
- message,
95
- timestamp: new Date().toISOString(),
96
- ...(metadata && { metadata })
97
- };
98
- await this.mcpServer.notification({
99
- method: 'notifications/progress',
100
- params: notificationParams
101
- });
102
- // Phase 3: Update analytics
103
- this.streamAnalytics.notificationsSent++;
104
- this.streamAnalytics.lastActivity = new Date().toISOString();
105
- console.log(`[MCP] Progress notification sent: ${progress}/${total} - ${message}`);
81
+ // Update progress with cancellation notice
82
+ await this.storeProgressUpdate(progressToken, stream.lastProgress, 100, `Build monitoring cancelled: ${reason}`);
83
+ // Unsubscribe from stream
84
+ await streamingService_1.streamingService.unsubscribeFromStream(stream.streamId);
85
+ // Cleanup
86
+ this.completeStream(progressToken, 'cancelled');
87
+ return true;
106
88
  }
107
89
  catch (error) {
108
- console.error(`[MCP] Failed to send progress notification:`, error);
109
- this.streamAnalytics.errorsEncountered++;
110
- // Phase 3: Enhanced error handling - attempt retry for critical notifications
111
- if (progress === 100 || progress === 0) {
112
- console.log(`[MCP] Retrying critical notification for ${progressToken}`);
113
- setTimeout(() => {
114
- this.sendProgressNotification(progressToken, progress, total, `[RETRY] ${message}`, metadata);
115
- }, 2000);
116
- }
117
- }
118
- }
119
- /**
120
- * Phase 3: Enhanced build progress monitoring with multi-stream management
121
- */
122
- startBuildProgressMonitoring(containerId, buildId, progressToken) {
123
- // Phase 3: Check for concurrent build limits
124
- this.cleanupInactiveMonitors();
125
- if (this.activeMonitors.size >= this.maxConcurrentBuilds) {
126
- console.log(`[MCP] Maximum concurrent builds reached (${this.maxConcurrentBuilds}), queuing build ${buildId}`);
127
- this.sendProgressNotification(progressToken, 5, 100, `Build queued - ${this.activeMonitors.size} builds in progress`);
128
- // In a real implementation, you might want to queue this build
129
- return;
130
- }
131
- // Phase 3: Create monitor entry
132
- const monitor = {
133
- containerId,
134
- buildId,
135
- progressToken,
136
- startTime: Date.now(),
137
- lastProgress: 10,
138
- lastLogCount: 0,
139
- status: 'active',
140
- retryCount: 0
141
- };
142
- this.activeMonitors.set(progressToken, monitor);
143
- this.streamAnalytics.totalBuilds++;
144
- this.streamAnalytics.activeBuilds++;
145
- // Run monitoring in background without blocking the main response
146
- setImmediate(async () => {
147
- console.log(`[MCP] Starting enhanced progress monitoring for build ${buildId} with token ${progressToken}`);
148
- const enhancedMonitor = async () => {
149
- const currentMonitor = this.activeMonitors.get(progressToken);
150
- if (!currentMonitor || currentMonitor.status !== 'active') {
151
- return; // Monitor was cancelled or completed
152
- }
153
- try {
154
- // Phase 3: Enhanced timeout handling
155
- const elapsed = Date.now() - currentMonitor.startTime;
156
- if (elapsed > this.globalMonitoringTimeout) {
157
- console.log(`[MCP] Build monitoring timeout for ${buildId} after ${elapsed}ms`);
158
- currentMonitor.status = 'error';
159
- await this.sendProgressNotification(progressToken, 100, 100, 'Build monitoring timeout - check build status manually', {
160
- elapsedTime: elapsed,
161
- reason: 'timeout'
162
- });
163
- this.completeMonitoring(progressToken, 'timeout');
164
- return;
165
- }
166
- // Phase 3: Smart progress calculation with retry logic
167
- let buildStatus;
168
- try {
169
- buildStatus = await this.getBuildStatus({ containerId, buildId });
170
- }
171
- catch (statusError) {
172
- currentMonitor.retryCount++;
173
- if (currentMonitor.retryCount > 3) {
174
- currentMonitor.status = 'error';
175
- await this.sendProgressNotification(progressToken, currentMonitor.lastProgress, 100, `Build status check failed after ${currentMonitor.retryCount} retries`);
176
- this.completeMonitoring(progressToken, 'api_error');
177
- return;
178
- }
179
- console.log(`[MCP] Build status check failed (attempt ${currentMonitor.retryCount}), retrying...`);
180
- setTimeout(enhancedMonitor, 5000); // Exponential backoff would be better
181
- return;
182
- }
183
- // Reset retry count on successful API call
184
- currentMonitor.retryCount = 0;
185
- // Phase 3: Enhanced progress calculation
186
- const apiProgress = buildStatus.progress || 0;
187
- const timeBasedProgress = Math.min((elapsed / 300000) * 80, 80); // 80% progress over 5 minutes
188
- const combinedProgress = Math.max(apiProgress, timeBasedProgress, currentMonitor.lastProgress);
189
- // Send progress update if changed significantly
190
- if (combinedProgress > currentMonitor.lastProgress + 5 ||
191
- buildStatus.status !== currentMonitor.lastStatus) {
192
- const progressMetadata = {
193
- buildStatus: buildStatus.status,
194
- elapsedTime: elapsed,
195
- apiProgress,
196
- timeBasedProgress,
197
- buildId,
198
- containerId
199
- };
200
- await this.sendProgressNotification(progressToken, Math.min(combinedProgress, 99), // Cap at 99% until completion
201
- 100, `Build ${buildStatus.status}: ${Math.round(combinedProgress)}% (${Math.round(elapsed / 1000)}s elapsed)`, progressMetadata);
202
- currentMonitor.lastProgress = combinedProgress;
203
- currentMonitor.lastStatus = buildStatus.status;
204
- }
205
- // Phase 3: Enhanced completion detection
206
- if (buildStatus.status === 'completed') {
207
- currentMonitor.status = 'completed';
208
- const completionTime = Date.now() - currentMonitor.startTime;
209
- await this.sendProgressNotification(progressToken, 100, 100, `Build completed successfully! (${Math.round(completionTime / 1000)}s total)`, {
210
- completionTime,
211
- buildId,
212
- finalStatus: 'completed'
213
- });
214
- this.completeMonitoring(progressToken, 'completed', completionTime);
215
- return;
216
- }
217
- if (buildStatus.status === 'failed') {
218
- currentMonitor.status = 'failed';
219
- await this.sendProgressNotification(progressToken, currentMonitor.lastProgress, 100, `Build failed: ${buildStatus.error || 'Unknown error'}`, {
220
- error: buildStatus.error,
221
- buildId,
222
- finalStatus: 'failed'
223
- });
224
- this.completeMonitoring(progressToken, 'failed');
225
- return;
226
- }
227
- if (buildStatus.status === 'cancelled') {
228
- currentMonitor.status = 'cancelled';
229
- await this.sendProgressNotification(progressToken, currentMonitor.lastProgress, 100, 'Build was cancelled', {
230
- buildId,
231
- finalStatus: 'cancelled'
232
- });
233
- this.completeMonitoring(progressToken, 'cancelled');
234
- return;
235
- }
236
- // Phase 3: Enhanced log-based progress estimation
237
- try {
238
- const logs = await this.getBuildLogs(containerId, buildId);
239
- if (logs.length > currentMonitor.lastLogCount) {
240
- const newLogCount = logs.length - currentMonitor.lastLogCount;
241
- currentMonitor.lastLogCount = logs.length;
242
- // More sophisticated progress estimation
243
- const logBasedProgress = Math.min(20 + (logs.length * 1.5), 90);
244
- if (logBasedProgress > currentMonitor.lastProgress) {
245
- await this.sendProgressNotification(progressToken, logBasedProgress, 100, `Build in progress - ${newLogCount} new log entries (${logs.length} total)`, { logCount: logs.length, newLogs: newLogCount });
246
- currentMonitor.lastProgress = logBasedProgress;
247
- }
248
- }
249
- }
250
- catch (logError) {
251
- // Log errors are not critical for progress monitoring
252
- console.log(`[MCP] Could not fetch logs for progress estimation: ${logError.message || 'Unknown error'}`);
253
- }
254
- // Continue monitoring if build is still running
255
- if (['pending', 'running', 'in_progress'].includes(buildStatus.status)) {
256
- // Phase 3: Adaptive polling interval based on build age and activity
257
- const adaptiveInterval = elapsed > 180000 ? 5000 : 3000; // Slower polling for older builds
258
- currentMonitor.timeoutHandle = setTimeout(enhancedMonitor, adaptiveInterval);
259
- }
260
- }
261
- catch (error) {
262
- console.error(`[MCP] Error in enhanced build progress monitoring:`, error);
263
- currentMonitor.retryCount++;
264
- this.streamAnalytics.errorsEncountered++;
265
- if (currentMonitor.retryCount <= 3) {
266
- await this.sendProgressNotification(progressToken, currentMonitor.lastProgress, 100, `Progress monitoring error (attempt ${currentMonitor.retryCount}) - retrying...`);
267
- setTimeout(enhancedMonitor, 10000); // Wait 10s before retry
268
- }
269
- else {
270
- currentMonitor.status = 'error';
271
- await this.sendProgressNotification(progressToken, currentMonitor.lastProgress, 100, 'Progress monitoring failed after multiple retries - build may still be running');
272
- this.completeMonitoring(progressToken, 'monitoring_error');
273
- }
274
- }
275
- };
276
- // Start enhanced monitoring
277
- await enhancedMonitor();
278
- });
279
- }
280
- /**
281
- * Phase 3: Complete monitoring and update analytics
282
- */
283
- completeMonitoring(progressToken, reason, completionTime) {
284
- const monitor = this.activeMonitors.get(progressToken);
285
- if (monitor) {
286
- if (monitor.timeoutHandle) {
287
- clearTimeout(monitor.timeoutHandle);
288
- }
289
- this.activeMonitors.delete(progressToken);
290
- this.streamAnalytics.activeBuilds = Math.max(0, this.streamAnalytics.activeBuilds - 1);
291
- // Update completion statistics
292
- if (reason === 'completed') {
293
- this.streamAnalytics.completedBuilds++;
294
- if (completionTime) {
295
- const currentAvg = this.streamAnalytics.averageBuildTime;
296
- const totalCompleted = this.streamAnalytics.completedBuilds;
297
- this.streamAnalytics.averageBuildTime =
298
- ((currentAvg * (totalCompleted - 1)) + completionTime) / totalCompleted;
299
- }
300
- }
301
- else if (reason === 'failed' || reason === 'monitoring_error' || reason === 'api_error') {
302
- this.streamAnalytics.failedBuilds++;
303
- }
304
- console.log(`[MCP] Monitoring completed for ${monitor.buildId} (${reason})`);
90
+ console.error(`[Build] Error cancelling monitoring for token ${progressToken}:`, error);
91
+ return false;
305
92
  }
306
93
  }
307
94
  /**
308
- * Start a container build with optional progress tracking
95
+ * Start a container build with real-time streaming progress
309
96
  */
310
97
  async buildContainer(params, progressToken) {
311
98
  try {
312
99
  if (!params.containerId) {
313
100
  throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
314
101
  }
315
- // Phase 2: Initialize progress tracking with MCP notifications
102
+ // Initialize progress tracking with streaming notifications
316
103
  if (progressToken) {
317
- console.log(`[MCP] Build started with progress token: ${progressToken} for container: ${params.containerId}`);
318
- await this.sendProgressNotification(progressToken, 0, 100, 'Build initialization started');
104
+ console.log(`[Build] Starting build with streaming progress token: ${progressToken} for container: ${params.containerId}`);
105
+ await this.storeProgressUpdate(progressToken, 0, 100, 'Build initialization started');
319
106
  }
320
107
  const buildRequest = {
321
108
  buildArgs: params.buildArgs || {},
322
109
  target: params.target
323
110
  };
324
- // Use the correct endpoint: POST /v1/containers/{id}/build (not builds)
111
+ // Start the build via API
325
112
  const response = await this.httpClient.post(`/v1/containers/${params.containerId}/build`, buildRequest);
326
113
  if (!response.data) {
327
114
  throw errorHandler_1.ErrorHandler.createServerError('Invalid response from build start API');
@@ -338,11 +125,11 @@ class BuildService {
338
125
  error: data.error,
339
126
  logs: data.logs
340
127
  };
341
- // Phase 2: Send progress notification for build started
128
+ // Set up real-time streaming for progress updates
342
129
  if (progressToken) {
343
- await this.sendProgressNotification(progressToken, 10, 100, `Build started - ID: ${buildResult.id}`);
344
- // Start monitoring build progress in background
345
- this.startBuildProgressMonitoring(params.containerId, buildResult.id, progressToken);
130
+ await this.storeProgressUpdate(progressToken, 10, 100, `Build started - ID: ${buildResult.id}`);
131
+ // Create streaming subscription instead of polling
132
+ await this.setupStreamingMonitoring(params.containerId, buildResult.id, progressToken);
346
133
  }
347
134
  return buildResult;
348
135
  }
@@ -374,6 +161,195 @@ class BuildService {
374
161
  throw new errorHandler_1.ApiError(enhancedError.message, enhancedError.status, enhancedError.code, enhancedError, context.requestId, context.operation);
375
162
  }
376
163
  }
164
+ /**
165
+ * Set up real-time streaming monitoring for build progress
166
+ * Replaces the old polling-based monitoring system
167
+ */
168
+ async setupStreamingMonitoring(containerId, buildId, progressToken) {
169
+ try {
170
+ console.log(`[Build] Setting up real-time streaming for build ${buildId} with token ${progressToken}`);
171
+ // Create build stream
172
+ const streamSession = await streamingService_1.streamingService.createStream({
173
+ streamType: 'build',
174
+ resourceId: containerId,
175
+ eventFilters: [
176
+ 'build-started',
177
+ 'build-step-completed',
178
+ 'build-completed',
179
+ 'build-failed',
180
+ 'build-cancelled'
181
+ ],
182
+ ttlMinutes: 120, // 2 hours
183
+ metadata: {
184
+ buildId,
185
+ progressToken
186
+ }
187
+ });
188
+ // Track active stream
189
+ const activeStream = {
190
+ containerId,
191
+ buildId,
192
+ progressToken,
193
+ streamId: streamSession.streamId,
194
+ startTime: Date.now(),
195
+ lastProgress: 10,
196
+ status: 'active'
197
+ };
198
+ this.activeStreams.set(progressToken, activeStream);
199
+ this.streamAnalytics.totalBuilds++;
200
+ this.streamAnalytics.activeBuilds++;
201
+ // Subscribe to stream events
202
+ await streamingService_1.streamingService.subscribeToStream(streamSession.streamId, (event) => {
203
+ this.handleBuildStreamEvent(event, progressToken);
204
+ });
205
+ console.log(`[Build] Real-time streaming active for build ${buildId} (stream: ${streamSession.streamId})`);
206
+ }
207
+ catch (error) {
208
+ console.error(`[Build] Failed to setup streaming monitoring:`, error);
209
+ // Fallback to basic progress tracking if streaming fails
210
+ await this.storeProgressUpdate(progressToken, 10, 100, 'Build started - streaming unavailable, check build status manually');
211
+ // Don't throw - let build continue even if streaming fails
212
+ }
213
+ }
214
+ /**
215
+ * Handle real-time build stream events
216
+ */
217
+ async handleBuildStreamEvent(event, progressToken) {
218
+ try {
219
+ const stream = this.activeStreams.get(progressToken);
220
+ if (!stream) {
221
+ console.warn(`[Build] Received event for unknown progress token: ${progressToken}`);
222
+ return;
223
+ }
224
+ console.log(`[Build] Processing ${event.eventType} for build ${stream.buildId}: ${event.data.message}`);
225
+ // Convert stream event to progress update
226
+ const progressUpdate = this.convertStreamEventToProgress(event, progressToken, stream);
227
+ // Store progress update
228
+ await this.storeProgressUpdate(progressToken, progressUpdate.progress, progressUpdate.total, progressUpdate.message, progressUpdate.metadata);
229
+ // Update stream tracking
230
+ stream.lastProgress = progressUpdate.progress;
231
+ // Handle completion events
232
+ if (['build-completed', 'build-failed', 'build-cancelled'].includes(event.eventType)) {
233
+ const completionTime = Date.now() - stream.startTime;
234
+ this.completeStream(progressToken, event.eventType.replace('build-', ''), completionTime);
235
+ }
236
+ }
237
+ catch (error) {
238
+ console.error(`[Build] Error handling stream event:`, error);
239
+ this.streamAnalytics.errorsEncountered++;
240
+ }
241
+ }
242
+ /**
243
+ * Convert stream event to progress update format
244
+ */
245
+ convertStreamEventToProgress(event, progressToken, stream) {
246
+ const baseProgress = {
247
+ progressToken,
248
+ total: 100,
249
+ timestamp: event.timestamp,
250
+ metadata: {
251
+ eventType: event.eventType,
252
+ buildId: stream.buildId,
253
+ containerId: stream.containerId,
254
+ streamId: stream.streamId,
255
+ ...event.data.details
256
+ }
257
+ };
258
+ switch (event.eventType) {
259
+ case 'build-started':
260
+ return {
261
+ ...baseProgress,
262
+ progress: 0,
263
+ message: event.data.message || 'Build started'
264
+ };
265
+ case 'build-step-completed':
266
+ return {
267
+ ...baseProgress,
268
+ progress: event.data.progress || 0,
269
+ message: event.data.message || `Step ${event.data.stepNumber}/${event.data.totalSteps} completed`
270
+ };
271
+ case 'build-completed':
272
+ return {
273
+ ...baseProgress,
274
+ progress: 100,
275
+ message: event.data.message || 'Build completed successfully!'
276
+ };
277
+ case 'build-failed':
278
+ return {
279
+ ...baseProgress,
280
+ progress: Math.max(stream.lastProgress, 0),
281
+ message: event.data.message || 'Build failed'
282
+ };
283
+ case 'build-cancelled':
284
+ return {
285
+ ...baseProgress,
286
+ progress: Math.max(stream.lastProgress, 0),
287
+ message: event.data.message || 'Build cancelled'
288
+ };
289
+ default:
290
+ return {
291
+ ...baseProgress,
292
+ progress: Math.max(stream.lastProgress, 0),
293
+ message: event.data.message || `Unknown event: ${event.eventType}`
294
+ };
295
+ }
296
+ }
297
+ /**
298
+ * Store progress update with expiration
299
+ */
300
+ async storeProgressUpdate(progressToken, progress, total = 100, message, metadata) {
301
+ if (!progressToken)
302
+ return;
303
+ try {
304
+ const notificationData = {
305
+ progressToken,
306
+ progress,
307
+ total,
308
+ message,
309
+ timestamp: new Date().toISOString(),
310
+ expiresAt: Date.now() + (30 * 60 * 1000), // 30-minute expiration
311
+ ...(metadata && { metadata })
312
+ };
313
+ this.progressUpdates.set(progressToken, notificationData);
314
+ this.streamAnalytics.notificationsSent++;
315
+ this.streamAnalytics.lastActivity = new Date().toISOString();
316
+ console.log(`[Build] Progress stored: ${progress}/${total} - ${message} (token: ${progressToken})`);
317
+ }
318
+ catch (error) {
319
+ console.error(`[Build] Failed to store progress update:`, error);
320
+ this.streamAnalytics.errorsEncountered++;
321
+ }
322
+ }
323
+ /**
324
+ * Complete stream monitoring and update analytics
325
+ */
326
+ completeStream(progressToken, reason, completionTime) {
327
+ const stream = this.activeStreams.get(progressToken);
328
+ if (!stream)
329
+ return;
330
+ // Unsubscribe from stream
331
+ streamingService_1.streamingService.unsubscribeFromStream(stream.streamId).catch(error => {
332
+ console.error(`[Build] Error unsubscribing from stream ${stream.streamId}:`, error);
333
+ });
334
+ // Update analytics
335
+ this.streamAnalytics.activeBuilds = Math.max(0, this.streamAnalytics.activeBuilds - 1);
336
+ if (reason === 'completed') {
337
+ this.streamAnalytics.completedBuilds++;
338
+ if (completionTime) {
339
+ const currentAvg = this.streamAnalytics.averageBuildTime;
340
+ const totalCompleted = this.streamAnalytics.completedBuilds;
341
+ this.streamAnalytics.averageBuildTime =
342
+ ((currentAvg * (totalCompleted - 1)) + completionTime) / totalCompleted;
343
+ }
344
+ }
345
+ else if (reason === 'failed') {
346
+ this.streamAnalytics.failedBuilds++;
347
+ }
348
+ // Clean up
349
+ this.activeStreams.delete(progressToken);
350
+ console.log(`[Build] Stream monitoring completed for ${stream.buildId} (${reason})`);
351
+ }
352
+ // Keep existing API methods unchanged for backward compatibility
377
353
  /**
378
354
  * Get build status
379
355
  */
@@ -382,14 +358,11 @@ class BuildService {
382
358
  if (!params.containerId) {
383
359
  throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
384
360
  }
385
- // Use the build logs endpoint to get build status
386
- // The server provides build logs at /v1/containers/{id}/builds/{buildId}
387
361
  let endpoint = `/v1/containers/${params.containerId}/builds`;
388
362
  if (params.buildId) {
389
363
  endpoint += `/${params.buildId}`;
390
364
  }
391
365
  else {
392
- // List all builds for the container and get the latest
393
366
  const builds = await this.listBuilds({ containerId: params.containerId, limit: 1 });
394
367
  if (builds.builds.length === 0) {
395
368
  throw errorHandler_1.ErrorHandler.createNotFoundError(`No builds found for container ${params.containerId}`);
@@ -420,7 +393,6 @@ class BuildService {
420
393
  async listBuilds(params = {}) {
421
394
  try {
422
395
  if (!params.containerId) {
423
- // No global builds endpoint available, return empty list
424
396
  return {
425
397
  builds: [],
426
398
  total: 0,
@@ -436,14 +408,12 @@ class BuildService {
436
408
  queryParams.append('limit', params.limit.toString());
437
409
  if (params.offset)
438
410
  queryParams.append('offset', params.offset.toString());
439
- // Use the container-specific builds endpoint (for build logs)
440
411
  const endpoint = `/v1/containers/${params.containerId}/builds`;
441
412
  const response = await this.httpClient.get(`${endpoint}?${queryParams.toString()}`);
442
413
  if (!response.data) {
443
414
  throw errorHandler_1.ErrorHandler.createServerError('Invalid response format from builds list API');
444
415
  }
445
416
  const data = response.data;
446
- // Handle case where response is an array directly (build logs)
447
417
  let builds = [];
448
418
  let total = 0;
449
419
  if (Array.isArray(data)) {
@@ -455,7 +425,6 @@ class BuildService {
455
425
  total = data.total || data.builds.length;
456
426
  }
457
427
  else {
458
- // No builds found
459
428
  builds = [];
460
429
  total = 0;
461
430
  }
@@ -469,7 +438,6 @@ class BuildService {
469
438
  }
470
439
  catch (error) {
471
440
  if (error instanceof errorHandler_1.ApiError && error.status === 404) {
472
- // No builds found, return empty list
473
441
  return {
474
442
  builds: [],
475
443
  total: 0,
@@ -489,19 +457,15 @@ class BuildService {
489
457
  if (!containerId) {
490
458
  throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
491
459
  }
492
- // The logs are actually contained within the build details response
493
- // Use the build details endpoint instead of a separate logs endpoint
494
460
  let endpoint = `/v1/containers/${containerId}/builds`;
495
461
  if (buildId) {
496
462
  endpoint += `/${buildId}`;
497
463
  }
498
464
  else {
499
- // Get the latest build first
500
465
  const builds = await this.listBuilds({ containerId, limit: 1 });
501
466
  if (builds.builds.length === 0) {
502
- return []; // No builds found
467
+ return [];
503
468
  }
504
- // Use the latest build ID
505
469
  buildId = builds.builds[0].id;
506
470
  endpoint += `/${buildId}`;
507
471
  }
@@ -509,13 +473,11 @@ class BuildService {
509
473
  if (!response.data) {
510
474
  return [];
511
475
  }
512
- // Handle the API response format where logs are in the 'entries' field
513
476
  const buildData = response.data;
514
477
  const entries = buildData.entries || [];
515
478
  if (!Array.isArray(entries)) {
516
- return []; // No logs available yet
479
+ return [];
517
480
  }
518
- // Transform entries to BuildLogEntry format
519
481
  return entries.map((entry, index) => ({
520
482
  id: entry.id || `${containerId}-${buildId}-${index}`,
521
483
  containerId: containerId,
@@ -529,135 +491,11 @@ class BuildService {
529
491
  }
530
492
  catch (error) {
531
493
  if (error instanceof errorHandler_1.ApiError && error.status === 404) {
532
- return []; // No logs available yet
494
+ return [];
533
495
  }
534
496
  throw errorHandler_1.ErrorHandler.processError(error, 'BuildService.getBuildLogs');
535
497
  }
536
498
  }
537
- /**
538
- * Cancel a running build
539
- */
540
- async cancelBuild(containerId, buildId) {
541
- try {
542
- if (!containerId) {
543
- throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
544
- }
545
- if (!buildId) {
546
- throw errorHandler_1.ErrorHandler.createValidationError('Build ID is required');
547
- }
548
- await this.httpClient.post(`/v1/containers/${containerId}/builds/${buildId}/cancel`);
549
- return {
550
- success: true,
551
- message: `Build ${buildId} cancelled successfully`
552
- };
553
- }
554
- catch (error) {
555
- if (error instanceof errorHandler_1.ApiError && error.status === 404) {
556
- throw errorHandler_1.ErrorHandler.createNotFoundError(`Build ${buildId} not found`);
557
- }
558
- throw errorHandler_1.ErrorHandler.processError(error, 'BuildService.cancelBuild');
559
- }
560
- }
561
- /**
562
- * Retry a failed build
563
- */
564
- async retryBuild(containerId, buildId) {
565
- try {
566
- if (!containerId) {
567
- throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
568
- }
569
- if (!buildId) {
570
- throw errorHandler_1.ErrorHandler.createValidationError('Build ID is required');
571
- }
572
- const response = await this.httpClient.post(`/v1/containers/${containerId}/builds/${buildId}/retry`);
573
- if (!response.data) {
574
- throw errorHandler_1.ErrorHandler.createServerError('Invalid response from build retry API');
575
- }
576
- return response.data;
577
- }
578
- catch (error) {
579
- if (error instanceof errorHandler_1.ApiError && error.status === 404) {
580
- throw errorHandler_1.ErrorHandler.createNotFoundError(`Build ${buildId} not found`);
581
- }
582
- throw errorHandler_1.ErrorHandler.processError(error, 'BuildService.retryBuild');
583
- }
584
- }
585
- /**
586
- * Get build statistics
587
- */
588
- async getBuildStats(containerId) {
589
- try {
590
- let endpoint = '/v1/builds/stats';
591
- if (containerId) {
592
- endpoint = `/v1/containers/${containerId}/builds/stats`;
593
- }
594
- const response = await this.httpClient.get(endpoint);
595
- if (!response.data) {
596
- return { total: 0, completed: 0, failed: 0, running: 0, pending: 0 };
597
- }
598
- return {
599
- total: response.data.total || 0,
600
- completed: response.data.completed || 0,
601
- failed: response.data.failed || 0,
602
- running: response.data.running || 0,
603
- pending: response.data.pending || 0
604
- };
605
- }
606
- catch (error) {
607
- if (error instanceof errorHandler_1.ApiError && error.status === 404) {
608
- return { total: 0, completed: 0, failed: 0, running: 0, pending: 0 };
609
- }
610
- throw errorHandler_1.ErrorHandler.processError(error, 'BuildService.getBuildStats');
611
- }
612
- }
613
- /**
614
- * Stream build logs (for real-time updates)
615
- */
616
- async streamBuildLogs(containerId, buildId, onLog, onError, onComplete) {
617
- try {
618
- if (!containerId) {
619
- throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
620
- }
621
- if (!buildId) {
622
- throw errorHandler_1.ErrorHandler.createValidationError('Build ID is required');
623
- }
624
- // For now, implement polling-based streaming
625
- // In a real implementation, this could use Server-Sent Events or WebSockets
626
- let lastLogCount = 0;
627
- const pollInterval = 2000; // 2 seconds
628
- const poll = async () => {
629
- try {
630
- const logs = await this.getBuildLogs(containerId, buildId);
631
- // Send only new logs
632
- if (logs.length > lastLogCount) {
633
- const newLogs = logs.slice(lastLogCount);
634
- newLogs.forEach(log => onLog(log));
635
- lastLogCount = logs.length;
636
- }
637
- // Check if build is complete
638
- const buildStatus = await this.getBuildStatus({ containerId, buildId });
639
- if (buildStatus.status === 'completed' || buildStatus.status === 'failed' || buildStatus.status === 'cancelled') {
640
- if (onComplete) {
641
- onComplete();
642
- }
643
- return;
644
- }
645
- // Continue polling
646
- setTimeout(poll, pollInterval);
647
- }
648
- catch (error) {
649
- if (onError) {
650
- onError(error);
651
- }
652
- }
653
- };
654
- // Start polling
655
- poll();
656
- }
657
- catch (error) {
658
- throw errorHandler_1.ErrorHandler.processError(error, 'BuildService.streamBuildLogs');
659
- }
660
- }
661
499
  }
662
500
  exports.BuildService = BuildService;
663
501
  // Export singleton instance