@172ai/containers-mcp-server 1.7.1 → 1.7.3

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