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:
Alejandro Martinez
2026-02-12 14:37:40 +01:00
parent 39d8afb251
commit aa477a553b
80 changed files with 3618 additions and 660 deletions

View File

@@ -1,7 +1,7 @@
import { q as decryptString, v as createSlotValueFromString, w as isAstroComponentFactory, k as renderComponent, r as renderTemplate, R as ROUTE_TYPE_HEADER, x as REROUTE_DIRECTIVE_HEADER, A as AstroError, y as i18nNoLocaleFoundInPath, z as ResponseSentError, B as ActionNotFoundError, C as MiddlewareNoDataOrNextCalled, D as MiddlewareNotAResponse, G as originPathnameSymbol, H as RewriteWithBodyUsed, J as GetStaticPathsRequired, K as InvalidGetStaticPathsReturn, O as InvalidGetStaticPathsEntry, P as GetStaticPathsExpectedParams, Q as GetStaticPathsInvalidRouteParam, S as PageNumberParamNotFound, T as DEFAULT_404_COMPONENT, V as NoMatchingStaticPathFound, W as PrerenderDynamicEndpointPathCollide, X as ReservedSlotName, Y as renderSlotToString, Z as renderJSX, _ as chunkToString, $ as isRenderInstruction, a0 as ForbiddenRewrite, a1 as SessionStorageInitError, a2 as SessionStorageSaveError, a3 as ASTRO_VERSION, a4 as CspNotEnabled, a5 as LocalsReassigned, a6 as generateCspDigest, a7 as PrerenderClientAddressNotAvailable, a8 as clientAddressSymbol, a9 as ClientAddressNotAvailable, aa as StaticClientAddressNotAvailable, ab as AstroResponseHeadersReassigned, ac as responseSentSymbol$1, ad as renderPage, ae as REWRITE_DIRECTIVE_HEADER_KEY, af as REWRITE_DIRECTIVE_HEADER_VALUE, ag as renderEndpoint, ah as LocalsNotAnObject, ai as FailedToFindPageMapSSR, aj as REROUTABLE_STATUS_CODES, ak as nodeRequestAbortControllerCleanupSymbol } from './astro/server_B-2LxKLH.mjs';
import { q as decryptString, v as createSlotValueFromString, w as isAstroComponentFactory, k as renderComponent, r as renderTemplate, R as ROUTE_TYPE_HEADER, x as REROUTE_DIRECTIVE_HEADER, A as AstroError, y as i18nNoLocaleFoundInPath, z as ResponseSentError, B as ActionNotFoundError, C as MiddlewareNoDataOrNextCalled, D as MiddlewareNotAResponse, G as originPathnameSymbol, H as RewriteWithBodyUsed, J as GetStaticPathsRequired, K as InvalidGetStaticPathsReturn, O as InvalidGetStaticPathsEntry, P as GetStaticPathsExpectedParams, Q as GetStaticPathsInvalidRouteParam, S as PageNumberParamNotFound, T as DEFAULT_404_COMPONENT, V as NoMatchingStaticPathFound, W as PrerenderDynamicEndpointPathCollide, X as ReservedSlotName, Y as renderSlotToString, Z as renderJSX, _ as chunkToString, $ as isRenderInstruction, a0 as ForbiddenRewrite, a1 as SessionStorageInitError, a2 as SessionStorageSaveError, a3 as ASTRO_VERSION, a4 as CspNotEnabled, a5 as LocalsReassigned, a6 as generateCspDigest, a7 as PrerenderClientAddressNotAvailable, a8 as clientAddressSymbol, a9 as ClientAddressNotAvailable, aa as StaticClientAddressNotAvailable, ab as AstroResponseHeadersReassigned, ac as responseSentSymbol$1, ad as renderPage, ae as REWRITE_DIRECTIVE_HEADER_KEY, af as REWRITE_DIRECTIVE_HEADER_VALUE, ag as renderEndpoint, ah as LocalsNotAnObject, ai as FailedToFindPageMapSSR, aj as REROUTABLE_STATUS_CODES, ak as nodeRequestAbortControllerCleanupSymbol } from './astro/server_CF97kUu8.mjs';
import colors from 'piccolore';
import 'clsx';
import { A as ActionError, d as deserializeActionResult, s as serializeActionResult, a as ACTION_RPC_ROUTE_PATTERN, b as ACTION_QUERY_PARAMS, g as getActionQueryString, D as DEFAULT_404_ROUTE, c as default404Instance, N as NOOP_MIDDLEWARE_FN, e as ensure404Route } from './astro-designed-error-pages_B_BAqCrl.mjs';
import { A as ActionError, d as deserializeActionResult, s as serializeActionResult, a as ACTION_RPC_ROUTE_PATTERN, b as ACTION_QUERY_PARAMS, g as getActionQueryString, D as DEFAULT_404_ROUTE, c as default404Instance, N as NOOP_MIDDLEWARE_FN, e as ensure404Route } from './astro-designed-error-pages_DSexancP.mjs';
import 'es-module-lexer';
import buffer from 'node:buffer';
import crypto$1 from 'node:crypto';

View File

@@ -1,24 +0,0 @@
import { e as createComponent, n as renderHead, o as renderSlot, r as renderTemplate, h as createAstro } from './astro/server_B-2LxKLH.mjs';
import 'piccolore';
import 'clsx';
/* empty css */
const $$Astro = createAstro();
const $$Base = createComponent(($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
Astro2.self = $$Base;
const { title = "Skillit" } = Astro2.props;
return renderTemplate`<html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"><title>${title}</title>${renderHead()}</head> <body class="min-h-screen font-sans text-gray-300 antialiased"> <!-- Subtle gradient glow --> <div class="pointer-events-none fixed inset-0 overflow-hidden"> <div class="absolute -top-40 left-1/2 -translate-x-1/2 h-80 w-[600px] rounded-full bg-accent-500/[0.07] blur-[120px]"></div> </div> <nav class="relative z-50 border-b border-white/[0.06] bg-surface-50/80 backdrop-blur-xl"> <div class="mx-auto max-w-5xl flex items-center justify-between px-6 py-4"> <a href="/" class="group flex items-center gap-2.5"> <div class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-accent-500 to-accent-600 shadow-lg shadow-accent-500/20"> <svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"> <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"></path> </svg> </div> <span class="text-lg font-bold tracking-tight text-white group-hover:text-accent-400 transition-colors">skillit</span> </a> <a href="/new" class="inline-flex items-center gap-1.5 rounded-lg bg-accent-500 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-accent-500/20 hover:bg-accent-600 hover:shadow-accent-500/30 active:scale-[0.97] transition-all"> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"></path> </svg>
New Skill
</a> </div> </nav> <main class="relative mx-auto max-w-5xl px-6 py-10"> ${renderSlot($$result, $$slots["default"])} </main> </body></html>`;
}, "/Users/alex/projects/skillit/src/layouts/Base.astro", void 0);
const _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
export { $$Base as $, _export_sfc as _ };

View File

@@ -0,0 +1,24 @@
import { e as createAstro, f as createComponent, n as renderHead, o as renderSlot, r as renderTemplate } from './astro/server_CF97kUu8.mjs';
import 'piccolore';
import 'clsx';
/* empty css */
const $$Astro = createAstro("https://skills.here.run.place");
const $$Base = createComponent(($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
Astro2.self = $$Base;
const { title = "Skills Here" } = Astro2.props;
return renderTemplate`<html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><title>${title}</title>${renderHead()}</head> <body class="min-h-screen font-sans text-gray-300 antialiased"> <!-- Subtle gradient glow --> <div class="pointer-events-none fixed inset-0 overflow-hidden"> <div class="absolute -top-40 left-1/2 -translate-x-1/2 h-80 w-[600px] rounded-full bg-accent-500/[0.07] blur-[120px]"></div> </div> <nav class="relative z-50 border-b border-white/[0.06] bg-surface-50/80 backdrop-blur-xl"> <div class="mx-auto max-w-6xl flex items-center justify-between px-6 py-4"> <a href="/" class="group flex items-center gap-2.5"> <img src="/favicon.svg" alt="Skills Here" class="h-8 w-8"> <span class="text-lg font-bold tracking-tight text-white group-hover:text-accent-400 transition-colors">Skills Here</span> </a> <a href="/new" class="inline-flex items-center gap-1.5 rounded-lg bg-accent-500 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-accent-500/20 hover:bg-accent-600 hover:shadow-accent-500/30 active:scale-[0.97] transition-all"> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"></path> </svg>
New Skill
</a> </div> </nav> <main class="relative mx-auto max-w-6xl px-6 py-10"> ${renderSlot($$result, $$slots["default"])} </main> </body></html>`;
}, "/Users/alex/projects/skillit/src/layouts/Base.astro", void 0);
const _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
export { $$Base as $, _export_sfc as _ };

View File

@@ -1,4 +1,4 @@
import { al as NOOP_MIDDLEWARE_HEADER, am as REDIRECT_STATUS_CODES, A as AstroError, an as ActionsReturnedInvalidDataError, T as DEFAULT_404_COMPONENT } from './astro/server_B-2LxKLH.mjs';
import { al as NOOP_MIDDLEWARE_HEADER, am as REDIRECT_STATUS_CODES, A as AstroError, an as ActionsReturnedInvalidDataError, T as DEFAULT_404_COMPONENT } from './astro/server_CF97kUu8.mjs';
import { parse, stringify } from 'devalue';
import { escape } from 'html-escaper';
@@ -12,7 +12,7 @@ const ACTION_QUERY_PARAMS$1 = {
actionName: "_action"};
const ACTION_RPC_ROUTE_PATTERN = "/_actions/[...path]";
const __vite_import_meta_env__ = {"ASSETS_PREFIX": undefined, "BASE_URL": "/", "DEV": false, "MODE": "production", "PROD": true, "SITE": undefined, "SSR": true};
const __vite_import_meta_env__ = {"ASSETS_PREFIX": undefined, "BASE_URL": "/", "DEV": false, "MODE": "production", "PROD": true, "SITE": "https://skills.here.run.place", "SSR": true};
const ACTION_QUERY_PARAMS = ACTION_QUERY_PARAMS$1;
const codeToStatusMap = {
// Implemented from IANA HTTP Status Code Registry

View File

@@ -455,7 +455,7 @@ Use import.meta.glob instead: https://vitejs.dev/guide/features.html#glob-import
}
function createAstro(site) {
return {
site: void 0,
site: new URL(site) ,
generator: `Astro v${ASTRO_VERSION}`,
glob: createAstroGlobFn()
};
@@ -2837,4 +2837,4 @@ function spreadAttributes(values = {}, _name, { class: scopedClassName } = {}) {
return markHTMLString(output);
}
export { isRenderInstruction as $, AstroError as A, ActionNotFoundError as B, MiddlewareNoDataOrNextCalled as C, MiddlewareNotAResponse as D, ExpectedImage as E, FailedToFetchRemoteImageDimensions as F, originPathnameSymbol as G, RewriteWithBodyUsed as H, IncompatibleDescriptorOptions as I, GetStaticPathsRequired as J, InvalidGetStaticPathsReturn as K, LocalImageUsedWrongly as L, MissingImageDimension as M, NoImageMetadata as N, InvalidGetStaticPathsEntry as O, GetStaticPathsExpectedParams as P, GetStaticPathsInvalidRouteParam as Q, ROUTE_TYPE_HEADER as R, PageNumberParamNotFound as S, DEFAULT_404_COMPONENT as T, UnsupportedImageFormat as U, NoMatchingStaticPathFound as V, PrerenderDynamicEndpointPathCollide as W, ReservedSlotName as X, renderSlotToString as Y, renderJSX as Z, chunkToString as _, UnsupportedImageConversion as a, ForbiddenRewrite as a0, SessionStorageInitError as a1, SessionStorageSaveError as a2, ASTRO_VERSION as a3, CspNotEnabled as a4, LocalsReassigned as a5, generateCspDigest as a6, PrerenderClientAddressNotAvailable as a7, clientAddressSymbol as a8, ClientAddressNotAvailable as a9, StaticClientAddressNotAvailable as aa, AstroResponseHeadersReassigned as ab, responseSentSymbol as ac, renderPage as ad, REWRITE_DIRECTIVE_HEADER_KEY as ae, REWRITE_DIRECTIVE_HEADER_VALUE as af, renderEndpoint as ag, LocalsNotAnObject as ah, FailedToFindPageMapSSR as ai, REROUTABLE_STATUS_CODES as aj, nodeRequestAbortControllerCleanupSymbol as ak, NOOP_MIDDLEWARE_HEADER as al, REDIRECT_STATUS_CODES as am, ActionsReturnedInvalidDataError as an, MissingSharp as ao, ExpectedImageOptions as b, ExpectedNotESMImage as c, InvalidImageService as d, createComponent as e, ImageMissingAlt as f, addAttribute as g, createAstro as h, ExperimentalFontsNotEnabled as i, FontFamilyNotFound as j, renderComponent as k, renderScript as l, maybeRenderHead as m, renderHead as n, renderSlot as o, decodeKey as p, decryptString as q, renderTemplate as r, spreadAttributes as s, toStyleString as t, unescapeHTML as u, createSlotValueFromString as v, isAstroComponentFactory as w, REROUTE_DIRECTIVE_HEADER as x, i18nNoLocaleFoundInPath as y, ResponseSentError as z };
export { isRenderInstruction as $, AstroError as A, ActionNotFoundError as B, MiddlewareNoDataOrNextCalled as C, MiddlewareNotAResponse as D, ExpectedImage as E, FailedToFetchRemoteImageDimensions as F, originPathnameSymbol as G, RewriteWithBodyUsed as H, IncompatibleDescriptorOptions as I, GetStaticPathsRequired as J, InvalidGetStaticPathsReturn as K, LocalImageUsedWrongly as L, MissingImageDimension as M, NoImageMetadata as N, InvalidGetStaticPathsEntry as O, GetStaticPathsExpectedParams as P, GetStaticPathsInvalidRouteParam as Q, ROUTE_TYPE_HEADER as R, PageNumberParamNotFound as S, DEFAULT_404_COMPONENT as T, UnsupportedImageFormat as U, NoMatchingStaticPathFound as V, PrerenderDynamicEndpointPathCollide as W, ReservedSlotName as X, renderSlotToString as Y, renderJSX as Z, chunkToString as _, UnsupportedImageConversion as a, ForbiddenRewrite as a0, SessionStorageInitError as a1, SessionStorageSaveError as a2, ASTRO_VERSION as a3, CspNotEnabled as a4, LocalsReassigned as a5, generateCspDigest as a6, PrerenderClientAddressNotAvailable as a7, clientAddressSymbol as a8, ClientAddressNotAvailable as a9, StaticClientAddressNotAvailable as aa, AstroResponseHeadersReassigned as ab, responseSentSymbol as ac, renderPage as ad, REWRITE_DIRECTIVE_HEADER_KEY as ae, REWRITE_DIRECTIVE_HEADER_VALUE as af, renderEndpoint as ag, LocalsNotAnObject as ah, FailedToFindPageMapSSR as ai, REROUTABLE_STATUS_CODES as aj, nodeRequestAbortControllerCleanupSymbol as ak, NOOP_MIDDLEWARE_HEADER as al, REDIRECT_STATUS_CODES as am, ActionsReturnedInvalidDataError as an, MissingSharp as ao, ExpectedImageOptions as b, ExpectedNotESMImage as c, InvalidImageService as d, createAstro as e, createComponent as f, ImageMissingAlt as g, addAttribute as h, ExperimentalFontsNotEnabled as i, FontFamilyNotFound as j, renderComponent as k, renderScript as l, maybeRenderHead as m, renderHead as n, renderSlot as o, decodeKey as p, decryptString as q, renderTemplate as r, spreadAttributes as s, toStyleString as t, unescapeHTML as u, createSlotValueFromString as v, isAstroComponentFactory as w, REROUTE_DIRECTIVE_HEADER as x, i18nNoLocaleFoundInPath as y, ResponseSentError as z };

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
import { i as isRemoteAllowed, j as joinPaths, a as isRemotePath, r as removeQueryString, b as isParentDirectory } from './remote_B3W5fv4r.mjs';
import { A as AstroError, E as ExpectedImage, L as LocalImageUsedWrongly, M as MissingImageDimension, U as UnsupportedImageFormat, I as IncompatibleDescriptorOptions, a as UnsupportedImageConversion, t as toStyleString, N as NoImageMetadata, F as FailedToFetchRemoteImageDimensions, b as ExpectedImageOptions, c as ExpectedNotESMImage, d as InvalidImageService, e as createComponent, f as ImageMissingAlt, m as maybeRenderHead, g as addAttribute, s as spreadAttributes, r as renderTemplate, h as createAstro, i as ExperimentalFontsNotEnabled, j as FontFamilyNotFound, u as unescapeHTML } from './astro/server_B-2LxKLH.mjs';
import { A as AstroError, E as ExpectedImage, L as LocalImageUsedWrongly, M as MissingImageDimension, U as UnsupportedImageFormat, I as IncompatibleDescriptorOptions, a as UnsupportedImageConversion, t as toStyleString, N as NoImageMetadata, F as FailedToFetchRemoteImageDimensions, b as ExpectedImageOptions, c as ExpectedNotESMImage, d as InvalidImageService, e as createAstro, f as createComponent, g as ImageMissingAlt, m as maybeRenderHead, h as addAttribute, s as spreadAttributes, r as renderTemplate, i as ExperimentalFontsNotEnabled, j as FontFamilyNotFound, u as unescapeHTML } from './astro/server_CF97kUu8.mjs';
import 'clsx';
import * as mime from 'mrmime';
import 'piccolore';
@@ -1434,7 +1434,7 @@ async function getConfiguredImageService() {
if (!globalThis?.astroAsset?.imageService) {
const { default: service } = await import(
// @ts-expect-error
'./sharp_CRCimLOL.mjs'
'./sharp_D9uxjd11.mjs'
).catch((e) => {
const error = new AstroError(InvalidImageService);
error.cause = e;
@@ -1589,7 +1589,7 @@ async function getImage$1(options, imageConfig) {
};
}
const $$Astro$2 = createAstro();
const $$Astro$2 = createAstro("https://skills.here.run.place");
const $$Image = createComponent(async ($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro$2, $$props, $$slots);
Astro2.self = $$Image;
@@ -1618,7 +1618,7 @@ const $$Image = createComponent(async ($$result, $$props, $$slots) => {
return renderTemplate`${maybeRenderHead()}<img${addAttribute(image.src, "src")}${spreadAttributes(attributes)}${addAttribute(className, "class")}>`;
}, "/Users/alex/projects/skillit/node_modules/astro/components/Image.astro", void 0);
const $$Astro$1 = createAstro();
const $$Astro$1 = createAstro("https://skills.here.run.place");
const $$Picture = createComponent(async ($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro$1, $$props, $$slots);
Astro2.self = $$Picture;
@@ -1728,7 +1728,7 @@ function checkWeight(input, target) {
return input === target;
}
const $$Astro = createAstro();
const $$Astro = createAstro("https://skills.here.run.place");
const $$Font = createComponent(($$result, $$props, $$slots) => {
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
Astro2.self = $$Font;

View File

@@ -1,5 +1,5 @@
import { A as AstroError, ao as MissingSharp } from './astro/server_B-2LxKLH.mjs';
import { b as baseService, p as parseQuality } from './node_WXNYuHqd.mjs';
import { A as AstroError, ao as MissingSharp } from './astro/server_CF97kUu8.mjs';
import { b as baseService, p as parseQuality } from './node_HH9e2ntY.mjs';
let sharp;
const qualityTable = {

View File

@@ -31,6 +31,10 @@ function parseSkill(slug, raw) {
"disable-model-invocation": Boolean(data["disable-model-invocation"]),
context: data.context || "",
agent: data.agent || "",
author: data.author || "",
"author-email": data["author-email"] || "",
"fork-of": data["fork-of"] || "",
tags: parseTools(data.tags),
hooks: typeof data.hooks === "object" && data.hooks !== null ? data.hooks : null,
content: content.trim(),
raw
@@ -49,6 +53,14 @@ async function listSkills() {
}
return skills.sort((a, b) => a.name.localeCompare(b.name));
}
async function getAllTags() {
const all = await listSkills();
return [...new Set(all.flatMap((s) => s.tags))].sort();
}
async function getForksOf(slug) {
const all = await listSkills();
return all.filter((s) => s["fork-of"] === slug);
}
async function getSkill(slug) {
try {
const raw = await fs.readFile(skillPath(slug), "utf-8");
@@ -94,4 +106,4 @@ async function deleteSkill(slug) {
await fs.unlink(dest);
}
export { createSkill as c, deleteSkill as d, getSkill as g, isValidSlug as i, listSkills as l, updateSkill as u };
export { getAllTags as a, getForksOf as b, createSkill as c, deleteSkill as d, getSkill as g, isValidSlug as i, listSkills as l, updateSkill as u };

46
dist/server/chunks/stats_CaDi9y9J.mjs vendored Normal file
View File

@@ -0,0 +1,46 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const STATS_FILE = path.resolve(process.env.STATS_FILE || "data/stats.json");
async function readStore() {
try {
const raw = await fs.readFile(STATS_FILE, "utf-8");
return JSON.parse(raw);
} catch {
return {};
}
}
async function writeStore(store) {
const dir = path.dirname(STATS_FILE);
await fs.mkdir(dir, { recursive: true });
const tmp = STATS_FILE + ".tmp";
await fs.writeFile(tmp, JSON.stringify(store, null, 2), "utf-8");
await fs.rename(tmp, STATS_FILE);
}
function ensure(store, slug) {
if (!store[slug]) {
store[slug] = { downloads: 0, pushes: 0, lastPushedAt: null };
}
return store[slug];
}
async function recordDownload(slug) {
const store = await readStore();
ensure(store, slug).downloads++;
await writeStore(store);
}
async function recordPush(slug) {
const store = await readStore();
const entry = ensure(store, slug);
entry.pushes++;
entry.lastPushedAt = (/* @__PURE__ */ new Date()).toISOString();
await writeStore(store);
}
async function getStatsForSlug(slug) {
const store = await readStore();
return store[slug] || { downloads: 0, pushes: 0, lastPushedAt: null };
}
async function getAllStats() {
return readStore();
}
export { recordDownload as a, getAllStats as b, getStatsForSlug as g, recordPush as r };

309
dist/server/chunks/sync_BEq_wzpT.mjs vendored Normal file
View File

@@ -0,0 +1,309 @@
import { l as listSkills } from './skills_BacVQUiS.mjs';
function isPowerShell(request) {
const ua = request.headers.get("user-agent") || "";
return /PowerShell/i.test(ua);
}
async function buildPushScript(baseUrl, skillsDir) {
const lines = [
"#!/usr/bin/env bash",
"set -euo pipefail",
"",
`SKILLS_DIR="${skillsDir}"`,
`BASE_URL="${baseUrl}"`,
'FILTER="${1:-}"',
'TOKEN_FILE="$HOME/.claude/skills.here-token"',
"",
"# Get git author if available",
'AUTHOR_NAME=$(git config user.name 2>/dev/null || echo "")',
'AUTHOR_EMAIL=$(git config user.email 2>/dev/null || echo "")',
"",
"# Load or register token",
'TOKEN=""',
'if [ -f "$TOKEN_FILE" ]; then',
' TOKEN=$(cat "$TOKEN_FILE")',
'elif [ -n "$AUTHOR_EMAIL" ]; then',
' echo "No token found. Registering with $AUTHOR_EMAIL..."',
" REGISTER_RESPONSE=$(curl -sS -X POST \\",
' -H "Content-Type: application/json" \\',
' -d "{\\"email\\": \\"$AUTHOR_EMAIL\\", \\"name\\": \\"$AUTHOR_NAME\\"}" \\',
' -w "\\n%{http_code}" \\',
' "$BASE_URL/api/auth/register")',
` REGISTER_BODY=$(echo "$REGISTER_RESPONSE" | sed '$d')`,
' REGISTER_STATUS=$(echo "$REGISTER_RESPONSE" | tail -1)',
' if [ "$REGISTER_STATUS" = "201" ]; then',
' TOKEN=$(echo "$REGISTER_BODY" | jq -r .token)',
' mkdir -p "$(dirname "$TOKEN_FILE")"',
' echo "$TOKEN" > "$TOKEN_FILE"',
' chmod 600 "$TOKEN_FILE"',
' echo " Token saved to $TOKEN_FILE"',
' elif [ "$REGISTER_STATUS" = "409" ]; then',
' echo " Email already registered. Place your token in $TOKEN_FILE"',
' echo " Continuing without token (unprotected skills only)..."',
" else",
' echo " Registration failed ($REGISTER_STATUS): $REGISTER_BODY"',
' echo " Continuing without token (unprotected skills only)..."',
" fi",
"fi",
"",
'if [ ! -d "$SKILLS_DIR" ]; then',
' echo "No skills directory found at $SKILLS_DIR"',
" exit 1",
"fi",
"",
'AUTH_HEADER=""',
'if [ -n "$TOKEN" ]; then',
' AUTH_HEADER="Authorization: Bearer $TOKEN"',
"fi",
"",
"push_skill() {",
' local file="$1"',
' local slug=$(basename "$file" .md)',
' local content=$(cat "$file")',
"",
" # Inject author into frontmatter if available and not already present",
' if [ -n "$AUTHOR_EMAIL" ] && ! echo "$content" | grep -q "^author-email:"; then',
` content=$(echo "$content" | awk -v name="$AUTHOR_NAME" -v email="$AUTHOR_EMAIL" 'NR==1 && /^---$/{print; if (name) print "author: " name; print "author-email: " email; next} {print}')`,
" fi",
"",
" # Build curl auth args",
' local auth_args=""',
' if [ -n "$AUTH_HEADER" ]; then',
' auth_args="-H \\"$AUTH_HEADER\\""',
" fi",
"",
" # Try PUT (update), fallback to POST (create)",
" local response",
' if [ -n "$TOKEN" ]; then',
' response=$(curl -sS -o /dev/null -w "%{http_code}" -X PUT \\',
' -H "Content-Type: application/json" \\',
' -H "Authorization: Bearer $TOKEN" \\',
' -d "{\\"content\\": $(echo "$content" | jq -Rs .)}" \\',
' "$BASE_URL/api/skills/$slug")',
" else",
' response=$(curl -sS -o /dev/null -w "%{http_code}" -X PUT \\',
' -H "Content-Type: application/json" \\',
' -d "{\\"content\\": $(echo "$content" | jq -Rs .)}" \\',
' "$BASE_URL/api/skills/$slug")',
" fi",
"",
' if [ "$response" = "403" ]; then',
' echo " ✗ $slug (permission denied — token missing or invalid)"',
" return 1",
" fi",
"",
' if [ "$response" = "404" ]; then',
' if [ -n "$TOKEN" ]; then',
' local post_status=$(curl -sS -o /dev/null -w "%{http_code}" -X POST \\',
' -H "Content-Type: application/json" \\',
' -H "Authorization: Bearer $TOKEN" \\',
' -d "{\\"slug\\": \\"$slug\\", \\"content\\": $(echo "$content" | jq -Rs .)}" \\',
' "$BASE_URL/api/skills")',
" else",
' local post_status=$(curl -sS -o /dev/null -w "%{http_code}" -X POST \\',
' -H "Content-Type: application/json" \\',
' -d "{\\"slug\\": \\"$slug\\", \\"content\\": $(echo "$content" | jq -Rs .)}" \\',
' "$BASE_URL/api/skills")',
" fi",
' if [ "$post_status" = "403" ]; then',
' echo " ✗ $slug (permission denied — token missing or invalid)"',
" return 1",
" fi",
" fi",
"",
' echo " ✓ $slug"',
"}",
"",
"count=0",
"failed=0",
'if [ -n "$FILTER" ]; then',
" # Push a specific skill",
' file="$SKILLS_DIR/${FILTER%.md}.md"',
' if [ ! -f "$file" ]; then',
' echo "Skill not found: $file"',
" exit 1",
" fi",
' if push_skill "$file"; then',
" count=1",
" else",
" failed=1",
" fi",
"else",
" # Push all skills",
' for file in "$SKILLS_DIR"/*.md; do',
' [ -f "$file" ] || continue',
' if push_skill "$file"; then',
" count=$((count + 1))",
" else",
" failed=$((failed + 1))",
" fi",
" done",
"fi",
"",
'echo "Pushed $count skill(s) to $BASE_URL"',
'if [ "$failed" -gt 0 ]; then',
' echo "$failed skill(s) failed (permission denied)"',
"fi",
""
];
return lines.join("\n");
}
async function buildSyncScript(baseUrl, skillsDir) {
const skills = await listSkills();
const lines = [
"#!/usr/bin/env bash",
"set -euo pipefail",
"",
`SKILLS_DIR="${skillsDir}"`,
'mkdir -p "$SKILLS_DIR"',
""
];
if (skills.length === 0) {
lines.push('echo "No skills available to sync."');
} else {
lines.push(`echo "Syncing ${skills.length} skill(s) from ${baseUrl}..."`);
lines.push("");
for (const skill of skills) {
const skillUrl = `${baseUrl}/${skill.slug}`;
lines.push(`curl -fsSL "${skillUrl}" -o "$SKILLS_DIR/${skill.slug}.md"`);
lines.push(`echo " ✓ ${skill.name}"`);
}
lines.push("");
lines.push('echo "Done! Skills synced to $SKILLS_DIR"');
}
lines.push("");
return lines.join("\n");
}
async function buildSyncScriptPS(baseUrl, skillsDir) {
const skills = await listSkills();
const lines = [
'$ErrorActionPreference = "Stop"',
"",
`$SkillsDir = "${skillsDir}"`,
"New-Item -ItemType Directory -Force -Path $SkillsDir | Out-Null",
""
];
if (skills.length === 0) {
lines.push('Write-Host "No skills available to sync."');
} else {
lines.push(`Write-Host "Syncing ${skills.length} skill(s) from ${baseUrl}..."`);
lines.push("");
for (const skill of skills) {
const skillUrl = `${baseUrl}/${skill.slug}`;
lines.push(`Invoke-WebRequest -Uri "${skillUrl}" -OutFile (Join-Path $SkillsDir "${skill.slug}.md")`);
lines.push(`Write-Host " ✓ ${skill.name}"`);
}
lines.push("");
lines.push('Write-Host "Done! Skills synced to $SkillsDir"');
}
lines.push("");
return lines.join("\n");
}
async function buildPushScriptPS(baseUrl, skillsDir) {
const lines = [
'$ErrorActionPreference = "Stop"',
"",
`$SkillsDir = "${skillsDir}"`,
`$BaseUrl = "${baseUrl}"`,
'$Filter = if ($args.Count -gt 0) { $args[0] } else { "" }',
'$TokenFile = Join-Path $HOME ".claude\\skills.here-token"',
"",
"# Get git author if available",
'$AuthorName = try { git config user.name 2>$null } catch { "" }',
'$AuthorEmail = try { git config user.email 2>$null } catch { "" }',
"",
"# Load or register token",
'$Token = ""',
"if (Test-Path $TokenFile) {",
" $Token = (Get-Content $TokenFile -Raw).Trim()",
"} elseif ($AuthorEmail) {",
' Write-Host "No token found. Registering with $AuthorEmail..."',
" try {",
" $body = @{ email = $AuthorEmail; name = $AuthorName } | ConvertTo-Json",
' $resp = Invoke-WebRequest -Uri "$BaseUrl/api/auth/register" -Method POST -ContentType "application/json" -Body $body',
" if ($resp.StatusCode -eq 201) {",
" $Token = ($resp.Content | ConvertFrom-Json).token",
" New-Item -ItemType Directory -Force -Path (Split-Path $TokenFile) | Out-Null",
" Set-Content -Path $TokenFile -Value $Token",
' Write-Host " Token saved to $TokenFile"',
" }",
" } catch {",
" $code = $_.Exception.Response.StatusCode.value__",
" if ($code -eq 409) {",
' Write-Host " Email already registered. Place your token in $TokenFile"',
" } else {",
' Write-Host " Registration failed: $_"',
" }",
' Write-Host " Continuing without token (unprotected skills only)..."',
" }",
"}",
"",
"if (-not (Test-Path $SkillsDir)) {",
' Write-Host "No skills directory found at $SkillsDir"',
" exit 1",
"}",
"",
'$headers = @{ "Content-Type" = "application/json" }',
'if ($Token) { $headers["Authorization"] = "Bearer $Token" }',
"",
"function Push-Skill($file) {",
" $slug = [IO.Path]::GetFileNameWithoutExtension($file)",
" $content = Get-Content $file -Raw",
"",
" # Inject author if available and not present",
' if ($AuthorEmail -and $content -notmatch "(?m)^author-email:") {',
' $inject = ""',
' if ($AuthorName) { $inject += "author: $AuthorName`n" }',
' $inject += "author-email: $AuthorEmail"',
' $content = $content -replace "^---$", "---`n$inject" -replace "^---`n", "---`n"',
" }",
"",
" $body = @{ content = $content } | ConvertTo-Json",
" try {",
' Invoke-WebRequest -Uri "$BaseUrl/api/skills/$slug" -Method PUT -Headers $headers -Body $body | Out-Null',
' Write-Host " ✓ $slug"',
" return $true",
" } catch {",
" $code = $_.Exception.Response.StatusCode.value__",
" if ($code -eq 403) {",
' Write-Host " ✗ $slug (permission denied)"',
" return $false",
" }",
" if ($code -eq 404) {",
" $postBody = @{ slug = $slug; content = $content } | ConvertTo-Json",
" try {",
' Invoke-WebRequest -Uri "$BaseUrl/api/skills" -Method POST -Headers $headers -Body $postBody | Out-Null',
' Write-Host " ✓ $slug"',
" return $true",
" } catch {",
" $postCode = $_.Exception.Response.StatusCode.value__",
" if ($postCode -eq 403) {",
' Write-Host " ✗ $slug (permission denied)"',
" return $false",
" }",
" }",
" }",
" }",
' Write-Host " ✓ $slug"',
" return $true",
"}",
"",
"$count = 0; $failed = 0",
"if ($Filter) {",
` $file = Join-Path $SkillsDir "$($Filter -replace '\\.md$','').md"`,
' if (-not (Test-Path $file)) { Write-Host "Skill not found: $file"; exit 1 }',
" if (Push-Skill $file) { $count++ } else { $failed++ }",
"} else {",
' Get-ChildItem -Path $SkillsDir -Filter "*.md" | ForEach-Object {',
" if (Push-Skill $_.FullName) { $count++ } else { $failed++ }",
" }",
"}",
"",
'Write-Host "Pushed $count skill(s) to $BaseUrl"',
'if ($failed -gt 0) { Write-Host "$failed skill(s) failed (permission denied)" }',
""
];
return lines.join("\n");
}
export { buildSyncScriptPS as a, buildSyncScript as b, buildPushScript as c, buildPushScriptPS as d, isPowerShell as i };

View File

@@ -1,71 +0,0 @@
import { l as listSkills } from './skills_COWfD5oy.mjs';
async function buildPushScript(baseUrl, skillsDir) {
const lines = [
"#!/usr/bin/env bash",
"set -euo pipefail",
"",
`SKILLS_DIR="${skillsDir}"`,
`BASE_URL="${baseUrl}"`,
"",
'if [ ! -d "$SKILLS_DIR" ]; then',
' echo "No skills directory found at $SKILLS_DIR"',
" exit 1",
"fi",
"",
"count=0",
'for file in "$SKILLS_DIR"/*.md; do',
' [ -f "$file" ] || continue',
' slug=$(basename "$file" .md)',
' content=$(cat "$file")',
"",
" # Try PUT (update), fallback to POST (create)",
' status=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \\',
' -H "Content-Type: application/json" \\',
' -d "{\\"content\\": $(echo "$content" | jq -Rs .)}" \\',
' "$BASE_URL/api/skills/$slug")',
"",
' if [ "$status" = "404" ]; then',
" curl -fsS -X POST \\",
' -H "Content-Type: application/json" \\',
' -d "{\\"slug\\": \\"$slug\\", \\"content\\": $(echo "$content" | jq -Rs .)}" \\',
' "$BASE_URL/api/skills" > /dev/null',
" fi",
"",
' echo " ✓ $slug"',
" count=$((count + 1))",
"done",
"",
'echo "Pushed $count skill(s) to $BASE_URL"',
""
];
return lines.join("\n");
}
async function buildSyncScript(baseUrl, skillsDir) {
const skills = await listSkills();
const lines = [
"#!/usr/bin/env bash",
"set -euo pipefail",
"",
`SKILLS_DIR="${skillsDir}"`,
'mkdir -p "$SKILLS_DIR"',
""
];
if (skills.length === 0) {
lines.push('echo "No skills available to sync."');
} else {
lines.push(`echo "Syncing ${skills.length} skill(s) from ${baseUrl}..."`);
lines.push("");
for (const skill of skills) {
const skillUrl = `${baseUrl}/${skill.slug}`;
lines.push(`curl -fsSL "${skillUrl}" -o "$SKILLS_DIR/${skill.slug}.md"`);
lines.push(`echo " ✓ ${skill.name}"`);
}
lines.push("");
lines.push('echo "Done! Skills synced to $SKILLS_DIR"');
}
lines.push("");
return lines.join("\n");
}
export { buildPushScript as a, buildSyncScript as b };

55
dist/server/chunks/tokens_CAzj9Aj8.mjs vendored Normal file
View File

@@ -0,0 +1,55 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import crypto from 'node:crypto';
const TOKENS_FILE = path.resolve(process.env.TOKENS_FILE || "data/tokens.json");
async function readStore() {
try {
const raw = await fs.readFile(TOKENS_FILE, "utf-8");
return JSON.parse(raw);
} catch {
return {};
}
}
async function writeStore(store) {
const dir = path.dirname(TOKENS_FILE);
await fs.mkdir(dir, { recursive: true });
const tmp = TOKENS_FILE + ".tmp";
await fs.writeFile(tmp, JSON.stringify(store, null, 2), "utf-8");
await fs.rename(tmp, TOKENS_FILE);
}
function hashToken(token) {
return crypto.createHash("sha256").update(token).digest("hex");
}
async function generateToken(email, name) {
const store = await readStore();
if (store[email]) {
throw new Error("Email already registered");
}
const token = crypto.randomBytes(32).toString("hex");
store[email] = {
hash: hashToken(token),
name,
createdAt: (/* @__PURE__ */ new Date()).toISOString()
};
await writeStore(store);
return token;
}
async function verifyToken(email, token) {
if (!email || !token) return false;
const store = await readStore();
const entry = store[email];
if (!entry) return false;
return entry.hash === hashToken(token);
}
async function hasToken(email) {
const store = await readStore();
return Boolean(store[email]);
}
function extractBearerToken(request) {
const auth = request.headers.get("Authorization") || "";
const match = auth.match(/^Bearer\s+(\S+)$/i);
return match ? match[1] : "";
}
export { extractBearerToken as e, generateToken as g, hasToken as h, verifyToken as v };