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}