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.

166 lines • 6.15 kB
/** * 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