@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.
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +190 -2
- package/dist/server.js.map +1 -1
- package/dist/services/buildService.d.ts +39 -53
- package/dist/services/buildService.d.ts.map +1 -1
- package/dist/services/buildService.js +238 -400
- package/dist/services/buildService.js.map +1 -1
- package/dist/services/executionService.d.ts +40 -52
- package/dist/services/executionService.d.ts.map +1 -1
- package/dist/services/executionService.js +291 -325
- 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,9 @@ 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();
|
|
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
|
-
*
|
|
39
|
+
* Get stream analytics and monitoring information
|
|
40
40
|
*/
|
|
41
41
|
getStreamAnalytics() {
|
|
42
42
|
return {
|
|
43
43
|
...this.streamAnalytics,
|
|
44
|
-
|
|
44
|
+
activeStreams: Array.from(this.activeStreams.values())
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
48
|
+
* Get progress update for a specific token
|
|
49
49
|
*/
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
*
|
|
62
|
+
* Get all progress updates for debugging
|
|
65
63
|
*/
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
72
|
+
* Cancel build monitoring for a specific token
|
|
83
73
|
*/
|
|
84
|
-
async
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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(`[
|
|
109
|
-
|
|
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
|
|
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
|
-
//
|
|
102
|
+
// Initialize progress tracking with streaming notifications
|
|
316
103
|
if (progressToken) {
|
|
317
|
-
console.log(`[
|
|
318
|
-
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');
|
|
319
106
|
}
|
|
320
107
|
const buildRequest = {
|
|
321
108
|
buildArgs: params.buildArgs || {},
|
|
322
109
|
target: params.target
|
|
323
110
|
};
|
|
324
|
-
//
|
|
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
|
-
//
|
|
128
|
+
// Set up real-time streaming for progress updates
|
|
342
129
|
if (progressToken) {
|
|
343
|
-
await this.
|
|
344
|
-
//
|
|
345
|
-
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);
|
|
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 [];
|
|
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 [];
|
|
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 [];
|
|
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
|