Add remaining source changes: resource data, refactored pages, updated styles
- Add example resources (agent, output-style, rule, skill) - Refactor legacy skill pages to use generic resource system - Update favicon, global styles, models, skills lib, and stats - Update PLAN.md
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
data/tokens.json
|
||||
data/tokens.json
|
||||
dist/
|
||||
|
||||
1
dist/client/_astro/DeleteButton.CHpgpUL1.js
vendored
1
dist/client/_astro/DeleteButton.CHpgpUL1.js
vendored
File diff suppressed because one or more lines are too long
1
dist/client/_astro/EditGate.mFTQzSOo.js
vendored
1
dist/client/_astro/EditGate.mFTQzSOo.js
vendored
@@ -1 +0,0 @@
|
||||
import{c as d,a as o,d as u,l as b,g as p,t as f,w as C,v as _,b as m,T as S,F as T,h as E,i,m as M,o as c}from"./runtime-dom.esm-bundler.A7MyAQcw.js";import{_ as V}from"./_plugin-vue_export-helper.DlAUqK2U.js";const B=E({__name:"EditGate",props:{slug:{},authorEmail:{},authorName:{},authorHasToken:{type:Boolean}},setup(h,{expose:e}){e();const r=h,t=i(!1),s=i(""),a=i(""),n=i(!1),v=i();async function x(){if(!r.authorEmail||!r.authorHasToken){window.location.href=`/${r.slug}/edit`;return}const l=localStorage.getItem("skillshere-token")||"";if(l)try{if((await fetch("/api/auth/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:r.authorEmail,token:l})})).ok){localStorage.setItem("skillshere-token",l),window.location.href=`/${r.slug}/edit`;return}}catch{}t.value=!0,a.value="",s.value="",M(()=>v.value?.focus())}async function g(){n.value=!0,a.value="";try{const l=await fetch("/api/auth/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:r.authorEmail,token:s.value})});if(!l.ok){const y=await l.json();a.value=y.error||"Invalid token";return}localStorage.setItem("skillshere-token",s.value),window.location.href=`/${r.slug}/edit`}catch{a.value="Could not verify token"}finally{n.value=!1}}function w(){t.value=!1,window.location.href=`/new?from=${encodeURIComponent(r.slug)}`}const k={props:r,showModal:t,token:s,error:a,verifying:n,tokenInput:v,handleClick:x,verify:g,forkSkill:w};return Object.defineProperty(k,"__isScriptSetup",{enumerable:!1,value:!0}),k}}),I={class:"w-full max-w-md rounded-2xl border border-white/[0.08] bg-[var(--color-surface-200)] p-6 shadow-2xl"},N={class:"text-sm text-gray-500 mb-4"},j={class:"text-gray-300"},O={key:0,class:"mt-2 text-sm text-red-400"},A={class:"mt-4 flex items-center gap-3"},H=["disabled"],P={key:0,class:"h-4 w-4 animate-spin",fill:"none",viewBox:"0 0 24 24"};function F(h,e,r,t,s,a){return c(),d(T,null,[o("button",{onClick:t.handleClick,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"},[...e[3]||(e[3]=[o("svg",{class:"h-3.5 w-3.5",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor","stroke-width":"2"},[o("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"})],-1),u(" Edit ",-1)])]),(c(),b(S,{to:"body"},[t.showModal?(c(),d("div",{key:0,class:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:e[2]||(e[2]=p(n=>t.showModal=!1,["self"]))},[o("div",I,[e[7]||(e[7]=o("h3",{class:"text-lg font-semibold text-white mb-1"},"Author Verification",-1)),o("p",N,[e[4]||(e[4]=u(" This skill is owned by ",-1)),o("strong",j,f(r.authorName||r.authorEmail),1),e[5]||(e[5]=u(". Enter your token to edit. ",-1))]),o("form",{onSubmit:p(t.verify,["prevent"])},[C(o("input",{ref:"tokenInput","onUpdate:modelValue":e[0]||(e[0]=n=>t.token=n),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"},null,512),[[_,t.token]]),t.error?(c(),d("p",O,f(t.error),1)):m("",!0),o("div",A,[o("button",{type:"submit",disabled:t.verifying||!t.token,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"},[t.verifying?(c(),d("svg",P,[...e[6]||(e[6]=[o("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor","stroke-width":"4"},null,-1),o("path",{class:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"},null,-1)])])):m("",!0),u(" "+f(t.verifying?"Verifying...":"Continue to Edit"),1)],8,H),o("button",{type:"button",onClick:t.forkSkill,class:"text-sm text-[var(--color-accent-400)] hover:text-[var(--color-accent-300)] transition-colors"}," Fork instead "),o("button",{type:"button",onClick:e[1]||(e[1]=n=>t.showModal=!1),class:"ml-auto text-sm text-gray-600 hover:text-gray-300 transition-colors"}," Cancel ")])],32)])])):m("",!0)]))],64)}const D=V(B,[["render",F]]);export{D as default};
|
||||
66
dist/client/_astro/SkillEditor.DYeR1V3X.js
vendored
66
dist/client/_astro/SkillEditor.DYeR1V3X.js
vendored
File diff suppressed because one or more lines are too long
1
dist/client/_astro/SkillSearch.A7FO5axR.js
vendored
1
dist/client/_astro/SkillSearch.A7FO5axR.js
vendored
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
const s=(t,r)=>{const o=t.__vccOpts||t;for(const[c,e]of r)o[c]=e;return o};export{s as _};
|
||||
1
dist/client/_astro/_slug_.DRpcPMTm.css
vendored
1
dist/client/_astro/_slug_.DRpcPMTm.css
vendored
File diff suppressed because one or more lines are too long
1
dist/client/_astro/client.BnTlSu1B.js
vendored
1
dist/client/_astro/client.BnTlSu1B.js
vendored
@@ -1 +0,0 @@
|
||||
import{h as m,s as r,u as y,x as v,S}from"./runtime-dom.esm-bundler.A7MyAQcw.js";const g=()=>{},h=m({props:{value:String,name:String,hydrate:{type:Boolean,default:!0}},setup({name:t,value:s,hydrate:o}){if(!s)return()=>null;let c=o?"astro-slot":"astro-static-slot";return()=>r(c,{name:t,innerHTML:s})}});var A=h;let p=new WeakMap;var H=t=>async(s,o,c,{client:l})=>{if(!t.hasAttribute("ssr"))return;const f=s.name?`${s.name} Host`:void 0,u={};for(const[n,a]of Object.entries(c))u[n]=()=>r(A,{value:a,name:n==="default"?void 0:n});const i=l!=="only",d=i?y:v;let e=p.get(t);if(e)e.props=o,e.slots=u,e.component.$forceUpdate();else{e={props:o,slots:u};const n=d({name:f,render(){let a=r(s,e.props,e.slots);return e.component=this,b(s.setup)&&(a=r(S,null,a)),a}});n.config.idPrefix=t.getAttribute("prefix")??void 0,await g(),n.mount(t,i),p.set(t,e),t.addEventListener("astro:unmount",()=>n.unmount(),{once:!0})}};function b(t){const s=t?.constructor;return s&&s.name==="AsyncFunction"}export{H as default};
|
||||
File diff suppressed because one or more lines are too long
21
dist/client/favicon.svg
vendored
21
dist/client/favicon.svg
vendored
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 12 KiB |
1
dist/server/_@astrojs-ssr-adapter.mjs
vendored
1
dist/server/_@astrojs-ssr-adapter.mjs
vendored
@@ -1 +0,0 @@
|
||||
export { c as createExports, a as start } from './chunks/_@astrojs-ssr-adapter_BeL8VyJ8.mjs';
|
||||
3
dist/server/_noop-middleware.mjs
vendored
3
dist/server/_noop-middleware.mjs
vendored
@@ -1,3 +0,0 @@
|
||||
const onRequest = (_, next) => next();
|
||||
|
||||
export { onRequest };
|
||||
4412
dist/server/chunks/_@astrojs-ssr-adapter_BeL8VyJ8.mjs
vendored
4412
dist/server/chunks/_@astrojs-ssr-adapter_BeL8VyJ8.mjs
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,24 +0,0 @@
|
||||
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 _ };
|
||||
@@ -1,364 +0,0 @@
|
||||
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';
|
||||
|
||||
const NOOP_MIDDLEWARE_FN = async (_ctx, next) => {
|
||||
const response = await next();
|
||||
response.headers.set(NOOP_MIDDLEWARE_HEADER, "true");
|
||||
return response;
|
||||
};
|
||||
|
||||
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": "https://skills.here.run.place", "SSR": true};
|
||||
const ACTION_QUERY_PARAMS = ACTION_QUERY_PARAMS$1;
|
||||
const codeToStatusMap = {
|
||||
// Implemented from IANA HTTP Status Code Registry
|
||||
// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||
BAD_REQUEST: 400,
|
||||
UNAUTHORIZED: 401,
|
||||
PAYMENT_REQUIRED: 402,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
METHOD_NOT_ALLOWED: 405,
|
||||
NOT_ACCEPTABLE: 406,
|
||||
PROXY_AUTHENTICATION_REQUIRED: 407,
|
||||
REQUEST_TIMEOUT: 408,
|
||||
CONFLICT: 409,
|
||||
GONE: 410,
|
||||
LENGTH_REQUIRED: 411,
|
||||
PRECONDITION_FAILED: 412,
|
||||
CONTENT_TOO_LARGE: 413,
|
||||
URI_TOO_LONG: 414,
|
||||
UNSUPPORTED_MEDIA_TYPE: 415,
|
||||
RANGE_NOT_SATISFIABLE: 416,
|
||||
EXPECTATION_FAILED: 417,
|
||||
MISDIRECTED_REQUEST: 421,
|
||||
UNPROCESSABLE_CONTENT: 422,
|
||||
LOCKED: 423,
|
||||
FAILED_DEPENDENCY: 424,
|
||||
TOO_EARLY: 425,
|
||||
UPGRADE_REQUIRED: 426,
|
||||
PRECONDITION_REQUIRED: 428,
|
||||
TOO_MANY_REQUESTS: 429,
|
||||
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
||||
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
|
||||
INTERNAL_SERVER_ERROR: 500,
|
||||
NOT_IMPLEMENTED: 501,
|
||||
BAD_GATEWAY: 502,
|
||||
SERVICE_UNAVAILABLE: 503,
|
||||
GATEWAY_TIMEOUT: 504,
|
||||
HTTP_VERSION_NOT_SUPPORTED: 505,
|
||||
VARIANT_ALSO_NEGOTIATES: 506,
|
||||
INSUFFICIENT_STORAGE: 507,
|
||||
LOOP_DETECTED: 508,
|
||||
NETWORK_AUTHENTICATION_REQUIRED: 511
|
||||
};
|
||||
const statusToCodeMap = Object.entries(codeToStatusMap).reduce(
|
||||
// reverse the key-value pairs
|
||||
(acc, [key, value]) => ({ ...acc, [value]: key }),
|
||||
{}
|
||||
);
|
||||
class ActionError extends Error {
|
||||
type = "AstroActionError";
|
||||
code = "INTERNAL_SERVER_ERROR";
|
||||
status = 500;
|
||||
constructor(params) {
|
||||
super(params.message);
|
||||
this.code = params.code;
|
||||
this.status = ActionError.codeToStatus(params.code);
|
||||
if (params.stack) {
|
||||
this.stack = params.stack;
|
||||
}
|
||||
}
|
||||
static codeToStatus(code) {
|
||||
return codeToStatusMap[code];
|
||||
}
|
||||
static statusToCode(status) {
|
||||
return statusToCodeMap[status] ?? "INTERNAL_SERVER_ERROR";
|
||||
}
|
||||
static fromJson(body) {
|
||||
if (isInputError(body)) {
|
||||
return new ActionInputError(body.issues);
|
||||
}
|
||||
if (isActionError(body)) {
|
||||
return new ActionError(body);
|
||||
}
|
||||
return new ActionError({
|
||||
code: "INTERNAL_SERVER_ERROR"
|
||||
});
|
||||
}
|
||||
}
|
||||
function isActionError(error) {
|
||||
return typeof error === "object" && error != null && "type" in error && error.type === "AstroActionError";
|
||||
}
|
||||
function isInputError(error) {
|
||||
return typeof error === "object" && error != null && "type" in error && error.type === "AstroActionInputError" && "issues" in error && Array.isArray(error.issues);
|
||||
}
|
||||
class ActionInputError extends ActionError {
|
||||
type = "AstroActionInputError";
|
||||
// We don't expose all ZodError properties.
|
||||
// Not all properties will serialize from server to client,
|
||||
// and we don't want to import the full ZodError object into the client.
|
||||
issues;
|
||||
fields;
|
||||
constructor(issues) {
|
||||
super({
|
||||
message: `Failed to validate: ${JSON.stringify(issues, null, 2)}`,
|
||||
code: "BAD_REQUEST"
|
||||
});
|
||||
this.issues = issues;
|
||||
this.fields = {};
|
||||
for (const issue of issues) {
|
||||
if (issue.path.length > 0) {
|
||||
const key = issue.path[0].toString();
|
||||
this.fields[key] ??= [];
|
||||
this.fields[key]?.push(issue.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function getActionQueryString(name) {
|
||||
const searchParams = new URLSearchParams({ [ACTION_QUERY_PARAMS$1.actionName]: name });
|
||||
return `?${searchParams.toString()}`;
|
||||
}
|
||||
function serializeActionResult(res) {
|
||||
if (res.error) {
|
||||
if (Object.assign(__vite_import_meta_env__, { _: process.env._ })?.DEV) {
|
||||
actionResultErrorStack.set(res.error.stack);
|
||||
}
|
||||
let body2;
|
||||
if (res.error instanceof ActionInputError) {
|
||||
body2 = {
|
||||
type: res.error.type,
|
||||
issues: res.error.issues,
|
||||
fields: res.error.fields
|
||||
};
|
||||
} else {
|
||||
body2 = {
|
||||
...res.error,
|
||||
message: res.error.message
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "error",
|
||||
status: res.error.status,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body2)
|
||||
};
|
||||
}
|
||||
if (res.data === void 0) {
|
||||
return {
|
||||
type: "empty",
|
||||
status: 204
|
||||
};
|
||||
}
|
||||
let body;
|
||||
try {
|
||||
body = stringify(res.data, {
|
||||
// Add support for URL objects
|
||||
URL: (value) => value instanceof URL && value.href
|
||||
});
|
||||
} catch (e) {
|
||||
let hint = ActionsReturnedInvalidDataError.hint;
|
||||
if (res.data instanceof Response) {
|
||||
hint = REDIRECT_STATUS_CODES.includes(res.data.status) ? "If you need to redirect when the action succeeds, trigger a redirect where the action is called. See the Actions guide for server and client redirect examples: https://docs.astro.build/en/guides/actions." : "If you need to return a Response object, try using a server endpoint instead. See https://docs.astro.build/en/guides/endpoints/#server-endpoints-api-routes";
|
||||
}
|
||||
throw new AstroError({
|
||||
...ActionsReturnedInvalidDataError,
|
||||
message: ActionsReturnedInvalidDataError.message(String(e)),
|
||||
hint
|
||||
});
|
||||
}
|
||||
return {
|
||||
type: "data",
|
||||
status: 200,
|
||||
contentType: "application/json+devalue",
|
||||
body
|
||||
};
|
||||
}
|
||||
function deserializeActionResult(res) {
|
||||
if (res.type === "error") {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(res.body);
|
||||
} catch {
|
||||
return {
|
||||
data: void 0,
|
||||
error: new ActionError({
|
||||
message: res.body,
|
||||
code: "INTERNAL_SERVER_ERROR"
|
||||
})
|
||||
};
|
||||
}
|
||||
if (Object.assign(__vite_import_meta_env__, { _: process.env._ })?.PROD) {
|
||||
return { error: ActionError.fromJson(json), data: void 0 };
|
||||
} else {
|
||||
const error = ActionError.fromJson(json);
|
||||
error.stack = actionResultErrorStack.get();
|
||||
return {
|
||||
error,
|
||||
data: void 0
|
||||
};
|
||||
}
|
||||
}
|
||||
if (res.type === "empty") {
|
||||
return { data: void 0, error: void 0 };
|
||||
}
|
||||
return {
|
||||
data: parse(res.body, {
|
||||
URL: (href) => new URL(href)
|
||||
}),
|
||||
error: void 0
|
||||
};
|
||||
}
|
||||
const actionResultErrorStack = /* @__PURE__ */ (function actionResultErrorStackFn() {
|
||||
let errorStack;
|
||||
return {
|
||||
set(stack) {
|
||||
errorStack = stack;
|
||||
},
|
||||
get() {
|
||||
return errorStack;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
function template({
|
||||
title,
|
||||
pathname,
|
||||
statusCode = 404,
|
||||
tabTitle,
|
||||
body
|
||||
}) {
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>${tabTitle}</title>
|
||||
<style>
|
||||
:root {
|
||||
--gray-10: hsl(258, 7%, 10%);
|
||||
--gray-20: hsl(258, 7%, 20%);
|
||||
--gray-30: hsl(258, 7%, 30%);
|
||||
--gray-40: hsl(258, 7%, 40%);
|
||||
--gray-50: hsl(258, 7%, 50%);
|
||||
--gray-60: hsl(258, 7%, 60%);
|
||||
--gray-70: hsl(258, 7%, 70%);
|
||||
--gray-80: hsl(258, 7%, 80%);
|
||||
--gray-90: hsl(258, 7%, 90%);
|
||||
--black: #13151A;
|
||||
--accent-light: #E0CCFA;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
background: var(--black);
|
||||
color-scheme: dark;
|
||||
accent-color: var(--accent-light);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--gray-10);
|
||||
color: var(--gray-80);
|
||||
font-family: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 8px;
|
||||
color: white;
|
||||
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-weight: 700;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.statusCode {
|
||||
color: var(--accent-light);
|
||||
}
|
||||
|
||||
.astro-icon {
|
||||
height: 124px;
|
||||
width: 124px;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
padding: 2px 8px;
|
||||
background: rgba(0,0,0, 0.25);
|
||||
border: 1px solid rgba(255,255,255, 0.25);
|
||||
border-radius: 4px;
|
||||
font-size: 1.2em;
|
||||
margin-top: 0;
|
||||
max-width: 60em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="center">
|
||||
<svg class="astro-icon" xmlns="http://www.w3.org/2000/svg" width="64" height="80" viewBox="0 0 64 80" fill="none"> <path d="M20.5253 67.6322C16.9291 64.3531 15.8793 57.4632 17.3776 52.4717C19.9755 55.6188 23.575 56.6157 27.3035 57.1784C33.0594 58.0468 38.7122 57.722 44.0592 55.0977C44.6709 54.7972 45.2362 54.3978 45.9045 53.9931C46.4062 55.4451 46.5368 56.9109 46.3616 58.4028C45.9355 62.0362 44.1228 64.8429 41.2397 66.9705C40.0868 67.8215 38.8669 68.5822 37.6762 69.3846C34.0181 71.8508 33.0285 74.7426 34.403 78.9491C34.4357 79.0516 34.4649 79.1541 34.5388 79.4042C32.6711 78.5705 31.3069 77.3565 30.2674 75.7604C29.1694 74.0757 28.6471 72.2121 28.6196 70.1957C28.6059 69.2144 28.6059 68.2244 28.4736 67.257C28.1506 64.8985 27.0406 63.8425 24.9496 63.7817C22.8036 63.7192 21.106 65.0426 20.6559 67.1268C20.6215 67.2865 20.5717 67.4446 20.5218 67.6304L20.5253 67.6322Z" fill="white"/> <path d="M20.5253 67.6322C16.9291 64.3531 15.8793 57.4632 17.3776 52.4717C19.9755 55.6188 23.575 56.6157 27.3035 57.1784C33.0594 58.0468 38.7122 57.722 44.0592 55.0977C44.6709 54.7972 45.2362 54.3978 45.9045 53.9931C46.4062 55.4451 46.5368 56.9109 46.3616 58.4028C45.9355 62.0362 44.1228 64.8429 41.2397 66.9705C40.0868 67.8215 38.8669 68.5822 37.6762 69.3846C34.0181 71.8508 33.0285 74.7426 34.403 78.9491C34.4357 79.0516 34.4649 79.1541 34.5388 79.4042C32.6711 78.5705 31.3069 77.3565 30.2674 75.7604C29.1694 74.0757 28.6471 72.2121 28.6196 70.1957C28.6059 69.2144 28.6059 68.2244 28.4736 67.257C28.1506 64.8985 27.0406 63.8425 24.9496 63.7817C22.8036 63.7192 21.106 65.0426 20.6559 67.1268C20.6215 67.2865 20.5717 67.4446 20.5218 67.6304L20.5253 67.6322Z" fill="url(#paint0_linear_738_686)"/> <path d="M0 51.6401C0 51.6401 10.6488 46.4654 21.3274 46.4654L29.3786 21.6102C29.6801 20.4082 30.5602 19.5913 31.5538 19.5913C32.5474 19.5913 33.4275 20.4082 33.7289 21.6102L41.7802 46.4654C54.4274 46.4654 63.1076 51.6401 63.1076 51.6401C63.1076 51.6401 45.0197 2.48776 44.9843 2.38914C44.4652 0.935933 43.5888 0 42.4073 0H20.7022C19.5206 0 18.6796 0.935933 18.1251 2.38914C18.086 2.4859 0 51.6401 0 51.6401Z" fill="white"/> <defs> <linearGradient id="paint0_linear_738_686" x1="31.554" y1="75.4423" x2="39.7462" y2="48.376" gradientUnits="userSpaceOnUse"> <stop stop-color="#D83333"/> <stop offset="1" stop-color="#F041FF"/> </linearGradient> </defs> </svg>
|
||||
<h1>${statusCode ? `<span class="statusCode">${statusCode}: </span> ` : ""}<span class="statusMessage">${title}</span></h1>
|
||||
${body || `
|
||||
<pre>Path: ${escape(pathname)}</pre>
|
||||
`}
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
const DEFAULT_404_ROUTE = {
|
||||
component: DEFAULT_404_COMPONENT,
|
||||
generate: () => "",
|
||||
params: [],
|
||||
pattern: /^\/404\/?$/,
|
||||
prerender: false,
|
||||
pathname: "/404",
|
||||
segments: [[{ content: "404", dynamic: false, spread: false }]],
|
||||
type: "page",
|
||||
route: "/404",
|
||||
fallbackRoutes: [],
|
||||
isIndex: false,
|
||||
origin: "internal"
|
||||
};
|
||||
function ensure404Route(manifest) {
|
||||
if (!manifest.routes.some((route) => route.route === "/404")) {
|
||||
manifest.routes.push(DEFAULT_404_ROUTE);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
async function default404Page({ pathname }) {
|
||||
return new Response(
|
||||
template({
|
||||
statusCode: 404,
|
||||
title: "Not found",
|
||||
tabTitle: "404: Not Found",
|
||||
pathname
|
||||
}),
|
||||
{ status: 404, headers: { "Content-Type": "text/html" } }
|
||||
);
|
||||
}
|
||||
default404Page.isAstroComponentFactory = true;
|
||||
const default404Instance = {
|
||||
default: default404Page
|
||||
};
|
||||
|
||||
export { ActionError as A, DEFAULT_404_ROUTE as D, NOOP_MIDDLEWARE_FN as N, ACTION_RPC_ROUTE_PATTERN as a, ACTION_QUERY_PARAMS as b, default404Instance as c, deserializeActionResult as d, ensure404Route as e, getActionQueryString as g, serializeActionResult as s };
|
||||
2840
dist/server/chunks/astro/server_CF97kUu8.mjs
vendored
2840
dist/server/chunks/astro/server_CF97kUu8.mjs
vendored
File diff suppressed because it is too large
Load Diff
157
dist/server/chunks/fs-lite_COtHaKzy.mjs
vendored
157
dist/server/chunks/fs-lite_COtHaKzy.mjs
vendored
@@ -1,157 +0,0 @@
|
||||
import { promises, existsSync } from 'node:fs';
|
||||
import { resolve, dirname, join } from 'node:path';
|
||||
|
||||
function defineDriver(factory) {
|
||||
return factory;
|
||||
}
|
||||
function createError(driver, message, opts) {
|
||||
const err = new Error(`[unstorage] [${driver}] ${message}`, opts);
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(err, createError);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
function createRequiredError(driver, name) {
|
||||
if (Array.isArray(name)) {
|
||||
return createError(
|
||||
driver,
|
||||
`Missing some of the required options ${name.map((n) => "`" + n + "`").join(", ")}`
|
||||
);
|
||||
}
|
||||
return createError(driver, `Missing required option \`${name}\`.`);
|
||||
}
|
||||
|
||||
function ignoreNotfound(err) {
|
||||
return err.code === "ENOENT" || err.code === "EISDIR" ? null : err;
|
||||
}
|
||||
function ignoreExists(err) {
|
||||
return err.code === "EEXIST" ? null : err;
|
||||
}
|
||||
async function writeFile(path, data, encoding) {
|
||||
await ensuredir(dirname(path));
|
||||
return promises.writeFile(path, data, encoding);
|
||||
}
|
||||
function readFile(path, encoding) {
|
||||
return promises.readFile(path, encoding).catch(ignoreNotfound);
|
||||
}
|
||||
function unlink(path) {
|
||||
return promises.unlink(path).catch(ignoreNotfound);
|
||||
}
|
||||
function readdir(dir) {
|
||||
return promises.readdir(dir, { withFileTypes: true }).catch(ignoreNotfound).then((r) => r || []);
|
||||
}
|
||||
async function ensuredir(dir) {
|
||||
if (existsSync(dir)) {
|
||||
return;
|
||||
}
|
||||
await ensuredir(dirname(dir)).catch(ignoreExists);
|
||||
await promises.mkdir(dir).catch(ignoreExists);
|
||||
}
|
||||
async function readdirRecursive(dir, ignore, maxDepth) {
|
||||
if (ignore && ignore(dir)) {
|
||||
return [];
|
||||
}
|
||||
const entries = await readdir(dir);
|
||||
const files = [];
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (maxDepth === void 0 || maxDepth > 0) {
|
||||
const dirFiles = await readdirRecursive(
|
||||
entryPath,
|
||||
ignore,
|
||||
maxDepth === void 0 ? void 0 : maxDepth - 1
|
||||
);
|
||||
files.push(...dirFiles.map((f) => entry.name + "/" + f));
|
||||
}
|
||||
} else {
|
||||
if (!(ignore && ignore(entry.name))) {
|
||||
files.push(entry.name);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
return files;
|
||||
}
|
||||
async function rmRecursive(dir) {
|
||||
const entries = await readdir(dir);
|
||||
await Promise.all(
|
||||
entries.map((entry) => {
|
||||
const entryPath = resolve(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return rmRecursive(entryPath).then(() => promises.rmdir(entryPath));
|
||||
} else {
|
||||
return promises.unlink(entryPath);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const PATH_TRAVERSE_RE = /\.\.:|\.\.$/;
|
||||
const DRIVER_NAME = "fs-lite";
|
||||
const fsLite = defineDriver((opts = {}) => {
|
||||
if (!opts.base) {
|
||||
throw createRequiredError(DRIVER_NAME, "base");
|
||||
}
|
||||
opts.base = resolve(opts.base);
|
||||
const r = (key) => {
|
||||
if (PATH_TRAVERSE_RE.test(key)) {
|
||||
throw createError(
|
||||
DRIVER_NAME,
|
||||
`Invalid key: ${JSON.stringify(key)}. It should not contain .. segments`
|
||||
);
|
||||
}
|
||||
const resolved = join(opts.base, key.replace(/:/g, "/"));
|
||||
return resolved;
|
||||
};
|
||||
return {
|
||||
name: DRIVER_NAME,
|
||||
options: opts,
|
||||
flags: {
|
||||
maxDepth: true
|
||||
},
|
||||
hasItem(key) {
|
||||
return existsSync(r(key));
|
||||
},
|
||||
getItem(key) {
|
||||
return readFile(r(key), "utf8");
|
||||
},
|
||||
getItemRaw(key) {
|
||||
return readFile(r(key));
|
||||
},
|
||||
async getMeta(key) {
|
||||
const { atime, mtime, size, birthtime, ctime } = await promises.stat(r(key)).catch(() => ({}));
|
||||
return { atime, mtime, size, birthtime, ctime };
|
||||
},
|
||||
setItem(key, value) {
|
||||
if (opts.readOnly) {
|
||||
return;
|
||||
}
|
||||
return writeFile(r(key), value, "utf8");
|
||||
},
|
||||
setItemRaw(key, value) {
|
||||
if (opts.readOnly) {
|
||||
return;
|
||||
}
|
||||
return writeFile(r(key), value);
|
||||
},
|
||||
removeItem(key) {
|
||||
if (opts.readOnly) {
|
||||
return;
|
||||
}
|
||||
return unlink(r(key));
|
||||
},
|
||||
getKeys(_base, topts) {
|
||||
return readdirRecursive(r("."), opts.ignore, topts?.maxDepth);
|
||||
},
|
||||
async clear() {
|
||||
if (opts.readOnly || opts.noClear) {
|
||||
return;
|
||||
}
|
||||
await rmRecursive(r("."));
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export { fsLite as default };
|
||||
441
dist/server/chunks/models_BK7lP4G3.mjs
vendored
441
dist/server/chunks/models_BK7lP4G3.mjs
vendored
File diff suppressed because one or more lines are too long
1896
dist/server/chunks/node_HH9e2ntY.mjs
vendored
1896
dist/server/chunks/node_HH9e2ntY.mjs
vendored
File diff suppressed because it is too large
Load Diff
188
dist/server/chunks/remote_B3W5fv4r.mjs
vendored
188
dist/server/chunks/remote_B3W5fv4r.mjs
vendored
@@ -1,188 +0,0 @@
|
||||
function appendForwardSlash(path) {
|
||||
return path.endsWith("/") ? path : path + "/";
|
||||
}
|
||||
function prependForwardSlash(path) {
|
||||
return path[0] === "/" ? path : "/" + path;
|
||||
}
|
||||
const MANY_TRAILING_SLASHES = /\/{2,}$/g;
|
||||
function collapseDuplicateTrailingSlashes(path, trailingSlash) {
|
||||
if (!path) {
|
||||
return path;
|
||||
}
|
||||
return path.replace(MANY_TRAILING_SLASHES, trailingSlash ? "/" : "") || "/";
|
||||
}
|
||||
function removeTrailingForwardSlash(path) {
|
||||
return path.endsWith("/") ? path.slice(0, path.length - 1) : path;
|
||||
}
|
||||
function removeLeadingForwardSlash(path) {
|
||||
return path.startsWith("/") ? path.substring(1) : path;
|
||||
}
|
||||
function trimSlashes(path) {
|
||||
return path.replace(/^\/|\/$/g, "");
|
||||
}
|
||||
function isString(path) {
|
||||
return typeof path === "string" || path instanceof String;
|
||||
}
|
||||
const INTERNAL_PREFIXES = /* @__PURE__ */ new Set(["/_", "/@", "/.", "//"]);
|
||||
const JUST_SLASHES = /^\/{2,}$/;
|
||||
function isInternalPath(path) {
|
||||
return INTERNAL_PREFIXES.has(path.slice(0, 2)) && !JUST_SLASHES.test(path);
|
||||
}
|
||||
function joinPaths(...paths) {
|
||||
return paths.filter(isString).map((path, i) => {
|
||||
if (i === 0) {
|
||||
return removeTrailingForwardSlash(path);
|
||||
} else if (i === paths.length - 1) {
|
||||
return removeLeadingForwardSlash(path);
|
||||
} else {
|
||||
return trimSlashes(path);
|
||||
}
|
||||
}).join("/");
|
||||
}
|
||||
function removeQueryString(path) {
|
||||
const index = path.lastIndexOf("?");
|
||||
return index > 0 ? path.substring(0, index) : path;
|
||||
}
|
||||
function isRemotePath(src) {
|
||||
if (!src) return false;
|
||||
const trimmed = src.trim();
|
||||
if (!trimmed) return false;
|
||||
let decoded = trimmed;
|
||||
let previousDecoded = "";
|
||||
let maxIterations = 10;
|
||||
while (decoded !== previousDecoded && maxIterations > 0) {
|
||||
previousDecoded = decoded;
|
||||
try {
|
||||
decoded = decodeURIComponent(decoded);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
maxIterations--;
|
||||
}
|
||||
if (/^[a-zA-Z]:/.test(decoded)) {
|
||||
return false;
|
||||
}
|
||||
if (decoded[0] === "/" && decoded[1] !== "/" && decoded[1] !== "\\") {
|
||||
return false;
|
||||
}
|
||||
if (decoded[0] === "\\") {
|
||||
return true;
|
||||
}
|
||||
if (decoded.startsWith("//")) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const url = new URL(decoded, "http://n");
|
||||
if (url.username || url.password) {
|
||||
return true;
|
||||
}
|
||||
if (decoded.includes("@") && !url.pathname.includes("@") && !url.search.includes("@")) {
|
||||
return true;
|
||||
}
|
||||
if (url.origin !== "http://n") {
|
||||
const protocol = url.protocol.toLowerCase();
|
||||
if (protocol === "file:") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (URL.canParse(decoded)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
function isParentDirectory(parentPath, childPath) {
|
||||
if (!parentPath || !childPath) {
|
||||
return false;
|
||||
}
|
||||
if (parentPath.includes("://") || childPath.includes("://")) {
|
||||
return false;
|
||||
}
|
||||
if (isRemotePath(parentPath) || isRemotePath(childPath)) {
|
||||
return false;
|
||||
}
|
||||
if (parentPath.includes("..") || childPath.includes("..")) {
|
||||
return false;
|
||||
}
|
||||
if (parentPath.includes("\0") || childPath.includes("\0")) {
|
||||
return false;
|
||||
}
|
||||
const normalizedParent = appendForwardSlash(slash(parentPath).toLowerCase());
|
||||
const normalizedChild = slash(childPath).toLowerCase();
|
||||
if (normalizedParent === normalizedChild || normalizedParent === normalizedChild + "/") {
|
||||
return false;
|
||||
}
|
||||
return normalizedChild.startsWith(normalizedParent);
|
||||
}
|
||||
function slash(path) {
|
||||
return path.replace(/\\/g, "/");
|
||||
}
|
||||
function fileExtension(path) {
|
||||
const ext = path.split(".").pop();
|
||||
return ext !== path ? `.${ext}` : "";
|
||||
}
|
||||
const WITH_FILE_EXT = /\/[^/]+\.\w+$/;
|
||||
function hasFileExtension(path) {
|
||||
return WITH_FILE_EXT.test(path);
|
||||
}
|
||||
|
||||
function matchPattern(url, remotePattern) {
|
||||
return matchProtocol(url, remotePattern.protocol) && matchHostname(url, remotePattern.hostname, true) && matchPort(url, remotePattern.port) && matchPathname(url, remotePattern.pathname, true);
|
||||
}
|
||||
function matchPort(url, port) {
|
||||
return !port || port === url.port;
|
||||
}
|
||||
function matchProtocol(url, protocol) {
|
||||
return !protocol || protocol === url.protocol.slice(0, -1);
|
||||
}
|
||||
function matchHostname(url, hostname, allowWildcard = false) {
|
||||
if (!hostname) {
|
||||
return true;
|
||||
} else if (!allowWildcard || !hostname.startsWith("*")) {
|
||||
return hostname === url.hostname;
|
||||
} else if (hostname.startsWith("**.")) {
|
||||
const slicedHostname = hostname.slice(2);
|
||||
return slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname);
|
||||
} else if (hostname.startsWith("*.")) {
|
||||
const slicedHostname = hostname.slice(1);
|
||||
if (!url.hostname.endsWith(slicedHostname)) {
|
||||
return false;
|
||||
}
|
||||
const subdomainWithDot = url.hostname.slice(0, -(slicedHostname.length - 1));
|
||||
return subdomainWithDot.endsWith(".") && !subdomainWithDot.slice(0, -1).includes(".");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function matchPathname(url, pathname, allowWildcard = false) {
|
||||
if (!pathname) {
|
||||
return true;
|
||||
} else if (!allowWildcard || !pathname.endsWith("*")) {
|
||||
return pathname === url.pathname;
|
||||
} else if (pathname.endsWith("/**")) {
|
||||
const slicedPathname = pathname.slice(0, -2);
|
||||
return slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname);
|
||||
} else if (pathname.endsWith("/*")) {
|
||||
const slicedPathname = pathname.slice(0, -1);
|
||||
const additionalPathChunks = url.pathname.replace(slicedPathname, "").split("/").filter(Boolean);
|
||||
return additionalPathChunks.length === 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isRemoteAllowed(src, {
|
||||
domains,
|
||||
remotePatterns
|
||||
}) {
|
||||
if (!URL.canParse(src)) {
|
||||
return false;
|
||||
}
|
||||
const url = new URL(src);
|
||||
if (!["http:", "https:", "data:"].includes(url.protocol)) {
|
||||
return false;
|
||||
}
|
||||
return domains.some((domain) => matchHostname(url, domain)) || remotePatterns.some((remotePattern) => matchPattern(url, remotePattern));
|
||||
}
|
||||
|
||||
export { isRemotePath as a, isParentDirectory as b, appendForwardSlash as c, removeTrailingForwardSlash as d, isInternalPath as e, fileExtension as f, collapseDuplicateTrailingSlashes as g, hasFileExtension as h, isRemoteAllowed as i, joinPaths as j, matchPattern as m, prependForwardSlash as p, removeQueryString as r, slash as s, trimSlashes as t };
|
||||
101
dist/server/chunks/sharp_D9uxjd11.mjs
vendored
101
dist/server/chunks/sharp_D9uxjd11.mjs
vendored
@@ -1,101 +0,0 @@
|
||||
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 = {
|
||||
low: 25,
|
||||
mid: 50,
|
||||
high: 80,
|
||||
max: 100
|
||||
};
|
||||
async function loadSharp() {
|
||||
let sharpImport;
|
||||
try {
|
||||
sharpImport = (await import('sharp')).default;
|
||||
} catch {
|
||||
throw new AstroError(MissingSharp);
|
||||
}
|
||||
sharpImport.cache(false);
|
||||
return sharpImport;
|
||||
}
|
||||
const fitMap = {
|
||||
fill: "fill",
|
||||
contain: "inside",
|
||||
cover: "cover",
|
||||
none: "outside",
|
||||
"scale-down": "inside",
|
||||
outside: "outside",
|
||||
inside: "inside"
|
||||
};
|
||||
const sharpService = {
|
||||
validateOptions: baseService.validateOptions,
|
||||
getURL: baseService.getURL,
|
||||
parseURL: baseService.parseURL,
|
||||
getHTMLAttributes: baseService.getHTMLAttributes,
|
||||
getSrcSet: baseService.getSrcSet,
|
||||
async transform(inputBuffer, transformOptions, config) {
|
||||
if (!sharp) sharp = await loadSharp();
|
||||
const transform = transformOptions;
|
||||
const kernel = config.service.config.kernel;
|
||||
if (transform.format === "svg") return { data: inputBuffer, format: "svg" };
|
||||
const result = sharp(inputBuffer, {
|
||||
failOnError: false,
|
||||
pages: -1,
|
||||
limitInputPixels: config.service.config.limitInputPixels
|
||||
});
|
||||
result.rotate();
|
||||
const { format } = await result.metadata();
|
||||
const withoutEnlargement = Boolean(transform.fit);
|
||||
if (transform.width && transform.height && transform.fit) {
|
||||
const fit = fitMap[transform.fit] ?? "inside";
|
||||
result.resize({
|
||||
width: Math.round(transform.width),
|
||||
height: Math.round(transform.height),
|
||||
kernel,
|
||||
fit,
|
||||
position: transform.position,
|
||||
withoutEnlargement
|
||||
});
|
||||
} else if (transform.height && !transform.width) {
|
||||
result.resize({
|
||||
height: Math.round(transform.height),
|
||||
kernel,
|
||||
withoutEnlargement
|
||||
});
|
||||
} else if (transform.width) {
|
||||
result.resize({
|
||||
width: Math.round(transform.width),
|
||||
kernel,
|
||||
withoutEnlargement
|
||||
});
|
||||
}
|
||||
if (transform.background) {
|
||||
result.flatten({ background: transform.background });
|
||||
}
|
||||
if (transform.format) {
|
||||
let quality = void 0;
|
||||
if (transform.quality) {
|
||||
const parsedQuality = parseQuality(transform.quality);
|
||||
if (typeof parsedQuality === "number") {
|
||||
quality = parsedQuality;
|
||||
} else {
|
||||
quality = transform.quality in qualityTable ? qualityTable[transform.quality] : void 0;
|
||||
}
|
||||
}
|
||||
if (transform.format === "webp" && format === "gif") {
|
||||
result.webp({ quality: typeof quality === "number" ? quality : void 0, loop: 0 });
|
||||
} else {
|
||||
result.toFormat(transform.format, { quality });
|
||||
}
|
||||
}
|
||||
const { data, info } = await result.toBuffer({ resolveWithObject: true });
|
||||
const needsCopy = "buffer" in data && data.buffer instanceof SharedArrayBuffer;
|
||||
return {
|
||||
data: needsCopy ? new Uint8Array(data) : data,
|
||||
format: info.format
|
||||
};
|
||||
}
|
||||
};
|
||||
var sharp_default = sharpService;
|
||||
|
||||
export { sharp_default as default };
|
||||
109
dist/server/chunks/skills_BacVQUiS.mjs
vendored
109
dist/server/chunks/skills_BacVQUiS.mjs
vendored
@@ -1,109 +0,0 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import matter from 'gray-matter';
|
||||
|
||||
const SKILLS_DIR = path.resolve(
|
||||
process.env.SKILLS_DIR || "data/skills"
|
||||
);
|
||||
const SLUG_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
||||
const MAX_SLUG_LENGTH = 64;
|
||||
function isValidSlug(slug) {
|
||||
return slug.length >= 2 && slug.length <= MAX_SLUG_LENGTH && SLUG_RE.test(slug);
|
||||
}
|
||||
function skillPath(slug) {
|
||||
return path.join(SKILLS_DIR, `${slug}.md`);
|
||||
}
|
||||
function parseTools(val) {
|
||||
if (Array.isArray(val)) return val.map(String);
|
||||
if (typeof val === "string") return val.split(",").map((t) => t.trim()).filter(Boolean);
|
||||
return [];
|
||||
}
|
||||
function parseSkill(slug, raw) {
|
||||
const { data, content } = matter(raw);
|
||||
return {
|
||||
slug,
|
||||
name: data.name || slug,
|
||||
description: data.description || "",
|
||||
"allowed-tools": parseTools(data["allowed-tools"] ?? data.allowedTools),
|
||||
"argument-hint": data["argument-hint"] || "",
|
||||
model: data.model || "",
|
||||
"user-invocable": data["user-invocable"] !== false,
|
||||
"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
|
||||
};
|
||||
}
|
||||
async function listSkills() {
|
||||
await fs.mkdir(SKILLS_DIR, { recursive: true });
|
||||
const files = await fs.readdir(SKILLS_DIR);
|
||||
const skills = [];
|
||||
for (const file of files) {
|
||||
if (!file.endsWith(".md")) continue;
|
||||
const slug = file.replace(/\.md$/, "");
|
||||
const raw = await fs.readFile(path.join(SKILLS_DIR, file), "utf-8");
|
||||
const { content: _, raw: __, ...meta } = parseSkill(slug, raw);
|
||||
skills.push(meta);
|
||||
}
|
||||
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");
|
||||
return parseSkill(slug, raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function createSkill(slug, content) {
|
||||
if (!isValidSlug(slug)) {
|
||||
throw new Error(`Invalid slug: ${slug}`);
|
||||
}
|
||||
await fs.mkdir(SKILLS_DIR, { recursive: true });
|
||||
const dest = skillPath(slug);
|
||||
try {
|
||||
await fs.access(dest);
|
||||
throw new Error(`Skill already exists: ${slug}`);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.startsWith("Skill already exists")) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
await fs.writeFile(dest, content, "utf-8");
|
||||
return parseSkill(slug, content);
|
||||
}
|
||||
async function updateSkill(slug, content) {
|
||||
const dest = skillPath(slug);
|
||||
try {
|
||||
await fs.access(dest);
|
||||
} catch {
|
||||
throw new Error(`Skill not found: ${slug}`);
|
||||
}
|
||||
await fs.writeFile(dest, content, "utf-8");
|
||||
return parseSkill(slug, content);
|
||||
}
|
||||
async function deleteSkill(slug) {
|
||||
const dest = skillPath(slug);
|
||||
try {
|
||||
await fs.access(dest);
|
||||
} catch {
|
||||
throw new Error(`Skill not found: ${slug}`);
|
||||
}
|
||||
await fs.unlink(dest);
|
||||
}
|
||||
|
||||
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
46
dist/server/chunks/stats_CaDi9y9J.mjs
vendored
@@ -1,46 +0,0 @@
|
||||
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
309
dist/server/chunks/sync_BEq_wzpT.mjs
vendored
@@ -1,309 +0,0 @@
|
||||
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 };
|
||||
55
dist/server/chunks/tokens_CAzj9Aj8.mjs
vendored
55
dist/server/chunks/tokens_CAzj9Aj8.mjs
vendored
@@ -1,55 +0,0 @@
|
||||
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 };
|
||||
69
dist/server/entry.mjs
vendored
69
dist/server/entry.mjs
vendored
@@ -1,69 +0,0 @@
|
||||
import { renderers } from './renderers.mjs';
|
||||
import { c as createExports, s as serverEntrypointModule } from './chunks/_@astrojs-ssr-adapter_BeL8VyJ8.mjs';
|
||||
import { manifest } from './manifest_Bz0Ba_R4.mjs';
|
||||
|
||||
const serverIslandMap = new Map();;
|
||||
|
||||
const _page0 = () => import('./pages/_image.astro.mjs');
|
||||
const _page1 = () => import('./pages/api/auth/register.astro.mjs');
|
||||
const _page2 = () => import('./pages/api/auth/verify.astro.mjs');
|
||||
const _page3 = () => import('./pages/api/skills/_slug_.astro.mjs');
|
||||
const _page4 = () => import('./pages/api/skills.astro.mjs');
|
||||
const _page5 = () => import('./pages/api/sync/project.astro.mjs');
|
||||
const _page6 = () => import('./pages/api/sync.astro.mjs');
|
||||
const _page7 = () => import('./pages/gi.astro.mjs');
|
||||
const _page8 = () => import('./pages/gp.astro.mjs');
|
||||
const _page9 = () => import('./pages/i.astro.mjs');
|
||||
const _page10 = () => import('./pages/new.astro.mjs');
|
||||
const _page11 = () => import('./pages/p.astro.mjs');
|
||||
const _page12 = () => import('./pages/_slug_/edit.astro.mjs');
|
||||
const _page13 = () => import('./pages/_slug_/gi.astro.mjs');
|
||||
const _page14 = () => import('./pages/_slug_/i.astro.mjs');
|
||||
const _page15 = () => import('./pages/_slug_.astro.mjs');
|
||||
const _page16 = () => import('./pages/index.astro.mjs');
|
||||
const pageMap = new Map([
|
||||
["node_modules/astro/dist/assets/endpoint/node.js", _page0],
|
||||
["src/pages/api/auth/register.ts", _page1],
|
||||
["src/pages/api/auth/verify.ts", _page2],
|
||||
["src/pages/api/skills/[slug].ts", _page3],
|
||||
["src/pages/api/skills/index.ts", _page4],
|
||||
["src/pages/api/sync/project.ts", _page5],
|
||||
["src/pages/api/sync/index.ts", _page6],
|
||||
["src/pages/gi.ts", _page7],
|
||||
["src/pages/gp.ts", _page8],
|
||||
["src/pages/i.ts", _page9],
|
||||
["src/pages/new.astro", _page10],
|
||||
["src/pages/p.ts", _page11],
|
||||
["src/pages/[slug]/edit.astro", _page12],
|
||||
["src/pages/[slug]/gi.ts", _page13],
|
||||
["src/pages/[slug]/i.ts", _page14],
|
||||
["src/pages/[slug].astro", _page15],
|
||||
["src/pages/index.astro", _page16]
|
||||
]);
|
||||
|
||||
const _manifest = Object.assign(manifest, {
|
||||
pageMap,
|
||||
serverIslandMap,
|
||||
renderers,
|
||||
actions: () => import('./noop-entrypoint.mjs'),
|
||||
middleware: () => import('./_noop-middleware.mjs')
|
||||
});
|
||||
const _args = {
|
||||
"mode": "standalone",
|
||||
"client": "file:///Users/alex/projects/skillit/dist/client/",
|
||||
"server": "file:///Users/alex/projects/skillit/dist/server/",
|
||||
"host": false,
|
||||
"port": 4321,
|
||||
"assets": "_astro",
|
||||
"experimentalStaticHeaders": false
|
||||
};
|
||||
const _exports = createExports(_manifest, _args);
|
||||
const handler = _exports['handler'];
|
||||
const startServer = _exports['startServer'];
|
||||
const options = _exports['options'];
|
||||
const _start = 'start';
|
||||
if (Object.prototype.hasOwnProperty.call(serverEntrypointModule, _start)) {
|
||||
serverEntrypointModule[_start](_manifest, _args);
|
||||
}
|
||||
|
||||
export { handler, options, pageMap, startServer };
|
||||
101
dist/server/manifest_Bz0Ba_R4.mjs
vendored
101
dist/server/manifest_Bz0Ba_R4.mjs
vendored
File diff suppressed because one or more lines are too long
3
dist/server/noop-entrypoint.mjs
vendored
3
dist/server/noop-entrypoint.mjs
vendored
@@ -1,3 +0,0 @@
|
||||
const server = {};
|
||||
|
||||
export { server };
|
||||
2
dist/server/pages/_image.astro.mjs
vendored
2
dist/server/pages/_image.astro.mjs
vendored
@@ -1,2 +0,0 @@
|
||||
export { a as page } from '../chunks/node_HH9e2ntY.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
292
dist/server/pages/_slug_.astro.mjs
vendored
292
dist/server/pages/_slug_.astro.mjs
vendored
@@ -1,292 +0,0 @@
|
||||
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_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: {},
|
||||
authorEmail: {},
|
||||
authorName: {},
|
||||
authorHasToken: { type: Boolean }
|
||||
},
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
const props = __props;
|
||||
const deleting = ref(false);
|
||||
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 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) {
|
||||
const data = await res.json().catch(() => ({ error: "Failed to delete" }));
|
||||
throw new Error(data.error || "Failed to delete");
|
||||
}
|
||||
window.location.href = "/";
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : "Failed to delete skill.";
|
||||
deleting.value = false;
|
||||
}
|
||||
}
|
||||
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(`<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) => {
|
||||
const ssrContext = useSSRContext();
|
||||
(ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("src/components/DeleteButton.vue");
|
||||
return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
|
||||
};
|
||||
const DeleteButton = /* @__PURE__ */ _export_sfc(_sfc_main, [["ssrRender", _sfc_ssrRender]]);
|
||||
|
||||
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;
|
||||
const { slug } = Astro2.params;
|
||||
const skill = await getSkill(slug);
|
||||
if (!skill) {
|
||||
return Astro2.redirect("/");
|
||||
}
|
||||
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 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 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";
|
||||
const $$url = "/[slug]";
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
default: $$slug,
|
||||
file: $$file,
|
||||
url: $$url
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
38
dist/server/pages/_slug_/edit.astro.mjs
vendored
38
dist/server/pages/_slug_/edit.astro.mjs
vendored
@@ -1,38 +0,0 @@
|
||||
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_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("https://skills.here.run.place");
|
||||
const $$Edit = createComponent(async ($$result, $$props, $$slots) => {
|
||||
const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
|
||||
Astro2.self = $$Edit;
|
||||
const { slug } = Astro2.params;
|
||||
const skill = await getSkill(slug);
|
||||
if (!skill) {
|
||||
return Astro2.redirect("/");
|
||||
}
|
||||
const availableTools = await getAvailableTools();
|
||||
const availableModels = await getAvailableModels();
|
||||
const allowedTools = skill["allowed-tools"].join(", ");
|
||||
const hooksJson = skill.hooks ? JSON.stringify(skill.hooks, null, 2) : "";
|
||||
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";
|
||||
const $$url = "/[slug]/edit";
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
default: $$Edit,
|
||||
file: $$file,
|
||||
url: $$url
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
40
dist/server/pages/_slug_/gi.astro.mjs
vendored
40
dist/server/pages/_slug_/gi.astro.mjs
vendored
@@ -1,40 +0,0 @@
|
||||
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
40
dist/server/pages/_slug_/i.astro.mjs
vendored
@@ -1,40 +0,0 @@
|
||||
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
55
dist/server/pages/api/auth/register.astro.mjs
vendored
@@ -1,55 +0,0 @@
|
||||
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
40
dist/server/pages/api/auth/verify.astro.mjs
vendored
@@ -1,40 +0,0 @@
|
||||
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 };
|
||||
78
dist/server/pages/api/skills.astro.mjs
vendored
78
dist/server/pages/api/skills.astro.mjs
vendored
@@ -1,78 +0,0 @@
|
||||
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 () => {
|
||||
const skills = await listSkills();
|
||||
return new Response(JSON.stringify(skills), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
};
|
||||
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 { slug, content } = body;
|
||||
if (!slug || !content) {
|
||||
return new Response(JSON.stringify({ error: "slug and content are required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
if (!isValidSlug(slug)) {
|
||||
return new Response(JSON.stringify({ error: "Invalid slug. Use lowercase alphanumeric and hyphens, 2-64 chars." }), {
|
||||
status: 400,
|
||||
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" }
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
if (message.includes("already exists")) {
|
||||
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,
|
||||
GET,
|
||||
POST
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
102
dist/server/pages/api/skills/_slug_.astro.mjs
vendored
102
dist/server/pages/api/skills/_slug_.astro.mjs
vendored
@@ -1,102 +0,0 @@
|
||||
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 }) => {
|
||||
const skill = await getSkill(params.slug);
|
||||
if (!skill) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
return new Response(skill.raw, {
|
||||
headers: { "Content-Type": "text/markdown; charset=utf-8" }
|
||||
});
|
||||
};
|
||||
const PUT = async ({ params, request }) => {
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: "Invalid JSON" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
if (!body.content) {
|
||||
return new Response(JSON.stringify({ error: "content is required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
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";
|
||||
return new Response(JSON.stringify({ error: message }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
};
|
||||
const DELETE = 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 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" }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
DELETE,
|
||||
GET,
|
||||
PUT
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
18
dist/server/pages/api/sync.astro.mjs
vendored
18
dist/server/pages/api/sync.astro.mjs
vendored
@@ -1,18 +0,0 @@
|
||||
import { 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");
|
||||
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 };
|
||||
18
dist/server/pages/api/sync/project.astro.mjs
vendored
18
dist/server/pages/api/sync/project.astro.mjs
vendored
@@ -1,18 +0,0 @@
|
||||
import { b as buildSyncScript } from '../../../chunks/sync_BEq_wzpT.mjs';
|
||||
export { renderers } from '../../../renderers.mjs';
|
||||
|
||||
const GET = async ({ url }) => {
|
||||
const script = await buildSyncScript(url.origin, ".claude/skills");
|
||||
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 };
|
||||
19
dist/server/pages/gi.astro.mjs
vendored
19
dist/server/pages/gi.astro.mjs
vendored
@@ -1,19 +0,0 @@
|
||||
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, 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" }
|
||||
});
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
GET
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
18
dist/server/pages/gp.astro.mjs
vendored
18
dist/server/pages/gp.astro.mjs
vendored
@@ -1,18 +0,0 @@
|
||||
import { c as buildPushScript } from '../chunks/sync_BEq_wzpT.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const GET = async ({ url }) => {
|
||||
const script = await buildPushScript(url.origin, "$HOME/.claude/skills");
|
||||
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 };
|
||||
18
dist/server/pages/i.astro.mjs
vendored
18
dist/server/pages/i.astro.mjs
vendored
@@ -1,18 +0,0 @@
|
||||
import { b as buildSyncScript } from '../chunks/sync_BEq_wzpT.mjs';
|
||||
export { renderers } from '../renderers.mjs';
|
||||
|
||||
const GET = async ({ url }) => {
|
||||
const script = await buildSyncScript(url.origin, ".claude/skills");
|
||||
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 };
|
||||
332
dist/server/pages/index.astro.mjs
vendored
332
dist/server/pages/index.astro.mjs
vendored
File diff suppressed because one or more lines are too long
37
dist/server/pages/new.astro.mjs
vendored
37
dist/server/pages/new.astro.mjs
vendored
@@ -1,37 +0,0 @@
|
||||
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_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();
|
||||
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";
|
||||
const $$url = "/new";
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
default: $$New,
|
||||
file: $$file,
|
||||
url: $$url
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
19
dist/server/pages/p.astro.mjs
vendored
19
dist/server/pages/p.astro.mjs
vendored
@@ -1,19 +0,0 @@
|
||||
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, 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" }
|
||||
});
|
||||
};
|
||||
|
||||
const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
||||
__proto__: null,
|
||||
GET
|
||||
}, Symbol.toStringTag, { value: 'Module' }));
|
||||
|
||||
const page = () => _page;
|
||||
|
||||
export { page };
|
||||
81
dist/server/renderers.mjs
vendored
81
dist/server/renderers.mjs
vendored
@@ -1,81 +0,0 @@
|
||||
import { defineComponent, h, createSSRApp } from 'vue';
|
||||
import { renderToString } from 'vue/server-renderer';
|
||||
|
||||
const setup = () => {};
|
||||
|
||||
const contexts = /* @__PURE__ */ new WeakMap();
|
||||
const ID_PREFIX = "s";
|
||||
function getContext(rendererContextResult) {
|
||||
if (contexts.has(rendererContextResult)) {
|
||||
return contexts.get(rendererContextResult);
|
||||
}
|
||||
const ctx = {
|
||||
currentIndex: 0,
|
||||
get id() {
|
||||
return ID_PREFIX + this.currentIndex.toString();
|
||||
}
|
||||
};
|
||||
contexts.set(rendererContextResult, ctx);
|
||||
return ctx;
|
||||
}
|
||||
function incrementId(rendererContextResult) {
|
||||
const ctx = getContext(rendererContextResult);
|
||||
const id = ctx.id;
|
||||
ctx.currentIndex++;
|
||||
return id;
|
||||
}
|
||||
|
||||
const StaticHtml = defineComponent({
|
||||
props: {
|
||||
value: String,
|
||||
name: String,
|
||||
hydrate: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
setup({ name, value, hydrate }) {
|
||||
if (!value) return () => null;
|
||||
let tagName = hydrate ? "astro-slot" : "astro-static-slot";
|
||||
return () => h(tagName, { name, innerHTML: value });
|
||||
}
|
||||
});
|
||||
var static_html_default = StaticHtml;
|
||||
|
||||
async function check(Component) {
|
||||
return !!Component["ssrRender"] || !!Component["__ssrInlineRender"];
|
||||
}
|
||||
async function renderToStaticMarkup(Component, inputProps, slotted, metadata) {
|
||||
let prefix;
|
||||
if (this && this.result) {
|
||||
prefix = incrementId(this.result);
|
||||
}
|
||||
const attrs = { prefix };
|
||||
const slots = {};
|
||||
const props = { ...inputProps };
|
||||
delete props.slot;
|
||||
for (const [key, value] of Object.entries(slotted)) {
|
||||
slots[key] = () => h(static_html_default, {
|
||||
value,
|
||||
name: key === "default" ? void 0 : key,
|
||||
// Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
|
||||
hydrate: metadata?.astroStaticSlot ? !!metadata.hydrate : true
|
||||
});
|
||||
}
|
||||
const app = createSSRApp({ render: () => h(Component, props, slots) });
|
||||
app.config.idPrefix = prefix;
|
||||
await setup();
|
||||
const html = await renderToString(app);
|
||||
return { html, attrs };
|
||||
}
|
||||
const renderer = {
|
||||
name: "@astrojs/vue",
|
||||
check,
|
||||
renderToStaticMarkup,
|
||||
supportsAstroStaticSlot: true
|
||||
};
|
||||
var server_default = renderer;
|
||||
|
||||
const renderers = [Object.assign({"name":"@astrojs/vue","clientEntrypoint":"@astrojs/vue/client.js","serverEntrypoint":"@astrojs/vue/server.js"}, { ssr: server_default }),];
|
||||
|
||||
export { renderers };
|
||||
8
node_modules/.vite/deps/_metadata.json
generated
vendored
8
node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"hash": "867abd67",
|
||||
"configHash": "8be89d65",
|
||||
"lockfileHash": "e3b0c442",
|
||||
"browserHash": "88ab680e",
|
||||
"optimized": {},
|
||||
"chunks": {}
|
||||
}
|
||||
3
node_modules/.vite/deps/package.json
generated
vendored
3
node_modules/.vite/deps/package.json
generated
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -27,23 +27,17 @@ function displayName(id: string): string {
|
||||
.replace(/^claude-/, '');
|
||||
|
||||
const parts = clean.split('-');
|
||||
const words = parts.map((p, i) => {
|
||||
if (/^\d+$/.test(p) && i === parts.length - 1 && parts.length > 1) {
|
||||
// Last numeric part after a name: turn "4-6" into "4.6" or "4-5" into "4.5"
|
||||
const prev = parts[i - 1];
|
||||
if (/^\d+$/.test(prev)) return null; // will be joined with prev
|
||||
return p;
|
||||
}
|
||||
return p.charAt(0).toUpperCase() + p.slice(1);
|
||||
}).filter(Boolean);
|
||||
const words = parts.map(p =>
|
||||
/^\d+$/.test(p) ? p : p.charAt(0).toUpperCase() + p.slice(1)
|
||||
);
|
||||
|
||||
// Join consecutive numbers with dots: ["4", "6"] → "4.6"
|
||||
const result: string[] = [];
|
||||
for (const w of words) {
|
||||
if (/^\d+$/.test(w!) && result.length > 0 && /^\d+$/.test(result[result.length - 1])) {
|
||||
if (/^\d+$/.test(w) && result.length > 0 && /^\d+$/.test(result[result.length - 1])) {
|
||||
result[result.length - 1] += '.' + w;
|
||||
} else {
|
||||
result.push(w!);
|
||||
result.push(w);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import matter from 'gray-matter';
|
||||
/**
|
||||
* Backward-compatible wrapper around resources.ts for skills.
|
||||
* All new code should use resources.ts directly.
|
||||
*/
|
||||
import {
|
||||
listResources,
|
||||
getResource,
|
||||
createResource,
|
||||
updateResource,
|
||||
deleteResource,
|
||||
getAllTags as resourceGetAllTags,
|
||||
getForksOf as resourceGetForksOf,
|
||||
isValidSlug,
|
||||
type ResourceMeta,
|
||||
type Resource,
|
||||
type ResourceFormat,
|
||||
type ResourceFileEntry,
|
||||
} from './resources';
|
||||
import { getTypeConfig } from './registry';
|
||||
|
||||
export const SKILLS_DIR = path.resolve(
|
||||
process.env.SKILLS_DIR || 'data/skills'
|
||||
);
|
||||
export { isValidSlug };
|
||||
|
||||
const SLUG_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
||||
const MAX_SLUG_LENGTH = 64;
|
||||
export const SKILLS_DIR = getTypeConfig('skills').dataDir;
|
||||
|
||||
export interface SkillMeta {
|
||||
slug: string;
|
||||
@@ -25,127 +38,81 @@ export interface SkillMeta {
|
||||
'fork-of': string;
|
||||
tags: string[];
|
||||
hooks: Record<string, unknown> | null;
|
||||
format: ResourceFormat;
|
||||
}
|
||||
|
||||
export interface Skill extends SkillMeta {
|
||||
content: string;
|
||||
raw: string;
|
||||
files: ResourceFileEntry[];
|
||||
}
|
||||
|
||||
export function isValidSlug(slug: string): boolean {
|
||||
return slug.length >= 2 && slug.length <= MAX_SLUG_LENGTH && SLUG_RE.test(slug);
|
||||
}
|
||||
|
||||
function skillPath(slug: string): string {
|
||||
return path.join(SKILLS_DIR, `${slug}.md`);
|
||||
}
|
||||
|
||||
function parseTools(val: unknown): string[] {
|
||||
function parseList(val: unknown): string[] {
|
||||
if (Array.isArray(val)) return val.map(String);
|
||||
if (typeof val === 'string') return val.split(',').map(t => t.trim()).filter(Boolean);
|
||||
return [];
|
||||
}
|
||||
|
||||
function parseSkill(slug: string, raw: string): Skill {
|
||||
const { data, content } = matter(raw);
|
||||
function toSkillMeta(r: ResourceMeta): SkillMeta {
|
||||
const f = r.fields;
|
||||
return {
|
||||
slug,
|
||||
name: (data.name as string) || slug,
|
||||
description: (data.description as string) || '',
|
||||
'allowed-tools': parseTools(data['allowed-tools'] ?? data.allowedTools),
|
||||
'argument-hint': (data['argument-hint'] as string) || '',
|
||||
model: (data.model as string) || '',
|
||||
'user-invocable': data['user-invocable'] !== false,
|
||||
'disable-model-invocation': Boolean(data['disable-model-invocation']),
|
||||
context: (data.context as string) || '',
|
||||
agent: (data.agent as string) || '',
|
||||
author: (data.author as string) || '',
|
||||
'author-email': (data['author-email'] as string) || '',
|
||||
'fork-of': (data['fork-of'] as string) || '',
|
||||
tags: parseTools(data.tags),
|
||||
hooks: (typeof data.hooks === 'object' && data.hooks !== null) ? data.hooks as Record<string, unknown> : null,
|
||||
content: content.trim(),
|
||||
raw,
|
||||
slug: r.slug,
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
'allowed-tools': parseList(f['allowed-tools'] ?? f.allowedTools),
|
||||
'argument-hint': (f['argument-hint'] as string) || '',
|
||||
model: (f.model as string) || '',
|
||||
'user-invocable': f['user-invocable'] !== false,
|
||||
'disable-model-invocation': Boolean(f['disable-model-invocation']),
|
||||
context: (f.context as string) || '',
|
||||
agent: (f.agent as string) || '',
|
||||
author: r.author,
|
||||
'author-email': r['author-email'],
|
||||
'fork-of': r['fork-of'],
|
||||
tags: r.tags,
|
||||
hooks: (typeof f.hooks === 'object' && f.hooks !== null) ? f.hooks as Record<string, unknown> : null,
|
||||
format: r.format,
|
||||
};
|
||||
}
|
||||
|
||||
function toSkill(r: Resource): Skill {
|
||||
return {
|
||||
...toSkillMeta(r),
|
||||
content: r.content,
|
||||
raw: r.raw,
|
||||
files: r.files,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listSkills(): Promise<SkillMeta[]> {
|
||||
await fs.mkdir(SKILLS_DIR, { recursive: true });
|
||||
const files = await fs.readdir(SKILLS_DIR);
|
||||
const skills: SkillMeta[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.md')) continue;
|
||||
const slug = file.replace(/\.md$/, '');
|
||||
const raw = await fs.readFile(path.join(SKILLS_DIR, file), 'utf-8');
|
||||
const { content: _, raw: __, ...meta } = parseSkill(slug, raw);
|
||||
skills.push(meta);
|
||||
}
|
||||
|
||||
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const resources = await listResources('skills');
|
||||
return resources.map(toSkillMeta);
|
||||
}
|
||||
|
||||
export async function getAllTags(): Promise<string[]> {
|
||||
const all = await listSkills();
|
||||
return [...new Set(all.flatMap(s => s.tags))].sort();
|
||||
return resourceGetAllTags('skills');
|
||||
}
|
||||
|
||||
export async function getForksOf(slug: string): Promise<SkillMeta[]> {
|
||||
const all = await listSkills();
|
||||
return all.filter(s => s['fork-of'] === slug);
|
||||
const resources = await resourceGetForksOf('skills', slug);
|
||||
return resources.map(toSkillMeta);
|
||||
}
|
||||
|
||||
export async function getSkill(slug: string): Promise<Skill | null> {
|
||||
try {
|
||||
const raw = await fs.readFile(skillPath(slug), 'utf-8');
|
||||
return parseSkill(slug, raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const r = await getResource('skills', slug);
|
||||
return r ? toSkill(r) : null;
|
||||
}
|
||||
|
||||
export async function createSkill(slug: string, content: string): Promise<Skill> {
|
||||
if (!isValidSlug(slug)) {
|
||||
throw new Error(`Invalid slug: ${slug}`);
|
||||
}
|
||||
|
||||
await fs.mkdir(SKILLS_DIR, { recursive: true });
|
||||
const dest = skillPath(slug);
|
||||
|
||||
try {
|
||||
await fs.access(dest);
|
||||
throw new Error(`Skill already exists: ${slug}`);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.startsWith('Skill already exists')) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(dest, content, 'utf-8');
|
||||
return parseSkill(slug, content);
|
||||
export async function createSkill(slug: string, content: string, format?: ResourceFormat): Promise<Skill> {
|
||||
const r = await createResource('skills', slug, content, format);
|
||||
return toSkill(r);
|
||||
}
|
||||
|
||||
export async function updateSkill(slug: string, content: string): Promise<Skill> {
|
||||
const dest = skillPath(slug);
|
||||
|
||||
try {
|
||||
await fs.access(dest);
|
||||
} catch {
|
||||
throw new Error(`Skill not found: ${slug}`);
|
||||
}
|
||||
|
||||
await fs.writeFile(dest, content, 'utf-8');
|
||||
return parseSkill(slug, content);
|
||||
const r = await updateResource('skills', slug, content);
|
||||
return toSkill(r);
|
||||
}
|
||||
|
||||
export async function deleteSkill(slug: string): Promise<void> {
|
||||
const dest = skillPath(slug);
|
||||
|
||||
try {
|
||||
await fs.access(dest);
|
||||
} catch {
|
||||
throw new Error(`Skill not found: ${slug}`);
|
||||
}
|
||||
|
||||
await fs.unlink(dest);
|
||||
return deleteResource('skills', slug);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { ResourceType } from './registry';
|
||||
|
||||
const STATS_FILE = path.resolve(process.env.STATS_FILE || 'data/stats.json');
|
||||
|
||||
export interface SkillStats {
|
||||
export interface ResourceStats {
|
||||
downloads: number;
|
||||
pushes: number;
|
||||
lastPushedAt: string | null;
|
||||
}
|
||||
|
||||
type StatsStore = Record<string, SkillStats>;
|
||||
/** @deprecated Use ResourceStats */
|
||||
export type SkillStats = ResourceStats;
|
||||
|
||||
type StatsStore = Record<string, ResourceStats>;
|
||||
|
||||
async function readStore(): Promise<StatsStore> {
|
||||
try {
|
||||
@@ -28,32 +32,63 @@ async function writeStore(store: StatsStore): Promise<void> {
|
||||
await fs.rename(tmp, STATS_FILE);
|
||||
}
|
||||
|
||||
function ensure(store: StatsStore, slug: string): SkillStats {
|
||||
if (!store[slug]) {
|
||||
store[slug] = { downloads: 0, pushes: 0, lastPushedAt: null };
|
||||
}
|
||||
return store[slug];
|
||||
/** Build the key used in stats.json. New format: "type:slug". Legacy keys (bare slug) are migrated lazily. */
|
||||
function statsKey(type: ResourceType, slug: string): string {
|
||||
return `${type}:${slug}`;
|
||||
}
|
||||
|
||||
export async function recordDownload(slug: string): Promise<void> {
|
||||
function ensure(store: StatsStore, key: string): ResourceStats {
|
||||
if (!store[key]) {
|
||||
store[key] = { downloads: 0, pushes: 0, lastPushedAt: null };
|
||||
}
|
||||
return store[key];
|
||||
}
|
||||
|
||||
/** Lazy migration: if a bare slug key exists (legacy) and the namespaced key doesn't, migrate it. */
|
||||
function migrateKey(store: StatsStore, type: ResourceType, slug: string): string {
|
||||
const newKey = statsKey(type, slug);
|
||||
if (!store[newKey] && store[slug] && type === 'skills') {
|
||||
store[newKey] = store[slug];
|
||||
delete store[slug];
|
||||
}
|
||||
return newKey;
|
||||
}
|
||||
|
||||
export async function recordDownload(slug: string, type: ResourceType = 'skills'): Promise<void> {
|
||||
const store = await readStore();
|
||||
ensure(store, slug).downloads++;
|
||||
const key = migrateKey(store, type, slug);
|
||||
ensure(store, key).downloads++;
|
||||
await writeStore(store);
|
||||
}
|
||||
|
||||
export async function recordPush(slug: string): Promise<void> {
|
||||
export async function recordPush(slug: string, type: ResourceType = 'skills'): Promise<void> {
|
||||
const store = await readStore();
|
||||
const entry = ensure(store, slug);
|
||||
const key = migrateKey(store, type, slug);
|
||||
const entry = ensure(store, key);
|
||||
entry.pushes++;
|
||||
entry.lastPushedAt = new Date().toISOString();
|
||||
await writeStore(store);
|
||||
}
|
||||
|
||||
export async function getStatsForSlug(slug: string): Promise<SkillStats> {
|
||||
export async function getStatsForSlug(slug: string, type: ResourceType = 'skills'): Promise<ResourceStats> {
|
||||
const store = await readStore();
|
||||
return store[slug] || { downloads: 0, pushes: 0, lastPushedAt: null };
|
||||
const key = statsKey(type, slug);
|
||||
// Check namespaced key first, fall back to legacy bare slug for skills
|
||||
return store[key] || (type === 'skills' ? store[slug] : null) || { downloads: 0, pushes: 0, lastPushedAt: null };
|
||||
}
|
||||
|
||||
export async function getAllStats(): Promise<StatsStore> {
|
||||
return readStore();
|
||||
export async function getAllStats(type?: ResourceType): Promise<Record<string, ResourceStats>> {
|
||||
const store = await readStore();
|
||||
if (!type) return store;
|
||||
const prefix = `${type}:`;
|
||||
const filtered: Record<string, ResourceStats> = {};
|
||||
for (const [key, val] of Object.entries(store)) {
|
||||
if (key.startsWith(prefix)) {
|
||||
filtered[key.slice(prefix.length)] = val;
|
||||
} else if (type === 'skills' && !key.includes(':')) {
|
||||
// Legacy bare slug keys are skills
|
||||
filtered[key] = val;
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import EditGate from '../components/EditGate.vue';
|
||||
import DeleteButton from '../components/DeleteButton.vue';
|
||||
import { getSkill, getForksOf } from '../lib/skills';
|
||||
import { hasToken } from '../lib/tokens';
|
||||
import { recordDownload, getStatsForSlug } from '../lib/stats';
|
||||
import { marked } from 'marked';
|
||||
/**
|
||||
* Backward compatibility: /<slug> redirects to /skills/<slug> for browsers,
|
||||
* serves raw markdown for CLI (curl/wget).
|
||||
*/
|
||||
import { getSkill } from '../lib/skills';
|
||||
import { recordDownload } from '../lib/stats';
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
// Skip if slug matches a known route prefix (type pages handle themselves)
|
||||
const reserved = new Set(['skills', 'agents', 'output-styles', 'rules', 'new', 'api', 'i', 'gi', 'p', 'gp']);
|
||||
if (reserved.has(slug!)) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
const skill = await getSkill(slug!);
|
||||
|
||||
if (!skill) {
|
||||
return Astro.redirect('/');
|
||||
}
|
||||
|
||||
// curl / wget → raw markdown
|
||||
// CLI (curl/wget) → serve raw markdown directly (backward compat)
|
||||
const accept = Astro.request.headers.get('accept') || '';
|
||||
if (!accept.includes('text/html')) {
|
||||
recordDownload(slug!);
|
||||
@@ -23,165 +29,6 @@ if (!accept.includes('text/html')) {
|
||||
});
|
||||
}
|
||||
|
||||
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 origin = Astro.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`,
|
||||
};
|
||||
// Browser → redirect to new URL
|
||||
return Astro.redirect(`/skills/${slug}`, 301);
|
||||
---
|
||||
|
||||
<Base title={`${skill.name} — Skills Here`}>
|
||||
<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" />
|
||||
</svg>
|
||||
Back
|
||||
</a>
|
||||
|
||||
<!-- Header + Install -->
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<div class="rounded-2xl border border-white/[0.06] bg-surface-100 p-6" style="min-width: 0;">
|
||||
<h1 class="text-2xl font-bold tracking-tight text-white mb-1">{skill.name}</h1>
|
||||
{skill.description && <p class="text-gray-500 leading-relaxed mb-3">{skill.description}</p>}
|
||||
{skill['allowed-tools'].length > 0 && (
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{skill['allowed-tools'].map((tool) => (
|
||||
<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>
|
||||
)}
|
||||
{skill.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-1.5 mt-3">
|
||||
{skill.tags.map((tag) => (
|
||||
<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)]">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{skill.author && (
|
||||
<p class="text-xs text-gray-600 mt-3">by {skill.author}</p>
|
||||
)}
|
||||
{skill['fork-of'] && (
|
||||
<p class="text-xs text-gray-600 mt-1">forked from <a href={`/${skill['fork-of']}`} class="text-[var(--color-accent-500)] hover:text-[var(--color-accent-400)] transition-colors">{skill['fork-of']}</a></p>
|
||||
)}
|
||||
{forks.length > 0 && (
|
||||
<details class="mt-3">
|
||||
<summary class="text-xs text-gray-500 cursor-pointer hover:text-gray-300 transition-colors select-none">
|
||||
{forks.length} fork{forks.length !== 1 ? 's' : ''}
|
||||
</summary>
|
||||
<ul class="mt-1.5 space-y-1 pl-3 border-l border-white/[0.06]">
|
||||
{forks.map((f) => (
|
||||
<li>
|
||||
<a href={`/${f.slug}`} class="text-xs text-[var(--color-accent-500)] hover:text-[var(--color-accent-400)] transition-colors">
|
||||
{f.name}
|
||||
{f.author && <span class="text-gray-600"> 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]">
|
||||
<span class="inline-flex items-center gap-1.5 text-xs text-gray-500">
|
||||
<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="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" />
|
||||
</svg>
|
||||
{stats.downloads} download{stats.downloads !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5 text-xs text-gray-500">
|
||||
<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="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" />
|
||||
</svg>
|
||||
{stats.pushes} push{stats.pushes !== 1 ? 'es' : ''}
|
||||
</span>
|
||||
{stats.lastPushedAt && (
|
||||
<span class="text-xs text-gray-600">
|
||||
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;">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-white">Install this skill</h2>
|
||||
<div class="flex rounded-lg border border-white/[0.06] overflow-hidden" id="os-tabs">
|
||||
<button data-os="unix" class="os-tab px-2.5 py-1 text-[11px] font-medium transition-all">macOS / Linux</button>
|
||||
<button data-os="win" class="os-tab px-2.5 py-1 text-[11px] font-medium transition-all">Windows</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 leading-relaxed">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">
|
||||
<code data-cmd="unix" class="flex-1 text-xs font-mono text-gray-500 select-all truncate">{cmds.unix}</code>
|
||||
<code data-cmd="win" class="flex-1 text-xs font-mono text-gray-500 select-all truncate hidden">{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">Copy</button>
|
||||
</div>
|
||||
<details class="group">
|
||||
<summary class="text-xs text-gray-600 cursor-pointer hover:text-gray-400 transition-colors">More options</summary>
|
||||
<div class="mt-3 space-y-3 text-xs text-gray-500">
|
||||
<div>
|
||||
<p class="mb-1.5">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">
|
||||
<code data-cmd="unix" class="flex-1 font-mono text-gray-500 select-all truncate">{cmds.unixGlobal}</code>
|
||||
<code data-cmd="win" class="flex-1 font-mono text-gray-500 select-all truncate hidden">{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">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skill content -->
|
||||
<div class="relative rounded-2xl border border-white/[0.06] bg-surface-100 p-8">
|
||||
<div class="absolute top-4 right-4 flex items-center gap-2">
|
||||
<EditGate slug={slug!} authorEmail={skill['author-email']} authorName={skill.author} authorHasToken={authorHasToken} client:load />
|
||||
<DeleteButton slug={slug!} authorEmail={skill['author-email']} authorName={skill.author} authorHasToken={authorHasToken} client:load />
|
||||
</div>
|
||||
<article class="skill-prose" set:html={html} />
|
||||
</div>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.os-tab { color: var(--color-gray-600); }
|
||||
.os-tab.active { background: rgba(255,255,255,0.06); color: white; }
|
||||
</style>
|
||||
<script>
|
||||
// OS detection + tab switching
|
||||
const isWin = /Win/.test(navigator.platform);
|
||||
function setOS(os: string) {
|
||||
document.querySelectorAll<HTMLElement>('[data-cmd]').forEach(el => {
|
||||
el.classList.toggle('hidden', el.dataset.cmd !== os);
|
||||
});
|
||||
document.querySelectorAll<HTMLElement>('.os-tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.os === os);
|
||||
});
|
||||
}
|
||||
setOS(isWin ? 'win' : 'unix');
|
||||
document.querySelectorAll<HTMLButtonElement>('.os-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => setOS(tab.dataset.os!));
|
||||
});
|
||||
|
||||
// Copy buttons
|
||||
document.querySelectorAll<HTMLButtonElement>('[data-copy]').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const container = btn.parentElement!;
|
||||
const visible = container.querySelector<HTMLElement>('[data-cmd]:not(.hidden)');
|
||||
const code = visible?.textContent?.trim();
|
||||
if (code) {
|
||||
navigator.clipboard.writeText(code);
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => btn.textContent = 'Copy', 1500);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,53 +1,7 @@
|
||||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import SkillEditor from '../../components/SkillEditor.vue';
|
||||
import { getSkill, getAllTags } from '../../lib/skills';
|
||||
import { getAvailableTools } from '../../lib/tools';
|
||||
import { getAvailableModels } from '../../lib/models';
|
||||
|
||||
/**
|
||||
* Backward compatibility: /<slug>/edit → /skills/<slug>/edit
|
||||
*/
|
||||
const { slug } = Astro.params;
|
||||
const skill = await getSkill(slug!);
|
||||
|
||||
if (!skill) {
|
||||
return Astro.redirect('/');
|
||||
}
|
||||
|
||||
const availableTools = await getAvailableTools();
|
||||
const availableModels = await getAvailableModels();
|
||||
const allowedTools = skill['allowed-tools'].join(', ');
|
||||
const hooksJson = skill.hooks ? JSON.stringify(skill.hooks, null, 2) : '';
|
||||
const availableTags = await getAllTags();
|
||||
return Astro.redirect(`/skills/${slug}/edit`, 301);
|
||||
---
|
||||
|
||||
<Base title={`Edit ${skill.name} — Skills Here`}>
|
||||
<a href={`/${slug}`} 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" />
|
||||
</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>
|
||||
<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
|
||||
/>
|
||||
</Base>
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { APIRoute } from 'astro';
|
||||
import { getSkill } from '../../lib/skills';
|
||||
import { isPowerShell } from '../../lib/sync';
|
||||
|
||||
/** Backward compat: /<slug>/gi — installs skill globally to ~/.claude/skills/ */
|
||||
export const GET: APIRoute = async ({ params, url, request }) => {
|
||||
const { slug } = params;
|
||||
const skill = await getSkill(slug!);
|
||||
@@ -17,7 +18,7 @@ export const GET: APIRoute = async ({ params, url, request }) => {
|
||||
'$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")`,
|
||||
`Invoke-WebRequest -Uri "${origin}/skills/${slug}" -OutFile (Join-Path $Dir "${slug}.md")`,
|
||||
`Write-Host "✓ Installed ${skill.name} globally to $Dir\\${slug}.md"`,
|
||||
'',
|
||||
].join('\n')
|
||||
@@ -25,7 +26,7 @@ export const GET: APIRoute = async ({ params, url, request }) => {
|
||||
'#!/usr/bin/env bash',
|
||||
'set -euo pipefail',
|
||||
'mkdir -p ~/.claude/skills',
|
||||
`curl -fsSL "${origin}/${slug}" -o ~/.claude/skills/${slug}.md`,
|
||||
`curl -fsSL "${origin}/skills/${slug}" -o ~/.claude/skills/${slug}.md`,
|
||||
`echo "✓ Installed ${skill.name} globally to ~/.claude/skills/${slug}.md"`,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { APIRoute } from 'astro';
|
||||
import { getSkill } from '../../lib/skills';
|
||||
import { isPowerShell } from '../../lib/sync';
|
||||
|
||||
/** Backward compat: /<slug>/i — installs skill to .claude/skills/ */
|
||||
export const GET: APIRoute = async ({ params, url, request }) => {
|
||||
const { slug } = params;
|
||||
const skill = await getSkill(slug!);
|
||||
@@ -17,7 +18,7 @@ export const GET: APIRoute = async ({ params, url, request }) => {
|
||||
'$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")`,
|
||||
`Invoke-WebRequest -Uri "${origin}/skills/${slug}" -OutFile (Join-Path $Dir "${slug}.md")`,
|
||||
`Write-Host "✓ Installed ${skill.name} to $Dir\\${slug}.md"`,
|
||||
'',
|
||||
].join('\n')
|
||||
@@ -25,7 +26,7 @@ export const GET: APIRoute = async ({ params, url, request }) => {
|
||||
'#!/usr/bin/env bash',
|
||||
'set -euo pipefail',
|
||||
'mkdir -p .claude/skills',
|
||||
`curl -fsSL "${origin}/${slug}" -o ".claude/skills/${slug}.md"`,
|
||||
`curl -fsSL "${origin}/skills/${slug}" -o ".claude/skills/${slug}.md"`,
|
||||
`echo "✓ Installed ${skill.name} to .claude/skills/${slug}.md"`,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
@@ -1,60 +1,8 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import SkillEditor from '../components/SkillEditor.vue';
|
||||
import { getAvailableTools } from '../lib/tools';
|
||||
import { getAvailableModels } from '../lib/models';
|
||||
import { getSkill, getAllTags } from '../lib/skills';
|
||||
|
||||
const availableTools = await getAvailableTools();
|
||||
const availableModels = await getAvailableModels();
|
||||
const availableTags = await getAllTags();
|
||||
|
||||
// Fork support: /new?from=original-slug
|
||||
const fromSlug = Astro.url.searchParams.get('from');
|
||||
let forkSource: Awaited<ReturnType<typeof getSkill>> = null;
|
||||
if (fromSlug) {
|
||||
forkSource = await getSkill(fromSlug);
|
||||
}
|
||||
|
||||
const isFork = Boolean(forkSource);
|
||||
const title = isFork ? `Fork ${forkSource!.name} — Skills Here` : 'New Skill — Skills Here';
|
||||
/**
|
||||
* Backward compatibility: /new → /skills/new
|
||||
* Preserves query params (e.g. ?from=slug for forking)
|
||||
*/
|
||||
const search = Astro.url.search;
|
||||
return Astro.redirect(`/skills/new${search}`, 301);
|
||||
---
|
||||
|
||||
<Base title={title}>
|
||||
<a href={isFork ? `/${fromSlug}` : '/'} 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" />
|
||||
</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 ? (
|
||||
<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>
|
||||
) : (
|
||||
<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 ? (
|
||||
<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
|
||||
/>
|
||||
) : (
|
||||
<SkillEditor mode="create" :availableTools={availableTools} :availableModels={availableModels} availableTags={availableTags.join(',')} client:load />
|
||||
)}
|
||||
</Base>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
@theme {
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, monospace;
|
||||
--color-accent-400: #fb923c;
|
||||
--color-accent-500: #f97316;
|
||||
--color-accent-600: #ea580c;
|
||||
--color-accent-400: #e04540;
|
||||
--color-accent-500: #cc3530;
|
||||
--color-accent-600: #a82a26;
|
||||
--color-surface-50: #0a0a0f;
|
||||
--color-surface-100: #12121a;
|
||||
--color-surface-200: #1a1a25;
|
||||
@@ -19,7 +19,7 @@ body {
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: rgb(249 115 22 / 0.3);
|
||||
background-color: rgb(204 53 48 / 0.3);
|
||||
}
|
||||
|
||||
/* Prose overrides for markdown content */
|
||||
@@ -29,8 +29,8 @@ body {
|
||||
.skill-prose p { margin-bottom: 0.75rem; color: #a3a3a3; line-height: 1.7; }
|
||||
.skill-prose ul, .skill-prose ol { margin-bottom: 0.75rem; padding-left: 1.5rem; color: #a3a3a3; }
|
||||
.skill-prose li { margin-bottom: 0.25rem; line-height: 1.6; }
|
||||
.skill-prose code { background: rgb(255 255 255 / 0.06); padding: 0.15rem 0.4rem; border-radius: 0.25rem; font-size: 0.85em; color: #fb923c; }
|
||||
.skill-prose code { background: rgb(255 255 255 / 0.06); padding: 0.15rem 0.4rem; border-radius: 0.25rem; font-size: 0.85em; color: #e04540; }
|
||||
.skill-prose pre { background: rgb(0 0 0 / 0.4); padding: 1rem; border-radius: 0.5rem; overflow-x: auto; margin-bottom: 1rem; border: 1px solid rgb(255 255 255 / 0.06); }
|
||||
.skill-prose pre code { background: none; padding: 0; color: #d4d4d4; }
|
||||
.skill-prose a { color: #fb923c; text-decoration: underline; text-underline-offset: 2px; }
|
||||
.skill-prose blockquote { border-left: 3px solid #f97316; padding-left: 1rem; color: #737373; font-style: italic; }
|
||||
.skill-prose a { color: #e04540; text-decoration: underline; text-underline-offset: 2px; }
|
||||
.skill-prose blockquote { border-left: 3px solid #cc3530; padding-left: 1rem; color: #737373; font-style: italic; }
|
||||
|
||||
Reference in New Issue
Block a user