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
JavaScript
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