UNPKG

run402-mcp

Version:

MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.

82 lines • 5.76 kB
import { guessContentType } from "../namespaces/deploy.js"; import { collectLocalFileReferences, collectAuthoringFileReferences } from "./deploy-manifest.js"; import { scanDeploymentSources } from "./source-scan.js"; import { fileSetFromDirSync } from "./files.js"; import { LocalError } from "../errors.js"; /** * The content type a site entry will ship with: an explicit `contentType` * on a wrapper, `FsFileSource` or `ContentRef` wins; otherwise the path's * extension decides, exactly as the deploy planner infers it. */ function siteEntryContentType(path, source) { if (source && typeof source === "object" && !(source instanceof Uint8Array) && !(source instanceof ArrayBuffer)) { const explicit = source.contentType; if (typeof explicit === "string" && explicit.length > 0) return explicit; } return guessContentType(path); } function isLocalDirRef(value) { return typeof value === "object" && value !== null && value.__source === "local-dir" && typeof value.path === "string"; } /** * The files a site slice will ship. A `local-dir` marker is walked with the * same rules deploy uses, so the summary counts real files rather than the * marker's own keys; a directory that cannot be walked counts as empty here * (the file-reference check and deploy itself report why). */ function siteEntries(slice) { if (!isLocalDirRef(slice)) return (slice ?? {}); let files; try { files = fileSetFromDirSync(slice.path, { ignore: slice.ignore, includeSensitive: slice.includeSensitive }); } catch (err) { if (err instanceof LocalError) return {}; throw err; } if (!slice.prefix) return files; const sep = slice.prefix.endsWith("/") ? "" : "/"; return Object.fromEntries(Object.entries(files).map(([rel, source]) => [`${slice.prefix}${sep}${rel}`, source])); } /** * `summary.site` for `--check`: how many paths the site slice ships and a * per-content-type tally, so "can I ship a .webp?" is answered by the * preflight rather than by reading the SDK. */ export function summarizeSiteInventory(spec) { const entries = siteEntries(spec.site?.replace ?? spec.site?.patch?.put); const by_content_type = {}; for (const [path, source] of Object.entries(entries)) { const type = siteEntryContentType(path, source); by_content_type[type] = (by_content_type[type] ?? 0) + 1; } const sorted = Object.fromEntries(Object.entries(by_content_type).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))); return { paths: Object.keys(entries).length, by_content_type: sorted }; } export function describeLocalPreflight(input) { const spec = input.spec ?? {}; const refs = input.authoring ? collectAuthoringFileReferences(input.authoring, input.appRoot) : collectLocalFileReferences(spec); const scan = scanDeploymentSources(spec, input.appRoot); const warningRoutes = (spec.routes?.replace ?? []).filter((route) => route.target.type === "function" && spec.functions?.replace && !spec.functions.replace[route.target.name]); const checks = [ { name: "manifest", status: "passed", scope: input.manifestPath, evidence_count: 1 }, { name: "target_agreement", status: "passed", scope: input.appRoot, evidence_count: input.target.project_id ? 1 : 0 }, { name: "file_references", status: input.buildDeferred ? "deferred" : "passed", scope: input.appRoot, evidence_count: input.buildDeferred ? 0 : refs.length, ...(input.buildDeferred ? { reason: "Build commands may produce these files. No build was executed." } : {}) }, { name: "source_scan", status: process.env.RUN402_DEPLOY_SKIP_SCAN === "1" ? "skipped" : "passed", scope: scan.scan_root, evidence_count: scan.checked_files, reason: process.env.RUN402_DEPLOY_SKIP_SCAN === "1" ? "Explicit RUN402_DEPLOY_SKIP_SCAN override." : "Selected application source and explicit file references; unrelated applications excluded." }, ...["remote_policy", "quota", "cost", "secret_existence", "migration_registry", "content_presence", "base_release_drift"].map((name) => ({ name, status: "deferred", scope: "gateway", evidence_count: 0, reason: "Requires gateway --plan review." })), ]; return { app_root: input.appRoot, manifest_path: input.manifestPath, gateway_validated: false, target: { ...input.target, api_base: input.apiBase ?? null, profile: input.profile ?? null }, checks, warnings: [...scan.findings.filter((finding) => finding.severity !== "error"), ...warningRoutes.map((route) => ({ code: "LOCAL_ROUTE_TARGET_UNDECLARED", message: "Route references a function absent from functions.replace; review this target.", pattern: route.pattern }))], summary: { file_references: input.buildDeferred ? null : refs.length, site: summarizeSiteInventory(spec), functions: Object.keys(spec.functions?.replace ?? spec.functions?.patch?.set ?? {}).length, migrations: spec.database?.migrations?.length ?? 0, routes: spec.routes?.replace?.length ?? 0 }, next_actions: input.target.project_id ? [{ type: "review_plan", ...(input.manifestPath ? { argv: ["run402", ...(input.entryPoint === "deploy" ? ["deploy"] : ["up"]), "--manifest", input.manifestPath, "--project", input.target.project_id, "--plan"] } : { sdk_call: "project(project_id).apply.plan(spec)" }), why: "Review gateway-authoritative policy, cost and current state." }] : [{ type: "select_project", safe_to_auto_execute: false, why: input.target.source === "create" ? "Create intent is selected; the new project must exist before gateway plan review." : "Choose an existing project or an explicit new-project name before deploying." }], }; } //# sourceMappingURL=preflight.js.map