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.
@@ -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, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;").replace(/\//g, "&#x2F;");
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
+ };