deploy-mcp 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +369 -86
- package/dist/chunk-AIITEXOJ.js +1563 -0
- package/dist/index.js +6 -28
- package/dist/worker.js +415 -27
- package/package.json +10 -7
- package/dist/chunk-QRZL43CY.js +0 -297
|
@@ -0,0 +1,1563 @@
|
|
|
1
|
+
// src/core/tools.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var SUPPORTED_PLATFORMS = ["vercel"];
|
|
4
|
+
var checkDeploymentStatusSchema = z.object({
|
|
5
|
+
platform: z.enum(SUPPORTED_PLATFORMS).describe("The deployment platform"),
|
|
6
|
+
project: z.string().describe("The project name or ID"),
|
|
7
|
+
token: z.string().optional().describe("API token for authentication (optional if set in environment)")
|
|
8
|
+
});
|
|
9
|
+
var watchDeploymentSchema = z.object({
|
|
10
|
+
platform: z.enum(SUPPORTED_PLATFORMS).describe("The deployment platform"),
|
|
11
|
+
project: z.string().describe("The project name or ID"),
|
|
12
|
+
deploymentId: z.string().optional().describe("Specific deployment ID to watch (optional, defaults to latest)"),
|
|
13
|
+
token: z.string().optional().describe("API token for authentication (optional if set in environment)")
|
|
14
|
+
});
|
|
15
|
+
var compareDeploymentsSchema = z.object({
|
|
16
|
+
platform: z.enum(SUPPORTED_PLATFORMS).describe("The deployment platform"),
|
|
17
|
+
project: z.string().describe("The project name or ID"),
|
|
18
|
+
count: z.number().default(2).describe("Number of deployments to compare (default: 2)"),
|
|
19
|
+
token: z.string().optional().describe("API token for authentication (optional if set in environment)")
|
|
20
|
+
});
|
|
21
|
+
var getDeploymentLogsSchema = z.object({
|
|
22
|
+
platform: z.enum(SUPPORTED_PLATFORMS).describe("The deployment platform"),
|
|
23
|
+
deploymentId: z.string().describe("The deployment ID"),
|
|
24
|
+
filter: z.enum(["error", "warning", "all"]).default("error").describe("Filter logs by type (default: error)"),
|
|
25
|
+
token: z.string().optional().describe("API token for authentication (optional if set in environment)")
|
|
26
|
+
});
|
|
27
|
+
var tools = [
|
|
28
|
+
{
|
|
29
|
+
name: "check_deployment_status",
|
|
30
|
+
description: "Check the latest deployment status for a project on a platform",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
platform: {
|
|
35
|
+
type: "string",
|
|
36
|
+
enum: ["vercel"],
|
|
37
|
+
description: "The deployment platform"
|
|
38
|
+
},
|
|
39
|
+
project: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "The project name or ID"
|
|
42
|
+
},
|
|
43
|
+
token: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "API token for authentication (optional if set in environment)"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
required: ["platform", "project"]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "watch_deployment",
|
|
53
|
+
description: "Stream real-time deployment progress with detailed status updates and error information",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
platform: {
|
|
58
|
+
type: "string",
|
|
59
|
+
enum: ["vercel"],
|
|
60
|
+
description: "The deployment platform"
|
|
61
|
+
},
|
|
62
|
+
project: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "The project name or ID"
|
|
65
|
+
},
|
|
66
|
+
deploymentId: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Specific deployment ID to watch (optional, defaults to latest)"
|
|
69
|
+
},
|
|
70
|
+
token: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "API token for authentication (optional if set in environment)"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
required: ["platform", "project"]
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "compare_deployments",
|
|
80
|
+
description: "Compare recent deployments to identify changes, performance differences, and potential issues",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
platform: {
|
|
85
|
+
type: "string",
|
|
86
|
+
enum: ["vercel"],
|
|
87
|
+
description: "The deployment platform"
|
|
88
|
+
},
|
|
89
|
+
project: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "The project name or ID"
|
|
92
|
+
},
|
|
93
|
+
count: {
|
|
94
|
+
type: "number",
|
|
95
|
+
default: 2,
|
|
96
|
+
description: "Number of deployments to compare (default: 2)"
|
|
97
|
+
},
|
|
98
|
+
token: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "API token for authentication (optional if set in environment)"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
required: ["platform", "project"]
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "get_deployment_logs",
|
|
108
|
+
description: "Fetch detailed logs for a specific deployment, useful for debugging failed deployments",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
platform: {
|
|
113
|
+
type: "string",
|
|
114
|
+
enum: ["vercel"],
|
|
115
|
+
description: "The deployment platform"
|
|
116
|
+
},
|
|
117
|
+
deploymentId: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "The deployment ID"
|
|
120
|
+
},
|
|
121
|
+
filter: {
|
|
122
|
+
type: "string",
|
|
123
|
+
enum: ["error", "warning", "all"],
|
|
124
|
+
default: "error",
|
|
125
|
+
description: "Filter logs by type (default: error)"
|
|
126
|
+
},
|
|
127
|
+
token: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "API token for authentication (optional if set in environment)"
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
required: ["platform", "deploymentId"]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
// src/adapters/base/adapter.ts
|
|
138
|
+
var BaseAdapter = class {
|
|
139
|
+
formatTimestamp(date) {
|
|
140
|
+
return new Date(date).toISOString();
|
|
141
|
+
}
|
|
142
|
+
calculateDuration(start, end) {
|
|
143
|
+
const startTime = new Date(start).getTime();
|
|
144
|
+
const endTime = end ? new Date(end).getTime() : Date.now();
|
|
145
|
+
return Math.floor((endTime - startTime) / 1e3);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/adapters/base/types.ts
|
|
150
|
+
var AdapterException = class extends Error {
|
|
151
|
+
constructor(type, message, originalError) {
|
|
152
|
+
super(message);
|
|
153
|
+
this.type = type;
|
|
154
|
+
this.originalError = originalError;
|
|
155
|
+
this.name = "AdapterException";
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/core/constants.ts
|
|
160
|
+
var MAX_DEPLOYMENT_WATCH_ATTEMPTS = 120;
|
|
161
|
+
var BUILD_TIME_SECONDS_DIVISOR = 1e3;
|
|
162
|
+
var MAX_WATCH_TIME_MS = 24e4;
|
|
163
|
+
var MAX_BACKOFF_DELAY_MS = 3e4;
|
|
164
|
+
var BACKOFF_JITTER_MS = 1e3;
|
|
165
|
+
var HIGH_RISK_THRESHOLD_PERCENT = 50;
|
|
166
|
+
var MEDIUM_RISK_THRESHOLD_PERCENT = 20;
|
|
167
|
+
var DEFAULT_COMPARISON_COUNT = 2;
|
|
168
|
+
var SINGLE_DEPLOYMENT_FETCH = 1;
|
|
169
|
+
var MAX_TOKENS_PER_MINUTE = 30;
|
|
170
|
+
var RATE_LIMITER_CLEANUP_AGE_MS = 36e5;
|
|
171
|
+
var DEPLOYMENT_STATES = {
|
|
172
|
+
INITIALIZING: "INITIALIZING",
|
|
173
|
+
BUILDING: "BUILDING",
|
|
174
|
+
UPLOADING: "UPLOADING",
|
|
175
|
+
DEPLOYING: "DEPLOYING",
|
|
176
|
+
READY: "READY",
|
|
177
|
+
ERROR: "ERROR",
|
|
178
|
+
CANCELED: "CANCELED"
|
|
179
|
+
};
|
|
180
|
+
var POLLING_INTERVALS_BY_STATE = {
|
|
181
|
+
INITIALIZING: 5e3,
|
|
182
|
+
// 5s - slower at start
|
|
183
|
+
BUILDING: 3e3,
|
|
184
|
+
// 3s - active building
|
|
185
|
+
UPLOADING: 2e3,
|
|
186
|
+
// 2s - final stages
|
|
187
|
+
DEPLOYING: 2e3,
|
|
188
|
+
// 2s - final stages
|
|
189
|
+
READY: 0,
|
|
190
|
+
// Stop polling
|
|
191
|
+
ERROR: 0,
|
|
192
|
+
// Stop polling
|
|
193
|
+
CANCELED: 0,
|
|
194
|
+
// Stop polling
|
|
195
|
+
UNKNOWN: 1e4
|
|
196
|
+
// 10s - unknown states
|
|
197
|
+
};
|
|
198
|
+
var LOG_FILTERS = {
|
|
199
|
+
ERROR: /error|fail|exception|critical/i,
|
|
200
|
+
WARNING: /warning|warn|deprecat/i
|
|
201
|
+
};
|
|
202
|
+
var DEFAULTS = {
|
|
203
|
+
DEPLOYMENT_ID_SLICE_LENGTH: 7,
|
|
204
|
+
ERROR_LINE_TRIM_INDEX: 0,
|
|
205
|
+
PERCENTAGE_MULTIPLIER: 100,
|
|
206
|
+
MAX_EVENT_BUFFER_SIZE: 100,
|
|
207
|
+
MAX_CACHE_SIZE: 10,
|
|
208
|
+
CACHE_TTL_MS: 30 * 60 * 1e3,
|
|
209
|
+
// 30 minutes
|
|
210
|
+
CACHE_CLEANUP_INTERVAL_MS: 5 * 60 * 1e3
|
|
211
|
+
// 5 minutes
|
|
212
|
+
};
|
|
213
|
+
var ERROR_MESSAGES = {
|
|
214
|
+
NO_TOKEN: "No token provided",
|
|
215
|
+
NO_DEPLOYMENT_FOUND: "No deployment found for this project",
|
|
216
|
+
DEPLOYMENT_TAKING_LONG: "\u26A0\uFE0F Deployment is taking longer than expected",
|
|
217
|
+
NO_LOGS_AVAILABLE: "No logs available",
|
|
218
|
+
NO_LOGS_MATCHING_FILTER: "No logs matching filter criteria"
|
|
219
|
+
};
|
|
220
|
+
var STATUS_ICONS = {
|
|
221
|
+
ROCKET: "\u{1F680}",
|
|
222
|
+
HOURGLASS: "\u23F3",
|
|
223
|
+
HAMMER: "\u{1F528}",
|
|
224
|
+
PACKAGE: "\u{1F4E6}",
|
|
225
|
+
GLOBE: "\u{1F30D}",
|
|
226
|
+
SUCCESS: "\u2705",
|
|
227
|
+
ERROR: "\u274C",
|
|
228
|
+
WARNING: "\u26A0\uFE0F",
|
|
229
|
+
CHART: "\u{1F4CA}",
|
|
230
|
+
LIGHTNING: "\u26A1",
|
|
231
|
+
TURTLE: "\u{1F422}",
|
|
232
|
+
LIGHTBULB: "\u{1F4A1}",
|
|
233
|
+
PIN: "\u{1F4CD}"
|
|
234
|
+
};
|
|
235
|
+
var STATUS_MESSAGES = {
|
|
236
|
+
STARTING_WATCH: (id) => `${STATUS_ICONS.ROCKET} Starting to watch deployment ${id}...`,
|
|
237
|
+
INITIALIZING: `${STATUS_ICONS.HOURGLASS} Initializing deployment...`,
|
|
238
|
+
BUILDING: `${STATUS_ICONS.HAMMER} Building application...`,
|
|
239
|
+
UPLOADING: `${STATUS_ICONS.PACKAGE} Uploading to edge network...`,
|
|
240
|
+
DEPLOYING: `${STATUS_ICONS.GLOBE} Deploying to production...`,
|
|
241
|
+
DEPLOYMENT_SUCCESS: `${STATUS_ICONS.SUCCESS} Deployment successful!`,
|
|
242
|
+
DEPLOYMENT_FAILED: (message) => `${STATUS_ICONS.ERROR} Deployment failed: ${message}`,
|
|
243
|
+
BUILD_TIME_SAME: (time) => `${STATUS_ICONS.CHART} Build time: ${time}s (same as previous)`,
|
|
244
|
+
BUILD_TIME_CHANGE: (current, delta, faster) => `${STATUS_ICONS.CHART} Build time: ${current}s (${Math.abs(delta)}s ${faster ? "faster" : "slower"} than previous) ${faster ? STATUS_ICONS.LIGHTNING : STATUS_ICONS.TURTLE}`
|
|
245
|
+
};
|
|
246
|
+
var ERROR_TEXT_PATTERNS = {
|
|
247
|
+
UNAUTHORIZED: ["401", "unauthorized"],
|
|
248
|
+
NOT_FOUND: ["404", "not found"],
|
|
249
|
+
RATE_LIMITED: ["429", "rate limit"],
|
|
250
|
+
TIMEOUT: ["timeout", "AbortError"]
|
|
251
|
+
};
|
|
252
|
+
var API_EVENT_TYPES = {
|
|
253
|
+
STDOUT: "stdout",
|
|
254
|
+
STDERR: "stderr"
|
|
255
|
+
};
|
|
256
|
+
var API_MESSAGES = {
|
|
257
|
+
NO_LOGS_AVAILABLE: "No logs available",
|
|
258
|
+
INVALID_TOKEN: "Invalid Vercel token",
|
|
259
|
+
PROJECT_NOT_FOUND: "Project not found",
|
|
260
|
+
RATE_LIMIT_EXCEEDED: "Rate limit exceeded",
|
|
261
|
+
REQUEST_TIMEOUT: "Request timeout",
|
|
262
|
+
FAILED_TO_VALIDATE_TOKEN: "Failed to validate Vercel token"
|
|
263
|
+
};
|
|
264
|
+
var API_PARAMS = {
|
|
265
|
+
BUILDS: 1,
|
|
266
|
+
LOGS: 1
|
|
267
|
+
};
|
|
268
|
+
var API_CONFIG = {
|
|
269
|
+
VERCEL_BASE_URL: "https://api.vercel.com",
|
|
270
|
+
DEFAULT_TIMEOUT_MS: 1e4,
|
|
271
|
+
DEFAULT_RETRY_ATTEMPTS: 3,
|
|
272
|
+
DEFAULT_DEPLOYMENT_LIMIT: 10,
|
|
273
|
+
SINGLE_DEPLOYMENT_LIMIT: 1
|
|
274
|
+
};
|
|
275
|
+
var PLATFORM_NAMES = {
|
|
276
|
+
VERCEL: "vercel",
|
|
277
|
+
NETLIFY: "netlify",
|
|
278
|
+
RAILWAY: "railway",
|
|
279
|
+
RENDER: "render"
|
|
280
|
+
};
|
|
281
|
+
var ENVIRONMENT_TYPES = {
|
|
282
|
+
PRODUCTION: "production",
|
|
283
|
+
PREVIEW: "preview",
|
|
284
|
+
DEVELOPMENT: "development"
|
|
285
|
+
};
|
|
286
|
+
var VERCEL_STATES = {
|
|
287
|
+
READY: "READY",
|
|
288
|
+
ERROR: "ERROR",
|
|
289
|
+
CANCELED: "CANCELED",
|
|
290
|
+
BUILDING: "BUILDING",
|
|
291
|
+
INITIALIZING: "INITIALIZING",
|
|
292
|
+
QUEUED: "QUEUED"
|
|
293
|
+
};
|
|
294
|
+
var ADAPTER_ERRORS = {
|
|
295
|
+
TOKEN_REQUIRED: "Vercel token required. Set VERCEL_TOKEN environment variable or pass token parameter.",
|
|
296
|
+
FETCH_DEPLOYMENT_FAILED: "Failed to fetch deployment status from Vercel",
|
|
297
|
+
UNKNOWN_STATUS: "unknown"
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/adapters/base/api-client.ts
|
|
301
|
+
var RateLimitError = class extends Error {
|
|
302
|
+
constructor(retryAfter, endpoint) {
|
|
303
|
+
super(`Rate limit exceeded for ${endpoint}. Retry after ${retryAfter}ms`);
|
|
304
|
+
this.retryAfter = retryAfter;
|
|
305
|
+
this.endpoint = endpoint;
|
|
306
|
+
this.name = "RateLimitError";
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
var HTTPError = class _HTTPError extends Error {
|
|
310
|
+
constructor(statusCode, statusText, url, method) {
|
|
311
|
+
super(`${method} ${url} failed with ${statusCode}: ${statusText}`);
|
|
312
|
+
this.statusCode = statusCode;
|
|
313
|
+
this.statusText = statusText;
|
|
314
|
+
this.url = url;
|
|
315
|
+
this.method = method;
|
|
316
|
+
this.name = "HTTPError";
|
|
317
|
+
Error.captureStackTrace(this, _HTTPError);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
var BaseAPIClient = class {
|
|
321
|
+
baseUrl;
|
|
322
|
+
defaultHeaders;
|
|
323
|
+
timeout;
|
|
324
|
+
maxRetries;
|
|
325
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
326
|
+
rateLimiters = /* @__PURE__ */ new Map();
|
|
327
|
+
maxTokensPerMinute = MAX_TOKENS_PER_MINUTE;
|
|
328
|
+
constructor(config) {
|
|
329
|
+
this.baseUrl = new URL(config.baseUrl);
|
|
330
|
+
this.defaultHeaders = new Headers({
|
|
331
|
+
"Content-Type": "application/json",
|
|
332
|
+
"User-Agent": "deploy-mcp/1.0.0",
|
|
333
|
+
...config.headers
|
|
334
|
+
});
|
|
335
|
+
this.timeout = config.timeout ?? API_CONFIG.DEFAULT_TIMEOUT_MS;
|
|
336
|
+
this.maxRetries = config.retry ?? API_CONFIG.DEFAULT_RETRY_ATTEMPTS;
|
|
337
|
+
}
|
|
338
|
+
async request(endpoint, options) {
|
|
339
|
+
if (options?.token) {
|
|
340
|
+
await this.checkRateLimit(options.token, endpoint.path);
|
|
341
|
+
}
|
|
342
|
+
const cacheKey = this.getCacheKey(endpoint, options);
|
|
343
|
+
if (endpoint.method === "GET" && !options?.body) {
|
|
344
|
+
const pending = this.pendingRequests.get(cacheKey);
|
|
345
|
+
if (pending) {
|
|
346
|
+
return pending;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const requestPromise = this.executeRequest(endpoint, options);
|
|
350
|
+
if (endpoint.method === "GET" && !options?.body) {
|
|
351
|
+
this.pendingRequests.set(cacheKey, requestPromise);
|
|
352
|
+
requestPromise.then(() => {
|
|
353
|
+
this.pendingRequests.delete(cacheKey);
|
|
354
|
+
}).catch(() => {
|
|
355
|
+
this.pendingRequests.delete(cacheKey);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return requestPromise;
|
|
359
|
+
}
|
|
360
|
+
async executeRequest(endpoint, options) {
|
|
361
|
+
const url = this.buildUrl(endpoint.path, options?.searchParams);
|
|
362
|
+
const headers = this.mergeHeaders(options?.headers);
|
|
363
|
+
let lastError = new Error("No attempts made");
|
|
364
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
365
|
+
try {
|
|
366
|
+
const response = await this.fetchWithTimeout(url, {
|
|
367
|
+
method: endpoint.method,
|
|
368
|
+
headers,
|
|
369
|
+
body: options?.body ? JSON.stringify(options.body) : void 0,
|
|
370
|
+
signal: options?.signal
|
|
371
|
+
});
|
|
372
|
+
if (!response.ok) {
|
|
373
|
+
throw new HTTPError(
|
|
374
|
+
response.status,
|
|
375
|
+
response.statusText,
|
|
376
|
+
url.toString(),
|
|
377
|
+
endpoint.method
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const text = await response.text();
|
|
381
|
+
if (!text) {
|
|
382
|
+
return {};
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
return JSON.parse(text);
|
|
386
|
+
} catch {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Invalid JSON response from ${endpoint.path}: ${text.slice(0, 100)}`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
} catch (error) {
|
|
392
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
393
|
+
if (error instanceof HTTPError && error.statusCode >= 400 && error.statusCode < 500 || error instanceof Error && error.name === "AbortError") {
|
|
394
|
+
throw this.enhanceError(lastError, endpoint);
|
|
395
|
+
}
|
|
396
|
+
if (attempt === this.maxRetries) {
|
|
397
|
+
throw this.enhanceError(lastError, endpoint);
|
|
398
|
+
}
|
|
399
|
+
const baseDelay = Math.min(
|
|
400
|
+
1e3 * Math.pow(2, attempt),
|
|
401
|
+
MAX_BACKOFF_DELAY_MS
|
|
402
|
+
);
|
|
403
|
+
const jitter = Math.random() * BACKOFF_JITTER_MS;
|
|
404
|
+
const totalDelay = Math.min(baseDelay + jitter, MAX_BACKOFF_DELAY_MS);
|
|
405
|
+
await this.delay(totalDelay);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
throw this.enhanceError(lastError, endpoint);
|
|
409
|
+
}
|
|
410
|
+
async fetchWithTimeout(url, init) {
|
|
411
|
+
const controller = new AbortController();
|
|
412
|
+
if (init.signal) {
|
|
413
|
+
init.signal.addEventListener("abort", () => controller.abort());
|
|
414
|
+
}
|
|
415
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
416
|
+
try {
|
|
417
|
+
return await fetch(url, {
|
|
418
|
+
...init,
|
|
419
|
+
signal: controller.signal,
|
|
420
|
+
keepalive: true
|
|
421
|
+
});
|
|
422
|
+
} finally {
|
|
423
|
+
clearTimeout(timeoutId);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
buildUrl(path, params) {
|
|
427
|
+
const url = new URL(path, this.baseUrl);
|
|
428
|
+
if (params) {
|
|
429
|
+
const searchParams = new URLSearchParams();
|
|
430
|
+
for (const [key, value] of Object.entries(params)) {
|
|
431
|
+
if (value !== void 0 && value !== null) {
|
|
432
|
+
searchParams.set(key, String(value));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
url.search = searchParams.toString();
|
|
436
|
+
}
|
|
437
|
+
return url;
|
|
438
|
+
}
|
|
439
|
+
mergeHeaders(headers) {
|
|
440
|
+
if (!headers) {
|
|
441
|
+
return this.defaultHeaders;
|
|
442
|
+
}
|
|
443
|
+
const merged = new Headers(this.defaultHeaders);
|
|
444
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
445
|
+
merged.set(key, value);
|
|
446
|
+
}
|
|
447
|
+
return merged;
|
|
448
|
+
}
|
|
449
|
+
enhanceError(error, endpoint) {
|
|
450
|
+
const enhanced = new Error(
|
|
451
|
+
`API request failed for ${endpoint.path}: ${error.message}
|
|
452
|
+
See docs: ${endpoint.docsUrl}`
|
|
453
|
+
);
|
|
454
|
+
enhanced.stack = error.stack;
|
|
455
|
+
enhanced.cause = error;
|
|
456
|
+
return enhanced;
|
|
457
|
+
}
|
|
458
|
+
getCacheKey(endpoint, options) {
|
|
459
|
+
const params = options?.searchParams ? JSON.stringify(options.searchParams) : "";
|
|
460
|
+
return `${endpoint.method}:${endpoint.path}:${params}`;
|
|
461
|
+
}
|
|
462
|
+
delay(ms) {
|
|
463
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
464
|
+
}
|
|
465
|
+
getRateLimiter(token) {
|
|
466
|
+
if (!this.rateLimiters.has(token)) {
|
|
467
|
+
this.rateLimiters.set(token, {
|
|
468
|
+
tokens: this.maxTokensPerMinute,
|
|
469
|
+
lastRefill: Date.now(),
|
|
470
|
+
refillRate: this.maxTokensPerMinute
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return this.rateLimiters.get(token);
|
|
474
|
+
}
|
|
475
|
+
async checkRateLimit(token, endpoint) {
|
|
476
|
+
const limiter = this.getRateLimiter(token);
|
|
477
|
+
const now = Date.now();
|
|
478
|
+
const timeSinceLastRefill = now - limiter.lastRefill;
|
|
479
|
+
const minutesElapsed = timeSinceLastRefill / 6e4;
|
|
480
|
+
const tokensToAdd = minutesElapsed * limiter.refillRate;
|
|
481
|
+
limiter.tokens = Math.min(
|
|
482
|
+
this.maxTokensPerMinute,
|
|
483
|
+
limiter.tokens + tokensToAdd
|
|
484
|
+
);
|
|
485
|
+
limiter.lastRefill = now;
|
|
486
|
+
if (limiter.tokens < 1) {
|
|
487
|
+
const waitTime = (1 - limiter.tokens) * (6e4 / limiter.refillRate);
|
|
488
|
+
throw new RateLimitError(Math.ceil(waitTime), endpoint);
|
|
489
|
+
}
|
|
490
|
+
limiter.tokens -= 1;
|
|
491
|
+
}
|
|
492
|
+
cleanupRateLimiters() {
|
|
493
|
+
const now = Date.now();
|
|
494
|
+
const maxAge = RATE_LIMITER_CLEANUP_AGE_MS;
|
|
495
|
+
for (const [token, limiter] of this.rateLimiters.entries()) {
|
|
496
|
+
if (now - limiter.lastRefill > maxAge) {
|
|
497
|
+
this.rateLimiters.delete(token);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// src/adapters/vercel/endpoints.ts
|
|
504
|
+
var VercelEndpoints = {
|
|
505
|
+
listDeployments: {
|
|
506
|
+
path: "v6/deployments",
|
|
507
|
+
method: "GET",
|
|
508
|
+
docsUrl: "https://vercel.com/docs/rest-api/endpoints/deployments#list-deployments",
|
|
509
|
+
description: "List deployments for authenticated user or team"
|
|
510
|
+
},
|
|
511
|
+
getDeployment: {
|
|
512
|
+
path: "v13/deployments",
|
|
513
|
+
method: "GET",
|
|
514
|
+
docsUrl: "https://vercel.com/docs/rest-api/endpoints/deployments#get-a-deployment-by-id-or-url",
|
|
515
|
+
description: "Get deployment by ID or URL"
|
|
516
|
+
},
|
|
517
|
+
getDeploymentEvents: {
|
|
518
|
+
path: "v2/deployments",
|
|
519
|
+
method: "GET",
|
|
520
|
+
docsUrl: "https://vercel.com/docs/rest-api/endpoints/deployments#get-deployment-events",
|
|
521
|
+
description: "Get build logs and events for a deployment"
|
|
522
|
+
},
|
|
523
|
+
getUser: {
|
|
524
|
+
path: "v2/user",
|
|
525
|
+
method: "GET",
|
|
526
|
+
docsUrl: "https://vercel.com/docs/rest-api/endpoints/user#get-the-authenticated-user",
|
|
527
|
+
description: "Get authenticated user information"
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// src/adapters/vercel/api.ts
|
|
532
|
+
var VercelAPI = class extends BaseAPIClient {
|
|
533
|
+
endpoints = VercelEndpoints;
|
|
534
|
+
config;
|
|
535
|
+
constructor(config) {
|
|
536
|
+
super({
|
|
537
|
+
baseUrl: config.baseUrl,
|
|
538
|
+
timeout: config.timeout,
|
|
539
|
+
retry: config.retryAttempts
|
|
540
|
+
});
|
|
541
|
+
this.config = config;
|
|
542
|
+
}
|
|
543
|
+
async getDeployments(projectId, token, limit = 1) {
|
|
544
|
+
try {
|
|
545
|
+
return await this.request(
|
|
546
|
+
this.endpoints.listDeployments,
|
|
547
|
+
{
|
|
548
|
+
searchParams: { projectId, limit },
|
|
549
|
+
headers: {
|
|
550
|
+
Authorization: `Bearer ${token}`
|
|
551
|
+
},
|
|
552
|
+
token
|
|
553
|
+
// Pass token for rate limiting
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
throw this.handleApiError(
|
|
558
|
+
error,
|
|
559
|
+
`Failed to fetch deployments for project ${projectId}`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async getUser(token) {
|
|
564
|
+
try {
|
|
565
|
+
return await this.request(this.endpoints.getUser, {
|
|
566
|
+
headers: {
|
|
567
|
+
Authorization: `Bearer ${token}`
|
|
568
|
+
},
|
|
569
|
+
token
|
|
570
|
+
// Pass token for rate limiting
|
|
571
|
+
});
|
|
572
|
+
} catch (error) {
|
|
573
|
+
throw this.handleApiError(error, API_MESSAGES.FAILED_TO_VALIDATE_TOKEN);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async getDeploymentById(deploymentId, token) {
|
|
577
|
+
try {
|
|
578
|
+
const endpoint = {
|
|
579
|
+
...this.endpoints.getDeployment,
|
|
580
|
+
path: `${this.endpoints.getDeployment.path}/${deploymentId}`
|
|
581
|
+
};
|
|
582
|
+
return await this.request(endpoint, {
|
|
583
|
+
headers: {
|
|
584
|
+
Authorization: `Bearer ${token}`
|
|
585
|
+
},
|
|
586
|
+
token
|
|
587
|
+
// Pass token for rate limiting
|
|
588
|
+
});
|
|
589
|
+
} catch (error) {
|
|
590
|
+
throw this.handleApiError(
|
|
591
|
+
error,
|
|
592
|
+
`Failed to fetch deployment ${deploymentId}`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async getDeploymentLogs(deploymentId, token) {
|
|
597
|
+
try {
|
|
598
|
+
const endpoint = {
|
|
599
|
+
...this.endpoints.getDeploymentEvents,
|
|
600
|
+
path: `${this.endpoints.getDeploymentEvents.path}/${deploymentId}/events`
|
|
601
|
+
};
|
|
602
|
+
const response = await this.request(endpoint, {
|
|
603
|
+
searchParams: {
|
|
604
|
+
builds: API_PARAMS.BUILDS,
|
|
605
|
+
logs: API_PARAMS.LOGS
|
|
606
|
+
},
|
|
607
|
+
headers: {
|
|
608
|
+
Authorization: `Bearer ${token}`
|
|
609
|
+
},
|
|
610
|
+
token
|
|
611
|
+
// Pass token for rate limiting
|
|
612
|
+
});
|
|
613
|
+
if (!response || !Array.isArray(response)) {
|
|
614
|
+
return API_MESSAGES.NO_LOGS_AVAILABLE;
|
|
615
|
+
}
|
|
616
|
+
const logs = response.filter(
|
|
617
|
+
(event) => event.type === API_EVENT_TYPES.STDOUT || event.type === API_EVENT_TYPES.STDERR
|
|
618
|
+
).map((event) => event.payload?.text || event.text || "").join("\n");
|
|
619
|
+
const sanitizedLogs = logs.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\//g, "/");
|
|
620
|
+
return sanitizedLogs || API_MESSAGES.NO_LOGS_AVAILABLE;
|
|
621
|
+
} catch (error) {
|
|
622
|
+
throw this.handleApiError(
|
|
623
|
+
error,
|
|
624
|
+
`Failed to fetch logs for deployment ${deploymentId}`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
handleApiError(error, context) {
|
|
629
|
+
if (error instanceof Error) {
|
|
630
|
+
const message = error.message.toLowerCase();
|
|
631
|
+
if (ERROR_TEXT_PATTERNS.UNAUTHORIZED.some(
|
|
632
|
+
(pattern) => message.includes(pattern)
|
|
633
|
+
)) {
|
|
634
|
+
return new AdapterException(
|
|
635
|
+
"UNAUTHORIZED",
|
|
636
|
+
API_MESSAGES.INVALID_TOKEN,
|
|
637
|
+
error
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
if (ERROR_TEXT_PATTERNS.NOT_FOUND.some((pattern) => message.includes(pattern))) {
|
|
641
|
+
return new AdapterException(
|
|
642
|
+
"NOT_FOUND",
|
|
643
|
+
API_MESSAGES.PROJECT_NOT_FOUND,
|
|
644
|
+
error
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
if (ERROR_TEXT_PATTERNS.RATE_LIMITED.some(
|
|
648
|
+
(pattern) => message.includes(pattern)
|
|
649
|
+
)) {
|
|
650
|
+
return new AdapterException(
|
|
651
|
+
"RATE_LIMITED",
|
|
652
|
+
API_MESSAGES.RATE_LIMIT_EXCEEDED,
|
|
653
|
+
error
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
if (ERROR_TEXT_PATTERNS.TIMEOUT.some(
|
|
657
|
+
(pattern) => message.includes(pattern) || error.name === pattern
|
|
658
|
+
)) {
|
|
659
|
+
return new AdapterException(
|
|
660
|
+
"NETWORK_ERROR",
|
|
661
|
+
API_MESSAGES.REQUEST_TIMEOUT,
|
|
662
|
+
error
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
return new AdapterException("NETWORK_ERROR", context, error);
|
|
666
|
+
}
|
|
667
|
+
return new AdapterException("UNKNOWN_ERROR", context);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// src/adapters/vercel/index.ts
|
|
672
|
+
var VercelAdapter = class extends BaseAdapter {
|
|
673
|
+
name = PLATFORM_NAMES.VERCEL;
|
|
674
|
+
api;
|
|
675
|
+
constructor(config) {
|
|
676
|
+
super();
|
|
677
|
+
const defaultConfig = {
|
|
678
|
+
baseUrl: API_CONFIG.VERCEL_BASE_URL,
|
|
679
|
+
timeout: API_CONFIG.DEFAULT_TIMEOUT_MS,
|
|
680
|
+
retryAttempts: API_CONFIG.DEFAULT_RETRY_ATTEMPTS
|
|
681
|
+
};
|
|
682
|
+
this.api = new VercelAPI({ ...defaultConfig, ...config });
|
|
683
|
+
}
|
|
684
|
+
async getLatestDeployment(project, token) {
|
|
685
|
+
const apiToken = token || process.env.VERCEL_TOKEN;
|
|
686
|
+
if (!apiToken) {
|
|
687
|
+
throw new Error(ADAPTER_ERRORS.TOKEN_REQUIRED);
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
const data = await this.api.getDeployments(
|
|
691
|
+
project,
|
|
692
|
+
apiToken,
|
|
693
|
+
API_CONFIG.SINGLE_DEPLOYMENT_LIMIT
|
|
694
|
+
);
|
|
695
|
+
if (!data.deployments || data.deployments.length === 0) {
|
|
696
|
+
return {
|
|
697
|
+
status: ADAPTER_ERRORS.UNKNOWN_STATUS,
|
|
698
|
+
projectName: project,
|
|
699
|
+
platform: PLATFORM_NAMES.VERCEL
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
return this.transformDeployment(data.deployments[0]);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (error instanceof Error) {
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
throw new Error(ADAPTER_ERRORS.FETCH_DEPLOYMENT_FAILED);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async authenticate(token) {
|
|
711
|
+
try {
|
|
712
|
+
await this.api.getUser(token);
|
|
713
|
+
return true;
|
|
714
|
+
} catch {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
transformDeployment(deployment) {
|
|
719
|
+
const status = this.mapState(deployment.state);
|
|
720
|
+
return {
|
|
721
|
+
id: deployment.uid,
|
|
722
|
+
status,
|
|
723
|
+
url: deployment.url ? `https://${deployment.url}` : void 0,
|
|
724
|
+
projectName: deployment.name,
|
|
725
|
+
platform: PLATFORM_NAMES.VERCEL,
|
|
726
|
+
timestamp: this.formatTimestamp(deployment.createdAt),
|
|
727
|
+
duration: deployment.ready ? this.calculateDuration(deployment.createdAt, deployment.ready) : void 0,
|
|
728
|
+
environment: deployment.target || ENVIRONMENT_TYPES.PRODUCTION,
|
|
729
|
+
commit: deployment.meta ? {
|
|
730
|
+
sha: deployment.meta.githubCommitSha,
|
|
731
|
+
message: deployment.meta.githubCommitMessage,
|
|
732
|
+
author: deployment.meta.githubCommitAuthorName
|
|
733
|
+
} : void 0
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
mapState(state) {
|
|
737
|
+
switch (state) {
|
|
738
|
+
case VERCEL_STATES.READY:
|
|
739
|
+
return "success";
|
|
740
|
+
case VERCEL_STATES.ERROR:
|
|
741
|
+
case VERCEL_STATES.CANCELED:
|
|
742
|
+
return "failed";
|
|
743
|
+
case VERCEL_STATES.BUILDING:
|
|
744
|
+
case VERCEL_STATES.INITIALIZING:
|
|
745
|
+
case VERCEL_STATES.QUEUED:
|
|
746
|
+
return "building";
|
|
747
|
+
default:
|
|
748
|
+
return ADAPTER_ERRORS.UNKNOWN_STATUS;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async getDeploymentById(deploymentId, token) {
|
|
752
|
+
return this.api.getDeploymentById(deploymentId, token);
|
|
753
|
+
}
|
|
754
|
+
async getRecentDeployments(project, token, limit = API_CONFIG.DEFAULT_DEPLOYMENT_LIMIT) {
|
|
755
|
+
const data = await this.api.getDeployments(project, token, limit);
|
|
756
|
+
return data.deployments || [];
|
|
757
|
+
}
|
|
758
|
+
async getDeploymentLogs(deploymentId, token) {
|
|
759
|
+
return this.api.getDeploymentLogs(deploymentId, token);
|
|
760
|
+
}
|
|
761
|
+
async getDeploymentStatus(project, token) {
|
|
762
|
+
return this.getLatestDeployment(project, token);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// src/core/deployment-intelligence.ts
|
|
767
|
+
var DeploymentIntelligence = class {
|
|
768
|
+
adapter;
|
|
769
|
+
constructor(platform) {
|
|
770
|
+
this.adapter = this.createAdapter(platform);
|
|
771
|
+
}
|
|
772
|
+
getPollingInterval(state) {
|
|
773
|
+
return POLLING_INTERVALS_BY_STATE[state] || POLLING_INTERVALS_BY_STATE.UNKNOWN;
|
|
774
|
+
}
|
|
775
|
+
getTimeAgo(timestamp) {
|
|
776
|
+
const now = Date.now();
|
|
777
|
+
const diff = now - timestamp;
|
|
778
|
+
const seconds = Math.floor(diff / 1e3);
|
|
779
|
+
const minutes = Math.floor(seconds / 60);
|
|
780
|
+
const hours = Math.floor(minutes / 60);
|
|
781
|
+
const days = Math.floor(hours / 24);
|
|
782
|
+
if (days > 0) return `${days} day${days > 1 ? "s" : ""} ago`;
|
|
783
|
+
if (hours > 0) return `${hours} hour${hours > 1 ? "s" : ""} ago`;
|
|
784
|
+
if (minutes > 0) return `${minutes} minute${minutes > 1 ? "s" : ""} ago`;
|
|
785
|
+
return `${seconds} second${seconds > 1 ? "s" : ""} ago`;
|
|
786
|
+
}
|
|
787
|
+
mapDeploymentState(state) {
|
|
788
|
+
switch (state) {
|
|
789
|
+
case "READY":
|
|
790
|
+
return "success";
|
|
791
|
+
case "ERROR":
|
|
792
|
+
case "CANCELED":
|
|
793
|
+
return "failed";
|
|
794
|
+
case "BUILDING":
|
|
795
|
+
case "INITIALIZING":
|
|
796
|
+
case "QUEUED":
|
|
797
|
+
return "building";
|
|
798
|
+
default:
|
|
799
|
+
return "unknown";
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
createAdapter(platform) {
|
|
803
|
+
switch (platform) {
|
|
804
|
+
case "vercel":
|
|
805
|
+
return new VercelAdapter();
|
|
806
|
+
// Ready for future platforms
|
|
807
|
+
// case "netlify":
|
|
808
|
+
// return new NetlifyAdapter();
|
|
809
|
+
// case "railway":
|
|
810
|
+
// return new RailwayAdapter();
|
|
811
|
+
// case "render":
|
|
812
|
+
// return new RenderAdapter();
|
|
813
|
+
default:
|
|
814
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async *watchDeployment(args) {
|
|
818
|
+
const token = args.token || process.env.VERCEL_TOKEN;
|
|
819
|
+
if (!token) {
|
|
820
|
+
yield {
|
|
821
|
+
type: "error",
|
|
822
|
+
message: ERROR_MESSAGES.NO_TOKEN,
|
|
823
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
824
|
+
};
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
let deploymentId = args.deploymentId;
|
|
829
|
+
if (!deploymentId) {
|
|
830
|
+
const deployments = await this.adapter.getRecentDeployments(
|
|
831
|
+
args.project,
|
|
832
|
+
token,
|
|
833
|
+
SINGLE_DEPLOYMENT_FETCH
|
|
834
|
+
);
|
|
835
|
+
if (deployments.length > 0) {
|
|
836
|
+
deploymentId = deployments[0].uid || deployments[0].id;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (!deploymentId) {
|
|
840
|
+
yield {
|
|
841
|
+
type: "error",
|
|
842
|
+
message: ERROR_MESSAGES.NO_DEPLOYMENT_FOUND,
|
|
843
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
844
|
+
};
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
yield {
|
|
848
|
+
type: "progress",
|
|
849
|
+
message: STATUS_MESSAGES.STARTING_WATCH(
|
|
850
|
+
deploymentId.slice(0, DEFAULTS.DEPLOYMENT_ID_SLICE_LENGTH)
|
|
851
|
+
),
|
|
852
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
853
|
+
};
|
|
854
|
+
let lastState = "";
|
|
855
|
+
let attempts = 0;
|
|
856
|
+
const maxAttempts = MAX_DEPLOYMENT_WATCH_ATTEMPTS;
|
|
857
|
+
const startTime = Date.now();
|
|
858
|
+
const maxWatchTime = MAX_WATCH_TIME_MS;
|
|
859
|
+
while (attempts < maxAttempts && Date.now() - startTime < maxWatchTime) {
|
|
860
|
+
try {
|
|
861
|
+
const deployment = await this.adapter.getDeploymentById(
|
|
862
|
+
deploymentId,
|
|
863
|
+
token
|
|
864
|
+
);
|
|
865
|
+
if (deployment.readyState !== lastState) {
|
|
866
|
+
lastState = deployment.readyState;
|
|
867
|
+
switch (deployment.readyState) {
|
|
868
|
+
case DEPLOYMENT_STATES.INITIALIZING:
|
|
869
|
+
yield {
|
|
870
|
+
type: "progress",
|
|
871
|
+
message: STATUS_MESSAGES.INITIALIZING,
|
|
872
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
873
|
+
};
|
|
874
|
+
break;
|
|
875
|
+
case DEPLOYMENT_STATES.BUILDING:
|
|
876
|
+
yield {
|
|
877
|
+
type: "progress",
|
|
878
|
+
message: STATUS_MESSAGES.BUILDING,
|
|
879
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
880
|
+
};
|
|
881
|
+
break;
|
|
882
|
+
case DEPLOYMENT_STATES.UPLOADING:
|
|
883
|
+
yield {
|
|
884
|
+
type: "progress",
|
|
885
|
+
message: STATUS_MESSAGES.UPLOADING,
|
|
886
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
887
|
+
};
|
|
888
|
+
break;
|
|
889
|
+
case DEPLOYMENT_STATES.DEPLOYING:
|
|
890
|
+
yield {
|
|
891
|
+
type: "progress",
|
|
892
|
+
message: STATUS_MESSAGES.DEPLOYING,
|
|
893
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
894
|
+
};
|
|
895
|
+
break;
|
|
896
|
+
case DEPLOYMENT_STATES.READY: {
|
|
897
|
+
const duration = deployment.buildingAt && deployment.ready ? deployment.ready - deployment.buildingAt : void 0;
|
|
898
|
+
yield {
|
|
899
|
+
type: "success",
|
|
900
|
+
message: STATUS_MESSAGES.DEPLOYMENT_SUCCESS,
|
|
901
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
902
|
+
details: {
|
|
903
|
+
url: deployment.url,
|
|
904
|
+
duration: duration ? Math.round(duration / BUILD_TIME_SECONDS_DIVISOR) : void 0
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
try {
|
|
908
|
+
const comparison = await this.compareWithPrevious(
|
|
909
|
+
args.project,
|
|
910
|
+
deploymentId,
|
|
911
|
+
token
|
|
912
|
+
);
|
|
913
|
+
if (comparison) {
|
|
914
|
+
yield {
|
|
915
|
+
type: "progress",
|
|
916
|
+
message: this.formatComparison(comparison),
|
|
917
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
} catch (e) {
|
|
921
|
+
console.error("Failed to compare deployments:", e);
|
|
922
|
+
}
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
case DEPLOYMENT_STATES.ERROR:
|
|
926
|
+
case DEPLOYMENT_STATES.CANCELED: {
|
|
927
|
+
const errorDetails = await this.analyzeError(
|
|
928
|
+
deploymentId,
|
|
929
|
+
token
|
|
930
|
+
);
|
|
931
|
+
yield {
|
|
932
|
+
type: "error",
|
|
933
|
+
message: STATUS_MESSAGES.DEPLOYMENT_FAILED(
|
|
934
|
+
errorDetails.message
|
|
935
|
+
),
|
|
936
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
937
|
+
details: {
|
|
938
|
+
suggestion: errorDetails.suggestion,
|
|
939
|
+
file: errorDetails.location
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (deployment.readyState === DEPLOYMENT_STATES.READY || deployment.readyState === DEPLOYMENT_STATES.ERROR || deployment.readyState === DEPLOYMENT_STATES.CANCELED) {
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
const pollInterval = this.getPollingInterval(lastState || "UNKNOWN");
|
|
950
|
+
if (pollInterval === 0) {
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
954
|
+
attempts++;
|
|
955
|
+
} catch (error) {
|
|
956
|
+
console.error("Error checking deployment status:", error);
|
|
957
|
+
attempts++;
|
|
958
|
+
const pollInterval = this.getPollingInterval(lastState || "UNKNOWN");
|
|
959
|
+
if (pollInterval === 0) {
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (attempts >= maxAttempts || Date.now() - startTime >= MAX_WATCH_TIME_MS) {
|
|
966
|
+
const timeoutSeconds = Math.round((Date.now() - startTime) / 1e3);
|
|
967
|
+
yield {
|
|
968
|
+
type: "warning",
|
|
969
|
+
message: `Deployment watch timed out after ${timeoutSeconds} seconds. The deployment may still be running.`,
|
|
970
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
971
|
+
details: {
|
|
972
|
+
suggestion: "Check the platform dashboard for the latest status"
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
} catch (error) {
|
|
977
|
+
yield {
|
|
978
|
+
type: "error",
|
|
979
|
+
message: `Error watching deployment: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
980
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
async compareDeployments(args) {
|
|
985
|
+
const token = args.token || process.env.VERCEL_TOKEN;
|
|
986
|
+
if (!token) {
|
|
987
|
+
throw new Error("No Vercel token provided");
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
const deployments = await this.adapter.getRecentDeployments(
|
|
991
|
+
args.project,
|
|
992
|
+
token,
|
|
993
|
+
args.count
|
|
994
|
+
);
|
|
995
|
+
if (deployments.length < 2) {
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
const [current, previous] = deployments;
|
|
999
|
+
const currentBuildTime = current.buildingAt && current.ready ? Math.round(
|
|
1000
|
+
(current.ready - current.buildingAt) / BUILD_TIME_SECONDS_DIVISOR
|
|
1001
|
+
) : 0;
|
|
1002
|
+
const previousBuildTime = previous.buildingAt && previous.ready ? Math.round(
|
|
1003
|
+
(previous.ready - previous.buildingAt) / BUILD_TIME_SECONDS_DIVISOR
|
|
1004
|
+
) : 0;
|
|
1005
|
+
const comparison = {
|
|
1006
|
+
deployments: {
|
|
1007
|
+
current: {
|
|
1008
|
+
id: current.uid || current.id,
|
|
1009
|
+
url: current.url ? `https://${current.url}` : void 0,
|
|
1010
|
+
timestamp: new Date(current.createdAt).toISOString(),
|
|
1011
|
+
commit: current.meta ? {
|
|
1012
|
+
sha: current.meta.githubCommitSha,
|
|
1013
|
+
message: current.meta.githubCommitMessage,
|
|
1014
|
+
author: current.meta.githubCommitAuthorName
|
|
1015
|
+
} : void 0,
|
|
1016
|
+
buildTime: currentBuildTime,
|
|
1017
|
+
status: this.mapDeploymentState(
|
|
1018
|
+
current.state || current.readyState
|
|
1019
|
+
),
|
|
1020
|
+
timeAgo: this.getTimeAgo(current.createdAt)
|
|
1021
|
+
},
|
|
1022
|
+
previous: {
|
|
1023
|
+
id: previous.uid || previous.id,
|
|
1024
|
+
url: previous.url ? `https://${previous.url}` : void 0,
|
|
1025
|
+
timestamp: new Date(previous.createdAt).toISOString(),
|
|
1026
|
+
commit: previous.meta ? {
|
|
1027
|
+
sha: previous.meta.githubCommitSha,
|
|
1028
|
+
message: previous.meta.githubCommitMessage,
|
|
1029
|
+
author: previous.meta.githubCommitAuthorName
|
|
1030
|
+
} : void 0,
|
|
1031
|
+
buildTime: previousBuildTime,
|
|
1032
|
+
status: this.mapDeploymentState(
|
|
1033
|
+
previous.state || previous.readyState
|
|
1034
|
+
),
|
|
1035
|
+
timeAgo: this.getTimeAgo(previous.createdAt)
|
|
1036
|
+
}
|
|
1037
|
+
},
|
|
1038
|
+
performance: {
|
|
1039
|
+
buildTime: {
|
|
1040
|
+
current: currentBuildTime,
|
|
1041
|
+
previous: previousBuildTime,
|
|
1042
|
+
delta: 0,
|
|
1043
|
+
percentage: 0
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
changes: {
|
|
1047
|
+
filesChanged: 0
|
|
1048
|
+
},
|
|
1049
|
+
risk: "LOW"
|
|
1050
|
+
};
|
|
1051
|
+
comparison.performance.buildTime.delta = comparison.performance.buildTime.current - comparison.performance.buildTime.previous;
|
|
1052
|
+
comparison.performance.buildTime.percentage = comparison.performance.buildTime.previous > 0 ? Math.round(
|
|
1053
|
+
comparison.performance.buildTime.delta / comparison.performance.buildTime.previous * DEFAULTS.PERCENTAGE_MULTIPLIER
|
|
1054
|
+
) : 0;
|
|
1055
|
+
if (Math.abs(comparison.performance.buildTime.percentage) > HIGH_RISK_THRESHOLD_PERCENT) {
|
|
1056
|
+
comparison.risk = "HIGH";
|
|
1057
|
+
} else if (Math.abs(comparison.performance.buildTime.percentage) > MEDIUM_RISK_THRESHOLD_PERCENT) {
|
|
1058
|
+
comparison.risk = "MEDIUM";
|
|
1059
|
+
}
|
|
1060
|
+
return comparison;
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
console.error("Error comparing deployments:", error);
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
async getDeploymentLogs(args) {
|
|
1067
|
+
const token = args.token || process.env.VERCEL_TOKEN;
|
|
1068
|
+
if (!token) {
|
|
1069
|
+
throw new Error("No Vercel token provided");
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
const logs = await this.adapter.getDeploymentLogs(
|
|
1073
|
+
args.deploymentId,
|
|
1074
|
+
token
|
|
1075
|
+
);
|
|
1076
|
+
let filteredLogs = logs;
|
|
1077
|
+
if (args.filter === "error") {
|
|
1078
|
+
const lines = logs.split("\n");
|
|
1079
|
+
filteredLogs = lines.filter((line) => LOG_FILTERS.ERROR.test(line)).join("\n");
|
|
1080
|
+
} else if (args.filter === "warning") {
|
|
1081
|
+
const lines = logs.split("\n");
|
|
1082
|
+
filteredLogs = lines.filter((line) => LOG_FILTERS.WARNING.test(line)).join("\n");
|
|
1083
|
+
}
|
|
1084
|
+
const analysis = this.analyzeLogs(filteredLogs);
|
|
1085
|
+
return {
|
|
1086
|
+
logs: filteredLogs || ERROR_MESSAGES.NO_LOGS_MATCHING_FILTER,
|
|
1087
|
+
analysis
|
|
1088
|
+
};
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
`Failed to get deployment logs: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
async analyzeError(deploymentId, token) {
|
|
1096
|
+
try {
|
|
1097
|
+
const logs = await this.adapter.getDeploymentLogs(deploymentId, token);
|
|
1098
|
+
return this.analyzeLogs(logs);
|
|
1099
|
+
} catch {
|
|
1100
|
+
return {
|
|
1101
|
+
type: "UNKNOWN",
|
|
1102
|
+
message: "Deployment failed - unable to fetch logs",
|
|
1103
|
+
suggestion: "Check deployment platform dashboard"
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
analyzeLogs(logs) {
|
|
1108
|
+
const lowerLogs = logs.toLowerCase();
|
|
1109
|
+
const lines = logs.split("\n");
|
|
1110
|
+
let type = "UNKNOWN";
|
|
1111
|
+
let quickContext = "";
|
|
1112
|
+
let location;
|
|
1113
|
+
let errorLine;
|
|
1114
|
+
for (const line of lines) {
|
|
1115
|
+
const fileMatch = line.match(/([^\s:]+\.(tsx?|jsx?|js|ts)):(\d+):(\d+)/);
|
|
1116
|
+
if (fileMatch && line.toLowerCase().includes("error")) {
|
|
1117
|
+
location = `${fileMatch[1]}:${fileMatch[3]}:${fileMatch[4]}`;
|
|
1118
|
+
errorLine = line.trim();
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (!errorLine) {
|
|
1123
|
+
errorLine = lines.find((line) => line.toLowerCase().includes("error"))?.trim() || "Check logs for details";
|
|
1124
|
+
}
|
|
1125
|
+
if (lowerLogs.includes("cannot find module") || lowerLogs.includes("modulenotfounderror")) {
|
|
1126
|
+
type = "MISSING_DEPENDENCY";
|
|
1127
|
+
quickContext = "Missing package/dependency";
|
|
1128
|
+
} else if (lowerLogs.includes("environment variable") || lowerLogs.includes("env var")) {
|
|
1129
|
+
type = "ENV_VAR";
|
|
1130
|
+
quickContext = "Environment variable issue";
|
|
1131
|
+
} else if (lowerLogs.includes("timeout") || lowerLogs.includes("time limit")) {
|
|
1132
|
+
type = "TIMEOUT";
|
|
1133
|
+
quickContext = "Build timeout exceeded";
|
|
1134
|
+
} else if (lowerLogs.includes("error") || lowerLogs.includes("failed")) {
|
|
1135
|
+
type = "BUILD";
|
|
1136
|
+
quickContext = "Build failed";
|
|
1137
|
+
}
|
|
1138
|
+
return {
|
|
1139
|
+
type,
|
|
1140
|
+
message: quickContext || errorLine,
|
|
1141
|
+
location,
|
|
1142
|
+
suggestion: location ? `Error at ${location} - check the file for issues` : "See full logs below for detailed analysis"
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
async compareWithPrevious(project, currentDeploymentId, token) {
|
|
1146
|
+
try {
|
|
1147
|
+
const deployments = await this.adapter.getRecentDeployments(
|
|
1148
|
+
project,
|
|
1149
|
+
token,
|
|
1150
|
+
DEFAULT_COMPARISON_COUNT
|
|
1151
|
+
);
|
|
1152
|
+
if (deployments.length < 2) {
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
const current = deployments.find((d) => d.id === currentDeploymentId) || deployments[0];
|
|
1156
|
+
const previous = deployments.find((d) => d.id !== currentDeploymentId) || deployments[1];
|
|
1157
|
+
const currentBuildTime = current.buildingAt && current.ready ? Math.round(
|
|
1158
|
+
(current.ready - current.buildingAt) / BUILD_TIME_SECONDS_DIVISOR
|
|
1159
|
+
) : 0;
|
|
1160
|
+
const previousBuildTime = previous.buildingAt && previous.ready ? Math.round(
|
|
1161
|
+
(previous.ready - previous.buildingAt) / BUILD_TIME_SECONDS_DIVISOR
|
|
1162
|
+
) : 0;
|
|
1163
|
+
const delta = currentBuildTime - previousBuildTime;
|
|
1164
|
+
const percentage = previousBuildTime > 0 ? Math.round(
|
|
1165
|
+
delta / previousBuildTime * DEFAULTS.PERCENTAGE_MULTIPLIER
|
|
1166
|
+
) : 0;
|
|
1167
|
+
return {
|
|
1168
|
+
deployments: {
|
|
1169
|
+
current: {
|
|
1170
|
+
id: current.uid || current.id || "",
|
|
1171
|
+
url: current.url,
|
|
1172
|
+
timestamp: current.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1173
|
+
status: current.state || "unknown",
|
|
1174
|
+
buildTime: currentBuildTime,
|
|
1175
|
+
timeAgo: this.getTimeAgo(new Date(current.createdAt).getTime())
|
|
1176
|
+
},
|
|
1177
|
+
previous: {
|
|
1178
|
+
id: previous.uid || previous.id || "",
|
|
1179
|
+
url: previous.url,
|
|
1180
|
+
timestamp: previous.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1181
|
+
status: previous.state || "unknown",
|
|
1182
|
+
buildTime: previousBuildTime,
|
|
1183
|
+
timeAgo: this.getTimeAgo(new Date(previous.createdAt).getTime())
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
1186
|
+
performance: {
|
|
1187
|
+
buildTime: {
|
|
1188
|
+
current: currentBuildTime,
|
|
1189
|
+
previous: previousBuildTime,
|
|
1190
|
+
delta,
|
|
1191
|
+
percentage
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
changes: {},
|
|
1195
|
+
risk: Math.abs(percentage) > HIGH_RISK_THRESHOLD_PERCENT ? "HIGH" : Math.abs(percentage) > MEDIUM_RISK_THRESHOLD_PERCENT ? "MEDIUM" : "LOW"
|
|
1196
|
+
};
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
console.error("Error comparing with previous deployment:", error);
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
formatComparison(comparison) {
|
|
1203
|
+
const { buildTime } = comparison.performance;
|
|
1204
|
+
if (buildTime.delta === 0) {
|
|
1205
|
+
return STATUS_MESSAGES.BUILD_TIME_SAME(buildTime.current);
|
|
1206
|
+
}
|
|
1207
|
+
const faster = buildTime.delta < 0;
|
|
1208
|
+
return STATUS_MESSAGES.BUILD_TIME_CHANGE(
|
|
1209
|
+
buildTime.current,
|
|
1210
|
+
buildTime.delta,
|
|
1211
|
+
faster
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
// src/core/response-formatter.ts
|
|
1217
|
+
var ResponseFormatter = class {
|
|
1218
|
+
static formatComparison(data) {
|
|
1219
|
+
const current = data.deployments.current;
|
|
1220
|
+
const previous = data.deployments.previous;
|
|
1221
|
+
const perf = data.performance.buildTime;
|
|
1222
|
+
const display = `## Deployment Comparison
|
|
1223
|
+
|
|
1224
|
+
### Current Deployment
|
|
1225
|
+
**Status:** ${current.status === "success" ? "\u2705 Success" : "\u274C Failed"}
|
|
1226
|
+
**URL:** ${current.url || "N/A"}
|
|
1227
|
+
**Time:** ${current.timeAgo}
|
|
1228
|
+
**Build Duration:** ${current.buildTime}s
|
|
1229
|
+
|
|
1230
|
+
**Commit:** \`${current.commit?.sha?.slice(0, 7) || "N/A"}\` ${current.commit?.message || "No message"}
|
|
1231
|
+
**Author:** ${current.commit?.author || "Unknown"}
|
|
1232
|
+
|
|
1233
|
+
### Previous Deployment
|
|
1234
|
+
**Status:** ${previous.status === "success" ? "\u2705 Success" : "\u274C Failed"}
|
|
1235
|
+
**URL:** ${previous.url || "N/A"}
|
|
1236
|
+
**Time:** ${previous.timeAgo}
|
|
1237
|
+
**Build Duration:** ${previous.buildTime}s
|
|
1238
|
+
|
|
1239
|
+
**Commit:** \`${previous.commit?.sha?.slice(0, 7) || "N/A"}\` ${previous.commit?.message || "No message"}
|
|
1240
|
+
**Author:** ${previous.commit?.author || "Unknown"}
|
|
1241
|
+
|
|
1242
|
+
### Performance Analysis
|
|
1243
|
+
**Build Time Change:** ${Math.abs(perf.delta)}s ${perf.delta < 0 ? "faster" : "slower"} (${perf.percentage}% ${perf.delta < 0 ? "improvement" : "increase"})
|
|
1244
|
+
**Risk Level:** ${data.risk === "LOW" ? "\u{1F7E2} Low" : data.risk === "MEDIUM" ? "\u{1F7E1} Medium" : "\u{1F534} High"}
|
|
1245
|
+
`;
|
|
1246
|
+
return {
|
|
1247
|
+
version: "1.0",
|
|
1248
|
+
tool: "compare_deployments",
|
|
1249
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1250
|
+
display,
|
|
1251
|
+
data,
|
|
1252
|
+
highlights: {
|
|
1253
|
+
url: current.url,
|
|
1254
|
+
status: current.status,
|
|
1255
|
+
duration: `${perf.delta}s ${perf.delta < 0 ? "faster" : "slower"}`
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
static formatLogs(logs, analysis, summary) {
|
|
1260
|
+
const truncatedLogs = logs.split("\n").slice(0, 30).join("\n");
|
|
1261
|
+
const isTruncated = logs.split("\n").length > 30;
|
|
1262
|
+
const display = `## Deployment Logs
|
|
1263
|
+
|
|
1264
|
+
### Summary
|
|
1265
|
+
**Errors:** ${summary.errorCount === 0 ? "\u2705" : "\u274C"} ${summary.errorCount} error${summary.errorCount !== 1 ? "s" : ""}
|
|
1266
|
+
**Warnings:** ${summary.warningCount === 0 ? "\u2705" : "\u26A0\uFE0F"} ${summary.warningCount} warning${summary.warningCount !== 1 ? "s" : ""}
|
|
1267
|
+
**URL:** ${summary.deploymentUrl || "Not available"}
|
|
1268
|
+
|
|
1269
|
+
${analysis.type !== "UNKNOWN" ? `### Error Analysis
|
|
1270
|
+
**Type:** ${analysis.type}
|
|
1271
|
+
**Message:** ${analysis.message}
|
|
1272
|
+
**Location:** ${analysis.location ? `\`${analysis.location}\`` : "Unknown"}
|
|
1273
|
+
**Suggested Fix:** ${analysis.suggestion || "Check the logs below for details"}
|
|
1274
|
+
|
|
1275
|
+
` : ""}
|
|
1276
|
+
### Logs
|
|
1277
|
+
\`\`\`
|
|
1278
|
+
${truncatedLogs}
|
|
1279
|
+
${isTruncated ? "\n... (truncated - showing first 30 lines)" : ""}
|
|
1280
|
+
\`\`\`
|
|
1281
|
+
`;
|
|
1282
|
+
return {
|
|
1283
|
+
version: "1.0",
|
|
1284
|
+
tool: "get_deployment_logs",
|
|
1285
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1286
|
+
display,
|
|
1287
|
+
data: { logs, analysis, summary },
|
|
1288
|
+
highlights: {
|
|
1289
|
+
url: summary.deploymentUrl,
|
|
1290
|
+
error: analysis.message,
|
|
1291
|
+
status: summary.hasErrors ? "error" : "success"
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
static formatStatus(status) {
|
|
1296
|
+
const statusIcon = status.status === "success" ? "\u2705 Success" : status.status === "building" ? "\u{1F504} Building" : status.status === "error" ? "\u274C Failed" : status.status;
|
|
1297
|
+
const display = `## Deployment Status
|
|
1298
|
+
|
|
1299
|
+
### Current Status
|
|
1300
|
+
**Project:** ${status.projectName}
|
|
1301
|
+
**Platform:** ${status.platform}
|
|
1302
|
+
**Status:** ${statusIcon}
|
|
1303
|
+
**Environment:** ${status.environment || "production"}
|
|
1304
|
+
**URL:** ${status.url || "Not available"}
|
|
1305
|
+
**Duration:** ${status.duration ? `${status.duration}s` : "N/A"}
|
|
1306
|
+
**Deployed:** ${status.timestamp}
|
|
1307
|
+
|
|
1308
|
+
${status.commit ? `### Commit Info
|
|
1309
|
+
**SHA:** \`${status.commit.sha}\`
|
|
1310
|
+
**Message:** ${status.commit.message}
|
|
1311
|
+
**Author:** ${status.commit.author}
|
|
1312
|
+
` : ""}`;
|
|
1313
|
+
return {
|
|
1314
|
+
version: "1.0",
|
|
1315
|
+
tool: "check_deployment_status",
|
|
1316
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1317
|
+
display,
|
|
1318
|
+
data: status,
|
|
1319
|
+
highlights: {
|
|
1320
|
+
url: status.url,
|
|
1321
|
+
status: status.status,
|
|
1322
|
+
duration: status.duration ? `${status.duration}s` : void 0
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
static formatWatchEvent(event) {
|
|
1327
|
+
const icons = {
|
|
1328
|
+
progress: "\u{1F504}",
|
|
1329
|
+
success: "\u2705",
|
|
1330
|
+
error: "\u274C",
|
|
1331
|
+
warning: "\u26A0\uFE0F"
|
|
1332
|
+
};
|
|
1333
|
+
let formatted = `${icons[event.type] || ""} **${event.message}**`;
|
|
1334
|
+
if (event.details) {
|
|
1335
|
+
if (event.details.url) {
|
|
1336
|
+
formatted += `
|
|
1337
|
+
URL: ${event.details.url}`;
|
|
1338
|
+
}
|
|
1339
|
+
if (event.details.duration) {
|
|
1340
|
+
formatted += `
|
|
1341
|
+
Duration: ${event.details.duration}s`;
|
|
1342
|
+
}
|
|
1343
|
+
if (event.details.suggestion) {
|
|
1344
|
+
formatted += `
|
|
1345
|
+
\u{1F4A1} ${event.details.suggestion}`;
|
|
1346
|
+
}
|
|
1347
|
+
if (event.details.file) {
|
|
1348
|
+
formatted += `
|
|
1349
|
+
File: \`${event.details.file}\``;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return formatted;
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
// src/core/mcp-handler.ts
|
|
1357
|
+
var MCPHandler = class {
|
|
1358
|
+
constructor(adapters) {
|
|
1359
|
+
this.adapters = adapters;
|
|
1360
|
+
this.startCacheCleanup();
|
|
1361
|
+
}
|
|
1362
|
+
deploymentIntelligenceCache = /* @__PURE__ */ new Map();
|
|
1363
|
+
cacheTimestamps = /* @__PURE__ */ new Map();
|
|
1364
|
+
maxCacheAge = DEFAULTS.CACHE_TTL_MS;
|
|
1365
|
+
cleanupTimer;
|
|
1366
|
+
getDeploymentIntelligence(platform) {
|
|
1367
|
+
const now = Date.now();
|
|
1368
|
+
const cached = this.deploymentIntelligenceCache.get(platform);
|
|
1369
|
+
const timestamp = this.cacheTimestamps.get(platform);
|
|
1370
|
+
if (cached && timestamp && now - timestamp < this.maxCacheAge) {
|
|
1371
|
+
this.cacheTimestamps.set(platform, now);
|
|
1372
|
+
return cached;
|
|
1373
|
+
}
|
|
1374
|
+
if (cached) {
|
|
1375
|
+
this.deploymentIntelligenceCache.delete(platform);
|
|
1376
|
+
this.cacheTimestamps.delete(platform);
|
|
1377
|
+
}
|
|
1378
|
+
const instance = new DeploymentIntelligence(platform);
|
|
1379
|
+
this.deploymentIntelligenceCache.set(platform, instance);
|
|
1380
|
+
this.cacheTimestamps.set(platform, now);
|
|
1381
|
+
if (this.deploymentIntelligenceCache.size > DEFAULTS.MAX_CACHE_SIZE) {
|
|
1382
|
+
this.evictOldestEntry();
|
|
1383
|
+
}
|
|
1384
|
+
return instance;
|
|
1385
|
+
}
|
|
1386
|
+
evictOldestEntry() {
|
|
1387
|
+
let oldestPlatform;
|
|
1388
|
+
let oldestTime = Date.now();
|
|
1389
|
+
for (const [platform, timestamp] of this.cacheTimestamps.entries()) {
|
|
1390
|
+
if (timestamp < oldestTime) {
|
|
1391
|
+
oldestTime = timestamp;
|
|
1392
|
+
oldestPlatform = platform;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (oldestPlatform) {
|
|
1396
|
+
this.deploymentIntelligenceCache.delete(oldestPlatform);
|
|
1397
|
+
this.cacheTimestamps.delete(oldestPlatform);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
startCacheCleanup() {
|
|
1401
|
+
this.cleanupTimer = setInterval(() => {
|
|
1402
|
+
const now = Date.now();
|
|
1403
|
+
const platformsToDelete = [];
|
|
1404
|
+
for (const [platform, timestamp] of this.cacheTimestamps.entries()) {
|
|
1405
|
+
if (now - timestamp > this.maxCacheAge) {
|
|
1406
|
+
platformsToDelete.push(platform);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
for (const platform of platformsToDelete) {
|
|
1410
|
+
this.deploymentIntelligenceCache.delete(platform);
|
|
1411
|
+
this.cacheTimestamps.delete(platform);
|
|
1412
|
+
}
|
|
1413
|
+
}, DEFAULTS.CACHE_CLEANUP_INTERVAL_MS);
|
|
1414
|
+
}
|
|
1415
|
+
dispose() {
|
|
1416
|
+
if (this.cleanupTimer) {
|
|
1417
|
+
clearInterval(this.cleanupTimer);
|
|
1418
|
+
}
|
|
1419
|
+
this.deploymentIntelligenceCache.clear();
|
|
1420
|
+
this.cacheTimestamps.clear();
|
|
1421
|
+
}
|
|
1422
|
+
async handleToolCall(tool, args) {
|
|
1423
|
+
switch (tool) {
|
|
1424
|
+
case "check_deployment_status":
|
|
1425
|
+
return this.checkDeploymentStatus(args);
|
|
1426
|
+
case "watch_deployment":
|
|
1427
|
+
return this.watchDeployment(args);
|
|
1428
|
+
case "compare_deployments":
|
|
1429
|
+
return this.compareDeployments(args);
|
|
1430
|
+
case "get_deployment_logs":
|
|
1431
|
+
return this.getDeploymentLogs(args);
|
|
1432
|
+
default:
|
|
1433
|
+
throw new Error(`Unknown tool: ${tool}`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
async handleRequest(request) {
|
|
1437
|
+
if (request.method === "tools/call") {
|
|
1438
|
+
const { name, arguments: args } = request.params;
|
|
1439
|
+
const result = await this.handleToolCall(name, args);
|
|
1440
|
+
const text = result.display ? result.display : JSON.stringify(result, null, 2);
|
|
1441
|
+
return {
|
|
1442
|
+
content: [
|
|
1443
|
+
{
|
|
1444
|
+
type: "text",
|
|
1445
|
+
text
|
|
1446
|
+
}
|
|
1447
|
+
]
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
if (request.method === "tools/list") {
|
|
1451
|
+
return {
|
|
1452
|
+
tools: tools.map((tool) => ({
|
|
1453
|
+
...tool,
|
|
1454
|
+
inputSchema: this.schemaToJsonSchema(tool.inputSchema)
|
|
1455
|
+
}))
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
throw new Error(`Unknown method: ${request.method}`);
|
|
1459
|
+
}
|
|
1460
|
+
async checkDeploymentStatus(args) {
|
|
1461
|
+
const validated = checkDeploymentStatusSchema.parse(args);
|
|
1462
|
+
const adapter = this.adapters.get(validated.platform);
|
|
1463
|
+
if (!adapter) {
|
|
1464
|
+
throw new Error(`Unsupported platform: ${validated.platform}`);
|
|
1465
|
+
}
|
|
1466
|
+
try {
|
|
1467
|
+
const status = await adapter.getLatestDeployment(
|
|
1468
|
+
validated.project,
|
|
1469
|
+
validated.token
|
|
1470
|
+
);
|
|
1471
|
+
const formattedStatus = this.formatResponse(status, validated.platform);
|
|
1472
|
+
return ResponseFormatter.formatStatus(formattedStatus);
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
console.error(`Error checking deployment status: ${error}`);
|
|
1475
|
+
const errorStatus = {
|
|
1476
|
+
status: "error",
|
|
1477
|
+
platform: validated.platform,
|
|
1478
|
+
projectName: validated.project,
|
|
1479
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1480
|
+
};
|
|
1481
|
+
return ResponseFormatter.formatStatus(errorStatus);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
formatResponse(status, platform) {
|
|
1485
|
+
return {
|
|
1486
|
+
...status,
|
|
1487
|
+
platform,
|
|
1488
|
+
timestamp: status.timestamp || (/* @__PURE__ */ new Date()).toISOString()
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
async watchDeployment(args) {
|
|
1492
|
+
const validated = watchDeploymentSchema.parse(args);
|
|
1493
|
+
const intelligence = this.getDeploymentIntelligence(validated.platform);
|
|
1494
|
+
const messages = [];
|
|
1495
|
+
const events = [];
|
|
1496
|
+
const generator = intelligence.watchDeployment(validated);
|
|
1497
|
+
for await (const event of generator) {
|
|
1498
|
+
const formatted = ResponseFormatter.formatWatchEvent(event);
|
|
1499
|
+
messages.push(formatted);
|
|
1500
|
+
events.push(event);
|
|
1501
|
+
}
|
|
1502
|
+
const display = `## Deployment Watch
|
|
1503
|
+
|
|
1504
|
+
### Real-time Updates
|
|
1505
|
+
${messages.join("\n\n")}
|
|
1506
|
+
`;
|
|
1507
|
+
const finalEvent = events[events.length - 1];
|
|
1508
|
+
const status = finalEvent?.type === "success" ? "success" : finalEvent?.type === "error" ? "error" : "in_progress";
|
|
1509
|
+
return {
|
|
1510
|
+
version: "1.0",
|
|
1511
|
+
tool: "watch_deployment",
|
|
1512
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1513
|
+
display,
|
|
1514
|
+
data: { events },
|
|
1515
|
+
highlights: {
|
|
1516
|
+
status,
|
|
1517
|
+
url: finalEvent?.details?.url,
|
|
1518
|
+
duration: finalEvent?.details?.duration ? `${finalEvent.details.duration}s` : void 0
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
async compareDeployments(args) {
|
|
1523
|
+
const validated = compareDeploymentsSchema.parse(args);
|
|
1524
|
+
const intelligence = this.getDeploymentIntelligence(validated.platform);
|
|
1525
|
+
const comparison = await intelligence.compareDeployments(validated);
|
|
1526
|
+
if (!comparison) {
|
|
1527
|
+
return {
|
|
1528
|
+
version: "1.0",
|
|
1529
|
+
tool: "compare_deployments",
|
|
1530
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1531
|
+
display: "Not enough deployments to compare",
|
|
1532
|
+
data: { message: "Not enough deployments to compare" },
|
|
1533
|
+
highlights: { status: "error" }
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
return ResponseFormatter.formatComparison(comparison);
|
|
1537
|
+
}
|
|
1538
|
+
async getDeploymentLogs(args) {
|
|
1539
|
+
const validated = getDeploymentLogsSchema.parse(args);
|
|
1540
|
+
const intelligence = this.getDeploymentIntelligence(validated.platform);
|
|
1541
|
+
const result = await intelligence.getDeploymentLogs(validated);
|
|
1542
|
+
const summary = {
|
|
1543
|
+
errorCount: (result.logs.match(/error/gi) || []).length,
|
|
1544
|
+
warningCount: (result.logs.match(/warning/gi) || []).length,
|
|
1545
|
+
deploymentUrl: null,
|
|
1546
|
+
hasErrors: result.analysis?.type !== "UNKNOWN"
|
|
1547
|
+
};
|
|
1548
|
+
return ResponseFormatter.formatLogs(
|
|
1549
|
+
result.logs,
|
|
1550
|
+
result.analysis || { type: "UNKNOWN", message: "No errors detected" },
|
|
1551
|
+
summary
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
schemaToJsonSchema(zodSchema) {
|
|
1555
|
+
return JSON.parse(JSON.stringify(zodSchema));
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
|
|
1559
|
+
export {
|
|
1560
|
+
tools,
|
|
1561
|
+
VercelAdapter,
|
|
1562
|
+
MCPHandler
|
|
1563
|
+
};
|