deploy-mcp
Version:
Universal deployment tracker for AI assistants
1,556 lines (1,543 loc) • 91.7 kB
JavaScript
// 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, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\//g, "/");
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: