Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions website/src/scripts/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
copyToClipboard,
showToast,
downloadFile,
downloadZipBundle,
shareFile,
getResourceType,
escapeHtml,
Expand Down Expand Up @@ -46,6 +47,7 @@ interface SkillFile {
}

interface SkillItem extends ResourceItem {
id: string;
skillFile: string;
files: SkillFile[];
}
Expand All @@ -56,6 +58,10 @@ interface SkillsData {

let skillsCache: SkillsData | null | undefined;

function getSkillDownloadName(skill: SkillItem): string {
return skill.id || skill.path.split("/").pop() || "skill";
}

const RESOURCE_TYPE_TO_JSON: Record<string, string> = {
agent: "agents.json",
instruction: "instructions.json",
Expand Down Expand Up @@ -524,6 +530,24 @@ export function setupModal(): void {

downloadBtn?.addEventListener("click", async () => {
if (currentFilePath) {
if (currentFileType === "skill") {
const skill = await getSkillItemByFilePath(currentFilePath);
if (!skill || skill.files.length === 0) {
showToast("No files found for this skill.", "error");
return;
}

try {
await downloadZipBundle(getSkillDownloadName(skill), skill.files);
showToast("Download started!", "success");
} catch (error) {
const message =
error instanceof Error ? error.message : "Download failed";
showToast(message, "error");
}
return;
}

const success = await downloadFile(currentFilePath);
showToast(
success ? "Download started!" : "Download failed",
Expand Down Expand Up @@ -868,6 +892,12 @@ export async function openFileModal(
// Show copy/download buttons for regular files
if (copyBtn) copyBtn.style.display = "inline-flex";
if (downloadBtn) downloadBtn.style.display = "inline-flex";
if (downloadBtn) {
downloadBtn.setAttribute(
"aria-label",
type === "skill" ? "Download skill as ZIP" : "Download file"
);
}
renderPlainText("Loading...");
hideSkillFileSwitcher();
updateViewButtons();
Expand Down
40 changes: 2 additions & 38 deletions website/src/scripts/pages/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import { FuzzySearch, type SearchItem } from "../search";
import {
fetchData,
debounce,
getRawGitHubUrl,
showToast,
loadJSZip,
downloadZipBundle,
} from "../utils";
import { setupModal, openFileModal } from "../modal";
import {
Expand Down Expand Up @@ -157,42 +156,7 @@ async function downloadHook(
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';

try {
const JSZip = await loadJSZip();
const zip = new JSZip();
const folder = zip.folder(hook.id);

const fetchPromises = files.map(async (file) => {
const url = getRawGitHubUrl(file.path);
try {
const response = await fetch(url);
if (!response.ok) return null;
const content = await response.text();
return { name: file.name, content };
} catch {
return null;
}
});

const results = await Promise.all(fetchPromises);
let addedFiles = 0;
for (const result of results) {
if (result && folder) {
folder.file(result.name, result.content);
addedFiles++;
}
}

if (addedFiles === 0) throw new Error("Failed to fetch any files");

const blob = await zip.generateAsync({ type: "blob" });
const downloadUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = `${hook.id}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(downloadUrl);
await downloadZipBundle(hook.id, files);

btn.innerHTML =
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
Expand Down
40 changes: 2 additions & 38 deletions website/src/scripts/pages/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import { FuzzySearch, type SearchItem } from "../search";
import {
fetchData,
debounce,
getRawGitHubUrl,
showToast,
loadJSZip,
downloadZipBundle,
} from "../utils";
import { setupModal, openFileModal } from "../modal";
import {
Expand Down Expand Up @@ -137,42 +136,7 @@ async function downloadSkill(
'<svg class="spinner" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 0a8 8 0 1 0 8 8h-1.5A6.5 6.5 0 1 1 8 1.5V0z"/></svg> Preparing...';

try {
const JSZip = await loadJSZip();
const zip = new JSZip();
const folder = zip.folder(skill.id);

const fetchPromises = skill.files.map(async (file) => {
const url = getRawGitHubUrl(file.path);
try {
const response = await fetch(url);
if (!response.ok) return null;
const content = await response.text();
return { name: file.name, content };
} catch {
return null;
}
});

const results = await Promise.all(fetchPromises);
let addedFiles = 0;
for (const result of results) {
if (result && folder) {
folder.file(result.name, result.content);
addedFiles++;
}
}

if (addedFiles === 0) throw new Error("Failed to fetch any files");

const blob = await zip.generateAsync({ type: "blob" });
const downloadUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = `${skill.id}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(downloadUrl);
await downloadZipBundle(skill.id, skill.files);

btn.innerHTML =
'<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z"/></svg> Downloaded!';
Expand Down
70 changes: 61 additions & 9 deletions website/src/scripts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,66 @@ export async function loadJSZip() {
return JSZip;
}

export interface ZipDownloadFile {
name: string;
path: string;
}

function triggerBlobDownload(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}

export async function downloadZipBundle(
bundleName: string,
files: ZipDownloadFile[]
): Promise<void> {
if (files.length === 0) {
throw new Error("No files found for this download.");
}

const JSZip = await loadJSZip();
const zip = new JSZip();
const folder = zip.folder(bundleName);

const fetchPromises = files.map(async (file) => {
try {
const response = await fetch(getRawGitHubUrl(file.path));
if (!response.ok) return null;

return {
name: file.name,
content: await response.text(),
};
} catch {
return null;
}
});

const results = await Promise.all(fetchPromises);
let addedFiles = 0;

for (const result of results) {
if (result && folder) {
folder.file(result.name, result.content);
addedFiles++;
}
}

if (addedFiles === 0) {
throw new Error("Failed to fetch any files");
}

const blob = await zip.generateAsync({ type: "blob" });
triggerBlobDownload(blob, `${bundleName}.zip`);
}

/**
* Fetch raw file content from GitHub
*/
Expand Down Expand Up @@ -156,15 +216,7 @@ export async function downloadFile(filePath: string): Promise<boolean> {
const filename = filePath.split("/").pop() || "file.md";

const blob = new Blob([content], { type: "text/markdown" });
const url = URL.createObjectURL(blob);

const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
triggerBlobDownload(blob, filename);

return true;
} catch (error) {
Expand Down
Loading