import { createServer, type Server } from "node:http";
import { createReadStream } from "node:fs";
import { mkdir, readFile, readdir, stat, writeFile, cp } from "node:fs/promises";
import { existsSync } from "node:fs";
import { extname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type { ArtifactMetadata } from "../types.js";
import { resolveDesignSystem } from "../design/resolveDesignSystem.js";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const packageRoot = resolve(__dirname, "../..");
export function artifactsRoot(cwd: string) {
return join(cwd, ".pi", "artifacts");
}
export function slugify(input: string) {
const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 54);
return slug || "artifact";
}
export function artifactDir(cwd: string, id: string) {
return join(artifactsRoot(cwd), id);
}
export async function readTextAsset(pathFromSrc: string) {
return readFile(join(packageRoot, "src", pathFromSrc), "utf8");
}
export async function ensureArtifactScaffold(cwd: string, params: { id?: string; title: string; kind?: string }) {
const base = artifactsRoot(cwd);
await mkdir(base, { recursive: true });
let id = params.id ? slugify(params.id) : slugify(params.title);
let dir = artifactDir(cwd, id);
if (!params.id) {
let suffix = 2;
while (existsSync(join(dir, "artifact.json"))) {
id = `${slugify(params.title)}-${suffix++}`;
dir = artifactDir(cwd, id);
}
}
await mkdir(join(dir, "source"), { recursive: true });
await mkdir(join(dir, "dist"), { recursive: true });
await mkdir(join(dir, "deploy"), { recursive: true });
const theme = await readTextAsset("design/artifact-theme.css");
const template = await readTextAsset("design/artifact-template.html");
const design = await resolveDesignSystem(cwd);
const designNote = design
? `Project/user design system active: ${escapeHtml(design.displayPath)}. Follow its Markdown guidance for visual choices while preserving self-contained artifact constraints.`
: "This artifact uses Pi Artifact UI, grounded in Vercel Geist.";
const designComment = design ? `\n\n` : "";
const content = ` \n `;
const html = template
.replace("{{title}}", escapeHtml(params.title))
.replace("{{artifactThemeCss}}", indent(theme, 4))
.replace("{{artifactSpecificCss}}", "")
.replace("{{artifactContent}}", designComment + content)
.replace("{{artifactScript}}", "");
const sourcePath = join(dir, "source", "index.html");
if (!existsSync(sourcePath)) await writeFile(sourcePath, html, "utf8");
await buildDist(cwd, id);
const now = new Date().toISOString();
const metaPath = join(dir, "artifact.json");
if (!existsSync(metaPath)) {
const meta: ArtifactMetadata = {
id,
title: params.title,
kind: params.kind,
createdAt: now,
updatedAt: now,
designSystem: design ? { scope: design.scope, path: design.displayPath, fingerprint: design.fingerprint } : { scope: "built-in" },
entry: "dist/index.html",
versions: [{ version: 1, createdAt: now, path: "dist/index.html" }],
};
await writeMetadata(cwd, meta);
}
return { id, dir, sourcePath, distPath: join(dir, "dist", "index.html") };
}
export async function buildDist(cwd: string, id: string, noindex = true) {
const dir = artifactDir(cwd, id);
await mkdir(join(dir, "dist"), { recursive: true });
const source = join(dir, "source", "index.html");
if (existsSync(source)) {
await cp(source, join(dir, "dist", "index.html"));
}
const headers = [
"/*",
noindex ? " X-Robots-Tag: noindex" : undefined,
" Referrer-Policy: no-referrer",
" X-Content-Type-Options: nosniff",
].filter(Boolean).join("\n") + "\n";
await writeFile(join(dir, "dist", "_headers"), headers, "utf8");
try {
const meta = await readMetadata(cwd, id);
await writeFile(join(dir, "dist", "thumbnail.svg"), renderThumbnail(meta.title), "utf8");
} catch {}
}
export async function importArtifactFromUrl(cwd: string, url: string, title?: string, id?: string) {
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`);
const html = await response.text();
const inferredTitle = title || html.match(/
]*>([^<]+)<\/title>/i)?.[1]?.trim() || new URL(url).hostname;
const created = await ensureArtifactScaffold(cwd, { title: inferredTitle, id });
await writeFile(join(artifactDir(cwd, created.id), "source", "index.html"), html, "utf8");
await buildDist(cwd, created.id);
const meta = await readMetadata(cwd, created.id);
meta.latestPublishedUrl = url;
await writeMetadata(cwd, meta);
return created;
}
function renderThumbnail(title: string) {
const safe = escapeHtml(title);
return ``;
}
export async function readMetadata(cwd: string, id: string): Promise {
return JSON.parse(await readFile(join(artifactDir(cwd, id), "artifact.json"), "utf8"));
}
export async function writeMetadata(cwd: string, meta: ArtifactMetadata) {
meta.updatedAt = new Date().toISOString();
await writeFile(join(artifactDir(cwd, meta.id), "artifact.json"), JSON.stringify(meta, null, 2) + "\n", "utf8");
}
export async function listArtifacts(cwd: string): Promise {
const root = artifactsRoot(cwd);
if (!existsSync(root)) return [];
const names = await readdir(root);
const metas: ArtifactMetadata[] = [];
for (const name of names) {
try {
const p = join(root, name, "artifact.json");
if (existsSync(p)) metas.push(JSON.parse(await readFile(p, "utf8")));
} catch {}
}
return metas.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
}
export async function walkFiles(dir: string): Promise> {
const out: Array<{ path: string; rel: string; bytes: number }> = [];
async function walk(current: string) {
for (const entry of await readdir(current, { withFileTypes: true })) {
const p = join(current, entry.name);
if (entry.isDirectory()) await walk(p);
else if (entry.isFile()) {
const s = await stat(p);
out.push({ path: p, rel: relative(dir, p), bytes: s.size });
}
}
}
if (existsSync(dir)) await walk(dir);
return out;
}
const mime: Record = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
};
let previewServer: Server | undefined;
let previewPort: number | undefined;
export async function ensurePreviewServer(cwd: string): Promise {
if (previewServer && previewPort) return previewPort;
previewServer = createServer((req, res) => {
const url = new URL(req.url || "/", "http://localhost");
const prefix = "/artifacts/";
if (!url.pathname.startsWith(prefix)) {
res.writeHead(404); res.end("Not found"); return;
}
const parts = url.pathname.slice(prefix.length).split("/").filter(Boolean);
const id = parts.shift();
if (!id) { res.writeHead(404); res.end("Missing artifact id"); return; }
const rel = parts.length ? parts.join("/") : "index.html";
const safeRel = rel.replace(/\.\.+/g, "");
const p = join(artifactDir(cwd, id), "dist", safeRel);
if (!resolve(p).startsWith(resolve(artifactDir(cwd, id), "dist")) || !existsSync(p)) {
res.writeHead(404); res.end("Not found"); return;
}
res.writeHead(200, { "content-type": mime[extname(p)] || "application/octet-stream" });
createReadStream(p).pipe(res);
});
await new Promise((resolve, reject) => {
previewServer!.once("error", reject);
previewServer!.listen(0, "127.0.0.1", () => resolve());
});
const addr = previewServer.address();
previewPort = typeof addr === "object" && addr ? addr.port : 0;
return previewPort;
}
export function previewUrl(port: number, id: string) {
return `http://127.0.0.1:${port}/artifacts/${encodeURIComponent(id)}/`;
}
export function escapeHtml(s: string) {
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]!));
}
function indent(s: string, spaces: number) {
const pad = " ".repeat(spaces);
return s.split("\n").map((line) => line ? pad + line : line).join("\n");
}