api-handlers.js

  1import { readdir, readFile } from "fs/promises";
  2import { basename, join, dirname } from "path";
  3import { existsSync } from "fs";
  4import { fileURLToPath } from "url";
  5import { readPatterns, parseFrontmatter } from "../../scripts/lib/utils.js";
  6import { FILE_DOWNLOAD_PROVIDER_CONFIG_DIRS } from "../../lib/download-providers.js";
  7import {
  8	isAllowedBundleProvider,
  9	isAllowedFileProvider,
 10	isAllowedType,
 11	isValidId,
 12	sanitizeFilename
 13} from "./validation.js";
 14
 15// Get project root directory (works in both Node.js and Bun, including Vercel)
 16const __filename = fileURLToPath(import.meta.url);
 17const __dirname = dirname(__filename);
 18const PROJECT_ROOT = join(__dirname, "..", "..");
 19
 20// Helper to read file content (works in both Node.js and Bun)
 21async function readFileContent(filePath) {
 22	return readFile(filePath, "utf-8");
 23}
 24
 25// Read all skills from source/skills/ subdirectories
 26export async function getSkills() {
 27	const skillsDir = join(PROJECT_ROOT, "source", "skills");
 28	const entries = await readdir(skillsDir, { withFileTypes: true });
 29	const skills = [];
 30
 31	for (const entry of entries) {
 32		if (!entry.isDirectory()) continue;
 33		const skillMdPath = join(skillsDir, entry.name, "SKILL.md");
 34		if (!existsSync(skillMdPath)) continue;
 35
 36		const content = await readFileContent(skillMdPath);
 37		const { frontmatter } = parseFrontmatter(content);
 38
 39		skills.push({
 40			id: entry.name,
 41			name: frontmatter.name || entry.name,
 42			description: frontmatter.description || "No description available",
 43			userInvocable: frontmatter['user-invocable'] === true || frontmatter['user-invocable'] === 'true',
 44		});
 45	}
 46
 47	return skills;
 48}
 49
 50// Read commands (user-invocable skills)
 51export async function getCommands() {
 52	const allSkills = await getSkills();
 53	return allSkills.filter(s => s.userInvocable);
 54}
 55
 56// Get command/skill source content
 57export async function getCommandSource(id) {
 58	if (!isValidId(id)) {
 59		return { error: "Invalid command ID", status: 400 };
 60	}
 61
 62	const skillPath = join(PROJECT_ROOT, "source", "skills", id, "SKILL.md");
 63
 64	try {
 65		if (!existsSync(skillPath)) {
 66			return null;
 67		}
 68		const content = await readFileContent(skillPath);
 69		return content;
 70	} catch (error) {
 71		console.error("Error reading skill source:", error);
 72		return null;
 73	}
 74}
 75
 76// Get the appropriate file path for a provider
 77export function getFilePath(type, provider, id) {
 78	const distDir = join(PROJECT_ROOT, "dist");
 79	const configDir = FILE_DOWNLOAD_PROVIDER_CONFIG_DIRS[provider];
 80	if (!configDir) return null;
 81
 82	// Everything is a skill now
 83	if (type === "skill" || type === "command") {
 84		return join(distDir, provider, configDir, "skills", id, "SKILL.md");
 85	}
 86
 87	return null;
 88}
 89
 90// Handle individual file download
 91export async function handleFileDownload(type, provider, id) {
 92	if (!isAllowedType(type)) {
 93		return new Response("Invalid type", { status: 400 });
 94	}
 95
 96	if (!isAllowedFileProvider(provider)) {
 97		return new Response("Invalid provider", { status: 400 });
 98	}
 99
100	if (!isValidId(id)) {
101		return new Response("Invalid file ID", { status: 400 });
102	}
103
104	const filePath = getFilePath(type, provider, id);
105
106	if (!filePath) {
107		return new Response("Invalid provider", { status: 400 });
108	}
109
110	try {
111		if (!existsSync(filePath)) {
112			return new Response("File not found", { status: 404 });
113		}
114
115		const content = await readFile(filePath);
116		const fileName = sanitizeFilename(basename(filePath));
117		return new Response(content, {
118			headers: {
119				"Content-Type": "application/octet-stream",
120				"Content-Disposition": `attachment; filename="${fileName}"`,
121			},
122		});
123	} catch (error) {
124		console.error("Error downloading file:", error);
125		return new Response("Error downloading file", { status: 500 });
126	}
127}
128
129// Extract patterns from SKILL.md using the shared utility
130export async function getPatterns() {
131	try {
132		return readPatterns(PROJECT_ROOT);
133	} catch (error) {
134		console.error("Error reading patterns:", error);
135		return { patterns: [], antipatterns: [] };
136	}
137}
138
139// Handle bundle download
140export async function handleBundleDownload(provider) {
141	if (!isAllowedBundleProvider(provider)) {
142		return new Response("Invalid provider", { status: 400 });
143	}
144
145	const distDir = join(PROJECT_ROOT, "dist");
146	const zipPath = join(distDir, `${provider}.zip`);
147
148	try {
149		if (!existsSync(zipPath)) {
150			return new Response("Bundle not found", { status: 404 });
151		}
152
153		const content = await readFile(zipPath);
154		const safeProvider = sanitizeFilename(provider);
155		return new Response(content, {
156			headers: {
157				"Content-Type": "application/zip",
158				"Content-Disposition": `attachment; filename="impeccable-style-${safeProvider}.zip"`,
159			},
160		});
161	} catch (error) {
162		console.error("Error downloading bundle:", error);
163		return new Response("Error downloading bundle", { status: 500 });
164	}
165}