Add author auth, forking, tags, and stats tracking
Introduce token-based author authentication (register/verify API), skill forking with EditGate protection, tag metadata on skills, and download/push stats. Enhanced push scripts with token auth and per-skill filtering. Updated UI with stats, tags, and author info on skill cards.
This commit is contained in:
2
dist/server/pages/_image.astro.mjs
vendored
2
dist/server/pages/_image.astro.mjs
vendored
@@ -1,2 +1,2 @@
|
||||
export { a as page } from '../chunks/node_WXNYuHqd.mjs';
|
||||
export { a as page } from '../chunks/node_HH9e2ntY.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
252
dist/server/pages/_slug_.astro.mjs
vendored
252
dist/server/pages/_slug_.astro.mjs
vendored
@@ -1,45 +1,240 @@
|
||||
import { e as createComponent, k as renderComponent, l as renderScript, r as renderTemplate, h as createAstro, m as maybeRenderHead, g as addAttribute, u as unescapeHTML } from '../chunks/astro/server_B-2LxKLH.mjs';
|
||||
import { e as createAstro, f as createComponent, k as renderComponent, l as renderScript, r as renderTemplate, m as maybeRenderHead, h as addAttribute, u as unescapeHTML } from '../chunks/astro/server_CF97kUu8.mjs';
|
||||
import 'piccolore';
|
||||
import { _ as _export_sfc, $ as $$Base } from '../chunks/_plugin-vue_export-helper_B1lnwsE2.mjs';
|
||||
import { useSSRContext, defineComponent, ref, mergeProps } from 'vue';
|
||||
import { ssrRenderAttrs, ssrInterpolate } from 'vue/server-renderer';
|
||||
import { g as getSkill } from '../chunks/skills_COWfD5oy.mjs';
|
||||
import { _ as _export_sfc, $ as $$Base } from '../chunks/_plugin-vue_export-helper_CEgY73aA.mjs';
|
||||
import { useSSRContext, defineComponent, ref, nextTick, mergeProps } from 'vue';
|
||||
import { ssrRenderTeleport, ssrInterpolate, ssrRenderAttr, ssrIncludeBooleanAttr, ssrRenderAttrs } from 'vue/server-renderer';
|
||||
import { g as getSkill, b as getForksOf } from '../chunks/skills_BacVQUiS.mjs';
|
||||
import { h as hasToken } from '../chunks/tokens_CAzj9Aj8.mjs';
|
||||
import { a as recordDownload, g as getStatsForSlug } from '../chunks/stats_CaDi9y9J.mjs';
|
||||
import { marked } from 'marked';
|
||||
/* empty css */
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
||||
__name: "EditGate",
|
||||
props: {
|
||||
slug: {},
|
||||
authorEmail: {},
|
||||
authorName: {},
|
||||
authorHasToken: { type: Boolean }
|
||||
},
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
const props = __props;
|
||||
const showModal = ref(false);
|
||||
const token = ref("");
|
||||
const error = ref("");
|
||||
const verifying = ref(false);
|
||||
const tokenInput = ref();
|
||||
async function handleClick() {
|
||||
if (!props.authorEmail || !props.authorHasToken) {
|
||||
window.location.href = `/${props.slug}/edit`;
|
||||
return;
|
||||
}
|
||||
const saved = localStorage.getItem("skillshere-token") || "";
|
||||
if (saved) {
|
||||
try {
|
||||
const res = await fetch("/api/auth/verify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: props.authorEmail, token: saved })
|
||||
});
|
||||
if (res.ok) {
|
||||
localStorage.setItem("skillshere-token", saved);
|
||||
window.location.href = `/${props.slug}/edit`;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
showModal.value = true;
|
||||
error.value = "";
|
||||
token.value = "";
|
||||
nextTick(() => tokenInput.value?.focus());
|
||||
}
|
||||
async function verify() {
|
||||
verifying.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const res = await fetch("/api/auth/verify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: props.authorEmail, token: token.value })
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
error.value = data.error || "Invalid token";
|
||||
return;
|
||||
}
|
||||
localStorage.setItem("skillshere-token", token.value);
|
||||
window.location.href = `/${props.slug}/edit`;
|
||||
} catch {
|
||||
error.value = "Could not verify token";
|
||||
} finally {
|
||||
verifying.value = false;
|
||||
}
|
||||
}
|
||||
function forkSkill() {
|
||||
showModal.value = false;
|
||||
window.location.href = `/new?from=${encodeURIComponent(props.slug)}`;
|
||||
}
|
||||
const __returned__ = { props, showModal, token, error, verifying, tokenInput, handleClick, verify, forkSkill };
|
||||
Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
|
||||
return __returned__;
|
||||
}
|
||||
});
|
||||
function _sfc_ssrRender$1(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
|
||||
_push(`<!--[--><button class="inline-flex items-center gap-1.5 rounded-lg border border-white/[0.08] bg-surface-200 px-3.5 py-2 text-sm font-medium text-gray-300 hover:border-white/[0.15] hover:text-white transition-all"><svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"></path></svg> Edit </button>`);
|
||||
ssrRenderTeleport(_push, (_push2) => {
|
||||
if ($setup.showModal) {
|
||||
_push2(`<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"><div class="w-full max-w-md rounded-2xl border border-white/[0.08] bg-[var(--color-surface-200)] p-6 shadow-2xl"><h3 class="text-lg font-semibold text-white mb-1">Author Verification</h3><p class="text-sm text-gray-500 mb-4"> This skill is owned by <strong class="text-gray-300">${ssrInterpolate($props.authorName || $props.authorEmail)}</strong>. Enter your token to edit. </p><form><input${ssrRenderAttr("value", $setup.token)} type="password" placeholder="Paste your author token..." class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white placeholder-gray-600 font-mono focus:border-[var(--color-accent-500)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--color-accent-500)]/20 transition-all">`);
|
||||
if ($setup.error) {
|
||||
_push2(`<p class="mt-2 text-sm text-red-400">${ssrInterpolate($setup.error)}</p>`);
|
||||
} else {
|
||||
_push2(`<!---->`);
|
||||
}
|
||||
_push2(`<div class="mt-4 flex items-center gap-3"><button type="submit"${ssrIncludeBooleanAttr($setup.verifying || !$setup.token) ? " disabled" : ""} class="inline-flex items-center gap-2 rounded-xl bg-[var(--color-accent-500)] px-5 py-2 text-sm font-semibold text-white shadow-lg shadow-[var(--color-accent-500)]/20 hover:bg-[var(--color-accent-600)] disabled:opacity-50 active:scale-[0.97] transition-all">`);
|
||||
if ($setup.verifying) {
|
||||
_push2(`<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>`);
|
||||
} else {
|
||||
_push2(`<!---->`);
|
||||
}
|
||||
_push2(` ${ssrInterpolate($setup.verifying ? "Verifying..." : "Continue to Edit")}</button><button type="button" class="text-sm text-[var(--color-accent-400)] hover:text-[var(--color-accent-300)] transition-colors"> Fork instead </button><button type="button" class="ml-auto text-sm text-gray-600 hover:text-gray-300 transition-colors"> Cancel </button></div></form></div></div>`);
|
||||
} else {
|
||||
_push2(`<!---->`);
|
||||
}
|
||||
}, "body", false, _parent);
|
||||
_push(`<!--]-->`);
|
||||
}
|
||||
const _sfc_setup$1 = _sfc_main$1.setup;
|
||||
_sfc_main$1.setup = (props, ctx) => {
|
||||
const ssrContext = useSSRContext();
|
||||
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/components/EditGate.vue");
|
||||
return _sfc_setup$1 ? _sfc_setup$1(props, ctx) : void 0;
|
||||
};
|
||||
const EditGate = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["ssrRender", _sfc_ssrRender$1]]);
|
||||
|
||||
const _sfc_main = /* @__PURE__ */ defineComponent({
|
||||
__name: "DeleteButton",
|
||||
props: {
|
||||
slug: {}
|
||||
slug: {},
|
||||
authorEmail: {},
|
||||
authorName: {},
|
||||
authorHasToken: { type: Boolean }
|
||||
},
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
const props = __props;
|
||||
const deleting = ref(false);
|
||||
async function handleDelete() {
|
||||
if (!confirm(`Delete "${props.slug}"? This cannot be undone.`)) return;
|
||||
const showModal = ref(false);
|
||||
const token = ref("");
|
||||
const error = ref("");
|
||||
const tokenInput = ref();
|
||||
async function handleClick() {
|
||||
if (props.authorEmail && props.authorHasToken) {
|
||||
const saved = localStorage.getItem("skillshere-token") || "";
|
||||
if (saved) {
|
||||
try {
|
||||
const res = await fetch("/api/auth/verify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: props.authorEmail, token: saved })
|
||||
});
|
||||
if (res.ok) {
|
||||
doDelete(saved);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
showModal.value = true;
|
||||
error.value = "";
|
||||
token.value = "";
|
||||
nextTick(() => tokenInput.value?.focus());
|
||||
} else {
|
||||
doDelete("");
|
||||
}
|
||||
}
|
||||
async function verifyAndDelete() {
|
||||
error.value = "";
|
||||
deleting.value = true;
|
||||
try {
|
||||
const res = await fetch(`/api/skills/${props.slug}`, { method: "DELETE" });
|
||||
const verifyRes = await fetch("/api/auth/verify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: props.authorEmail, token: token.value })
|
||||
});
|
||||
if (!verifyRes.ok) {
|
||||
const data = await verifyRes.json();
|
||||
error.value = data.error || "Invalid token";
|
||||
deleting.value = false;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
error.value = "Could not verify token";
|
||||
deleting.value = false;
|
||||
return;
|
||||
}
|
||||
localStorage.setItem("skillshere-token", token.value);
|
||||
doDelete(token.value);
|
||||
}
|
||||
async function doDelete(authToken) {
|
||||
if (!confirm(`Delete "${props.slug}"? This cannot be undone.`)) {
|
||||
deleting.value = false;
|
||||
return;
|
||||
}
|
||||
deleting.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const headers = {};
|
||||
if (authToken) {
|
||||
headers["Authorization"] = `Bearer ${authToken}`;
|
||||
}
|
||||
const res = await fetch(`/api/skills/${props.slug}`, { method: "DELETE", headers });
|
||||
if (res.status === 403) {
|
||||
const data = await res.json();
|
||||
error.value = data.error || "Permission denied";
|
||||
showModal.value = true;
|
||||
deleting.value = false;
|
||||
return;
|
||||
}
|
||||
if (!res.ok && res.status !== 204) {
|
||||
throw new Error("Failed to delete");
|
||||
const data = await res.json().catch(() => ({ error: "Failed to delete" }));
|
||||
throw new Error(data.error || "Failed to delete");
|
||||
}
|
||||
window.location.href = "/";
|
||||
} catch {
|
||||
alert("Failed to delete skill.");
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "Failed to delete skill.";
|
||||
deleting.value = false;
|
||||
}
|
||||
}
|
||||
const __returned__ = { props, deleting, handleDelete };
|
||||
const __returned__ = { props, deleting, showModal, token, error, tokenInput, handleClick, verifyAndDelete, doDelete };
|
||||
Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
|
||||
return __returned__;
|
||||
}
|
||||
});
|
||||
function _sfc_ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
|
||||
_push(`<button${ssrRenderAttrs(mergeProps({
|
||||
disabled: $setup.deleting,
|
||||
class: "inline-flex items-center gap-1.5 rounded-lg border border-red-500/20 bg-red-500/5 px-3.5 py-2 text-sm font-medium text-red-400 hover:bg-red-500/10 hover:border-red-500/30 disabled:opacity-50 active:scale-[0.97] transition-all"
|
||||
}, _attrs))}><svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"></path></svg> ${ssrInterpolate($setup.deleting ? "Deleting..." : "Delete")}</button>`);
|
||||
_push(`<div${ssrRenderAttrs(mergeProps({ class: "inline-flex" }, _attrs))}><button${ssrIncludeBooleanAttr($setup.deleting) ? " disabled" : ""} class="inline-flex items-center gap-1.5 rounded-lg border border-red-500/20 bg-red-500/5 px-3.5 py-2 text-sm font-medium text-red-400 hover:bg-red-500/10 hover:border-red-500/30 disabled:opacity-50 active:scale-[0.97] transition-all"><svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"></path></svg> ${ssrInterpolate($setup.deleting ? "Deleting..." : "Delete")}</button>`);
|
||||
ssrRenderTeleport(_push, (_push2) => {
|
||||
if ($setup.showModal) {
|
||||
_push2(`<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"><div class="w-full max-w-md rounded-2xl border border-white/[0.08] bg-[var(--color-surface-200)] p-6 shadow-2xl"><h3 class="text-lg font-semibold text-red-400 mb-1">Delete Skill</h3><p class="text-sm text-gray-500 mb-4"> This skill is owned by <strong class="text-gray-300">${ssrInterpolate($props.authorName || $props.authorEmail)}</strong>. Enter your token to delete it. </p><form><input${ssrRenderAttr("value", $setup.token)} type="password" placeholder="Paste your author token..." class="w-full rounded-xl border border-white/[0.06] bg-[var(--color-surface-100)] px-4 py-2.5 text-sm text-white placeholder-gray-600 font-mono focus:border-red-500/50 focus:outline-none focus:ring-1 focus:ring-red-500/20 transition-all">`);
|
||||
if ($setup.error) {
|
||||
_push2(`<p class="mt-2 text-sm text-red-400">${ssrInterpolate($setup.error)}</p>`);
|
||||
} else {
|
||||
_push2(`<!---->`);
|
||||
}
|
||||
_push2(`<div class="mt-4 flex items-center gap-3"><button type="submit"${ssrIncludeBooleanAttr($setup.deleting || !$setup.token) ? " disabled" : ""} class="inline-flex items-center gap-2 rounded-xl bg-red-500 px-5 py-2 text-sm font-semibold text-white hover:bg-red-600 disabled:opacity-50 active:scale-[0.97] transition-all">`);
|
||||
if ($setup.deleting) {
|
||||
_push2(`<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>`);
|
||||
} else {
|
||||
_push2(`<!---->`);
|
||||
}
|
||||
_push2(` ${ssrInterpolate($setup.deleting ? "Deleting..." : "Delete Permanently")}</button><button type="button" class="ml-auto text-sm text-gray-600 hover:text-gray-300 transition-colors"> Cancel </button></div></form></div></div>`);
|
||||
} else {
|
||||
_push2(`<!---->`);
|
||||
}
|
||||
}, "body", false, _parent);
|
||||
_push(`</div>`);
|
||||
}
|
||||
const _sfc_setup = _sfc_main.setup;
|
||||
_sfc_main.setup = (props, ctx) => {
|
||||
@@ -49,7 +244,7 @@ _sfc_main.setup = (props, ctx) => {
|
||||
};
|
||||
const DeleteButton = /* @__PURE__ */ _export_sfc(_sfc_main, [["ssrRender", _sfc_ssrRender]]);
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$Astro = createAstro("https://skills.here.run.place");
|
||||
const $$slug = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$slug;
|
||||
@@ -60,19 +255,26 @@ const $$slug = createComponent(async ($$result, $$props, $$slots) => {
|
||||
}
|
||||
const accept = Astro2.request.headers.get("accept") || "";
|
||||
if (!accept.includes("text/html")) {
|
||||
recordDownload(slug);
|
||||
return new Response(skill.raw, {
|
||||
headers: { "Content-Type": "text/markdown; charset=utf-8" }
|
||||
});
|
||||
}
|
||||
const authorHasToken = skill["author-email"] ? await hasToken(skill["author-email"]) : false;
|
||||
const forks = await getForksOf(slug);
|
||||
const stats = await getStatsForSlug(slug);
|
||||
const html = await marked(skill.content);
|
||||
const installCmd = `curl -fsSL ${Astro2.url.origin}/${slug} -o .claude/skills/${slug}.md`;
|
||||
return renderTemplate`${renderComponent($$result, "Base", $$Base, { "title": `${skill.name} \u2014 Skillit` }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<div class="mb-8"> <a href="/" class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-300 transition-colors mb-4"> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"></path> </svg>
|
||||
const origin = Astro2.url.origin;
|
||||
const cmds = {
|
||||
unix: `curl -fsSL ${origin}/${slug}/i | bash`,
|
||||
unixGlobal: `curl -fsSL ${origin}/${slug}/gi | bash`,
|
||||
win: `irm ${origin}/${slug}/i | iex`,
|
||||
winGlobal: `irm ${origin}/${slug}/gi | iex`
|
||||
};
|
||||
return renderTemplate`${renderComponent($$result, "Base", $$Base, { "title": `${skill.name} \u2014 Skills Here`, "data-astro-cid-yvbahnfj": true }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<a href="/" class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-300 transition-colors mb-4" data-astro-cid-yvbahnfj> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" data-astro-cid-yvbahnfj> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" data-astro-cid-yvbahnfj></path> </svg>
|
||||
Back
|
||||
</a> <div class="flex items-start justify-between gap-4"> <div> <h1 class="text-2xl font-bold tracking-tight text-white">${skill.name}</h1> ${skill.description && renderTemplate`<p class="text-gray-500 mt-1.5 leading-relaxed">${skill.description}</p>`} </div> <div class="flex items-center gap-2 shrink-0"> <a${addAttribute(`/${slug}/edit`, "href")} class="inline-flex items-center gap-1.5 rounded-lg border border-white/[0.08] bg-surface-200 px-3.5 py-2 text-sm font-medium text-gray-300 hover:border-white/[0.15] hover:text-white transition-all"> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"></path> </svg>
|
||||
Edit
|
||||
</a> ${renderComponent($$result2, "DeleteButton", DeleteButton, { "slug": slug, "client:load": true, "client:component-hydration": "load", "client:component-path": "/Users/alex/projects/skillit/src/components/DeleteButton.vue", "client:component-export": "default" })} </div> </div> </div> ${skill["allowed-tools"].length > 0 && renderTemplate`<div class="flex flex-wrap gap-1.5 mb-8"> ${skill["allowed-tools"].map((tool) => renderTemplate`<span class="rounded-md bg-white/[0.04] border border-white/[0.06] px-2.5 py-1 text-xs font-medium text-gray-400"> ${tool} </span>`)} </div>`} <div class="rounded-2xl border border-white/[0.06] bg-surface-100 p-6 mb-8 max-w-2xl space-y-4"> <h2 class="text-sm font-semibold text-white">Install this skill</h2> <p class="text-xs text-gray-500 leading-relaxed">Run this in your project root. The skill file will be saved to <code class="text-gray-400 font-mono bg-white/[0.04] px-1 py-0.5 rounded">.claude/skills/${slug}.md</code> and Claude Code will load it automatically.</p> <div class="flex items-center gap-3 rounded-xl bg-surface-50 border border-white/[0.06] px-4 py-3"> <code class="flex-1 text-xs font-mono text-gray-500 select-all truncate">${installCmd}</code> <button id="copy-install" class="shrink-0 rounded-md bg-white/[0.06] border border-white/[0.06] px-2.5 py-1 text-xs font-medium text-gray-400 hover:text-white hover:bg-white/[0.1] transition-all">
|
||||
Copy
|
||||
</button> </div> ${skill["allowed-tools"].length > 0 && renderTemplate`<p class="text-xs text-gray-600 leading-relaxed">This skill uses: ${skill["allowed-tools"].join(", ")}. Claude will have access to these tools when this skill is active.</p>`} </div> <article class="skill-prose rounded-2xl border border-white/[0.06] bg-surface-100 p-8">${unescapeHTML(html)}</article> ` })} ${renderScript($$result, "/Users/alex/projects/skillit/src/pages/[slug].astro?astro&type=script&index=0&lang.ts")}`;
|
||||
</a> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 2rem;" data-astro-cid-yvbahnfj> <div class="rounded-2xl border border-white/[0.06] bg-surface-100 p-6" style="min-width: 0;" data-astro-cid-yvbahnfj> <h1 class="text-2xl font-bold tracking-tight text-white mb-1" data-astro-cid-yvbahnfj>${skill.name}</h1> ${skill.description && renderTemplate`<p class="text-gray-500 leading-relaxed mb-3" data-astro-cid-yvbahnfj>${skill.description}</p>`} ${skill["allowed-tools"].length > 0 && renderTemplate`<div class="flex flex-wrap gap-1.5" data-astro-cid-yvbahnfj> ${skill["allowed-tools"].map((tool) => renderTemplate`<span class="rounded-md bg-white/[0.04] border border-white/[0.06] px-2.5 py-1 text-xs font-medium text-gray-400" data-astro-cid-yvbahnfj> ${tool} </span>`)} </div>`} ${skill.tags.length > 0 && renderTemplate`<div class="flex flex-wrap gap-1.5 mt-3" data-astro-cid-yvbahnfj> ${skill.tags.map((tag) => renderTemplate`<span class="rounded-full bg-[var(--color-accent-500)]/10 px-2.5 py-0.5 text-xs font-medium text-[var(--color-accent-400)]" data-astro-cid-yvbahnfj> ${tag} </span>`)} </div>`} ${skill.author && renderTemplate`<p class="text-xs text-gray-600 mt-3" data-astro-cid-yvbahnfj>by ${skill.author}</p>`} ${skill["fork-of"] && renderTemplate`<p class="text-xs text-gray-600 mt-1" data-astro-cid-yvbahnfj>forked from <a${addAttribute(`/${skill["fork-of"]}`, "href")} class="text-[var(--color-accent-500)] hover:text-[var(--color-accent-400)] transition-colors" data-astro-cid-yvbahnfj>${skill["fork-of"]}</a></p>`} ${forks.length > 0 && renderTemplate`<details class="mt-3" data-astro-cid-yvbahnfj> <summary class="text-xs text-gray-500 cursor-pointer hover:text-gray-300 transition-colors select-none" data-astro-cid-yvbahnfj> ${forks.length} fork${forks.length !== 1 ? "s" : ""} </summary> <ul class="mt-1.5 space-y-1 pl-3 border-l border-white/[0.06]" data-astro-cid-yvbahnfj> ${forks.map((f) => renderTemplate`<li data-astro-cid-yvbahnfj> <a${addAttribute(`/${f.slug}`, "href")} class="text-xs text-[var(--color-accent-500)] hover:text-[var(--color-accent-400)] transition-colors" data-astro-cid-yvbahnfj> ${f.name} ${f.author && renderTemplate`<span class="text-gray-600" data-astro-cid-yvbahnfj> by ${f.author}</span>`} </a> </li>`)} </ul> </details>`} <div class="flex flex-wrap items-center gap-4 mt-4 pt-3 border-t border-white/[0.06]" data-astro-cid-yvbahnfj> <span class="inline-flex items-center gap-1.5 text-xs text-gray-500" data-astro-cid-yvbahnfj> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" data-astro-cid-yvbahnfj> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" data-astro-cid-yvbahnfj></path> </svg> ${stats.downloads} download${stats.downloads !== 1 ? "s" : ""} </span> <span class="inline-flex items-center gap-1.5 text-xs text-gray-500" data-astro-cid-yvbahnfj> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" data-astro-cid-yvbahnfj> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" data-astro-cid-yvbahnfj></path> </svg> ${stats.pushes} push${stats.pushes !== 1 ? "es" : ""} </span> ${stats.lastPushedAt && renderTemplate`<span class="text-xs text-gray-600" data-astro-cid-yvbahnfj>
|
||||
Last updated ${new Date(stats.lastPushedAt).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })} </span>`} </div> </div> <div class="rounded-2xl border border-white/[0.06] bg-surface-100 p-6 space-y-4" style="min-width: 0;" data-astro-cid-yvbahnfj> <div class="flex items-center justify-between" data-astro-cid-yvbahnfj> <h2 class="text-sm font-semibold text-white" data-astro-cid-yvbahnfj>Install this skill</h2> <div class="flex rounded-lg border border-white/[0.06] overflow-hidden" id="os-tabs" data-astro-cid-yvbahnfj> <button data-os="unix" class="os-tab px-2.5 py-1 text-[11px] font-medium transition-all" data-astro-cid-yvbahnfj>macOS / Linux</button> <button data-os="win" class="os-tab px-2.5 py-1 text-[11px] font-medium transition-all" data-astro-cid-yvbahnfj>Windows</button> </div> </div> <p class="text-xs text-gray-500 leading-relaxed" data-astro-cid-yvbahnfj>Run in your project root to add this skill.</p> <div class="flex items-center gap-3 rounded-xl bg-surface-50 border border-white/[0.06] px-4 py-3" data-astro-cid-yvbahnfj> <code data-cmd="unix" class="flex-1 text-xs font-mono text-gray-500 select-all truncate" data-astro-cid-yvbahnfj>${cmds.unix}</code> <code data-cmd="win" class="flex-1 text-xs font-mono text-gray-500 select-all truncate hidden" data-astro-cid-yvbahnfj>${cmds.win}</code> <button data-copy class="shrink-0 rounded-md bg-white/[0.06] border border-white/[0.06] px-2.5 py-1 text-xs font-medium text-gray-400 hover:text-white hover:bg-white/[0.1] transition-all" data-astro-cid-yvbahnfj>Copy</button> </div> <details class="group" data-astro-cid-yvbahnfj> <summary class="text-xs text-gray-600 cursor-pointer hover:text-gray-400 transition-colors" data-astro-cid-yvbahnfj>More options</summary> <div class="mt-3 space-y-3 text-xs text-gray-500" data-astro-cid-yvbahnfj> <div data-astro-cid-yvbahnfj> <p class="mb-1.5" data-astro-cid-yvbahnfj>Install globally (available in all projects):</p> <div class="flex items-center gap-3 rounded-lg bg-surface-50 border border-white/[0.06] px-3 py-2" data-astro-cid-yvbahnfj> <code data-cmd="unix" class="flex-1 font-mono text-gray-500 select-all truncate" data-astro-cid-yvbahnfj>${cmds.unixGlobal}</code> <code data-cmd="win" class="flex-1 font-mono text-gray-500 select-all truncate hidden" data-astro-cid-yvbahnfj>${cmds.winGlobal}</code> <button data-copy class="shrink-0 rounded bg-white/[0.06] border border-white/[0.06] px-2 py-0.5 font-medium text-gray-500 hover:text-white hover:bg-white/[0.1] transition-all" data-astro-cid-yvbahnfj>Copy</button> </div> </div> </div> </details> </div> </div> <div class="relative rounded-2xl border border-white/[0.06] bg-surface-100 p-8" data-astro-cid-yvbahnfj> <div class="absolute top-4 right-4 flex items-center gap-2" data-astro-cid-yvbahnfj> ${renderComponent($$result2, "EditGate", EditGate, { "slug": slug, "authorEmail": skill["author-email"], "authorName": skill.author, "authorHasToken": authorHasToken, "client:load": true, "client:component-hydration": "load", "client:component-path": "/Users/alex/projects/skillit/src/components/EditGate.vue", "client:component-export": "default", "data-astro-cid-yvbahnfj": true })} ${renderComponent($$result2, "DeleteButton", DeleteButton, { "slug": slug, "authorEmail": skill["author-email"], "authorName": skill.author, "authorHasToken": authorHasToken, "client:load": true, "client:component-hydration": "load", "client:component-path": "/Users/alex/projects/skillit/src/components/DeleteButton.vue", "client:component-export": "default", "data-astro-cid-yvbahnfj": true })} </div> <article class="skill-prose" data-astro-cid-yvbahnfj>${unescapeHTML(html)}</article> </div> ` })} ${renderScript($$result, "/Users/alex/projects/skillit/src/pages/[slug].astro?astro&type=script&index=0&lang.ts")}`;
|
||||
}, "/Users/alex/projects/skillit/src/pages/[slug].astro", void 0);
|
||||
|
||||
const $$file = "/Users/alex/projects/skillit/src/pages/[slug].astro";
|
||||
|
||||
15
dist/server/pages/_slug_/edit.astro.mjs
vendored
15
dist/server/pages/_slug_/edit.astro.mjs
vendored
@@ -1,11 +1,11 @@
|
||||
import { e as createComponent, k as renderComponent, r as renderTemplate, h as createAstro, m as maybeRenderHead, g as addAttribute } from '../../chunks/astro/server_B-2LxKLH.mjs';
|
||||
import { e as createAstro, f as createComponent, k as renderComponent, r as renderTemplate, m as maybeRenderHead, h as addAttribute } from '../../chunks/astro/server_CF97kUu8.mjs';
|
||||
import 'piccolore';
|
||||
import { $ as $$Base } from '../../chunks/_plugin-vue_export-helper_B1lnwsE2.mjs';
|
||||
import { g as getAvailableTools, a as getAvailableModels, S as SkillEditor } from '../../chunks/models_DPfuEi7q.mjs';
|
||||
import { g as getSkill } from '../../chunks/skills_COWfD5oy.mjs';
|
||||
import { $ as $$Base } from '../../chunks/_plugin-vue_export-helper_CEgY73aA.mjs';
|
||||
import { g as getAvailableTools, a as getAvailableModels, S as SkillEditor } from '../../chunks/models_BK7lP4G3.mjs';
|
||||
import { g as getSkill, a as getAllTags } from '../../chunks/skills_BacVQUiS.mjs';
|
||||
export { renderers } from '../../renderers.mjs';
|
||||
|
||||
const $$Astro = createAstro();
|
||||
const $$Astro = createAstro("https://skills.here.run.place");
|
||||
const $$Edit = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$Edit;
|
||||
@@ -18,8 +18,9 @@ const $$Edit = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const availableModels = await getAvailableModels();
|
||||
const allowedTools = skill["allowed-tools"].join(", ");
|
||||
const hooksJson = skill.hooks ? JSON.stringify(skill.hooks, null, 2) : "";
|
||||
return renderTemplate`${renderComponent($$result, "Base", $$Base, { "title": `Edit ${skill.name} \u2014 Skillit` }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<a${addAttribute(`/${slug}`, "href")} class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-300 transition-colors mb-4"> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"></path> </svg>
|
||||
Back to ${skill.name} </a> <h1 class="text-2xl font-bold tracking-tight text-white mb-2">Edit Skill</h1> <p class="text-sm text-gray-500 mb-8">Editing <strong class="text-gray-400">${skill.name}</strong>. Users who already installed this skill will get the updated version on their next sync.</p> ${renderComponent($$result2, "SkillEditor", SkillEditor, { "mode": "edit", "slug": slug, "initialName": skill.name, "initialDescription": skill.description, "initialAllowedTools": allowedTools, "initialArgumentHint": skill["argument-hint"], "initialModel": skill.model, "initialUserInvocable": skill["user-invocable"], "initialDisableModelInvocation": skill["disable-model-invocation"], "initialContext": skill.context, "initialAgent": skill.agent, "initialHooks": hooksJson, "initialBody": skill.content, ":availableTools": availableTools, ":availableModels": availableModels, "client:load": true, "client:component-hydration": "load", "client:component-path": "/Users/alex/projects/skillit/src/components/SkillEditor.vue", "client:component-export": "default" })} ` })}`;
|
||||
const availableTags = await getAllTags();
|
||||
return renderTemplate`${renderComponent($$result, "Base", $$Base, { "title": `Edit ${skill.name} \u2014 Skills Here` }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<a${addAttribute(`/${slug}`, "href")} class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-300 transition-colors mb-4"> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"></path> </svg>
|
||||
Back to ${skill.name} </a> <h1 class="text-2xl font-bold tracking-tight text-white mb-2">Edit Skill</h1> <p class="text-sm text-gray-500 mb-8">Editing <strong class="text-gray-400">${skill.name}</strong>. Users who already installed this skill will get the updated version on their next sync.</p> ${renderComponent($$result2, "SkillEditor", SkillEditor, { "mode": "edit", "slug": slug, "initialName": skill.name, "initialDescription": skill.description, "initialAllowedTools": allowedTools, "initialArgumentHint": skill["argument-hint"], "initialModel": skill.model, "initialUserInvocable": skill["user-invocable"], "initialDisableModelInvocation": skill["disable-model-invocation"], "initialContext": skill.context, "initialAgent": skill.agent, "initialHooks": hooksJson, "initialBody": skill.content, "initialAuthor": skill.author, "initialAuthorEmail": skill["author-email"], "initialTags": skill.tags.join(", "), ":availableTools": availableTools, ":availableModels": availableModels, "availableTags": availableTags.join(","), "client:load": true, "client:component-hydration": "load", "client:component-path": "/Users/alex/projects/skillit/src/components/SkillEditor.vue", "client:component-export": "default" })} ` })}`;
|
||||
}, "/Users/alex/projects/skillit/src/pages/[slug]/edit.astro", void 0);
|
||||
|
||||
const $$file = "/Users/alex/projects/skillit/src/pages/[slug]/edit.astro";
|
||||
|
||||
40
dist/server/pages/_slug_/gi.astro.mjs
vendored
Normal file
40
dist/server/pages/_slug_/gi.astro.mjs
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
import { g as getSkill } from '../../chunks/skills_BacVQUiS.mjs';
|
||||
import { i as isPowerShell } from '../../chunks/sync_BEq_wzpT.mjs';
|
||||
export { renderers } from '../../renderers.mjs';
|
||||
|
||||
const GET = async ({ params, url, request }) => {
|
||||
const { slug } = params;
|
||||
const skill = await getSkill(slug);
|
||||
if (!skill) {
|
||||
return new Response("Skill not found", { status: 404 });
|
||||
}
|
||||
const origin = url.origin;
|
||||
const ps = isPowerShell(request);
|
||||
const script = ps ? [
|
||||
'$ErrorActionPreference = "Stop"',
|
||||
'$Dir = Join-Path $HOME ".claude\\skills"',
|
||||
"New-Item -ItemType Directory -Force -Path $Dir | Out-Null",
|
||||
`Invoke-WebRequest -Uri "${origin}/${slug}" -OutFile (Join-Path $Dir "${slug}.md")`,
|
||||
`Write-Host "✓ Installed ${skill.name} globally to $Dir\\${slug}.md"`,
|
||||
""
|
||||
].join("\n") : [
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
"mkdir -p ~/.claude/skills",
|
||||
`curl -fsSL "${origin}/${slug}" -o ~/.claude/skills/${slug}.md`,
|
||||
`echo "✓ Installed ${skill.name} globally to ~/.claude/skills/${slug}.md"`,
|
||||
""
|
||||
].join("\n");
|
||||
return new Response(script, {
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
||||
});
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
GET
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
40
dist/server/pages/_slug_/i.astro.mjs
vendored
Normal file
40
dist/server/pages/_slug_/i.astro.mjs
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
import { g as getSkill } from '../../chunks/skills_BacVQUiS.mjs';
|
||||
import { i as isPowerShell } from '../../chunks/sync_BEq_wzpT.mjs';
|
||||
export { renderers } from '../../renderers.mjs';
|
||||
|
||||
const GET = async ({ params, url, request }) => {
|
||||
const { slug } = params;
|
||||
const skill = await getSkill(slug);
|
||||
if (!skill) {
|
||||
return new Response("Skill not found", { status: 404 });
|
||||
}
|
||||
const origin = url.origin;
|
||||
const ps = isPowerShell(request);
|
||||
const script = ps ? [
|
||||
'$ErrorActionPreference = "Stop"',
|
||||
'$Dir = ".claude\\skills"',
|
||||
"New-Item -ItemType Directory -Force -Path $Dir | Out-Null",
|
||||
`Invoke-WebRequest -Uri "${origin}/${slug}" -OutFile (Join-Path $Dir "${slug}.md")`,
|
||||
`Write-Host "✓ Installed ${skill.name} to $Dir\\${slug}.md"`,
|
||||
""
|
||||
].join("\n") : [
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
"mkdir -p .claude/skills",
|
||||
`curl -fsSL "${origin}/${slug}" -o ".claude/skills/${slug}.md"`,
|
||||
`echo "✓ Installed ${skill.name} to .claude/skills/${slug}.md"`,
|
||||
""
|
||||
].join("\n");
|
||||
return new Response(script, {
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
||||
});
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
GET
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
55
dist/server/pages/api/auth/register.astro.mjs
vendored
Normal file
55
dist/server/pages/api/auth/register.astro.mjs
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
import { h as hasToken, g as generateToken } from '../../../chunks/tokens_CAzj9Aj8.mjs';
|
||||
export { renderers } from '../../../renderers.mjs';
|
||||
|
||||
const POST = async ({ request }) => {
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: "Invalid JSON" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
const { email, name } = body;
|
||||
if (!email) {
|
||||
return new Response(JSON.stringify({ error: "email is required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
if (await hasToken(email)) {
|
||||
return new Response(JSON.stringify({ error: "Email already registered" }), {
|
||||
status: 409,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
try {
|
||||
const token = await generateToken(email, name || "");
|
||||
return new Response(JSON.stringify({ token }), {
|
||||
status: 201,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
if (message.includes("already registered")) {
|
||||
return new Response(JSON.stringify({ error: message }), {
|
||||
status: 409,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ error: message }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
POST
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
40
dist/server/pages/api/auth/verify.astro.mjs
vendored
Normal file
40
dist/server/pages/api/auth/verify.astro.mjs
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
import { v as verifyToken } from '../../../chunks/tokens_CAzj9Aj8.mjs';
|
||||
export { renderers } from '../../../renderers.mjs';
|
||||
|
||||
const POST = async ({ request }) => {
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: "Invalid JSON" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
const { email, token } = body;
|
||||
if (!email || !token) {
|
||||
return new Response(JSON.stringify({ error: "email and token are required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
const valid = await verifyToken(email, token);
|
||||
if (!valid) {
|
||||
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ ok: true }), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
POST
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
18
dist/server/pages/api/skills.astro.mjs
vendored
18
dist/server/pages/api/skills.astro.mjs
vendored
@@ -1,4 +1,7 @@
|
||||
import { l as listSkills, i as isValidSlug, c as createSkill } from '../../chunks/skills_COWfD5oy.mjs';
|
||||
import matter from 'gray-matter';
|
||||
import { l as listSkills, i as isValidSlug, c as createSkill } from '../../chunks/skills_BacVQUiS.mjs';
|
||||
import { h as hasToken, e as extractBearerToken, v as verifyToken } from '../../chunks/tokens_CAzj9Aj8.mjs';
|
||||
import { r as recordPush } from '../../chunks/stats_CaDi9y9J.mjs';
|
||||
export { renderers } from '../../renderers.mjs';
|
||||
|
||||
const GET = async () => {
|
||||
@@ -30,8 +33,21 @@ const POST = async ({ request }) => {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
const parsed = matter(content);
|
||||
const authorEmail = parsed.data["author-email"] || "";
|
||||
if (authorEmail && await hasToken(authorEmail)) {
|
||||
const token = extractBearerToken(request);
|
||||
const valid = await verifyToken(authorEmail, token);
|
||||
if (!valid) {
|
||||
return new Response(JSON.stringify({ error: "Valid token required to create a skill with author-email. Register first via POST /api/auth/register." }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
}
|
||||
try {
|
||||
const skill = await createSkill(slug, content);
|
||||
recordPush(slug);
|
||||
return new Response(JSON.stringify(skill), {
|
||||
status: 201,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
|
||||
54
dist/server/pages/api/skills/_slug_.astro.mjs
vendored
54
dist/server/pages/api/skills/_slug_.astro.mjs
vendored
@@ -1,4 +1,7 @@
|
||||
import { d as deleteSkill, g as getSkill, u as updateSkill } from '../../../chunks/skills_COWfD5oy.mjs';
|
||||
import 'gray-matter';
|
||||
import { g as getSkill, d as deleteSkill, u as updateSkill } from '../../../chunks/skills_BacVQUiS.mjs';
|
||||
import { h as hasToken, e as extractBearerToken, v as verifyToken } from '../../../chunks/tokens_CAzj9Aj8.mjs';
|
||||
import { r as recordPush } from '../../../chunks/stats_CaDi9y9J.mjs';
|
||||
export { renderers } from '../../../renderers.mjs';
|
||||
|
||||
const GET = async ({ params }) => {
|
||||
@@ -27,36 +30,59 @@ const PUT = async ({ params, request }) => {
|
||||
});
|
||||
}
|
||||
try {
|
||||
const existing = await getSkill(params.slug);
|
||||
if (!existing) {
|
||||
return new Response(JSON.stringify({ error: "Skill not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
if (existing["author-email"] && await hasToken(existing["author-email"])) {
|
||||
const token = extractBearerToken(request);
|
||||
const valid = await verifyToken(existing["author-email"], token);
|
||||
if (!valid) {
|
||||
return new Response(JSON.stringify({ error: `Only ${existing.author || existing["author-email"]} can update this skill. Provide a valid token via Authorization: Bearer header.` }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
}
|
||||
const skill = await updateSkill(params.slug, body.content);
|
||||
recordPush(params.slug);
|
||||
return new Response(JSON.stringify(skill), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
if (message.includes("not found")) {
|
||||
return new Response(JSON.stringify({ error: message }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ error: message }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
};
|
||||
const DELETE = async ({ params }) => {
|
||||
const DELETE = async ({ params, request }) => {
|
||||
try {
|
||||
await deleteSkill(params.slug);
|
||||
return new Response(null, { status: 204 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
if (message.includes("not found")) {
|
||||
return new Response(JSON.stringify({ error: message }), {
|
||||
const existing = await getSkill(params.slug);
|
||||
if (!existing) {
|
||||
return new Response(JSON.stringify({ error: "Skill not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
if (existing["author-email"] && await hasToken(existing["author-email"])) {
|
||||
const token = extractBearerToken(request);
|
||||
const valid = await verifyToken(existing["author-email"], token);
|
||||
if (!valid) {
|
||||
return new Response(JSON.stringify({ error: `Only ${existing.author || existing["author-email"]} can delete this skill. Provide a valid token via Authorization: Bearer header.` }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
}
|
||||
await deleteSkill(params.slug);
|
||||
return new Response(null, { status: 204 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
return new Response(JSON.stringify({ error: message }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
|
||||
2
dist/server/pages/api/sync.astro.mjs
vendored
2
dist/server/pages/api/sync.astro.mjs
vendored
@@ -1,4 +1,4 @@
|
||||
import { b as buildSyncScript } from '../../chunks/sync_B_Og9xl3.mjs';
|
||||
import { b as buildSyncScript } from '../../chunks/sync_BEq_wzpT.mjs';
|
||||
export { renderers } from '../../renderers.mjs';
|
||||
|
||||
const GET = async ({ url }) => {
|
||||
|
||||
2
dist/server/pages/api/sync/project.astro.mjs
vendored
2
dist/server/pages/api/sync/project.astro.mjs
vendored
@@ -1,4 +1,4 @@
|
||||
import { b as buildSyncScript } from '../../../chunks/sync_B_Og9xl3.mjs';
|
||||
import { b as buildSyncScript } from '../../../chunks/sync_BEq_wzpT.mjs';
|
||||
export { renderers } from '../../../renderers.mjs';
|
||||
|
||||
const GET = async ({ url }) => {
|
||||
|
||||
7
dist/server/pages/gi.astro.mjs
vendored
7
dist/server/pages/gi.astro.mjs
vendored
@@ -1,8 +1,9 @@
|
||||
import { b as buildSyncScript } from '../chunks/sync_B_Og9xl3.mjs';
|
||||
import { i as isPowerShell, a as buildSyncScriptPS, b as buildSyncScript } from '../chunks/sync_BEq_wzpT.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const GET = async ({ url }) => {
|
||||
const script = await buildSyncScript(url.origin, "$HOME/.claude/skills");
|
||||
const GET = async ({ url, request }) => {
|
||||
const ps = isPowerShell(request);
|
||||
const script = ps ? await buildSyncScriptPS(url.origin, "$HOME\\.claude\\skills") : await buildSyncScript(url.origin, "$HOME/.claude/skills");
|
||||
return new Response(script, {
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
||||
});
|
||||
|
||||
2
dist/server/pages/gp.astro.mjs
vendored
2
dist/server/pages/gp.astro.mjs
vendored
@@ -1,4 +1,4 @@
|
||||
import { a as buildPushScript } from '../chunks/sync_B_Og9xl3.mjs';
|
||||
import { c as buildPushScript } from '../chunks/sync_BEq_wzpT.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const GET = async ({ url }) => {
|
||||
|
||||
2
dist/server/pages/i.astro.mjs
vendored
2
dist/server/pages/i.astro.mjs
vendored
@@ -1,4 +1,4 @@
|
||||
import { b as buildSyncScript } from '../chunks/sync_B_Og9xl3.mjs';
|
||||
import { b as buildSyncScript } from '../chunks/sync_BEq_wzpT.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const GET = async ({ url }) => {
|
||||
|
||||
297
dist/server/pages/index.astro.mjs
vendored
297
dist/server/pages/index.astro.mjs
vendored
File diff suppressed because one or more lines are too long
22
dist/server/pages/new.astro.mjs
vendored
22
dist/server/pages/new.astro.mjs
vendored
@@ -1,15 +1,25 @@
|
||||
import { e as createComponent, k as renderComponent, r as renderTemplate, m as maybeRenderHead } from '../chunks/astro/server_B-2LxKLH.mjs';
|
||||
import { e as createAstro, f as createComponent, k as renderComponent, r as renderTemplate, m as maybeRenderHead, h as addAttribute } from '../chunks/astro/server_CF97kUu8.mjs';
|
||||
import 'piccolore';
|
||||
import { $ as $$Base } from '../chunks/_plugin-vue_export-helper_B1lnwsE2.mjs';
|
||||
import { g as getAvailableTools, a as getAvailableModels, S as SkillEditor } from '../chunks/models_DPfuEi7q.mjs';
|
||||
import { $ as $$Base } from '../chunks/_plugin-vue_export-helper_CEgY73aA.mjs';
|
||||
import { g as getAvailableTools, a as getAvailableModels, S as SkillEditor } from '../chunks/models_BK7lP4G3.mjs';
|
||||
import { a as getAllTags, g as getSkill } from '../chunks/skills_BacVQUiS.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const $$Astro = createAstro("https://skills.here.run.place");
|
||||
const $$New = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$New;
|
||||
const availableTools = await getAvailableTools();
|
||||
const availableModels = await getAvailableModels();
|
||||
return renderTemplate`${renderComponent($$result, "Base", $$Base, { "title": "New Skill \u2014 Skillit" }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<a href="/" class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-300 transition-colors mb-4"> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"></path> </svg>
|
||||
Back
|
||||
</a> <h1 class="text-2xl font-bold tracking-tight text-white mb-2">New Skill</h1> <p class="text-sm text-gray-500 mb-8 max-w-xl">Write a prompt in Markdown that tells Claude how to behave. The <strong class="text-gray-400">body</strong> is the instruction Claude receives. Use <strong class="text-gray-400">Allowed Tools</strong> to restrict which tools the skill can use.</p> ${renderComponent($$result2, "SkillEditor", SkillEditor, { "mode": "create", ":availableTools": availableTools, ":availableModels": availableModels, "client:load": true, "client:component-hydration": "load", "client:component-path": "/Users/alex/projects/skillit/src/components/SkillEditor.vue", "client:component-export": "default" })} ` })}`;
|
||||
const availableTags = await getAllTags();
|
||||
const fromSlug = Astro2.url.searchParams.get("from");
|
||||
let forkSource = null;
|
||||
if (fromSlug) {
|
||||
forkSource = await getSkill(fromSlug);
|
||||
}
|
||||
const isFork = Boolean(forkSource);
|
||||
const title = isFork ? `Fork ${forkSource.name} \u2014 Skills Here` : "New Skill \u2014 Skills Here";
|
||||
return renderTemplate`${renderComponent($$result, "Base", $$Base, { "title": title }, { "default": async ($$result2) => renderTemplate` ${maybeRenderHead()}<a${addAttribute(isFork ? `/${fromSlug}` : "/", "href")} class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-300 transition-colors mb-4"> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"></path> </svg> ${isFork ? `Back to ${forkSource.name}` : "Back"} </a> <h1 class="text-2xl font-bold tracking-tight text-white mb-2">${isFork ? "Fork Skill" : "New Skill"}</h1> ${isFork ? renderTemplate`<p class="text-sm text-gray-500 mb-8">Creating an independent copy of <strong class="text-gray-400">${forkSource.name}</strong>. Change the <strong class="text-gray-400">name</strong> to generate a new slug before saving.</p>` : renderTemplate`<p class="text-sm text-gray-500 mb-8 max-w-xl">Write a prompt in Markdown that tells Claude how to behave. The <strong class="text-gray-400">body</strong> is the instruction Claude receives. Use <strong class="text-gray-400">Allowed Tools</strong> to restrict which tools the skill can use.</p>`}${isFork ? renderTemplate`${renderComponent($$result2, "SkillEditor", SkillEditor, { "mode": "create", "forkOf": fromSlug, "initialName": forkSource.name, "initialDescription": forkSource.description, "initialAllowedTools": forkSource["allowed-tools"].join(", "), "initialArgumentHint": forkSource["argument-hint"], "initialModel": forkSource.model, "initialUserInvocable": forkSource["user-invocable"], "initialDisableModelInvocation": forkSource["disable-model-invocation"], "initialContext": forkSource.context, "initialAgent": forkSource.agent, "initialHooks": forkSource.hooks ? JSON.stringify(forkSource.hooks, null, 2) : "", "initialBody": forkSource.content, "initialTags": forkSource.tags.join(", "), ":availableTools": availableTools, ":availableModels": availableModels, "availableTags": availableTags.join(","), "client:load": true, "client:component-hydration": "load", "client:component-path": "/Users/alex/projects/skillit/src/components/SkillEditor.vue", "client:component-export": "default" })}` : renderTemplate`${renderComponent($$result2, "SkillEditor", SkillEditor, { "mode": "create", ":availableTools": availableTools, ":availableModels": availableModels, "availableTags": availableTags.join(","), "client:load": true, "client:component-hydration": "load", "client:component-path": "/Users/alex/projects/skillit/src/components/SkillEditor.vue", "client:component-export": "default" })}`}` })}`;
|
||||
}, "/Users/alex/projects/skillit/src/pages/new.astro", void 0);
|
||||
|
||||
const $$file = "/Users/alex/projects/skillit/src/pages/new.astro";
|
||||
|
||||
7
dist/server/pages/p.astro.mjs
vendored
7
dist/server/pages/p.astro.mjs
vendored
@@ -1,8 +1,9 @@
|
||||
import { a as buildPushScript } from '../chunks/sync_B_Og9xl3.mjs';
|
||||
import { i as isPowerShell, d as buildPushScriptPS, c as buildPushScript } from '../chunks/sync_BEq_wzpT.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const GET = async ({ url }) => {
|
||||
const script = await buildPushScript(url.origin, ".claude/skills");
|
||||
const GET = async ({ url, request }) => {
|
||||
const ps = isPowerShell(request);
|
||||
const script = ps ? await buildPushScriptPS(url.origin, ".claude\\skills") : await buildPushScript(url.origin, ".claude/skills");
|
||||
return new Response(script, {
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user