UNPKG

deploy-mcp

Version:

Universal deployment tracker for AI assistants

1,556 lines (1,543 loc) 91.7 kB
// src/core/tools.ts import { z } from "zod"; var SUPPORTED_PLATFORMS = ["vercel", "netlify", "cloudflare-pages"]; var checkDeploymentStatusSchema = z.object({ platform: z.enum(SUPPORTED_PLATFORMS).describe("The deployment platform"), project: z.string().describe("The project name or ID"), token: z.string().optional().describe("API token for authentication (optional if set in environment)"), limit: z.number().min(1).max(20).default(1).describe("Number of recent deployments to return (default: 1, max: 20)") }); var watchDeploymentSchema = z.object({ platform: z.enum(SUPPORTED_PLATFORMS).describe("The deployment platform"), project: z.string().describe("The project name or ID"), deploymentId: z.string().optional().describe("Specific deployment ID to watch (optional, defaults to latest)"), token: z.string().optional().describe("API token for authentication (optional if set in environment)") }); var compareDeploymentsSchema = z.object({ platform: z.enum(SUPPORTED_PLATFORMS).describe("The deployment platform"), project: z.string().describe("The project name or ID"), mode: z.enum([ "last_vs_previous", // Default: current vs previous "current_vs_success", // Compare to last successful deploy "current_vs_production", // Compare to what's in production "between_dates", // Compare deployments from specific dates "by_ids" // Compare two specific deployment IDs ]).default("last_vs_previous").describe("Comparison mode to use"), deploymentA: z.string().optional().describe("First deployment ID (for by_ids mode)"), deploymentB: z.string().optional().describe("Second deployment ID (for by_ids mode)"), dateFrom: z.string().optional().describe("Start date (for between_dates mode, ISO format)"), dateTo: z.string().optional().describe("End date (for between_dates mode, ISO format)"), token: z.string().optional().describe("API token for authentication (optional if set in environment)") }); var getDeploymentLogsSchema = z.object({ platform: z.enum(SUPPORTED_PLATFORMS).describe("The deployment platform"), deploymentId: z.string().describe("The deployment ID or 'latest' for most recent"), project: z.string().optional().describe( "Project/site name (required when using 'latest' as deploymentId)" ), filter: z.enum(["error", "warning", "all"]).default("error").describe("Filter logs by type (default: error)"), token: z.string().optional().describe("API token for authentication (optional if set in environment)") }); var listProjectsSchema = z.object({ platform: z.enum(SUPPORTED_PLATFORMS).describe("The deployment platform"), limit: z.number().min(1).max(100).default(20).describe("Maximum number of projects to return (default: 20, max: 100)"), token: z.string().optional().describe("API token for authentication (optional if set in environment)") }); var tools = [ { name: "check_deployment_status", description: "Check the latest deployment status for a project on a platform", inputSchema: { type: "object", properties: { platform: { type: "string", enum: SUPPORTED_PLATFORMS, description: "The deployment platform" }, project: { type: "string", description: "The project name or ID" }, token: { type: "string", description: "API token for authentication (optional if set in environment)" }, limit: { type: "number", minimum: 1, maximum: 20, default: 1, description: "Number of recent deployments to return (default: 1, max: 20)" } }, required: ["platform", "project"] } }, { name: "watch_deployment", description: "Stream real-time deployment progress with detailed status updates and error information", inputSchema: { type: "object", properties: { platform: { type: "string", enum: SUPPORTED_PLATFORMS, description: "The deployment platform" }, project: { type: "string", description: "The project name or ID" }, deploymentId: { type: "string", description: "Specific deployment ID to watch (optional, defaults to latest)" }, token: { type: "string", description: "API token for authentication (optional if set in environment)" } }, required: ["platform", "project"] } }, { name: "compare_deployments", description: "Compare deployments using smart comparison modes to identify changes, performance differences, and potential issues", inputSchema: { type: "object", properties: { platform: { type: "string", enum: SUPPORTED_PLATFORMS, description: "The deployment platform" }, project: { type: "string", description: "The project name or ID" }, mode: { type: "string", enum: [ "last_vs_previous", "current_vs_success", "current_vs_production", "between_dates", "by_ids" ], default: "last_vs_previous", description: "Comparison mode: last_vs_previous (default), current_vs_success, current_vs_production, between_dates, or by_ids" }, deploymentA: { type: "string", description: "First deployment ID (required for by_ids mode)" }, deploymentB: { type: "string", description: "Second deployment ID (required for by_ids mode)" }, dateFrom: { type: "string", description: "Start date in ISO format (required for between_dates mode)" }, dateTo: { type: "string", description: "End date in ISO format (required for between_dates mode)" }, token: { type: "string", description: "API token for authentication (optional if set in environment)" } }, required: ["platform", "project"] } }, { name: "get_deployment_logs", description: "Fetch detailed logs for a specific deployment, useful for debugging failed deployments", inputSchema: { type: "object", properties: { platform: { type: "string", enum: SUPPORTED_PLATFORMS, description: "The deployment platform" }, deploymentId: { type: "string", description: "The deployment ID or 'latest' for most recent" }, project: { type: "string", description: "Project/site name (required when using 'latest' as deploymentId)" }, filter: { type: "string", enum: ["error", "warning", "all"], default: "error", description: "Filter logs by type (default: error)" }, token: { type: "string", description: "API token for authentication (optional if set in environment)" } }, required: ["platform", "deploymentId"] } }, { name: "list_projects", description: "List all available projects/sites on a platform that you have access to", inputSchema: { type: "object", properties: { platform: { type: "string", enum: SUPPORTED_PLATFORMS, description: "The deployment platform" }, limit: { type: "number", minimum: 1, maximum: 100, default: 20, description: "Maximum number of projects to return (default: 20, max: 100)" }, token: { type: "string", description: "API token for authentication (optional if set in environment)" } }, required: ["platform"] } } ]; // src/adapters/base/adapter.ts var BaseAdapter = class { formatTimestamp(date) { return new Date(date).toISOString(); } calculateDuration(start, end) { const startTime = new Date(start).getTime(); const endTime = end ? new Date(end).getTime() : Date.now(); return Math.floor((endTime - startTime) / 1e3); } }; // src/adapters/base/types.ts var AdapterException = class extends Error { constructor(type, message, originalError) { super(message); this.type = type; this.originalError = originalError; this.name = "AdapterException"; } }; // src/core/constants.ts var MAX_DEPLOYMENT_WATCH_ATTEMPTS = 120; var BUILD_TIME_SECONDS_DIVISOR = 1e3; var MAX_WATCH_TIME_MS = 24e4; var MAX_BACKOFF_DELAY_MS = 3e4; var BACKOFF_JITTER_MS = 1e3; var HIGH_RISK_THRESHOLD_PERCENT = 50; var MEDIUM_RISK_THRESHOLD_PERCENT = 20; var DEFAULT_COMPARISON_COUNT = 2; var SINGLE_DEPLOYMENT_FETCH = 1; var MAX_TOKENS_PER_MINUTE = 30; var RATE_LIMITER_CLEANUP_AGE_MS = 36e5; var DEPLOYMENT_STATES = { INITIALIZING: "INITIALIZING", BUILDING: "BUILDING", UPLOADING: "UPLOADING", DEPLOYING: "DEPLOYING", READY: "READY", ERROR: "ERROR", CANCELED: "CANCELED" }; var POLLING_INTERVALS_BY_STATE = { INITIALIZING: 5e3, // 5s - slower at start BUILDING: 3e3, // 3s - active building UPLOADING: 2e3, // 2s - final stages DEPLOYING: 2e3, // 2s - final stages READY: 0, // Stop polling ERROR: 0, // Stop polling CANCELED: 0, // Stop polling UNKNOWN: 1e4 // 10s - unknown states }; var LOG_FILTERS = { ERROR: /error|fail|exception|critical/i, WARNING: /warning|warn|deprecat/i }; var DEFAULTS = { DEPLOYMENT_ID_SLICE_LENGTH: 7, ERROR_LINE_TRIM_INDEX: 0, PERCENTAGE_MULTIPLIER: 100, MAX_EVENT_BUFFER_SIZE: 100, MAX_CACHE_SIZE: 10, CACHE_TTL_MS: 30 * 60 * 1e3, // 30 minutes CACHE_CLEANUP_INTERVAL_MS: 5 * 60 * 1e3 // 5 minutes }; var ERROR_MESSAGES = { NO_TOKEN: "No token provided", NO_DEPLOYMENT_FOUND: "No deployment found for this project", DEPLOYMENT_TAKING_LONG: "\u26A0\uFE0F Deployment is taking longer than expected", NO_LOGS_AVAILABLE: "No logs available", NO_LOGS_MATCHING_FILTER: "No logs matching filter criteria" }; var STATUS_ICONS = { ROCKET: "\u{1F680}", HOURGLASS: "\u23F3", HAMMER: "\u{1F528}", PACKAGE: "\u{1F4E6}", GLOBE: "\u{1F30D}", SUCCESS: "\u2705", ERROR: "\u274C", WARNING: "\u26A0\uFE0F", CHART: "\u{1F4CA}", LIGHTNING: "\u26A1", TURTLE: "\u{1F422}", LIGHTBULB: "\u{1F4A1}", PIN: "\u{1F4CD}" }; var STATUS_MESSAGES = { STARTING_WATCH: (id) => `${STATUS_ICONS.ROCKET} Starting to watch deployment ${id}...`, INITIALIZING: `${STATUS_ICONS.HOURGLASS} Initializing deployment...`, BUILDING: `${STATUS_ICONS.HAMMER} Building application...`, UPLOADING: `${STATUS_ICONS.PACKAGE} Uploading to edge network...`, DEPLOYING: `${STATUS_ICONS.GLOBE} Deploying to production...`, DEPLOYMENT_SUCCESS: `${STATUS_ICONS.SUCCESS} Deployment successful!`, DEPLOYMENT_FAILED: (message) => `${STATUS_ICONS.ERROR} Deployment failed: ${message}`, BUILD_TIME_SAME: (time) => `${STATUS_ICONS.CHART} Build time: ${time}s (same as previous)`, 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}` }; var ERROR_TEXT_PATTERNS = { UNAUTHORIZED: ["401", "unauthorized"], NOT_FOUND: ["404", "not found"], RATE_LIMITED: ["429", "rate limit"], TIMEOUT: ["timeout", "AbortError"] }; var API_EVENT_TYPES = { STDOUT: "stdout", STDERR: "stderr" }; var API_MESSAGES = { NO_LOGS_AVAILABLE: "No logs available", INVALID_TOKEN: "Invalid Vercel token", PROJECT_NOT_FOUND: "Project not found", RATE_LIMIT_EXCEEDED: "Rate limit exceeded", REQUEST_TIMEOUT: "Request timeout", FAILED_TO_VALIDATE_TOKEN: "Failed to validate Vercel token" }; var API_PARAMS = { BUILDS: 1, LOGS: 1 }; var API_CONFIG = { VERCEL_BASE_URL: "https://api.vercel.com", CLOUDFLARE_BASE_URL: "https://api.cloudflare.com/client/v4", DEFAULT_TIMEOUT_MS: 1e4, DEFAULT_RETRY_ATTEMPTS: 3, DEFAULT_DEPLOYMENT_LIMIT: 10, SINGLE_DEPLOYMENT_LIMIT: 1 }; var PLATFORM = { VERCEL: "vercel", NETLIFY: "netlify", CLOUDFLARE_PAGES: "cloudflare-pages", GITHUB_PAGES: "github-pages" }; var ENVIRONMENT_TYPES = { PRODUCTION: "production", PREVIEW: "preview", DEVELOPMENT: "development" }; var VERCEL_STATES = { READY: "READY", ERROR: "ERROR", CANCELED: "CANCELED", BUILDING: "BUILDING", INITIALIZING: "INITIALIZING", QUEUED: "QUEUED" }; var NETLIFY_STATES = { NEW: "new", PENDING_REVIEW: "pending_review", ACCEPTED: "accepted", REJECTED: "rejected", ENQUEUED: "enqueued", BUILDING: "building", UPLOADING: "uploading", UPLOADED: "uploaded", PREPARING: "preparing", PREPARED: "prepared", PROCESSING: "processing", PROCESSED: "processed", READY: "ready", ERROR: "error", RETRYING: "retrying" }; var CLOUDFLARE_PAGES_STATES = { ACTIVE: "active", SUCCESS: "success", FAILED: "failed", CANCELED: "canceled", SKIPPED: "skipped" }; var ADAPTER_ERRORS = { TOKEN_REQUIRED: "API token required. Set appropriate environment variable or pass token parameter.", VERCEL_TOKEN_REQUIRED: "Vercel token required. Set VERCEL_TOKEN environment variable or pass token parameter.", NETLIFY_TOKEN_REQUIRED: "Netlify token required. Set NETLIFY_TOKEN environment variable or pass token parameter.", CLOUDFLARE_TOKEN_REQUIRED: "Cloudflare token required. Set CLOUDFLARE_TOKEN environment variable or pass token parameter.", FETCH_DEPLOYMENT_FAILED: "Failed to fetch deployment status", UNKNOWN_STATUS: "unknown", CLOUDFLARE_ACCOUNT_ID_REQUIRED: "Cloudflare account ID required. Provide as 'accountId:apiToken' or set CLOUDFLARE_ACCOUNT_ID" }; // src/adapters/base/api-client.ts var RateLimitError = class extends Error { constructor(retryAfter, endpoint) { super(`Rate limit exceeded for ${endpoint}. Retry after ${retryAfter}ms`); this.retryAfter = retryAfter; this.endpoint = endpoint; this.name = "RateLimitError"; } }; var HTTPError = class _HTTPError extends Error { constructor(statusCode, statusText, url, method) { super(`${method} ${url} failed with ${statusCode}: ${statusText}`); this.statusCode = statusCode; this.statusText = statusText; this.url = url; this.method = method; this.name = "HTTPError"; Error.captureStackTrace(this, _HTTPError); } }; var BaseAPIClient = class { baseUrl; defaultHeaders; timeout; maxRetries; pendingRequests = /* @__PURE__ */ new Map(); rateLimiters = /* @__PURE__ */ new Map(); maxTokensPerMinute = MAX_TOKENS_PER_MINUTE; constructor(config) { this.baseUrl = new URL(config.baseUrl); this.defaultHeaders = new Headers({ "Content-Type": "application/json", "User-Agent": "deploy-mcp/1.0.0", ...config.headers }); this.timeout = config.timeout ?? API_CONFIG.DEFAULT_TIMEOUT_MS; this.maxRetries = config.retry ?? API_CONFIG.DEFAULT_RETRY_ATTEMPTS; } async request(endpoint, options) { if (options?.token) { await this.checkRateLimit(options.token, endpoint.path); } const cacheKey = this.getCacheKey(endpoint, options); if (endpoint.method === "GET" && !options?.body) { const pending = this.pendingRequests.get(cacheKey); if (pending) { return pending; } } const requestPromise = this.executeRequest(endpoint, options); if (endpoint.method === "GET" && !options?.body) { this.pendingRequests.set(cacheKey, requestPromise); requestPromise.then(() => { this.pendingRequests.delete(cacheKey); }).catch(() => { this.pendingRequests.delete(cacheKey); }); } return requestPromise; } async executeRequest(endpoint, options) { const url = this.buildUrl(endpoint.path, options?.searchParams); const headers = this.mergeHeaders(options?.headers); let lastError = new Error("No attempts made"); for (let attempt = 0; attempt <= this.maxRetries; attempt++) { try { const response = await this.fetchWithTimeout(url, { method: endpoint.method, headers, body: options?.body ? JSON.stringify(options.body) : void 0, signal: options?.signal }); if (!response.ok) { throw new HTTPError( response.status, response.statusText, url.toString(), endpoint.method ); } const text = await response.text(); if (!text) { return {}; } try { return JSON.parse(text); } catch { throw new Error( `Invalid JSON response from ${endpoint.path}: ${text.slice(0, 100)}` ); } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (error instanceof HTTPError && error.statusCode >= 400 && error.statusCode < 500 || error instanceof Error && error.name === "AbortError") { throw this.enhanceError(lastError, endpoint); } if (attempt === this.maxRetries) { throw this.enhanceError(lastError, endpoint); } const baseDelay = Math.min( 1e3 * Math.pow(2, attempt), MAX_BACKOFF_DELAY_MS ); const jitter = Math.random() * BACKOFF_JITTER_MS; const totalDelay = Math.min(baseDelay + jitter, MAX_BACKOFF_DELAY_MS); await this.delay(totalDelay); } } throw this.enhanceError(lastError, endpoint); } async fetchWithTimeout(url, init) { const controller = new AbortController(); if (init.signal) { init.signal.addEventListener("abort", () => controller.abort()); } const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { return await fetch(url, { ...init, signal: controller.signal, keepalive: true }); } finally { clearTimeout(timeoutId); } } buildUrl(path, params) { const cleanPath = path.startsWith("/") ? path.slice(1) : path; const baseUrlString = this.baseUrl.toString(); const baseWithSlash = baseUrlString.endsWith("/") ? baseUrlString : baseUrlString + "/"; const url = new URL(cleanPath, baseWithSlash); if (params) { const searchParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== void 0 && value !== null) { searchParams.set(key, String(value)); } } url.search = searchParams.toString(); } return url; } mergeHeaders(headers) { if (!headers) { return this.defaultHeaders; } const merged = new Headers(this.defaultHeaders); for (const [key, value] of Object.entries(headers)) { merged.set(key, value); } return merged; } enhanceError(error, endpoint) { const enhanced = new Error( `API request failed for ${endpoint.path}: ${error.message} See docs: ${endpoint.docsUrl}` ); enhanced.stack = error.stack; enhanced.cause = error; return enhanced; } getCacheKey(endpoint, options) { const params = options?.searchParams ? JSON.stringify(options.searchParams) : ""; return `${endpoint.method}:${endpoint.path}:${params}`; } delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } getRateLimiter(token) { if (!this.rateLimiters.has(token)) { this.rateLimiters.set(token, { tokens: this.maxTokensPerMinute, lastRefill: Date.now(), refillRate: this.maxTokensPerMinute }); } return this.rateLimiters.get(token); } async checkRateLimit(token, endpoint) { const limiter = this.getRateLimiter(token); const now = Date.now(); const timeSinceLastRefill = now - limiter.lastRefill; const minutesElapsed = timeSinceLastRefill / 6e4; const tokensToAdd = minutesElapsed * limiter.refillRate; limiter.tokens = Math.min( this.maxTokensPerMinute, limiter.tokens + tokensToAdd ); limiter.lastRefill = now; if (limiter.tokens < 1) { const waitTime = (1 - limiter.tokens) * (6e4 / limiter.refillRate); throw new RateLimitError(Math.ceil(waitTime), endpoint); } limiter.tokens -= 1; } cleanupRateLimiters() { const now = Date.now(); const maxAge = RATE_LIMITER_CLEANUP_AGE_MS; for (const [token, limiter] of this.rateLimiters.entries()) { if (now - limiter.lastRefill > maxAge) { this.rateLimiters.delete(token); } } } }; // src/adapters/vercel/endpoints.ts var VercelEndpoints = { listDeployments: { path: "/v6/deployments", method: "GET", docsUrl: "https://vercel.com/docs/rest-api/endpoints/deployments#list-deployments", description: "List deployments for authenticated user or team" }, getDeployment: { path: "/v13/deployments", method: "GET", docsUrl: "https://vercel.com/docs/rest-api/endpoints/deployments#get-a-deployment-by-id-or-url", description: "Get deployment by ID or URL" }, getDeploymentEvents: { path: "/v2/deployments", method: "GET", docsUrl: "https://vercel.com/docs/rest-api/endpoints/deployments#get-deployment-events", description: "Get build logs and events for a deployment" }, getUser: { path: "/v2/user", method: "GET", docsUrl: "https://vercel.com/docs/rest-api/endpoints/user#get-the-authenticated-user", description: "Get authenticated user information" }, listProjects: { path: "/v10/projects", method: "GET", docsUrl: "https://vercel.com/docs/rest-api/reference/endpoints/projects/retrieve-a-list-of-projects", description: "List all projects for authenticated user or team" } }; // src/adapters/vercel/api.ts var VercelAPI = class extends BaseAPIClient { endpoints = VercelEndpoints; config; constructor(config) { super({ baseUrl: config.baseUrl, timeout: config.timeout, retry: config.retryAttempts }); this.config = config; } async getDeployments(projectId, token, limit = 1) { try { return await this.request( this.endpoints.listDeployments, { searchParams: { projectId, limit }, headers: { Authorization: `Bearer ${token}` }, token // Pass token for rate limiting } ); } catch (error) { throw this.handleApiError( error, `Failed to fetch deployments for project ${projectId}` ); } } async getUser(token) { try { return await this.request(this.endpoints.getUser, { headers: { Authorization: `Bearer ${token}` }, token // Pass token for rate limiting }); } catch (error) { throw this.handleApiError(error, API_MESSAGES.FAILED_TO_VALIDATE_TOKEN); } } async getDeploymentById(deploymentId, token) { try { const endpoint = { ...this.endpoints.getDeployment, path: `${this.endpoints.getDeployment.path}/${deploymentId}` }; return await this.request(endpoint, { headers: { Authorization: `Bearer ${token}` }, token // Pass token for rate limiting }); } catch (error) { throw this.handleApiError( error, `Failed to fetch deployment ${deploymentId}` ); } } async getDeploymentLogs(deploymentId, token) { try { const endpoint = { ...this.endpoints.getDeploymentEvents, path: `${this.endpoints.getDeploymentEvents.path}/${deploymentId}/events` }; const response = await this.request(endpoint, { searchParams: { builds: API_PARAMS.BUILDS, logs: API_PARAMS.LOGS }, headers: { Authorization: `Bearer ${token}` }, token // Pass token for rate limiting }); if (!response || !Array.isArray(response)) { return API_MESSAGES.NO_LOGS_AVAILABLE; } const logs = response.filter( (event) => event.type === API_EVENT_TYPES.STDOUT || event.type === API_EVENT_TYPES.STDERR ).map((event) => event.payload?.text || event.text || "").join("\n"); const sanitizedLogs = logs.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;").replace(/\//g, "&#x2F;"); return sanitizedLogs || API_MESSAGES.NO_LOGS_AVAILABLE; } catch (error) { throw this.handleApiError( error, `Failed to fetch logs for deployment ${deploymentId}` ); } } async listProjects(token, limit = 20) { try { return await this.request( this.endpoints.listProjects, { searchParams: { limit: limit.toString() }, headers: { Authorization: `Bearer ${token}` }, token // Pass token for rate limiting } ); } catch (error) { throw this.handleApiError(error, "Failed to fetch projects list"); } } handleApiError(error, context) { if (error instanceof Error) { const message = error.message.toLowerCase(); if (ERROR_TEXT_PATTERNS.UNAUTHORIZED.some( (pattern) => message.includes(pattern) )) { return new AdapterException( "UNAUTHORIZED", API_MESSAGES.INVALID_TOKEN, error ); } if (ERROR_TEXT_PATTERNS.NOT_FOUND.some((pattern) => message.includes(pattern))) { return new AdapterException( "NOT_FOUND", API_MESSAGES.PROJECT_NOT_FOUND, error ); } if (ERROR_TEXT_PATTERNS.RATE_LIMITED.some( (pattern) => message.includes(pattern) )) { return new AdapterException( "RATE_LIMITED", API_MESSAGES.RATE_LIMIT_EXCEEDED, error ); } if (ERROR_TEXT_PATTERNS.TIMEOUT.some( (pattern) => message.includes(pattern) || error.name === pattern )) { return new AdapterException( "NETWORK_ERROR", API_MESSAGES.REQUEST_TIMEOUT, error ); } return new AdapterException("NETWORK_ERROR", context, error); } return new AdapterException("UNKNOWN_ERROR", context); } }; // src/adapters/vercel/index.ts var VercelAdapter = class extends BaseAdapter { name = PLATFORM.VERCEL; api; constructor(config) { super(); const defaultConfig = { baseUrl: API_CONFIG.VERCEL_BASE_URL, timeout: API_CONFIG.DEFAULT_TIMEOUT_MS, retryAttempts: API_CONFIG.DEFAULT_RETRY_ATTEMPTS }; this.api = new VercelAPI({ ...defaultConfig, ...config }); } async getLatestDeployment(project, token) { const apiToken = token || process.env.VERCEL_TOKEN; if (!apiToken) { throw new Error(ADAPTER_ERRORS.VERCEL_TOKEN_REQUIRED); } try { const data = await this.api.getDeployments( project, apiToken, API_CONFIG.SINGLE_DEPLOYMENT_LIMIT ); if (!data.deployments || data.deployments.length === 0) { return { status: ADAPTER_ERRORS.UNKNOWN_STATUS, projectName: project, platform: PLATFORM.VERCEL }; } return this.transformDeployment(data.deployments[0]); } catch (error) { if (error instanceof Error) { throw error; } throw new Error(ADAPTER_ERRORS.FETCH_DEPLOYMENT_FAILED); } } async authenticate(token) { try { await this.api.getUser(token); return true; } catch { return false; } } transformDeployment(deployment) { const status = this.mapState(deployment.state); return { id: deployment.uid, status, url: deployment.url ? `https://${deployment.url}` : void 0, projectName: deployment.name, platform: PLATFORM.VERCEL, timestamp: this.formatTimestamp(deployment.createdAt), duration: deployment.ready ? this.calculateDuration(deployment.createdAt, deployment.ready) : void 0, environment: deployment.target || ENVIRONMENT_TYPES.PRODUCTION, commit: deployment.meta ? { sha: deployment.meta.githubCommitSha, message: deployment.meta.githubCommitMessage, author: deployment.meta.githubCommitAuthorName } : void 0 }; } mapState(state) { switch (state) { case VERCEL_STATES.READY: return "success"; case VERCEL_STATES.ERROR: case VERCEL_STATES.CANCELED: return "failed"; case VERCEL_STATES.BUILDING: case VERCEL_STATES.INITIALIZING: case VERCEL_STATES.QUEUED: return "building"; default: return ADAPTER_ERRORS.UNKNOWN_STATUS; } } async getDeploymentById(deploymentId, token) { return this.api.getDeploymentById(deploymentId, token); } async getRecentDeployments(project, token, limit = API_CONFIG.DEFAULT_DEPLOYMENT_LIMIT) { const data = await this.api.getDeployments(project, token, limit); return data.deployments || []; } async getDeploymentLogs(deploymentId, token) { return this.api.getDeploymentLogs(deploymentId, token); } async getDeploymentStatus(project, token) { return this.getLatestDeployment(project, token); } async listProjects(token, limit = 20) { const response = await this.api.listProjects(token, limit); return response.projects.map((project) => ({ id: project.id, name: project.name, url: project.latestDeployments?.[0]?.url ? `https://${project.latestDeployments[0].url}` : void 0 })); } }; // src/adapters/netlify/endpoints.ts var NetlifyEndpoints = { listSites: { path: "/sites", method: "GET", docsUrl: "https://docs.netlify.com/api/get-started/#sites", description: "List all sites for the current user" }, getSite: { path: "/sites/{site_id}", method: "GET", docsUrl: "https://docs.netlify.com/api/get-started/#get-site", description: "Get a specific site by ID" }, listDeploys: { path: "/sites/{site_id}/deploys", method: "GET", docsUrl: "https://docs.netlify.com/api/get-started/#list-site-deploys", description: "List all deploys for a site" }, getDeploy: { path: "/deploys/{deploy_id}", method: "GET", docsUrl: "https://docs.netlify.com/api/get-started/#get-deploy", description: "Get a specific deploy by ID" }, getUser: { path: "/user", method: "GET", docsUrl: "https://docs.netlify.com/api/get-started/#get-current-user", description: "Get the current user" } }; // src/adapters/netlify/api.ts var NetlifyAPI = class extends BaseAPIClient { endpoints = NetlifyEndpoints; config; siteCache = /* @__PURE__ */ new Map(); // name -> id cache constructor(config) { const fullConfig = { baseUrl: "https://api.netlify.com/api/v1", timeout: config?.timeout ?? API_CONFIG.DEFAULT_TIMEOUT_MS, retryAttempts: config?.retryAttempts ?? API_CONFIG.DEFAULT_RETRY_ATTEMPTS, ...config }; super({ baseUrl: fullConfig.baseUrl, timeout: fullConfig.timeout, retry: fullConfig.retryAttempts }); this.config = fullConfig; } /** * Get site ID from site name or ID * Netlify API requires site ID for most endpoints */ async getSiteId(siteNameOrId, token) { if (this.siteCache.has(siteNameOrId)) { return this.siteCache.get(siteNameOrId); } const sites = await this.listSites(token); const site = sites.find( (s) => s.name === siteNameOrId || s.id === siteNameOrId ); if (!site) { throw new Error(`Site not found: ${siteNameOrId}`); } this.siteCache.set(siteNameOrId, site.id); return site.id; } async listDeploys(siteNameOrId, token, limit = 10) { const siteId = await this.getSiteId(siteNameOrId, token); const endpoint = { ...this.endpoints.listDeploys, path: this.endpoints.listDeploys.path.replace("{site_id}", siteId) }; const options = { headers: { Authorization: `Bearer ${token}` }, searchParams: { per_page: limit }, token }; return this.request(endpoint, options); } async getDeploy(deployId, token) { const endpoint = { ...this.endpoints.getDeploy, path: this.endpoints.getDeploy.path.replace("{deploy_id}", deployId) }; const options = { headers: { Authorization: `Bearer ${token}` }, token }; return this.request(endpoint, options); } async getDeployLog(deployId, token) { const deploy = await this.getDeploy(deployId, token); if (!deploy.log_access_attributes?.url) { throw new Error("Deploy logs not available"); } const response = await fetch(deploy.log_access_attributes.url); if (!response.ok) { throw new Error(`Failed to fetch logs: ${response.statusText}`); } return response.text(); } async getUser(token) { const options = { headers: { Authorization: `Bearer ${token}` }, token }; return this.request(this.endpoints.getUser, options); } async listSites(token, limit = 20) { const options = { headers: { Authorization: `Bearer ${token}` }, searchParams: { per_page: limit.toString() }, token }; return this.request(this.endpoints.listSites, options); } }; // src/adapters/netlify/index.ts var NetlifyAdapter = class extends BaseAdapter { name = PLATFORM.NETLIFY; api; constructor() { super(); this.api = new NetlifyAPI(); } /** * Map Netlify deploy states to our standard states * Source: https://github.com/netlify/open-api/blob/master/swagger.yml */ mapState(state) { switch (state) { case "ready": case "processed": return "success"; case "error": case "rejected": return "failed"; case "new": case "pending_review": case "accepted": case "enqueued": case "building": case "uploading": case "uploaded": case "preparing": case "prepared": case "processing": case "retrying": return "building"; default: return "unknown"; } } /** * Transform Netlify deploy to our standard format */ transformDeploy(deploy) { const status = this.mapState(deploy.state); let duration; if (deploy.created_at && deploy.published_at) { duration = this.calculateDuration(deploy.created_at, deploy.published_at); } else if (deploy.deploy_time) { duration = Math.round(deploy.deploy_time / 1e3); } return { id: deploy.id, status, url: deploy.ssl_url || deploy.url || deploy.deploy_ssl_url || deploy.deploy_url, projectName: deploy.name || deploy.site_id, platform: "netlify", timestamp: this.formatTimestamp(deploy.created_at), duration, environment: deploy.context || "production", commit: deploy.commit_ref ? { sha: deploy.commit_ref, message: deploy.title } : void 0 }; } async getLatestDeployment(project, token) { const apiToken = token || process.env.NETLIFY_TOKEN; if (!apiToken) { throw new AdapterException( "UNAUTHORIZED", "Netlify token required. Set NETLIFY_TOKEN environment variable or pass token parameter." ); } try { const deploys = await this.api.listDeploys(project, apiToken, 1); if (!deploys || deploys.length === 0) { throw new AdapterException( "NOT_FOUND", `No deployments found for site: ${project}` ); } return this.transformDeploy(deploys[0]); } catch (error) { if (error instanceof AdapterException) { throw error; } const message = error instanceof Error ? error.message : String(error); if (message.includes("Site not found")) { throw new AdapterException( "NOT_FOUND", `Site not found: ${project}. Make sure the site name is correct.` ); } if (message.includes("401") || message.includes("Unauthorized")) { throw new AdapterException( "UNAUTHORIZED", "Invalid Netlify token. Check your NETLIFY_TOKEN." ); } throw new AdapterException("UNKNOWN", `Netlify API error: ${message}`); } } async authenticate(token) { try { const user = await this.api.getUser(token); return !!user.id; } catch { return false; } } async getDeploymentById(deploymentId, token) { try { return await this.api.getDeploy(deploymentId, token); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new AdapterException( "NOT_FOUND", `Deployment not found: ${deploymentId}. ${message}` ); } } async getRecentDeployments(project, token, limit = 10) { try { return await this.api.listDeploys(project, token, limit); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (message.includes("Site not found")) { throw new AdapterException("NOT_FOUND", `Site not found: ${project}`); } throw new AdapterException( "UNKNOWN", `Failed to fetch deployments: ${message}` ); } } async getDeploymentLogs(deploymentId, token) { try { return await this.api.getDeployLog(deploymentId, token); } catch (error) { const message = error instanceof Error ? error.message : String(error); if (message.includes("not available")) { return "Deploy logs not available for this deployment."; } throw new AdapterException( "UNKNOWN", `Failed to fetch deployment logs: ${message}` ); } } async listProjects(token, limit = 20) { const sites = await this.api.listSites(token, limit); return sites.map((site) => ({ id: site.id, name: site.name, url: site.ssl_url || site.url || void 0 })); } }; // src/adapters/cloudflare-pages/endpoints.ts var CloudflarePagesEndpoints = { // Base URL base: "https://api.cloudflare.com/client/v4", // Projects listProjects: (accountId) => `/accounts/${accountId}/pages/projects`, getProject: (accountId, projectName) => `/accounts/${accountId}/pages/projects/${projectName}`, // Deployments listDeployments: (accountId, projectName) => `/accounts/${accountId}/pages/projects/${projectName}/deployments`, getDeployment: (accountId, projectName, deploymentId) => `/accounts/${accountId}/pages/projects/${projectName}/deployments/${deploymentId}`, createDeployment: (accountId, projectName) => `/accounts/${accountId}/pages/projects/${projectName}/deployments`, deleteDeployment: (accountId, projectName, deploymentId) => `/accounts/${accountId}/pages/projects/${projectName}/deployments/${deploymentId}`, retryDeployment: (accountId, projectName, deploymentId) => `/accounts/${accountId}/pages/projects/${projectName}/deployments/${deploymentId}/retry`, rollbackDeployment: (accountId, projectName, deploymentId) => `/accounts/${accountId}/pages/projects/${projectName}/deployments/${deploymentId}/rollback`, // Logs getDeploymentLogs: (accountId, projectName, deploymentId) => `/accounts/${accountId}/pages/projects/${projectName}/deployments/${deploymentId}/history/logs`, // Documentation URLs docs: { api: "https://developers.cloudflare.com/api/resources/pages/", gettingStarted: "https://developers.cloudflare.com/pages/get-started/", authentication: "https://developers.cloudflare.com/fundamentals/api/get-started/create-token/", deployments: "https://developers.cloudflare.com/pages/configuration/deployments/", buildConfiguration: "https://developers.cloudflare.com/pages/configuration/build-configuration/" } }; // src/adapters/cloudflare-pages/api.ts var CloudflarePagesAPI = class extends BaseAPIClient { accountId; apiToken; endpoints = { listProjects: { path: "", // Will be set dynamically method: "GET", docsUrl: CloudflarePagesEndpoints.docs.api, description: "List all Cloudflare Pages projects" }, getProject: { path: "pages/projects/{projectName}", method: "GET", docsUrl: CloudflarePagesEndpoints.docs.api, description: "Get details for a specific project" }, listDeployments: { path: "pages/projects/{projectName}/deployments", method: "GET", docsUrl: CloudflarePagesEndpoints.docs.deployments, description: "List deployments for a project" }, getDeployment: { path: "pages/projects/{projectName}/deployments/{deploymentId}", method: "GET", docsUrl: CloudflarePagesEndpoints.docs.deployments, description: "Get details for a specific deployment" }, getDeploymentLogs: { path: "pages/projects/{projectName}/deployments/{deploymentId}/history/logs", method: "GET", docsUrl: CloudflarePagesEndpoints.docs.deployments, description: "Get logs for a deployment" }, createDeployment: { path: "pages/projects/{projectName}/deployments", method: "POST", docsUrl: CloudflarePagesEndpoints.docs.deployments, description: "Create a new deployment" }, retryDeployment: { path: "pages/projects/{projectName}/deployments/{deploymentId}/retry", method: "POST", docsUrl: CloudflarePagesEndpoints.docs.deployments, description: "Retry a failed deployment" } }; constructor(accountId, apiToken) { super({ baseUrl: CloudflarePagesEndpoints.base, headers: { Authorization: `Bearer ${apiToken}` }, timeout: 3e4, retry: 3 }); this.accountId = accountId; this.apiToken = apiToken; } cleanPath(path) { return path.startsWith("/") ? path.slice(1) : path; } async listProjects(options) { const endpoint = this.endpoints.listProjects; const path = this.cleanPath( CloudflarePagesEndpoints.listProjects(this.accountId) ); const response = await this.request( { ...endpoint, path }, { searchParams: options?.page || options?.perPage ? { ...options.page && { page: options.page }, ...options.perPage && { per_page: options.perPage } } : void 0, token: this.apiToken } ); if (!response.success) { throw new Error( `Failed to list projects: ${response.errors?.[0]?.message || "Unknown error"}` ); } return response.result; } async getProject(projectName) { const endpoint = this.endpoints.getProject; const path = this.cleanPath( CloudflarePagesEndpoints.getProject(this.accountId, projectName) ); const response = await this.request( { ...endpoint, path }, { token: this.apiToken } ); if (!response.success) { throw new Error( `Failed to get project ${projectName}: ${response.errors?.[0]?.message || "Unknown error"}` ); } return response.result; } async listDeployments(projectName, options) { const endpoint = this.endpoints.listDeployments; const path = this.cleanPath( CloudflarePagesEndpoints.listDeployments(this.accountId, projectName) ); const searchParams = {}; if (options?.page) { searchParams.page = options.page; } if (options?.perPage) { searchParams.per_page = options.perPage; } if (options?.environment) { searchParams.env = options.environment; } const response = await this.request( { ...endpoint, path }, { searchParams, token: this.apiToken } ); if (!response.success) { throw new Error( `Failed to list deployments for ${projectName}: ${response.errors?.[0]?.message || "Unknown error"}` ); } return response.result; } async getDeployment(projectName, deploymentId) { const endpoint = this.endpoints.getDeployment; const path = this.cleanPath( CloudflarePagesEndpoints.getDeployment( this.accountId, projectName, deploymentId ) ); const response = await this.request( { ...endpoint, path }, { token: this.apiToken } ); if (!response.success) { throw new Error( `Failed to get deployment ${deploymentId}: ${response.errors?.[0]?.message || "Unknown error"}` ); } return response.result; } async getLatestDeployment(projectName, environment = "production") { const deployments = await this.listDeployments(projectName, { environment }); return deployments[0] || null; } async getDeploymentLogs(projectName, deploymentId) { const endpoint = this.endpoints.getDeploymentLogs; const path = this.cleanPath( CloudflarePagesEndpoints.getDeploymentLogs( this.accountId, projectName, deploymentId ) ); const response = await this.request( { ...endpoint, path }, { token: this.apiToken } ); if (!response.success) { throw new Error( `Failed to get deployment logs: ${response.errors?.[0]?.message || "Unknown error"}` ); } return response; } async createDeployment(projectName, branch) { const endpoint = this.endpoints.createDeployment; const path = this.cleanPath( CloudflarePagesEndpoints.createDeployment(this.accountId, projectName) ); const body = branch ? { branch } : {}; const response = await this.request( { ...endpoint, path }, { body, token: this.apiToken } ); if (!response.success) { throw new Error( `Failed to create deployment: ${response.errors?.[0]?.message || "Unknown error"}` ); } return response.result; } async retryDeployment(projectName, deploymentId) { const endpoint = this.endpoints.retryDeployment; const path = this.cleanPath( CloudflarePagesEndpoints.retryDeployment( this.accountId, projectName, deploymentId ) ); const response = await this.request( { ...endpoint, path }, { token: this.apiToken } ); if (!response.success) { throw new Error( `Failed to retry deployment: ${response.errors?.[0]?.message || "Unknown error"}` ); } return response.result; } }; // src/adapters/cloudflare-pages/index.ts var CloudflarePagesAdapter = class extends BaseAdapter { name = "cloudflare-pages"; api = null; getAPI(token) { let accountId; let apiToken; if (token.includes(":")) { [accountId, apiToken] = token.split(":", 2); } else { accountId = process.env.CLOUDFLARE_ACCOUNT_ID || ""; apiToken = token; } if (!accountId) { throw new Error( "Cloudflare account ID is required. Provide it as 'accountId:apiToken' or set CLOUDFLARE_ACCOUNT_ID environment variable" ); } if (!this.api || this.api["accountId"] !== accountId) { this.api = new CloudflarePagesAPI(accountId, apiToken); } return this.api; } async getLatestDeployment(project, token) { const apiToken = token || process.env.CLOUDFLARE_TOKEN || process.env.CLOUDFLARE_API_TOKEN; if (!apiToken) { throw new Error("Cloudflare API token is required"); } try { const api = this.getAPI(apiToken); const deployment = await api.getLatestDeployment(project); if (!deployment) { return { status: "unknown", projectName: project, platform: "cloudflare-pages" }; } return this.transformDeployment(deployment); } catch (error) { if (error instanceof Error) { throw error; } throw new Error(`Failed to fetch deployment: ${error}`); } } async authenticate(token) { try { const api = this.getAPI(token); await api.listProjects({ perPage: 1 }); return true; } catch { return false; } } async getDeploymentById(deploymentId, token) { if (!deploymentId.includes(":")) { throw new Error( "Deployment ID must be in format 'projectName:deploymentId' for Cloudflare Pages" ); } const [projectName, actualDeploymentId] = deploymentId.split(":", 2); const api = this.getAPI(token); return api.getDeployment(projectName, actualDeploymentId); } async getRecentDeployments(project, token, _limit = 10) { const api = this.getAPI(token); return api.listDeployments(project); } async getDeploymentLogs(deploymentId, token) { if (!deploymentId.includes(":")) { throw new Error( "Deployment ID must be in format 'projectName:deploymentId' for Cloudflare Pages" ); } const [projectName, actualDeploymentId] = deploymentId.split(":", 2); const api = this.getAPI(token); const response = await api.getDeploymentLogs( projectName, actualDeploymentId ); if (!response.result?.data || response.result.data.length === 0) { return "No logs available"; } return response.result.data.map( (log) => `[${log.timestamp}] ${log.level.toUpperCase()}: ${log.message}` ).join("\n"); } async listProjects(token, _limit = 20) { const api = this.getAPI(token); const projects = await api.listProjects(); return projects.map((project) => ({ id: project.id, name: project.name, url: project.domains?.[0] ? `https://${project.domains[0]}` : `https://${project.subdomain}.pages.dev` })); } transformDeployment(deployment) { const status = this.mapStageStatus(deployment.latest_stage?.status); return { id: deployment.id, status, url: deployment.url, projectName: deployment.project_name, platform: "cloudflare-pages", timestamp: this.formatTimestamp(deployment.created_on), duration: