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.
166 lines • 6.15 kB
JavaScript
/**
* Node-only filesystem byte-source helpers for `apply`.
*
* `fileSetFromDir(path)` walks a directory and emits a path-keyed `FileSet`
* of `FsFileSource` markers. The SDK normalizer reads each file's bytes
* lazily during the plan/upload phases — never loaded into memory at
* collection time. Suitable for multi-GB build outputs.
*/
import { readdir, lstat } from "node:fs/promises";
import { readdirSync, lstatSync } from "node:fs";
import { join, relative, sep } from "node:path";
import { LocalError } from "../errors.js";
const DEFAULT_IGNORE = new Set([".git", "node_modules", ".DS_Store"]);
const DEFAULT_SENSITIVE_IGNORE_NAMES = new Set([
".env",
".envrc",
".npmrc",
".pnpmrc",
".yarnrc",
".netrc",
".pypirc",
"id_dsa",
"id_ecdsa",
"id_ed25519",
"id_rsa",
]);
const DEFAULT_SENSITIVE_IGNORE_SUFFIXES = [".key", ".pem", ".p12", ".pfx"];
const CONTEXT = "collecting files for deploy";
/**
* Walk a directory and return a `FileSet` whose values are `FsFileSource`
* markers. Each marker holds an absolute path and an extension-derived
* content-type; the deploy normalizer hashes and uploads each file from
* disk on demand.
*
* Skips `.git`, `node_modules`, `.DS_Store`, common secret-bearing filenames
* like `.env`, `.npmrc`, and private-key material (plus any names in
* `opts.ignore`) at every depth. Pass `{ includeSensitive: true }` to opt in
* to collecting those sensitive filenames deliberately. Rejects symlinks.
* Throws `LocalError` if the directory does not exist, is not a directory, or
* contains no deployable files after the ignore list is applied.
*
* @example
* import { run402 } from "@run402/sdk/node";
* import { fileSetFromDir } from "@run402/sdk/node";
* const r = run402();
* await r.project(id).apply({
* project,
* site: { replace: fileSetFromDir("./dist") },
* });
*/
export async function fileSetFromDir(root, opts = {}) {
const ignore = ignoreSetFor(opts);
let rootStat;
try {
rootStat = await lstat(root);
}
catch (err) {
throw unreadableDir(root, err);
}
assertWalkableRoot(root, rootStat);
const out = {};
await walkInto(root, root, ignore, opts.includeSensitive === true, out);
return nonEmpty(root, out);
}
/**
* Synchronous twin of {@link fileSetFromDir}: the same ignore rules, symlink
* refusal, and errors. For local previews that must not go async, such as the
* `run402 up --check` site summary, so they count exactly what deploy uploads.
*/
export function fileSetFromDirSync(root, opts = {}) {
const ignore = ignoreSetFor(opts);
let rootStat;
try {
rootStat = lstatSync(root);
}
catch (err) {
throw unreadableDir(root, err);
}
assertWalkableRoot(root, rootStat);
const out = {};
walkIntoSync(root, root, ignore, opts.includeSensitive === true, out);
return nonEmpty(root, out);
}
function ignoreSetFor(opts) {
const ignore = new Set(DEFAULT_IGNORE);
if (opts.ignore)
for (const name of opts.ignore)
ignore.add(name);
return ignore;
}
function unreadableDir(path, err) {
return new LocalError(`cannot read directory ${path}: ${err.message}`, CONTEXT, err);
}
function assertWalkableRoot(root, rootStat) {
if (rootStat.isSymbolicLink()) {
throw new LocalError(`symlink found at ${root} (following symlinks is not supported)`, CONTEXT);
}
if (!rootStat.isDirectory()) {
throw new LocalError(`path ${root} is not a directory`, CONTEXT);
}
}
function nonEmpty(root, out) {
if (Object.keys(out).length === 0) {
throw new LocalError(`directory ${root} contains no deployable files`, CONTEXT);
}
return out;
}
/** What one directory entry means for the walk; shared by both walkers. */
function classifyEntry(entry, current, ignore, includeSensitive) {
if (ignore.has(entry.name))
return { kind: "skip" };
if (!includeSensitive && isSensitiveIgnoredName(entry.name))
return { kind: "skip" };
const fullPath = join(current, entry.name);
if (entry.isSymbolicLink()) {
throw new LocalError(`symlink found at ${fullPath} (following symlinks is not supported)`, CONTEXT);
}
if (entry.isDirectory())
return { kind: "dir", fullPath };
if (entry.isFile())
return { kind: "file", fullPath };
return { kind: "skip" };
}
async function walkInto(root, current, ignore, includeSensitive, out) {
let entries;
try {
entries = await readdir(current, { withFileTypes: true });
}
catch (err) {
throw unreadableDir(current, err);
}
for (const entry of entries) {
const found = classifyEntry(entry, current, ignore, includeSensitive);
if (found.kind === "dir")
await walkInto(root, found.fullPath, ignore, includeSensitive, out);
else if (found.kind === "file")
out[normalizeRelPath(relative(root, found.fullPath))] = { __source: "fs-file", path: found.fullPath };
}
}
function walkIntoSync(root, current, ignore, includeSensitive, out) {
let entries;
try {
entries = readdirSync(current, { withFileTypes: true });
}
catch (err) {
throw unreadableDir(current, err);
}
for (const entry of entries) {
const found = classifyEntry(entry, current, ignore, includeSensitive);
if (found.kind === "dir")
walkIntoSync(root, found.fullPath, ignore, includeSensitive, out);
else if (found.kind === "file")
out[normalizeRelPath(relative(root, found.fullPath))] = { __source: "fs-file", path: found.fullPath };
}
}
function isSensitiveIgnoredName(name) {
const lower = name.toLowerCase();
return (lower.startsWith(".env.") ||
DEFAULT_SENSITIVE_IGNORE_NAMES.has(lower) ||
DEFAULT_SENSITIVE_IGNORE_SUFFIXES.some((suffix) => lower.endsWith(suffix)));
}
/** Normalize a relative path to POSIX forward slashes. Exposed for tests. */
export function normalizeRelPath(rel) {
return sep === "/" ? rel : rel.split(sep).join("/");
}
//# sourceMappingURL=files.js.map