@172ai/containers-mcp-server 1.7.1 → 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.
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +43 -2
- package/dist/server.js.map +1 -1
- package/dist/services/buildService.d.ts +31 -56
- package/dist/services/buildService.d.ts.map +1 -1
- package/dist/services/buildService.js +228 -420
- package/dist/services/buildService.js.map +1 -1
- package/dist/services/executionService.d.ts +32 -55
- package/dist/services/executionService.d.ts.map +1 -1
- package/dist/services/executionService.js +274 -336
- package/dist/services/executionService.js.map +1 -1
- package/dist/services/streamingService.d.ts +95 -0
- package/dist/services/streamingService.d.ts.map +1 -0
- package/dist/services/streamingService.js +298 -0
- package/dist/services/streamingService.js.map +1 -0
- package/package.json +8 -7
|
@@ -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
|
-
//
|
|
17
|
-
this.
|
|
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
|
-
*
|
|
39
|
+
* Get stream analytics and monitoring information
|
|
41
40
|
*/
|
|
42
41
|
getStreamAnalytics() {
|
|
43
42
|
return {
|
|
44
43
|
...this.streamAnalytics,
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
85
|
-
if (!
|
|
75
|
+
const stream = this.activeStreams.get(progressToken);
|
|
76
|
+
if (!stream) {
|
|
86
77
|
return false;
|
|
87
78
|
}
|
|
88
|
-
|
|
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
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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(`[
|
|
146
|
-
|
|
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
|
|
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
|
-
//
|
|
102
|
+
// Initialize progress tracking with streaming notifications
|
|
346
103
|
if (progressToken) {
|
|
347
|
-
console.log(`[
|
|
348
|
-
await this.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
128
|
+
// Set up real-time streaming for progress updates
|
|
372
129
|
if (progressToken) {
|
|
373
|
-
await this.
|
|
374
|
-
//
|
|
375
|
-
this.
|
|
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,195 @@ 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:`, 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
|
|
407
353
|
/**
|
|
408
354
|
* Get build status
|
|
409
355
|
*/
|
|
@@ -412,14 +358,11 @@ class BuildService {
|
|
|
412
358
|
if (!params.containerId) {
|
|
413
359
|
throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
|
|
414
360
|
}
|
|
415
|
-
// Use the build logs endpoint to get build status
|
|
416
|
-
// The server provides build logs at /v1/containers/{id}/builds/{buildId}
|
|
417
361
|
let endpoint = `/v1/containers/${params.containerId}/builds`;
|
|
418
362
|
if (params.buildId) {
|
|
419
363
|
endpoint += `/${params.buildId}`;
|
|
420
364
|
}
|
|
421
365
|
else {
|
|
422
|
-
// List all builds for the container and get the latest
|
|
423
366
|
const builds = await this.listBuilds({ containerId: params.containerId, limit: 1 });
|
|
424
367
|
if (builds.builds.length === 0) {
|
|
425
368
|
throw errorHandler_1.ErrorHandler.createNotFoundError(`No builds found for container ${params.containerId}`);
|
|
@@ -450,7 +393,6 @@ class BuildService {
|
|
|
450
393
|
async listBuilds(params = {}) {
|
|
451
394
|
try {
|
|
452
395
|
if (!params.containerId) {
|
|
453
|
-
// No global builds endpoint available, return empty list
|
|
454
396
|
return {
|
|
455
397
|
builds: [],
|
|
456
398
|
total: 0,
|
|
@@ -466,14 +408,12 @@ class BuildService {
|
|
|
466
408
|
queryParams.append('limit', params.limit.toString());
|
|
467
409
|
if (params.offset)
|
|
468
410
|
queryParams.append('offset', params.offset.toString());
|
|
469
|
-
// Use the container-specific builds endpoint (for build logs)
|
|
470
411
|
const endpoint = `/v1/containers/${params.containerId}/builds`;
|
|
471
412
|
const response = await this.httpClient.get(`${endpoint}?${queryParams.toString()}`);
|
|
472
413
|
if (!response.data) {
|
|
473
414
|
throw errorHandler_1.ErrorHandler.createServerError('Invalid response format from builds list API');
|
|
474
415
|
}
|
|
475
416
|
const data = response.data;
|
|
476
|
-
// Handle case where response is an array directly (build logs)
|
|
477
417
|
let builds = [];
|
|
478
418
|
let total = 0;
|
|
479
419
|
if (Array.isArray(data)) {
|
|
@@ -485,7 +425,6 @@ class BuildService {
|
|
|
485
425
|
total = data.total || data.builds.length;
|
|
486
426
|
}
|
|
487
427
|
else {
|
|
488
|
-
// No builds found
|
|
489
428
|
builds = [];
|
|
490
429
|
total = 0;
|
|
491
430
|
}
|
|
@@ -499,7 +438,6 @@ class BuildService {
|
|
|
499
438
|
}
|
|
500
439
|
catch (error) {
|
|
501
440
|
if (error instanceof errorHandler_1.ApiError && error.status === 404) {
|
|
502
|
-
// No builds found, return empty list
|
|
503
441
|
return {
|
|
504
442
|
builds: [],
|
|
505
443
|
total: 0,
|
|
@@ -519,19 +457,15 @@ class BuildService {
|
|
|
519
457
|
if (!containerId) {
|
|
520
458
|
throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
|
|
521
459
|
}
|
|
522
|
-
// The logs are actually contained within the build details response
|
|
523
|
-
// Use the build details endpoint instead of a separate logs endpoint
|
|
524
460
|
let endpoint = `/v1/containers/${containerId}/builds`;
|
|
525
461
|
if (buildId) {
|
|
526
462
|
endpoint += `/${buildId}`;
|
|
527
463
|
}
|
|
528
464
|
else {
|
|
529
|
-
// Get the latest build first
|
|
530
465
|
const builds = await this.listBuilds({ containerId, limit: 1 });
|
|
531
466
|
if (builds.builds.length === 0) {
|
|
532
|
-
return [];
|
|
467
|
+
return [];
|
|
533
468
|
}
|
|
534
|
-
// Use the latest build ID
|
|
535
469
|
buildId = builds.builds[0].id;
|
|
536
470
|
endpoint += `/${buildId}`;
|
|
537
471
|
}
|
|
@@ -539,13 +473,11 @@ class BuildService {
|
|
|
539
473
|
if (!response.data) {
|
|
540
474
|
return [];
|
|
541
475
|
}
|
|
542
|
-
// Handle the API response format where logs are in the 'entries' field
|
|
543
476
|
const buildData = response.data;
|
|
544
477
|
const entries = buildData.entries || [];
|
|
545
478
|
if (!Array.isArray(entries)) {
|
|
546
|
-
return [];
|
|
479
|
+
return [];
|
|
547
480
|
}
|
|
548
|
-
// Transform entries to BuildLogEntry format
|
|
549
481
|
return entries.map((entry, index) => ({
|
|
550
482
|
id: entry.id || `${containerId}-${buildId}-${index}`,
|
|
551
483
|
containerId: containerId,
|
|
@@ -559,135 +491,11 @@ class BuildService {
|
|
|
559
491
|
}
|
|
560
492
|
catch (error) {
|
|
561
493
|
if (error instanceof errorHandler_1.ApiError && error.status === 404) {
|
|
562
|
-
return [];
|
|
494
|
+
return [];
|
|
563
495
|
}
|
|
564
496
|
throw errorHandler_1.ErrorHandler.processError(error, 'BuildService.getBuildLogs');
|
|
565
497
|
}
|
|
566
498
|
}
|
|
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
499
|
}
|
|
692
500
|
exports.BuildService = BuildService;
|
|
693
501
|
// Export singleton instance
|