deploy-mcp 0.2.0 → 0.4.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 +255 -418
- package/dist/chunk-V4XY4X6P.js +2007 -0
- package/dist/index.js +9 -30
- package/dist/worker.js +496 -920
- package/package.json +17 -8
- package/dist/chunk-QRZL43CY.js +0 -297
|
@@ -0,0 +1,2007 @@
|
|
|
1
|
+
// src/core/tools.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var SUPPORTED_PLATFORMS = ["vercel", "netlify"];
|
|
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
|
+
mode: z.enum([
|
|
19
|
+
"last_vs_previous",
|
|
20
|
+
// Default: current vs previous
|
|
21
|
+
"current_vs_success",
|
|
22
|
+
// Compare to last successful deploy
|
|
23
|
+
"current_vs_production",
|
|
24
|
+
// Compare to what's in production
|
|
25
|
+
"between_dates",
|
|
26
|
+
// Compare deployments from specific dates
|
|
27
|
+
"by_ids"
|
|
28
|
+
// Compare two specific deployment IDs
|
|
29
|
+
]).default("last_vs_previous").describe("Comparison mode to use"),
|
|
30
|
+
deploymentA: z.string().optional().describe("First deployment ID (for by_ids mode)"),
|
|
31
|
+
deploymentB: z.string().optional().describe("Second deployment ID (for by_ids mode)"),
|
|
32
|
+
dateFrom: z.string().optional().describe("Start date (for between_dates mode, ISO format)"),
|
|
33
|
+
dateTo: z.string().optional().describe("End date (for between_dates mode, ISO format)"),
|
|
34
|
+
token: z.string().optional().describe("API token for authentication (optional if set in environment)")
|
|
35
|
+
});
|
|
36
|
+
var getDeploymentLogsSchema = z.object({
|
|
37
|
+
platform: z.enum(SUPPORTED_PLATFORMS).describe("The deployment platform"),
|
|
38
|
+
deploymentId: z.string().describe("The deployment ID or 'latest' for most recent"),
|
|
39
|
+
project: z.string().optional().describe(
|
|
40
|
+
"Project/site name (required when using 'latest' as deploymentId)"
|
|
41
|
+
),
|
|
42
|
+
filter: z.enum(["error", "warning", "all"]).default("error").describe("Filter logs by type (default: error)"),
|
|
43
|
+
token: z.string().optional().describe("API token for authentication (optional if set in environment)")
|
|
44
|
+
});
|
|
45
|
+
var tools = [
|
|
46
|
+
{
|
|
47
|
+
name: "check_deployment_status",
|
|
48
|
+
description: "Check the latest deployment status for a project on a platform",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
platform: {
|
|
53
|
+
type: "string",
|
|
54
|
+
enum: ["vercel", "netlify"],
|
|
55
|
+
description: "The deployment platform"
|
|
56
|
+
},
|
|
57
|
+
project: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "The project name or ID"
|
|
60
|
+
},
|
|
61
|
+
token: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "API token for authentication (optional if set in environment)"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
required: ["platform", "project"]
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "watch_deployment",
|
|
71
|
+
description: "Stream real-time deployment progress with detailed status updates and error information",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
platform: {
|
|
76
|
+
type: "string",
|
|
77
|
+
enum: ["vercel", "netlify"],
|
|
78
|
+
description: "The deployment platform"
|
|
79
|
+
},
|
|
80
|
+
project: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "The project name or ID"
|
|
83
|
+
},
|
|
84
|
+
deploymentId: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Specific deployment ID to watch (optional, defaults to latest)"
|
|
87
|
+
},
|
|
88
|
+
token: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "API token for authentication (optional if set in environment)"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
required: ["platform", "project"]
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "compare_deployments",
|
|
98
|
+
description: "Compare deployments using smart comparison modes to identify changes, performance differences, and potential issues",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
platform: {
|
|
103
|
+
type: "string",
|
|
104
|
+
enum: ["vercel", "netlify"],
|
|
105
|
+
description: "The deployment platform"
|
|
106
|
+
},
|
|
107
|
+
project: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "The project name or ID"
|
|
110
|
+
},
|
|
111
|
+
mode: {
|
|
112
|
+
type: "string",
|
|
113
|
+
enum: [
|
|
114
|
+
"last_vs_previous",
|
|
115
|
+
"current_vs_success",
|
|
116
|
+
"current_vs_production",
|
|
117
|
+
"between_dates",
|
|
118
|
+
"by_ids"
|
|
119
|
+
],
|
|
120
|
+
default: "last_vs_previous",
|
|
121
|
+
description: "Comparison mode: last_vs_previous (default), current_vs_success, current_vs_production, between_dates, or by_ids"
|
|
122
|
+
},
|
|
123
|
+
deploymentA: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "First deployment ID (required for by_ids mode)"
|
|
126
|
+
},
|
|
127
|
+
deploymentB: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "Second deployment ID (required for by_ids mode)"
|
|
130
|
+
},
|
|
131
|
+
dateFrom: {
|
|
132
|
+
type: "string",
|
|
133
|
+
description: "Start date in ISO format (required for between_dates mode)"
|
|
134
|
+
},
|
|
135
|
+
dateTo: {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "End date in ISO format (required for between_dates mode)"
|
|
138
|
+
},
|
|
139
|
+
token: {
|
|
140
|
+
type: "string",
|
|
141
|
+
description: "API token for authentication (optional if set in environment)"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
required: ["platform", "project"]
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "get_deployment_logs",
|
|
149
|
+
description: "Fetch detailed logs for a specific deployment, useful for debugging failed deployments",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
platform: {
|
|
154
|
+
type: "string",
|
|
155
|
+
enum: ["vercel", "netlify"],
|
|
156
|
+
description: "The deployment platform"
|
|
157
|
+
},
|
|
158
|
+
deploymentId: {
|
|
159
|
+
type: "string",
|
|
160
|
+
description: "The deployment ID or 'latest' for most recent"
|
|
161
|
+
},
|
|
162
|
+
project: {
|
|
163
|
+
type: "string",
|
|
164
|
+
description: "Project/site name (required when using 'latest' as deploymentId)"
|
|
165
|
+
},
|
|
166
|
+
filter: {
|
|
167
|
+
type: "string",
|
|
168
|
+
enum: ["error", "warning", "all"],
|
|
169
|
+
default: "error",
|
|
170
|
+
description: "Filter logs by type (default: error)"
|
|
171
|
+
},
|
|
172
|
+
token: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "API token for authentication (optional if set in environment)"
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
required: ["platform", "deploymentId"]
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
// src/adapters/base/adapter.ts
|
|
183
|
+
var BaseAdapter = class {
|
|
184
|
+
formatTimestamp(date) {
|
|
185
|
+
return new Date(date).toISOString();
|
|
186
|
+
}
|
|
187
|
+
calculateDuration(start, end) {
|
|
188
|
+
const startTime = new Date(start).getTime();
|
|
189
|
+
const endTime = end ? new Date(end).getTime() : Date.now();
|
|
190
|
+
return Math.floor((endTime - startTime) / 1e3);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/adapters/base/types.ts
|
|
195
|
+
var AdapterException = class extends Error {
|
|
196
|
+
constructor(type, message, originalError) {
|
|
197
|
+
super(message);
|
|
198
|
+
this.type = type;
|
|
199
|
+
this.originalError = originalError;
|
|
200
|
+
this.name = "AdapterException";
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/core/constants.ts
|
|
205
|
+
var MAX_DEPLOYMENT_WATCH_ATTEMPTS = 120;
|
|
206
|
+
var BUILD_TIME_SECONDS_DIVISOR = 1e3;
|
|
207
|
+
var MAX_WATCH_TIME_MS = 24e4;
|
|
208
|
+
var MAX_BACKOFF_DELAY_MS = 3e4;
|
|
209
|
+
var BACKOFF_JITTER_MS = 1e3;
|
|
210
|
+
var HIGH_RISK_THRESHOLD_PERCENT = 50;
|
|
211
|
+
var MEDIUM_RISK_THRESHOLD_PERCENT = 20;
|
|
212
|
+
var DEFAULT_COMPARISON_COUNT = 2;
|
|
213
|
+
var SINGLE_DEPLOYMENT_FETCH = 1;
|
|
214
|
+
var MAX_TOKENS_PER_MINUTE = 30;
|
|
215
|
+
var RATE_LIMITER_CLEANUP_AGE_MS = 36e5;
|
|
216
|
+
var DEPLOYMENT_STATES = {
|
|
217
|
+
INITIALIZING: "INITIALIZING",
|
|
218
|
+
BUILDING: "BUILDING",
|
|
219
|
+
UPLOADING: "UPLOADING",
|
|
220
|
+
DEPLOYING: "DEPLOYING",
|
|
221
|
+
READY: "READY",
|
|
222
|
+
ERROR: "ERROR",
|
|
223
|
+
CANCELED: "CANCELED"
|
|
224
|
+
};
|
|
225
|
+
var POLLING_INTERVALS_BY_STATE = {
|
|
226
|
+
INITIALIZING: 5e3,
|
|
227
|
+
// 5s - slower at start
|
|
228
|
+
BUILDING: 3e3,
|
|
229
|
+
// 3s - active building
|
|
230
|
+
UPLOADING: 2e3,
|
|
231
|
+
// 2s - final stages
|
|
232
|
+
DEPLOYING: 2e3,
|
|
233
|
+
// 2s - final stages
|
|
234
|
+
READY: 0,
|
|
235
|
+
// Stop polling
|
|
236
|
+
ERROR: 0,
|
|
237
|
+
// Stop polling
|
|
238
|
+
CANCELED: 0,
|
|
239
|
+
// Stop polling
|
|
240
|
+
UNKNOWN: 1e4
|
|
241
|
+
// 10s - unknown states
|
|
242
|
+
};
|
|
243
|
+
var LOG_FILTERS = {
|
|
244
|
+
ERROR: /error|fail|exception|critical/i,
|
|
245
|
+
WARNING: /warning|warn|deprecat/i
|
|
246
|
+
};
|
|
247
|
+
var DEFAULTS = {
|
|
248
|
+
DEPLOYMENT_ID_SLICE_LENGTH: 7,
|
|
249
|
+
ERROR_LINE_TRIM_INDEX: 0,
|
|
250
|
+
PERCENTAGE_MULTIPLIER: 100,
|
|
251
|
+
MAX_EVENT_BUFFER_SIZE: 100,
|
|
252
|
+
MAX_CACHE_SIZE: 10,
|
|
253
|
+
CACHE_TTL_MS: 30 * 60 * 1e3,
|
|
254
|
+
// 30 minutes
|
|
255
|
+
CACHE_CLEANUP_INTERVAL_MS: 5 * 60 * 1e3
|
|
256
|
+
// 5 minutes
|
|
257
|
+
};
|
|
258
|
+
var ERROR_MESSAGES = {
|
|
259
|
+
NO_TOKEN: "No token provided",
|
|
260
|
+
NO_DEPLOYMENT_FOUND: "No deployment found for this project",
|
|
261
|
+
DEPLOYMENT_TAKING_LONG: "\u26A0\uFE0F Deployment is taking longer than expected",
|
|
262
|
+
NO_LOGS_AVAILABLE: "No logs available",
|
|
263
|
+
NO_LOGS_MATCHING_FILTER: "No logs matching filter criteria"
|
|
264
|
+
};
|
|
265
|
+
var STATUS_ICONS = {
|
|
266
|
+
ROCKET: "\u{1F680}",
|
|
267
|
+
HOURGLASS: "\u23F3",
|
|
268
|
+
HAMMER: "\u{1F528}",
|
|
269
|
+
PACKAGE: "\u{1F4E6}",
|
|
270
|
+
GLOBE: "\u{1F30D}",
|
|
271
|
+
SUCCESS: "\u2705",
|
|
272
|
+
ERROR: "\u274C",
|
|
273
|
+
WARNING: "\u26A0\uFE0F",
|
|
274
|
+
CHART: "\u{1F4CA}",
|
|
275
|
+
LIGHTNING: "\u26A1",
|
|
276
|
+
TURTLE: "\u{1F422}",
|
|
277
|
+
LIGHTBULB: "\u{1F4A1}",
|
|
278
|
+
PIN: "\u{1F4CD}"
|
|
279
|
+
};
|
|
280
|
+
var STATUS_MESSAGES = {
|
|
281
|
+
STARTING_WATCH: (id) => `${STATUS_ICONS.ROCKET} Starting to watch deployment ${id}...`,
|
|
282
|
+
INITIALIZING: `${STATUS_ICONS.HOURGLASS} Initializing deployment...`,
|
|
283
|
+
BUILDING: `${STATUS_ICONS.HAMMER} Building application...`,
|
|
284
|
+
UPLOADING: `${STATUS_ICONS.PACKAGE} Uploading to edge network...`,
|
|
285
|
+
DEPLOYING: `${STATUS_ICONS.GLOBE} Deploying to production...`,
|
|
286
|
+
DEPLOYMENT_SUCCESS: `${STATUS_ICONS.SUCCESS} Deployment successful!`,
|
|
287
|
+
DEPLOYMENT_FAILED: (message) => `${STATUS_ICONS.ERROR} Deployment failed: ${message}`,
|
|
288
|
+
BUILD_TIME_SAME: (time) => `${STATUS_ICONS.CHART} Build time: ${time}s (same as previous)`,
|
|
289
|
+
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}`
|
|
290
|
+
};
|
|
291
|
+
var ERROR_TEXT_PATTERNS = {
|
|
292
|
+
UNAUTHORIZED: ["401", "unauthorized"],
|
|
293
|
+
NOT_FOUND: ["404", "not found"],
|
|
294
|
+
RATE_LIMITED: ["429", "rate limit"],
|
|
295
|
+
TIMEOUT: ["timeout", "AbortError"]
|
|
296
|
+
};
|
|
297
|
+
var API_EVENT_TYPES = {
|
|
298
|
+
STDOUT: "stdout",
|
|
299
|
+
STDERR: "stderr"
|
|
300
|
+
};
|
|
301
|
+
var API_MESSAGES = {
|
|
302
|
+
NO_LOGS_AVAILABLE: "No logs available",
|
|
303
|
+
INVALID_TOKEN: "Invalid Vercel token",
|
|
304
|
+
PROJECT_NOT_FOUND: "Project not found",
|
|
305
|
+
RATE_LIMIT_EXCEEDED: "Rate limit exceeded",
|
|
306
|
+
REQUEST_TIMEOUT: "Request timeout",
|
|
307
|
+
FAILED_TO_VALIDATE_TOKEN: "Failed to validate Vercel token"
|
|
308
|
+
};
|
|
309
|
+
var API_PARAMS = {
|
|
310
|
+
BUILDS: 1,
|
|
311
|
+
LOGS: 1
|
|
312
|
+
};
|
|
313
|
+
var API_CONFIG = {
|
|
314
|
+
VERCEL_BASE_URL: "https://api.vercel.com",
|
|
315
|
+
DEFAULT_TIMEOUT_MS: 1e4,
|
|
316
|
+
DEFAULT_RETRY_ATTEMPTS: 3,
|
|
317
|
+
DEFAULT_DEPLOYMENT_LIMIT: 10,
|
|
318
|
+
SINGLE_DEPLOYMENT_LIMIT: 1
|
|
319
|
+
};
|
|
320
|
+
var PLATFORM_NAMES = {
|
|
321
|
+
VERCEL: "vercel",
|
|
322
|
+
NETLIFY: "netlify",
|
|
323
|
+
RAILWAY: "railway",
|
|
324
|
+
RENDER: "render"
|
|
325
|
+
};
|
|
326
|
+
var ENVIRONMENT_TYPES = {
|
|
327
|
+
PRODUCTION: "production",
|
|
328
|
+
PREVIEW: "preview",
|
|
329
|
+
DEVELOPMENT: "development"
|
|
330
|
+
};
|
|
331
|
+
var VERCEL_STATES = {
|
|
332
|
+
READY: "READY",
|
|
333
|
+
ERROR: "ERROR",
|
|
334
|
+
CANCELED: "CANCELED",
|
|
335
|
+
BUILDING: "BUILDING",
|
|
336
|
+
INITIALIZING: "INITIALIZING",
|
|
337
|
+
QUEUED: "QUEUED"
|
|
338
|
+
};
|
|
339
|
+
var ADAPTER_ERRORS = {
|
|
340
|
+
TOKEN_REQUIRED: "Vercel token required. Set VERCEL_TOKEN environment variable or pass token parameter.",
|
|
341
|
+
FETCH_DEPLOYMENT_FAILED: "Failed to fetch deployment status from Vercel",
|
|
342
|
+
UNKNOWN_STATUS: "unknown"
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/adapters/base/api-client.ts
|
|
346
|
+
var RateLimitError = class extends Error {
|
|
347
|
+
constructor(retryAfter, endpoint) {
|
|
348
|
+
super(`Rate limit exceeded for ${endpoint}. Retry after ${retryAfter}ms`);
|
|
349
|
+
this.retryAfter = retryAfter;
|
|
350
|
+
this.endpoint = endpoint;
|
|
351
|
+
this.name = "RateLimitError";
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
var HTTPError = class _HTTPError extends Error {
|
|
355
|
+
constructor(statusCode, statusText, url, method) {
|
|
356
|
+
super(`${method} ${url} failed with ${statusCode}: ${statusText}`);
|
|
357
|
+
this.statusCode = statusCode;
|
|
358
|
+
this.statusText = statusText;
|
|
359
|
+
this.url = url;
|
|
360
|
+
this.method = method;
|
|
361
|
+
this.name = "HTTPError";
|
|
362
|
+
Error.captureStackTrace(this, _HTTPError);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
var BaseAPIClient = class {
|
|
366
|
+
baseUrl;
|
|
367
|
+
defaultHeaders;
|
|
368
|
+
timeout;
|
|
369
|
+
maxRetries;
|
|
370
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
371
|
+
rateLimiters = /* @__PURE__ */ new Map();
|
|
372
|
+
maxTokensPerMinute = MAX_TOKENS_PER_MINUTE;
|
|
373
|
+
constructor(config) {
|
|
374
|
+
this.baseUrl = new URL(config.baseUrl);
|
|
375
|
+
this.defaultHeaders = new Headers({
|
|
376
|
+
"Content-Type": "application/json",
|
|
377
|
+
"User-Agent": "deploy-mcp/1.0.0",
|
|
378
|
+
...config.headers
|
|
379
|
+
});
|
|
380
|
+
this.timeout = config.timeout ?? API_CONFIG.DEFAULT_TIMEOUT_MS;
|
|
381
|
+
this.maxRetries = config.retry ?? API_CONFIG.DEFAULT_RETRY_ATTEMPTS;
|
|
382
|
+
}
|
|
383
|
+
async request(endpoint, options) {
|
|
384
|
+
if (options?.token) {
|
|
385
|
+
await this.checkRateLimit(options.token, endpoint.path);
|
|
386
|
+
}
|
|
387
|
+
const cacheKey = this.getCacheKey(endpoint, options);
|
|
388
|
+
if (endpoint.method === "GET" && !options?.body) {
|
|
389
|
+
const pending = this.pendingRequests.get(cacheKey);
|
|
390
|
+
if (pending) {
|
|
391
|
+
return pending;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const requestPromise = this.executeRequest(endpoint, options);
|
|
395
|
+
if (endpoint.method === "GET" && !options?.body) {
|
|
396
|
+
this.pendingRequests.set(cacheKey, requestPromise);
|
|
397
|
+
requestPromise.then(() => {
|
|
398
|
+
this.pendingRequests.delete(cacheKey);
|
|
399
|
+
}).catch(() => {
|
|
400
|
+
this.pendingRequests.delete(cacheKey);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return requestPromise;
|
|
404
|
+
}
|
|
405
|
+
async executeRequest(endpoint, options) {
|
|
406
|
+
const url = this.buildUrl(endpoint.path, options?.searchParams);
|
|
407
|
+
const headers = this.mergeHeaders(options?.headers);
|
|
408
|
+
let lastError = new Error("No attempts made");
|
|
409
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
410
|
+
try {
|
|
411
|
+
const response = await this.fetchWithTimeout(url, {
|
|
412
|
+
method: endpoint.method,
|
|
413
|
+
headers,
|
|
414
|
+
body: options?.body ? JSON.stringify(options.body) : void 0,
|
|
415
|
+
signal: options?.signal
|
|
416
|
+
});
|
|
417
|
+
if (!response.ok) {
|
|
418
|
+
throw new HTTPError(
|
|
419
|
+
response.status,
|
|
420
|
+
response.statusText,
|
|
421
|
+
url.toString(),
|
|
422
|
+
endpoint.method
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
const text = await response.text();
|
|
426
|
+
if (!text) {
|
|
427
|
+
return {};
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
return JSON.parse(text);
|
|
431
|
+
} catch {
|
|
432
|
+
throw new Error(
|
|
433
|
+
`Invalid JSON response from ${endpoint.path}: ${text.slice(0, 100)}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
438
|
+
if (error instanceof HTTPError && error.statusCode >= 400 && error.statusCode < 500 || error instanceof Error && error.name === "AbortError") {
|
|
439
|
+
throw this.enhanceError(lastError, endpoint);
|
|
440
|
+
}
|
|
441
|
+
if (attempt === this.maxRetries) {
|
|
442
|
+
throw this.enhanceError(lastError, endpoint);
|
|
443
|
+
}
|
|
444
|
+
const baseDelay = Math.min(
|
|
445
|
+
1e3 * Math.pow(2, attempt),
|
|
446
|
+
MAX_BACKOFF_DELAY_MS
|
|
447
|
+
);
|
|
448
|
+
const jitter = Math.random() * BACKOFF_JITTER_MS;
|
|
449
|
+
const totalDelay = Math.min(baseDelay + jitter, MAX_BACKOFF_DELAY_MS);
|
|
450
|
+
await this.delay(totalDelay);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
throw this.enhanceError(lastError, endpoint);
|
|
454
|
+
}
|
|
455
|
+
async fetchWithTimeout(url, init) {
|
|
456
|
+
const controller = new AbortController();
|
|
457
|
+
if (init.signal) {
|
|
458
|
+
init.signal.addEventListener("abort", () => controller.abort());
|
|
459
|
+
}
|
|
460
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
461
|
+
try {
|
|
462
|
+
return await fetch(url, {
|
|
463
|
+
...init,
|
|
464
|
+
signal: controller.signal,
|
|
465
|
+
keepalive: true
|
|
466
|
+
});
|
|
467
|
+
} finally {
|
|
468
|
+
clearTimeout(timeoutId);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
buildUrl(path, params) {
|
|
472
|
+
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
|
|
473
|
+
const baseUrlString = this.baseUrl.toString();
|
|
474
|
+
const baseWithSlash = baseUrlString.endsWith("/") ? baseUrlString : baseUrlString + "/";
|
|
475
|
+
const url = new URL(cleanPath, baseWithSlash);
|
|
476
|
+
if (params) {
|
|
477
|
+
const searchParams = new URLSearchParams();
|
|
478
|
+
for (const [key, value] of Object.entries(params)) {
|
|
479
|
+
if (value !== void 0 && value !== null) {
|
|
480
|
+
searchParams.set(key, String(value));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
url.search = searchParams.toString();
|
|
484
|
+
}
|
|
485
|
+
return url;
|
|
486
|
+
}
|
|
487
|
+
mergeHeaders(headers) {
|
|
488
|
+
if (!headers) {
|
|
489
|
+
return this.defaultHeaders;
|
|
490
|
+
}
|
|
491
|
+
const merged = new Headers(this.defaultHeaders);
|
|
492
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
493
|
+
merged.set(key, value);
|
|
494
|
+
}
|
|
495
|
+
return merged;
|
|
496
|
+
}
|
|
497
|
+
enhanceError(error, endpoint) {
|
|
498
|
+
const enhanced = new Error(
|
|
499
|
+
`API request failed for ${endpoint.path}: ${error.message}
|
|
500
|
+
See docs: ${endpoint.docsUrl}`
|
|
501
|
+
);
|
|
502
|
+
enhanced.stack = error.stack;
|
|
503
|
+
enhanced.cause = error;
|
|
504
|
+
return enhanced;
|
|
505
|
+
}
|
|
506
|
+
getCacheKey(endpoint, options) {
|
|
507
|
+
const params = options?.searchParams ? JSON.stringify(options.searchParams) : "";
|
|
508
|
+
return `${endpoint.method}:${endpoint.path}:${params}`;
|
|
509
|
+
}
|
|
510
|
+
delay(ms) {
|
|
511
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
512
|
+
}
|
|
513
|
+
getRateLimiter(token) {
|
|
514
|
+
if (!this.rateLimiters.has(token)) {
|
|
515
|
+
this.rateLimiters.set(token, {
|
|
516
|
+
tokens: this.maxTokensPerMinute,
|
|
517
|
+
lastRefill: Date.now(),
|
|
518
|
+
refillRate: this.maxTokensPerMinute
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
return this.rateLimiters.get(token);
|
|
522
|
+
}
|
|
523
|
+
async checkRateLimit(token, endpoint) {
|
|
524
|
+
const limiter = this.getRateLimiter(token);
|
|
525
|
+
const now = Date.now();
|
|
526
|
+
const timeSinceLastRefill = now - limiter.lastRefill;
|
|
527
|
+
const minutesElapsed = timeSinceLastRefill / 6e4;
|
|
528
|
+
const tokensToAdd = minutesElapsed * limiter.refillRate;
|
|
529
|
+
limiter.tokens = Math.min(
|
|
530
|
+
this.maxTokensPerMinute,
|
|
531
|
+
limiter.tokens + tokensToAdd
|
|
532
|
+
);
|
|
533
|
+
limiter.lastRefill = now;
|
|
534
|
+
if (limiter.tokens < 1) {
|
|
535
|
+
const waitTime = (1 - limiter.tokens) * (6e4 / limiter.refillRate);
|
|
536
|
+
throw new RateLimitError(Math.ceil(waitTime), endpoint);
|
|
537
|
+
}
|
|
538
|
+
limiter.tokens -= 1;
|
|
539
|
+
}
|
|
540
|
+
cleanupRateLimiters() {
|
|
541
|
+
const now = Date.now();
|
|
542
|
+
const maxAge = RATE_LIMITER_CLEANUP_AGE_MS;
|
|
543
|
+
for (const [token, limiter] of this.rateLimiters.entries()) {
|
|
544
|
+
if (now - limiter.lastRefill > maxAge) {
|
|
545
|
+
this.rateLimiters.delete(token);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// src/adapters/vercel/endpoints.ts
|
|
552
|
+
var VercelEndpoints = {
|
|
553
|
+
listDeployments: {
|
|
554
|
+
path: "/v6/deployments",
|
|
555
|
+
method: "GET",
|
|
556
|
+
docsUrl: "https://vercel.com/docs/rest-api/endpoints/deployments#list-deployments",
|
|
557
|
+
description: "List deployments for authenticated user or team"
|
|
558
|
+
},
|
|
559
|
+
getDeployment: {
|
|
560
|
+
path: "/v13/deployments",
|
|
561
|
+
method: "GET",
|
|
562
|
+
docsUrl: "https://vercel.com/docs/rest-api/endpoints/deployments#get-a-deployment-by-id-or-url",
|
|
563
|
+
description: "Get deployment by ID or URL"
|
|
564
|
+
},
|
|
565
|
+
getDeploymentEvents: {
|
|
566
|
+
path: "/v2/deployments",
|
|
567
|
+
method: "GET",
|
|
568
|
+
docsUrl: "https://vercel.com/docs/rest-api/endpoints/deployments#get-deployment-events",
|
|
569
|
+
description: "Get build logs and events for a deployment"
|
|
570
|
+
},
|
|
571
|
+
getUser: {
|
|
572
|
+
path: "/v2/user",
|
|
573
|
+
method: "GET",
|
|
574
|
+
docsUrl: "https://vercel.com/docs/rest-api/endpoints/user#get-the-authenticated-user",
|
|
575
|
+
description: "Get authenticated user information"
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// src/adapters/vercel/api.ts
|
|
580
|
+
var VercelAPI = class extends BaseAPIClient {
|
|
581
|
+
endpoints = VercelEndpoints;
|
|
582
|
+
config;
|
|
583
|
+
constructor(config) {
|
|
584
|
+
super({
|
|
585
|
+
baseUrl: config.baseUrl,
|
|
586
|
+
timeout: config.timeout,
|
|
587
|
+
retry: config.retryAttempts
|
|
588
|
+
});
|
|
589
|
+
this.config = config;
|
|
590
|
+
}
|
|
591
|
+
async getDeployments(projectId, token, limit = 1) {
|
|
592
|
+
try {
|
|
593
|
+
return await this.request(
|
|
594
|
+
this.endpoints.listDeployments,
|
|
595
|
+
{
|
|
596
|
+
searchParams: { projectId, limit },
|
|
597
|
+
headers: {
|
|
598
|
+
Authorization: `Bearer ${token}`
|
|
599
|
+
},
|
|
600
|
+
token
|
|
601
|
+
// Pass token for rate limiting
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
throw this.handleApiError(
|
|
606
|
+
error,
|
|
607
|
+
`Failed to fetch deployments for project ${projectId}`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async getUser(token) {
|
|
612
|
+
try {
|
|
613
|
+
return await this.request(this.endpoints.getUser, {
|
|
614
|
+
headers: {
|
|
615
|
+
Authorization: `Bearer ${token}`
|
|
616
|
+
},
|
|
617
|
+
token
|
|
618
|
+
// Pass token for rate limiting
|
|
619
|
+
});
|
|
620
|
+
} catch (error) {
|
|
621
|
+
throw this.handleApiError(error, API_MESSAGES.FAILED_TO_VALIDATE_TOKEN);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
async getDeploymentById(deploymentId, token) {
|
|
625
|
+
try {
|
|
626
|
+
const endpoint = {
|
|
627
|
+
...this.endpoints.getDeployment,
|
|
628
|
+
path: `${this.endpoints.getDeployment.path}/${deploymentId}`
|
|
629
|
+
};
|
|
630
|
+
return await this.request(endpoint, {
|
|
631
|
+
headers: {
|
|
632
|
+
Authorization: `Bearer ${token}`
|
|
633
|
+
},
|
|
634
|
+
token
|
|
635
|
+
// Pass token for rate limiting
|
|
636
|
+
});
|
|
637
|
+
} catch (error) {
|
|
638
|
+
throw this.handleApiError(
|
|
639
|
+
error,
|
|
640
|
+
`Failed to fetch deployment ${deploymentId}`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async getDeploymentLogs(deploymentId, token) {
|
|
645
|
+
try {
|
|
646
|
+
const endpoint = {
|
|
647
|
+
...this.endpoints.getDeploymentEvents,
|
|
648
|
+
path: `${this.endpoints.getDeploymentEvents.path}/${deploymentId}/events`
|
|
649
|
+
};
|
|
650
|
+
const response = await this.request(endpoint, {
|
|
651
|
+
searchParams: {
|
|
652
|
+
builds: API_PARAMS.BUILDS,
|
|
653
|
+
logs: API_PARAMS.LOGS
|
|
654
|
+
},
|
|
655
|
+
headers: {
|
|
656
|
+
Authorization: `Bearer ${token}`
|
|
657
|
+
},
|
|
658
|
+
token
|
|
659
|
+
// Pass token for rate limiting
|
|
660
|
+
});
|
|
661
|
+
if (!response || !Array.isArray(response)) {
|
|
662
|
+
return API_MESSAGES.NO_LOGS_AVAILABLE;
|
|
663
|
+
}
|
|
664
|
+
const logs = response.filter(
|
|
665
|
+
(event) => event.type === API_EVENT_TYPES.STDOUT || event.type === API_EVENT_TYPES.STDERR
|
|
666
|
+
).map((event) => event.payload?.text || event.text || "").join("\n");
|
|
667
|
+
const sanitizedLogs = logs.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\//g, "/");
|
|
668
|
+
return sanitizedLogs || API_MESSAGES.NO_LOGS_AVAILABLE;
|
|
669
|
+
} catch (error) {
|
|
670
|
+
throw this.handleApiError(
|
|
671
|
+
error,
|
|
672
|
+
`Failed to fetch logs for deployment ${deploymentId}`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
handleApiError(error, context) {
|
|
677
|
+
if (error instanceof Error) {
|
|
678
|
+
const message = error.message.toLowerCase();
|
|
679
|
+
if (ERROR_TEXT_PATTERNS.UNAUTHORIZED.some(
|
|
680
|
+
(pattern) => message.includes(pattern)
|
|
681
|
+
)) {
|
|
682
|
+
return new AdapterException(
|
|
683
|
+
"UNAUTHORIZED",
|
|
684
|
+
API_MESSAGES.INVALID_TOKEN,
|
|
685
|
+
error
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (ERROR_TEXT_PATTERNS.NOT_FOUND.some((pattern) => message.includes(pattern))) {
|
|
689
|
+
return new AdapterException(
|
|
690
|
+
"NOT_FOUND",
|
|
691
|
+
API_MESSAGES.PROJECT_NOT_FOUND,
|
|
692
|
+
error
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
if (ERROR_TEXT_PATTERNS.RATE_LIMITED.some(
|
|
696
|
+
(pattern) => message.includes(pattern)
|
|
697
|
+
)) {
|
|
698
|
+
return new AdapterException(
|
|
699
|
+
"RATE_LIMITED",
|
|
700
|
+
API_MESSAGES.RATE_LIMIT_EXCEEDED,
|
|
701
|
+
error
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
if (ERROR_TEXT_PATTERNS.TIMEOUT.some(
|
|
705
|
+
(pattern) => message.includes(pattern) || error.name === pattern
|
|
706
|
+
)) {
|
|
707
|
+
return new AdapterException(
|
|
708
|
+
"NETWORK_ERROR",
|
|
709
|
+
API_MESSAGES.REQUEST_TIMEOUT,
|
|
710
|
+
error
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
return new AdapterException("NETWORK_ERROR", context, error);
|
|
714
|
+
}
|
|
715
|
+
return new AdapterException("UNKNOWN_ERROR", context);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// src/adapters/vercel/index.ts
|
|
720
|
+
var VercelAdapter = class extends BaseAdapter {
|
|
721
|
+
name = PLATFORM_NAMES.VERCEL;
|
|
722
|
+
api;
|
|
723
|
+
constructor(config) {
|
|
724
|
+
super();
|
|
725
|
+
const defaultConfig = {
|
|
726
|
+
baseUrl: API_CONFIG.VERCEL_BASE_URL,
|
|
727
|
+
timeout: API_CONFIG.DEFAULT_TIMEOUT_MS,
|
|
728
|
+
retryAttempts: API_CONFIG.DEFAULT_RETRY_ATTEMPTS
|
|
729
|
+
};
|
|
730
|
+
this.api = new VercelAPI({ ...defaultConfig, ...config });
|
|
731
|
+
}
|
|
732
|
+
async getLatestDeployment(project, token) {
|
|
733
|
+
const apiToken = token || process.env.VERCEL_TOKEN;
|
|
734
|
+
if (!apiToken) {
|
|
735
|
+
throw new Error(ADAPTER_ERRORS.TOKEN_REQUIRED);
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
const data = await this.api.getDeployments(
|
|
739
|
+
project,
|
|
740
|
+
apiToken,
|
|
741
|
+
API_CONFIG.SINGLE_DEPLOYMENT_LIMIT
|
|
742
|
+
);
|
|
743
|
+
if (!data.deployments || data.deployments.length === 0) {
|
|
744
|
+
return {
|
|
745
|
+
status: ADAPTER_ERRORS.UNKNOWN_STATUS,
|
|
746
|
+
projectName: project,
|
|
747
|
+
platform: PLATFORM_NAMES.VERCEL
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
return this.transformDeployment(data.deployments[0]);
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (error instanceof Error) {
|
|
753
|
+
throw error;
|
|
754
|
+
}
|
|
755
|
+
throw new Error(ADAPTER_ERRORS.FETCH_DEPLOYMENT_FAILED);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async authenticate(token) {
|
|
759
|
+
try {
|
|
760
|
+
await this.api.getUser(token);
|
|
761
|
+
return true;
|
|
762
|
+
} catch {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
transformDeployment(deployment) {
|
|
767
|
+
const status = this.mapState(deployment.state);
|
|
768
|
+
return {
|
|
769
|
+
id: deployment.uid,
|
|
770
|
+
status,
|
|
771
|
+
url: deployment.url ? `https://${deployment.url}` : void 0,
|
|
772
|
+
projectName: deployment.name,
|
|
773
|
+
platform: PLATFORM_NAMES.VERCEL,
|
|
774
|
+
timestamp: this.formatTimestamp(deployment.createdAt),
|
|
775
|
+
duration: deployment.ready ? this.calculateDuration(deployment.createdAt, deployment.ready) : void 0,
|
|
776
|
+
environment: deployment.target || ENVIRONMENT_TYPES.PRODUCTION,
|
|
777
|
+
commit: deployment.meta ? {
|
|
778
|
+
sha: deployment.meta.githubCommitSha,
|
|
779
|
+
message: deployment.meta.githubCommitMessage,
|
|
780
|
+
author: deployment.meta.githubCommitAuthorName
|
|
781
|
+
} : void 0
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
mapState(state) {
|
|
785
|
+
switch (state) {
|
|
786
|
+
case VERCEL_STATES.READY:
|
|
787
|
+
return "success";
|
|
788
|
+
case VERCEL_STATES.ERROR:
|
|
789
|
+
case VERCEL_STATES.CANCELED:
|
|
790
|
+
return "failed";
|
|
791
|
+
case VERCEL_STATES.BUILDING:
|
|
792
|
+
case VERCEL_STATES.INITIALIZING:
|
|
793
|
+
case VERCEL_STATES.QUEUED:
|
|
794
|
+
return "building";
|
|
795
|
+
default:
|
|
796
|
+
return ADAPTER_ERRORS.UNKNOWN_STATUS;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async getDeploymentById(deploymentId, token) {
|
|
800
|
+
return this.api.getDeploymentById(deploymentId, token);
|
|
801
|
+
}
|
|
802
|
+
async getRecentDeployments(project, token, limit = API_CONFIG.DEFAULT_DEPLOYMENT_LIMIT) {
|
|
803
|
+
const data = await this.api.getDeployments(project, token, limit);
|
|
804
|
+
return data.deployments || [];
|
|
805
|
+
}
|
|
806
|
+
async getDeploymentLogs(deploymentId, token) {
|
|
807
|
+
return this.api.getDeploymentLogs(deploymentId, token);
|
|
808
|
+
}
|
|
809
|
+
async getDeploymentStatus(project, token) {
|
|
810
|
+
return this.getLatestDeployment(project, token);
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
// src/adapters/netlify/endpoints.ts
|
|
815
|
+
var NetlifyEndpoints = {
|
|
816
|
+
listSites: {
|
|
817
|
+
path: "/sites",
|
|
818
|
+
method: "GET",
|
|
819
|
+
docsUrl: "https://docs.netlify.com/api/get-started/#sites",
|
|
820
|
+
description: "List all sites for the current user"
|
|
821
|
+
},
|
|
822
|
+
getSite: {
|
|
823
|
+
path: "/sites/{site_id}",
|
|
824
|
+
method: "GET",
|
|
825
|
+
docsUrl: "https://docs.netlify.com/api/get-started/#get-site",
|
|
826
|
+
description: "Get a specific site by ID"
|
|
827
|
+
},
|
|
828
|
+
listDeploys: {
|
|
829
|
+
path: "/sites/{site_id}/deploys",
|
|
830
|
+
method: "GET",
|
|
831
|
+
docsUrl: "https://docs.netlify.com/api/get-started/#list-site-deploys",
|
|
832
|
+
description: "List all deploys for a site"
|
|
833
|
+
},
|
|
834
|
+
getDeploy: {
|
|
835
|
+
path: "/deploys/{deploy_id}",
|
|
836
|
+
method: "GET",
|
|
837
|
+
docsUrl: "https://docs.netlify.com/api/get-started/#get-deploy",
|
|
838
|
+
description: "Get a specific deploy by ID"
|
|
839
|
+
},
|
|
840
|
+
getUser: {
|
|
841
|
+
path: "/user",
|
|
842
|
+
method: "GET",
|
|
843
|
+
docsUrl: "https://docs.netlify.com/api/get-started/#get-current-user",
|
|
844
|
+
description: "Get the current user"
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// src/adapters/netlify/api.ts
|
|
849
|
+
var NetlifyAPI = class extends BaseAPIClient {
|
|
850
|
+
endpoints = NetlifyEndpoints;
|
|
851
|
+
config;
|
|
852
|
+
siteCache = /* @__PURE__ */ new Map();
|
|
853
|
+
// name -> id cache
|
|
854
|
+
constructor(config) {
|
|
855
|
+
const fullConfig = {
|
|
856
|
+
baseUrl: "https://api.netlify.com/api/v1",
|
|
857
|
+
timeout: config?.timeout ?? API_CONFIG.DEFAULT_TIMEOUT_MS,
|
|
858
|
+
retryAttempts: config?.retryAttempts ?? API_CONFIG.DEFAULT_RETRY_ATTEMPTS,
|
|
859
|
+
...config
|
|
860
|
+
};
|
|
861
|
+
super({
|
|
862
|
+
baseUrl: fullConfig.baseUrl,
|
|
863
|
+
timeout: fullConfig.timeout,
|
|
864
|
+
retry: fullConfig.retryAttempts
|
|
865
|
+
});
|
|
866
|
+
this.config = fullConfig;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Get site ID from site name or ID
|
|
870
|
+
* Netlify API requires site ID for most endpoints
|
|
871
|
+
*/
|
|
872
|
+
async getSiteId(siteNameOrId, token) {
|
|
873
|
+
if (this.siteCache.has(siteNameOrId)) {
|
|
874
|
+
return this.siteCache.get(siteNameOrId);
|
|
875
|
+
}
|
|
876
|
+
const sites = await this.listSites(token);
|
|
877
|
+
const site = sites.find(
|
|
878
|
+
(s) => s.name === siteNameOrId || s.id === siteNameOrId
|
|
879
|
+
);
|
|
880
|
+
if (!site) {
|
|
881
|
+
throw new Error(`Site not found: ${siteNameOrId}`);
|
|
882
|
+
}
|
|
883
|
+
this.siteCache.set(siteNameOrId, site.id);
|
|
884
|
+
return site.id;
|
|
885
|
+
}
|
|
886
|
+
async listSites(token) {
|
|
887
|
+
const options = {
|
|
888
|
+
headers: {
|
|
889
|
+
Authorization: `Bearer ${token}`
|
|
890
|
+
},
|
|
891
|
+
token
|
|
892
|
+
};
|
|
893
|
+
return this.request(this.endpoints.listSites, options);
|
|
894
|
+
}
|
|
895
|
+
async listDeploys(siteNameOrId, token, limit = 10) {
|
|
896
|
+
const siteId = await this.getSiteId(siteNameOrId, token);
|
|
897
|
+
const endpoint = {
|
|
898
|
+
...this.endpoints.listDeploys,
|
|
899
|
+
path: this.endpoints.listDeploys.path.replace("{site_id}", siteId)
|
|
900
|
+
};
|
|
901
|
+
const options = {
|
|
902
|
+
headers: {
|
|
903
|
+
Authorization: `Bearer ${token}`
|
|
904
|
+
},
|
|
905
|
+
searchParams: {
|
|
906
|
+
per_page: limit
|
|
907
|
+
},
|
|
908
|
+
token
|
|
909
|
+
};
|
|
910
|
+
return this.request(endpoint, options);
|
|
911
|
+
}
|
|
912
|
+
async getDeploy(deployId, token) {
|
|
913
|
+
const endpoint = {
|
|
914
|
+
...this.endpoints.getDeploy,
|
|
915
|
+
path: this.endpoints.getDeploy.path.replace("{deploy_id}", deployId)
|
|
916
|
+
};
|
|
917
|
+
const options = {
|
|
918
|
+
headers: {
|
|
919
|
+
Authorization: `Bearer ${token}`
|
|
920
|
+
},
|
|
921
|
+
token
|
|
922
|
+
};
|
|
923
|
+
return this.request(endpoint, options);
|
|
924
|
+
}
|
|
925
|
+
async getDeployLog(deployId, token) {
|
|
926
|
+
const deploy = await this.getDeploy(deployId, token);
|
|
927
|
+
if (!deploy.log_access_attributes?.url) {
|
|
928
|
+
throw new Error("Deploy logs not available");
|
|
929
|
+
}
|
|
930
|
+
const response = await fetch(deploy.log_access_attributes.url);
|
|
931
|
+
if (!response.ok) {
|
|
932
|
+
throw new Error(`Failed to fetch logs: ${response.statusText}`);
|
|
933
|
+
}
|
|
934
|
+
return response.text();
|
|
935
|
+
}
|
|
936
|
+
async getUser(token) {
|
|
937
|
+
const options = {
|
|
938
|
+
headers: {
|
|
939
|
+
Authorization: `Bearer ${token}`
|
|
940
|
+
},
|
|
941
|
+
token
|
|
942
|
+
};
|
|
943
|
+
return this.request(this.endpoints.getUser, options);
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
// src/adapters/netlify/index.ts
|
|
948
|
+
var NetlifyAdapter = class extends BaseAdapter {
|
|
949
|
+
name = "netlify";
|
|
950
|
+
api;
|
|
951
|
+
constructor() {
|
|
952
|
+
super();
|
|
953
|
+
this.api = new NetlifyAPI();
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Map Netlify deploy states to our standard states
|
|
957
|
+
* Source: https://github.com/netlify/open-api/blob/master/swagger.yml
|
|
958
|
+
*/
|
|
959
|
+
mapState(state) {
|
|
960
|
+
switch (state) {
|
|
961
|
+
case "ready":
|
|
962
|
+
case "processed":
|
|
963
|
+
return "success";
|
|
964
|
+
case "error":
|
|
965
|
+
case "rejected":
|
|
966
|
+
return "failed";
|
|
967
|
+
case "new":
|
|
968
|
+
case "pending_review":
|
|
969
|
+
case "accepted":
|
|
970
|
+
case "enqueued":
|
|
971
|
+
case "building":
|
|
972
|
+
case "uploading":
|
|
973
|
+
case "uploaded":
|
|
974
|
+
case "preparing":
|
|
975
|
+
case "prepared":
|
|
976
|
+
case "processing":
|
|
977
|
+
case "retrying":
|
|
978
|
+
return "building";
|
|
979
|
+
default:
|
|
980
|
+
return "unknown";
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Transform Netlify deploy to our standard format
|
|
985
|
+
*/
|
|
986
|
+
transformDeploy(deploy) {
|
|
987
|
+
const status = this.mapState(deploy.state);
|
|
988
|
+
let duration;
|
|
989
|
+
if (deploy.created_at && deploy.published_at) {
|
|
990
|
+
duration = this.calculateDuration(deploy.created_at, deploy.published_at);
|
|
991
|
+
} else if (deploy.deploy_time) {
|
|
992
|
+
duration = Math.round(deploy.deploy_time / 1e3);
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
id: deploy.id,
|
|
996
|
+
status,
|
|
997
|
+
url: deploy.ssl_url || deploy.url || deploy.deploy_ssl_url || deploy.deploy_url,
|
|
998
|
+
projectName: deploy.name || deploy.site_id,
|
|
999
|
+
platform: "netlify",
|
|
1000
|
+
timestamp: this.formatTimestamp(deploy.created_at),
|
|
1001
|
+
duration,
|
|
1002
|
+
environment: deploy.context || "production",
|
|
1003
|
+
commit: deploy.commit_ref ? {
|
|
1004
|
+
sha: deploy.commit_ref,
|
|
1005
|
+
message: deploy.title
|
|
1006
|
+
} : void 0
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
async getLatestDeployment(project, token) {
|
|
1010
|
+
const apiToken = token || process.env.NETLIFY_TOKEN;
|
|
1011
|
+
if (!apiToken) {
|
|
1012
|
+
throw new AdapterException(
|
|
1013
|
+
"UNAUTHORIZED",
|
|
1014
|
+
"Netlify token required. Set NETLIFY_TOKEN environment variable or pass token parameter."
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
const deploys = await this.api.listDeploys(project, apiToken, 1);
|
|
1019
|
+
if (!deploys || deploys.length === 0) {
|
|
1020
|
+
throw new AdapterException(
|
|
1021
|
+
"NOT_FOUND",
|
|
1022
|
+
`No deployments found for site: ${project}`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
return this.transformDeploy(deploys[0]);
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (error instanceof AdapterException) {
|
|
1028
|
+
throw error;
|
|
1029
|
+
}
|
|
1030
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1031
|
+
if (message.includes("Site not found")) {
|
|
1032
|
+
throw new AdapterException(
|
|
1033
|
+
"NOT_FOUND",
|
|
1034
|
+
`Site not found: ${project}. Make sure the site name is correct.`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
if (message.includes("401") || message.includes("Unauthorized")) {
|
|
1038
|
+
throw new AdapterException(
|
|
1039
|
+
"UNAUTHORIZED",
|
|
1040
|
+
"Invalid Netlify token. Check your NETLIFY_TOKEN."
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
throw new AdapterException("UNKNOWN", `Netlify API error: ${message}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
async authenticate(token) {
|
|
1047
|
+
try {
|
|
1048
|
+
const user = await this.api.getUser(token);
|
|
1049
|
+
return !!user.id;
|
|
1050
|
+
} catch {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
async getDeploymentById(deploymentId, token) {
|
|
1055
|
+
try {
|
|
1056
|
+
return await this.api.getDeploy(deploymentId, token);
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1059
|
+
throw new AdapterException(
|
|
1060
|
+
"NOT_FOUND",
|
|
1061
|
+
`Deployment not found: ${deploymentId}. ${message}`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async getRecentDeployments(project, token, limit = 10) {
|
|
1066
|
+
try {
|
|
1067
|
+
return await this.api.listDeploys(project, token, limit);
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1070
|
+
if (message.includes("Site not found")) {
|
|
1071
|
+
throw new AdapterException("NOT_FOUND", `Site not found: ${project}`);
|
|
1072
|
+
}
|
|
1073
|
+
throw new AdapterException(
|
|
1074
|
+
"UNKNOWN",
|
|
1075
|
+
`Failed to fetch deployments: ${message}`
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async getDeploymentLogs(deploymentId, token) {
|
|
1080
|
+
try {
|
|
1081
|
+
return await this.api.getDeployLog(deploymentId, token);
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1084
|
+
if (message.includes("not available")) {
|
|
1085
|
+
return "Deploy logs not available for this deployment.";
|
|
1086
|
+
}
|
|
1087
|
+
throw new AdapterException(
|
|
1088
|
+
"UNKNOWN",
|
|
1089
|
+
`Failed to fetch deployment logs: ${message}`
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
// src/core/deployment-intelligence.ts
|
|
1096
|
+
var DeploymentIntelligence = class {
|
|
1097
|
+
adapter;
|
|
1098
|
+
platform;
|
|
1099
|
+
constructor(platform) {
|
|
1100
|
+
this.platform = platform;
|
|
1101
|
+
this.adapter = this.createAdapter(platform);
|
|
1102
|
+
}
|
|
1103
|
+
getTokenForPlatform() {
|
|
1104
|
+
switch (this.platform) {
|
|
1105
|
+
case "vercel":
|
|
1106
|
+
return process.env.VERCEL_TOKEN;
|
|
1107
|
+
case "netlify":
|
|
1108
|
+
return process.env.NETLIFY_TOKEN;
|
|
1109
|
+
case "railway":
|
|
1110
|
+
return process.env.RAILWAY_TOKEN;
|
|
1111
|
+
case "render":
|
|
1112
|
+
return process.env.RENDER_TOKEN;
|
|
1113
|
+
default:
|
|
1114
|
+
return void 0;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
getPollingInterval(state) {
|
|
1118
|
+
return POLLING_INTERVALS_BY_STATE[state] || POLLING_INTERVALS_BY_STATE.UNKNOWN;
|
|
1119
|
+
}
|
|
1120
|
+
getTimeAgo(timestamp) {
|
|
1121
|
+
const now = Date.now();
|
|
1122
|
+
const diff = now - timestamp;
|
|
1123
|
+
const seconds = Math.floor(diff / 1e3);
|
|
1124
|
+
const minutes = Math.floor(seconds / 60);
|
|
1125
|
+
const hours = Math.floor(minutes / 60);
|
|
1126
|
+
const days = Math.floor(hours / 24);
|
|
1127
|
+
if (days > 0) return `${days} day${days > 1 ? "s" : ""} ago`;
|
|
1128
|
+
if (hours > 0) return `${hours} hour${hours > 1 ? "s" : ""} ago`;
|
|
1129
|
+
if (minutes > 0) return `${minutes} minute${minutes > 1 ? "s" : ""} ago`;
|
|
1130
|
+
return `${seconds} second${seconds > 1 ? "s" : ""} ago`;
|
|
1131
|
+
}
|
|
1132
|
+
mapDeploymentState(state) {
|
|
1133
|
+
switch (state) {
|
|
1134
|
+
case "READY":
|
|
1135
|
+
return "success";
|
|
1136
|
+
case "ERROR":
|
|
1137
|
+
case "CANCELED":
|
|
1138
|
+
return "failed";
|
|
1139
|
+
case "BUILDING":
|
|
1140
|
+
case "INITIALIZING":
|
|
1141
|
+
case "QUEUED":
|
|
1142
|
+
return "building";
|
|
1143
|
+
default:
|
|
1144
|
+
return "unknown";
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
createAdapter(platform) {
|
|
1148
|
+
switch (platform) {
|
|
1149
|
+
case "vercel":
|
|
1150
|
+
return new VercelAdapter();
|
|
1151
|
+
case "netlify":
|
|
1152
|
+
return new NetlifyAdapter();
|
|
1153
|
+
// Ready for future platforms
|
|
1154
|
+
// case "railway":
|
|
1155
|
+
// return new RailwayAdapter();
|
|
1156
|
+
// case "render":
|
|
1157
|
+
// return new RenderAdapter();
|
|
1158
|
+
default:
|
|
1159
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
async getDeployment(deploymentId, token) {
|
|
1163
|
+
return this.adapter.getDeploymentById(deploymentId, token);
|
|
1164
|
+
}
|
|
1165
|
+
async *watchDeployment(args) {
|
|
1166
|
+
const token = args.token || this.getTokenForPlatform();
|
|
1167
|
+
if (!token) {
|
|
1168
|
+
yield {
|
|
1169
|
+
type: "error",
|
|
1170
|
+
message: ERROR_MESSAGES.NO_TOKEN,
|
|
1171
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1172
|
+
};
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
let deploymentId = args.deploymentId;
|
|
1177
|
+
if (!deploymentId) {
|
|
1178
|
+
const deployments = await this.adapter.getRecentDeployments(
|
|
1179
|
+
args.project,
|
|
1180
|
+
token,
|
|
1181
|
+
SINGLE_DEPLOYMENT_FETCH
|
|
1182
|
+
);
|
|
1183
|
+
if (deployments.length > 0) {
|
|
1184
|
+
deploymentId = deployments[0].uid || deployments[0].id;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (!deploymentId) {
|
|
1188
|
+
yield {
|
|
1189
|
+
type: "error",
|
|
1190
|
+
message: ERROR_MESSAGES.NO_DEPLOYMENT_FOUND,
|
|
1191
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1192
|
+
};
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
yield {
|
|
1196
|
+
type: "progress",
|
|
1197
|
+
message: STATUS_MESSAGES.STARTING_WATCH(
|
|
1198
|
+
deploymentId.slice(0, DEFAULTS.DEPLOYMENT_ID_SLICE_LENGTH)
|
|
1199
|
+
),
|
|
1200
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1201
|
+
};
|
|
1202
|
+
let lastState = "";
|
|
1203
|
+
let attempts = 0;
|
|
1204
|
+
const maxAttempts = MAX_DEPLOYMENT_WATCH_ATTEMPTS;
|
|
1205
|
+
const startTime = Date.now();
|
|
1206
|
+
const maxWatchTime = MAX_WATCH_TIME_MS;
|
|
1207
|
+
while (attempts < maxAttempts && Date.now() - startTime < maxWatchTime) {
|
|
1208
|
+
try {
|
|
1209
|
+
const deployment = await this.adapter.getDeploymentById(
|
|
1210
|
+
deploymentId,
|
|
1211
|
+
token
|
|
1212
|
+
);
|
|
1213
|
+
if (deployment.readyState !== lastState) {
|
|
1214
|
+
lastState = deployment.readyState;
|
|
1215
|
+
switch (deployment.readyState) {
|
|
1216
|
+
case DEPLOYMENT_STATES.INITIALIZING:
|
|
1217
|
+
yield {
|
|
1218
|
+
type: "progress",
|
|
1219
|
+
message: STATUS_MESSAGES.INITIALIZING,
|
|
1220
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1221
|
+
};
|
|
1222
|
+
break;
|
|
1223
|
+
case DEPLOYMENT_STATES.BUILDING:
|
|
1224
|
+
yield {
|
|
1225
|
+
type: "progress",
|
|
1226
|
+
message: STATUS_MESSAGES.BUILDING,
|
|
1227
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1228
|
+
};
|
|
1229
|
+
break;
|
|
1230
|
+
case DEPLOYMENT_STATES.UPLOADING:
|
|
1231
|
+
yield {
|
|
1232
|
+
type: "progress",
|
|
1233
|
+
message: STATUS_MESSAGES.UPLOADING,
|
|
1234
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1235
|
+
};
|
|
1236
|
+
break;
|
|
1237
|
+
case DEPLOYMENT_STATES.DEPLOYING:
|
|
1238
|
+
yield {
|
|
1239
|
+
type: "progress",
|
|
1240
|
+
message: STATUS_MESSAGES.DEPLOYING,
|
|
1241
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1242
|
+
};
|
|
1243
|
+
break;
|
|
1244
|
+
case DEPLOYMENT_STATES.READY: {
|
|
1245
|
+
const duration = deployment.buildingAt && deployment.ready ? deployment.ready - deployment.buildingAt : void 0;
|
|
1246
|
+
yield {
|
|
1247
|
+
type: "success",
|
|
1248
|
+
message: STATUS_MESSAGES.DEPLOYMENT_SUCCESS,
|
|
1249
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1250
|
+
details: {
|
|
1251
|
+
url: deployment.url,
|
|
1252
|
+
duration: duration ? Math.round(duration / BUILD_TIME_SECONDS_DIVISOR) : void 0
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
try {
|
|
1256
|
+
const comparison = await this.compareWithPrevious(
|
|
1257
|
+
args.project,
|
|
1258
|
+
deploymentId,
|
|
1259
|
+
token
|
|
1260
|
+
);
|
|
1261
|
+
if (comparison) {
|
|
1262
|
+
yield {
|
|
1263
|
+
type: "progress",
|
|
1264
|
+
message: this.formatComparison(comparison),
|
|
1265
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
} catch (e) {
|
|
1269
|
+
console.error("Failed to compare deployments:", e);
|
|
1270
|
+
}
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
case DEPLOYMENT_STATES.ERROR:
|
|
1274
|
+
case DEPLOYMENT_STATES.CANCELED: {
|
|
1275
|
+
const errorDetails = await this.analyzeError(
|
|
1276
|
+
deploymentId,
|
|
1277
|
+
token
|
|
1278
|
+
);
|
|
1279
|
+
yield {
|
|
1280
|
+
type: "error",
|
|
1281
|
+
message: STATUS_MESSAGES.DEPLOYMENT_FAILED(
|
|
1282
|
+
errorDetails.message
|
|
1283
|
+
),
|
|
1284
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1285
|
+
details: {
|
|
1286
|
+
suggestion: errorDetails.suggestion,
|
|
1287
|
+
file: errorDetails.location
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
if (deployment.readyState === DEPLOYMENT_STATES.READY || deployment.readyState === DEPLOYMENT_STATES.ERROR || deployment.readyState === DEPLOYMENT_STATES.CANCELED) {
|
|
1295
|
+
break;
|
|
1296
|
+
}
|
|
1297
|
+
const pollInterval = this.getPollingInterval(lastState || "UNKNOWN");
|
|
1298
|
+
if (pollInterval === 0) {
|
|
1299
|
+
break;
|
|
1300
|
+
}
|
|
1301
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
1302
|
+
attempts++;
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
console.error("Error checking deployment status:", error);
|
|
1305
|
+
attempts++;
|
|
1306
|
+
const pollInterval = this.getPollingInterval(lastState || "UNKNOWN");
|
|
1307
|
+
if (pollInterval === 0) {
|
|
1308
|
+
break;
|
|
1309
|
+
}
|
|
1310
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (attempts >= maxAttempts || Date.now() - startTime >= MAX_WATCH_TIME_MS) {
|
|
1314
|
+
const timeoutSeconds = Math.round((Date.now() - startTime) / 1e3);
|
|
1315
|
+
yield {
|
|
1316
|
+
type: "warning",
|
|
1317
|
+
message: `Deployment watch timed out after ${timeoutSeconds} seconds. The deployment may still be running.`,
|
|
1318
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1319
|
+
details: {
|
|
1320
|
+
suggestion: "Check the platform dashboard for the latest status"
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
yield {
|
|
1326
|
+
type: "error",
|
|
1327
|
+
message: `Error watching deployment: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
1328
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async compareDeployments(args) {
|
|
1333
|
+
const token = args.token || this.getTokenForPlatform();
|
|
1334
|
+
if (!token) {
|
|
1335
|
+
throw new Error(`No ${this.platform} token provided`);
|
|
1336
|
+
}
|
|
1337
|
+
try {
|
|
1338
|
+
let current;
|
|
1339
|
+
let previous;
|
|
1340
|
+
switch (args.mode || "last_vs_previous") {
|
|
1341
|
+
case "last_vs_previous": {
|
|
1342
|
+
const deployments = await this.adapter.getRecentDeployments(
|
|
1343
|
+
args.project,
|
|
1344
|
+
token,
|
|
1345
|
+
2
|
|
1346
|
+
);
|
|
1347
|
+
if (deployments.length < 2) return null;
|
|
1348
|
+
[current, previous] = deployments;
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1351
|
+
case "current_vs_success": {
|
|
1352
|
+
const deployments = await this.adapter.getRecentDeployments(
|
|
1353
|
+
args.project,
|
|
1354
|
+
token,
|
|
1355
|
+
20
|
|
1356
|
+
// Look back further to find a success
|
|
1357
|
+
);
|
|
1358
|
+
if (deployments.length < 2) return null;
|
|
1359
|
+
current = deployments[0];
|
|
1360
|
+
previous = deployments.find(
|
|
1361
|
+
(d, index) => index > 0 && (d.state === "READY" || d.readyState === "READY")
|
|
1362
|
+
);
|
|
1363
|
+
if (!previous) {
|
|
1364
|
+
previous = deployments[1];
|
|
1365
|
+
}
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
case "current_vs_production": {
|
|
1369
|
+
const deployments = await this.adapter.getRecentDeployments(
|
|
1370
|
+
args.project,
|
|
1371
|
+
token,
|
|
1372
|
+
20
|
|
1373
|
+
);
|
|
1374
|
+
if (deployments.length < 2) return null;
|
|
1375
|
+
current = deployments[0];
|
|
1376
|
+
previous = deployments.find(
|
|
1377
|
+
(d, index) => index > 0 && d.target === "production"
|
|
1378
|
+
);
|
|
1379
|
+
if (!previous) {
|
|
1380
|
+
previous = deployments[1];
|
|
1381
|
+
}
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
case "between_dates": {
|
|
1385
|
+
if (!args.dateFrom || !args.dateTo) {
|
|
1386
|
+
throw new Error(
|
|
1387
|
+
"dateFrom and dateTo are required for between_dates mode"
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
const deployments = await this.adapter.getRecentDeployments(
|
|
1391
|
+
args.project,
|
|
1392
|
+
token,
|
|
1393
|
+
50
|
|
1394
|
+
// Get more to cover date range
|
|
1395
|
+
);
|
|
1396
|
+
const fromTime = new Date(args.dateFrom).getTime();
|
|
1397
|
+
const toTime = new Date(args.dateTo).getTime();
|
|
1398
|
+
const inRange = deployments.filter((d) => {
|
|
1399
|
+
const deployTime = new Date(d.createdAt).getTime();
|
|
1400
|
+
return deployTime >= fromTime && deployTime <= toTime;
|
|
1401
|
+
});
|
|
1402
|
+
if (inRange.length < 2) {
|
|
1403
|
+
throw new Error(
|
|
1404
|
+
"Not enough deployments found in the specified date range"
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
current = inRange[0];
|
|
1408
|
+
previous = inRange[inRange.length - 1];
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
case "by_ids": {
|
|
1412
|
+
if (!args.deploymentA || !args.deploymentB) {
|
|
1413
|
+
throw new Error(
|
|
1414
|
+
"deploymentA and deploymentB are required for by_ids mode"
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
[current, previous] = await Promise.all([
|
|
1418
|
+
this.getDeployment(args.deploymentA, token),
|
|
1419
|
+
this.getDeployment(args.deploymentB, token)
|
|
1420
|
+
]);
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
default: {
|
|
1424
|
+
const deployments = await this.adapter.getRecentDeployments(
|
|
1425
|
+
args.project,
|
|
1426
|
+
token,
|
|
1427
|
+
2
|
|
1428
|
+
);
|
|
1429
|
+
if (deployments.length < 2) return null;
|
|
1430
|
+
[current, previous] = deployments;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
const currentBuildTime = current.buildingAt && current.ready ? Math.round(
|
|
1434
|
+
(current.ready - current.buildingAt) / BUILD_TIME_SECONDS_DIVISOR
|
|
1435
|
+
) : 0;
|
|
1436
|
+
const previousBuildTime = previous.buildingAt && previous.ready ? Math.round(
|
|
1437
|
+
(previous.ready - previous.buildingAt) / BUILD_TIME_SECONDS_DIVISOR
|
|
1438
|
+
) : 0;
|
|
1439
|
+
const comparison = {
|
|
1440
|
+
deployments: {
|
|
1441
|
+
current: {
|
|
1442
|
+
id: current.uid || current.id,
|
|
1443
|
+
url: current.url ? `https://${current.url}` : void 0,
|
|
1444
|
+
timestamp: new Date(current.createdAt).toISOString(),
|
|
1445
|
+
commit: current.meta ? {
|
|
1446
|
+
sha: current.meta.githubCommitSha,
|
|
1447
|
+
message: current.meta.githubCommitMessage,
|
|
1448
|
+
author: current.meta.githubCommitAuthorName
|
|
1449
|
+
} : void 0,
|
|
1450
|
+
buildTime: currentBuildTime,
|
|
1451
|
+
status: this.mapDeploymentState(
|
|
1452
|
+
current.state || current.readyState
|
|
1453
|
+
),
|
|
1454
|
+
timeAgo: this.getTimeAgo(current.createdAt)
|
|
1455
|
+
},
|
|
1456
|
+
previous: {
|
|
1457
|
+
id: previous.uid || previous.id,
|
|
1458
|
+
url: previous.url ? `https://${previous.url}` : void 0,
|
|
1459
|
+
timestamp: new Date(previous.createdAt).toISOString(),
|
|
1460
|
+
commit: previous.meta ? {
|
|
1461
|
+
sha: previous.meta.githubCommitSha,
|
|
1462
|
+
message: previous.meta.githubCommitMessage,
|
|
1463
|
+
author: previous.meta.githubCommitAuthorName
|
|
1464
|
+
} : void 0,
|
|
1465
|
+
buildTime: previousBuildTime,
|
|
1466
|
+
status: this.mapDeploymentState(
|
|
1467
|
+
previous.state || previous.readyState
|
|
1468
|
+
),
|
|
1469
|
+
timeAgo: this.getTimeAgo(previous.createdAt)
|
|
1470
|
+
}
|
|
1471
|
+
},
|
|
1472
|
+
performance: {
|
|
1473
|
+
buildTime: {
|
|
1474
|
+
current: currentBuildTime,
|
|
1475
|
+
previous: previousBuildTime,
|
|
1476
|
+
delta: 0,
|
|
1477
|
+
percentage: 0
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1480
|
+
changes: {
|
|
1481
|
+
filesChanged: 0
|
|
1482
|
+
},
|
|
1483
|
+
risk: "LOW"
|
|
1484
|
+
};
|
|
1485
|
+
comparison.performance.buildTime.delta = comparison.performance.buildTime.current - comparison.performance.buildTime.previous;
|
|
1486
|
+
comparison.performance.buildTime.percentage = comparison.performance.buildTime.previous > 0 ? Math.round(
|
|
1487
|
+
comparison.performance.buildTime.delta / comparison.performance.buildTime.previous * DEFAULTS.PERCENTAGE_MULTIPLIER
|
|
1488
|
+
) : 0;
|
|
1489
|
+
if (Math.abs(comparison.performance.buildTime.percentage) > HIGH_RISK_THRESHOLD_PERCENT) {
|
|
1490
|
+
comparison.risk = "HIGH";
|
|
1491
|
+
} else if (Math.abs(comparison.performance.buildTime.percentage) > MEDIUM_RISK_THRESHOLD_PERCENT) {
|
|
1492
|
+
comparison.risk = "MEDIUM";
|
|
1493
|
+
}
|
|
1494
|
+
return comparison;
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
console.error("Error comparing deployments:", error);
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
async getDeploymentLogs(args) {
|
|
1501
|
+
const token = args.token || this.getTokenForPlatform();
|
|
1502
|
+
if (!token) {
|
|
1503
|
+
throw new Error(`No ${this.platform} token provided`);
|
|
1504
|
+
}
|
|
1505
|
+
try {
|
|
1506
|
+
let actualDeploymentId = args.deploymentId;
|
|
1507
|
+
if (args.deploymentId === "latest" && args.project) {
|
|
1508
|
+
const latestDeployment = await this.adapter.getLatestDeployment(
|
|
1509
|
+
args.project,
|
|
1510
|
+
token
|
|
1511
|
+
);
|
|
1512
|
+
if (!latestDeployment.id) {
|
|
1513
|
+
throw new Error("Latest deployment has no ID");
|
|
1514
|
+
}
|
|
1515
|
+
actualDeploymentId = latestDeployment.id;
|
|
1516
|
+
} else if (args.deploymentId === "latest" && !args.project) {
|
|
1517
|
+
throw new Error("Project name required when using 'latest' deployment");
|
|
1518
|
+
}
|
|
1519
|
+
const logs = await this.adapter.getDeploymentLogs(
|
|
1520
|
+
actualDeploymentId,
|
|
1521
|
+
token
|
|
1522
|
+
);
|
|
1523
|
+
let filteredLogs = logs;
|
|
1524
|
+
if (args.filter === "error") {
|
|
1525
|
+
const lines = logs.split("\n");
|
|
1526
|
+
filteredLogs = lines.filter((line) => LOG_FILTERS.ERROR.test(line)).join("\n");
|
|
1527
|
+
} else if (args.filter === "warning") {
|
|
1528
|
+
const lines = logs.split("\n");
|
|
1529
|
+
filteredLogs = lines.filter((line) => LOG_FILTERS.WARNING.test(line)).join("\n");
|
|
1530
|
+
}
|
|
1531
|
+
const analysis = this.analyzeLogs(filteredLogs);
|
|
1532
|
+
return {
|
|
1533
|
+
logs: filteredLogs || ERROR_MESSAGES.NO_LOGS_MATCHING_FILTER,
|
|
1534
|
+
analysis
|
|
1535
|
+
};
|
|
1536
|
+
} catch (error) {
|
|
1537
|
+
throw new Error(
|
|
1538
|
+
`Failed to get deployment logs: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
async analyzeError(deploymentId, token) {
|
|
1543
|
+
try {
|
|
1544
|
+
const logs = await this.adapter.getDeploymentLogs(deploymentId, token);
|
|
1545
|
+
return this.analyzeLogs(logs);
|
|
1546
|
+
} catch {
|
|
1547
|
+
return {
|
|
1548
|
+
type: "UNKNOWN",
|
|
1549
|
+
message: "Deployment failed - unable to fetch logs",
|
|
1550
|
+
suggestion: "Check deployment platform dashboard"
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
analyzeLogs(logs) {
|
|
1555
|
+
const lowerLogs = logs.toLowerCase();
|
|
1556
|
+
const lines = logs.split("\n");
|
|
1557
|
+
let type = "UNKNOWN";
|
|
1558
|
+
let quickContext = "";
|
|
1559
|
+
let location;
|
|
1560
|
+
let errorLine;
|
|
1561
|
+
for (const line of lines) {
|
|
1562
|
+
const fileMatch = line.match(/([^\s:]+\.(tsx?|jsx?|js|ts)):(\d+):(\d+)/);
|
|
1563
|
+
if (fileMatch && line.toLowerCase().includes("error")) {
|
|
1564
|
+
location = `${fileMatch[1]}:${fileMatch[3]}:${fileMatch[4]}`;
|
|
1565
|
+
errorLine = line.trim();
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (!errorLine) {
|
|
1570
|
+
errorLine = lines.find((line) => line.toLowerCase().includes("error"))?.trim() || "Check logs for details";
|
|
1571
|
+
}
|
|
1572
|
+
if (lowerLogs.includes("cannot find module") || lowerLogs.includes("modulenotfounderror")) {
|
|
1573
|
+
type = "MISSING_DEPENDENCY";
|
|
1574
|
+
quickContext = "Missing package/dependency";
|
|
1575
|
+
} else if (lowerLogs.includes("environment variable") || lowerLogs.includes("env var")) {
|
|
1576
|
+
type = "ENV_VAR";
|
|
1577
|
+
quickContext = "Environment variable issue";
|
|
1578
|
+
} else if (lowerLogs.includes("timeout") || lowerLogs.includes("time limit")) {
|
|
1579
|
+
type = "TIMEOUT";
|
|
1580
|
+
quickContext = "Build timeout exceeded";
|
|
1581
|
+
} else if (lowerLogs.includes("error") || lowerLogs.includes("failed")) {
|
|
1582
|
+
type = "BUILD";
|
|
1583
|
+
quickContext = "Build failed";
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
type,
|
|
1587
|
+
message: quickContext || errorLine,
|
|
1588
|
+
location,
|
|
1589
|
+
suggestion: location ? `Error at ${location} - check the file for issues` : "See full logs below for detailed analysis"
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
async compareWithPrevious(project, currentDeploymentId, token) {
|
|
1593
|
+
try {
|
|
1594
|
+
const deployments = await this.adapter.getRecentDeployments(
|
|
1595
|
+
project,
|
|
1596
|
+
token,
|
|
1597
|
+
DEFAULT_COMPARISON_COUNT
|
|
1598
|
+
);
|
|
1599
|
+
if (deployments.length < 2) {
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
const current = deployments.find((d) => d.id === currentDeploymentId) || deployments[0];
|
|
1603
|
+
const previous = deployments.find((d) => d.id !== currentDeploymentId) || deployments[1];
|
|
1604
|
+
const currentBuildTime = current.buildingAt && current.ready ? Math.round(
|
|
1605
|
+
(current.ready - current.buildingAt) / BUILD_TIME_SECONDS_DIVISOR
|
|
1606
|
+
) : 0;
|
|
1607
|
+
const previousBuildTime = previous.buildingAt && previous.ready ? Math.round(
|
|
1608
|
+
(previous.ready - previous.buildingAt) / BUILD_TIME_SECONDS_DIVISOR
|
|
1609
|
+
) : 0;
|
|
1610
|
+
const delta = currentBuildTime - previousBuildTime;
|
|
1611
|
+
const percentage = previousBuildTime > 0 ? Math.round(
|
|
1612
|
+
delta / previousBuildTime * DEFAULTS.PERCENTAGE_MULTIPLIER
|
|
1613
|
+
) : 0;
|
|
1614
|
+
return {
|
|
1615
|
+
deployments: {
|
|
1616
|
+
current: {
|
|
1617
|
+
id: current.uid || current.id || "",
|
|
1618
|
+
url: current.url,
|
|
1619
|
+
timestamp: current.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1620
|
+
status: current.state || "unknown",
|
|
1621
|
+
buildTime: currentBuildTime,
|
|
1622
|
+
timeAgo: this.getTimeAgo(new Date(current.createdAt).getTime())
|
|
1623
|
+
},
|
|
1624
|
+
previous: {
|
|
1625
|
+
id: previous.uid || previous.id || "",
|
|
1626
|
+
url: previous.url,
|
|
1627
|
+
timestamp: previous.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1628
|
+
status: previous.state || "unknown",
|
|
1629
|
+
buildTime: previousBuildTime,
|
|
1630
|
+
timeAgo: this.getTimeAgo(new Date(previous.createdAt).getTime())
|
|
1631
|
+
}
|
|
1632
|
+
},
|
|
1633
|
+
performance: {
|
|
1634
|
+
buildTime: {
|
|
1635
|
+
current: currentBuildTime,
|
|
1636
|
+
previous: previousBuildTime,
|
|
1637
|
+
delta,
|
|
1638
|
+
percentage
|
|
1639
|
+
}
|
|
1640
|
+
},
|
|
1641
|
+
changes: {},
|
|
1642
|
+
risk: Math.abs(percentage) > HIGH_RISK_THRESHOLD_PERCENT ? "HIGH" : Math.abs(percentage) > MEDIUM_RISK_THRESHOLD_PERCENT ? "MEDIUM" : "LOW"
|
|
1643
|
+
};
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
console.error("Error comparing with previous deployment:", error);
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
formatComparison(comparison) {
|
|
1650
|
+
const { buildTime } = comparison.performance;
|
|
1651
|
+
if (buildTime.delta === 0) {
|
|
1652
|
+
return STATUS_MESSAGES.BUILD_TIME_SAME(buildTime.current);
|
|
1653
|
+
}
|
|
1654
|
+
const faster = buildTime.delta < 0;
|
|
1655
|
+
return STATUS_MESSAGES.BUILD_TIME_CHANGE(
|
|
1656
|
+
buildTime.current,
|
|
1657
|
+
buildTime.delta,
|
|
1658
|
+
faster
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
|
|
1663
|
+
// src/core/response-formatter.ts
|
|
1664
|
+
var ResponseFormatter = class {
|
|
1665
|
+
static formatComparison(data) {
|
|
1666
|
+
const current = data.deployments.current;
|
|
1667
|
+
const previous = data.deployments.previous;
|
|
1668
|
+
const perf = data.performance.buildTime;
|
|
1669
|
+
const display = `## Deployment Comparison
|
|
1670
|
+
|
|
1671
|
+
### Current Deployment
|
|
1672
|
+
**Status:** ${current.status === "success" ? "\u2705 Success" : "\u274C Failed"}
|
|
1673
|
+
**URL:** ${current.url || "N/A"}
|
|
1674
|
+
**Time:** ${current.timeAgo}
|
|
1675
|
+
**Build Duration:** ${current.buildTime}s
|
|
1676
|
+
|
|
1677
|
+
**Commit:** \`${current.commit?.sha?.slice(0, 7) || "N/A"}\` ${current.commit?.message || "No message"}
|
|
1678
|
+
**Author:** ${current.commit?.author || "Unknown"}
|
|
1679
|
+
|
|
1680
|
+
### Previous Deployment
|
|
1681
|
+
**Status:** ${previous.status === "success" ? "\u2705 Success" : "\u274C Failed"}
|
|
1682
|
+
**URL:** ${previous.url || "N/A"}
|
|
1683
|
+
**Time:** ${previous.timeAgo}
|
|
1684
|
+
**Build Duration:** ${previous.buildTime}s
|
|
1685
|
+
|
|
1686
|
+
**Commit:** \`${previous.commit?.sha?.slice(0, 7) || "N/A"}\` ${previous.commit?.message || "No message"}
|
|
1687
|
+
**Author:** ${previous.commit?.author || "Unknown"}
|
|
1688
|
+
|
|
1689
|
+
### Performance Analysis
|
|
1690
|
+
**Build Time Change:** ${Math.abs(perf.delta)}s ${perf.delta < 0 ? "faster" : "slower"} (${perf.percentage}% ${perf.delta < 0 ? "improvement" : "increase"})
|
|
1691
|
+
**Risk Level:** ${data.risk === "LOW" ? "\u{1F7E2} Low" : data.risk === "MEDIUM" ? "\u{1F7E1} Medium" : "\u{1F534} High"}
|
|
1692
|
+
`;
|
|
1693
|
+
return {
|
|
1694
|
+
version: "1.0",
|
|
1695
|
+
tool: "compare_deployments",
|
|
1696
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1697
|
+
display,
|
|
1698
|
+
data,
|
|
1699
|
+
highlights: {
|
|
1700
|
+
url: current.url,
|
|
1701
|
+
status: current.status,
|
|
1702
|
+
duration: `${perf.delta}s ${perf.delta < 0 ? "faster" : "slower"}`
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
static formatLogs(logs, analysis, summary) {
|
|
1707
|
+
const truncatedLogs = logs.split("\n").slice(0, 30).join("\n");
|
|
1708
|
+
const isTruncated = logs.split("\n").length > 30;
|
|
1709
|
+
const display = `## Deployment Logs
|
|
1710
|
+
|
|
1711
|
+
### Summary
|
|
1712
|
+
**Errors:** ${summary.errorCount === 0 ? "\u2705" : "\u274C"} ${summary.errorCount} error${summary.errorCount !== 1 ? "s" : ""}
|
|
1713
|
+
**Warnings:** ${summary.warningCount === 0 ? "\u2705" : "\u26A0\uFE0F"} ${summary.warningCount} warning${summary.warningCount !== 1 ? "s" : ""}
|
|
1714
|
+
**URL:** ${summary.deploymentUrl || "Not available"}
|
|
1715
|
+
|
|
1716
|
+
${analysis.type !== "UNKNOWN" ? `### Error Analysis
|
|
1717
|
+
**Type:** ${analysis.type}
|
|
1718
|
+
**Message:** ${analysis.message}
|
|
1719
|
+
**Location:** ${analysis.location ? `\`${analysis.location}\`` : "Unknown"}
|
|
1720
|
+
**Suggested Fix:** ${analysis.suggestion || "Check the logs below for details"}
|
|
1721
|
+
|
|
1722
|
+
` : ""}
|
|
1723
|
+
### Logs
|
|
1724
|
+
\`\`\`
|
|
1725
|
+
${truncatedLogs}
|
|
1726
|
+
${isTruncated ? "\n... (truncated - showing first 30 lines)" : ""}
|
|
1727
|
+
\`\`\`
|
|
1728
|
+
`;
|
|
1729
|
+
return {
|
|
1730
|
+
version: "1.0",
|
|
1731
|
+
tool: "get_deployment_logs",
|
|
1732
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1733
|
+
display,
|
|
1734
|
+
data: { logs, analysis, summary },
|
|
1735
|
+
highlights: {
|
|
1736
|
+
url: summary.deploymentUrl,
|
|
1737
|
+
error: analysis.message,
|
|
1738
|
+
status: summary.hasErrors ? "error" : "success"
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
static formatStatus(status) {
|
|
1743
|
+
const statusIcon = status.status === "success" ? "\u2705 Success" : status.status === "building" ? "\u{1F504} Building" : status.status === "error" ? "\u274C Failed" : status.status;
|
|
1744
|
+
const display = `## Deployment Status
|
|
1745
|
+
|
|
1746
|
+
### Current Status
|
|
1747
|
+
**Project:** ${status.projectName}
|
|
1748
|
+
**Platform:** ${status.platform}
|
|
1749
|
+
**Status:** ${statusIcon}
|
|
1750
|
+
**Environment:** ${status.environment || "production"}
|
|
1751
|
+
**URL:** ${status.url || "Not available"}
|
|
1752
|
+
**Duration:** ${status.duration ? `${status.duration}s` : "N/A"}
|
|
1753
|
+
**Deployed:** ${status.timestamp}
|
|
1754
|
+
|
|
1755
|
+
${status.commit ? `### Commit Info
|
|
1756
|
+
**SHA:** \`${status.commit.sha}\`
|
|
1757
|
+
**Message:** ${status.commit.message}
|
|
1758
|
+
**Author:** ${status.commit.author}
|
|
1759
|
+
` : ""}`;
|
|
1760
|
+
return {
|
|
1761
|
+
version: "1.0",
|
|
1762
|
+
tool: "check_deployment_status",
|
|
1763
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1764
|
+
display,
|
|
1765
|
+
data: status,
|
|
1766
|
+
highlights: {
|
|
1767
|
+
url: status.url,
|
|
1768
|
+
status: status.status,
|
|
1769
|
+
duration: status.duration ? `${status.duration}s` : void 0
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
static formatWatchEvent(event) {
|
|
1774
|
+
const icons = {
|
|
1775
|
+
progress: "\u{1F504}",
|
|
1776
|
+
success: "\u2705",
|
|
1777
|
+
error: "\u274C",
|
|
1778
|
+
warning: "\u26A0\uFE0F"
|
|
1779
|
+
};
|
|
1780
|
+
let formatted = `${icons[event.type] || ""} **${event.message}**`;
|
|
1781
|
+
if (event.details) {
|
|
1782
|
+
if (event.details.url) {
|
|
1783
|
+
formatted += `
|
|
1784
|
+
URL: ${event.details.url}`;
|
|
1785
|
+
}
|
|
1786
|
+
if (event.details.duration) {
|
|
1787
|
+
formatted += `
|
|
1788
|
+
Duration: ${event.details.duration}s`;
|
|
1789
|
+
}
|
|
1790
|
+
if (event.details.suggestion) {
|
|
1791
|
+
formatted += `
|
|
1792
|
+
\u{1F4A1} ${event.details.suggestion}`;
|
|
1793
|
+
}
|
|
1794
|
+
if (event.details.file) {
|
|
1795
|
+
formatted += `
|
|
1796
|
+
File: \`${event.details.file}\``;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return formatted;
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
|
|
1803
|
+
// src/core/mcp-handler.ts
|
|
1804
|
+
var MCPHandler = class {
|
|
1805
|
+
constructor(adapters) {
|
|
1806
|
+
this.adapters = adapters;
|
|
1807
|
+
this.startCacheCleanup();
|
|
1808
|
+
}
|
|
1809
|
+
deploymentIntelligenceCache = /* @__PURE__ */ new Map();
|
|
1810
|
+
cacheTimestamps = /* @__PURE__ */ new Map();
|
|
1811
|
+
maxCacheAge = DEFAULTS.CACHE_TTL_MS;
|
|
1812
|
+
cleanupTimer;
|
|
1813
|
+
getDeploymentIntelligence(platform) {
|
|
1814
|
+
const now = Date.now();
|
|
1815
|
+
const cached = this.deploymentIntelligenceCache.get(platform);
|
|
1816
|
+
const timestamp = this.cacheTimestamps.get(platform);
|
|
1817
|
+
if (cached && timestamp && now - timestamp < this.maxCacheAge) {
|
|
1818
|
+
this.cacheTimestamps.set(platform, now);
|
|
1819
|
+
return cached;
|
|
1820
|
+
}
|
|
1821
|
+
if (cached) {
|
|
1822
|
+
this.deploymentIntelligenceCache.delete(platform);
|
|
1823
|
+
this.cacheTimestamps.delete(platform);
|
|
1824
|
+
}
|
|
1825
|
+
const instance = new DeploymentIntelligence(platform);
|
|
1826
|
+
this.deploymentIntelligenceCache.set(platform, instance);
|
|
1827
|
+
this.cacheTimestamps.set(platform, now);
|
|
1828
|
+
if (this.deploymentIntelligenceCache.size > DEFAULTS.MAX_CACHE_SIZE) {
|
|
1829
|
+
this.evictOldestEntry();
|
|
1830
|
+
}
|
|
1831
|
+
return instance;
|
|
1832
|
+
}
|
|
1833
|
+
evictOldestEntry() {
|
|
1834
|
+
let oldestPlatform;
|
|
1835
|
+
let oldestTime = Date.now();
|
|
1836
|
+
for (const [platform, timestamp] of this.cacheTimestamps.entries()) {
|
|
1837
|
+
if (timestamp < oldestTime) {
|
|
1838
|
+
oldestTime = timestamp;
|
|
1839
|
+
oldestPlatform = platform;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
if (oldestPlatform) {
|
|
1843
|
+
this.deploymentIntelligenceCache.delete(oldestPlatform);
|
|
1844
|
+
this.cacheTimestamps.delete(oldestPlatform);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
startCacheCleanup() {
|
|
1848
|
+
this.cleanupTimer = setInterval(() => {
|
|
1849
|
+
const now = Date.now();
|
|
1850
|
+
const platformsToDelete = [];
|
|
1851
|
+
for (const [platform, timestamp] of this.cacheTimestamps.entries()) {
|
|
1852
|
+
if (now - timestamp > this.maxCacheAge) {
|
|
1853
|
+
platformsToDelete.push(platform);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
for (const platform of platformsToDelete) {
|
|
1857
|
+
this.deploymentIntelligenceCache.delete(platform);
|
|
1858
|
+
this.cacheTimestamps.delete(platform);
|
|
1859
|
+
}
|
|
1860
|
+
}, DEFAULTS.CACHE_CLEANUP_INTERVAL_MS);
|
|
1861
|
+
}
|
|
1862
|
+
dispose() {
|
|
1863
|
+
if (this.cleanupTimer) {
|
|
1864
|
+
clearInterval(this.cleanupTimer);
|
|
1865
|
+
}
|
|
1866
|
+
this.deploymentIntelligenceCache.clear();
|
|
1867
|
+
this.cacheTimestamps.clear();
|
|
1868
|
+
}
|
|
1869
|
+
async handleToolCall(tool, args) {
|
|
1870
|
+
switch (tool) {
|
|
1871
|
+
case "check_deployment_status":
|
|
1872
|
+
return this.checkDeploymentStatus(args);
|
|
1873
|
+
case "watch_deployment":
|
|
1874
|
+
return this.watchDeployment(args);
|
|
1875
|
+
case "compare_deployments":
|
|
1876
|
+
return this.compareDeployments(args);
|
|
1877
|
+
case "get_deployment_logs":
|
|
1878
|
+
return this.getDeploymentLogs(args);
|
|
1879
|
+
default:
|
|
1880
|
+
throw new Error(`Unknown tool: ${tool}`);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
async handleRequest(request) {
|
|
1884
|
+
if (request.method === "tools/call") {
|
|
1885
|
+
const { name, arguments: args } = request.params;
|
|
1886
|
+
const result = await this.handleToolCall(name, args);
|
|
1887
|
+
const text = result.display ? result.display : JSON.stringify(result, null, 2);
|
|
1888
|
+
return {
|
|
1889
|
+
content: [
|
|
1890
|
+
{
|
|
1891
|
+
type: "text",
|
|
1892
|
+
text
|
|
1893
|
+
}
|
|
1894
|
+
]
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
if (request.method === "tools/list") {
|
|
1898
|
+
return {
|
|
1899
|
+
tools: tools.map((tool) => ({
|
|
1900
|
+
...tool,
|
|
1901
|
+
inputSchema: this.schemaToJsonSchema(tool.inputSchema)
|
|
1902
|
+
}))
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
throw new Error(`Unknown method: ${request.method}`);
|
|
1906
|
+
}
|
|
1907
|
+
async checkDeploymentStatus(args) {
|
|
1908
|
+
const validated = checkDeploymentStatusSchema.parse(args);
|
|
1909
|
+
const adapter = this.adapters.get(validated.platform);
|
|
1910
|
+
if (!adapter) {
|
|
1911
|
+
throw new Error(`Unsupported platform: ${validated.platform}`);
|
|
1912
|
+
}
|
|
1913
|
+
try {
|
|
1914
|
+
const status = await adapter.getLatestDeployment(
|
|
1915
|
+
validated.project,
|
|
1916
|
+
validated.token
|
|
1917
|
+
);
|
|
1918
|
+
const formattedStatus = this.formatResponse(status, validated.platform);
|
|
1919
|
+
return ResponseFormatter.formatStatus(formattedStatus);
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
console.error(`Error checking deployment status:`, error);
|
|
1922
|
+
throw new Error(
|
|
1923
|
+
`Failed to get deployment status: ${error instanceof Error ? error.message : String(error)}`
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
formatResponse(status, platform) {
|
|
1928
|
+
return {
|
|
1929
|
+
...status,
|
|
1930
|
+
platform,
|
|
1931
|
+
timestamp: status.timestamp || (/* @__PURE__ */ new Date()).toISOString()
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
async watchDeployment(args) {
|
|
1935
|
+
const validated = watchDeploymentSchema.parse(args);
|
|
1936
|
+
const intelligence = this.getDeploymentIntelligence(validated.platform);
|
|
1937
|
+
const messages = [];
|
|
1938
|
+
const events = [];
|
|
1939
|
+
const generator = intelligence.watchDeployment(validated);
|
|
1940
|
+
for await (const event of generator) {
|
|
1941
|
+
const formatted = ResponseFormatter.formatWatchEvent(event);
|
|
1942
|
+
messages.push(formatted);
|
|
1943
|
+
events.push(event);
|
|
1944
|
+
}
|
|
1945
|
+
const display = `## Deployment Watch
|
|
1946
|
+
|
|
1947
|
+
### Real-time Updates
|
|
1948
|
+
${messages.join("\n\n")}
|
|
1949
|
+
`;
|
|
1950
|
+
const finalEvent = events[events.length - 1];
|
|
1951
|
+
const status = finalEvent?.type === "success" ? "success" : finalEvent?.type === "error" ? "error" : "in_progress";
|
|
1952
|
+
return {
|
|
1953
|
+
version: "1.0",
|
|
1954
|
+
tool: "watch_deployment",
|
|
1955
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1956
|
+
display,
|
|
1957
|
+
data: { events },
|
|
1958
|
+
highlights: {
|
|
1959
|
+
status,
|
|
1960
|
+
url: finalEvent?.details?.url,
|
|
1961
|
+
duration: finalEvent?.details?.duration ? `${finalEvent.details.duration}s` : void 0
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
async compareDeployments(args) {
|
|
1966
|
+
const validated = compareDeploymentsSchema.parse(args);
|
|
1967
|
+
const intelligence = this.getDeploymentIntelligence(validated.platform);
|
|
1968
|
+
const comparison = await intelligence.compareDeployments(validated);
|
|
1969
|
+
if (!comparison) {
|
|
1970
|
+
return {
|
|
1971
|
+
version: "1.0",
|
|
1972
|
+
tool: "compare_deployments",
|
|
1973
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1974
|
+
display: "Not enough deployments to compare",
|
|
1975
|
+
data: { message: "Not enough deployments to compare" },
|
|
1976
|
+
highlights: { status: "error" }
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
return ResponseFormatter.formatComparison(comparison);
|
|
1980
|
+
}
|
|
1981
|
+
async getDeploymentLogs(args) {
|
|
1982
|
+
const validated = getDeploymentLogsSchema.parse(args);
|
|
1983
|
+
const intelligence = this.getDeploymentIntelligence(validated.platform);
|
|
1984
|
+
const result = await intelligence.getDeploymentLogs(validated);
|
|
1985
|
+
const summary = {
|
|
1986
|
+
errorCount: (result.logs.match(/error/gi) || []).length,
|
|
1987
|
+
warningCount: (result.logs.match(/warning/gi) || []).length,
|
|
1988
|
+
deploymentUrl: null,
|
|
1989
|
+
hasErrors: result.analysis?.type !== "UNKNOWN"
|
|
1990
|
+
};
|
|
1991
|
+
return ResponseFormatter.formatLogs(
|
|
1992
|
+
result.logs,
|
|
1993
|
+
result.analysis || { type: "UNKNOWN", message: "No errors detected" },
|
|
1994
|
+
summary
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
schemaToJsonSchema(zodSchema) {
|
|
1998
|
+
return JSON.parse(JSON.stringify(zodSchema));
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
|
|
2002
|
+
export {
|
|
2003
|
+
tools,
|
|
2004
|
+
VercelAdapter,
|
|
2005
|
+
NetlifyAdapter,
|
|
2006
|
+
MCPHandler
|
|
2007
|
+
};
|